oh-my-codex 0.11.11 → 0.11.13

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 (342) hide show
  1. package/Cargo.lock +5 -5
  2. package/Cargo.toml +1 -1
  3. package/README.de.md +12 -6
  4. package/README.el.md +223 -0
  5. package/README.es.md +12 -6
  6. package/README.fr.md +11 -5
  7. package/README.it.md +12 -6
  8. package/README.ja.md +12 -6
  9. package/README.ko.md +12 -6
  10. package/README.md +56 -28
  11. package/README.pl.md +216 -0
  12. package/README.pt.md +12 -6
  13. package/README.ru.md +12 -6
  14. package/README.tr.md +12 -6
  15. package/README.vi.md +148 -183
  16. package/README.zh-TW.md +14 -17
  17. package/README.zh.md +12 -6
  18. package/crates/omx-runtime-core/src/engine.rs +122 -4
  19. package/crates/omx-runtime-core/src/lib.rs +17 -0
  20. package/dist/autoresearch/contracts.d.ts.map +1 -1
  21. package/dist/autoresearch/contracts.js +1 -0
  22. package/dist/autoresearch/contracts.js.map +1 -1
  23. package/dist/autoresearch/runtime.d.ts.map +1 -1
  24. package/dist/autoresearch/runtime.js +7 -1
  25. package/dist/autoresearch/runtime.js.map +1 -1
  26. package/dist/cli/__tests__/agents.test.js +24 -1
  27. package/dist/cli/__tests__/agents.test.js.map +1 -1
  28. package/dist/cli/__tests__/autoresearch.test.js +11 -0
  29. package/dist/cli/__tests__/autoresearch.test.js.map +1 -1
  30. package/dist/cli/__tests__/cleanup.test.js +117 -4
  31. package/dist/cli/__tests__/cleanup.test.js.map +1 -1
  32. package/dist/cli/__tests__/doctor-warning-copy.test.js +33 -3
  33. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  34. package/dist/cli/__tests__/error-handling-warnings.test.js +13 -0
  35. package/dist/cli/__tests__/error-handling-warnings.test.js.map +1 -1
  36. package/dist/cli/__tests__/exec.test.js +6 -0
  37. package/dist/cli/__tests__/exec.test.js.map +1 -1
  38. package/dist/cli/__tests__/index.test.js +101 -1
  39. package/dist/cli/__tests__/index.test.js.map +1 -1
  40. package/dist/cli/__tests__/launch-fallback.test.js +3 -0
  41. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  42. package/dist/cli/__tests__/package-bin-contract.test.js +10 -0
  43. package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
  44. package/dist/cli/__tests__/packaged-script-resolution.test.js +4 -3
  45. package/dist/cli/__tests__/packaged-script-resolution.test.js.map +1 -1
  46. package/dist/cli/__tests__/resume.test.js +6 -0
  47. package/dist/cli/__tests__/resume.test.js.map +1 -1
  48. package/dist/cli/__tests__/setup-refresh.test.js +29 -12
  49. package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
  50. package/dist/cli/__tests__/setup-scope.test.js +1 -1
  51. package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
  52. package/dist/cli/__tests__/star-prompt.test.js +16 -0
  53. package/dist/cli/__tests__/star-prompt.test.js.map +1 -1
  54. package/dist/cli/__tests__/uninstall.test.js +112 -1
  55. package/dist/cli/__tests__/uninstall.test.js.map +1 -1
  56. package/dist/cli/__tests__/windows-popup-loop-contract.test.d.ts +2 -0
  57. package/dist/cli/__tests__/windows-popup-loop-contract.test.d.ts.map +1 -0
  58. package/dist/cli/__tests__/windows-popup-loop-contract.test.js +30 -0
  59. package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -0
  60. package/dist/cli/agents.d.ts.map +1 -1
  61. package/dist/cli/agents.js +9 -3
  62. package/dist/cli/agents.js.map +1 -1
  63. package/dist/cli/autoresearch-guided.d.ts.map +1 -1
  64. package/dist/cli/autoresearch-guided.js +9 -3
  65. package/dist/cli/autoresearch-guided.js.map +1 -1
  66. package/dist/cli/autoresearch.d.ts.map +1 -1
  67. package/dist/cli/autoresearch.js +8 -2
  68. package/dist/cli/autoresearch.js.map +1 -1
  69. package/dist/cli/cleanup.d.ts +2 -0
  70. package/dist/cli/cleanup.d.ts.map +1 -1
  71. package/dist/cli/cleanup.js +27 -1
  72. package/dist/cli/cleanup.js.map +1 -1
  73. package/dist/cli/doctor.js +7 -0
  74. package/dist/cli/doctor.js.map +1 -1
  75. package/dist/cli/index.d.ts +9 -1
  76. package/dist/cli/index.d.ts.map +1 -1
  77. package/dist/cli/index.js +171 -55
  78. package/dist/cli/index.js.map +1 -1
  79. package/dist/cli/setup.d.ts.map +1 -1
  80. package/dist/cli/setup.js +18 -15
  81. package/dist/cli/setup.js.map +1 -1
  82. package/dist/cli/star-prompt.d.ts.map +1 -1
  83. package/dist/cli/star-prompt.js +2 -0
  84. package/dist/cli/star-prompt.js.map +1 -1
  85. package/dist/cli/team.d.ts.map +1 -1
  86. package/dist/cli/team.js +5 -1
  87. package/dist/cli/team.js.map +1 -1
  88. package/dist/cli/tmux-hook.d.ts.map +1 -1
  89. package/dist/cli/tmux-hook.js +4 -1
  90. package/dist/cli/tmux-hook.js.map +1 -1
  91. package/dist/cli/uninstall.d.ts.map +1 -1
  92. package/dist/cli/uninstall.js +26 -0
  93. package/dist/cli/uninstall.js.map +1 -1
  94. package/dist/cli/update.d.ts.map +1 -1
  95. package/dist/cli/update.js +1 -0
  96. package/dist/cli/update.js.map +1 -1
  97. package/dist/compat/__tests__/rust-runtime-compat.test.js +84 -1
  98. package/dist/compat/__tests__/rust-runtime-compat.test.js.map +1 -1
  99. package/dist/config/__tests__/generator-idempotent.test.js +4 -4
  100. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  101. package/dist/config/__tests__/mcp-registry.test.js +13 -16
  102. package/dist/config/__tests__/mcp-registry.test.js.map +1 -1
  103. package/dist/config/mcp-registry.d.ts +1 -0
  104. package/dist/config/mcp-registry.d.ts.map +1 -1
  105. package/dist/config/mcp-registry.js +4 -4
  106. package/dist/config/mcp-registry.js.map +1 -1
  107. package/dist/config/models.d.ts +1 -0
  108. package/dist/config/models.d.ts.map +1 -1
  109. package/dist/config/models.js +39 -1
  110. package/dist/config/models.js.map +1 -1
  111. package/dist/hooks/__tests__/keyword-detector.test.js +12 -1
  112. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  113. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +554 -18
  114. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  115. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +347 -16
  116. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
  117. package/dist/hooks/__tests__/notify-hook-modules.test.js +5 -0
  118. package/dist/hooks/__tests__/notify-hook-modules.test.js.map +1 -1
  119. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.d.ts +2 -0
  120. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.d.ts.map +1 -0
  121. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js +597 -0
  122. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js.map +1 -0
  123. package/dist/hooks/__tests__/notify-hook-regression-205.test.js +19 -1
  124. package/dist/hooks/__tests__/notify-hook-regression-205.test.js.map +1 -1
  125. package/dist/hooks/__tests__/notify-hook-session-scope.test.js +73 -53
  126. package/dist/hooks/__tests__/notify-hook-session-scope.test.js.map +1 -1
  127. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +193 -2
  128. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
  129. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +183 -0
  130. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  131. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +255 -97
  132. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
  133. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js +0 -0
  134. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +1 -1
  135. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +46 -0
  136. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
  137. package/dist/hooks/__tests__/prompt-team-routing.test.js +34 -0
  138. package/dist/hooks/__tests__/prompt-team-routing.test.js.map +1 -1
  139. package/dist/hooks/__tests__/tmux-hook-engine.test.js +32 -1
  140. package/dist/hooks/__tests__/tmux-hook-engine.test.js.map +1 -1
  141. package/dist/hooks/code-simplifier/index.d.ts.map +1 -1
  142. package/dist/hooks/code-simplifier/index.js +1 -0
  143. package/dist/hooks/code-simplifier/index.js.map +1 -1
  144. package/dist/hooks/codebase-map.d.ts.map +1 -1
  145. package/dist/hooks/codebase-map.js +1 -0
  146. package/dist/hooks/codebase-map.js.map +1 -1
  147. package/dist/hooks/extensibility/sdk/tmux.d.ts.map +1 -1
  148. package/dist/hooks/extensibility/sdk/tmux.js +3 -1
  149. package/dist/hooks/extensibility/sdk/tmux.js.map +1 -1
  150. package/dist/hooks/keyword-detector.d.ts +1 -0
  151. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  152. package/dist/hooks/keyword-detector.js +48 -0
  153. package/dist/hooks/keyword-detector.js.map +1 -1
  154. package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
  155. package/dist/hooks/prompt-guidance-contract.js +6 -0
  156. package/dist/hooks/prompt-guidance-contract.js.map +1 -1
  157. package/dist/hooks/session.d.ts.map +1 -1
  158. package/dist/hooks/session.js +1 -0
  159. package/dist/hooks/session.js.map +1 -1
  160. package/dist/hud/__tests__/state.test.js +70 -1
  161. package/dist/hud/__tests__/state.test.js.map +1 -1
  162. package/dist/hud/authority.d.ts.map +1 -1
  163. package/dist/hud/authority.js +1 -0
  164. package/dist/hud/authority.js.map +1 -1
  165. package/dist/hud/state.d.ts.map +1 -1
  166. package/dist/hud/state.js +52 -0
  167. package/dist/hud/state.js.map +1 -1
  168. package/dist/mcp/state-server.d.ts.map +1 -1
  169. package/dist/mcp/state-server.js +5 -0
  170. package/dist/mcp/state-server.js.map +1 -1
  171. package/dist/modes/__tests__/base-session-scope.test.js +46 -0
  172. package/dist/modes/__tests__/base-session-scope.test.js.map +1 -1
  173. package/dist/modes/base.d.ts.map +1 -1
  174. package/dist/modes/base.js +4 -0
  175. package/dist/modes/base.js.map +1 -1
  176. package/dist/notifications/__tests__/custom-alias-enablement.test.d.ts +2 -0
  177. package/dist/notifications/__tests__/custom-alias-enablement.test.d.ts.map +1 -0
  178. package/dist/notifications/__tests__/custom-alias-enablement.test.js +84 -0
  179. package/dist/notifications/__tests__/custom-alias-enablement.test.js.map +1 -0
  180. package/dist/notifications/__tests__/idle-cooldown.test.js +55 -0
  181. package/dist/notifications/__tests__/idle-cooldown.test.js.map +1 -1
  182. package/dist/notifications/idle-cooldown.d.ts +8 -6
  183. package/dist/notifications/idle-cooldown.d.ts.map +1 -1
  184. package/dist/notifications/idle-cooldown.js +53 -22
  185. package/dist/notifications/idle-cooldown.js.map +1 -1
  186. package/dist/notifications/notifier.js +1 -1
  187. package/dist/notifications/notifier.js.map +1 -1
  188. package/dist/notifications/reply-listener.d.ts.map +1 -1
  189. package/dist/notifications/reply-listener.js +1 -0
  190. package/dist/notifications/reply-listener.js.map +1 -1
  191. package/dist/notifications/tmux.d.ts.map +1 -1
  192. package/dist/notifications/tmux.js +4 -0
  193. package/dist/notifications/tmux.js.map +1 -1
  194. package/dist/openclaw/config.js +2 -2
  195. package/dist/openclaw/config.js.map +1 -1
  196. package/dist/runtime/bridge.d.ts +2 -0
  197. package/dist/runtime/bridge.d.ts.map +1 -1
  198. package/dist/runtime/bridge.js +8 -0
  199. package/dist/runtime/bridge.js.map +1 -1
  200. package/dist/scripts/notify-fallback-watcher.js +103 -53
  201. package/dist/scripts/notify-fallback-watcher.js.map +1 -1
  202. package/dist/scripts/notify-hook/auto-nudge.d.ts +2 -1
  203. package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
  204. package/dist/scripts/notify-hook/auto-nudge.js +90 -104
  205. package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
  206. package/dist/scripts/notify-hook/managed-tmux.d.ts +19 -0
  207. package/dist/scripts/notify-hook/managed-tmux.d.ts.map +1 -0
  208. package/dist/scripts/notify-hook/managed-tmux.js +320 -0
  209. package/dist/scripts/notify-hook/managed-tmux.js.map +1 -0
  210. package/dist/scripts/notify-hook/operational-events.d.ts.map +1 -1
  211. package/dist/scripts/notify-hook/operational-events.js +2 -0
  212. package/dist/scripts/notify-hook/operational-events.js.map +1 -1
  213. package/dist/scripts/notify-hook/ralph-session-resume.d.ts +22 -0
  214. package/dist/scripts/notify-hook/ralph-session-resume.d.ts.map +1 -0
  215. package/dist/scripts/notify-hook/ralph-session-resume.js +277 -0
  216. package/dist/scripts/notify-hook/ralph-session-resume.js.map +1 -0
  217. package/dist/scripts/notify-hook/state-io.d.ts +1 -1
  218. package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
  219. package/dist/scripts/notify-hook/state-io.js +2 -10
  220. package/dist/scripts/notify-hook/state-io.js.map +1 -1
  221. package/dist/scripts/notify-hook/team-dispatch.d.ts +1 -1
  222. package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
  223. package/dist/scripts/notify-hook/team-dispatch.js +123 -72
  224. package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
  225. package/dist/scripts/notify-hook/team-leader-nudge.d.ts +2 -1
  226. package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
  227. package/dist/scripts/notify-hook/team-leader-nudge.js +13 -5
  228. package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
  229. package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
  230. package/dist/scripts/notify-hook/team-tmux-guard.js +1 -19
  231. package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
  232. package/dist/scripts/notify-hook/team-worker.js +4 -4
  233. package/dist/scripts/notify-hook/team-worker.js.map +1 -1
  234. package/dist/scripts/notify-hook/tmux-injection.d.ts +1 -1
  235. package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
  236. package/dist/scripts/notify-hook/tmux-injection.js +102 -35
  237. package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
  238. package/dist/scripts/notify-hook.js +144 -20
  239. package/dist/scripts/notify-hook.js.map +1 -1
  240. package/dist/scripts/run-provider-advisor.js +2 -0
  241. package/dist/scripts/run-provider-advisor.js.map +1 -1
  242. package/dist/scripts/run-test-files.d.ts +2 -0
  243. package/dist/scripts/run-test-files.d.ts.map +1 -0
  244. package/dist/scripts/run-test-files.js +41 -0
  245. package/dist/scripts/run-test-files.js.map +1 -0
  246. package/dist/scripts/tmux-hook-engine.d.ts +2 -0
  247. package/dist/scripts/tmux-hook-engine.d.ts.map +1 -1
  248. package/dist/scripts/tmux-hook-engine.js +15 -0
  249. package/dist/scripts/tmux-hook-engine.js.map +1 -1
  250. package/dist/team/__tests__/api-interop.test.js +136 -4
  251. package/dist/team/__tests__/api-interop.test.js.map +1 -1
  252. package/dist/team/__tests__/leader-activity.test.js +107 -2
  253. package/dist/team/__tests__/leader-activity.test.js.map +1 -1
  254. package/dist/team/__tests__/runtime-cli.test.js +32 -0
  255. package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
  256. package/dist/team/__tests__/runtime.test.js +148 -0
  257. package/dist/team/__tests__/runtime.test.js.map +1 -1
  258. package/dist/team/__tests__/shutdown-fallback.test.js +13 -0
  259. package/dist/team/__tests__/shutdown-fallback.test.js.map +1 -1
  260. package/dist/team/__tests__/state-root.test.js +11 -1
  261. package/dist/team/__tests__/state-root.test.js.map +1 -1
  262. package/dist/team/__tests__/state.test.js +237 -0
  263. package/dist/team/__tests__/state.test.js.map +1 -1
  264. package/dist/team/__tests__/tmux-session.test.js +521 -2
  265. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  266. package/dist/team/api-interop.d.ts.map +1 -1
  267. package/dist/team/api-interop.js +41 -31
  268. package/dist/team/api-interop.js.map +1 -1
  269. package/dist/team/commit-hygiene.d.ts +60 -0
  270. package/dist/team/commit-hygiene.d.ts.map +1 -0
  271. package/dist/team/commit-hygiene.js +232 -0
  272. package/dist/team/commit-hygiene.js.map +1 -0
  273. package/dist/team/leader-activity.d.ts.map +1 -1
  274. package/dist/team/leader-activity.js +56 -4
  275. package/dist/team/leader-activity.js.map +1 -1
  276. package/dist/team/runtime-cli.d.ts +9 -1
  277. package/dist/team/runtime-cli.d.ts.map +1 -1
  278. package/dist/team/runtime-cli.js +15 -6
  279. package/dist/team/runtime-cli.js.map +1 -1
  280. package/dist/team/runtime.d.ts +7 -2
  281. package/dist/team/runtime.d.ts.map +1 -1
  282. package/dist/team/runtime.js +392 -171
  283. package/dist/team/runtime.js.map +1 -1
  284. package/dist/team/scaling.d.ts.map +1 -1
  285. package/dist/team/scaling.js +6 -2
  286. package/dist/team/scaling.js.map +1 -1
  287. package/dist/team/state/dispatch.d.ts +2 -0
  288. package/dist/team/state/dispatch.d.ts.map +1 -1
  289. package/dist/team/state/dispatch.js +86 -40
  290. package/dist/team/state/dispatch.js.map +1 -1
  291. package/dist/team/state/mailbox.d.ts +3 -0
  292. package/dist/team/state/mailbox.d.ts.map +1 -1
  293. package/dist/team/state/mailbox.js +93 -19
  294. package/dist/team/state/mailbox.js.map +1 -1
  295. package/dist/team/state-root.d.ts +1 -1
  296. package/dist/team/state-root.d.ts.map +1 -1
  297. package/dist/team/state-root.js +8 -3
  298. package/dist/team/state-root.js.map +1 -1
  299. package/dist/team/state.d.ts.map +1 -1
  300. package/dist/team/state.js +96 -2
  301. package/dist/team/state.js.map +1 -1
  302. package/dist/team/tmux-session.d.ts.map +1 -1
  303. package/dist/team/tmux-session.js +81 -29
  304. package/dist/team/tmux-session.js.map +1 -1
  305. package/dist/team/worker-bootstrap.d.ts.map +1 -1
  306. package/dist/team/worker-bootstrap.js +4 -0
  307. package/dist/team/worker-bootstrap.js.map +1 -1
  308. package/dist/team/worktree.d.ts.map +1 -1
  309. package/dist/team/worktree.js +9 -0
  310. package/dist/team/worktree.js.map +1 -1
  311. package/dist/utils/__tests__/paths.test.js +98 -11
  312. package/dist/utils/__tests__/paths.test.js.map +1 -1
  313. package/dist/utils/__tests__/platform-command.test.js +101 -2
  314. package/dist/utils/__tests__/platform-command.test.js.map +1 -1
  315. package/dist/utils/git-layout.d.ts +8 -0
  316. package/dist/utils/git-layout.d.ts.map +1 -0
  317. package/dist/utils/git-layout.js +58 -0
  318. package/dist/utils/git-layout.js.map +1 -0
  319. package/dist/utils/paths.d.ts +3 -0
  320. package/dist/utils/paths.d.ts.map +1 -1
  321. package/dist/utils/paths.js +14 -4
  322. package/dist/utils/paths.js.map +1 -1
  323. package/dist/utils/platform-command.d.ts.map +1 -1
  324. package/dist/utils/platform-command.js +35 -3
  325. package/dist/utils/platform-command.js.map +1 -1
  326. package/package.json +9 -5
  327. package/src/scripts/notify-fallback-watcher.ts +103 -53
  328. package/src/scripts/notify-hook/auto-nudge.ts +97 -103
  329. package/src/scripts/notify-hook/managed-tmux.ts +324 -0
  330. package/src/scripts/notify-hook/operational-events.ts +2 -0
  331. package/src/scripts/notify-hook/ralph-session-resume.ts +337 -0
  332. package/src/scripts/notify-hook/state-io.ts +2 -10
  333. package/src/scripts/notify-hook/team-dispatch.ts +131 -66
  334. package/src/scripts/notify-hook/team-leader-nudge.ts +19 -5
  335. package/src/scripts/notify-hook/team-tmux-guard.ts +0 -20
  336. package/src/scripts/notify-hook/team-worker.ts +4 -4
  337. package/src/scripts/notify-hook/tmux-injection.ts +103 -33
  338. package/src/scripts/notify-hook.ts +150 -21
  339. package/src/scripts/run-provider-advisor.ts +4 -2
  340. package/src/scripts/run-test-files.ts +48 -0
  341. package/src/scripts/tmux-hook-engine.ts +16 -0
  342. package/templates/AGENTS.md +51 -43
@@ -12,12 +12,12 @@ import { composeRoleInstructionsForRole } from '../agents/native-config.js';
12
12
  import { codexPromptsDir } from '../utils/paths.js';
13
13
  import { resolveTeamWorkerLaunchArgs, TEAM_LOW_COMPLEXITY_DEFAULT_MODEL, parseTeamWorkerLaunchArgs, splitWorkerLaunchArgs, resolveAgentDefaultModel, resolveAgentReasoningEffort, } from './model-contract.js';
14
14
  import { resolveCanonicalTeamStateRoot } from './state-root.js';
15
- import { isBridgeEnabled, getDefaultBridge } from '../runtime/bridge.js';
16
15
  import { inferPhaseTargetFromTaskCounts, reconcilePhaseStateForMonitor } from './phase-controller.js';
17
16
  import { getTeamTmuxSessions } from '../notifications/tmux.js';
18
17
  import { hasStructuredVerificationEvidence } from '../verification/verifier.js';
19
18
  import { buildRebalanceDecisions } from './rebalance-policy.js';
20
19
  import { readModeState, updateModeState } from '../modes/base.js';
20
+ import { appendTeamCommitHygieneEntries, buildTeamCommitHygieneContext, writeTeamCommitHygieneContext, } from './commit-hygiene.js';
21
21
  import { assertCleanLeaderWorkspaceForWorkerWorktrees, ensureWorktree, isGitRepository, planWorktreeTarget, rollbackProvisionedWorktrees, } from './worktree.js';
22
22
  async function syncRootTeamModeStateOnTerminalPhase(teamName, phase, cwd) {
23
23
  if (phase !== 'complete' && phase !== 'failed' && phase !== 'cancelled')
@@ -82,6 +82,7 @@ function runCommand(command, args, cwd) {
82
82
  const result = spawnSync(command, args, {
83
83
  cwd,
84
84
  encoding: 'utf-8',
85
+ windowsHide: true,
85
86
  });
86
87
  return {
87
88
  ok: result.status === 0,
@@ -173,11 +174,22 @@ function appendIntegrationReport(teamName, entry, cwd) {
173
174
  const line = `- [${timestamp}] ${entry.workerName}: ${entry.operation} conflict auto-resolved (${entry.strategy}) on files: ${entry.files.join(', ') || 'unknown'}. ${entry.detail}\n`;
174
175
  appendFileSync(reportPath, existsSync(reportPath) ? line : `# Integration Report\n\n${line}`);
175
176
  }
177
+ function resolveWorkerMergeRef(branchResult, workerHead) {
178
+ const branchRef = branchResult.ok ? branchResult.stdout.trim() : '';
179
+ if (!branchRef || branchRef === 'HEAD')
180
+ return workerHead;
181
+ return branchRef;
182
+ }
183
+ function leaderContainsCommit(repoRoot, cwd, commit) {
184
+ return runGitCommand(repoRoot, ['merge-base', '--is-ancestor', commit, 'HEAD'], cwd).ok;
185
+ }
176
186
  async function integrateWorkerCommitsIntoLeader(params) {
177
187
  const { teamName, config, previous, cwd } = params;
178
188
  const next = { ...(previous?.integrationByWorker ?? {}) };
179
189
  const leaderHeadAtCycleStart = resolveLeaderHead(resolve(config.workers[0]?.worktree_repo_root ?? cwd), cwd);
180
190
  const integratedWorkerNames = new Set();
191
+ const commitHygieneEntries = [];
192
+ const artifactCwd = config.leader_cwd ?? cwd;
181
193
  // ── Phase A: Auto-commit dirty worktrees ──
182
194
  for (const worker of config.workers) {
183
195
  if (!worker.worktree_repo_root || !worker.worktree_path || !existsSync(worker.worktree_path))
@@ -190,6 +202,16 @@ async function integrateWorkerCommitsIntoLeader(params) {
190
202
  worktree_path: resolve(worker.worktree_path),
191
203
  summary: `auto-committed dirty worktree for ${worker.name}`,
192
204
  }, cwd);
205
+ commitHygieneEntries.push({
206
+ recorded_at: new Date().toISOString(),
207
+ operation: 'auto_checkpoint',
208
+ worker_name: worker.name,
209
+ task_id: worker.assigned_tasks[0],
210
+ status: 'applied',
211
+ operational_commit: commitHash,
212
+ worktree_path: resolve(worker.worktree_path),
213
+ detail: 'Dirty worker worktree checkpointed before runtime integration.',
214
+ });
193
215
  }
194
216
  }
195
217
  // ── Phase B: Integrate worker commits to leader (hybrid strategy) ──
@@ -220,26 +242,69 @@ async function integrateWorkerCommitsIntoLeader(params) {
220
242
  if (workerIsAheadOfLeader) {
221
243
  // Worker is cleanly ahead → merge --no-ff -X theirs
222
244
  const workerBranch = runGitCommand(repoRoot, ['rev-parse', '--abbrev-ref', 'HEAD'], worktreePath);
223
- const branchRef = workerBranch.ok && workerBranch.stdout ? workerBranch.stdout : workerHead;
245
+ const branchRef = resolveWorkerMergeRef(workerBranch, workerHead);
224
246
  const merge = runGitCommand(repoRoot, ['merge', '--no-ff', '-X', 'theirs', '-m', `omx(team): merge ${worker.name}`, branchRef], cwd);
225
247
  if (merge.ok) {
226
248
  const newLeaderHead = resolveLeaderHead(repoRoot, cwd) ?? leaderHead;
227
- state.last_integrated_head = workerHead;
228
- state.last_leader_head = newLeaderHead;
229
- state.status = 'integrated';
230
- state.conflict_commit = undefined;
231
- state.conflict_files = undefined;
232
- state.updated_at = new Date().toISOString();
233
- integratedWorkerNames.add(worker.name);
234
- await appendIntegrationEvent(teamName, 'worker_merge_applied', worker, {
235
- worker_name: worker.name,
236
- worker_head: workerHead,
237
- leader_head_before: leaderHead,
238
- leader_head_after: newLeaderHead,
239
- worktree_path: worktreePath,
240
- summary: `merged ${worker.name} into leader via --no-ff -X theirs`,
241
- }, cwd);
242
- await sendIntegrationMessageToLeader(teamName, worker, `INTEGRATED: merged ${worker.name} (${workerHead.slice(0, 12)}) into leader HEAD ${newLeaderHead.slice(0, 12)} via merge --no-ff.`, cwd);
249
+ const workerIntegrated = leaderContainsCommit(repoRoot, cwd, workerHead);
250
+ const leaderAdvanced = newLeaderHead !== leaderHead;
251
+ if (workerIntegrated && leaderAdvanced) {
252
+ state.last_integrated_head = workerHead;
253
+ state.last_leader_head = newLeaderHead;
254
+ state.status = 'integrated';
255
+ state.conflict_commit = undefined;
256
+ state.conflict_files = undefined;
257
+ state.updated_at = new Date().toISOString();
258
+ integratedWorkerNames.add(worker.name);
259
+ await appendIntegrationEvent(teamName, 'worker_merge_applied', worker, {
260
+ worker_name: worker.name,
261
+ worker_head: workerHead,
262
+ leader_head_before: leaderHead,
263
+ leader_head_after: newLeaderHead,
264
+ worktree_path: worktreePath,
265
+ summary: `merged ${worker.name} into leader via --no-ff -X theirs`,
266
+ }, cwd);
267
+ await sendIntegrationMessageToLeader(teamName, worker, `INTEGRATED: merged ${worker.name} (${workerHead.slice(0, 12)}) into leader HEAD ${newLeaderHead.slice(0, 12)} via merge --no-ff.`, cwd);
268
+ commitHygieneEntries.push({
269
+ recorded_at: new Date().toISOString(),
270
+ operation: 'integration_merge',
271
+ worker_name: worker.name,
272
+ task_id: worker.assigned_tasks[0],
273
+ status: 'applied',
274
+ operational_commit: newLeaderHead,
275
+ source_commit: workerHead,
276
+ leader_head_before: leaderHead,
277
+ leader_head_after: newLeaderHead,
278
+ worktree_path: worktreePath,
279
+ detail: 'Leader created a runtime merge commit to integrate worker history.',
280
+ });
281
+ }
282
+ else {
283
+ state.last_leader_head = newLeaderHead;
284
+ state.status = 'idle';
285
+ state.updated_at = new Date().toISOString();
286
+ appendIntegrationReport(teamName, {
287
+ workerName: worker.name,
288
+ operation: 'merge',
289
+ strategy: '-X theirs',
290
+ files: [],
291
+ detail: `merge reported success but leader HEAD did not advance cleanly (leader_before=${leaderHead.slice(0, 12)}, leader_after=${newLeaderHead.slice(0, 12)}, worker_integrated=${workerIntegrated}, merge_ref=${branchRef}).`,
292
+ }, cwd);
293
+ await sendIntegrationMessageToLeader(teamName, worker, `INTEGRATION NO-OP: merge for ${worker.name} using ${branchRef.slice(0, 12)} reported success but leader HEAD stayed ${newLeaderHead.slice(0, 12)}. Inspect ${worktreePath}.`, cwd);
294
+ commitHygieneEntries.push({
295
+ recorded_at: new Date().toISOString(),
296
+ operation: 'integration_merge',
297
+ worker_name: worker.name,
298
+ task_id: worker.assigned_tasks[0],
299
+ status: 'skipped',
300
+ operational_commit: newLeaderHead,
301
+ source_commit: workerHead,
302
+ leader_head_before: leaderHead,
303
+ leader_head_after: newLeaderHead,
304
+ worktree_path: worktreePath,
305
+ detail: 'Merge command reported success but leader HEAD did not advance or contain the worker commit; runtime refused to report false integration.',
306
+ });
307
+ }
243
308
  }
244
309
  else {
245
310
  // Merge failed even with -X theirs (e.g. binary conflict) — abort and log
@@ -333,6 +398,19 @@ async function integrateWorkerCommitsIntoLeader(params) {
333
398
  summary: `cherry-picked ${commit.slice(0, 12)} from ${worker.name} with -X theirs`,
334
399
  }, cwd);
335
400
  await sendIntegrationMessageToLeader(teamName, worker, `INTEGRATED: cherry-picked ${commit.slice(0, 12)} from ${worker.name} into leader HEAD ${newLeaderHead.slice(0, 12)} (-X theirs).`, cwd);
401
+ commitHygieneEntries.push({
402
+ recorded_at: new Date().toISOString(),
403
+ operation: 'integration_cherry_pick',
404
+ worker_name: worker.name,
405
+ task_id: worker.assigned_tasks[0],
406
+ status: 'applied',
407
+ operational_commit: newLeaderHead,
408
+ source_commit: commit,
409
+ leader_head_before: leaderHead,
410
+ leader_head_after: newLeaderHead,
411
+ worktree_path: worktreePath,
412
+ detail: 'Leader created a runtime cherry-pick commit while integrating diverged worker history.',
413
+ });
336
414
  }
337
415
  if (allPicked) {
338
416
  integratedWorkerNames.add(worker.name);
@@ -376,8 +454,10 @@ async function integrateWorkerCommitsIntoLeader(params) {
376
454
  continue;
377
455
  }
378
456
  // Rebase with -X ours (in rebase context, "ours" = upstream = leader wins)
457
+ const workerHeadBeforeRebase = resolveWorkerHead(worktreePath);
379
458
  const rebase = runGitCommand(repoRoot, ['rebase', '-X', 'ours', newLeaderHead], worktreePath);
380
459
  if (rebase.ok) {
460
+ const workerHeadAfterRebase = resolveWorkerHead(worktreePath);
381
461
  const state = next[worker.name] ?? {};
382
462
  state.last_rebased_leader_head = newLeaderHead;
383
463
  state.status = 'idle';
@@ -391,6 +471,19 @@ async function integrateWorkerCommitsIntoLeader(params) {
391
471
  worktree_path: worktreePath,
392
472
  summary: `cross-rebased ${worker.name} onto ${newLeaderHead.slice(0, 12)} (-X ours)`,
393
473
  }, cwd);
474
+ commitHygieneEntries.push({
475
+ recorded_at: new Date().toISOString(),
476
+ operation: 'cross_rebase',
477
+ worker_name: worker.name,
478
+ task_id: worker.assigned_tasks[0],
479
+ status: 'applied',
480
+ operational_commit: workerHeadAfterRebase,
481
+ leader_head_after: newLeaderHead,
482
+ worker_head_before: workerHeadBeforeRebase,
483
+ worker_head_after: workerHeadAfterRebase,
484
+ worktree_path: worktreePath,
485
+ detail: 'Runtime rebase rewrote worker history onto the updated leader head.',
486
+ });
394
487
  }
395
488
  else {
396
489
  // Rebase failed — abort to restore worktree, log for retry next cycle
@@ -415,6 +508,9 @@ async function integrateWorkerCommitsIntoLeader(params) {
415
508
  }
416
509
  }
417
510
  }
511
+ if (commitHygieneEntries.length > 0) {
512
+ await appendTeamCommitHygieneEntries(teamName, commitHygieneEntries, artifactCwd);
513
+ }
418
514
  return next;
419
515
  }
420
516
  function renderWorktreeMergeReport(report) {
@@ -427,6 +523,8 @@ function renderWorktreeMergeReport(report) {
427
523
  `- synthetic_commit: ${report.syntheticCommit ?? 'none'}`,
428
524
  `- merge_outcome: ${report.mergeOutcome}`,
429
525
  `- merge_detail: ${report.mergeDetail}`,
526
+ `- leader_head_before: ${report.leaderHeadBefore ?? 'none'}`,
527
+ `- leader_head_after: ${report.leaderHeadAfter ?? 'none'}`,
430
528
  '',
431
529
  '## Summary',
432
530
  report.summaryText ?? 'sparkshell summary unavailable; using raw diff fallback.',
@@ -459,6 +557,8 @@ async function prepareShutdownMergeReport(worker, leaderCwd) {
459
557
  summaryText: null,
460
558
  mergeOutcome: 'skipped',
461
559
  mergeDetail: addResult.stderr || 'git add -A failed',
560
+ leaderHeadBefore: resolveLeaderHead(repoRoot, leaderCwd),
561
+ leaderHeadAfter: resolveLeaderHead(repoRoot, leaderCwd),
462
562
  };
463
563
  }
464
564
  const commitResult = runGitCommand(repoRoot, ['commit', '--no-verify', '-m', `omx(team): checkpoint ${worker.name} shutdown changes`], worktreePath);
@@ -477,6 +577,8 @@ async function prepareShutdownMergeReport(worker, leaderCwd) {
477
577
  summaryText: null,
478
578
  mergeOutcome: 'skipped',
479
579
  mergeDetail: commitResult.stderr || 'git commit failed',
580
+ leaderHeadBefore: resolveLeaderHead(repoRoot, leaderCwd),
581
+ leaderHeadAfter: resolveLeaderHead(repoRoot, leaderCwd),
480
582
  };
481
583
  }
482
584
  }
@@ -485,8 +587,10 @@ async function prepareShutdownMergeReport(worker, leaderCwd) {
485
587
  const diffText = getWorktreeDiffText(worktreePath);
486
588
  const summaryText = summarizeWorktreeDiffWithSparkShell(worktreePath);
487
589
  const reportPath = join(worktreePath, '.omx', 'diff.md');
590
+ const leaderHeadBefore = resolveLeaderHead(repoRoot, leaderCwd);
488
591
  let mergeOutcome = 'skipped';
489
592
  let mergeDetail = 'worktree merge skipped';
593
+ let leaderHeadAfter = leaderHeadBefore;
490
594
  if (sourceRef) {
491
595
  const alreadyMerged = runGitCommand(repoRoot, ['merge-base', '--is-ancestor', sourceRef, 'HEAD'], leaderCwd);
492
596
  if (alreadyMerged.ok) {
@@ -498,11 +602,13 @@ async function prepareShutdownMergeReport(worker, leaderCwd) {
498
602
  if (mergeResult.ok) {
499
603
  mergeOutcome = 'merged';
500
604
  mergeDetail = mergeResult.stdout || 'merged successfully';
605
+ leaderHeadAfter = resolveLeaderHead(repoRoot, leaderCwd) ?? leaderHeadBefore;
501
606
  }
502
607
  else {
503
608
  mergeOutcome = 'conflict';
504
609
  mergeDetail = mergeResult.stderr || mergeResult.stdout || 'merge failed';
505
610
  runGitCommand(repoRoot, ['merge', '--abort'], leaderCwd);
611
+ leaderHeadAfter = resolveLeaderHead(repoRoot, leaderCwd) ?? leaderHeadBefore;
506
612
  }
507
613
  }
508
614
  }
@@ -516,6 +622,8 @@ async function prepareShutdownMergeReport(worker, leaderCwd) {
516
622
  summaryText,
517
623
  mergeOutcome,
518
624
  mergeDetail,
625
+ leaderHeadBefore,
626
+ leaderHeadAfter,
519
627
  };
520
628
  await mkdir(join(worktreePath, '.omx'), { recursive: true });
521
629
  await writeFile(reportPath, renderWorktreeMergeReport(report), 'utf-8');
@@ -523,11 +631,14 @@ async function prepareShutdownMergeReport(worker, leaderCwd) {
523
631
  return report;
524
632
  }
525
633
  async function prepareWorkerWorktreeShutdownReports(config, leaderCwd) {
634
+ const reports = [];
526
635
  for (const worker of config.workers) {
527
636
  if (!worker.worktree_path || !worker.worktree_repo_root)
528
637
  continue;
529
638
  try {
530
- await prepareShutdownMergeReport(worker, leaderCwd);
639
+ const report = await prepareShutdownMergeReport(worker, leaderCwd);
640
+ if (report)
641
+ reports.push(report);
531
642
  }
532
643
  catch (error) {
533
644
  const worktreePath = resolve(worker.worktree_path);
@@ -546,6 +657,7 @@ async function prepareWorkerWorktreeShutdownReports(config, leaderCwd) {
546
657
  process.stdout.write(`${fallback}\n`);
547
658
  }
548
659
  }
660
+ return reports;
549
661
  }
550
662
  function resolveEffectiveTeamWorktreeMode(leaderCwd, requestedMode) {
551
663
  if (!isGitRepository(leaderCwd)) {
@@ -692,12 +804,20 @@ function registerPromptWorkerHandle(teamName, workerName, child) {
692
804
  }
693
805
  const processPid = pid;
694
806
  const existingTeamHandles = promptWorkerRegistry.get(teamName) ?? new Map();
695
- existingTeamHandles.set(workerName, { child, pid: processPid });
807
+ existingTeamHandles.set(workerName, {
808
+ child,
809
+ pid: processPid,
810
+ processGroupId: process.platform !== 'win32' ? processPid : null,
811
+ });
696
812
  promptWorkerRegistry.set(teamName, existingTeamHandles);
697
813
  child.on('exit', () => {
698
814
  const teamHandles = promptWorkerRegistry.get(teamName);
699
815
  if (!teamHandles)
700
816
  return;
817
+ const handle = teamHandles.get(workerName);
818
+ if (handle?.processGroupId && isProcessGroupAlive(handle.processGroupId)) {
819
+ return;
820
+ }
701
821
  teamHandles.delete(workerName);
702
822
  if (teamHandles.size === 0)
703
823
  promptWorkerRegistry.delete(teamName);
@@ -728,67 +848,215 @@ function isPidAlive(pid) {
728
848
  return false;
729
849
  }
730
850
  }
731
- async function waitForPidExit(pid, timeoutMs) {
732
- if (!isPidAlive(pid))
851
+ function isProcessGroupAlive(processGroupId) {
852
+ if (process.platform === 'win32')
853
+ return false;
854
+ if (!Number.isFinite(processGroupId) || processGroupId <= 0)
855
+ return false;
856
+ try {
857
+ process.kill(-processGroupId, 0);
858
+ return true;
859
+ }
860
+ catch (err) {
861
+ if (err.code === 'ESRCH')
862
+ return false;
863
+ process.stderr.write(`[team/runtime] operation failed: ${err}\n`);
864
+ return false;
865
+ }
866
+ }
867
+ function listProcessTreeEntries() {
868
+ if (process.platform === 'win32')
869
+ return [];
870
+ const result = spawnSync('ps', ['axww', '-o', 'pid=,ppid='], {
871
+ encoding: 'utf-8',
872
+ windowsHide: true,
873
+ });
874
+ if (result.status !== 0 || typeof result.stdout !== 'string')
875
+ return [];
876
+ return result.stdout
877
+ .split(/\r?\n/)
878
+ .map((line) => line.trim())
879
+ .filter(Boolean)
880
+ .map((line) => {
881
+ const match = line.match(/^(\d+)\s+(\d+)$/);
882
+ if (!match)
883
+ return null;
884
+ const pid = Number.parseInt(match[1], 10);
885
+ const ppid = Number.parseInt(match[2], 10);
886
+ if (!Number.isFinite(pid) || pid <= 0)
887
+ return null;
888
+ if (!Number.isFinite(ppid) || ppid < 0)
889
+ return null;
890
+ return { pid, ppid };
891
+ })
892
+ .filter((entry) => entry !== null);
893
+ }
894
+ function collectProcessTreePids(rootPid) {
895
+ if (!Number.isFinite(rootPid) || rootPid <= 0)
896
+ return [];
897
+ const childrenByPid = new Map();
898
+ for (const entry of listProcessTreeEntries()) {
899
+ const siblings = childrenByPid.get(entry.ppid) ?? [];
900
+ siblings.push(entry.pid);
901
+ childrenByPid.set(entry.ppid, siblings);
902
+ }
903
+ const ordered = [];
904
+ const stack = [rootPid];
905
+ const seen = new Set();
906
+ while (stack.length > 0) {
907
+ const pid = stack.pop();
908
+ if (seen.has(pid))
909
+ continue;
910
+ seen.add(pid);
911
+ ordered.push(pid);
912
+ for (const childPid of childrenByPid.get(pid) ?? []) {
913
+ if (!seen.has(childPid))
914
+ stack.push(childPid);
915
+ }
916
+ }
917
+ return ordered.reverse();
918
+ }
919
+ async function waitForTrackedPidsExit(pids, timeoutMs) {
920
+ const tracked = [...new Set(pids.filter((pid) => Number.isFinite(pid) && pid > 0))];
921
+ if (tracked.length === 0)
733
922
  return true;
734
923
  const deadline = Date.now() + Math.max(0, timeoutMs);
735
924
  while (Date.now() < deadline) {
925
+ if (tracked.every((pid) => !isPidAlive(pid)))
926
+ return true;
736
927
  await new Promise((resolve) => setTimeout(resolve, PROMPT_WORKER_EXIT_POLL_MS));
928
+ }
929
+ return tracked.every((pid) => !isPidAlive(pid));
930
+ }
931
+ async function terminateTrackedProcessTree(rootPid, processGroupId = null, graceMs = PROMPT_WORKER_SIGTERM_WAIT_MS, killWaitMs = PROMPT_WORKER_SIGKILL_WAIT_MS) {
932
+ if (processGroupId && process.platform !== 'win32') {
933
+ const trackedPids = collectProcessTreePids(rootPid);
934
+ try {
935
+ process.kill(-processGroupId, 'SIGTERM');
936
+ }
937
+ catch (err) {
938
+ if (err.code !== 'ESRCH') {
939
+ process.stderr.write(`[team/runtime] operation failed: ${err}\n`);
940
+ }
941
+ }
942
+ for (const pid of trackedPids) {
943
+ if (pid === rootPid)
944
+ continue;
945
+ try {
946
+ process.kill(pid, 'SIGTERM');
947
+ }
948
+ catch (err) {
949
+ if (err.code !== 'ESRCH') {
950
+ process.stderr.write(`[team/runtime] operation failed: ${err}\n`);
951
+ }
952
+ }
953
+ }
954
+ const groupDeadline = Date.now() + Math.max(0, graceMs);
955
+ while (Date.now() < groupDeadline) {
956
+ const groupAlive = isProcessGroupAlive(processGroupId);
957
+ const descendantsAlive = trackedPids.some((pid) => isPidAlive(pid));
958
+ if (!groupAlive && !descendantsAlive) {
959
+ return { terminated: true, forcedKill: false, trackedPids };
960
+ }
961
+ await new Promise((resolve) => setTimeout(resolve, PROMPT_WORKER_EXIT_POLL_MS));
962
+ }
963
+ try {
964
+ process.kill(-processGroupId, 'SIGKILL');
965
+ }
966
+ catch (err) {
967
+ if (err.code !== 'ESRCH') {
968
+ process.stderr.write(`[team/runtime] operation failed: ${err}\n`);
969
+ }
970
+ }
971
+ for (const pid of trackedPids) {
972
+ if (!isPidAlive(pid))
973
+ continue;
974
+ try {
975
+ process.kill(pid, 'SIGKILL');
976
+ }
977
+ catch (err) {
978
+ if (err.code !== 'ESRCH') {
979
+ process.stderr.write(`[team/runtime] operation failed: ${err}\n`);
980
+ }
981
+ }
982
+ }
983
+ const killDeadline = Date.now() + Math.max(0, killWaitMs);
984
+ while (Date.now() < killDeadline) {
985
+ const groupAlive = isProcessGroupAlive(processGroupId);
986
+ const descendantsAlive = trackedPids.some((pid) => isPidAlive(pid));
987
+ if (!groupAlive && !descendantsAlive) {
988
+ return { terminated: true, forcedKill: true, trackedPids };
989
+ }
990
+ await new Promise((resolve) => setTimeout(resolve, PROMPT_WORKER_EXIT_POLL_MS));
991
+ }
992
+ return {
993
+ terminated: !isProcessGroupAlive(processGroupId) && trackedPids.every((pid) => !isPidAlive(pid)),
994
+ forcedKill: true,
995
+ trackedPids,
996
+ };
997
+ }
998
+ const trackedPids = collectProcessTreePids(rootPid);
999
+ if (trackedPids.length === 0) {
1000
+ return {
1001
+ terminated: !isPidAlive(rootPid),
1002
+ forcedKill: false,
1003
+ trackedPids: [],
1004
+ };
1005
+ }
1006
+ for (const pid of trackedPids) {
1007
+ try {
1008
+ process.kill(pid, 'SIGTERM');
1009
+ }
1010
+ catch (err) {
1011
+ if (err.code !== 'ESRCH') {
1012
+ process.stderr.write(`[team/runtime] operation failed: ${err}\n`);
1013
+ }
1014
+ }
1015
+ }
1016
+ if (await waitForTrackedPidsExit(trackedPids, graceMs)) {
1017
+ return { terminated: true, forcedKill: false, trackedPids };
1018
+ }
1019
+ for (const pid of trackedPids) {
737
1020
  if (!isPidAlive(pid))
738
- return true;
1021
+ continue;
1022
+ try {
1023
+ process.kill(pid, 'SIGKILL');
1024
+ }
1025
+ catch (err) {
1026
+ if (err.code !== 'ESRCH') {
1027
+ process.stderr.write(`[team/runtime] operation failed: ${err}\n`);
1028
+ }
1029
+ }
739
1030
  }
740
- return !isPidAlive(pid);
1031
+ return {
1032
+ terminated: await waitForTrackedPidsExit(trackedPids, killWaitMs),
1033
+ forcedKill: true,
1034
+ trackedPids,
1035
+ };
741
1036
  }
742
1037
  async function teardownPromptWorker(teamName, workerName, fallbackPid, cwd, context) {
743
1038
  const handle = getPromptWorkerHandle(teamName, workerName);
744
1039
  const handlePid = handle?.pid;
1040
+ const processGroupId = handle?.processGroupId ?? null;
745
1041
  const pid = (typeof handlePid === 'number' && Number.isFinite(handlePid))
746
1042
  ? handlePid
747
1043
  : (Number.isFinite(fallbackPid) && (fallbackPid ?? 0) > 0 ? fallbackPid : null);
748
- if (pid === null) {
1044
+ if (pid === null && processGroupId === null) {
749
1045
  removePromptWorkerHandle(teamName, workerName);
750
1046
  return { terminated: true, forcedKill: false, pid: null };
751
1047
  }
752
- try {
753
- if (handle && handle.child.exitCode === null && !handle.child.killed) {
754
- handle.child.kill('SIGTERM');
755
- }
756
- else {
757
- process.kill(pid, 'SIGTERM');
758
- }
759
- }
760
- catch (err) {
761
- if (err.code !== 'ESRCH') {
762
- process.stderr.write(`[team/runtime] operation failed: ${err}\n`);
763
- }
764
- // Best effort.
765
- }
766
- const exitedOnTerm = await waitForPidExit(pid, PROMPT_WORKER_SIGTERM_WAIT_MS);
767
- if (exitedOnTerm) {
1048
+ const teardown = await terminateTrackedProcessTree(pid ?? 0, processGroupId);
1049
+ const processGone = processGroupId ? !isProcessGroupAlive(processGroupId) : !isPidAlive(pid);
1050
+ if (teardown.terminated && processGone) {
768
1051
  removePromptWorkerHandle(teamName, workerName);
769
- return { terminated: true, forcedKill: false, pid };
1052
+ return { terminated: true, forcedKill: teardown.forcedKill, pid };
770
1053
  }
771
1054
  await appendTeamEvent(teamName, {
772
1055
  type: 'worker_stopped',
773
1056
  worker: workerName,
774
1057
  reason: `prompt_force_kill:${context}:pid=${pid}`,
775
1058
  }, cwd).catch(() => { });
776
- try {
777
- if (handle && handle.child.exitCode === null) {
778
- handle.child.kill('SIGKILL');
779
- }
780
- else {
781
- process.kill(pid, 'SIGKILL');
782
- }
783
- }
784
- catch (err) {
785
- if (err.code !== 'ESRCH') {
786
- process.stderr.write(`[team/runtime] operation failed: ${err}\n`);
787
- }
788
- // Best effort.
789
- }
790
- const exitedOnKill = await waitForPidExit(pid, PROMPT_WORKER_SIGKILL_WAIT_MS);
791
- if (!exitedOnKill) {
1059
+ if (!teardown.terminated) {
792
1060
  await appendTeamEvent(teamName, {
793
1061
  type: 'worker_stopped',
794
1062
  worker: workerName,
@@ -796,18 +1064,22 @@ async function teardownPromptWorker(teamName, workerName, fallbackPid, cwd, cont
796
1064
  }, cwd).catch(() => { });
797
1065
  return {
798
1066
  terminated: false,
799
- forcedKill: true,
1067
+ forcedKill: teardown.forcedKill,
800
1068
  pid,
801
1069
  error: 'still_alive_after_sigkill',
802
1070
  };
803
1071
  }
804
1072
  removePromptWorkerHandle(teamName, workerName);
805
- return { terminated: true, forcedKill: true, pid };
1073
+ return { terminated: true, forcedKill: teardown.forcedKill, pid };
806
1074
  }
807
1075
  function isPromptWorkerAlive(config, worker) {
808
1076
  const handle = getPromptWorkerHandle(config.name, worker.name);
809
1077
  if (handle?.child.exitCode === null && !handle.child.killed)
810
1078
  return true;
1079
+ if (handle?.processGroupId && isProcessGroupAlive(handle.processGroupId))
1080
+ return true;
1081
+ if (process.platform !== 'win32' && isProcessGroupAlive(worker.pid))
1082
+ return true;
811
1083
  return isPidAlive(worker.pid);
812
1084
  }
813
1085
  export { TEAM_LOW_COMPLEXITY_DEFAULT_MODEL };
@@ -816,6 +1088,7 @@ function spawnPromptWorker(teamName, workerName, workerIndex, workerCwd, launchA
816
1088
  const processSpec = buildWorkerProcessLaunchSpec(teamName, workerIndex, launchArgs, workerCwd, workerEnv, workerCli, initialPrompt);
817
1089
  const child = spawn(processSpec.command, processSpec.args, {
818
1090
  cwd: workerCwd,
1091
+ detached: process.platform !== 'win32',
819
1092
  env: { ...process.env, ...processSpec.env },
820
1093
  stdio: ['pipe', 'ignore', 'ignore'],
821
1094
  });
@@ -1255,7 +1528,11 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
1255
1528
  }
1256
1529
  // In split-pane topology, we must not kill the entire tmux session; kill only created panes.
1257
1530
  if (sessionName.includes(':')) {
1258
- for (const paneId of createdWorkerPaneIds) {
1531
+ for (const [index, paneId] of createdWorkerPaneIds.entries()) {
1532
+ const panePid = getWorkerPanePid(sessionName, index + 1, paneId);
1533
+ if (panePid) {
1534
+ await terminateTrackedProcessTree(panePid);
1535
+ }
1259
1536
  try {
1260
1537
  await killWorkerByPaneIdAsync(paneId, createdLeaderPaneId);
1261
1538
  }
@@ -1669,7 +1946,7 @@ export async function shutdownTeam(teamName, cwd, options = {}) {
1669
1946
  }
1670
1947
  await cleanupTeamState(sanitized, cwd);
1671
1948
  restoreTeamModelInstructionsFile(sanitized);
1672
- return;
1949
+ return { commitHygieneArtifacts: null };
1673
1950
  }
1674
1951
  const manifest = await readTeamManifestV2(sanitized, cwd);
1675
1952
  const governance = resolveGovernancePolicy(manifest?.governance, manifest?.policy);
@@ -1772,6 +2049,12 @@ export async function shutdownTeam(teamName, cwd, options = {}) {
1772
2049
  const leaderPaneId = config.leader_pane_id;
1773
2050
  const hudPaneId = config.hud_pane_id;
1774
2051
  if (config.worker_launch_mode === 'interactive') {
2052
+ const workerPanePids = config.workers
2053
+ .map((w) => getWorkerPanePid(sessionName, w.index, w.pane_id))
2054
+ .filter((pid) => typeof pid === 'number' && Number.isFinite(pid) && pid > 0);
2055
+ for (const panePid of workerPanePids) {
2056
+ await terminateTrackedProcessTree(panePid);
2057
+ }
1775
2058
  let resizeHookWarning = null;
1776
2059
  if (config.resize_hook_name && config.resize_hook_target) {
1777
2060
  const resizeHookName = config.resize_hook_name;
@@ -1828,7 +2111,50 @@ export async function shutdownTeam(teamName, cwd, options = {}) {
1828
2111
  throw new Error(`shutdown_prompt_teardown_failed:${promptTeardownFailures.join(',')}`);
1829
2112
  }
1830
2113
  }
1831
- await prepareWorkerWorktreeShutdownReports(config, cwd);
2114
+ const shutdownReports = await prepareWorkerWorktreeShutdownReports(config, cwd);
2115
+ const commitHygieneEntries = [];
2116
+ for (const report of shutdownReports) {
2117
+ const worker = config.workers.find((entry) => entry.name === report.workerName);
2118
+ if (report.syntheticCommit) {
2119
+ commitHygieneEntries.push({
2120
+ recorded_at: new Date().toISOString(),
2121
+ operation: 'shutdown_checkpoint',
2122
+ worker_name: report.workerName,
2123
+ task_id: worker?.assigned_tasks[0],
2124
+ status: 'applied',
2125
+ operational_commit: report.syntheticCommit,
2126
+ source_commit: report.sourceRef,
2127
+ worktree_path: report.worktreePath,
2128
+ report_path: report.reportPath,
2129
+ detail: 'Runtime created a shutdown checkpoint commit to preserve worker worktree changes.',
2130
+ });
2131
+ }
2132
+ if (report.sourceRef && report.mergeOutcome !== 'skipped') {
2133
+ commitHygieneEntries.push({
2134
+ recorded_at: new Date().toISOString(),
2135
+ operation: 'shutdown_merge',
2136
+ worker_name: report.workerName,
2137
+ task_id: worker?.assigned_tasks[0],
2138
+ status: report.mergeOutcome === 'merged' ? 'applied' : report.mergeOutcome,
2139
+ operational_commit: report.mergeOutcome === 'merged' ? report.leaderHeadAfter : null,
2140
+ source_commit: report.sourceRef,
2141
+ leader_head_before: report.leaderHeadBefore,
2142
+ leader_head_after: report.leaderHeadAfter,
2143
+ worktree_path: report.worktreePath,
2144
+ report_path: report.reportPath,
2145
+ detail: report.mergeDetail,
2146
+ });
2147
+ }
2148
+ }
2149
+ const artifactCwd = config.leader_cwd ?? cwd;
2150
+ const ledger = await appendTeamCommitHygieneEntries(sanitized, commitHygieneEntries, artifactCwd);
2151
+ const taskView = await listTasks(sanitized, cwd).catch(() => []);
2152
+ const commitHygieneContext = buildTeamCommitHygieneContext({
2153
+ teamName: sanitized,
2154
+ tasks: taskView,
2155
+ ledger,
2156
+ });
2157
+ const commitHygieneArtifacts = await writeTeamCommitHygieneContext(sanitized, commitHygieneContext, artifactCwd);
1832
2158
  // 5. Remove worker worktree-root instructions and team-scoped fallback instructions.
1833
2159
  for (const worker of config.workers) {
1834
2160
  if (!worker.worktree_path || !worker.team_state_root)
@@ -1869,6 +2195,7 @@ export async function shutdownTeam(teamName, cwd, options = {}) {
1869
2195
  if (cleanupErrors.length > 0) {
1870
2196
  throw new Error(cleanupErrors.join(' | '));
1871
2197
  }
2198
+ return { commitHygieneArtifacts };
1872
2199
  }
1873
2200
  /**
1874
2201
  * Resume monitoring an existing team.
@@ -2090,22 +2417,6 @@ async function markDispatchRequestLeaderPaneMissingDeferred(params) {
2090
2417
  }
2091
2418
  async function dispatchCriticalInboxInstruction(params) {
2092
2419
  const { teamName, config, workerName, workerIndex, paneId, workerCli, inbox, triggerMessage, cwd, dispatchPolicy, inboxCorrelationKey, requireWorkerStartupEvidence, } = params;
2093
- // --- Rust runtime bridge: dual-write dispatch mutations ---
2094
- if (isBridgeEnabled()) {
2095
- try {
2096
- const bridge = getDefaultBridge(cwd);
2097
- const bridgeRequestId = `dispatch-${workerName}-${Date.now()}`;
2098
- bridge.execCommand({
2099
- command: 'QueueDispatch',
2100
- request_id: bridgeRequestId,
2101
- target: workerName,
2102
- metadata: { kind: 'inbox', inbox_correlation_key: inboxCorrelationKey },
2103
- });
2104
- }
2105
- catch (_bridgeErr) {
2106
- // Bridge failure is non-fatal — fall through to existing JS logic
2107
- }
2108
- }
2109
2420
  if (config.worker_launch_mode === 'prompt') {
2110
2421
  return await queueInboxInstruction({
2111
2422
  teamName,
@@ -2183,12 +2494,6 @@ async function dispatchCriticalInboxInstruction(params) {
2183
2494
  if (receipt?.status === 'failed') {
2184
2495
  const fallback = await notifyWorkerOutcome(config, workerIndex, triggerMessage, paneId);
2185
2496
  if (fallback.ok) {
2186
- if (isBridgeEnabled()) {
2187
- try {
2188
- getDefaultBridge(cwd).execCommand({ command: 'MarkNotified', request_id: queued.request_id, channel: `fallback_confirmed_after_failed_receipt:${fallback.reason}` });
2189
- }
2190
- catch (_) { /* non-fatal */ }
2191
- }
2192
2497
  await markDispatchRequestNotified(teamName, queued.request_id, { last_reason: `fallback_confirmed_after_failed_receipt:${fallback.reason}`, failed_at: undefined }, cwd).catch(() => null);
2193
2498
  return {
2194
2499
  ok: true,
@@ -2197,12 +2502,6 @@ async function dispatchCriticalInboxInstruction(params) {
2197
2502
  request_id: queued.request_id,
2198
2503
  };
2199
2504
  }
2200
- if (isBridgeEnabled()) {
2201
- try {
2202
- getDefaultBridge(cwd).execCommand({ command: 'MarkFailed', request_id: queued.request_id, reason: `fallback_attempted_but_unconfirmed:${fallback.reason}` });
2203
- }
2204
- catch (_) { /* non-fatal */ }
2205
- }
2206
2505
  await transitionDispatchRequest(teamName, queued.request_id, receipt.status, 'failed', { last_reason: `fallback_attempted_but_unconfirmed:${fallback.reason}` }, cwd).catch(() => { });
2207
2506
  return {
2208
2507
  ok: false,
@@ -2219,20 +2518,8 @@ async function dispatchCriticalInboxInstruction(params) {
2219
2518
  ? `${startupFallbackLabel}_fallback_failed:${fallback.reason}`
2220
2519
  : `fallback_attempted_but_unconfirmed:${fallback.reason}`;
2221
2520
  if (fallback.ok) {
2222
- if (isBridgeEnabled()) {
2223
- try {
2224
- getDefaultBridge(cwd).execCommand({ command: 'MarkNotified', request_id: queued.request_id, channel: `fallback_confirmed:${fallback.reason}` });
2225
- }
2226
- catch (_) { /* non-fatal */ }
2227
- }
2228
2521
  const marked = await markDispatchRequestNotified(teamName, queued.request_id, { last_reason: `fallback_confirmed:${fallback.reason}` }, cwd);
2229
2522
  if (!marked) {
2230
- if (isBridgeEnabled()) {
2231
- try {
2232
- getDefaultBridge(cwd).execCommand({ command: 'MarkFailed', request_id: queued.request_id, reason: `fallback_confirmed_after_failed_receipt:${fallback.reason}` });
2233
- }
2234
- catch (_) { /* non-fatal */ }
2235
- }
2236
2523
  await transitionDispatchRequest(teamName, queued.request_id, 'failed', 'failed', { last_reason: `fallback_confirmed_after_failed_receipt:${fallback.reason}` }, cwd).catch(() => { });
2237
2524
  }
2238
2525
  return {
@@ -2246,12 +2533,6 @@ async function dispatchCriticalInboxInstruction(params) {
2246
2533
  }
2247
2534
  const current = await readDispatchRequest(teamName, queued.request_id, cwd);
2248
2535
  if (current && current.status !== 'failed') {
2249
- if (isBridgeEnabled()) {
2250
- try {
2251
- getDefaultBridge(cwd).execCommand({ command: 'MarkFailed', request_id: queued.request_id, reason: fallbackFailureReason });
2252
- }
2253
- catch (_) { /* non-fatal */ }
2254
- }
2255
2536
  await transitionDispatchRequest(teamName, queued.request_id, current.status, 'failed', { last_reason: fallbackFailureReason }, cwd).catch(() => { });
2256
2537
  }
2257
2538
  return {
@@ -2383,21 +2664,6 @@ async function deliverPendingMailboxMessages(teamName, config, workers, previous
2383
2664
  continue;
2384
2665
  for (const msg of unnotified) {
2385
2666
  const triggerMessage = generateMailboxTriggerMessage(worker.name, teamName, 1, resolveInstructionStateRoot(workerInfo.worktree_path));
2386
- // --- Rust runtime bridge: dual-write mailbox dispatch ---
2387
- if (isBridgeEnabled()) {
2388
- try {
2389
- const bridge = getDefaultBridge(cwd);
2390
- bridge.execCommand({
2391
- command: 'QueueDispatch',
2392
- request_id: `mailbox-${msg.message_id}-${Date.now()}`,
2393
- target: worker.name,
2394
- metadata: { kind: 'mailbox', message_id: msg.message_id },
2395
- });
2396
- }
2397
- catch (_bridgeErr) {
2398
- // Bridge failure is non-fatal — fall through to existing JS logic
2399
- }
2400
- }
2401
2667
  const transportPreference = config.worker_launch_mode === 'prompt'
2402
2668
  ? 'prompt_stdin'
2403
2669
  : (dispatchPolicy.dispatch_mode === 'transport_direct' ? 'transport_direct' : 'hook_preferred_with_fallback');
@@ -2431,16 +2697,6 @@ async function deliverPendingMailboxMessages(teamName, config, workers, previous
2431
2697
  const direct = await notifyWorkerOutcome(config, workerInfo.index, triggerMessage, workerInfo.pane_id);
2432
2698
  outcome = { ...direct, request_id: queued.request.request_id, message_id: msg.message_id };
2433
2699
  if (outcome.ok) {
2434
- if (isBridgeEnabled()) {
2435
- try {
2436
- getDefaultBridge(cwd).execCommand({ command: 'MarkNotified', request_id: queued.request.request_id, channel: `direct:${outcome.reason}` });
2437
- }
2438
- catch (_) { /* non-fatal */ }
2439
- try {
2440
- getDefaultBridge(cwd).execCommand({ command: 'MarkMailboxNotified', message_id: msg.message_id });
2441
- }
2442
- catch (_) { /* non-fatal */ }
2443
- }
2444
2700
  await markMessageNotified(teamName, worker.name, msg.message_id, cwd).catch(() => false);
2445
2701
  await markDispatchRequestNotified(teamName, queued.request.request_id, { message_id: msg.message_id, last_reason: outcome.reason }, cwd).catch(() => null);
2446
2702
  }
@@ -2464,23 +2720,6 @@ export async function sendWorkerMessage(teamName, fromWorker, toWorker, body, cw
2464
2720
  throw new Error(`Team ${sanitized} not found`);
2465
2721
  const manifest = await readTeamManifestV2(sanitized, cwd);
2466
2722
  const dispatchPolicy = resolveDispatchPolicy(manifest?.policy, config.worker_launch_mode);
2467
- // --- Rust runtime bridge: dual-write mailbox message creation ---
2468
- if (isBridgeEnabled()) {
2469
- try {
2470
- const bridge = getDefaultBridge(cwd);
2471
- const bridgeMessageId = `msg-${fromWorker}-${toWorker}-${Date.now()}`;
2472
- bridge.execCommand({
2473
- command: 'CreateMailboxMessage',
2474
- message_id: bridgeMessageId,
2475
- from_worker: fromWorker,
2476
- to_worker: toWorker,
2477
- body,
2478
- });
2479
- }
2480
- catch (_bridgeErr) {
2481
- // Bridge failure is non-fatal — fall through to existing JS logic
2482
- }
2483
- }
2484
2723
  if (toWorker === 'leader-fixed') {
2485
2724
  const leaderTriggerMessage = generateLeaderMailboxTriggerMessage(sanitized, fromWorker);
2486
2725
  const leaderTransportPreference = dispatchPolicy.dispatch_mode === 'transport_direct'
@@ -2538,7 +2777,7 @@ export async function sendWorkerMessage(teamName, fromWorker, toWorker, body, cw
2538
2777
  }
2539
2778
  if (!finalOutcome.ok)
2540
2779
  throw new Error(`mailbox_notify_failed:${finalOutcome.reason}`);
2541
- return;
2780
+ return finalOutcome;
2542
2781
  }
2543
2782
  const recipient = config.workers.find((w) => w.name === toWorker);
2544
2783
  if (!recipient)
@@ -2583,6 +2822,7 @@ export async function sendWorkerMessage(teamName, fromWorker, toWorker, body, cw
2583
2822
  }
2584
2823
  if (!finalOutcome.ok)
2585
2824
  throw new Error(`mailbox_notify_failed:${finalOutcome.reason}`);
2825
+ return finalOutcome;
2586
2826
  }
2587
2827
  export async function broadcastWorkerMessage(teamName, fromWorker, body, cwd) {
2588
2828
  const sanitized = sanitizeTeamName(teamName);
@@ -2594,25 +2834,6 @@ export async function broadcastWorkerMessage(teamName, fromWorker, body, cwd) {
2594
2834
  const transportPreference = config.worker_launch_mode === 'prompt'
2595
2835
  ? 'prompt_stdin'
2596
2836
  : (dispatchPolicy.dispatch_mode === 'transport_direct' ? 'transport_direct' : 'hook_preferred_with_fallback');
2597
- // --- Rust runtime bridge: dual-write broadcast mailbox messages ---
2598
- if (isBridgeEnabled()) {
2599
- try {
2600
- const bridge = getDefaultBridge(cwd);
2601
- for (const w of config.workers) {
2602
- const bridgeMessageId = `bcast-${fromWorker}-${w.name}-${Date.now()}`;
2603
- bridge.execCommand({
2604
- command: 'CreateMailboxMessage',
2605
- message_id: bridgeMessageId,
2606
- from_worker: fromWorker,
2607
- to_worker: w.name,
2608
- body,
2609
- });
2610
- }
2611
- }
2612
- catch (_bridgeErr) {
2613
- // Bridge failure is non-fatal — fall through to existing JS logic
2614
- }
2615
- }
2616
2837
  const outcomes = await queueBroadcastMailboxMessage({
2617
2838
  teamName: sanitized,
2618
2839
  fromWorker,