@adhdev/daemon-core 0.9.82-rc.48 → 0.9.82-rc.481

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