@adhdev/daemon-core 0.9.82-rc.49 → 0.9.82-rc.490

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 (473) hide show
  1. package/dist/boot/daemon-lifecycle.d.ts +9 -0
  2. package/dist/boot/process-hardening.d.ts +50 -0
  3. package/dist/build-info.d.ts +37 -0
  4. package/dist/chat/source-machine.d.ts +166 -0
  5. package/dist/chat/source-resolver.d.ts +104 -0
  6. package/dist/chat/subscription-updates.d.ts +1 -0
  7. package/dist/cli-adapter-types.d.ts +68 -2
  8. package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
  9. package/dist/cli-adapters/cli-state-engine.d.ts +220 -0
  10. package/dist/cli-adapters/provider-cli-adapter.d.ts +137 -64
  11. package/dist/cli-adapters/provider-cli-parse.d.ts +5 -0
  12. package/dist/cli-adapters/provider-cli-runtime.d.ts +34 -0
  13. package/dist/cli-adapters/provider-cli-shared.d.ts +74 -0
  14. package/dist/cli-adapters/pty-write-chunking.d.ts +34 -0
  15. package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
  16. package/dist/cli-adapters/resolve-executable.d.ts +69 -0
  17. package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +6 -3
  18. package/dist/cli-adapters/terminal-backends/types.d.ts +10 -2
  19. package/dist/cli-adapters/terminal-screen.d.ts +4 -11
  20. package/dist/commands/chat-commands-debug-bundle.d.ts +14 -0
  21. package/dist/commands/chat-commands-read.d.ts +23 -0
  22. package/dist/commands/chat-commands-scope.d.ts +39 -0
  23. package/dist/commands/chat-commands-shared.d.ts +33 -0
  24. package/dist/commands/chat-commands-write.d.ts +14 -0
  25. package/dist/commands/chat-commands.d.ts +10 -23
  26. package/dist/commands/cli-manager.d.ts +71 -1
  27. package/dist/commands/handler.d.ts +125 -0
  28. package/dist/commands/high-family/index.d.ts +3 -0
  29. package/dist/commands/high-family/mesh-coordinator-launch.d.ts +2 -0
  30. package/dist/commands/high-family/mesh-events.d.ts +2 -0
  31. package/dist/commands/high-family/mesh-status.d.ts +2 -0
  32. package/dist/commands/high-family/types.d.ts +64 -0
  33. package/dist/commands/low-family/coordinator-prompt.d.ts +9 -0
  34. package/dist/commands/low-family/daemon-lifecycle.d.ts +2 -0
  35. package/dist/commands/low-family/diagnostics.d.ts +2 -0
  36. package/dist/commands/low-family/index.d.ts +3 -0
  37. package/dist/commands/low-family/mesh-ledger.d.ts +10 -0
  38. package/dist/commands/low-family/mesh-node-logs.d.ts +2 -0
  39. package/dist/commands/low-family/notification.d.ts +2 -0
  40. package/dist/commands/low-family/refine-config.d.ts +2 -0
  41. package/dist/commands/low-family/session-host.d.ts +2 -0
  42. package/dist/commands/low-family/spec-providerdev.d.ts +11 -0
  43. package/dist/commands/low-family/status-meta.d.ts +2 -0
  44. package/dist/commands/low-family/types.d.ts +33 -0
  45. package/dist/commands/med-family/cli-agent.d.ts +2 -0
  46. package/dist/commands/med-family/fast-forward.d.ts +2 -0
  47. package/dist/commands/med-family/ide.d.ts +10 -0
  48. package/dist/commands/med-family/index.d.ts +3 -0
  49. package/dist/commands/med-family/mesh-crud.d.ts +48 -0
  50. package/dist/commands/med-family/mesh-host-pairing.d.ts +2 -0
  51. package/dist/commands/med-family/mesh-queue.d.ts +2 -0
  52. package/dist/commands/med-family/mesh-restart.d.ts +2 -0
  53. package/dist/commands/med-family/types.d.ts +152 -0
  54. package/dist/commands/mesh-coordinator.d.ts +85 -1
  55. package/dist/commands/router-aggregate-status.d.ts +24 -0
  56. package/dist/commands/router-mesh-session-owner.d.ts +59 -0
  57. package/dist/commands/router-refine.d.ts +151 -0
  58. package/dist/commands/router-worktree-cleanup.d.ts +133 -0
  59. package/dist/commands/router.d.ts +203 -15
  60. package/dist/commands/upgrade-helper.d.ts +41 -1
  61. package/dist/config/chat-history.d.ts +11 -0
  62. package/dist/config/config.d.ts +19 -0
  63. package/dist/config/mesh-config.d.ts +65 -0
  64. package/dist/config/mesh-json-config.d.ts +199 -0
  65. package/dist/config/registry-resolver.d.ts +54 -0
  66. package/dist/config/repo-settings.d.ts +77 -0
  67. package/dist/config/state-store.d.ts +30 -0
  68. package/dist/daemon/dev-server.d.ts +0 -2
  69. package/dist/detection/cli-detector.d.ts +17 -0
  70. package/dist/detection/ide-detector.d.ts +13 -0
  71. package/dist/detection/win32-ide-version.d.ts +37 -0
  72. package/dist/git/change-impact-config.d.ts +159 -0
  73. package/dist/git/git-commands.d.ts +26 -2
  74. package/dist/git/git-diff.d.ts +6 -0
  75. package/dist/git/git-executor.d.ts +11 -0
  76. package/dist/git/git-status.d.ts +80 -0
  77. package/dist/git/git-types.d.ts +2 -50
  78. package/dist/git/git-worktree.d.ts +71 -1
  79. package/dist/git/index.d.ts +3 -1
  80. package/dist/index.d.ts +70 -17
  81. package/dist/index.js +53532 -19073
  82. package/dist/index.js.map +1 -1
  83. package/dist/index.mjs +53283 -18989
  84. package/dist/index.mjs.map +1 -1
  85. package/dist/installer.d.ts +1 -4
  86. package/dist/ipc/local-ipc-server.d.ts +91 -0
  87. package/dist/launch.d.ts +1 -1
  88. package/dist/logging/async-batch-writer.d.ts +10 -0
  89. package/dist/logging/debug-config.d.ts +16 -0
  90. package/dist/logging/log-redactor.d.ts +24 -0
  91. package/dist/logging/log-tail-reader.d.ts +81 -0
  92. package/dist/logging/logger.d.ts +1 -1
  93. package/dist/mesh/contracts.d.ts +256 -0
  94. package/dist/mesh/coordinator-prompt.d.ts +76 -0
  95. package/dist/mesh/coordinator-registry.d.ts +59 -0
  96. package/dist/mesh/mesh-active-work.d.ts +203 -0
  97. package/dist/mesh/mesh-clone-grace.d.ts +8 -0
  98. package/dist/mesh/mesh-completion-synthesis.d.ts +4 -0
  99. package/dist/mesh/mesh-coordinator-config.d.ts +21 -0
  100. package/dist/mesh/mesh-delivery-policy.d.ts +104 -0
  101. package/dist/mesh/mesh-event-classify.d.ts +7 -0
  102. package/dist/mesh/mesh-event-forwarding.d.ts +58 -0
  103. package/dist/mesh/mesh-event-trace.d.ts +21 -0
  104. package/dist/mesh/mesh-events-coordinator.d.ts +4 -0
  105. package/dist/mesh/mesh-events-pending.d.ts +258 -0
  106. package/dist/mesh/mesh-events-stale.d.ts +61 -0
  107. package/dist/mesh/mesh-events-utils.d.ts +75 -0
  108. package/dist/mesh/mesh-events.d.ts +6 -49
  109. package/dist/mesh/mesh-fast-forward.d.ts +81 -0
  110. package/dist/mesh/mesh-host-ownership.d.ts +21 -1
  111. package/dist/mesh/mesh-init.d.ts +128 -0
  112. package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -4
  113. package/dist/mesh/mesh-ledger.d.ts +139 -1
  114. package/dist/mesh/mesh-magi-status.d.ts +63 -0
  115. package/dist/mesh/mesh-missions.d.ts +281 -0
  116. package/dist/mesh/mesh-node-identity.d.ts +310 -0
  117. package/dist/mesh/mesh-queue-assignment.d.ts +162 -0
  118. package/dist/mesh/mesh-reconcile-acked-hold.d.ts +17 -0
  119. package/dist/mesh/mesh-reconcile-config.d.ts +6 -0
  120. package/dist/mesh/mesh-reconcile-identity.d.ts +6 -0
  121. package/dist/mesh/mesh-reconcile-loop.d.ts +84 -0
  122. package/dist/mesh/mesh-reconcile-v2-backstop.d.ts +16 -0
  123. package/dist/mesh/mesh-refine-batch.d.ts +68 -0
  124. package/dist/mesh/mesh-refine-gates.d.ts +457 -0
  125. package/dist/mesh/mesh-refine-status.d.ts +62 -0
  126. package/dist/mesh/mesh-remote-event-pull.d.ts +15 -0
  127. package/dist/mesh/mesh-review-inbox.d.ts +90 -0
  128. package/dist/mesh/mesh-routing.d.ts +70 -0
  129. package/dist/mesh/mesh-runtime-store.d.ts +567 -0
  130. package/dist/mesh/mesh-scheduling-runtime.d.ts +78 -0
  131. package/dist/mesh/mesh-task-inflight.d.ts +46 -0
  132. package/dist/mesh/mesh-task-stats.d.ts +49 -0
  133. package/dist/mesh/mesh-unresolved-forward-outbox.d.ts +34 -0
  134. package/dist/mesh/mesh-warmup-deadline.d.ts +68 -0
  135. package/dist/mesh/mesh-work-queue.d.ts +392 -1
  136. package/dist/mesh/preview-freshness.d.ts +28 -0
  137. package/dist/mesh/refine-config.d.ts +97 -0
  138. package/dist/mesh/worktree-bootstrap-config.d.ts +219 -0
  139. package/dist/providers/acp-provider-instance.d.ts +5 -0
  140. package/dist/providers/approval-utils.d.ts +46 -0
  141. package/dist/providers/chat-message-normalization.d.ts +58 -1
  142. package/dist/providers/cli-provider-effect-format.d.ts +30 -0
  143. package/dist/providers/cli-provider-history-dedup.d.ts +17 -0
  144. package/dist/providers/cli-provider-input-prompt.d.ts +12 -0
  145. package/dist/providers/cli-provider-instance-types.d.ts +45 -0
  146. package/dist/providers/cli-provider-instance.d.ts +426 -30
  147. package/dist/providers/cli-provider-status-helpers.d.ts +46 -0
  148. package/dist/providers/cli-provider-transcript-merge.d.ts +7 -0
  149. package/dist/providers/contracts.d.ts +196 -6
  150. package/dist/providers/external-sources.d.ts +71 -0
  151. package/dist/providers/manual-attendance.d.ts +79 -0
  152. package/dist/providers/native-history/antigravity-claim-registry.d.ts +39 -0
  153. package/dist/providers/native-history/antigravity-cli-transcript.d.ts +137 -0
  154. package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
  155. package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
  156. package/dist/providers/native-history/constants.d.ts +12 -0
  157. package/dist/providers/native-history/dispatcher.d.ts +44 -0
  158. package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
  159. package/dist/providers/native-history/index.d.ts +13 -0
  160. package/dist/providers/provider-instance-manager.d.ts +30 -0
  161. package/dist/providers/provider-instance.d.ts +31 -1
  162. package/dist/providers/provider-loader.d.ts +42 -5
  163. package/dist/providers/provider-trust.d.ts +31 -0
  164. package/dist/providers/read-chat-contract.d.ts +29 -0
  165. package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
  166. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +86 -0
  167. package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
  168. package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
  169. package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
  170. package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
  171. package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
  172. package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
  173. package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
  174. package/dist/providers/sdk/v1/index.d.ts +30 -0
  175. package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
  176. package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
  177. package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
  178. package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
  179. package/dist/providers/sdk/v1/types/common/index.d.ts +203 -0
  180. package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
  181. package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
  182. package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
  183. package/dist/providers/spec/adapter.d.ts +98 -0
  184. package/dist/providers/spec/cli-adapter.d.ts +223 -0
  185. package/dist/providers/spec/evaluator.d.ts +45 -0
  186. package/dist/providers/spec/fsm-driver.d.ts +449 -0
  187. package/dist/providers/spec/fsm-evaluator.d.ts +93 -0
  188. package/dist/providers/spec/fsm-loader.d.ts +14 -0
  189. package/dist/providers/spec/fsm-types.d.ts +202 -0
  190. package/dist/providers/spec/native-history-executor.d.ts +41 -0
  191. package/dist/providers/spec/pre-launch-trust.d.ts +16 -0
  192. package/dist/providers/spec/route.d.ts +4 -0
  193. package/dist/providers/spec/types.d.ts +244 -0
  194. package/dist/providers/status-monitor.d.ts +7 -7
  195. package/dist/providers/transcript-v2.d.ts +176 -0
  196. package/dist/providers/types/interactive-prompt.d.ts +84 -0
  197. package/dist/providers/working-dir.d.ts +17 -0
  198. package/dist/repo-mesh-types.d.ts +616 -12
  199. package/dist/runtime-defaults.d.ts +2 -0
  200. package/dist/session-host/managed-host.d.ts +64 -0
  201. package/dist/session-host/runtime-surface.d.ts +10 -16
  202. package/dist/sessions/registry.d.ts +26 -0
  203. package/dist/shared-types-extra.d.ts +2 -4
  204. package/dist/shared-types.d.ts +110 -55
  205. package/dist/status/chat-tail-hot-sessions.d.ts +40 -0
  206. package/dist/status/normalize.d.ts +1 -1
  207. package/dist/status/normalize.js +1 -0
  208. package/dist/status/normalize.js.map +1 -1
  209. package/dist/status/normalize.mjs +1 -0
  210. package/dist/status/normalize.mjs.map +1 -1
  211. package/dist/status/reporter.d.ts +2 -0
  212. package/dist/status/snapshot.d.ts +27 -0
  213. package/dist/system/hash.d.ts +8 -0
  214. package/dist/system/load-better-sqlite3.d.ts +21 -0
  215. package/dist/types.d.ts +20 -6
  216. package/package.json +11 -4
  217. package/src/agent-stream/poller.ts +2 -3
  218. package/src/agent-stream/provider-adapter.ts +1 -1
  219. package/src/boot/daemon-lifecycle.ts +80 -14
  220. package/src/boot/process-hardening.ts +89 -0
  221. package/src/build-info.ts +73 -0
  222. package/src/chat/source-machine.ts +534 -0
  223. package/src/chat/source-resolver.ts +0 -0
  224. package/src/chat/subscription-updates.ts +20 -1
  225. package/src/cli-adapter-types.d.ts +3 -1
  226. package/src/cli-adapter-types.ts +68 -2
  227. package/src/cli-adapters/cli-script-runner.ts +421 -0
  228. package/src/cli-adapters/cli-state-engine.ts +1282 -0
  229. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  230. package/src/cli-adapters/provider-cli-adapter.ts +1048 -1143
  231. package/src/cli-adapters/provider-cli-parse.d.ts +2 -0
  232. package/src/cli-adapters/provider-cli-parse.ts +19 -0
  233. package/src/cli-adapters/provider-cli-runtime.ts +56 -7
  234. package/src/cli-adapters/provider-cli-shared.d.ts +4 -0
  235. package/src/cli-adapters/provider-cli-shared.ts +121 -11
  236. package/src/cli-adapters/pty-transport.ts +2 -1
  237. package/src/cli-adapters/pty-write-chunking.ts +106 -0
  238. package/src/cli-adapters/raw-terminal-io.ts +252 -0
  239. package/src/cli-adapters/resolve-executable.ts +204 -0
  240. package/src/cli-adapters/session-host-transport.ts +2 -1
  241. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +63 -29
  242. package/src/cli-adapters/terminal-backends/types.ts +10 -3
  243. package/src/cli-adapters/terminal-screen.ts +16 -81
  244. package/src/commands/chat-commands-debug-bundle.ts +398 -0
  245. package/src/commands/chat-commands-read.ts +2788 -0
  246. package/src/commands/chat-commands-scope.ts +54 -0
  247. package/src/commands/chat-commands-shared.ts +114 -0
  248. package/src/commands/chat-commands-write.ts +891 -0
  249. package/src/commands/chat-commands.ts +19 -1841
  250. package/src/commands/cli-manager.d.ts +2 -0
  251. package/src/commands/cli-manager.ts +724 -27
  252. package/src/commands/handler.ts +865 -2
  253. package/src/commands/high-family/index.ts +28 -0
  254. package/src/commands/high-family/mesh-coordinator-launch.ts +696 -0
  255. package/src/commands/high-family/mesh-events.ts +102 -0
  256. package/src/commands/high-family/mesh-status.ts +857 -0
  257. package/src/commands/high-family/types.ts +80 -0
  258. package/src/commands/low-family/coordinator-prompt.ts +125 -0
  259. package/src/commands/low-family/daemon-lifecycle.ts +120 -0
  260. package/src/commands/low-family/diagnostics.ts +57 -0
  261. package/src/commands/low-family/index.ts +37 -0
  262. package/src/commands/low-family/mesh-ledger.ts +62 -0
  263. package/src/commands/low-family/mesh-node-logs.ts +87 -0
  264. package/src/commands/low-family/notification.ts +116 -0
  265. package/src/commands/low-family/refine-config.ts +106 -0
  266. package/src/commands/low-family/session-host.ts +282 -0
  267. package/src/commands/low-family/spec-providerdev.ts +217 -0
  268. package/src/commands/low-family/status-meta.ts +112 -0
  269. package/src/commands/low-family/types.ts +39 -0
  270. package/src/commands/med-family/cli-agent.ts +270 -0
  271. package/src/commands/med-family/fast-forward.ts +230 -0
  272. package/src/commands/med-family/ide.ts +163 -0
  273. package/src/commands/med-family/index.ts +37 -0
  274. package/src/commands/med-family/mesh-crud.ts +1268 -0
  275. package/src/commands/med-family/mesh-host-pairing.ts +234 -0
  276. package/src/commands/med-family/mesh-queue.ts +167 -0
  277. package/src/commands/med-family/mesh-restart.ts +92 -0
  278. package/src/commands/med-family/types.ts +153 -0
  279. package/src/commands/mesh-coordinator.ts +334 -124
  280. package/src/commands/router-aggregate-status.ts +209 -0
  281. package/src/commands/router-mesh-session-owner.ts +114 -0
  282. package/src/commands/router-refine.ts +1794 -0
  283. package/src/commands/router-worktree-cleanup.ts +870 -0
  284. package/src/commands/router.ts +567 -4149
  285. package/src/commands/stream-commands.ts +8 -0
  286. package/src/commands/upgrade-helper.ts +310 -45
  287. package/src/config/chat-history.ts +517 -24
  288. package/src/config/config.ts +30 -0
  289. package/src/config/mesh-config.ts +300 -24
  290. package/src/config/mesh-json-config.ts +376 -0
  291. package/src/config/recent-activity.ts +8 -2
  292. package/src/config/registry-resolver.ts +100 -0
  293. package/src/config/repo-settings.ts +111 -0
  294. package/src/config/state-store.ts +55 -0
  295. package/src/daemon/dev-auto-implement.ts +3 -2
  296. package/src/daemon/dev-cli-debug.ts +10 -1
  297. package/src/daemon/dev-server.ts +0 -541
  298. package/src/detection/cli-detector.ts +94 -9
  299. package/src/detection/ide-detector.ts +55 -16
  300. package/src/detection/win32-ide-version.ts +106 -0
  301. package/src/git/change-impact-config.ts +354 -0
  302. package/src/git/git-commands.ts +112 -16
  303. package/src/git/git-diff.ts +81 -11
  304. package/src/git/git-executor.ts +12 -0
  305. package/src/git/git-status.ts +822 -48
  306. package/src/git/git-types.ts +14 -62
  307. package/src/git/git-worktree.ts +261 -4
  308. package/src/git/index.ts +17 -0
  309. package/src/index.ts +190 -13
  310. package/src/installer.d.ts +1 -1
  311. package/src/installer.ts +8 -6
  312. package/src/ipc/local-ipc-server.ts +278 -0
  313. package/src/launch.d.ts +1 -1
  314. package/src/launch.ts +37 -28
  315. package/src/logging/async-batch-writer.ts +55 -0
  316. package/src/logging/command-log.ts +7 -5
  317. package/src/logging/debug-config.ts +25 -0
  318. package/src/logging/debug-trace.ts +7 -2
  319. package/src/logging/log-redactor.ts +100 -0
  320. package/src/logging/log-tail-reader.ts +341 -0
  321. package/src/logging/logger.ts +14 -7
  322. package/src/mesh/contracts.ts +510 -0
  323. package/src/mesh/coordinator-prompt.ts +627 -34
  324. package/src/mesh/coordinator-registry.ts +121 -0
  325. package/src/mesh/mesh-active-work.ts +706 -0
  326. package/src/mesh/mesh-clone-grace.ts +68 -0
  327. package/src/mesh/mesh-completion-synthesis.ts +416 -0
  328. package/src/mesh/mesh-coordinator-config.ts +97 -0
  329. package/src/mesh/mesh-delivery-policy.ts +284 -0
  330. package/src/mesh/mesh-event-classify.ts +78 -0
  331. package/src/mesh/mesh-event-forwarding.ts +2030 -0
  332. package/src/mesh/mesh-event-trace.ts +67 -0
  333. package/src/mesh/mesh-events-coordinator.ts +32 -0
  334. package/src/mesh/mesh-events-pending.ts +1548 -0
  335. package/src/mesh/mesh-events-stale.ts +452 -0
  336. package/src/mesh/mesh-events-utils.ts +443 -0
  337. package/src/mesh/mesh-events.ts +43 -1035
  338. package/src/mesh/mesh-fast-forward.ts +856 -0
  339. package/src/mesh/mesh-host-ownership.ts +41 -3
  340. package/src/mesh/mesh-init.ts +350 -0
  341. package/src/mesh/mesh-ledger-reconciliation.ts +12 -7
  342. package/src/mesh/mesh-ledger.ts +897 -102
  343. package/src/mesh/mesh-magi-status.ts +223 -0
  344. package/src/mesh/mesh-missions.ts +695 -0
  345. package/src/mesh/mesh-node-identity.ts +2065 -0
  346. package/src/mesh/mesh-queue-assignment.ts +2498 -0
  347. package/src/mesh/mesh-reconcile-acked-hold.ts +230 -0
  348. package/src/mesh/mesh-reconcile-config.ts +66 -0
  349. package/src/mesh/mesh-reconcile-identity.ts +103 -0
  350. package/src/mesh/mesh-reconcile-loop.ts +1696 -0
  351. package/src/mesh/mesh-reconcile-v2-backstop.ts +62 -0
  352. package/src/mesh/mesh-refine-batch.ts +205 -0
  353. package/src/mesh/mesh-refine-gates.ts +1792 -0
  354. package/src/mesh/mesh-refine-status.ts +231 -0
  355. package/src/mesh/mesh-remote-event-pull.ts +279 -0
  356. package/src/mesh/mesh-review-inbox.ts +308 -0
  357. package/src/mesh/mesh-routing.ts +291 -0
  358. package/src/mesh/mesh-runtime-store.ts +2292 -0
  359. package/src/mesh/mesh-scheduling-runtime.ts +198 -0
  360. package/src/mesh/mesh-task-inflight.ts +70 -0
  361. package/src/mesh/mesh-task-stats.ts +161 -0
  362. package/src/mesh/mesh-unresolved-forward-outbox.ts +215 -0
  363. package/src/mesh/mesh-warmup-deadline.ts +152 -0
  364. package/src/mesh/mesh-work-queue.ts +1489 -145
  365. package/src/mesh/preview-freshness.ts +163 -0
  366. package/src/mesh/refine-config.ts +129 -12
  367. package/src/mesh/worktree-bootstrap-config.ts +599 -0
  368. package/src/providers/acp-provider-instance.ts +43 -10
  369. package/src/providers/approval-utils.d.ts +5 -0
  370. package/src/providers/approval-utils.ts +100 -6
  371. package/src/providers/chat-message-normalization.ts +126 -4
  372. package/src/providers/cli-provider-effect-format.ts +53 -0
  373. package/src/providers/cli-provider-history-dedup.ts +75 -0
  374. package/src/providers/cli-provider-input-prompt.ts +133 -0
  375. package/src/providers/cli-provider-instance-types.ts +131 -0
  376. package/src/providers/cli-provider-instance.ts +2771 -499
  377. package/src/providers/cli-provider-status-helpers.ts +123 -0
  378. package/src/providers/cli-provider-transcript-merge.ts +114 -0
  379. package/src/providers/contracts.d.ts +55 -0
  380. package/src/providers/contracts.ts +207 -6
  381. package/src/providers/extension-provider-instance.ts +12 -7
  382. package/src/providers/external-sources.ts +218 -0
  383. package/src/providers/ide-provider-instance.ts +35 -12
  384. package/src/providers/manual-attendance.ts +105 -0
  385. package/src/providers/native-history/antigravity-claim-registry.ts +138 -0
  386. package/src/providers/native-history/antigravity-cli-transcript.ts +1275 -0
  387. package/src/providers/native-history/claude-cli-transcript.ts +396 -0
  388. package/src/providers/native-history/codex-cli-transcript.ts +479 -0
  389. package/src/providers/native-history/constants.ts +19 -0
  390. package/src/providers/native-history/dispatcher.ts +617 -0
  391. package/src/providers/native-history/hermes-cli-transcript.ts +312 -0
  392. package/src/providers/native-history/index.ts +30 -0
  393. package/src/providers/provider-instance-manager.ts +71 -0
  394. package/src/providers/provider-instance.ts +24 -1
  395. package/src/providers/provider-loader.ts +700 -55
  396. package/src/providers/provider-schema.ts +93 -14
  397. package/src/providers/provider-trust.ts +114 -0
  398. package/src/providers/read-chat-contract.ts +76 -16
  399. package/src/providers/sdk/README.md +49 -0
  400. package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
  401. package/src/providers/sdk/v1/builders/cli/detect-status.ts +313 -0
  402. package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
  403. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +261 -0
  404. package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
  405. package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
  406. package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
  407. package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
  408. package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
  409. package/src/providers/sdk/v1/index.ts +152 -0
  410. package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
  411. package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
  412. package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
  413. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +538 -0
  414. package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
  415. package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
  416. package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
  417. package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
  418. package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
  419. package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
  420. package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
  421. package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
  422. package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
  423. package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
  424. package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
  425. package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
  426. package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
  427. package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
  428. package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
  429. package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
  430. package/src/providers/sdk/v1/types/cli/index.ts +365 -0
  431. package/src/providers/sdk/v1/types/common/index.ts +229 -0
  432. package/src/providers/sdk/v1/validators/index.ts +19 -0
  433. package/src/providers/sdk/v1/validators/manifest.ts +110 -0
  434. package/src/providers/sdk/v1/validators/taint.ts +309 -0
  435. package/src/providers/spec/adapter.ts +246 -0
  436. package/src/providers/spec/cli-adapter.ts +1064 -0
  437. package/src/providers/spec/evaluator.ts +407 -0
  438. package/src/providers/spec/fsm-driver.ts +1528 -0
  439. package/src/providers/spec/fsm-evaluator.ts +290 -0
  440. package/src/providers/spec/fsm-loader.ts +128 -0
  441. package/src/providers/spec/fsm-types.ts +301 -0
  442. package/src/providers/spec/native-history-executor.ts +1241 -0
  443. package/src/providers/spec/pre-launch-trust.ts +104 -0
  444. package/src/providers/spec/route.ts +51 -0
  445. package/src/providers/spec/types.ts +284 -0
  446. package/src/providers/status-monitor.d.ts +7 -7
  447. package/src/providers/status-monitor.ts +37 -22
  448. package/src/providers/transcript-v2.ts +567 -0
  449. package/src/providers/types/interactive-prompt.ts +536 -0
  450. package/src/providers/version-archive.ts +64 -24
  451. package/src/providers/working-dir.ts +23 -0
  452. package/src/repo-mesh-types.ts +861 -14
  453. package/src/runtime-defaults.ts +39 -0
  454. package/src/session-host/managed-host.ts +218 -0
  455. package/src/session-host/runtime-surface.ts +20 -80
  456. package/src/sessions/registry.ts +44 -0
  457. package/src/shared-types-extra.ts +2 -4
  458. package/src/shared-types.d.ts +8 -0
  459. package/src/shared-types.ts +148 -55
  460. package/src/status/builders.ts +69 -6
  461. package/src/status/chat-tail-hot-sessions.ts +117 -2
  462. package/src/status/normalize.ts +2 -0
  463. package/src/status/reporter.ts +19 -1
  464. package/src/status/snapshot.ts +100 -27
  465. package/src/system/hash.ts +23 -0
  466. package/src/system/host-memory.ts +29 -12
  467. package/src/system/load-better-sqlite3.ts +68 -0
  468. package/src/types.ts +26 -6
  469. package/dist/cli-adapters/terminal-backends/xterm-backend.d.ts +0 -17
  470. package/dist/mesh/mesh-sync.d.ts +0 -53
  471. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +0 -16
  472. package/src/cli-adapters/terminal-backends/xterm-backend.ts +0 -97
  473. package/src/mesh/mesh-sync.ts +0 -111
@@ -0,0 +1,1794 @@
1
+ /**
2
+ * Refinery job orchestration — extracted from router.ts (behavior-preserving code move).
3
+ *
4
+ * These functions were `DaemonCommandRouter` methods; they now take the router
5
+ * instance as `self`. The class keeps thin delegating wrappers for the entry
6
+ * points referenced elsewhere (startMeshRefineJob / batchRefineMeshNodes /
7
+ * startMeshRefineBatchJob bound into MedFamilyContext; resumePendingRefineJobsOnStartup
8
+ * called by the daemon boot lifecycle). No stage order, event, log string, error
9
+ * message, or result shape was changed — only physical location + `this.` → `self.`.
10
+ */
11
+ import { execFileSync } from 'node:child_process';
12
+ import type { DaemonCommandRouter, CommandRouterResult } from './router.js';
13
+ import { LOG } from '../logging/logger.js';
14
+ import { createInteractionId } from '../logging/debug-trace.js';
15
+ import { meshNodeIdMatches } from '@adhdev/mesh-shared';
16
+ import { handleMeshForwardEvent, queuePendingMeshCoordinatorEvent } from '../mesh/mesh-events.js';
17
+ import { analyzeMeshRefineNodeChangeArea, orderMeshRefineBatchNodes } from '../mesh/mesh-refine-batch.js';
18
+ import type { WorktreeBootstrapState } from '../mesh/worktree-bootstrap-config.js';
19
+ import { DEFAULT_MESH_POLICY } from '../repo-mesh-types.js';
20
+ import { classifyChangedPackages } from '../git/git-status.js';
21
+ import type { ChangedPackageClassification } from '../git/git-status.js';
22
+ import { readStringValue } from '../mesh/mesh-node-identity.js';
23
+ import {
24
+ alignRefinerySubmodulesAfterMerge,
25
+ buildMeshRefineValidationPlan,
26
+ buildSubmodulePublishRequiredNextStep,
27
+ MeshRefineAsyncJobStatus,
28
+ MeshRefineBatchJobHandle,
29
+ MeshRefineBatchJobStatus,
30
+ MeshRefineBatchTerminalJob,
31
+ MeshRefineJobHandle,
32
+ MeshRefineTerminalJob,
33
+ RefineContext,
34
+ RefineExecFileAsync,
35
+ RefineStageOutcome,
36
+ recordMeshRefineStage,
37
+ resolveRefineryAutoPublishSubmoduleMainCommits,
38
+ runMeshRefineEffectiveDiffGate,
39
+ runMeshRefinePatchEquivalenceGate,
40
+ runMeshRefineSubmoduleReachabilityGate,
41
+ runMeshRefineValidationGate,
42
+ truncateValidationOutput,
43
+ } from '../mesh/mesh-refine-gates.js';
44
+
45
+ export function buildRefineJobKey(self: DaemonCommandRouter, meshId: string, nodeId: string): string {
46
+ return `${meshId}:${nodeId}`;
47
+ }
48
+
49
+ export function buildRefineJobHandle(self: DaemonCommandRouter, args: {
50
+ meshId: string;
51
+ nodeId: string;
52
+ node?: any;
53
+ status?: MeshRefineAsyncJobStatus;
54
+ startedAt?: string;
55
+ completedAt?: string;
56
+ jobId?: string;
57
+ interactionId?: string;
58
+ retryOfJobId?: string;
59
+ coordinatorDaemonId?: string;
60
+ }): MeshRefineJobHandle {
61
+ return {
62
+ success: true,
63
+ async: true,
64
+ status: args.status || 'accepted',
65
+ jobId: args.jobId || `refine_${createInteractionId()}`,
66
+ interactionId: args.interactionId || createInteractionId(),
67
+ meshId: args.meshId,
68
+ nodeId: args.nodeId,
69
+ targetNodeId: args.nodeId,
70
+ targetDaemonId: readStringValue(args.node?.daemonId),
71
+ workspace: readStringValue(args.node?.workspace),
72
+ startedAt: args.startedAt || new Date().toISOString(),
73
+ ...(args.completedAt ? { completedAt: args.completedAt } : {}),
74
+ ...(args.retryOfJobId ? { retryOfJobId: args.retryOfJobId } : {}),
75
+ ...(args.coordinatorDaemonId ? { targetCoordinatorDaemonId: args.coordinatorDaemonId } : {}),
76
+ eventDelivery: { pendingEvents: true, ledger: true },
77
+ evidence: {
78
+ pendingEventsCommand: 'get_pending_mesh_events',
79
+ ledgerCommand: 'get_mesh_ledger_slice',
80
+ taskHistoryKind: args.status === 'completed' ? 'task_completed' : args.status === 'failed' ? 'task_failed' : 'task_dispatched',
81
+ },
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Slim the terminal-stage refine result down to the fields a coordinator needs to
87
+ * decide next-step, dropping the heavy per-command / per-entry detail.
88
+ *
89
+ * The full `CommandRouterResult` (with `validationSummary.commandsRun[]` carrying
90
+ * per-command stdout/stderr, `rejectedCommands`, `suggestions`, `suggestedConfig`,
91
+ * the full `patchEquivalence`, and `submoduleReachability.entries[]`/`.unreachable[]`)
92
+ * routinely exceeds 70KB and overflows the coordinator token limit when it rides on a
93
+ * `mesh_wait_events` payload. The full detail is still persisted verbatim to the ledger
94
+ * (`appendRefineJobLedger`) and `terminalRefineJobs`, so slimming only the EVENT loses
95
+ * nothing — the coordinator can pull the full record on demand via
96
+ * `evidence.ledgerCommand` / `taskHistoryKind`.
97
+ */
98
+ export function slimRefineEventResult(result: Record<string, unknown>): Record<string, unknown> {
99
+ const slim: Record<string, unknown> = {};
100
+ // Top-level scalars the coordinator branches on.
101
+ for (const key of [
102
+ 'success', 'code', 'error', 'convergenceStatus', 'blockedReason',
103
+ 'branch', 'into', 'terminalKind', 'nextStep', 'finalBranchConvergenceState',
104
+ ] as const) {
105
+ if (result[key] !== undefined) slim[key] = result[key];
106
+ }
107
+ // Mapped subset of the unreachable-submodule commits (path + autoPublishAllowed),
108
+ // not the full commit records.
109
+ if (Array.isArray(result.unreachableSubmoduleCommits)) {
110
+ slim.unreachableSubmoduleCommits = (result.unreachableSubmoduleCommits as Array<Record<string, unknown>>)
111
+ .map(e => ({ path: e?.path, autoPublishAllowed: e?.autoPublishAllowed }));
112
+ }
113
+ // Reduced validation summary — status + failure classification + config source
114
+ // + a count of commands run (drop the full commandsRun/rejectedCommands/
115
+ // suggestions/suggestedConfig detail).
116
+ if (result.validationSummary && typeof result.validationSummary === 'object') {
117
+ const vs = result.validationSummary as Record<string, unknown>;
118
+ slim.validationSummary = {
119
+ status: vs.status,
120
+ failureCode: vs.failureCode,
121
+ configSource: vs.configSource,
122
+ configSourceType: vs.configSourceType,
123
+ commandsRunCount: Array.isArray(vs.commandsRun) ? vs.commandsRun.length : undefined,
124
+ };
125
+ }
126
+ // Reduce patch-equivalence to just its verdict.
127
+ if (result.patchEquivalence && typeof result.patchEquivalence === 'object') {
128
+ const pe = result.patchEquivalence as Record<string, unknown>;
129
+ slim.patchEquivalence = { status: pe.status, equivalent: pe.equivalent };
130
+ }
131
+ // Reduce submodule reachability to counts; drop the full entries/unreachable arrays.
132
+ if (result.submoduleReachability && typeof result.submoduleReachability === 'object') {
133
+ const sr = result.submoduleReachability as Record<string, unknown>;
134
+ slim.submoduleReachability = {
135
+ checked: Array.isArray(sr.entries) ? sr.entries.length : undefined,
136
+ unreachable: Array.isArray(sr.unreachable) ? sr.unreachable.length : undefined,
137
+ };
138
+ }
139
+ return slim;
140
+ }
141
+
142
+ export function queueRefineJobEvent(self: DaemonCommandRouter, event: 'refine:accepted' | 'refine:completed' | 'refine:failed', handle: MeshRefineJobHandle, result?: Record<string, unknown>): void {
143
+ const slimResult = result ? slimRefineEventResult(result) : undefined;
144
+ const metadataEvent = {
145
+ source: 'refine_mesh_node_async_job',
146
+ jobId: handle.jobId,
147
+ interactionId: handle.interactionId,
148
+ meshId: handle.meshId,
149
+ nodeId: handle.targetNodeId,
150
+ targetDaemonId: handle.targetDaemonId,
151
+ workspace: handle.workspace,
152
+ status: handle.status,
153
+ startedAt: handle.startedAt,
154
+ completedAt: handle.completedAt,
155
+ retryOfJobId: handle.retryOfJobId,
156
+ ...(slimResult ? { result: slimResult } : {}),
157
+ };
158
+ const eventPayload = {
159
+ event,
160
+ meshId: handle.meshId,
161
+ nodeLabel: handle.targetNodeId,
162
+ nodeId: handle.targetNodeId,
163
+ workspace: handle.workspace,
164
+ metadataEvent,
165
+ queuedAt: Date.now(),
166
+ ...(handle.targetCoordinatorDaemonId ? { targetCoordinatorDaemonId: handle.targetCoordinatorDaemonId } : {}),
167
+ };
168
+ if (typeof self.deps.instanceManager?.getByCategory === 'function') {
169
+ const forwarded = handleMeshForwardEvent(
170
+ { instanceManager: self.deps.instanceManager } as any,
171
+ {
172
+ event,
173
+ meshId: handle.meshId,
174
+ nodeId: handle.targetNodeId,
175
+ workspace: handle.workspace,
176
+ jobId: handle.jobId,
177
+ interactionId: handle.interactionId,
178
+ status: handle.status,
179
+ targetDaemonId: handle.targetDaemonId,
180
+ startedAt: handle.startedAt,
181
+ completedAt: handle.completedAt,
182
+ retryOfJobId: handle.retryOfJobId,
183
+ ...(slimResult ? { result: slimResult } : {}),
184
+ },
185
+ );
186
+ if (forwarded?.success === true) return;
187
+ LOG.warn('Mesh', `[Refinery] Failed to forward async refine event ${event}: ${forwarded?.error || 'unknown error'}`);
188
+ }
189
+ queuePendingMeshCoordinatorEvent(eventPayload);
190
+ }
191
+
192
+ export async function appendRefineJobLedger(self: DaemonCommandRouter, kind: 'task_dispatched' | 'task_completed' | 'task_failed', handle: MeshRefineJobHandle, result?: Record<string, unknown>): Promise<void> {
193
+ try {
194
+ const { appendLedgerEntry, buildLedgerOriginatingCoordinatorStamp } = await import('../mesh/mesh-ledger.js');
195
+ // B2a: on dispatch, stamp the originating coordinator so a later completion
196
+ // emit can restore `dispatchedBy` and route the terminal event back (unicast).
197
+ // Refine jobs carry only a coordinator DAEMON id (no session), which is enough
198
+ // to route to the daemon-level coordinator. Absent → omitted (v1 entry).
199
+ const originatingStamp = kind === 'task_dispatched'
200
+ ? buildLedgerOriginatingCoordinatorStamp({ coordinatorDaemonId: handle.targetCoordinatorDaemonId })
201
+ : undefined;
202
+ appendLedgerEntry(handle.meshId, {
203
+ kind,
204
+ nodeId: handle.targetNodeId,
205
+ payload: {
206
+ source: 'refine_mesh_node_async_job',
207
+ refineJob: {
208
+ jobId: handle.jobId,
209
+ interactionId: handle.interactionId,
210
+ status: handle.status,
211
+ meshId: handle.meshId,
212
+ nodeId: handle.targetNodeId,
213
+ targetDaemonId: handle.targetDaemonId,
214
+ targetCoordinatorDaemonId: handle.targetCoordinatorDaemonId,
215
+ workspace: handle.workspace,
216
+ startedAt: handle.startedAt,
217
+ completedAt: handle.completedAt,
218
+ retryOfJobId: handle.retryOfJobId,
219
+ },
220
+ async: true,
221
+ retryOfJobId: handle.retryOfJobId,
222
+ ...(originatingStamp ? { originatingCoordinator: originatingStamp } : {}),
223
+ ...(result ? {
224
+ success: result.success === true,
225
+ result,
226
+ finalBranchConvergenceState: result.finalBranchConvergenceState,
227
+ ...(result.blockerContext ? { blockerContext: result.blockerContext } : {}),
228
+ } : {}),
229
+ },
230
+ });
231
+ } catch (e: any) {
232
+ LOG.warn('Mesh', `[Refinery] Failed to append async refine ledger entry: ${e?.message || e}`);
233
+ }
234
+ }
235
+
236
+ /**
237
+ * On daemon restart, scan all mesh ledgers for refine jobs that were dispatched
238
+ * but never completed/failed (i.e. the daemon died mid-job). Re-queue each one
239
+ * so the job runs to completion automatically without coordinator intervention.
240
+ */
241
+ export async function resumePendingRefineJobsOnStartup(self: DaemonCommandRouter): Promise<void> {
242
+ try {
243
+ const { listMeshes } = await import('../config/mesh-config.js');
244
+ const { readLedgerEntries } = await import('../mesh/mesh-ledger.js');
245
+ const meshIds: string[] = listMeshes().map(m => m.id).filter(Boolean) as string[];
246
+ for (const meshId of meshIds) {
247
+ const entries = readLedgerEntries(meshId, { kind: ['task_dispatched', 'task_completed', 'task_failed'] });
248
+ // Build set of nodeIds that already have a terminal entry.
249
+ const terminal = new Set<string>();
250
+ for (const e of entries) {
251
+ if ((e.kind === 'task_completed' || e.kind === 'task_failed') && e.nodeId) {
252
+ const jobId = (e.payload as any)?.refineJob?.jobId;
253
+ if (jobId) terminal.add(`${e.nodeId}:${jobId}`);
254
+ }
255
+ }
256
+ // Re-dispatch dispatched jobs with no matching terminal entry.
257
+ for (const e of entries) {
258
+ if (e.kind !== 'task_dispatched' || !e.nodeId) continue;
259
+ const source = (e.payload as any)?.source;
260
+ if (source !== 'refine_mesh_node_async_job') continue;
261
+ const jobId = (e.payload as any)?.refineJob?.jobId;
262
+ if (!jobId || terminal.has(`${e.nodeId}:${jobId}`)) continue;
263
+ const key = buildRefineJobKey(self, meshId, e.nodeId);
264
+ if (self.runningRefineJobs.has(key)) continue;
265
+ const coordinatorDaemonId = (e.payload as any)?.refineJob?.targetCoordinatorDaemonId;
266
+ LOG.info('Mesh', `[Refinery] Auto-resuming interrupted refine job for node ${e.nodeId} (jobId=${jobId})`);
267
+ void startMeshRefineJob(self, meshId, e.nodeId, {
268
+ coordinatorDaemonId,
269
+ });
270
+ }
271
+ }
272
+ } catch (e: any) {
273
+ LOG.warn('Mesh', `[Refinery] resumePendingRefineJobsOnStartup failed: ${e?.message || e}`);
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Synchronous refinery for a single worktree node — the gate pipeline that
279
+ * validates, preflights (patch-equivalence / submodule-reachability /
280
+ * no-op), merges, aligns submodules, cleans up the worktree node and
281
+ * (optionally) pushes. The body is a flat sequence of stage methods; each
282
+ * stage either returns a terminal CommandRouterResult (gate failure or a
283
+ * successful already-merged short-circuit) or `continue` with the extended
284
+ * context. Behavior — stage order, every early-exit, and every result shape —
285
+ * is identical to the previous single inlined body.
286
+ */
287
+ export async function executeMeshRefineNodeSynchronously(self: DaemonCommandRouter, meshId: string, nodeId: string, args: any): Promise<CommandRouterResult> {
288
+ const refineStages: Array<Record<string, unknown>> = [];
289
+ try {
290
+ const resolved = await refineResolveRefsStage(self, meshId, nodeId, args, refineStages);
291
+ if (resolved.kind === 'terminal') return resolved.result;
292
+ const ctx = resolved.ctx;
293
+
294
+ const validation = await refineValidationStage(self, ctx);
295
+ if (validation.kind === 'terminal') return validation.result;
296
+
297
+ const patchEquivalence = await refinePatchEquivalenceStage(self, ctx);
298
+ if (patchEquivalence.kind === 'terminal') return patchEquivalence.result;
299
+
300
+ const submoduleReachability = await refineSubmoduleReachabilityStage(self, ctx);
301
+ if (submoduleReachability.kind === 'terminal') return submoduleReachability.result;
302
+
303
+ const effectiveDiff = await refineEffectiveDiffStage(self, ctx);
304
+ if (effectiveDiff.kind === 'terminal') return effectiveDiff.result;
305
+
306
+ const merge = await refineMergeAndFinalizeStage(self, ctx);
307
+ return (merge as { kind: 'terminal'; result: CommandRouterResult }).result;
308
+ } catch (e: any) {
309
+ return { success: false, error: e.message, refineStages };
310
+ }
311
+ }
312
+
313
+ /**
314
+ * resolve_refs stage: resolve the mesh / worktree node / source node /
315
+ * repoRoot, then the worktree branch, base branch, fetched base head and
316
+ * branch head. Seeds the RefineContext consumed by every later stage.
317
+ */
318
+ export async function refineResolveRefsStage(self: DaemonCommandRouter,
319
+ meshId: string,
320
+ nodeId: string,
321
+ args: any,
322
+ refineStages: Array<Record<string, unknown>>,
323
+ ): Promise<RefineStageOutcome> {
324
+ // preferInline: same as startMeshRefineJob — inline-cache-only clone nodes must resolve.
325
+ const meshRecord = await self.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
326
+ const mesh = meshRecord?.mesh;
327
+ const node = mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
328
+ if (!node) return { kind: 'terminal', result: { success: false, error: `Node '${nodeId}' not found in mesh`, refineStages } };
329
+
330
+ if (!node.isLocalWorktree || !node.workspace) {
331
+ return { kind: 'terminal', result: { success: false, error: `Refinery requires a local worktree node`, refineStages } };
332
+ }
333
+
334
+ const sourceNode = node.clonedFromNodeId
335
+ ? mesh?.nodes.find((n: any) => meshNodeIdMatches(n, node.clonedFromNodeId))
336
+ : mesh?.nodes.find((n: any) => !n.isLocalWorktree);
337
+ const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
338
+ if (!repoRoot) return { kind: 'terminal', result: { success: false, error: 'Source node repoRoot not found', refineStages } };
339
+
340
+ const { execFile } = await import('node:child_process');
341
+ const { promisify } = await import('node:util');
342
+ const execFileAsync = promisify(execFile) as unknown as RefineExecFileAsync;
343
+
344
+ const resolveStarted = Date.now();
345
+ const { stdout: branchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: node.workspace, encoding: 'utf8' });
346
+ const branch = branchStdout.trim();
347
+ if (!branch) return { kind: 'terminal', result: { success: false, error: 'Could not determine branch of the worktree node', refineStages } };
348
+
349
+ const { stdout: baseBranchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: repoRoot, encoding: 'utf8' });
350
+ const baseBranch = baseBranchStdout.trim();
351
+
352
+ // Fetch origin so baseHead reflects the latest pushed state, not a stale local HEAD.
353
+ // This prevents patch_equivalence failures when sequential Refines push to origin/main
354
+ // but the local main checkout hasn't been fast-forwarded yet.
355
+ let fetchWarning: string | undefined;
356
+ try {
357
+ await execFileAsync('git', ['fetch', 'origin', baseBranch], { cwd: repoRoot, encoding: 'utf8' });
358
+ } catch (e: any) {
359
+ fetchWarning = `git fetch origin ${baseBranch} failed (proceeding with local HEAD): ${e?.message}`;
360
+ }
361
+
362
+ // Prefer origin/<baseBranch> as the authoritative base; fall back to local HEAD if fetch failed.
363
+ let baseHeadRaw: string;
364
+ try {
365
+ const { stdout } = await execFileAsync('git', ['rev-parse', `origin/${baseBranch}`], { cwd: repoRoot, encoding: 'utf8' });
366
+ baseHeadRaw = stdout.trim();
367
+ } catch {
368
+ const { stdout: localHead } = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: repoRoot, encoding: 'utf8' });
369
+ baseHeadRaw = localHead.trim();
370
+ }
371
+
372
+ const { stdout: branchHeadStdout } = await execFileAsync('git', ['rev-parse', branch], { cwd: node.workspace, encoding: 'utf8' });
373
+ const baseHead = baseHeadRaw;
374
+ const branchHead = branchHeadStdout.trim();
375
+
376
+ // Coarse daemon-vs-web change-impact for baseHead..branchHead, computed
377
+ // against the worktree so the same policy (.adhdev/change-impact.*) as the
378
+ // stale-build detector applies. Threaded onto ctx so the validation gate
379
+ // can scope its command set: a web-only branch skips daemon-scoped commands.
380
+ // FAIL-OPEN: any classification error leaves changeImpact undefined → the
381
+ // gate runs the full command set (never skip on uncertainty).
382
+ let changeImpact: ChangedPackageClassification | undefined;
383
+ try {
384
+ changeImpact = await classifyChangedPackages(node.workspace, baseHead, branchHead);
385
+ } catch {
386
+ changeImpact = undefined;
387
+ }
388
+ recordMeshRefineStage(refineStages, 'resolve_refs', 'passed', resolveStarted, {
389
+ branch, baseBranch, baseHead, branchHead,
390
+ ...(changeImpact ? { changeImpact } : {}),
391
+ ...(fetchWarning ? { fetchWarning } : {}),
392
+ });
393
+
394
+ return {
395
+ kind: 'continue',
396
+ ctx: {
397
+ meshId,
398
+ nodeId,
399
+ args,
400
+ refineStages,
401
+ execFileAsync,
402
+ mesh,
403
+ node,
404
+ sourceNode,
405
+ repoRoot,
406
+ branch,
407
+ baseBranch,
408
+ baseHead,
409
+ branchHead,
410
+ changeImpact,
411
+ validationSummary: undefined as any,
412
+ patchEquivalence: undefined as any,
413
+ submoduleReachability: undefined as any,
414
+ },
415
+ };
416
+ }
417
+
418
+ /**
419
+ * validation stage: run the refinery validation gate (typecheck / test /
420
+ * lint / build per node config) and block on failure or when no allowlisted
421
+ * command was available. On pass, stores the summary on the context.
422
+ */
423
+ export async function refineValidationStage(self: DaemonCommandRouter, ctx: RefineContext): Promise<RefineStageOutcome> {
424
+ const { mesh, node, branch, baseBranch, refineStages } = ctx;
425
+ const validationStarted = Date.now();
426
+ const validationSummary = await runMeshRefineValidationGate(mesh, node.workspace, {
427
+ // (a) Scope the validation command set by coarse change-impact (resolved
428
+ // in resolve_refs). Undefined → gate runs the full command set (fail-open).
429
+ changeImpact: ctx.changeImpact,
430
+ // M2-2: consume the node's persisted bootstrap state; persist re-runs.
431
+ persistedBootstrapState: (node as any).worktreeBootstrap as WorktreeBootstrapState | undefined,
432
+ onBootstrapStateChange: (state) => {
433
+ (node as any).worktreeBootstrap = state;
434
+ void import('../config/mesh-config.js')
435
+ .then(({ updateNode }) => updateNode(mesh.id, node.id, { worktreeBootstrap: state } as any))
436
+ .catch(() => { /* persistence is best-effort */ });
437
+ },
438
+ });
439
+ ctx.validationSummary = validationSummary;
440
+ recordMeshRefineStage(
441
+ refineStages,
442
+ 'validation',
443
+ validationSummary.status === 'passed' ? 'passed' : validationSummary.status === 'failed' ? 'failed' : 'skipped',
444
+ validationStarted,
445
+ { validationStatus: validationSummary.status, commandsRun: validationSummary.commandsRun.length },
446
+ );
447
+ if (validationSummary.status === 'failed') {
448
+ const firstFailedCmd = Array.isArray(validationSummary.commandsRun)
449
+ ? (validationSummary.commandsRun as Array<Record<string, unknown>>).find(c => c.success === false)
450
+ : undefined;
451
+ const buildValidationFailedError = (): string => {
452
+ const base = validationSummary.failureCode === 'missing_dependencies'
453
+ ? 'Refinery validation dependencies are missing for a change-affected package; merge/refine was not attempted. '
454
+ + 'To make this self-service, either (1) configure .adhdev/worktree_bootstrap.json (or validation.bootstrapCommands in .adhdev/refine.json) so Refinery installs deps before validation, '
455
+ + 'or (2) converge the branch via the documented manual fast-forward-only bypass (rebase onto the fetched base, verify strict ancestry, then push ff-only) instead of the refine gate.'
456
+ : validationSummary.failureCode === 'dependency_bootstrap_failed'
457
+ ? 'Refinery dependency/bootstrap command failed; merge/refine was not attempted.'
458
+ : validationSummary.failureCode === 'spawn_resolution_failed'
459
+ ? (validationSummary.spawnResolutionError
460
+ || 'Refinery validation command could not be spawned (executable not found); merge/refine was not attempted.')
461
+ : 'Refinery validation gate failed; merge/refine was not attempted.';
462
+ if (!firstFailedCmd) return base;
463
+ const cmdName = typeof firstFailedCmd.displayCommand === 'string' ? firstFailedCmd.displayCommand
464
+ : typeof firstFailedCmd.command === 'string'
465
+ ? [firstFailedCmd.command, ...(Array.isArray(firstFailedCmd.args) ? firstFailedCmd.args : [])].join(' ').trim()
466
+ : typeof firstFailedCmd.cmd === 'string' ? firstFailedCmd.cmd : '';
467
+ const rawOutput = [firstFailedCmd.stdout, firstFailedCmd.stderr, firstFailedCmd.output]
468
+ .filter(s => typeof s === 'string' && s.length > 0)
469
+ .join('\n');
470
+ const tail = rawOutput.length > 800 ? rawOutput.slice(-800) : rawOutput;
471
+ return [
472
+ base,
473
+ cmdName ? `First failing command: ${cmdName}` : '',
474
+ tail ? `Output (tail):\n${tail}` : '',
475
+ ].filter(Boolean).join('\n');
476
+ };
477
+ return { kind: 'terminal', result: {
478
+ success: false,
479
+ code: validationSummary.failureCode || 'validation_failed',
480
+ convergenceStatus: 'blocked_review',
481
+ error: buildValidationFailedError(),
482
+ branch,
483
+ into: baseBranch,
484
+ validationSummary,
485
+ refineStages,
486
+ finalBranchConvergenceState: {
487
+ branch,
488
+ baseBranch,
489
+ merged: false,
490
+ removed: false,
491
+ validation: 'failed',
492
+ status: 'blocked_review',
493
+ },
494
+ } };
495
+ }
496
+ if (validationSummary.status === 'skipped') {
497
+ return { kind: 'terminal', result: {
498
+ success: false,
499
+ code: 'validation_unavailable',
500
+ convergenceStatus: 'blocked_review',
501
+ error: 'Refinery validation gate is required but no allowlisted validation command was available; merge/refine was not attempted.',
502
+ branch,
503
+ into: baseBranch,
504
+ validationSummary,
505
+ refineStages,
506
+ finalBranchConvergenceState: {
507
+ branch,
508
+ baseBranch,
509
+ merged: false,
510
+ removed: false,
511
+ validation: 'unavailable',
512
+ status: 'blocked_review',
513
+ },
514
+ } };
515
+ }
516
+
517
+ return { kind: 'continue', ctx };
518
+ }
519
+
520
+ /**
521
+ * patch_equivalence stage: preflight that the worktree branch's cumulative
522
+ * patch is equivalent to base+branch. On a "behind base" branch, auto-rebase
523
+ * once and re-check; on an empty merge-tree with real branch changes, treat as
524
+ * already-merged-via-another-path and short-circuit to cleanup. Mutates the
525
+ * context's branchHead (after rebase) and patchEquivalence (rebased gate).
526
+ */
527
+ export async function refinePatchEquivalenceStage(self: DaemonCommandRouter, ctx: RefineContext): Promise<RefineStageOutcome> {
528
+ const { meshId, nodeId, args, repoRoot, baseHead, node, branch, baseBranch, validationSummary, refineStages, execFileAsync } = ctx;
529
+ let branchHead = ctx.branchHead;
530
+ const patchEquivalenceStarted = Date.now();
531
+ let patchEquivalence = await runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead);
532
+ recordMeshRefineStage(refineStages, 'patch_equivalence', patchEquivalence.status, patchEquivalenceStarted, {
533
+ equivalent: patchEquivalence.equivalent,
534
+ expectedPatchId: patchEquivalence.expectedPatchId,
535
+ actualPatchId: patchEquivalence.actualPatchId,
536
+ error: patchEquivalence.error,
537
+ actionableHint: patchEquivalence.actionableHint,
538
+ });
539
+ if (!patchEquivalence.equivalent) {
540
+ // Auto-rebase: if branch is simply behind base, attempt rebase automatically before failing.
541
+ let didAutoRebase = false;
542
+ let isBehindBase = false;
543
+ try {
544
+ execFileSync('git', ['merge-base', '--is-ancestor', branchHead, baseHead], {
545
+ cwd: node.workspace,
546
+ stdio: 'ignore',
547
+ });
548
+ isBehindBase = true;
549
+ } catch { /* non-zero exit means branchHead is not an ancestor of baseHead */ }
550
+
551
+ if (isBehindBase) {
552
+ const autoRebaseStarted = Date.now();
553
+ try {
554
+ execFileSync('git', ['rebase', baseHead], {
555
+ cwd: node.workspace,
556
+ stdio: ['ignore', 'pipe', 'pipe'],
557
+ });
558
+ const { stdout: rebasedHeadStdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: node.workspace, encoding: 'utf8' });
559
+ branchHead = rebasedHeadStdout.trim();
560
+ const rebasedPatchEquivalence = await runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead);
561
+ recordMeshRefineStage(refineStages, 'patch_equivalence_after_auto_rebase', rebasedPatchEquivalence.status, autoRebaseStarted, {
562
+ equivalent: rebasedPatchEquivalence.equivalent,
563
+ expectedPatchId: rebasedPatchEquivalence.expectedPatchId,
564
+ actualPatchId: rebasedPatchEquivalence.actualPatchId,
565
+ error: rebasedPatchEquivalence.error,
566
+ rebasedBranchHead: branchHead,
567
+ });
568
+ if (rebasedPatchEquivalence.equivalent) {
569
+ patchEquivalence = rebasedPatchEquivalence;
570
+ didAutoRebase = true;
571
+ } else {
572
+ return { kind: 'terminal', result: {
573
+ success: false,
574
+ code: 'needs_rebase',
575
+ convergenceStatus: 'blocked_review',
576
+ error: 'Branch was rebased onto base but patch equivalence still failed; manual intervention required.',
577
+ branch,
578
+ into: baseBranch,
579
+ validationSummary,
580
+ patchEquivalence: rebasedPatchEquivalence,
581
+ refineStages,
582
+ finalBranchConvergenceState: {
583
+ branch,
584
+ baseBranch,
585
+ merged: false,
586
+ removed: false,
587
+ validation: 'passed',
588
+ patchEquivalence: 'failed',
589
+ status: 'blocked_review',
590
+ },
591
+ } };
592
+ }
593
+ } catch (rebaseErr: any) {
594
+ try { execFileSync('git', ['rebase', '--abort'], { cwd: node.workspace, stdio: 'ignore' }); } catch { /* ignore */ }
595
+ recordMeshRefineStage(refineStages, 'patch_equivalence_after_auto_rebase', 'failed', autoRebaseStarted, {
596
+ error: rebaseErr?.message || String(rebaseErr),
597
+ });
598
+ return { kind: 'terminal', result: {
599
+ success: false,
600
+ code: 'needs_rebase_with_conflicts',
601
+ convergenceStatus: 'blocked_review',
602
+ error: 'Branch is behind base and auto-rebase failed due to conflicts; resolve conflicts manually and retry.',
603
+ branch,
604
+ into: baseBranch,
605
+ validationSummary,
606
+ patchEquivalence,
607
+ refineStages,
608
+ finalBranchConvergenceState: {
609
+ branch,
610
+ baseBranch,
611
+ merged: false,
612
+ removed: false,
613
+ validation: 'passed',
614
+ patchEquivalence: 'failed',
615
+ status: 'blocked_review',
616
+ },
617
+ } };
618
+ }
619
+ }
620
+
621
+ // If the actual patch-id is empty, the merge-tree produces no diff vs base —
622
+ // meaning the branch content is already present in base (landed via a different
623
+ // path, e.g. cherry-pick or direct commit). Treat this as "already merged":
624
+ // skip the merge step but still run cleanup so the worktree node is removed.
625
+ // "already merged via another path": the branch has real changes
626
+ // (expectedPatchId non-empty) but the merge-tree produces no diff
627
+ // against base (actualPatchId empty) — meaning every change in the
628
+ // branch is already present in base via a cherry-pick or direct commit.
629
+ // If both patch-ids are empty, the branch itself has no changes; that
630
+ // is a degenerate worktree case, not an "already merged" scenario.
631
+ const alreadyMergedViaOtherPath = !patchEquivalence.actualPatchId && !!patchEquivalence.expectedPatchId;
632
+ if (!didAutoRebase && !alreadyMergedViaOtherPath) {
633
+ return { kind: 'terminal', result: {
634
+ success: false,
635
+ code: 'patch_equivalence_failed',
636
+ convergenceStatus: 'blocked_review',
637
+ error: 'Refinery patch-equivalence preflight failed; merge/refine was not attempted.',
638
+ branch,
639
+ into: baseBranch,
640
+ validationSummary,
641
+ patchEquivalence,
642
+ refineStages,
643
+ finalBranchConvergenceState: {
644
+ branch,
645
+ baseBranch,
646
+ merged: false,
647
+ removed: false,
648
+ validation: 'passed',
649
+ patchEquivalence: 'failed',
650
+ status: 'blocked_review',
651
+ },
652
+ } };
653
+ }
654
+
655
+ if (!didAutoRebase && alreadyMergedViaOtherPath) {
656
+ // Content already in base — skip merge, go straight to cleanup.
657
+ recordMeshRefineStage(refineStages, 'merge', 'skipped', Date.now(), {
658
+ reason: 'already_merged_via_other_path',
659
+ note: 'actualPatchId is empty; branch content is already present in base via a different commit path',
660
+ });
661
+ const cleanupStarted = Date.now();
662
+ const removeResult = await self.execute('remove_mesh_node', {
663
+ meshId,
664
+ nodeId,
665
+ sessionCleanupMode: 'preserve',
666
+ inlineMesh: args?.inlineMesh,
667
+ });
668
+ recordMeshRefineStage(refineStages, 'cleanup', removeResult?.success === false ? 'failed' : 'passed', cleanupStarted, {
669
+ removed: removeResult?.removed,
670
+ code: removeResult?.code,
671
+ error: removeResult?.error,
672
+ });
673
+ try {
674
+ const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
675
+ appendLedgerEntry(meshId, {
676
+ kind: 'node_removed',
677
+ nodeId,
678
+ payload: { alreadyMergedViaOtherPath: true, branch, into: baseBranch, validationSummary, patchEquivalence },
679
+ });
680
+ } catch { /* ledger append is best-effort */ }
681
+ return { kind: 'terminal', result: {
682
+ success: removeResult?.success !== false,
683
+ code: 'already_merged',
684
+ merged: false,
685
+ alreadyMergedViaOtherPath: true,
686
+ branch,
687
+ into: baseBranch,
688
+ removeResult,
689
+ validationSummary,
690
+ patchEquivalence,
691
+ refineStages,
692
+ finalBranchConvergenceState: {
693
+ branch: baseBranch,
694
+ mergedBranch: branch,
695
+ baseBranch,
696
+ merged: false,
697
+ alreadyMergedViaOtherPath: true,
698
+ removed: removeResult?.success !== false,
699
+ validation: 'passed',
700
+ patchEquivalence: 'already_merged',
701
+ status: removeResult?.success === false ? 'merged_cleanup_failed' : 'merged_to_main',
702
+ },
703
+ } };
704
+ }
705
+ }
706
+
707
+ ctx.branchHead = branchHead;
708
+ ctx.patchEquivalence = patchEquivalence;
709
+ return { kind: 'continue', ctx };
710
+ }
711
+
712
+ /**
713
+ * submodule_reachability stage: verify every submodule gitlink commit that
714
+ * would land via the merge is reachable from its configured remote main
715
+ * branch (optionally auto-publishing when policy allows). Blocks the merge
716
+ * when any commit is unreachable. Stores the result on the context.
717
+ */
718
+ export async function refineSubmoduleReachabilityStage(self: DaemonCommandRouter, ctx: RefineContext): Promise<RefineStageOutcome> {
719
+ const { mesh, node, repoRoot, branch, baseBranch, branchHead, validationSummary, patchEquivalence, refineStages } = ctx;
720
+ const submoduleReachabilityStarted = Date.now();
721
+ const autoPublishSubmoduleMainCommits = resolveRefineryAutoPublishSubmoduleMainCommits(mesh, node.workspace);
722
+ const submoduleReachability = await runMeshRefineSubmoduleReachabilityGate(repoRoot, patchEquivalence.mergedTree || branchHead, {
723
+ allowAutoPublishSubmoduleMainCommits: autoPublishSubmoduleMainCommits.enabled,
724
+ autoPublishPolicySource: autoPublishSubmoduleMainCommits.source,
725
+ worktreeRoot: node.workspace,
726
+ });
727
+ recordMeshRefineStage(refineStages, 'submodule_reachability', submoduleReachability.status, submoduleReachabilityStarted, {
728
+ checked: submoduleReachability.checked,
729
+ autoPublishAllowed: submoduleReachability.autoPublishAllowed,
730
+ autoPublishPolicySource: submoduleReachability.autoPublishPolicySource,
731
+ autoPublished: submoduleReachability.entries
732
+ .filter(entry => entry.autoPublishAttempted)
733
+ .map(entry => ({
734
+ path: entry.path,
735
+ commit: entry.commit,
736
+ remote: entry.remote,
737
+ remoteUrl: entry.remoteUrl,
738
+ remoteMainBranch: entry.remoteMainBranch,
739
+ refspec: entry.autoPublishRefspec,
740
+ succeeded: entry.autoPublishSucceeded,
741
+ verified: entry.autoPublishVerified,
742
+ remoteMainReachable: entry.remoteMainReachable,
743
+ error: entry.error,
744
+ })),
745
+ autoPublishSkipped: submoduleReachability.entries
746
+ .filter(entry => entry.autoPublishAllowed === true && entry.autoPublishAttempted !== true)
747
+ .map(entry => ({
748
+ path: entry.path,
749
+ commit: entry.commit,
750
+ remote: entry.remote,
751
+ remoteUrl: entry.remoteUrl,
752
+ remoteMainBranch: entry.remoteMainBranch,
753
+ reason: entry.autoPublishSkippedReason || entry.error || 'auto-publish was allowed but no publish attempt was possible',
754
+ })),
755
+ unreachable: submoduleReachability.unreachable.map(entry => ({
756
+ path: entry.path,
757
+ commit: entry.commit,
758
+ publishRequired: entry.publishRequired === true,
759
+ autoPublishAllowed: entry.autoPublishAllowed,
760
+ autoPublishAttempted: entry.autoPublishAttempted,
761
+ autoPublishSucceeded: entry.autoPublishSucceeded,
762
+ autoPublishVerified: entry.autoPublishVerified,
763
+ autoPublishRefspec: entry.autoPublishRefspec,
764
+ autoPublishSkippedReason: entry.autoPublishSkippedReason,
765
+ remote: entry.remote,
766
+ remoteUrl: entry.remoteUrl,
767
+ remoteReachable: entry.remoteReachable,
768
+ remoteMainBranch: entry.remoteMainBranch,
769
+ remoteMainReachable: entry.remoteMainReachable,
770
+ error: entry.error,
771
+ })),
772
+ error: submoduleReachability.error,
773
+ });
774
+ if (submoduleReachability.status === 'failed') {
775
+ const nextStep = buildSubmodulePublishRequiredNextStep(submoduleReachability.unreachable);
776
+ return { kind: 'terminal', result: {
777
+ success: false,
778
+ code: 'submodule_reachability_failed',
779
+ convergenceStatus: 'blocked_review',
780
+ publishRequired: true,
781
+ blockedReason: 'submodule_publish_required',
782
+ error: 'Refinery submodule reachability preflight failed because one or more submodule gitlink commits are not reachable from their configured remote main branch; merge/refine cleanup was not attempted.',
783
+ nextStep,
784
+ nextSteps: [
785
+ 'Ask the user for explicit approval before pushing or publishing any submodule commit.',
786
+ 'Push/publish each unreachable submodule commit to the configured submodule remote main branch shown in the evidence.',
787
+ 'Rerun mesh_refine_node after remote reachability is confirmed.',
788
+ 'Do not merge the root branch until every submodule gitlink commit is reachable from submodule origin/main.',
789
+ ],
790
+ unreachableSubmoduleCommits: submoduleReachability.unreachable.map(entry => ({
791
+ path: entry.path,
792
+ commit: entry.commit,
793
+ remote: entry.remote,
794
+ remoteUrl: entry.remoteUrl,
795
+ remoteReachable: entry.remoteReachable,
796
+ remoteMainBranch: entry.remoteMainBranch,
797
+ remoteMainReachable: entry.remoteMainReachable,
798
+ autoPublishAllowed: entry.autoPublishAllowed,
799
+ autoPublishAttempted: entry.autoPublishAttempted,
800
+ autoPublishSucceeded: entry.autoPublishSucceeded,
801
+ autoPublishVerified: entry.autoPublishVerified,
802
+ autoPublishRefspec: entry.autoPublishRefspec,
803
+ autoPublishSkippedReason: entry.autoPublishSkippedReason,
804
+ error: entry.error,
805
+ })),
806
+ branch,
807
+ into: baseBranch,
808
+ validationSummary,
809
+ patchEquivalence,
810
+ submoduleReachability,
811
+ refineStages,
812
+ finalBranchConvergenceState: {
813
+ branch,
814
+ baseBranch,
815
+ merged: false,
816
+ removed: false,
817
+ validation: 'passed',
818
+ patchEquivalence: 'passed',
819
+ submoduleReachability: 'failed',
820
+ status: 'blocked_review',
821
+ reason: 'submodule_publish_required',
822
+ nextStep,
823
+ },
824
+ } };
825
+ }
826
+
827
+ ctx.submoduleReachability = submoduleReachability;
828
+ return { kind: 'continue', ctx };
829
+ }
830
+
831
+ /**
832
+ * effective_diff stage (no-op guard): block a silent no-op merge where the
833
+ * branch produces no effective root-tree diff against base — typically a
834
+ * submodule that has commits but whose root-level gitlink (pointer) bump was
835
+ * never committed, so the merge would land nothing real on main.
836
+ */
837
+ export async function refineEffectiveDiffStage(self: DaemonCommandRouter, ctx: RefineContext): Promise<RefineStageOutcome> {
838
+ const { repoRoot, baseHead, branchHead, branch, baseBranch, validationSummary, patchEquivalence, refineStages } = ctx;
839
+ // No-op guard: block a silent no-op merge where the root tree is identical to base.
840
+ // This catches the trap where a submodule has commits but the root branch never
841
+ // committed the gitlink (oss-pointer) bump — merging would report success while the
842
+ // real change never lands on main. A committed gitlink bump shows up in the root
843
+ // diff, so legitimate oss-pointer refines pass through untouched.
844
+ const effectiveDiffStarted = Date.now();
845
+ const effectiveDiff = await runMeshRefineEffectiveDiffGate(repoRoot, baseHead, branchHead);
846
+ recordMeshRefineStage(refineStages, 'effective_diff', effectiveDiff.status, effectiveDiffStarted, {
847
+ hasEffectiveDiff: effectiveDiff.hasEffectiveDiff,
848
+ changedPaths: effectiveDiff.changedPaths,
849
+ submoduleHints: effectiveDiff.submoduleHints,
850
+ ...(effectiveDiff.error ? { error: effectiveDiff.error } : {}),
851
+ });
852
+ if (effectiveDiff.status === 'failed' && !effectiveDiff.hasEffectiveDiff) {
853
+ const hintLines = (effectiveDiff.submoduleHints || []).map(h => ` - ${h.path}: ${h.reason}`);
854
+ const message = [
855
+ `Refinery no-op guard: branch '${branch}' has no effective root-tree diff against '${baseBranch}' (${baseHead.slice(0, 12)}); nothing would merge.`,
856
+ 'This usually means a submodule (e.g. oss) has commits but the root branch never committed the gitlink (pointer) bump, so the merge would be a silent no-op while the real change never reaches main.',
857
+ hintLines.length ? `Submodules with uncommitted pointer bumps:\n${hintLines.join('\n')}` : '',
858
+ `Fix: commit the submodule pointer bump on '${branch}' (git add <submodule-path> && git commit), then re-run refine.`,
859
+ ].filter(Boolean).join('\n');
860
+ return { kind: 'terminal', result: {
861
+ success: false,
862
+ code: 'no_effective_diff',
863
+ convergenceStatus: 'blocked_review',
864
+ error: message,
865
+ branch,
866
+ into: baseBranch,
867
+ validationSummary,
868
+ patchEquivalence,
869
+ effectiveDiff,
870
+ refineStages,
871
+ finalBranchConvergenceState: {
872
+ branch,
873
+ baseBranch,
874
+ merged: false,
875
+ removed: false,
876
+ validation: 'passed',
877
+ patchEquivalence: 'passed',
878
+ effectiveDiff: 'no_effective_diff',
879
+ status: 'blocked_review',
880
+ reason: 'no_effective_diff',
881
+ ...(effectiveDiff.submoduleHints?.length ? { submoduleHints: effectiveDiff.submoduleHints } : {}),
882
+ },
883
+ } };
884
+ }
885
+
886
+ return { kind: 'continue', ctx };
887
+ }
888
+
889
+ /**
890
+ * merge + finalize stage: perform the --no-ff merge, align submodule
891
+ * checkouts after merge, clean up (remove) the worktree node per policy,
892
+ * append the refinery ledger entry, and (unless approval is required) push the
893
+ * base branch. Always terminal — produces the final CommandRouterResult.
894
+ */
895
+ export async function refineMergeAndFinalizeStage(self: DaemonCommandRouter, ctx: RefineContext): Promise<RefineStageOutcome> {
896
+ const { meshId, nodeId, args, repoRoot, baseHead, node, branch, baseBranch, sourceNode, validationSummary, patchEquivalence, submoduleReachability, mesh, refineStages, execFileAsync } = ctx;
897
+ let mergeResult: Record<string, unknown> | undefined;
898
+ const mergeStarted = Date.now();
899
+ try {
900
+ const result = await execFileAsync('git', ['merge', '--no-ff', branch, '-m', `Auto-merge branch '${branch}' via Refinery`], { cwd: repoRoot, encoding: 'utf8' });
901
+ mergeResult = {
902
+ stdout: truncateValidationOutput(result.stdout),
903
+ stderr: truncateValidationOutput(result.stderr),
904
+ durationMs: Date.now() - mergeStarted,
905
+ };
906
+ recordMeshRefineStage(refineStages, 'merge', 'passed', mergeStarted, mergeResult);
907
+ } catch (e: any) {
908
+ recordMeshRefineStage(refineStages, 'merge', 'failed', mergeStarted, {
909
+ error: e?.message || String(e),
910
+ stdout: truncateValidationOutput(e?.stdout),
911
+ stderr: truncateValidationOutput(e?.stderr),
912
+ });
913
+ return { kind: 'terminal', result: {
914
+ success: false,
915
+ error: `Merge failed (conflicts?): ${e.message}`,
916
+ validationSummary,
917
+ patchEquivalence,
918
+ refineStages,
919
+ finalBranchConvergenceState: {
920
+ branch,
921
+ baseBranch,
922
+ merged: false,
923
+ removed: false,
924
+ validation: 'passed',
925
+ patchEquivalence: 'passed',
926
+ status: 'not_mergeable',
927
+ },
928
+ } };
929
+ }
930
+
931
+ const submoduleAlignmentStarted = Date.now();
932
+ const submoduleAlignment = await alignRefinerySubmodulesAfterMerge(repoRoot, baseHead, 'HEAD', {
933
+ submoduleIgnorePaths: Array.isArray(sourceNode?.policy?.submoduleIgnorePaths)
934
+ ? sourceNode.policy.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string')
935
+ : undefined,
936
+ });
937
+ if (submoduleAlignment.status !== 'skipped') {
938
+ recordMeshRefineStage(refineStages, 'submodule_alignment', submoduleAlignment.status, submoduleAlignmentStarted, {
939
+ changedGitlinkPaths: submoduleAlignment.changedGitlinkPaths,
940
+ outOfSyncPaths: submoduleAlignment.outOfSyncPaths,
941
+ updatedPaths: submoduleAlignment.updatedPaths,
942
+ verifiedPaths: submoduleAlignment.verifiedPaths,
943
+ command: submoduleAlignment.command,
944
+ error: submoduleAlignment.error,
945
+ });
946
+ }
947
+ if (submoduleAlignment.status === 'failed') {
948
+ return { kind: 'terminal', result: {
949
+ success: false,
950
+ code: 'post_merge_submodule_alignment_failed',
951
+ error: 'Refinery merge completed but post-merge submodule checkout alignment failed; run the reported git submodule update command and re-check base workspace status.',
952
+ merged: true,
953
+ branch,
954
+ into: baseBranch,
955
+ validationSummary,
956
+ patchEquivalence,
957
+ submoduleReachability,
958
+ submoduleAlignment,
959
+ mergeResult,
960
+ refineStages,
961
+ finalBranchConvergenceState: {
962
+ branch: baseBranch,
963
+ mergedBranch: branch,
964
+ baseBranch,
965
+ merged: true,
966
+ removed: false,
967
+ validation: 'passed',
968
+ patchEquivalence: 'passed',
969
+ submoduleReachability: 'passed',
970
+ submoduleAlignment: 'failed',
971
+ status: 'post_merge_alignment_failed',
972
+ nextStep: submoduleAlignment.command || 'Run git submodule update --init --recursive for the reported path(s), then re-check base workspace status.',
973
+ },
974
+ } };
975
+ }
976
+
977
+ const cleanupStarted = Date.now();
978
+ // Honor the mesh policy for delegated-session cleanup on the auto-removed
979
+ // worktree node (previously hardcoded to 'preserve', which orphaned the
980
+ // delegate session as an idle record on the coordinator daemon). Fall back
981
+ // to 'preserve' when no policy is set.
982
+ const refineSessionCleanupMode = self.normalizeMeshSessionCleanupMode(
983
+ mesh?.policy?.sessionCleanupOnNodeRemove,
984
+ );
985
+ // The delegate session launched for a clone worktree is frequently matched
986
+ // by workspace ONLY (no meta.meshNodeId binding), which remove_mesh_node's
987
+ // shared-daemon guard skips. Since refine knows exactly which workspace it
988
+ // just merged, collect that workspace's live session ids explicitly and pass
989
+ // them through — explicit sessionIds bypass the workspace-only-match guard so
990
+ // the policy-driven stop/delete actually runs.
991
+ let refineSessionIds: string[] | undefined;
992
+ if (refineSessionCleanupMode !== 'preserve' && self.deps.sessionHostControl) {
993
+ try {
994
+ const liveSessions = await self.deps.sessionHostControl.listSessions();
995
+ const workspace = typeof node.workspace === 'string' ? node.workspace : '';
996
+ refineSessionIds = liveSessions
997
+ .filter((record: any) => {
998
+ const sid = typeof record?.sessionId === 'string' ? record.sessionId : '';
999
+ if (!sid) return false;
1000
+ // Never sweep the coordinator's own session for this mesh.
1001
+ if (readStringValue(record?.meta?.meshCoordinatorFor) === meshId) return false;
1002
+ const boundToNode = readStringValue(record?.meta?.meshNodeId) === nodeId;
1003
+ const matchedByWorkspace = !!workspace && record?.workspace === workspace;
1004
+ return boundToNode || matchedByWorkspace;
1005
+ })
1006
+ .map((record: any) => String(record.sessionId));
1007
+ } catch {
1008
+ // listSessions failure is non-fatal — fall back to the policy-mode
1009
+ // cleanup without explicit ids (still better than hardcoded preserve).
1010
+ refineSessionIds = undefined;
1011
+ }
1012
+ }
1013
+ const removeResult = await self.execute('remove_mesh_node', {
1014
+ meshId,
1015
+ nodeId,
1016
+ sessionCleanupMode: refineSessionCleanupMode,
1017
+ ...(refineSessionIds && refineSessionIds.length > 0 ? { sessionIds: refineSessionIds } : {}),
1018
+ inlineMesh: args?.inlineMesh,
1019
+ // REFINE-CLEANUP: refine reaches cleanup only AFTER a verified merge
1020
+ // convergence, so any residual worktree dirtiness here is incidental
1021
+ // (e.g. a bootstrap lockfile rewrite) — never unmerged work. `force`
1022
+ // sets requireClean=false so a plain-dirty worktree no longer aborts
1023
+ // removal with merged_cleanup_failed. Branch-ref deletion still keys off
1024
+ // mergeConvergence (NOT the force flag), so no merged work can be lost.
1025
+ force: true,
1026
+ });
1027
+ recordMeshRefineStage(refineStages, 'cleanup', removeResult?.success === false ? 'failed' : 'passed', cleanupStarted, {
1028
+ removed: removeResult?.removed,
1029
+ code: removeResult?.code,
1030
+ error: removeResult?.error,
1031
+ });
1032
+
1033
+ let ledgerError: string | undefined;
1034
+ const ledgerStarted = Date.now();
1035
+ try {
1036
+ const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
1037
+ appendLedgerEntry(meshId, {
1038
+ kind: 'node_removed',
1039
+ nodeId,
1040
+ payload: { refined: true, mergedBranch: branch, into: baseBranch, validationSummary, patchEquivalence, submoduleReachability, submoduleAlignment },
1041
+ });
1042
+ recordMeshRefineStage(refineStages, 'ledger', 'passed', ledgerStarted);
1043
+ } catch (e: any) {
1044
+ ledgerError = e?.message || String(e);
1045
+ recordMeshRefineStage(refineStages, 'ledger', 'failed', ledgerStarted, { error: ledgerError });
1046
+ }
1047
+
1048
+ const finalBranchConvergenceState = {
1049
+ branch: baseBranch,
1050
+ mergedBranch: branch,
1051
+ baseBranch,
1052
+ merged: true,
1053
+ removed: removeResult?.success !== false,
1054
+ validation: 'passed',
1055
+ patchEquivalence: 'passed',
1056
+ submoduleAlignment: submoduleAlignment.status,
1057
+ status: removeResult?.success === false ? 'merged_cleanup_failed' : 'merged',
1058
+ };
1059
+
1060
+ if (removeResult?.success === false) {
1061
+ return { kind: 'terminal', result: {
1062
+ success: false,
1063
+ code: 'cleanup_failed',
1064
+ error: 'Refinery merge completed but worktree cleanup failed; manual cleanup/retry is required.',
1065
+ merged: true,
1066
+ branch,
1067
+ into: baseBranch,
1068
+ removeResult,
1069
+ validationSummary,
1070
+ patchEquivalence,
1071
+ submoduleReachability,
1072
+ submoduleAlignment,
1073
+ mergeResult,
1074
+ refineStages,
1075
+ ...(ledgerError ? { ledgerError } : {}),
1076
+ finalBranchConvergenceState,
1077
+ } };
1078
+ }
1079
+
1080
+ // Push logic: after a successful merge, either auto-push or surface push info
1081
+ // so coordinators don't need manual discovery after each refine.
1082
+ const requireApprovalForPush: boolean = (mesh as any)?.policy?.requireApprovalForPush ?? DEFAULT_MESH_POLICY.requireApprovalForPush;
1083
+ let pushResult: Record<string, unknown> | undefined;
1084
+ if (!requireApprovalForPush) {
1085
+ const pushStarted = Date.now();
1086
+ try {
1087
+ await execFileAsync('git', ['push', 'origin', baseBranch], { cwd: repoRoot, encoding: 'utf8' });
1088
+ pushResult = { pushed: true, remote: 'origin', branch: baseBranch, durationMs: Date.now() - pushStarted };
1089
+ recordMeshRefineStage(refineStages, 'push', 'passed', pushStarted, pushResult);
1090
+ finalBranchConvergenceState.status = 'merged_pushed';
1091
+ } catch (e: any) {
1092
+ pushResult = {
1093
+ pushed: false,
1094
+ remote: 'origin',
1095
+ branch: baseBranch,
1096
+ error: e?.message || String(e),
1097
+ stderr: e?.stderr,
1098
+ durationMs: Date.now() - pushStarted,
1099
+ };
1100
+ recordMeshRefineStage(refineStages, 'push', 'failed', pushStarted, pushResult);
1101
+ }
1102
+ }
1103
+
1104
+ return { kind: 'terminal', result: {
1105
+ success: true,
1106
+ merged: true,
1107
+ branch,
1108
+ into: baseBranch,
1109
+ removeResult,
1110
+ validationSummary,
1111
+ patchEquivalence,
1112
+ submoduleReachability,
1113
+ submoduleAlignment,
1114
+ mergeResult,
1115
+ refineStages,
1116
+ ...(ledgerError ? { ledgerError } : {}),
1117
+ finalBranchConvergenceState,
1118
+ // Push outcome or readiness info for coordinator.
1119
+ ...(pushResult
1120
+ ? { pushResult }
1121
+ : {
1122
+ pushReady: true,
1123
+ pushCommand: `git push origin ${baseBranch}`,
1124
+ pushNote: 'requireApprovalForPush is enabled — run the push command or obtain user approval before pushing.',
1125
+ }),
1126
+ } };
1127
+ }
1128
+
1129
+ /**
1130
+ * Batch refinery: converge multiple sibling worktree nodes onto the base branch
1131
+ * in one sequential pipeline, absorbing the rebase + patch-equivalence churn that
1132
+ * arises when several siblings touch the same submodule.
1133
+ *
1134
+ * Reuses executeMeshRefineNodeSynchronously per node — every node goes through the
1135
+ * exact same validation / patch-equivalence / submodule-reachability / merge / cleanup
1136
+ * gates, including its built-in auto-rebase onto fresh origin/<base>. Because each
1137
+ * node fetches origin/<base> at the start of its own refine, a node merged earlier in
1138
+ * the batch advances the base, and the next node's refine auto-rebases onto it before
1139
+ * re-running patch-equivalence. No force-push, no reset — conflicting nodes are
1140
+ * isolated as blocked_review while the rest of the batch proceeds.
1141
+ */
1142
+ export async function batchRefineMeshNodes(self: DaemonCommandRouter, meshId: string, requestedNodeIds: string[] | undefined, args: any): Promise<CommandRouterResult> {
1143
+ // preferInline: same membership authority as refine_mesh_node — inline-cache-only
1144
+ // clone nodes (created in this MCP session) must resolve.
1145
+ const meshRecord = await self.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
1146
+ const mesh = meshRecord?.mesh;
1147
+ if (!mesh) return { success: false, error: `Mesh '${meshId}' not found` };
1148
+
1149
+ const allNodes: any[] = Array.isArray(mesh.nodes) ? mesh.nodes : [];
1150
+ const isConvergeable = (n: any) => n?.isLocalWorktree && typeof n.workspace === 'string' && n.workspace;
1151
+
1152
+ let targetNodes: any[];
1153
+ if (Array.isArray(requestedNodeIds) && requestedNodeIds.length > 0) {
1154
+ targetNodes = [];
1155
+ const missing: string[] = [];
1156
+ const nonWorktree: string[] = [];
1157
+ for (const nodeId of requestedNodeIds) {
1158
+ const node = allNodes.find(n => meshNodeIdMatches(n, nodeId));
1159
+ if (!node) { missing.push(nodeId); continue; }
1160
+ if (!isConvergeable(node)) { nonWorktree.push(nodeId); continue; }
1161
+ targetNodes.push(node);
1162
+ }
1163
+ if (missing.length || nonWorktree.length) {
1164
+ return {
1165
+ success: false,
1166
+ error: 'One or more requested nodes are not convergeable local worktree nodes.',
1167
+ ...(missing.length ? { missingNodeIds: missing } : {}),
1168
+ ...(nonWorktree.length ? { nonWorktreeNodeIds: nonWorktree } : {}),
1169
+ };
1170
+ }
1171
+ } else {
1172
+ // Auto-collect: every local worktree node is a convergence candidate.
1173
+ targetNodes = allNodes.filter(isConvergeable);
1174
+ }
1175
+
1176
+ if (targetNodes.length === 0) {
1177
+ return { success: true, batch: true, dryRun: args?.dryRun !== false, nodeCount: 0, order: [], results: [], note: 'No convergeable local worktree nodes found.' };
1178
+ }
1179
+
1180
+ const { execFile } = await import('node:child_process');
1181
+ const { promisify } = await import('node:util');
1182
+ const execFileAsync = promisify(execFile);
1183
+
1184
+ // Resolve the base repo root and a base ref to analyze change areas against.
1185
+ const resolveRepoRootFor = (node: any): string | undefined => {
1186
+ const sourceNode = node.clonedFromNodeId
1187
+ ? allNodes.find(n => meshNodeIdMatches(n, node.clonedFromNodeId))
1188
+ : allNodes.find(n => !n.isLocalWorktree);
1189
+ return sourceNode?.repoRoot || sourceNode?.workspace;
1190
+ };
1191
+
1192
+ // Analyze change areas for ordering. The repoRoot is shared across siblings of
1193
+ // the same source; resolve a base ref (origin/<base> preferred) once per repoRoot.
1194
+ const repoRootBaseRef = new Map<string, string>();
1195
+ const submodulePathsByRepoRoot = new Map<string, Set<string>>();
1196
+ const resolveBaseRef = async (repoRoot: string): Promise<string> => {
1197
+ const cached = repoRootBaseRef.get(repoRoot);
1198
+ if (cached) return cached;
1199
+ let baseBranch = 'main';
1200
+ try {
1201
+ const { stdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: repoRoot, encoding: 'utf8' });
1202
+ if (stdout.trim()) baseBranch = stdout.trim();
1203
+ } catch { /* fall back to main */ }
1204
+ let baseRef = 'HEAD';
1205
+ try {
1206
+ await execFileAsync('git', ['fetch', 'origin', baseBranch], { cwd: repoRoot, encoding: 'utf8' });
1207
+ } catch { /* offline / no remote — fall through to local refs */ }
1208
+ try {
1209
+ const { stdout } = await execFileAsync('git', ['rev-parse', `origin/${baseBranch}`], { cwd: repoRoot, encoding: 'utf8' });
1210
+ baseRef = stdout.trim();
1211
+ } catch {
1212
+ try {
1213
+ const { stdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: repoRoot, encoding: 'utf8' });
1214
+ baseRef = stdout.trim();
1215
+ } catch { /* leave HEAD */ }
1216
+ }
1217
+ repoRootBaseRef.set(repoRoot, baseRef);
1218
+ return baseRef;
1219
+ };
1220
+
1221
+ const changeAreas: Array<Awaited<ReturnType<typeof analyzeMeshRefineNodeChangeArea>>> = [];
1222
+ for (const node of targetNodes) {
1223
+ const repoRoot = resolveRepoRootFor(node);
1224
+ let branch = typeof node.worktreeBranch === 'string' ? node.worktreeBranch : '';
1225
+ try {
1226
+ const { stdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: node.workspace, encoding: 'utf8' });
1227
+ if (stdout.trim()) branch = stdout.trim();
1228
+ } catch { /* use stored worktreeBranch */ }
1229
+
1230
+ if (!repoRoot || !branch) {
1231
+ changeAreas.push({
1232
+ nodeId: node.id, workspace: node.workspace, branch: branch || '(unknown)',
1233
+ changedTopLevelPaths: [], changedFiles: [], touchedSubmodulePaths: [],
1234
+ touchesSubmodule: false, aheadCount: 0,
1235
+ error: !repoRoot ? 'source repoRoot not found' : 'branch not resolved',
1236
+ });
1237
+ continue;
1238
+ }
1239
+ if (!submodulePathsByRepoRoot.has(repoRoot)) {
1240
+ // Resolve declared submodule paths once per repo root.
1241
+ let subPaths = new Set<string>();
1242
+ try {
1243
+ const { stdout } = await execFileAsync('git', ['config', '--file', '.gitmodules', '--get-regexp', 'path'], { cwd: repoRoot, encoding: 'utf8' });
1244
+ for (const line of stdout.split('\n')) {
1245
+ const trimmed = line.trim();
1246
+ const spaceIdx = trimmed.indexOf(' ');
1247
+ if (spaceIdx === -1) continue;
1248
+ const value = trimmed.slice(spaceIdx + 1).trim();
1249
+ if (value) subPaths.add(value);
1250
+ }
1251
+ } catch { subPaths = new Set(); }
1252
+ submodulePathsByRepoRoot.set(repoRoot, subPaths);
1253
+ }
1254
+ const baseRef = await resolveBaseRef(repoRoot);
1255
+ let branchRef = branch;
1256
+ try {
1257
+ const { stdout } = await execFileAsync('git', ['rev-parse', branch], { cwd: node.workspace, encoding: 'utf8' });
1258
+ branchRef = stdout.trim() || branch;
1259
+ } catch { /* use branch name */ }
1260
+ changeAreas.push(await analyzeMeshRefineNodeChangeArea({
1261
+ nodeId: node.id,
1262
+ workspace: node.workspace,
1263
+ branch,
1264
+ baseRef,
1265
+ branchRef,
1266
+ diffCwd: node.workspace,
1267
+ submodulePaths: submodulePathsByRepoRoot.get(repoRoot)!,
1268
+ }));
1269
+ }
1270
+
1271
+ const ordering = orderMeshRefineBatchNodes(changeAreas);
1272
+ const orderedNodes = ordering.order
1273
+ .map(nodeId => targetNodes.find(n => meshNodeIdMatches(n, nodeId)))
1274
+ .filter((n): n is any => !!n);
1275
+
1276
+ const dryRun = args?.dryRun !== false && args?.execute !== true;
1277
+ if (dryRun) {
1278
+ return {
1279
+ success: true,
1280
+ batch: true,
1281
+ dryRun: true,
1282
+ nodeCount: orderedNodes.length,
1283
+ order: ordering.order,
1284
+ orderingRationale: ordering.rationale,
1285
+ changeAreas: ordering.changeAreas,
1286
+ plan: orderedNodes.map(node => ({
1287
+ nodeId: node.id,
1288
+ workspace: node.workspace,
1289
+ validationPlan: buildMeshRefineValidationPlan(mesh, node.workspace),
1290
+ mergeWillRun: false,
1291
+ })),
1292
+ note: 'Dry-run: no validation, rebase, or merge was executed. Re-run with execute=true to converge nodes in this order.',
1293
+ };
1294
+ }
1295
+
1296
+ // Execute: refine each node in order via the shared convergence core.
1297
+ return runMeshRefineBatchConvergence(self, meshId, orderedNodes, ordering, args);
1298
+ }
1299
+
1300
+ /**
1301
+ * Convergence core shared by the synchronous batch entry and the async batch job.
1302
+ * Refines each node in order: the per-node refine pipeline fetches origin/<base>
1303
+ * fresh, so each merged sibling advances the base before the next node's auto-rebase
1304
+ * + patch-equivalence re-check. A blocked/failed node is isolated; the batch
1305
+ * continues with the remaining nodes. Does NOT touch the per-node merge logic — it
1306
+ * only sequences calls to executeMeshRefineNodeSynchronously and aggregates outcomes.
1307
+ */
1308
+ export async function runMeshRefineBatchConvergence(self: DaemonCommandRouter,
1309
+ meshId: string,
1310
+ orderedNodes: any[],
1311
+ ordering: { order: string[]; rationale?: unknown },
1312
+ args: any,
1313
+ ): Promise<CommandRouterResult> {
1314
+ type BatchNodeOutcome = {
1315
+ nodeId: string;
1316
+ workspace: string;
1317
+ convergence: 'merged_to_main' | 'blocked_review' | 'skipped_patch_equivalent' | 'not_mergeable';
1318
+ code?: string;
1319
+ reason?: string;
1320
+ stage?: string;
1321
+ error?: string;
1322
+ finalBranchConvergenceState?: Record<string, unknown>;
1323
+ };
1324
+ const results: BatchNodeOutcome[] = [];
1325
+ for (const node of orderedNodes) {
1326
+ let result: Record<string, unknown>;
1327
+ try {
1328
+ result = await executeMeshRefineNodeSynchronously(self, meshId, node.id, args) as Record<string, unknown>;
1329
+ } catch (e: any) {
1330
+ result = { success: false, error: e?.message || String(e) };
1331
+ }
1332
+ const code = typeof result.code === 'string' ? result.code : '';
1333
+ // already_merged (branch content already on base via another path) is a
1334
+ // non-error skip regardless of success flag — the worktree converges with
1335
+ // no new merge. A real `git merge` conflict surfaces as merge_failed →
1336
+ // not_mergeable. Everything else that failed is isolated as blocked_review.
1337
+ let convergence: BatchNodeOutcome['convergence'];
1338
+ if (code === 'already_merged' && result.alreadyMergedViaOtherPath) {
1339
+ convergence = 'skipped_patch_equivalent';
1340
+ } else if (result.success === true) {
1341
+ convergence = 'merged_to_main';
1342
+ } else if (code === 'merge_failed') {
1343
+ convergence = 'not_mergeable';
1344
+ } else {
1345
+ convergence = 'blocked_review';
1346
+ }
1347
+ const fbcs = (result.finalBranchConvergenceState && typeof result.finalBranchConvergenceState === 'object')
1348
+ ? result.finalBranchConvergenceState as Record<string, unknown>
1349
+ : undefined;
1350
+ const stage = Array.isArray(result.refineStages)
1351
+ ? (result.refineStages as Array<Record<string, unknown>>).filter(s => s.status === 'failed').map(s => s.stage).filter(Boolean).pop() as string | undefined
1352
+ : undefined;
1353
+ results.push({
1354
+ nodeId: node.id,
1355
+ workspace: node.workspace,
1356
+ convergence,
1357
+ ...(code ? { code } : {}),
1358
+ ...(typeof result.blockedReason === 'string' ? { reason: result.blockedReason } : {}),
1359
+ ...(stage ? { stage } : {}),
1360
+ ...(typeof result.error === 'string' ? { error: result.error } : {}),
1361
+ ...(fbcs ? { finalBranchConvergenceState: fbcs } : {}),
1362
+ });
1363
+ }
1364
+
1365
+ const summary = {
1366
+ merged: results.filter(r => r.convergence === 'merged_to_main').length,
1367
+ skipped: results.filter(r => r.convergence === 'skipped_patch_equivalent').length,
1368
+ blocked: results.filter(r => r.convergence === 'blocked_review').length,
1369
+ notMergeable: results.filter(r => r.convergence === 'not_mergeable').length,
1370
+ };
1371
+ const allConverged = summary.blocked === 0 && summary.notMergeable === 0;
1372
+ return {
1373
+ success: true,
1374
+ batch: true,
1375
+ dryRun: false,
1376
+ nodeCount: orderedNodes.length,
1377
+ order: ordering.order,
1378
+ orderingRationale: ordering.rationale,
1379
+ summary,
1380
+ allConverged,
1381
+ results,
1382
+ ...(allConverged ? {} : {
1383
+ nextStep: 'Resolve blocked_review / not_mergeable nodes manually (see per-node code/stage/error), then re-run mesh_refine_batch for the remaining nodes.',
1384
+ }),
1385
+ };
1386
+ }
1387
+
1388
+ export function buildRefineBatchJobKey(self: DaemonCommandRouter, meshId: string): string {
1389
+ return `${meshId}::batch`;
1390
+ }
1391
+
1392
+ export function buildRefineBatchJobHandle(self: DaemonCommandRouter, args: {
1393
+ meshId: string;
1394
+ nodeIds: string[];
1395
+ order: string[];
1396
+ status?: MeshRefineBatchJobStatus;
1397
+ startedAt?: string;
1398
+ completedAt?: string;
1399
+ jobId?: string;
1400
+ interactionId?: string;
1401
+ coordinatorDaemonId?: string;
1402
+ }): MeshRefineBatchJobHandle {
1403
+ return {
1404
+ success: true,
1405
+ async: true,
1406
+ batch: true,
1407
+ status: args.status || 'accepted',
1408
+ jobId: args.jobId || `refine_batch_${createInteractionId()}`,
1409
+ interactionId: args.interactionId || createInteractionId(),
1410
+ meshId: args.meshId,
1411
+ batchLabel: `batch:${args.nodeIds.length} node${args.nodeIds.length === 1 ? '' : 's'}`,
1412
+ nodeIds: args.nodeIds,
1413
+ nodeCount: args.nodeIds.length,
1414
+ order: args.order,
1415
+ startedAt: args.startedAt || new Date().toISOString(),
1416
+ ...(args.completedAt ? { completedAt: args.completedAt } : {}),
1417
+ ...(args.coordinatorDaemonId ? { targetCoordinatorDaemonId: args.coordinatorDaemonId } : {}),
1418
+ eventDelivery: { pendingEvents: true, ledger: true },
1419
+ evidence: {
1420
+ pendingEventsCommand: 'get_pending_mesh_events',
1421
+ ledgerCommand: 'get_mesh_ledger_slice',
1422
+ taskHistoryKind: args.status === 'completed' ? 'task_completed' : args.status === 'failed' ? 'task_failed' : 'task_dispatched',
1423
+ },
1424
+ };
1425
+ }
1426
+
1427
+ /**
1428
+ * Emit a batch Refinery terminal/accepted event through the SAME pending-event +
1429
+ * forward mechanism single-node refine uses (queueRefineJobEvent), so the
1430
+ * coordinator's existing refine:accepted/completed/failed handling and message
1431
+ * renderer apply unchanged. The aggregate per-node results ride along in `result`.
1432
+ */
1433
+ export function queueRefineBatchJobEvent(self: DaemonCommandRouter,
1434
+ event: 'refine:accepted' | 'refine:completed' | 'refine:failed',
1435
+ handle: MeshRefineBatchJobHandle,
1436
+ result?: Record<string, unknown>,
1437
+ ): void {
1438
+ const metadataEvent = {
1439
+ source: 'refine_mesh_node_async_job',
1440
+ batch: true,
1441
+ jobId: handle.jobId,
1442
+ interactionId: handle.interactionId,
1443
+ meshId: handle.meshId,
1444
+ nodeId: handle.batchLabel,
1445
+ nodeIds: handle.nodeIds,
1446
+ workspace: undefined,
1447
+ status: handle.status,
1448
+ startedAt: handle.startedAt,
1449
+ completedAt: handle.completedAt,
1450
+ order: handle.order,
1451
+ ...(result ? { result } : {}),
1452
+ };
1453
+ const eventPayload = {
1454
+ event,
1455
+ meshId: handle.meshId,
1456
+ nodeLabel: handle.batchLabel,
1457
+ nodeId: handle.batchLabel,
1458
+ metadataEvent,
1459
+ queuedAt: Date.now(),
1460
+ ...(handle.targetCoordinatorDaemonId ? { targetCoordinatorDaemonId: handle.targetCoordinatorDaemonId } : {}),
1461
+ };
1462
+ if (typeof self.deps.instanceManager?.getByCategory === 'function') {
1463
+ const forwarded = handleMeshForwardEvent(
1464
+ { instanceManager: self.deps.instanceManager } as any,
1465
+ {
1466
+ event,
1467
+ meshId: handle.meshId,
1468
+ nodeId: handle.batchLabel,
1469
+ jobId: handle.jobId,
1470
+ interactionId: handle.interactionId,
1471
+ status: handle.status,
1472
+ startedAt: handle.startedAt,
1473
+ completedAt: handle.completedAt,
1474
+ ...(result ? { result } : {}),
1475
+ },
1476
+ );
1477
+ if (forwarded?.success === true) return;
1478
+ LOG.warn('Mesh', `[Refinery] Failed to forward async refine batch event ${event}: ${forwarded?.error || 'unknown error'}`);
1479
+ }
1480
+ queuePendingMeshCoordinatorEvent(eventPayload);
1481
+ }
1482
+
1483
+ export async function appendRefineBatchJobLedger(self: DaemonCommandRouter,
1484
+ kind: 'task_dispatched' | 'task_completed' | 'task_failed',
1485
+ handle: MeshRefineBatchJobHandle,
1486
+ result?: Record<string, unknown>,
1487
+ ): Promise<void> {
1488
+ try {
1489
+ const { appendLedgerEntry, buildLedgerOriginatingCoordinatorStamp } = await import('../mesh/mesh-ledger.js');
1490
+ // B2a: stamp the originating coordinator on dispatch (see appendRefineJobLedger).
1491
+ const originatingStamp = kind === 'task_dispatched'
1492
+ ? buildLedgerOriginatingCoordinatorStamp({ coordinatorDaemonId: handle.targetCoordinatorDaemonId })
1493
+ : undefined;
1494
+ appendLedgerEntry(handle.meshId, {
1495
+ kind,
1496
+ nodeId: handle.batchLabel,
1497
+ payload: {
1498
+ source: 'refine_mesh_node_async_job',
1499
+ refineJob: {
1500
+ batch: true,
1501
+ jobId: handle.jobId,
1502
+ interactionId: handle.interactionId,
1503
+ status: handle.status,
1504
+ meshId: handle.meshId,
1505
+ nodeIds: handle.nodeIds,
1506
+ order: handle.order,
1507
+ targetCoordinatorDaemonId: handle.targetCoordinatorDaemonId,
1508
+ startedAt: handle.startedAt,
1509
+ completedAt: handle.completedAt,
1510
+ },
1511
+ async: true,
1512
+ batch: true,
1513
+ ...(originatingStamp ? { originatingCoordinator: originatingStamp } : {}),
1514
+ ...(result ? {
1515
+ success: result.success === true,
1516
+ result,
1517
+ } : {}),
1518
+ },
1519
+ });
1520
+ } catch (e: any) {
1521
+ LOG.warn('Mesh', `[Refinery] Failed to append async refine batch ledger entry: ${e?.message || e}`);
1522
+ }
1523
+ }
1524
+
1525
+ export async function finishMeshRefineBatchJob(self: DaemonCommandRouter,
1526
+ handle: MeshRefineBatchJobHandle,
1527
+ orderedNodes: any[],
1528
+ ordering: { order: string[]; rationale?: unknown },
1529
+ args: any,
1530
+ ): Promise<void> {
1531
+ const key = buildRefineBatchJobKey(self, handle.meshId);
1532
+ let result: Record<string, unknown>;
1533
+ try {
1534
+ result = await runMeshRefineBatchConvergence(self, handle.meshId, orderedNodes, ordering, args) as Record<string, unknown>;
1535
+ } catch (e: any) {
1536
+ result = { success: false, error: e?.message || String(e), batch: true };
1537
+ }
1538
+ const completedAt = new Date().toISOString();
1539
+
1540
+ // The batch as a whole "completed" only when every node converged (no blocked /
1541
+ // not_mergeable). A partial batch is reported as a terminal failure so the
1542
+ // coordinator inspects the per-node blockers rather than assuming a clean merge.
1543
+ const summary = (result.summary && typeof result.summary === 'object') ? result.summary as Record<string, number> : undefined;
1544
+ const allConverged = result.allConverged === true;
1545
+ const isTerminalSuccess = result.success === true && allConverged;
1546
+
1547
+ const nextStep = typeof result.nextStep === 'string' && result.nextStep
1548
+ ? result.nextStep
1549
+ : isTerminalSuccess
1550
+ ? 'All batched nodes converged onto base. Continue from the updated mesh state.'
1551
+ : 'Resolve blocked_review / not_mergeable nodes (see per-node code/stage/error in result.results), then re-run mesh_refine_batch for the remaining nodes.';
1552
+ const normalizedResult = {
1553
+ ...result,
1554
+ batch: true,
1555
+ nextStep,
1556
+ ...(summary ? {
1557
+ convergenceStatus: allConverged ? 'all_converged' : 'partial',
1558
+ } : {}),
1559
+ };
1560
+
1561
+ const terminalHandle = buildRefineBatchJobHandle(self, {
1562
+ meshId: handle.meshId,
1563
+ nodeIds: handle.nodeIds,
1564
+ order: handle.order,
1565
+ status: isTerminalSuccess ? 'completed' : 'failed',
1566
+ startedAt: handle.startedAt,
1567
+ completedAt,
1568
+ jobId: handle.jobId,
1569
+ interactionId: handle.interactionId,
1570
+ coordinatorDaemonId: handle.targetCoordinatorDaemonId,
1571
+ });
1572
+ const terminal: MeshRefineBatchTerminalJob = { ...terminalHandle, result: normalizedResult };
1573
+ self.terminalRefineBatchJobs.set(key, terminal);
1574
+ self.runningRefineBatchJobs.delete(key);
1575
+ self.invalidateAggregateMeshStatus(handle.meshId);
1576
+ await appendRefineBatchJobLedger(self, isTerminalSuccess ? 'task_completed' : 'task_failed', terminalHandle, normalizedResult);
1577
+ queueRefineBatchJobEvent(self, isTerminalSuccess ? 'refine:completed' : 'refine:failed', terminalHandle, normalizedResult);
1578
+ }
1579
+
1580
+ /**
1581
+ * Async entry for the batch Refinery execute path. Mirrors startMeshRefineJob:
1582
+ * resolves the plan synchronously (so target/ordering errors and the dry-run shape
1583
+ * stay synchronous), then for execute=true registers an in-flight batch job, returns
1584
+ * {async:true, status:'accepted', batch:true, ...plan} immediately, and runs the
1585
+ * convergence loop in the background — emitting the same terminal refine event.
1586
+ * Idempotent: a batch already in flight for this mesh returns the running handle
1587
+ * with duplicate:true rather than spawning a second background job.
1588
+ */
1589
+ export async function startMeshRefineBatchJob(self: DaemonCommandRouter, meshId: string, requestedNodeIds: string[] | undefined, args: any): Promise<CommandRouterResult> {
1590
+ // Resolve the plan up-front. For dry-run this returns the synchronous plan; for
1591
+ // execute it returns the same plan shape but we hand convergence to the bg job.
1592
+ const plan = await batchRefineMeshNodes(self, meshId, requestedNodeIds, { ...args, dryRun: true, execute: false });
1593
+ const planRecord = plan as Record<string, unknown>;
1594
+ if (planRecord.success !== true) return plan;
1595
+
1596
+ // If the caller actually asked for a dry-run, return the plan as-is (sync).
1597
+ if (args?.dryRun === true && args?.execute !== true) return plan;
1598
+
1599
+ const order = Array.isArray(planRecord.order) ? (planRecord.order as unknown[]).filter((v): v is string => typeof v === 'string') : [];
1600
+ const nodeIds = order.slice();
1601
+ if (nodeIds.length === 0) {
1602
+ // No convergeable nodes — nothing to dispatch; return the empty plan synchronously.
1603
+ return { ...planRecord, success: true, batch: true, dryRun: false, async: false };
1604
+ }
1605
+
1606
+ const key = buildRefineBatchJobKey(self, meshId);
1607
+ const running = self.runningRefineBatchJobs.get(key);
1608
+ if (running) return { ...running, duplicate: true };
1609
+
1610
+ // Re-resolve the ordered node objects against current membership so the bg job
1611
+ // refines real nodes (the plan only carries ids). preferInline matches refine_mesh_node.
1612
+ const meshRecord = await self.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
1613
+ const mesh = meshRecord?.mesh;
1614
+ const allNodes: any[] = Array.isArray(mesh?.nodes) ? mesh.nodes : [];
1615
+ const orderedNodes = nodeIds
1616
+ .map(id => allNodes.find(n => meshNodeIdMatches(n, id)))
1617
+ .filter((n): n is any => !!n);
1618
+ if (orderedNodes.length === 0) {
1619
+ return { success: false, error: 'Batch nodes no longer resolvable in mesh', batch: true };
1620
+ }
1621
+ const ordering = {
1622
+ order,
1623
+ rationale: planRecord.orderingRationale,
1624
+ };
1625
+
1626
+ const coordinatorDaemonId = typeof args?.coordinatorDaemonId === 'string' && args.coordinatorDaemonId.trim()
1627
+ ? args.coordinatorDaemonId.trim()
1628
+ : (self.deps.statusInstanceId || undefined);
1629
+ const handle = buildRefineBatchJobHandle(self, { meshId, nodeIds, order, coordinatorDaemonId });
1630
+ self.runningRefineBatchJobs.set(key, handle);
1631
+ await appendRefineBatchJobLedger(self, 'task_dispatched', handle);
1632
+ queueRefineBatchJobEvent(self, 'refine:accepted', handle);
1633
+
1634
+ setImmediate(() => {
1635
+ void finishMeshRefineBatchJob(self, handle, orderedNodes, ordering, args);
1636
+ });
1637
+
1638
+ // Return the accepted handle plus the plan so the coordinator sees the target set.
1639
+ return {
1640
+ ...handle,
1641
+ order,
1642
+ orderingRationale: planRecord.orderingRationale,
1643
+ plan: planRecord.plan,
1644
+ note: 'Batch convergence accepted and running in the background. Completion/failure (with per-node results) will be delivered as a terminal refine event; do not poll repeatedly.',
1645
+ };
1646
+ }
1647
+
1648
+ export async function finishMeshRefineJob(self: DaemonCommandRouter, handle: MeshRefineJobHandle, args: any): Promise<void> {
1649
+ const key = buildRefineJobKey(self, handle.meshId, handle.targetNodeId);
1650
+ let result: Record<string, unknown>;
1651
+ try {
1652
+ result = await executeMeshRefineNodeSynchronously(self, handle.meshId, handle.targetNodeId, args) as Record<string, unknown>;
1653
+ } catch (e: any) {
1654
+ result = { success: false, error: e?.message || String(e) };
1655
+ }
1656
+ const completedAt = new Date().toISOString();
1657
+
1658
+ // B1: Discriminated terminal status — do not rely solely on result.success.
1659
+ // Map known failure codes to structured terminal kinds.
1660
+ type RefineTerminalKind = 'completed' | 'blocked_review' | 'validation_failed' | 'submodule_reachability_failed' | 'merge_failed' | 'cleanup_failed';
1661
+ const refineCode = typeof result.code === 'string' ? result.code : '';
1662
+ const refineTerminalKind: RefineTerminalKind = result.success === true
1663
+ ? 'completed'
1664
+ : refineCode === 'blocked_review'
1665
+ ? 'blocked_review'
1666
+ : refineCode === 'validation_failed' || refineCode === 'validation_dependencies_missing'
1667
+ ? 'validation_failed'
1668
+ : refineCode === 'submodule_reachability_failed'
1669
+ ? 'submodule_reachability_failed'
1670
+ : refineCode === 'merge_failed' || refineCode === 'patch_equivalence_failed' || refineCode === 'needs_rebase' || refineCode === 'needs_rebase_with_conflicts'
1671
+ ? 'merge_failed'
1672
+ : refineCode === 'cleanup_failed'
1673
+ ? 'cleanup_failed'
1674
+ : 'merge_failed'; // fallback for unclassified failures
1675
+ const isTerminalSuccess = refineTerminalKind === 'completed';
1676
+
1677
+ // Build structured blocker context for task_failed ledger entries so coordinators
1678
+ // can inspect the failure cause without parsing free-form error strings.
1679
+ const blockerContext: Record<string, unknown> | undefined = isTerminalSuccess ? undefined : (() => {
1680
+ const code = typeof result.code === 'string' ? result.code : refineTerminalKind;
1681
+ const stage = refineTerminalKind === 'validation_failed' ? 'validation'
1682
+ : refineTerminalKind === 'submodule_reachability_failed' ? 'submodule_reachability'
1683
+ : refineCode === 'patch_equivalence_failed' ? 'patch_equivalence'
1684
+ : refineCode === 'needs_rebase' || refineCode === 'needs_rebase_with_conflicts' ? 'patch_equivalence'
1685
+ : refineTerminalKind === 'merge_failed' ? 'merge'
1686
+ : refineTerminalKind === 'cleanup_failed' ? 'cleanup'
1687
+ : 'unknown';
1688
+ const ctx: Record<string, unknown> = {
1689
+ stage,
1690
+ reason: code,
1691
+ terminalKind: refineTerminalKind,
1692
+ };
1693
+ if (typeof result.error === 'string') ctx.error = result.error;
1694
+ if (typeof result.blockedReason === 'string') ctx.blockedReason = result.blockedReason;
1695
+ // Patch equivalence details
1696
+ if (stage === 'patch_equivalence' && result.patchEquivalence) {
1697
+ const pe = result.patchEquivalence as Record<string, unknown>;
1698
+ ctx.details = {
1699
+ expectedPatchId: pe.expectedPatchId,
1700
+ actualPatchId: pe.actualPatchId,
1701
+ status: pe.status,
1702
+ actionableHint: pe.actionableHint,
1703
+ error: pe.error,
1704
+ };
1705
+ }
1706
+ // Submodule reachability details
1707
+ if (stage === 'submodule_reachability' && Array.isArray(result.unreachableSubmoduleCommits)) {
1708
+ ctx.details = {
1709
+ unreachableCount: (result.unreachableSubmoduleCommits as unknown[]).length,
1710
+ paths: (result.unreachableSubmoduleCommits as Array<Record<string, unknown>>).map(e => e.path),
1711
+ autoPublishAllowed: (result.unreachableSubmoduleCommits as Array<Record<string, unknown>>)[0]?.autoPublishAllowed,
1712
+ };
1713
+ }
1714
+ // Validation details
1715
+ if (stage === 'validation' && result.validationSummary) {
1716
+ const vs = result.validationSummary as Record<string, unknown>;
1717
+ ctx.details = {
1718
+ failureCode: vs.failureCode,
1719
+ commandsRun: Array.isArray(vs.commandsRun) ? vs.commandsRun.length : undefined,
1720
+ };
1721
+ }
1722
+ return ctx;
1723
+ })();
1724
+
1725
+ const normalizedResult = {
1726
+ ...result,
1727
+ terminalKind: refineTerminalKind,
1728
+ ...(blockerContext ? { blockerContext } : {}),
1729
+ ...(result.nextStep === undefined && !isTerminalSuccess ? {
1730
+ nextStep: refineTerminalKind === 'blocked_review'
1731
+ ? 'Request user review/approval before attempting to merge again.'
1732
+ : refineTerminalKind === 'validation_failed'
1733
+ ? 'Fix failing tests or configure validation.bootstrapCommands and retry mesh_refine_node.'
1734
+ : refineTerminalKind === 'submodule_reachability_failed'
1735
+ ? 'Push unreachable submodule commits to origin/main, then retry mesh_refine_node.'
1736
+ : refineTerminalKind === 'merge_failed'
1737
+ ? 'Resolve merge conflicts or patch equivalence issues, then retry mesh_refine_node.'
1738
+ : refineTerminalKind === 'cleanup_failed'
1739
+ ? 'Manually remove the worktree and retry or use mesh_remove_node.'
1740
+ : 'Inspect refineStages for the failing stage and retry.',
1741
+ } : {}),
1742
+ };
1743
+
1744
+ const terminalHandle = buildRefineJobHandle(self, {
1745
+ meshId: handle.meshId,
1746
+ nodeId: handle.targetNodeId,
1747
+ status: isTerminalSuccess ? 'completed' : 'failed',
1748
+ startedAt: handle.startedAt,
1749
+ completedAt,
1750
+ jobId: handle.jobId,
1751
+ interactionId: handle.interactionId,
1752
+ retryOfJobId: handle.retryOfJobId,
1753
+ node: { daemonId: handle.targetDaemonId, workspace: handle.workspace },
1754
+ coordinatorDaemonId: handle.targetCoordinatorDaemonId,
1755
+ });
1756
+ const terminal: MeshRefineTerminalJob = { ...terminalHandle, result: normalizedResult };
1757
+ self.terminalRefineJobs.set(key, terminal);
1758
+ self.runningRefineJobs.delete(key);
1759
+ self.invalidateAggregateMeshStatus(handle.meshId);
1760
+ await appendRefineJobLedger(self, isTerminalSuccess ? 'task_completed' : 'task_failed', terminalHandle, normalizedResult);
1761
+ queueRefineJobEvent(self, isTerminalSuccess ? 'refine:completed' : 'refine:failed', terminalHandle, normalizedResult);
1762
+ }
1763
+
1764
+ export async function startMeshRefineJob(self: DaemonCommandRouter, meshId: string, nodeId: string, args: any): Promise<CommandRouterResult> {
1765
+ const key = buildRefineJobKey(self, meshId, nodeId);
1766
+ const running = self.runningRefineJobs.get(key);
1767
+ if (running) return { ...running, duplicate: true };
1768
+ const terminal = self.terminalRefineJobs.get(key);
1769
+
1770
+ // preferInline so inline-cache-only clone worktree nodes resolve — same
1771
+ // membership authority as clone_mesh_node / get_mesh. Without it refine reads
1772
+ // config-first and misses nodes that only live in the inline cache.
1773
+ const meshRecord = await self.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
1774
+ const mesh = meshRecord?.mesh;
1775
+ const node = mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
1776
+ if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh` };
1777
+ if (!node.isLocalWorktree || !node.workspace) return { success: false, error: `Refinery requires a local worktree node` };
1778
+
1779
+ // Capture the caller's coordinator daemon ID so completed/failed events are
1780
+ // scoped to that coordinator's pending-events queue and survive daemon restarts.
1781
+ const coordinatorDaemonId = typeof args?.coordinatorDaemonId === 'string' && args.coordinatorDaemonId.trim()
1782
+ ? args.coordinatorDaemonId.trim()
1783
+ : (self.deps.statusInstanceId || undefined);
1784
+ const handle = buildRefineJobHandle(self, { meshId, nodeId, node, retryOfJobId: terminal?.jobId, coordinatorDaemonId });
1785
+ self.runningRefineJobs.set(key, handle);
1786
+ await appendRefineJobLedger(self, 'task_dispatched', handle);
1787
+ queueRefineJobEvent(self, 'refine:accepted', handle);
1788
+
1789
+ setImmediate(() => {
1790
+ void finishMeshRefineJob(self, handle, args);
1791
+ });
1792
+
1793
+ return handle;
1794
+ }