@adhdev/daemon-core 0.9.82-rc.46 → 0.9.82-rc.460

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 (439) 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 +200 -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 +7 -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 +25 -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-refine.d.ts +137 -0
  56. package/dist/commands/router-worktree-cleanup.d.ts +133 -0
  57. package/dist/commands/router.d.ts +241 -12
  58. package/dist/commands/upgrade-helper.d.ts +41 -1
  59. package/dist/config/chat-history.d.ts +9 -0
  60. package/dist/config/config.d.ts +19 -0
  61. package/dist/config/mesh-config.d.ts +129 -1
  62. package/dist/config/mesh-json-config.d.ts +199 -0
  63. package/dist/config/registry-resolver.d.ts +54 -0
  64. package/dist/config/repo-settings.d.ts +77 -0
  65. package/dist/daemon/dev-server.d.ts +0 -2
  66. package/dist/detection/ide-detector.d.ts +13 -0
  67. package/dist/detection/win32-ide-version.d.ts +37 -0
  68. package/dist/git/change-impact-config.d.ts +159 -0
  69. package/dist/git/git-commands.d.ts +12 -1
  70. package/dist/git/git-diff.d.ts +6 -0
  71. package/dist/git/git-executor.d.ts +11 -0
  72. package/dist/git/git-status.d.ts +57 -0
  73. package/dist/git/git-types.d.ts +2 -50
  74. package/dist/git/git-worktree.d.ts +71 -1
  75. package/dist/git/index.d.ts +3 -1
  76. package/dist/index.d.ts +67 -15
  77. package/dist/index.js +57943 -25340
  78. package/dist/index.js.map +1 -1
  79. package/dist/index.mjs +57611 -25174
  80. package/dist/index.mjs.map +1 -1
  81. package/dist/installer.d.ts +1 -4
  82. package/dist/ipc/local-ipc-server.d.ts +91 -0
  83. package/dist/launch.d.ts +1 -1
  84. package/dist/logging/async-batch-writer.d.ts +10 -0
  85. package/dist/logging/debug-config.d.ts +16 -0
  86. package/dist/logging/log-redactor.d.ts +24 -0
  87. package/dist/logging/log-tail-reader.d.ts +81 -0
  88. package/dist/logging/logger.d.ts +1 -1
  89. package/dist/mesh/contracts.d.ts +230 -0
  90. package/dist/mesh/coordinator-prompt.d.ts +76 -0
  91. package/dist/mesh/coordinator-registry.d.ts +59 -0
  92. package/dist/mesh/mesh-active-work.d.ts +174 -0
  93. package/dist/mesh/mesh-clone-grace.d.ts +8 -0
  94. package/dist/mesh/mesh-coordinator-config.d.ts +21 -0
  95. package/dist/mesh/mesh-delivery-policy.d.ts +131 -0
  96. package/dist/mesh/mesh-event-classify.d.ts +7 -0
  97. package/dist/mesh/mesh-event-forwarding.d.ts +41 -0
  98. package/dist/mesh/mesh-event-trace.d.ts +21 -0
  99. package/dist/mesh/mesh-events-coordinator.d.ts +4 -0
  100. package/dist/mesh/mesh-events-pending.d.ts +113 -0
  101. package/dist/mesh/mesh-events-stale.d.ts +61 -0
  102. package/dist/mesh/mesh-events-utils.d.ts +75 -0
  103. package/dist/mesh/mesh-events.d.ts +6 -49
  104. package/dist/mesh/mesh-fast-forward.d.ts +81 -0
  105. package/dist/mesh/mesh-host-ownership.d.ts +29 -0
  106. package/dist/mesh/mesh-init.d.ts +128 -0
  107. package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -4
  108. package/dist/mesh/mesh-ledger.d.ts +133 -1
  109. package/dist/mesh/mesh-magi-status.d.ts +63 -0
  110. package/dist/mesh/mesh-missions.d.ts +189 -0
  111. package/dist/mesh/mesh-node-identity.d.ts +306 -0
  112. package/dist/mesh/mesh-queue-assignment.d.ts +158 -0
  113. package/dist/mesh/mesh-reconcile-loop.d.ts +80 -0
  114. package/dist/mesh/mesh-refine-batch.d.ts +68 -0
  115. package/dist/mesh/mesh-refine-gates.d.ts +428 -0
  116. package/dist/mesh/mesh-refine-status.d.ts +62 -0
  117. package/dist/mesh/mesh-review-inbox.d.ts +90 -0
  118. package/dist/mesh/mesh-routing.d.ts +70 -0
  119. package/dist/mesh/mesh-runtime-store.d.ts +476 -0
  120. package/dist/mesh/mesh-scheduling-runtime.d.ts +78 -0
  121. package/dist/mesh/mesh-task-inflight.d.ts +46 -0
  122. package/dist/mesh/mesh-task-stats.d.ts +49 -0
  123. package/dist/mesh/mesh-unresolved-forward-outbox.d.ts +30 -0
  124. package/dist/mesh/mesh-warmup-deadline.d.ts +68 -0
  125. package/dist/mesh/mesh-work-queue.d.ts +318 -5
  126. package/dist/mesh/preview-freshness.d.ts +28 -0
  127. package/dist/mesh/refine-config.d.ts +216 -0
  128. package/dist/mesh/worktree-bootstrap-config.d.ts +219 -0
  129. package/dist/providers/acp-provider-instance.d.ts +5 -0
  130. package/dist/providers/approval-utils.d.ts +45 -0
  131. package/dist/providers/chat-message-normalization.d.ts +58 -1
  132. package/dist/providers/cli-provider-instance.d.ts +290 -3
  133. package/dist/providers/contracts.d.ts +149 -6
  134. package/dist/providers/external-sources.d.ts +71 -0
  135. package/dist/providers/manual-attendance.d.ts +79 -0
  136. package/dist/providers/native-history/antigravity-claim-registry.d.ts +28 -0
  137. package/dist/providers/native-history/antigravity-cli-transcript.d.ts +137 -0
  138. package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
  139. package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
  140. package/dist/providers/native-history/constants.d.ts +12 -0
  141. package/dist/providers/native-history/dispatcher.d.ts +31 -0
  142. package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
  143. package/dist/providers/native-history/index.d.ts +13 -0
  144. package/dist/providers/provider-instance-manager.d.ts +29 -0
  145. package/dist/providers/provider-instance.d.ts +30 -1
  146. package/dist/providers/provider-loader.d.ts +42 -5
  147. package/dist/providers/provider-trust.d.ts +31 -0
  148. package/dist/providers/read-chat-contract.d.ts +29 -0
  149. package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
  150. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +86 -0
  151. package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
  152. package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
  153. package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
  154. package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
  155. package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
  156. package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
  157. package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
  158. package/dist/providers/sdk/v1/index.d.ts +30 -0
  159. package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
  160. package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
  161. package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
  162. package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
  163. package/dist/providers/sdk/v1/types/common/index.d.ts +203 -0
  164. package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
  165. package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
  166. package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
  167. package/dist/providers/spec/adapter.d.ts +98 -0
  168. package/dist/providers/spec/cli-adapter.d.ts +223 -0
  169. package/dist/providers/spec/evaluator.d.ts +45 -0
  170. package/dist/providers/spec/fsm-driver.d.ts +449 -0
  171. package/dist/providers/spec/fsm-evaluator.d.ts +93 -0
  172. package/dist/providers/spec/fsm-loader.d.ts +14 -0
  173. package/dist/providers/spec/fsm-types.d.ts +202 -0
  174. package/dist/providers/spec/native-history-executor.d.ts +41 -0
  175. package/dist/providers/spec/pre-launch-trust.d.ts +16 -0
  176. package/dist/providers/spec/route.d.ts +4 -0
  177. package/dist/providers/spec/types.d.ts +222 -0
  178. package/dist/providers/status-monitor.d.ts +7 -7
  179. package/dist/providers/transcript-v2.d.ts +176 -0
  180. package/dist/providers/types/interactive-prompt.d.ts +84 -0
  181. package/dist/providers/working-dir.d.ts +17 -0
  182. package/dist/repo-mesh-types.d.ts +519 -13
  183. package/dist/runtime-defaults.d.ts +2 -0
  184. package/dist/session-host/managed-host.d.ts +64 -0
  185. package/dist/session-host/runtime-surface.d.ts +10 -16
  186. package/dist/sessions/registry.d.ts +6 -0
  187. package/dist/shared-types-extra.d.ts +2 -4
  188. package/dist/shared-types.d.ts +86 -55
  189. package/dist/status/normalize.d.ts +1 -1
  190. package/dist/status/normalize.js +1 -0
  191. package/dist/status/normalize.js.map +1 -1
  192. package/dist/status/normalize.mjs +1 -0
  193. package/dist/status/normalize.mjs.map +1 -1
  194. package/dist/status/reporter.d.ts +2 -0
  195. package/dist/status/snapshot.d.ts +26 -0
  196. package/dist/system/hash.d.ts +8 -0
  197. package/dist/system/load-better-sqlite3.d.ts +21 -0
  198. package/dist/types.d.ts +5 -0
  199. package/package.json +11 -4
  200. package/src/agent-stream/poller.ts +2 -3
  201. package/src/agent-stream/provider-adapter.ts +1 -1
  202. package/src/boot/daemon-lifecycle.ts +65 -12
  203. package/src/boot/process-hardening.ts +89 -0
  204. package/src/build-info.ts +73 -0
  205. package/src/chat/source-machine.ts +534 -0
  206. package/src/chat/source-resolver.ts +0 -0
  207. package/src/chat/subscription-updates.ts +20 -1
  208. package/src/cli-adapter-types.d.ts +3 -1
  209. package/src/cli-adapter-types.ts +68 -2
  210. package/src/cli-adapters/cli-script-runner.ts +421 -0
  211. package/src/cli-adapters/cli-state-engine.ts +1246 -0
  212. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  213. package/src/cli-adapters/provider-cli-adapter.ts +914 -1144
  214. package/src/cli-adapters/provider-cli-parse.d.ts +2 -0
  215. package/src/cli-adapters/provider-cli-parse.ts +19 -0
  216. package/src/cli-adapters/provider-cli-runtime.ts +56 -7
  217. package/src/cli-adapters/provider-cli-shared.d.ts +4 -0
  218. package/src/cli-adapters/provider-cli-shared.ts +121 -11
  219. package/src/cli-adapters/pty-transport.ts +2 -1
  220. package/src/cli-adapters/pty-write-chunking.ts +106 -0
  221. package/src/cli-adapters/raw-terminal-io.ts +252 -0
  222. package/src/cli-adapters/resolve-executable.ts +204 -0
  223. package/src/cli-adapters/session-host-transport.ts +2 -1
  224. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +63 -29
  225. package/src/cli-adapters/terminal-backends/types.ts +10 -3
  226. package/src/cli-adapters/terminal-screen.ts +16 -81
  227. package/src/commands/chat-commands-debug-bundle.ts +398 -0
  228. package/src/commands/chat-commands-read.ts +2488 -0
  229. package/src/commands/chat-commands-scope.ts +54 -0
  230. package/src/commands/chat-commands-shared.ts +114 -0
  231. package/src/commands/chat-commands-write.ts +891 -0
  232. package/src/commands/chat-commands.ts +19 -1841
  233. package/src/commands/cli-manager.ts +596 -27
  234. package/src/commands/handler.ts +845 -2
  235. package/src/commands/high-family/index.ts +28 -0
  236. package/src/commands/high-family/mesh-coordinator-launch.ts +680 -0
  237. package/src/commands/high-family/mesh-events.ts +89 -0
  238. package/src/commands/high-family/mesh-status.ts +822 -0
  239. package/src/commands/high-family/types.ts +80 -0
  240. package/src/commands/low-family/coordinator-prompt.ts +72 -0
  241. package/src/commands/low-family/daemon-lifecycle.ts +120 -0
  242. package/src/commands/low-family/diagnostics.ts +57 -0
  243. package/src/commands/low-family/index.ts +37 -0
  244. package/src/commands/low-family/mesh-ledger.ts +62 -0
  245. package/src/commands/low-family/mesh-node-logs.ts +87 -0
  246. package/src/commands/low-family/notification.ts +116 -0
  247. package/src/commands/low-family/refine-config.ts +106 -0
  248. package/src/commands/low-family/session-host.ts +274 -0
  249. package/src/commands/low-family/spec-providerdev.ts +217 -0
  250. package/src/commands/low-family/status-meta.ts +112 -0
  251. package/src/commands/low-family/types.ts +39 -0
  252. package/src/commands/med-family/cli-agent.ts +241 -0
  253. package/src/commands/med-family/fast-forward.ts +230 -0
  254. package/src/commands/med-family/ide.ts +163 -0
  255. package/src/commands/med-family/index.ts +37 -0
  256. package/src/commands/med-family/mesh-crud.ts +1287 -0
  257. package/src/commands/med-family/mesh-host-pairing.ts +234 -0
  258. package/src/commands/med-family/mesh-queue.ts +167 -0
  259. package/src/commands/med-family/mesh-restart.ts +92 -0
  260. package/src/commands/med-family/types.ts +153 -0
  261. package/src/commands/mesh-coordinator.ts +334 -124
  262. package/src/commands/router-refine.ts +1711 -0
  263. package/src/commands/router-worktree-cleanup.ts +870 -0
  264. package/src/commands/router.ts +1025 -4070
  265. package/src/commands/stream-commands.ts +8 -0
  266. package/src/commands/upgrade-helper.ts +310 -45
  267. package/src/config/chat-history.ts +493 -24
  268. package/src/config/config.ts +30 -0
  269. package/src/config/mesh-config.ts +619 -23
  270. package/src/config/mesh-json-config.ts +376 -0
  271. package/src/config/recent-activity.ts +8 -2
  272. package/src/config/registry-resolver.ts +100 -0
  273. package/src/config/repo-settings.ts +111 -0
  274. package/src/daemon/dev-auto-implement.ts +3 -2
  275. package/src/daemon/dev-cli-debug.ts +10 -1
  276. package/src/daemon/dev-server.ts +0 -541
  277. package/src/detection/cli-detector.ts +28 -9
  278. package/src/detection/ide-detector.ts +55 -16
  279. package/src/detection/win32-ide-version.ts +106 -0
  280. package/src/git/change-impact-config.ts +354 -0
  281. package/src/git/git-commands.ts +78 -15
  282. package/src/git/git-diff.ts +81 -11
  283. package/src/git/git-executor.ts +12 -0
  284. package/src/git/git-status.ts +767 -48
  285. package/src/git/git-types.ts +14 -62
  286. package/src/git/git-worktree.ts +261 -4
  287. package/src/git/index.ts +17 -0
  288. package/src/index.ts +200 -12
  289. package/src/installer.d.ts +1 -1
  290. package/src/installer.ts +8 -6
  291. package/src/ipc/local-ipc-server.ts +278 -0
  292. package/src/launch.d.ts +1 -1
  293. package/src/launch.ts +37 -28
  294. package/src/logging/async-batch-writer.ts +55 -0
  295. package/src/logging/command-log.ts +7 -5
  296. package/src/logging/debug-config.ts +25 -0
  297. package/src/logging/debug-trace.ts +7 -2
  298. package/src/logging/log-redactor.ts +100 -0
  299. package/src/logging/log-tail-reader.ts +341 -0
  300. package/src/logging/logger.ts +14 -7
  301. package/src/mesh/contracts.ts +469 -0
  302. package/src/mesh/coordinator-prompt.ts +536 -34
  303. package/src/mesh/coordinator-registry.ts +121 -0
  304. package/src/mesh/mesh-active-work.ts +656 -0
  305. package/src/mesh/mesh-clone-grace.ts +68 -0
  306. package/src/mesh/mesh-coordinator-config.ts +97 -0
  307. package/src/mesh/mesh-delivery-policy.ts +315 -0
  308. package/src/mesh/mesh-event-classify.ts +73 -0
  309. package/src/mesh/mesh-event-forwarding.ts +1932 -0
  310. package/src/mesh/mesh-event-trace.ts +67 -0
  311. package/src/mesh/mesh-events-coordinator.ts +32 -0
  312. package/src/mesh/mesh-events-pending.ts +766 -0
  313. package/src/mesh/mesh-events-stale.ts +440 -0
  314. package/src/mesh/mesh-events-utils.ts +443 -0
  315. package/src/mesh/mesh-events.ts +33 -1035
  316. package/src/mesh/mesh-fast-forward.ts +856 -0
  317. package/src/mesh/mesh-host-ownership.ts +111 -0
  318. package/src/mesh/mesh-init.ts +350 -0
  319. package/src/mesh/mesh-ledger-reconciliation.ts +12 -7
  320. package/src/mesh/mesh-ledger.ts +873 -102
  321. package/src/mesh/mesh-magi-status.ts +223 -0
  322. package/src/mesh/mesh-missions.ts +449 -0
  323. package/src/mesh/mesh-node-identity.ts +1994 -0
  324. package/src/mesh/mesh-queue-assignment.ts +2233 -0
  325. package/src/mesh/mesh-reconcile-loop.ts +2390 -0
  326. package/src/mesh/mesh-refine-batch.ts +205 -0
  327. package/src/mesh/mesh-refine-gates.ts +1686 -0
  328. package/src/mesh/mesh-refine-status.ts +231 -0
  329. package/src/mesh/mesh-review-inbox.ts +308 -0
  330. package/src/mesh/mesh-routing.ts +291 -0
  331. package/src/mesh/mesh-runtime-store.ts +2026 -0
  332. package/src/mesh/mesh-scheduling-runtime.ts +198 -0
  333. package/src/mesh/mesh-task-inflight.ts +70 -0
  334. package/src/mesh/mesh-task-stats.ts +161 -0
  335. package/src/mesh/mesh-unresolved-forward-outbox.ts +185 -0
  336. package/src/mesh/mesh-warmup-deadline.ts +152 -0
  337. package/src/mesh/mesh-work-queue.ts +1281 -141
  338. package/src/mesh/preview-freshness.ts +163 -0
  339. package/src/mesh/refine-config.ts +423 -0
  340. package/src/mesh/worktree-bootstrap-config.ts +599 -0
  341. package/src/providers/acp-provider-instance.ts +43 -10
  342. package/src/providers/approval-utils.d.ts +5 -0
  343. package/src/providers/approval-utils.ts +99 -5
  344. package/src/providers/chat-message-normalization.ts +126 -4
  345. package/src/providers/cli-provider-instance.ts +2456 -107
  346. package/src/providers/contracts.d.ts +55 -0
  347. package/src/providers/contracts.ts +160 -6
  348. package/src/providers/extension-provider-instance.ts +12 -7
  349. package/src/providers/external-sources.ts +218 -0
  350. package/src/providers/ide-provider-instance.ts +35 -12
  351. package/src/providers/manual-attendance.ts +105 -0
  352. package/src/providers/native-history/antigravity-claim-registry.ts +131 -0
  353. package/src/providers/native-history/antigravity-cli-transcript.ts +1193 -0
  354. package/src/providers/native-history/claude-cli-transcript.ts +396 -0
  355. package/src/providers/native-history/codex-cli-transcript.ts +479 -0
  356. package/src/providers/native-history/constants.ts +19 -0
  357. package/src/providers/native-history/dispatcher.ts +503 -0
  358. package/src/providers/native-history/hermes-cli-transcript.ts +260 -0
  359. package/src/providers/native-history/index.ts +30 -0
  360. package/src/providers/provider-instance-manager.ts +71 -0
  361. package/src/providers/provider-instance.ts +24 -1
  362. package/src/providers/provider-loader.ts +700 -55
  363. package/src/providers/provider-schema.ts +87 -14
  364. package/src/providers/provider-trust.ts +114 -0
  365. package/src/providers/read-chat-contract.ts +76 -16
  366. package/src/providers/sdk/README.md +49 -0
  367. package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
  368. package/src/providers/sdk/v1/builders/cli/detect-status.ts +313 -0
  369. package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
  370. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +261 -0
  371. package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
  372. package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
  373. package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
  374. package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
  375. package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
  376. package/src/providers/sdk/v1/index.ts +152 -0
  377. package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
  378. package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
  379. package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
  380. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +509 -0
  381. package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
  382. package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
  383. package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
  384. package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
  385. package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
  386. package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
  387. package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
  388. package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
  389. package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
  390. package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
  391. package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
  392. package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
  393. package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
  394. package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
  395. package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
  396. package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
  397. package/src/providers/sdk/v1/types/cli/index.ts +365 -0
  398. package/src/providers/sdk/v1/types/common/index.ts +229 -0
  399. package/src/providers/sdk/v1/validators/index.ts +19 -0
  400. package/src/providers/sdk/v1/validators/manifest.ts +110 -0
  401. package/src/providers/sdk/v1/validators/taint.ts +309 -0
  402. package/src/providers/spec/adapter.ts +246 -0
  403. package/src/providers/spec/cli-adapter.ts +1064 -0
  404. package/src/providers/spec/evaluator.ts +407 -0
  405. package/src/providers/spec/fsm-driver.ts +1528 -0
  406. package/src/providers/spec/fsm-evaluator.ts +290 -0
  407. package/src/providers/spec/fsm-loader.ts +128 -0
  408. package/src/providers/spec/fsm-types.ts +301 -0
  409. package/src/providers/spec/native-history-executor.ts +1174 -0
  410. package/src/providers/spec/pre-launch-trust.ts +104 -0
  411. package/src/providers/spec/route.ts +51 -0
  412. package/src/providers/spec/types.ts +262 -0
  413. package/src/providers/status-monitor.d.ts +7 -7
  414. package/src/providers/status-monitor.ts +37 -22
  415. package/src/providers/transcript-v2.ts +567 -0
  416. package/src/providers/types/interactive-prompt.ts +536 -0
  417. package/src/providers/version-archive.ts +64 -24
  418. package/src/providers/working-dir.ts +23 -0
  419. package/src/repo-mesh-types.ts +758 -14
  420. package/src/runtime-defaults.ts +39 -0
  421. package/src/session-host/managed-host.ts +218 -0
  422. package/src/session-host/runtime-surface.ts +20 -80
  423. package/src/sessions/registry.ts +6 -0
  424. package/src/shared-types-extra.ts +2 -4
  425. package/src/shared-types.d.ts +8 -0
  426. package/src/shared-types.ts +124 -55
  427. package/src/status/builders.ts +26 -6
  428. package/src/status/normalize.ts +2 -0
  429. package/src/status/reporter.ts +19 -1
  430. package/src/status/snapshot.ts +95 -26
  431. package/src/system/hash.ts +23 -0
  432. package/src/system/host-memory.ts +29 -12
  433. package/src/system/load-better-sqlite3.ts +68 -0
  434. package/src/types.ts +5 -0
  435. package/dist/cli-adapters/terminal-backends/xterm-backend.d.ts +0 -17
  436. package/dist/mesh/mesh-sync.d.ts +0 -53
  437. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +0 -16
  438. package/src/cli-adapters/terminal-backends/xterm-backend.ts +0 -97
  439. package/src/mesh/mesh-sync.ts +0 -111
@@ -0,0 +1,1686 @@
1
+ /**
2
+ * Mesh refine validation gates & gitlink fast-forward evaluation
3
+ *
4
+ * Extracted from commands/router.ts (behavior-preserving move). Contains:
5
+ * - the MeshCoordinator config-format type
6
+ * - refine validation / patch-equivalence / effective-diff / submodule
7
+ * reachability gates and their summary types + job handles
8
+ * - gitlink trivial-fast-forward evaluation and submodule alignment helpers
9
+ *
10
+ * router.ts re-exports every public symbol from here so existing import paths
11
+ * keep working. `CommandRouterResult` is imported type-only from router.ts
12
+ * (erased at compile time — no runtime import cycle).
13
+ */
14
+
15
+ import { getGitRepoStatus } from '../git/git-status.js';
16
+ import * as yaml from 'js-yaml';
17
+ import { loadMeshRefineConfig, resolveMeshRefineValidationPlan } from '../mesh/refine-config.js';
18
+ import type { MeshRefineValidationCommandPlan } from '../mesh/refine-config.js';
19
+ import { evaluateWorktreeBootstrapState, loadMeshWorktreeBootstrapConfig, runMeshWorktreeBootstrap, resolveSubmoduleDefaultBranch } from '../mesh/worktree-bootstrap-config.js';
20
+ import type { WorktreeBootstrapState } from '../mesh/worktree-bootstrap-config.js';
21
+ import { basename as pathBasename, join as pathJoin, resolve as pathResolve } from 'path';
22
+ import * as fs from 'fs';
23
+ import { execFileSync } from 'node:child_process';
24
+ import { resolveWin32Executable, buildWin32ExecFileSpawn } from '../cli-adapters/resolve-executable.js';
25
+ import type { CommandRouterResult } from '../commands/router.js';
26
+
27
+ // Fix (4): resolve the git executable to an absolute path once on win32. A bare `git` handed to
28
+ // execFile(Sync)/execFileAsync is resolved by libuv's spawn search, which appends only .com/.exe
29
+ // (no PATHEXT) over the inherited PATH — so a `git.cmd`/`git.exe` that `where` finds is missed and
30
+ // the spawn ENOENTs (the live win32 refine/batch failure). resolveWin32Executable is the same helper
31
+ // the validation/bootstrap spawn path already uses. No-op on non-win32 (returns 'git' verbatim), and
32
+ // no shell:true is used anywhere here so there is no quoting risk.
33
+ const GIT = process.platform === 'win32' ? resolveWin32Executable('git') : 'git';
34
+
35
+ export type MeshCoordinatorConfigFormat = 'claude_mcp_json' | 'hermes_config_yaml';
36
+ type MeshRefineValidationStatus = 'passed' | 'failed' | 'skipped';
37
+ type MeshRefineValidationCommand = MeshRefineValidationCommandPlan;
38
+
39
+ type MeshRefineValidationSummary = {
40
+ status: MeshRefineValidationStatus;
41
+ required: true;
42
+ commandsRun: Array<Record<string, unknown>>;
43
+ bootstrapCommandsRun: Array<Record<string, unknown>>;
44
+ rejectedCommands: Array<Record<string, unknown>>;
45
+ skippedReason?: string;
46
+ failureKind?: string;
47
+ failureCode?: string;
48
+ /** Human-readable cause when failureKind === 'spawn_resolution_failed' (win32 .cmd shim, etc). */
49
+ spawnResolutionError?: string;
50
+ timeoutMs: number;
51
+ outputLimitBytes: number;
52
+ configSource?: string;
53
+ configSourceType?: string;
54
+ suggestions?: unknown[];
55
+ suggestedConfig?: unknown;
56
+ /**
57
+ * M2-3: the bootstrap stage recorded separately from validation so review
58
+ * surfaces can distinguish environment failures from validation failures.
59
+ * cached — worktree_bootstrap was 'ready' (staleInputs unchanged), skipped
60
+ * ran — worktree_bootstrap was stale/never-ran and re-ran successfully
61
+ * failed — bootstrap run failed (refine stops before validation)
62
+ * skipped — refine config validation.bootstrap === 'skip'
63
+ * legacy — deprecated validation.bootstrapCommands path was used
64
+ * not_configured — no bootstrap definition anywhere
65
+ */
66
+ bootstrap?: {
67
+ stage: 'cached' | 'ran' | 'failed' | 'skipped' | 'legacy' | 'not_configured';
68
+ status?: string;
69
+ skipped?: boolean;
70
+ configSource?: string;
71
+ staleReason?: string;
72
+ error?: string;
73
+ commandsRun?: Array<Record<string, unknown>>;
74
+ };
75
+ /** M2-2: deprecation notices from the refine config (e.g. bootstrapCommands). */
76
+ deprecationWarnings?: string[];
77
+ };
78
+
79
+ type MeshRefineStageStatus = 'passed' | 'failed' | 'skipped';
80
+
81
+ type MeshRefinePatchEquivalenceSummary = {
82
+ status: MeshRefineStageStatus;
83
+ equivalent: boolean;
84
+ baseHead: string;
85
+ branchHead: string;
86
+ mergeBase?: string;
87
+ mergedTree?: string;
88
+ expectedPatchId?: string;
89
+ actualPatchId?: string;
90
+ durationMs: number;
91
+ error?: string;
92
+ stdout?: string;
93
+ stderr?: string;
94
+ actionableHint?: MeshRefineSubmoduleConflictHint;
95
+ /**
96
+ * Set when a `merge-tree` submodule conflict was reclassified as a trivial
97
+ * gitlink fast-forward and the gate passed via a synthesized merge tree.
98
+ */
99
+ gitlinkTrivialFastForward?: {
100
+ resolved: boolean;
101
+ gitlinks: Array<{ path: string; baseCommit?: string; branchCommit?: string; fastForward: boolean }>;
102
+ reason?: string;
103
+ };
104
+ };
105
+
106
+ type MeshRefineEffectiveDiffSummary = {
107
+ status: MeshRefineStageStatus;
108
+ /** True when there is at least one root-tree change between base and branch (incl. gitlink bumps). */
109
+ hasEffectiveDiff: boolean;
110
+ baseHead: string;
111
+ branchHead: string;
112
+ /** Root-level paths that differ between base and branch (capped). */
113
+ changedPaths?: string[];
114
+ /** Submodule paths with uncommitted/divergent commits but NO committed gitlink bump in the root tree. */
115
+ submoduleHints?: Array<{ path: string; reason: string }>;
116
+ durationMs: number;
117
+ error?: string;
118
+ stdout?: string;
119
+ stderr?: string;
120
+ };
121
+
122
+ type MeshRefineSubmoduleConflictHint = {
123
+ kind: 'submodule_conflict';
124
+ message: string;
125
+ conflicts: Array<{
126
+ path: string;
127
+ baseCommit?: string;
128
+ branchCommit?: string;
129
+ }>;
130
+ nextSteps: string[];
131
+ };
132
+
133
+ type MeshRefineSubmoduleAlignmentSummary = {
134
+ status: 'passed' | 'failed' | 'skipped';
135
+ changedGitlinkPaths: string[];
136
+ outOfSyncPaths: string[];
137
+ updatedPaths: string[];
138
+ verifiedPaths: string[];
139
+ durationMs: number;
140
+ reason?: string;
141
+ command?: string;
142
+ error?: string;
143
+ stdout?: string;
144
+ stderr?: string;
145
+ };
146
+
147
+ type MeshRefineSubmoduleReachabilityEntry = {
148
+ path: string;
149
+ commit: string;
150
+ reachable: boolean;
151
+ publishRequired?: boolean;
152
+ autoPublishAllowed?: boolean;
153
+ autoPublishAttempted?: boolean;
154
+ autoPublishSucceeded?: boolean;
155
+ autoPublishVerified?: boolean;
156
+ autoPublishRefspec?: string;
157
+ autoPublishSkippedReason?: string;
158
+ importedFromWorktree?: boolean;
159
+ checkedLocal?: boolean;
160
+ localReachable?: boolean;
161
+ remote?: string;
162
+ remoteUrl?: string;
163
+ remoteReachable?: boolean;
164
+ remoteMainBranch?: string;
165
+ remoteMainReachable?: boolean;
166
+ fetchedFromOrigin?: boolean;
167
+ error?: string;
168
+ publishStdout?: string;
169
+ publishStderr?: string;
170
+ };
171
+
172
+ type MeshRefineSubmoduleReachabilitySummary = {
173
+ status: MeshRefineStageStatus;
174
+ checked: number;
175
+ unreachable: MeshRefineSubmoduleReachabilityEntry[];
176
+ entries: MeshRefineSubmoduleReachabilityEntry[];
177
+ durationMs: number;
178
+ autoPublishAllowed?: boolean;
179
+ autoPublishPolicySource?: string;
180
+ error?: string;
181
+ };
182
+
183
+ export type MeshRefineAsyncJobStatus = 'accepted' | 'completed' | 'failed';
184
+
185
+ export type MeshRefineJobHandle = {
186
+ success: true;
187
+ async: true;
188
+ status: MeshRefineAsyncJobStatus;
189
+ jobId: string;
190
+ interactionId: string;
191
+ meshId: string;
192
+ nodeId: string;
193
+ targetNodeId: string;
194
+ targetDaemonId?: string;
195
+ workspace?: string;
196
+ startedAt: string;
197
+ completedAt?: string;
198
+ duplicate?: boolean;
199
+ retryOfJobId?: string;
200
+ /**
201
+ * The coordinator daemon ID that initiated this refine job.
202
+ * When set, events for this job are scoped to that coordinator's
203
+ * pending-events queue instead of the shared broadcast queue.
204
+ */
205
+ targetCoordinatorDaemonId?: string;
206
+ eventDelivery: {
207
+ pendingEvents: true;
208
+ ledger: true;
209
+ };
210
+ evidence: {
211
+ pendingEventsCommand: 'get_pending_mesh_events';
212
+ ledgerCommand: 'get_mesh_ledger_slice';
213
+ taskHistoryKind: 'task_dispatched' | 'task_completed' | 'task_failed';
214
+ };
215
+ };
216
+
217
+ export type MeshRefineTerminalJob = MeshRefineJobHandle & { result?: Record<string, unknown> };
218
+
219
+ export type MeshRefineBatchJobStatus = 'accepted' | 'completed' | 'failed';
220
+
221
+ /**
222
+ * Async handle returned by the batch Refinery the instant a convergence run is
223
+ * accepted. Mirrors {@link MeshRefineJobHandle} (async:true / status:'accepted' +
224
+ * terminal pending-event + ledger delivery) but scopes a whole batch of sibling
225
+ * nodes rather than a single node. The synthetic `batchLabel` is used as the
226
+ * `nodeLabel` for the shared refine event/message renderer.
227
+ */
228
+ export type MeshRefineBatchJobHandle = {
229
+ success: true;
230
+ async: true;
231
+ batch: true;
232
+ status: MeshRefineBatchJobStatus;
233
+ jobId: string;
234
+ interactionId: string;
235
+ meshId: string;
236
+ batchLabel: string;
237
+ nodeIds: string[];
238
+ nodeCount: number;
239
+ order: string[];
240
+ startedAt: string;
241
+ completedAt?: string;
242
+ duplicate?: boolean;
243
+ targetCoordinatorDaemonId?: string;
244
+ eventDelivery: {
245
+ pendingEvents: true;
246
+ ledger: true;
247
+ };
248
+ evidence: {
249
+ pendingEventsCommand: 'get_pending_mesh_events';
250
+ ledgerCommand: 'get_mesh_ledger_slice';
251
+ taskHistoryKind: 'task_dispatched' | 'task_completed' | 'task_failed';
252
+ };
253
+ };
254
+
255
+ export type MeshRefineBatchTerminalJob = MeshRefineBatchJobHandle & { result?: Record<string, unknown> };
256
+
257
+ const REFINE_VALIDATION_CATEGORIES = ['typecheck', 'test', 'lint', 'build'] as const;
258
+ const REFINE_VALIDATION_TIMEOUT_MS = 120_000;
259
+ const REFINE_VALIDATION_OUTPUT_LIMIT_BYTES = 128 * 1024;
260
+ const REFINE_VALIDATION_SUMMARY_CHARS = 2_000;
261
+ const REFINE_VALIDATION_MAX_COMMANDS = 4;
262
+ const REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES = 4 * 1024 * 1024;
263
+
264
+ export function truncateValidationOutput(value: unknown): string {
265
+ const text = typeof value === 'string' ? value : value == null ? '' : String(value);
266
+ if (text.length <= REFINE_VALIDATION_SUMMARY_CHARS) return text;
267
+ return `${text.slice(0, REFINE_VALIDATION_SUMMARY_CHARS)}\n[truncated ${text.length - REFINE_VALIDATION_SUMMARY_CHARS} chars]`;
268
+ }
269
+
270
+ /**
271
+ * A spawn-resolution failure is when the executable itself could not be found by
272
+ * the OS spawn boundary — `spawn <cmd> ENOENT` — as opposed to the command
273
+ * running and exiting non-zero. On win32 this is the .cmd-shim case: libuv's
274
+ * spawn search appends only .com/.exe, so a bare `npm`/`npx`/`tsc` (which are
275
+ * .cmd shims) ENOENTs even though it is installed. It carries no stderr, so it
276
+ * must be detected by error.code/syscall, not by string-matching output.
277
+ */
278
+ export function isSpawnResolutionError(error: any): boolean {
279
+ if (!error) return false;
280
+ if (error.code === 'ENOENT' && typeof error.syscall === 'string' && error.syscall.startsWith('spawn')) return true;
281
+ // Fall back to code alone: execFile sets syscall on the spawn boundary error,
282
+ // but guard for environments/mocks that only surface the code.
283
+ return error.code === 'ENOENT' && (error.syscall === undefined || String(error.syscall).startsWith('spawn'));
284
+ }
285
+
286
+ export function describeSpawnError(error: any, command: string, spawnResolutionFailed: boolean): string {
287
+ if (spawnResolutionFailed) {
288
+ const hint = process.platform === 'win32'
289
+ ? ' On Windows, npm-family commands (npm/npx/tsc/vitest) are .cmd shims that the bare-command spawn search does not resolve; configure an absolute path or ensure the command is on PATH.'
290
+ : '';
291
+ return `Could not resolve executable "${command}" (spawn ENOENT).${hint}`;
292
+ }
293
+ return String(error?.message || error);
294
+ }
295
+
296
+ export function recordMeshRefineStage(
297
+ stages: Array<Record<string, unknown>>,
298
+ stage: string,
299
+ status: MeshRefineStageStatus,
300
+ startedAt: number,
301
+ details?: Record<string, unknown>,
302
+ ): void {
303
+ stages.push({
304
+ stage,
305
+ status,
306
+ durationMs: Date.now() - startedAt,
307
+ ...(details || {}),
308
+ });
309
+ }
310
+
311
+ export function buildSubmodulePublishRequiredNextStep(entries: MeshRefineSubmoduleReachabilityEntry[]): string {
312
+ const refs = entries
313
+ .map(entry => `${entry.path}@${entry.commit}`)
314
+ .join(', ');
315
+ return `Ask the user for explicit approval to push/publish the unreachable submodule commit(s) (${refs}) to the configured submodule remote main branch, then rerun mesh_refine_node. Do not merge the root branch until every submodule gitlink commit is reachable from submodule origin/main.`;
316
+ }
317
+
318
+ /**
319
+ * Async git exec helper used across the synchronous-refine stage pipeline. Bound
320
+ * once in the orchestrator and threaded through RefineContext so every stage runs
321
+ * git the same way (execFile + promisify, utf8). Returns the child's stdout/stderr.
322
+ */
323
+ export type RefineExecFileAsync = (file: string, args: string[], options: { cwd: string; encoding: 'utf8' }) => Promise<{ stdout: string; stderr: string }>;
324
+
325
+ /**
326
+ * Accumulated state shared by the synchronous-refine stages. The orchestrator
327
+ * (executeMeshRefineNodeSynchronously) seeds this in the resolve_refs stage and
328
+ * each later stage reads / extends it. `branchHead` and `patchEquivalence` are the
329
+ * only fields a stage mutates after creation (auto-rebase updates both), so they
330
+ * are carried on the mutable context rather than re-threaded through return types.
331
+ */
332
+ export interface RefineContext {
333
+ meshId: string;
334
+ nodeId: string;
335
+ args: any;
336
+ refineStages: Array<Record<string, unknown>>;
337
+ execFileAsync: RefineExecFileAsync;
338
+ mesh: any;
339
+ node: any;
340
+ sourceNode: any;
341
+ repoRoot: string;
342
+ branch: string;
343
+ baseBranch: string;
344
+ baseHead: string;
345
+ branchHead: string;
346
+ validationSummary: Awaited<ReturnType<typeof runMeshRefineValidationGate>>;
347
+ patchEquivalence: Awaited<ReturnType<typeof runMeshRefinePatchEquivalenceGate>>;
348
+ submoduleReachability: Awaited<ReturnType<typeof runMeshRefineSubmoduleReachabilityGate>>;
349
+ }
350
+
351
+ /**
352
+ * Stage outcome for the synchronous-refine pipeline. A stage either produces a
353
+ * terminal CommandRouterResult (an early-exit gate failure, or a successful
354
+ * already-merged short-circuit), in which case the orchestrator returns it
355
+ * immediately, or it returns `continue` with the (possibly extended) context for
356
+ * the next stage. This makes the orchestrator a flat sequence of stage calls
357
+ * while preserving the original body's exact early-return control flow.
358
+ */
359
+ export type RefineStageOutcome =
360
+ | { kind: 'terminal'; result: CommandRouterResult }
361
+ | { kind: 'continue'; ctx: RefineContext };
362
+
363
+ export function resolveRefineryAutoPublishSubmoduleMainCommits(mesh: any, workspace: string): { enabled: boolean; source?: string } {
364
+ if (mesh?.policy?.allowAutoPublishSubmoduleMainCommits === true) {
365
+ process.stderr.write(
366
+ `[adhdev-mesh] WARNING: allowAutoPublishSubmoduleMainCommits is ENABLED via mesh.policy. `
367
+ + `Refinery may push unreachable submodule commits to submodule origin/main without additional user approval.\n`,
368
+ );
369
+ return { enabled: true, source: 'mesh.policy.allowAutoPublishSubmoduleMainCommits' };
370
+ }
371
+ const loaded = loadMeshRefineConfig(mesh, workspace);
372
+ if (loaded.config?.allowAutoPublishSubmoduleMainCommits === true) {
373
+ process.stderr.write(
374
+ `[adhdev-mesh] WARNING: allowAutoPublishSubmoduleMainCommits is ENABLED via ${loaded.path || loaded.source}. `
375
+ + `Refinery may push unreachable submodule commits to submodule origin/main without additional user approval.\n`,
376
+ );
377
+ return { enabled: true, source: loaded.path || loaded.source };
378
+ }
379
+ return { enabled: false };
380
+ }
381
+
382
+ async function computeGitPatchId(
383
+ cwd: string,
384
+ fromRef: string,
385
+ toRef: string,
386
+ excludePaths: string[] = [],
387
+ ): Promise<string> {
388
+ const { execFileSync } = await import('node:child_process');
389
+ // When excludePaths is non-empty we drop those paths from the diff via
390
+ // `:(exclude)` pathspecs. This is used to omit gitlink paths that have
391
+ // already been proven a safe fast-forward: their patch hunks legitimately
392
+ // differ between the expected (mergeBase→branch) and actual (base→merged)
393
+ // diffs because base may have advanced the same gitlink, so comparing them
394
+ // would spuriously fail patch-equivalence even though the merge is sound.
395
+ const diffArgs = ['diff', '--patch', '--full-index', fromRef, toRef];
396
+ if (excludePaths.length > 0) {
397
+ diffArgs.push('--', '.', ...excludePaths.map(path => `:(exclude)${path}`));
398
+ }
399
+ const diff = execFileSync(GIT, diffArgs, {
400
+ cwd,
401
+ encoding: 'utf8',
402
+ maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
403
+ });
404
+ if (!diff.trim()) return '';
405
+ const patchId = execFileSync(GIT, ['patch-id', '--stable'], {
406
+ cwd,
407
+ input: diff,
408
+ encoding: 'utf8',
409
+ maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
410
+ }).trim();
411
+ return patchId.split(/\s+/)[0] || '';
412
+ }
413
+
414
+ export async function runMeshRefinePatchEquivalenceGate(
415
+ repoRoot: string,
416
+ baseHead: string,
417
+ branchHead: string,
418
+ ): Promise<MeshRefinePatchEquivalenceSummary> {
419
+ const startedAt = Date.now();
420
+ try {
421
+ const { execFileSync } = await import('node:child_process');
422
+ const git = (args: string[]) => execFileSync(GIT, args, {
423
+ cwd: repoRoot,
424
+ encoding: 'utf8',
425
+ maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
426
+ });
427
+ const mergeBase = git(['merge-base', baseHead, branchHead]).trim();
428
+
429
+ // `git merge-tree --write-tree` refuses to merge gitlinks that differ
430
+ // across base/branch even when the advance is a strict fast-forward,
431
+ // failing with "Recursive merging with submodules currently only
432
+ // supports trivial cases". When that happens we check whether the
433
+ // conflict is *entirely* trivial-ff gitlinks and, if so, synthesize the
434
+ // merged tree ourselves (base tree + branch-side gitlinks).
435
+ let mergedTree = '';
436
+ let mergeTreeStdout = '';
437
+ let gitlinkTrivialFastForward: MeshRefinePatchEquivalenceSummary['gitlinkTrivialFastForward'];
438
+ try {
439
+ mergeTreeStdout = git(['merge-tree', '--write-tree', baseHead, branchHead]);
440
+ mergedTree = mergeTreeStdout.trim().split(/\s+/)[0] || '';
441
+ } catch (mergeTreeErr: any) {
442
+ const output = `${mergeTreeErr?.message || ''}\n${mergeTreeErr?.stdout || ''}\n${mergeTreeErr?.stderr || ''}`;
443
+ const isSubmoduleConflict = /(submodule|160000)/i.test(output)
444
+ || /Recursive merging with submodules/i.test(output);
445
+ if (!isSubmoduleConflict) throw mergeTreeErr;
446
+ const evaluation = evaluateGitlinkTrivialFastForward(repoRoot, baseHead, branchHead);
447
+ if (!evaluation.trivial) {
448
+ return {
449
+ status: 'failed',
450
+ equivalent: false,
451
+ baseHead,
452
+ branchHead,
453
+ mergeBase: mergeBase || undefined,
454
+ durationMs: Date.now() - startedAt,
455
+ error: mergeTreeErr?.message || String(mergeTreeErr),
456
+ stdout: truncateValidationOutput(mergeTreeErr?.stdout),
457
+ stderr: truncateValidationOutput(mergeTreeErr?.stderr),
458
+ gitlinkTrivialFastForward: { resolved: false, gitlinks: evaluation.gitlinks, reason: evaluation.reason },
459
+ actionableHint: buildPatchEquivalenceSubmoduleConflictHint(repoRoot, baseHead, branchHead, output),
460
+ };
461
+ }
462
+ // All conflicting gitlinks fast-forward and nothing else conflicts:
463
+ // synthesize the merge result as base's tree with branch-side gitlinks.
464
+ mergedTree = synthesizeTrivialFastForwardMergeTree(repoRoot, baseHead, branchHead, evaluation.gitlinks) || '';
465
+ gitlinkTrivialFastForward = { resolved: true, gitlinks: evaluation.gitlinks };
466
+ }
467
+
468
+ if (!mergeBase || !mergedTree) {
469
+ return {
470
+ status: 'failed',
471
+ equivalent: false,
472
+ baseHead,
473
+ branchHead,
474
+ mergeBase: mergeBase || undefined,
475
+ mergedTree: mergedTree || undefined,
476
+ durationMs: Date.now() - startedAt,
477
+ error: 'patch equivalence preflight could not resolve merge-base or synthetic merge tree',
478
+ stdout: truncateValidationOutput(mergeTreeStdout),
479
+ gitlinkTrivialFastForward,
480
+ };
481
+ }
482
+ // Exclude *proven fast-forward* gitlink paths from BOTH patch-ids. When
483
+ // base has advanced a submodule pointer (a sibling merged into main ahead
484
+ // of us) the gitlink hunk's old-value differs between the expected diff
485
+ // (mergeBase→branch, showing the full base→branch advance) and the actual
486
+ // diff (base→merged, showing only the shorter advanced-base→branch
487
+ // advance). That mismatch would spuriously fail equivalence even though
488
+ // advancing the pointer to the branch side is a provably safe
489
+ // fast-forward — this is the root cause of the diverged-base
490
+ // patch_equivalence_failed misjudgment.
491
+ //
492
+ // We exclude ONLY gitlinks whose base-side commit is an ancestor of the
493
+ // branch-side commit (a strict ff, both objects available locally). A
494
+ // non-ff or ambiguous gitlink (a genuine submodule divergence, or objects
495
+ // not fetched locally) is deliberately left in the diff so its differing
496
+ // hunk still drives the comparison — this preserves the original behavior
497
+ // and prevents a false pass on a real divergence.
498
+ const ffGitlinkExcludePaths = collectFastForwardGitlinkPaths(repoRoot, baseHead, branchHead);
499
+ const expectedPatchId = await computeGitPatchId(repoRoot, mergeBase, branchHead, ffGitlinkExcludePaths);
500
+ const actualPatchId = await computeGitPatchId(repoRoot, baseHead, mergedTree, ffGitlinkExcludePaths);
501
+ const equivalent = expectedPatchId === actualPatchId;
502
+ return {
503
+ status: equivalent ? 'passed' : 'failed',
504
+ equivalent,
505
+ baseHead,
506
+ branchHead,
507
+ mergeBase,
508
+ mergedTree,
509
+ expectedPatchId,
510
+ actualPatchId,
511
+ durationMs: Date.now() - startedAt,
512
+ gitlinkTrivialFastForward,
513
+ };
514
+ } catch (e: any) {
515
+ return {
516
+ status: 'failed',
517
+ equivalent: false,
518
+ baseHead,
519
+ branchHead,
520
+ durationMs: Date.now() - startedAt,
521
+ error: e?.message || String(e),
522
+ stdout: truncateValidationOutput(e?.stdout),
523
+ stderr: truncateValidationOutput(e?.stderr),
524
+ actionableHint: buildPatchEquivalenceSubmoduleConflictHint(
525
+ repoRoot,
526
+ baseHead,
527
+ branchHead,
528
+ `${e?.message || ''}\n${e?.stdout || ''}\n${e?.stderr || ''}`,
529
+ ),
530
+ };
531
+ }
532
+ }
533
+
534
+ export type MeshWorktreePatchContainmentSummary = {
535
+ /** True only when merging worktreeHead into ref introduces no new patch. */
536
+ contained: boolean;
537
+ ref: string;
538
+ worktreeHead: string;
539
+ mergeBase?: string;
540
+ mergedTree?: string;
541
+ /** patch-id of (ref -> synthesized merge tree); empty string when nothing new is added. */
542
+ residualPatchId?: string;
543
+ durationMs: number;
544
+ /** Set when the check could not run (treated conservatively as NOT contained). */
545
+ error?: string;
546
+ };
547
+
548
+ /**
549
+ * Patch-equivalence containment check for the worktree force-cleanup convergence
550
+ * guard. Answers a narrower question than {@link runMeshRefinePatchEquivalenceGate}:
551
+ * "are the worktree branch's changes ALREADY present in `ref` (e.g. origin/main),
552
+ * even though the worktree HEAD's commit SHA is not an ancestor of ref?"
553
+ *
554
+ * This is the cherry-pick / squash / rebase case: the same content landed on the
555
+ * default ref under a different commit SHA, so `merge-base --is-ancestor` (the
556
+ * primary cleanup guard) reports the worktree as un-converged and refuses to
557
+ * remove it. Refinery already accepts patch-equivalent landings via merge-tree +
558
+ * patch-id; this brings the same notion of "convergence" to the cleanup guard.
559
+ *
560
+ * Mechanism: synthesize the merge of `worktreeHead` into `ref` (reusing the same
561
+ * trivial-gitlink-fast-forward handling as the refine gate) and compute the
562
+ * patch-id of (ref -> mergedTree). If that residual diff is EMPTY, merging the
563
+ * worktree adds nothing new on top of ref — its changes are already present there
564
+ * and the worktree is safe to remove. A non-empty residual means the worktree
565
+ * still carries content not in ref, so it is NOT contained and must stay blocked.
566
+ *
567
+ * Conservative by construction: any merge-tree / patch-id failure, a genuine
568
+ * (non-trivial) submodule conflict, or any thrown error yields `contained: false`
569
+ * so an exception can never widen the cleanup allow-list.
570
+ */
571
+ export async function checkWorktreeChangesPatchEquivalentInRef(
572
+ repoRoot: string,
573
+ ref: string,
574
+ worktreeHead: string,
575
+ ): Promise<MeshWorktreePatchContainmentSummary> {
576
+ const startedAt = Date.now();
577
+ try {
578
+ const { execFileSync } = await import('node:child_process');
579
+ const git = (gitArgs: string[]) => execFileSync(GIT, gitArgs, {
580
+ cwd: repoRoot,
581
+ encoding: 'utf8',
582
+ maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
583
+ });
584
+ const mergeBase = git(['merge-base', ref, worktreeHead]).trim();
585
+
586
+ // Reuse the refine gate's trivial-gitlink-fast-forward handling: a clean
587
+ // submodule pointer fast-forward must not block the cleanup, but a real
588
+ // (non-ff) submodule divergence must keep it blocked.
589
+ let mergedTree = '';
590
+ try {
591
+ mergedTree = git(['merge-tree', '--write-tree', ref, worktreeHead]).trim().split(/\s+/)[0] || '';
592
+ } catch (mergeTreeErr: any) {
593
+ const output = `${mergeTreeErr?.message || ''}\n${mergeTreeErr?.stdout || ''}\n${mergeTreeErr?.stderr || ''}`;
594
+ const isSubmoduleConflict = /(submodule|160000)/i.test(output)
595
+ || /Recursive merging with submodules/i.test(output);
596
+ if (!isSubmoduleConflict) throw mergeTreeErr;
597
+ const evaluation = evaluateGitlinkTrivialFastForward(repoRoot, ref, worktreeHead);
598
+ if (!evaluation.trivial) {
599
+ // A genuine submodule divergence (or unfetched objects): we cannot
600
+ // prove containment, so block conservatively.
601
+ return {
602
+ contained: false,
603
+ ref,
604
+ worktreeHead,
605
+ mergeBase: mergeBase || undefined,
606
+ durationMs: Date.now() - startedAt,
607
+ error: `merge-tree submodule conflict is not a trivial fast-forward: ${evaluation.reason || 'unknown'}`,
608
+ };
609
+ }
610
+ mergedTree = synthesizeTrivialFastForwardMergeTree(repoRoot, ref, worktreeHead, evaluation.gitlinks) || '';
611
+ }
612
+
613
+ if (!mergedTree) {
614
+ return {
615
+ contained: false,
616
+ ref,
617
+ worktreeHead,
618
+ mergeBase: mergeBase || undefined,
619
+ durationMs: Date.now() - startedAt,
620
+ error: 'could not resolve synthetic merge tree for containment check',
621
+ };
622
+ }
623
+
624
+ // Exclude proven fast-forward gitlinks from the residual diff for the same
625
+ // reason the refine gate does: advancing a submodule pointer to a strict
626
+ // descendant is a safe fast-forward and must not count as "new content".
627
+ const ffGitlinkExcludePaths = collectFastForwardGitlinkPaths(repoRoot, ref, worktreeHead);
628
+ const residualPatchId = await computeGitPatchId(repoRoot, ref, mergedTree, ffGitlinkExcludePaths);
629
+ const contained = residualPatchId === '';
630
+ return {
631
+ contained,
632
+ ref,
633
+ worktreeHead,
634
+ mergeBase: mergeBase || undefined,
635
+ mergedTree,
636
+ residualPatchId,
637
+ durationMs: Date.now() - startedAt,
638
+ };
639
+ } catch (e: any) {
640
+ return {
641
+ contained: false,
642
+ ref,
643
+ worktreeHead,
644
+ durationMs: Date.now() - startedAt,
645
+ error: e?.message || String(e),
646
+ };
647
+ }
648
+ }
649
+
650
+ /**
651
+ * No-op guard: detect a "silent no-op" merge before the Refinery merge runs.
652
+ *
653
+ * A silent no-op occurs when the refine target branch's ROOT tree is byte-identical
654
+ * to the merge base (origin/main). This is the trap where a submodule (e.g. oss) has
655
+ * real commits but the root branch never committed the gitlink (oss-pointer) bump, so
656
+ * the root diff Refinery would merge is empty. Merging that produces a merge commit with
657
+ * no content change — reported as "success" while the actual work never reaches main.
658
+ *
659
+ * A committed gitlink bump (the legitimate oss-pointer bump) DOES show up in the root
660
+ * tree diff (as a 160000-mode entry), so this guard does NOT block legitimate refines —
661
+ * it only fires when the root tree diff vs base is COMPLETELY empty.
662
+ *
663
+ * Runs after the patch-equivalence gate; the "already merged via other path" case
664
+ * (branch has real changes already present in base) is handled upstream and never
665
+ * reaches here, so an empty root diff at this point is genuinely a no-op.
666
+ */
667
+ export async function runMeshRefineEffectiveDiffGate(
668
+ repoRoot: string,
669
+ baseHead: string,
670
+ branchHead: string,
671
+ ): Promise<MeshRefineEffectiveDiffSummary> {
672
+ const startedAt = Date.now();
673
+ try {
674
+ const { execFileSync } = await import('node:child_process');
675
+ const git = (args: string[], opts?: { cwd?: string }) => execFileSync(GIT, args, {
676
+ cwd: opts?.cwd || repoRoot,
677
+ encoding: 'utf8',
678
+ maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
679
+ });
680
+ // Root tree diff between base and branch. --raw surfaces gitlink (160000) entries,
681
+ // so a committed submodule-pointer bump counts as an effective change. An empty
682
+ // result means the branch's root tree is identical to base → nothing would merge.
683
+ const rawDiff = git(['diff', '--raw', baseHead, branchHead]).trim();
684
+ if (rawDiff) {
685
+ const changedPaths = rawDiff
686
+ .split('\n')
687
+ .map(line => line.split('\t').slice(1).join('\t').trim())
688
+ .filter(Boolean)
689
+ .slice(0, 50);
690
+ return {
691
+ status: 'passed',
692
+ hasEffectiveDiff: true,
693
+ baseHead,
694
+ branchHead,
695
+ changedPaths,
696
+ durationMs: Date.now() - startedAt,
697
+ };
698
+ }
699
+
700
+ // No root diff → silent no-op. Try to surface which submodule(s) have commits that
701
+ // were never captured by a committed gitlink bump, to make the message actionable.
702
+ const submoduleHints: Array<{ path: string; reason: string }> = [];
703
+ try {
704
+ // `git submodule status` flags submodules whose checked-out commit differs from
705
+ // the recorded gitlink with a leading '+'. That difference is exactly the
706
+ // uncommitted-pointer-bump situation this guard exists to catch.
707
+ const status = git(['submodule', 'status']);
708
+ for (const line of status.split('\n')) {
709
+ const trimmed = line.trimEnd();
710
+ if (!trimmed) continue;
711
+ if (trimmed.startsWith('+')) {
712
+ const parts = trimmed.slice(1).trim().split(/\s+/);
713
+ const path = parts[1] || parts[0] || '(unknown)';
714
+ submoduleHints.push({
715
+ path,
716
+ reason: 'submodule checked-out commit differs from the committed gitlink (pointer bump not committed on the root branch)',
717
+ });
718
+ }
719
+ }
720
+ } catch { /* submodule status is best-effort */ }
721
+
722
+ return {
723
+ status: 'failed',
724
+ hasEffectiveDiff: false,
725
+ baseHead,
726
+ branchHead,
727
+ ...(submoduleHints.length ? { submoduleHints } : {}),
728
+ durationMs: Date.now() - startedAt,
729
+ };
730
+ } catch (e: any) {
731
+ // On error, do NOT block the merge — fail open so a probe failure can't wedge refine.
732
+ return {
733
+ status: 'skipped',
734
+ hasEffectiveDiff: true,
735
+ baseHead,
736
+ branchHead,
737
+ durationMs: Date.now() - startedAt,
738
+ error: e?.message || String(e),
739
+ stdout: truncateValidationOutput(e?.stdout),
740
+ stderr: truncateValidationOutput(e?.stderr),
741
+ };
742
+ }
743
+ }
744
+
745
+ function buildPatchEquivalenceSubmoduleConflictHint(
746
+ repoRoot: string,
747
+ baseHead: string,
748
+ branchHead: string,
749
+ output: string,
750
+ ): MeshRefineSubmoduleConflictHint | undefined {
751
+ if (!/(submodule|160000)/i.test(output) || !/(conflict|failed to merge)/i.test(output)) return undefined;
752
+ const conflicts = readChangedGitlinkPaths(repoRoot, baseHead, branchHead)
753
+ .map(path => ({
754
+ path,
755
+ baseCommit: readTreeObject(repoRoot, baseHead, path),
756
+ branchCommit: readTreeObject(repoRoot, branchHead, path),
757
+ }));
758
+ if (conflicts.length === 0) return undefined;
759
+ return {
760
+ kind: 'submodule_conflict',
761
+ message: 'Refinery could not synthesize a safe merge tree because the branch and base point the same submodule path at different commits.',
762
+ conflicts,
763
+ nextSteps: [
764
+ 'Inspect the listed submodule path in both base and branch: baseCommit is the commit currently recorded by the base workspace, branchCommit is the commit recorded by the worktree branch.',
765
+ 'Resolve the submodule first by checking out or creating the intended submodule commit, then commit the chosen gitlink in the root branch.',
766
+ 'Ensure the chosen submodule commit is reachable from the configured submodule remote main branch, then rerun mesh_refine_node.',
767
+ ],
768
+ };
769
+ }
770
+
771
+ function readChangedGitlinkPaths(repoRoot: string, fromRef: string, toRef: string): string[] {
772
+ try {
773
+ const output = execFileSync(GIT, ['diff', '--raw', '--no-abbrev', fromRef, toRef], {
774
+ cwd: repoRoot,
775
+ encoding: 'utf8',
776
+ maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
777
+ });
778
+ const paths = new Set<string>();
779
+ for (const line of output.split('\n')) {
780
+ if (!line.trim()) continue;
781
+ const metaAndPath = line.split('\t');
782
+ const meta = metaAndPath[0] || '';
783
+ const path = metaAndPath[metaAndPath.length - 1]?.trim();
784
+ if (!path) continue;
785
+ const parts = meta.split(/\s+/);
786
+ if (parts[0]?.includes('160000') || parts[1]?.includes('160000')) {
787
+ paths.add(path);
788
+ }
789
+ }
790
+ return [...paths].sort();
791
+ } catch {
792
+ return [];
793
+ }
794
+ }
795
+
796
+ function readTreeObject(repoRoot: string, ref: string, path: string): string | undefined {
797
+ try {
798
+ const output = execFileSync(GIT, ['ls-tree', ref, '--', path], {
799
+ cwd: repoRoot,
800
+ encoding: 'utf8',
801
+ maxBuffer: 1024 * 1024,
802
+ }).trim();
803
+ const match = output.match(/\bcommit\s+([0-9a-f]{40})\b/i);
804
+ return match?.[1];
805
+ } catch {
806
+ return undefined;
807
+ }
808
+ }
809
+
810
+ /**
811
+ * Resolve the absolute path to the repo's real git directory. In a linked
812
+ * worktree, `.git` is a file pointing elsewhere, so we cannot assume a `.git`
813
+ * subdirectory exists — a temporary index file must live in the actual git dir.
814
+ */
815
+ function resolveGitDir(repoRoot: string): string {
816
+ const out = execFileSync(GIT, ['rev-parse', '--absolute-git-dir'], {
817
+ cwd: repoRoot,
818
+ encoding: 'utf8',
819
+ maxBuffer: 1024 * 1024,
820
+ }).trim();
821
+ return out;
822
+ }
823
+
824
+ /**
825
+ * Result of evaluating whether a `git merge-tree --write-tree` submodule
826
+ * conflict is in fact a trivial gitlink fast-forward that should pass the
827
+ * patch-equivalence gate.
828
+ *
829
+ * `git merge-tree` (and `git merge` with the default recursive strategy)
830
+ * refuses to 3-way merge gitlinks unless the case is "trivial" — and it
831
+ * treats *any* gitlink that differs across merge-base/base/branch as
832
+ * non-trivial, even when the branch-side commit is a strict descendant of the
833
+ * base-side commit (i.e. a real fast-forward). Refinery only ever wants to
834
+ * accept the branch's recorded gitlink, so a fast-forwardable bump is safe to
835
+ * resolve to the branch side without any conflict.
836
+ */
837
+ type GitlinkTrivialFastForwardEvaluation = {
838
+ /** True only when the merge-tree conflict is *fully* explained by trivial-ff gitlinks. */
839
+ trivial: boolean;
840
+ /** Why the evaluation declined to treat the conflict as trivial (set when trivial=false). */
841
+ reason?: string;
842
+ /** Per-path detail for the changed gitlinks that were inspected. */
843
+ gitlinks: Array<{
844
+ path: string;
845
+ baseCommit?: string;
846
+ branchCommit?: string;
847
+ fastForward: boolean;
848
+ }>;
849
+ };
850
+
851
+ /**
852
+ * Check, inside a submodule repo, whether `baseCommit` is an ancestor of
853
+ * `branchCommit` (i.e. advancing the gitlink from base→branch is a pure
854
+ * fast-forward). Returns false on any error or when either commit is missing
855
+ * locally — safety first, ambiguity stays "not a fast-forward".
856
+ */
857
+ function isSubmoduleFastForward(submoduleRepoPath: string, baseCommit: string, branchCommit: string): boolean {
858
+ if (!baseCommit || !branchCommit) return false;
859
+ if (baseCommit === branchCommit) return true;
860
+ try {
861
+ if (!fs.existsSync(submoduleRepoPath)) return false;
862
+ // Both commits must exist locally for the ancestry check to be meaningful.
863
+ execFileSync(GIT, ['cat-file', '-e', `${baseCommit}^{commit}`], { cwd: submoduleRepoPath, stdio: 'ignore' });
864
+ execFileSync(GIT, ['cat-file', '-e', `${branchCommit}^{commit}`], { cwd: submoduleRepoPath, stdio: 'ignore' });
865
+ // exit 0 ⇒ baseCommit is an ancestor of branchCommit ⇒ branch fast-forwards base.
866
+ execFileSync(GIT, ['merge-base', '--is-ancestor', baseCommit, branchCommit], { cwd: submoduleRepoPath, stdio: 'ignore' });
867
+ return true;
868
+ } catch {
869
+ return false;
870
+ }
871
+ }
872
+
873
+ /**
874
+ * Read the set of paths that differ between two refs, tagging whether each is a
875
+ * gitlink (submodule, mode 160000) on either side. Returns one entry per
876
+ * changed path. Empty on error.
877
+ */
878
+ function readChangedPathKinds(repoRoot: string, fromRef: string, toRef: string): Array<{ path: string; isGitlink: boolean }> {
879
+ try {
880
+ const output = execFileSync(GIT, ['diff', '--raw', '--no-abbrev', fromRef, toRef], {
881
+ cwd: repoRoot,
882
+ encoding: 'utf8',
883
+ maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
884
+ });
885
+ const result: Array<{ path: string; isGitlink: boolean }> = [];
886
+ const seen = new Set<string>();
887
+ for (const line of output.split('\n')) {
888
+ if (!line.trim()) continue;
889
+ const metaAndPath = line.split('\t');
890
+ const meta = metaAndPath[0] || '';
891
+ const path = metaAndPath[metaAndPath.length - 1]?.trim();
892
+ if (!path || seen.has(path)) continue;
893
+ seen.add(path);
894
+ const parts = meta.split(/\s+/);
895
+ const isGitlink = !!(parts[0]?.includes('160000') || parts[1]?.includes('160000'));
896
+ result.push({ path, isGitlink });
897
+ }
898
+ return result;
899
+ } catch {
900
+ return [];
901
+ }
902
+ }
903
+
904
+ /**
905
+ * Return the changed gitlink paths between base and branch whose advance is a
906
+ * strict fast-forward (the base-side commit is an ancestor of the branch-side
907
+ * commit inside that submodule's repo). These are the paths whose patch-id hunk
908
+ * may legitimately differ when base has advanced the same submodule, so they
909
+ * are safe to exclude from the patch-equivalence comparison. A non-ff (genuinely
910
+ * diverged) gitlink is deliberately excluded from this set so it still fails the
911
+ * gate.
912
+ */
913
+ export function collectFastForwardGitlinkPaths(repoRoot: string, baseHead: string, branchHead: string): string[] {
914
+ return readChangedGitlinkPaths(repoRoot, baseHead, branchHead).filter(path => {
915
+ const baseCommit = readTreeObject(repoRoot, baseHead, path);
916
+ const branchCommit = readTreeObject(repoRoot, branchHead, path);
917
+ if (!baseCommit || !branchCommit) return false;
918
+ return isSubmoduleFastForward(pathResolve(repoRoot, path), baseCommit, branchCommit);
919
+ });
920
+ }
921
+
922
+ /**
923
+ * Decide whether a merge-tree submodule conflict between base and branch is a
924
+ * trivial gitlink fast-forward (and nothing else).
925
+ *
926
+ * The conflict is treated as trivial ONLY when:
927
+ * 1. at least one changed gitlink exists,
928
+ * 2. every changed gitlink fast-forwards (base-commit is an ancestor of the
929
+ * branch-commit inside that submodule's repo), and
930
+ * 3. the *only* paths that changed on both sides of the merge (i.e. the paths
931
+ * that could possibly produce a 3-way conflict — the intersection of
932
+ * mergeBase→base and mergeBase→branch changes) are gitlinks. Any
933
+ * overlapping non-gitlink path means a genuine content conflict could be
934
+ * hiding behind the submodule failure, so we keep the block.
935
+ *
936
+ * If any of these fail, the conflict is left as a genuine block. This never
937
+ * passes a regular-file conflict or a diverged (non-ff) gitlink.
938
+ */
939
+ export function evaluateGitlinkTrivialFastForward(
940
+ repoRoot: string,
941
+ baseHead: string,
942
+ branchHead: string,
943
+ ): GitlinkTrivialFastForwardEvaluation {
944
+ const changedGitlinks = readChangedGitlinkPaths(repoRoot, baseHead, branchHead).map(path => {
945
+ const baseCommit = readTreeObject(repoRoot, baseHead, path);
946
+ const branchCommit = readTreeObject(repoRoot, branchHead, path);
947
+ const submoduleRepoPath = pathResolve(repoRoot, path);
948
+ const fastForward = !!baseCommit && !!branchCommit
949
+ && isSubmoduleFastForward(submoduleRepoPath, baseCommit, branchCommit);
950
+ return { path, baseCommit, branchCommit, fastForward };
951
+ });
952
+
953
+ if (changedGitlinks.length === 0) {
954
+ return { trivial: false, reason: 'no_changed_gitlinks', gitlinks: changedGitlinks };
955
+ }
956
+
957
+ const nonFastForward = changedGitlinks.filter(entry => !entry.fastForward);
958
+ if (nonFastForward.length > 0) {
959
+ return {
960
+ trivial: false,
961
+ reason: `diverged_gitlinks:${nonFastForward.map(entry => entry.path).join(',')}`,
962
+ gitlinks: changedGitlinks,
963
+ };
964
+ }
965
+
966
+ // Prove there is no *other* conflict (regular files, or a gitlink that
967
+ // diverged on both sides). A 3-way merge can only conflict on a path that
968
+ // changed on BOTH sides relative to the merge-base. Compute that overlap and
969
+ // require every overlapping path to be a gitlink — non-gitlink overlap means
970
+ // a genuine content conflict that must stay blocked.
971
+ let mergeBase = '';
972
+ try {
973
+ mergeBase = execFileSync(GIT, ['merge-base', baseHead, branchHead], {
974
+ cwd: repoRoot,
975
+ encoding: 'utf8',
976
+ maxBuffer: 1024 * 1024,
977
+ }).trim();
978
+ } catch {
979
+ return { trivial: false, reason: 'merge_base_unresolved', gitlinks: changedGitlinks };
980
+ }
981
+ if (!mergeBase) {
982
+ return { trivial: false, reason: 'merge_base_unresolved', gitlinks: changedGitlinks };
983
+ }
984
+
985
+ const baseSideChanges = readChangedPathKinds(repoRoot, mergeBase, baseHead);
986
+ const branchSideChanges = readChangedPathKinds(repoRoot, mergeBase, branchHead);
987
+ const baseChangedPaths = new Map(baseSideChanges.map(entry => [entry.path, entry]));
988
+ // Overlapping paths = candidates for a real 3-way conflict.
989
+ const overlapping = branchSideChanges.filter(entry => baseChangedPaths.has(entry.path));
990
+ const nonGitlinkOverlap = overlapping.filter(entry => {
991
+ const baseEntry = baseChangedPaths.get(entry.path);
992
+ return !(entry.isGitlink && baseEntry?.isGitlink);
993
+ });
994
+ if (nonGitlinkOverlap.length > 0) {
995
+ return {
996
+ trivial: false,
997
+ reason: `non_gitlink_overlap:${nonGitlinkOverlap.map(entry => entry.path).join(',')}`,
998
+ gitlinks: changedGitlinks,
999
+ };
1000
+ }
1001
+
1002
+ return { trivial: true, gitlinks: changedGitlinks };
1003
+ }
1004
+
1005
+ /**
1006
+ * Build a tree identical to `commitish`'s tree except every gitlink in `paths`
1007
+ * is rewritten to `placeholderCommit`. Used to neutralize submodule pointers so
1008
+ * `git merge-tree` stops bailing on the "Recursive merging with submodules"
1009
+ * limitation and can 3-way merge the surrounding regular-file content. Returns
1010
+ * the tree SHA, or undefined on failure.
1011
+ */
1012
+ function buildTreeWithGitlinksEqualized(
1013
+ repoRoot: string,
1014
+ commitish: string,
1015
+ paths: string[],
1016
+ placeholderCommit: string,
1017
+ ): string | undefined {
1018
+ try {
1019
+ const tree = execFileSync(GIT, ['rev-parse', `${commitish}^{tree}`], {
1020
+ cwd: repoRoot, encoding: 'utf8', maxBuffer: 1024 * 1024,
1021
+ }).trim();
1022
+ if (!tree) return undefined;
1023
+ const updates = paths.map(path => `160000 commit ${placeholderCommit}\t${path}`).join('\n');
1024
+ if (!updates) return tree;
1025
+ const tmpIndex = pathJoin(resolveGitDir(repoRoot), `adhdev-refine-eq-${commitish.slice(0, 12)}.index`);
1026
+ const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
1027
+ try {
1028
+ execFileSync(GIT, ['read-tree', tree], { cwd: repoRoot, env, stdio: 'ignore' });
1029
+ execFileSync(GIT, ['update-index', '--index-info'], {
1030
+ cwd: repoRoot, env, input: `${updates}\n`, encoding: 'utf8',
1031
+ stdio: ['pipe', 'ignore', 'ignore'],
1032
+ });
1033
+ const newTree = execFileSync(GIT, ['write-tree'], { cwd: repoRoot, env, encoding: 'utf8' }).trim();
1034
+ return newTree || undefined;
1035
+ } finally {
1036
+ try { fs.rmSync(tmpIndex, { force: true }); } catch { /* ignore */ }
1037
+ }
1038
+ } catch {
1039
+ return undefined;
1040
+ }
1041
+ }
1042
+
1043
+ /**
1044
+ * Synthesize the merge result for a trivial gitlink fast-forward.
1045
+ *
1046
+ * `git merge-tree` bails whenever a gitlink differs across base/branch even
1047
+ * when the advance is a strict fast-forward, so we synthesize the result it
1048
+ * *would* have produced. Crucially, when the merge-base of base and branch is
1049
+ * NOT `baseHead` (i.e. base has diverged — a sibling was merged into main
1050
+ * ahead of us), `baseHead`'s tree does not contain our branch's own
1051
+ * non-gitlink changes. Simply overlaying gitlinks onto `baseHead`'s tree would
1052
+ * therefore drop those changes and break patch-equivalence.
1053
+ *
1054
+ * To handle the diverged case correctly we run a REAL 3-way merge of the
1055
+ * regular-file content (with the conflicting gitlinks temporarily equalized to
1056
+ * a common placeholder so merge-tree won't bail), then overlay each changed
1057
+ * gitlink's branch-side commit onto the merged result. This preserves both
1058
+ * sides' non-gitlink changes.
1059
+ *
1060
+ * Returns the tree SHA, or undefined on failure / genuine non-gitlink
1061
+ * conflict. Caller must have already proven (via
1062
+ * evaluateGitlinkTrivialFastForward) that every changed gitlink fast-forwards
1063
+ * and no other path conflicts.
1064
+ */
1065
+ function synthesizeTrivialFastForwardMergeTree(
1066
+ repoRoot: string,
1067
+ baseHead: string,
1068
+ branchHead: string,
1069
+ gitlinks: Array<{ path: string; branchCommit?: string }>,
1070
+ ): string | undefined {
1071
+ try {
1072
+ const branchGitlinks = gitlinks.filter(entry => entry.branchCommit);
1073
+ const gitlinkPaths = branchGitlinks.map(entry => entry.path);
1074
+
1075
+ // Establish the regular-file content of the merge via a real 3-way merge
1076
+ // with the conflicting gitlinks neutralized. The placeholder is the
1077
+ // merge-base's value for a gitlink (or, failing that, any branch-side
1078
+ // commit) — it only needs to be identical across all three trees.
1079
+ const mergeBase = execFileSync(GIT, ['merge-base', baseHead, branchHead], {
1080
+ cwd: repoRoot, encoding: 'utf8', maxBuffer: 1024 * 1024,
1081
+ }).trim();
1082
+
1083
+ let mergedContentTree: string | undefined;
1084
+ if (mergeBase && gitlinkPaths.length > 0) {
1085
+ const placeholder = readTreeObject(repoRoot, mergeBase, gitlinkPaths[0])
1086
+ || branchGitlinks[0].branchCommit!;
1087
+ const baseEqTree = buildTreeWithGitlinksEqualized(repoRoot, mergeBase, gitlinkPaths, placeholder);
1088
+ const oursEqTree = buildTreeWithGitlinksEqualized(repoRoot, baseHead, gitlinkPaths, placeholder);
1089
+ const theirsEqTree = buildTreeWithGitlinksEqualized(repoRoot, branchHead, gitlinkPaths, placeholder);
1090
+ if (baseEqTree && oursEqTree && theirsEqTree) {
1091
+ try {
1092
+ // merge-tree --write-tree needs commits (to derive a merge-base);
1093
+ // synthesize ours/theirs as children of a common base commit.
1094
+ const baseEqCommit = execFileSync(GIT, ['commit-tree', baseEqTree, '-m', 'refine-ff-base'], {
1095
+ cwd: repoRoot, encoding: 'utf8', maxBuffer: 1024 * 1024,
1096
+ }).trim();
1097
+ const oursEqCommit = execFileSync(GIT, ['commit-tree', oursEqTree, '-p', baseEqCommit, '-m', 'refine-ff-ours'], {
1098
+ cwd: repoRoot, encoding: 'utf8', maxBuffer: 1024 * 1024,
1099
+ }).trim();
1100
+ const theirsEqCommit = execFileSync(GIT, ['commit-tree', theirsEqTree, '-p', baseEqCommit, '-m', 'refine-ff-theirs'], {
1101
+ cwd: repoRoot, encoding: 'utf8', maxBuffer: 1024 * 1024,
1102
+ }).trim();
1103
+ const mergeOut = execFileSync(GIT, ['merge-tree', '--write-tree', oursEqCommit, theirsEqCommit], {
1104
+ cwd: repoRoot, encoding: 'utf8', maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
1105
+ }).trim();
1106
+ mergedContentTree = mergeOut.split(/\s+/)[0] || undefined;
1107
+ } catch {
1108
+ // A real conflict in the equalized merge means a genuine
1109
+ // non-gitlink content conflict the evaluator did not foresee
1110
+ // (or unavailable objects). Fall through to the simple synth.
1111
+ mergedContentTree = undefined;
1112
+ }
1113
+ }
1114
+ }
1115
+
1116
+ // Fallback: when there is no diverged base (merge-base === baseHead) the
1117
+ // regular-file content of the merge is exactly baseHead's tree, so just
1118
+ // overlay the gitlinks. Also used when the real merge could not run.
1119
+ const contentTree = mergedContentTree
1120
+ || execFileSync(GIT, ['rev-parse', `${baseHead}^{tree}`], {
1121
+ cwd: repoRoot, encoding: 'utf8', maxBuffer: 1024 * 1024,
1122
+ }).trim();
1123
+ if (!contentTree) return undefined;
1124
+
1125
+ const updates = branchGitlinks
1126
+ .map(entry => `160000 commit ${entry.branchCommit}\t${entry.path}`)
1127
+ .join('\n');
1128
+ if (!updates) return contentTree;
1129
+ const tmpIndex = pathJoin(resolveGitDir(repoRoot), `adhdev-refine-ff-${baseHead.slice(0, 12)}-${branchHead.slice(0, 12)}.index`);
1130
+ const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
1131
+ try {
1132
+ execFileSync(GIT, ['read-tree', contentTree], { cwd: repoRoot, env, stdio: 'ignore' });
1133
+ execFileSync(GIT, ['update-index', '--index-info'], {
1134
+ cwd: repoRoot,
1135
+ env,
1136
+ input: `${updates}\n`,
1137
+ encoding: 'utf8',
1138
+ stdio: ['pipe', 'ignore', 'ignore'],
1139
+ });
1140
+ const newTree = execFileSync(GIT, ['write-tree'], { cwd: repoRoot, env, encoding: 'utf8' }).trim();
1141
+ return newTree || undefined;
1142
+ } finally {
1143
+ try { fs.rmSync(tmpIndex, { force: true }); } catch { /* ignore */ }
1144
+ }
1145
+ } catch {
1146
+ return undefined;
1147
+ }
1148
+ }
1149
+
1150
+ export async function alignRefinerySubmodulesAfterMerge(
1151
+ repoRoot: string,
1152
+ previousBaseHead: string,
1153
+ currentHead: string,
1154
+ options: { submoduleIgnorePaths?: string[] } = {},
1155
+ ): Promise<MeshRefineSubmoduleAlignmentSummary> {
1156
+ const startedAt = Date.now();
1157
+ const changedGitlinkPaths = readChangedGitlinkPaths(repoRoot, previousBaseHead, currentHead)
1158
+ .filter(path => !(options.submoduleIgnorePaths || []).includes(path));
1159
+ const preStatus = await getGitRepoStatus(repoRoot, {
1160
+ includeSubmodules: true,
1161
+ submoduleIgnorePaths: options.submoduleIgnorePaths,
1162
+ timeoutMs: 15_000,
1163
+ // Decision path — the out-of-sync submodule set drives a mutating `submodule
1164
+ // update`. Must not act on a TTL-cached status; bypass the C1 cache.
1165
+ forceFresh: true,
1166
+ });
1167
+ const outOfSyncPaths = (preStatus.submodules || [])
1168
+ .filter(submodule => submodule.dirty || submodule.outOfSync || !!submodule.error)
1169
+ .map(submodule => submodule.path);
1170
+ const updatePaths = [...new Set([...changedGitlinkPaths, ...outOfSyncPaths])].sort();
1171
+
1172
+ if (updatePaths.length === 0) {
1173
+ return {
1174
+ status: 'skipped',
1175
+ changedGitlinkPaths,
1176
+ outOfSyncPaths,
1177
+ updatedPaths: [],
1178
+ verifiedPaths: [],
1179
+ durationMs: Date.now() - startedAt,
1180
+ reason: 'no_changed_or_out_of_sync_submodules',
1181
+ };
1182
+ }
1183
+
1184
+ const commandArgs = ['submodule', 'update', '--init', '--recursive', '--', ...updatePaths];
1185
+ try {
1186
+ const { execFile } = await import('node:child_process');
1187
+ const { promisify } = await import('node:util');
1188
+ const execFileAsync = promisify(execFile);
1189
+ const result = await execFileAsync(GIT, commandArgs, {
1190
+ cwd: repoRoot,
1191
+ encoding: 'utf8',
1192
+ maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
1193
+ timeout: 60_000,
1194
+ });
1195
+ const postStatus = await getGitRepoStatus(repoRoot, {
1196
+ includeSubmodules: true,
1197
+ submoduleIgnorePaths: options.submoduleIgnorePaths,
1198
+ timeoutMs: 15_000,
1199
+ // Re-read AFTER `submodule update` mutated the tree — MUST be fresh, never the
1200
+ // cached preStatus from moments ago (which would falsely report still-dirty).
1201
+ forceFresh: true,
1202
+ });
1203
+ const remaining = (postStatus.submodules || [])
1204
+ .filter(submodule => updatePaths.includes(submodule.path) && (submodule.dirty || submodule.outOfSync || !!submodule.error));
1205
+ return {
1206
+ status: remaining.length === 0 ? 'passed' : 'failed',
1207
+ changedGitlinkPaths,
1208
+ outOfSyncPaths,
1209
+ updatedPaths: updatePaths,
1210
+ verifiedPaths: updatePaths.filter(path => !remaining.some(submodule => submodule.path === path)),
1211
+ durationMs: Date.now() - startedAt,
1212
+ command: `git ${commandArgs.join(' ')}`,
1213
+ stdout: truncateValidationOutput(result.stdout),
1214
+ stderr: truncateValidationOutput(result.stderr),
1215
+ ...(remaining.length > 0 ? { error: `Submodule checkout remained out of sync after update: ${remaining.map(entry => entry.path).join(', ')}` } : {}),
1216
+ };
1217
+ } catch (e: any) {
1218
+ return {
1219
+ status: 'failed',
1220
+ changedGitlinkPaths,
1221
+ outOfSyncPaths,
1222
+ updatedPaths: updatePaths,
1223
+ verifiedPaths: [],
1224
+ durationMs: Date.now() - startedAt,
1225
+ command: `git ${commandArgs.join(' ')}`,
1226
+ error: e?.message || String(e),
1227
+ stdout: truncateValidationOutput(e?.stdout),
1228
+ stderr: truncateValidationOutput(e?.stderr),
1229
+ };
1230
+ }
1231
+ }
1232
+
1233
+ export async function runMeshRefineSubmoduleReachabilityGate(
1234
+ repoRoot: string,
1235
+ mergedTree: string,
1236
+ options: { allowAutoPublishSubmoduleMainCommits?: boolean; autoPublishPolicySource?: string; worktreeRoot?: string } = {},
1237
+ ): Promise<MeshRefineSubmoduleReachabilitySummary> {
1238
+ const startedAt = Date.now();
1239
+ const entries: MeshRefineSubmoduleReachabilityEntry[] = [];
1240
+ try {
1241
+ const { execFile } = await import('node:child_process');
1242
+ const { promisify } = await import('node:util');
1243
+ const execFileAsync = promisify(execFile);
1244
+ const runGit = async (cwd: string, args: string[]): Promise<string> => {
1245
+ const { stdout } = await execFileAsync(GIT, args, {
1246
+ cwd,
1247
+ encoding: 'utf8',
1248
+ timeout: 30_000,
1249
+ maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
1250
+ windowsHide: true,
1251
+ });
1252
+ return String(stdout || '');
1253
+ };
1254
+ const verifyRemoteMainContainsCommit = async (submodulePath: string, commit: string, branch = 'main'): Promise<void> => {
1255
+ await runGit(submodulePath, ['-c', 'protocol.file.allow=always', 'fetch', 'origin', `refs/heads/${branch}:refs/remotes/origin/${branch}`]);
1256
+ await runGit(submodulePath, ['merge-base', '--is-ancestor', commit, `refs/remotes/origin/${branch}`]);
1257
+ };
1258
+ const publishCommitToRemoteMain = async (submodulePath: string, commit: string, branch = 'main'): Promise<{ stdout: string; stderr: string; refspec: string }> => {
1259
+ const refspec = `${commit}:refs/heads/${branch}`;
1260
+ const { stdout, stderr } = await execFileAsync(GIT, ['push', 'origin', refspec], {
1261
+ cwd: submodulePath,
1262
+ encoding: 'utf8',
1263
+ timeout: 30_000,
1264
+ maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
1265
+ windowsHide: true,
1266
+ });
1267
+ return { stdout: String(stdout || ''), stderr: String(stderr || ''), refspec };
1268
+ };
1269
+ const importCommitFromWorktreeSubmodule = async (submodulePath: string, worktreeSubmodulePath: string, commit: string): Promise<boolean> => {
1270
+ if (!fs.existsSync(worktreeSubmodulePath)) return false;
1271
+ try {
1272
+ await runGit(worktreeSubmodulePath, ['cat-file', '-e', `${commit}^{commit}`]);
1273
+ } catch {
1274
+ return false;
1275
+ }
1276
+ await runGit(submodulePath, ['-c', 'protocol.file.allow=always', 'fetch', worktreeSubmodulePath, commit]);
1277
+ await runGit(submodulePath, ['cat-file', '-e', `${commit}^{commit}`]);
1278
+ return true;
1279
+ };
1280
+
1281
+ const treeOutput = await runGit(repoRoot, ['ls-tree', '-r', '-z', mergedTree]);
1282
+ const gitlinks = treeOutput
1283
+ .split('\0')
1284
+ .filter(Boolean)
1285
+ .map(record => {
1286
+ const match = /^160000\s+commit\s+([0-9a-f]{40})\t(.+)$/.exec(record);
1287
+ return match ? { commit: match[1], path: match[2] } : null;
1288
+ })
1289
+ .filter((entry): entry is { commit: string; path: string } => !!entry);
1290
+
1291
+ for (const gitlink of gitlinks) {
1292
+ const submodulePath = pathResolve(repoRoot, gitlink.path);
1293
+ const entry: MeshRefineSubmoduleReachabilityEntry = {
1294
+ path: gitlink.path,
1295
+ commit: gitlink.commit,
1296
+ reachable: false,
1297
+ };
1298
+ // Resolved lazily once the submodule checkout/remote are confirmed; defaults
1299
+ // to 'main' so error messages emitted before resolution stay byte-identical
1300
+ // to the pre-generalization behavior on a main-default repo.
1301
+ let submoduleDefaultBranch = 'main';
1302
+ try {
1303
+ if (!fs.existsSync(submodulePath)) {
1304
+ entry.error = `Submodule checkout missing at ${gitlink.path}`;
1305
+ entry.publishRequired = true;
1306
+ if (options.allowAutoPublishSubmoduleMainCommits === true) {
1307
+ entry.autoPublishAllowed = true;
1308
+ entry.autoPublishAttempted = false;
1309
+ entry.autoPublishSkippedReason = `submodule checkout missing at ${gitlink.path}; cannot perform non-force push to origin/main`;
1310
+ }
1311
+ entries.push(entry);
1312
+ continue;
1313
+ }
1314
+
1315
+ entry.checkedLocal = true;
1316
+ try {
1317
+ await runGit(submodulePath, ['cat-file', '-e', `${gitlink.commit}^{commit}`]);
1318
+ entry.localReachable = true;
1319
+ } catch {
1320
+ entry.localReachable = false;
1321
+ if (options.allowAutoPublishSubmoduleMainCommits === true && options.worktreeRoot) {
1322
+ try {
1323
+ const imported = await importCommitFromWorktreeSubmodule(
1324
+ submodulePath,
1325
+ pathResolve(options.worktreeRoot, gitlink.path),
1326
+ gitlink.commit,
1327
+ );
1328
+ if (imported) {
1329
+ entry.localReachable = true;
1330
+ entry.importedFromWorktree = true;
1331
+ }
1332
+ } catch (importError: any) {
1333
+ entry.autoPublishSkippedReason = `candidate commit was not present in the source checkout and could not be imported from worktree submodule: ${truncateValidationOutput(importError?.stderr || importError?.message || String(importError))}`;
1334
+ }
1335
+ }
1336
+ // Probe the submodule remote before allowing cleanup/completion.
1337
+ }
1338
+
1339
+ try {
1340
+ entry.remote = 'origin';
1341
+ let remoteUrl = '';
1342
+ try {
1343
+ remoteUrl = (await runGit(submodulePath, ['remote', 'get-url', 'origin'])).trim();
1344
+ if (!remoteUrl) throw new Error('origin remote has no URL');
1345
+ entry.remoteUrl = remoteUrl;
1346
+ } catch {
1347
+ entry.error = 'Submodule remote reachability check failed: no configured origin remote';
1348
+ entry.publishRequired = true;
1349
+ if (options.allowAutoPublishSubmoduleMainCommits === true) {
1350
+ entry.autoPublishAllowed = true;
1351
+ entry.autoPublishAttempted = false;
1352
+ entry.autoPublishSkippedReason = 'submodule origin remote is not configured; cannot perform non-force push to origin/main';
1353
+ }
1354
+ entries.push(entry);
1355
+ continue;
1356
+ }
1357
+ // Generalize the submodule's default branch (F18): '.gitmodules'
1358
+ // branch → local remote HEAD → remote-advertised HEAD → 'main'. On a
1359
+ // main-default submodule this resolves to 'main' and every ref target
1360
+ // below is byte-identical to the prior hardcoded path.
1361
+ submoduleDefaultBranch = await resolveSubmoduleDefaultBranch({
1362
+ submoduleRepoPath: submodulePath,
1363
+ superprojectWorkspace: repoRoot,
1364
+ submodulePath: gitlink.path,
1365
+ });
1366
+ entry.remoteMainBranch = submoduleDefaultBranch;
1367
+ try {
1368
+ await verifyRemoteMainContainsCommit(submodulePath, gitlink.commit, submoduleDefaultBranch);
1369
+ entry.fetchedFromOrigin = true;
1370
+ entry.remoteReachable = true;
1371
+ entry.remoteMainReachable = true;
1372
+ entry.reachable = true;
1373
+ } catch (e: any) {
1374
+ entry.remoteReachable = false;
1375
+ entry.remoteMainReachable = false;
1376
+ entry.publishRequired = true;
1377
+ const details = truncateValidationOutput(e?.stderr || e?.message || String(e));
1378
+ entry.error = `Submodule remote main reachability check failed for origin/${submoduleDefaultBranch}: ${details}`;
1379
+ if (options.allowAutoPublishSubmoduleMainCommits === true && entry.localReachable === true) {
1380
+ entry.autoPublishAllowed = true;
1381
+ entry.autoPublishAttempted = true;
1382
+ try {
1383
+ const publish = await publishCommitToRemoteMain(submodulePath, gitlink.commit, submoduleDefaultBranch);
1384
+ entry.autoPublishRefspec = publish.refspec;
1385
+ entry.publishStdout = truncateValidationOutput(publish.stdout);
1386
+ entry.publishStderr = truncateValidationOutput(publish.stderr);
1387
+ entry.autoPublishSucceeded = true;
1388
+ await verifyRemoteMainContainsCommit(submodulePath, gitlink.commit, submoduleDefaultBranch);
1389
+ entry.fetchedFromOrigin = true;
1390
+ entry.remoteReachable = true;
1391
+ entry.remoteMainReachable = true;
1392
+ entry.autoPublishVerified = true;
1393
+ entry.publishRequired = false;
1394
+ entry.reachable = true;
1395
+ entry.error = undefined;
1396
+ } catch (publishError: any) {
1397
+ entry.autoPublishSucceeded = false;
1398
+ entry.autoPublishVerified = false;
1399
+ const publishDetails = truncateValidationOutput(publishError?.stderr || publishError?.message || String(publishError));
1400
+ entry.error = `Submodule auto-publish to origin/${submoduleDefaultBranch} failed or could not be verified: ${publishDetails}`;
1401
+ }
1402
+ } else if (options.allowAutoPublishSubmoduleMainCommits === true) {
1403
+ entry.autoPublishAllowed = true;
1404
+ entry.autoPublishAttempted = false;
1405
+ entry.autoPublishSkippedReason = entry.autoPublishSkippedReason
1406
+ || `candidate commit is not reachable in the source checkout or worktree submodule, so Refinery cannot push it to origin/${submoduleDefaultBranch}`;
1407
+ }
1408
+ }
1409
+ } catch (e: any) {
1410
+ entry.remoteReachable = false;
1411
+ entry.remoteMainReachable = false;
1412
+ entry.publishRequired = true;
1413
+ const details = truncateValidationOutput(e?.stderr || e?.message || String(e));
1414
+ entry.error = `Submodule remote main reachability check failed for origin/${submoduleDefaultBranch}: ${details}`;
1415
+ }
1416
+ } catch (e: any) {
1417
+ entry.error = truncateValidationOutput(e?.message || String(e));
1418
+ entry.publishRequired = true;
1419
+ }
1420
+ entries.push(entry);
1421
+ }
1422
+
1423
+ const unreachable = entries.filter(entry => !entry.reachable);
1424
+ return {
1425
+ status: unreachable.length ? 'failed' : 'passed',
1426
+ checked: entries.length,
1427
+ unreachable: unreachable.map(entry => ({ ...entry, publishRequired: entry.publishRequired !== false })),
1428
+ entries: entries.map(entry => entry.reachable ? entry : { ...entry, publishRequired: entry.publishRequired !== false }),
1429
+ durationMs: Date.now() - startedAt,
1430
+ autoPublishAllowed: options.allowAutoPublishSubmoduleMainCommits === true,
1431
+ autoPublishPolicySource: options.autoPublishPolicySource,
1432
+ };
1433
+ } catch (e: any) {
1434
+ const unreachable = entries.filter(entry => !entry.reachable).map(entry => ({ ...entry, publishRequired: true }));
1435
+ return {
1436
+ status: 'failed',
1437
+ checked: entries.length,
1438
+ unreachable,
1439
+ entries: entries.map(entry => entry.reachable ? entry : { ...entry, publishRequired: true }),
1440
+ durationMs: Date.now() - startedAt,
1441
+ autoPublishAllowed: options.allowAutoPublishSubmoduleMainCommits === true,
1442
+ autoPublishPolicySource: options.autoPublishPolicySource,
1443
+ error: truncateValidationOutput(e?.message || String(e)),
1444
+ };
1445
+ }
1446
+ }
1447
+
1448
+ export function buildMeshRefineValidationPlan(mesh: any, workspace: string): Record<string, unknown> {
1449
+ const plan = resolveMeshRefineValidationPlan(mesh, workspace);
1450
+ const mapCommand = (command: MeshRefineValidationCommandPlan) => ({
1451
+ displayCommand: command.displayCommand,
1452
+ category: command.category,
1453
+ source: command.source,
1454
+ cwd: command.cwd,
1455
+ timeoutMs: command.timeoutMs,
1456
+ });
1457
+ return {
1458
+ source: plan.source,
1459
+ sourceType: plan.sourceType,
1460
+ bootstrapCommands: plan.bootstrapCommands.map(mapCommand),
1461
+ commands: plan.commands.map(mapCommand),
1462
+ unavailableReason: plan.unavailableReason,
1463
+ rejectedCommands: plan.rejectedCommands,
1464
+ suggestions: plan.suggestions,
1465
+ suggestedConfig: plan.suggestedConfig,
1466
+ note: plan.sourceType === 'unavailable'
1467
+ ? 'No validation command will be executed until a repo mesh/refine config is provided. Heuristics are suggestions only.'
1468
+ : 'Validation commands are resolved from repo mesh/refine config; heuristics are suggestions only.',
1469
+ };
1470
+ }
1471
+
1472
+ export async function runMeshRefineValidationGate(
1473
+ mesh: any,
1474
+ workspace: string,
1475
+ opts?: {
1476
+ /** M2-2: persisted node bootstrap state for staleness evaluation. */
1477
+ persistedBootstrapState?: WorktreeBootstrapState | null;
1478
+ /** M2-2: called after an inherit-mode bootstrap run so the caller can persist the new state. */
1479
+ onBootstrapStateChange?: (state: WorktreeBootstrapState) => void;
1480
+ },
1481
+ ): Promise<MeshRefineValidationSummary> {
1482
+ const { execFile } = await import('node:child_process');
1483
+ const { promisify } = await import('node:util');
1484
+ const execFileAsync = promisify(execFile);
1485
+ const selection = resolveMeshRefineValidationPlan(mesh, workspace);
1486
+ const summary: MeshRefineValidationSummary = {
1487
+ status: 'skipped',
1488
+ required: true,
1489
+ commandsRun: [],
1490
+ bootstrapCommandsRun: [],
1491
+ rejectedCommands: selection.rejectedCommands,
1492
+ skippedReason: undefined,
1493
+ timeoutMs: REFINE_VALIDATION_TIMEOUT_MS,
1494
+ outputLimitBytes: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
1495
+ configSource: selection.source,
1496
+ configSourceType: selection.sourceType,
1497
+ suggestions: selection.suggestions,
1498
+ suggestedConfig: selection.suggestedConfig,
1499
+ ...(selection.deprecationWarnings.length > 0 ? { deprecationWarnings: selection.deprecationWarnings } : {}),
1500
+ };
1501
+
1502
+ if (!selection.commands.length) {
1503
+ summary.skippedReason = selection.unavailableReason || 'validation_unavailable: repo mesh/refine config did not provide executable validation.commands';
1504
+ return summary;
1505
+ }
1506
+
1507
+ // ── M2-2: Bootstrap stage — refine consumes the worktree_bootstrap config
1508
+ // instead of defining its own. Legacy validation.bootstrapCommands run
1509
+ // only when no worktree_bootstrap config exists (deprecation path).
1510
+ let runLegacyBootstrapCommands = selection.bootstrapCommands.length > 0;
1511
+ if (selection.bootstrapMode === 'skip') {
1512
+ summary.bootstrap = { stage: 'skipped', skipped: true };
1513
+ runLegacyBootstrapCommands = false;
1514
+ } else {
1515
+ const wbLoad = loadMeshWorktreeBootstrapConfig(mesh, workspace);
1516
+ const wbUsable = !!wbLoad.config && wbLoad.sourceType !== 'invalid'
1517
+ && wbLoad.config.enabled !== false && wbLoad.config.runOnClone !== false;
1518
+ if (wbUsable) {
1519
+ runLegacyBootstrapCommands = false; // worktree_bootstrap wins over deprecated bootstrapCommands
1520
+ const evaluated = evaluateWorktreeBootstrapState(mesh, workspace, opts?.persistedBootstrapState);
1521
+ if (evaluated.status === 'ready') {
1522
+ summary.bootstrap = { stage: 'cached', status: 'ready', skipped: true, configSource: evaluated.configSource };
1523
+ } else {
1524
+ const ran = await runMeshWorktreeBootstrap(mesh, workspace);
1525
+ try { opts?.onBootstrapStateChange?.(ran); } catch { /* persistence is best-effort */ }
1526
+ if (ran.status === 'ready') {
1527
+ summary.bootstrap = {
1528
+ stage: 'ran',
1529
+ status: 'ready',
1530
+ configSource: ran.configSource,
1531
+ ...(evaluated.staleReason ? { staleReason: evaluated.staleReason } : {}),
1532
+ commandsRun: ran.commandsRun,
1533
+ };
1534
+ } else {
1535
+ summary.bootstrap = {
1536
+ stage: 'failed',
1537
+ status: ran.status,
1538
+ configSource: ran.configSource,
1539
+ error: ran.error,
1540
+ commandsRun: ran.commandsRun,
1541
+ };
1542
+ summary.status = 'failed';
1543
+ summary.failureKind = 'dependency_bootstrap_failed';
1544
+ summary.failureCode = 'dependency_bootstrap_failed';
1545
+ return summary;
1546
+ }
1547
+ }
1548
+ } else if (!runLegacyBootstrapCommands) {
1549
+ summary.bootstrap = { stage: 'not_configured' };
1550
+ }
1551
+ }
1552
+
1553
+ const commandRecord = (candidate: MeshRefineValidationCommand, cwd: string, startedAt: number, result: any, passed: boolean, extras: Record<string, unknown> = {}) => ({
1554
+ command: candidate.command,
1555
+ args: candidate.args,
1556
+ displayCommand: candidate.displayCommand,
1557
+ category: candidate.category,
1558
+ source: candidate.source,
1559
+ cwd,
1560
+ passed,
1561
+ durationMs: Date.now() - startedAt,
1562
+ stdout: truncateValidationOutput(result?.stdout),
1563
+ stderr: truncateValidationOutput(result?.stderr || result?.message),
1564
+ ...extras,
1565
+ });
1566
+ const isPackageManagerValidation = (candidate: MeshRefineValidationCommand): boolean => {
1567
+ const command = pathBasename(candidate.command).replace(/\.(?:cmd|exe)$/i, '');
1568
+ return ['npm', 'pnpm', 'yarn', 'bun'].includes(command)
1569
+ && candidate.args.some(arg => arg === 'run' || arg === 'test' || arg === 'exec');
1570
+ };
1571
+ const dependenciesLikelyMissing = (cwd: string): boolean => {
1572
+ if (!fs.existsSync(pathJoin(cwd, 'package.json'))) return false;
1573
+ if (fs.existsSync(pathJoin(cwd, 'node_modules'))) return false;
1574
+ return ['package-lock.json', 'npm-shrinkwrap.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lockb', 'bun.lock']
1575
+ .some(lock => fs.existsSync(pathJoin(cwd, lock)));
1576
+ };
1577
+
1578
+ if (runLegacyBootstrapCommands) {
1579
+ summary.bootstrap = { stage: 'legacy' };
1580
+ for (const candidate of selection.bootstrapCommands) {
1581
+ const startedAt = Date.now();
1582
+ const cwd = candidate.cwd ? pathResolve(workspace, candidate.cwd) : workspace;
1583
+ const timeout = candidate.timeoutMs || REFINE_VALIDATION_TIMEOUT_MS;
1584
+ // On win32, libuv's spawn search only appends .com/.exe (not .cmd/.bat),
1585
+ // so a bare `npm`/`npx`/`tsc` (which are .cmd shims) throws spawn ENOENT.
1586
+ // Resolve to an absolute path via the same helper the PTY path uses
1587
+ // (no-op on non-win32 and when the command is already absolute).
1588
+ const resolvedCommand = resolveWin32Executable(candidate.command);
1589
+ // A win32 .cmd/.bat shim cannot be exec'd directly — wrap it in
1590
+ // cmd.exe /c (no-op off win32 / for a real .exe). Keep
1591
+ // resolvedCommand for diagnostics.
1592
+ const spawn = buildWin32ExecFileSpawn(resolvedCommand, candidate.args);
1593
+ try {
1594
+ const result = await execFileAsync(spawn.file, spawn.args, {
1595
+ cwd,
1596
+ encoding: 'utf8',
1597
+ timeout,
1598
+ maxBuffer: candidate.outputLimitBytes || REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
1599
+ env: { ...process.env, CI: process.env.CI || '1', ...(candidate.env || {}) },
1600
+ ...(spawn.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
1601
+ });
1602
+ summary.bootstrapCommandsRun.push(commandRecord(candidate, cwd, startedAt, result, true, { exitCode: 0 }));
1603
+ } catch (error: any) {
1604
+ const spawnResolutionFailed = isSpawnResolutionError(error);
1605
+ summary.bootstrapCommandsRun.push(commandRecord(candidate, cwd, startedAt, error, false, {
1606
+ exitCode: typeof error?.code === 'number' ? error.code : null,
1607
+ signal: typeof error?.signal === 'string' ? error.signal : null,
1608
+ timedOut: error?.killed === true || /timed out/i.test(String(error?.message || '')),
1609
+ ...(spawnResolutionFailed
1610
+ ? { failureKind: 'spawn_resolution_failed', resolvedCommand }
1611
+ : { failureKind: 'dependency_bootstrap_failed' }),
1612
+ }));
1613
+ summary.bootstrap = { stage: 'failed', error: describeSpawnError(error, resolvedCommand, spawnResolutionFailed) };
1614
+ summary.status = 'failed';
1615
+ summary.failureKind = spawnResolutionFailed ? 'spawn_resolution_failed' : 'dependency_bootstrap_failed';
1616
+ summary.failureCode = spawnResolutionFailed ? 'spawn_resolution_failed' : 'dependency_bootstrap_failed';
1617
+ return summary;
1618
+ }
1619
+ }
1620
+ }
1621
+
1622
+ for (const candidate of selection.commands) {
1623
+ const startedAt = Date.now();
1624
+ const cwd = candidate.cwd ? pathResolve(workspace, candidate.cwd) : workspace;
1625
+ const timeout = candidate.timeoutMs || REFINE_VALIDATION_TIMEOUT_MS;
1626
+ const bootstrapProvidedDependencies = summary.bootstrap?.stage === 'cached' || summary.bootstrap?.stage === 'ran' || summary.bootstrap?.stage === 'legacy';
1627
+ if (!bootstrapProvidedDependencies && isPackageManagerValidation(candidate) && dependenciesLikelyMissing(cwd)) {
1628
+ summary.commandsRun.push(commandRecord(candidate, cwd, startedAt, {
1629
+ stderr: 'Dependencies appear to be missing: package.json and a lockfile are present, but node_modules is absent. Configure validation.bootstrapCommands in repo mesh/refine config if Refinery should install/bootstrap before validation.',
1630
+ }, false, {
1631
+ exitCode: null,
1632
+ skipped: true,
1633
+ failureKind: 'missing_dependencies',
1634
+ }));
1635
+ summary.status = 'failed';
1636
+ summary.failureKind = 'missing_dependencies';
1637
+ summary.failureCode = 'missing_dependencies';
1638
+ return summary;
1639
+ }
1640
+ // See the bootstrap loop above: resolve the win32 .cmd shim to an
1641
+ // absolute path before handing it to the spawn boundary.
1642
+ const resolvedCommand = resolveWin32Executable(candidate.command);
1643
+ const spawn = buildWin32ExecFileSpawn(resolvedCommand, candidate.args);
1644
+ try {
1645
+ const result = await execFileAsync(spawn.file, spawn.args, {
1646
+ cwd,
1647
+ encoding: 'utf8',
1648
+ timeout,
1649
+ maxBuffer: candidate.outputLimitBytes || REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
1650
+ env: { ...process.env, CI: process.env.CI || '1', ...(candidate.env || {}) },
1651
+ ...(spawn.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
1652
+ });
1653
+ summary.commandsRun.push(commandRecord(candidate, cwd, startedAt, result, true, { exitCode: 0 }));
1654
+ } catch (error: any) {
1655
+ // ENOENT check first: a spawn-resolution failure ("spawn npm ENOENT")
1656
+ // carries no stderr and would otherwise fall through to an
1657
+ // unclassified generic failure. Classify it distinctly so the
1658
+ // coordinator surfaces the real cause (win32 .cmd resolution).
1659
+ const spawnResolutionFailed = isSpawnResolutionError(error);
1660
+ const stderr = truncateValidationOutput(error?.stderr || error?.message);
1661
+ const missingDependencyFailure = !spawnResolutionFailed
1662
+ && /Cannot find module|MODULE_NOT_FOUND|node_modules|command not found|not found/i.test(stderr);
1663
+ summary.commandsRun.push(commandRecord(candidate, cwd, startedAt, error, false, {
1664
+ exitCode: typeof error?.code === 'number' ? error.code : null,
1665
+ signal: typeof error?.signal === 'string' ? error.signal : null,
1666
+ timedOut: error?.killed === true || /timed out/i.test(String(error?.message || '')),
1667
+ ...(spawnResolutionFailed
1668
+ ? { failureKind: 'spawn_resolution_failed', resolvedCommand }
1669
+ : missingDependencyFailure ? { failureKind: 'missing_dependencies' } : {}),
1670
+ }));
1671
+ summary.status = 'failed';
1672
+ if (spawnResolutionFailed) {
1673
+ summary.failureKind = 'spawn_resolution_failed';
1674
+ summary.failureCode = 'spawn_resolution_failed';
1675
+ summary.spawnResolutionError = describeSpawnError(error, resolvedCommand, true);
1676
+ } else if (missingDependencyFailure) {
1677
+ summary.failureKind = 'missing_dependencies';
1678
+ summary.failureCode = 'missing_dependencies';
1679
+ }
1680
+ return summary;
1681
+ }
1682
+ }
1683
+
1684
+ summary.status = 'passed';
1685
+ return summary;
1686
+ }