oh-my-codex 0.20.2 → 0.20.3

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 (367) hide show
  1. package/Cargo.lock +7 -6
  2. package/Cargo.toml +1 -1
  3. package/README.md +26 -0
  4. package/crates/omx-runtime/Cargo.toml +1 -0
  5. package/crates/omx-runtime/src/main.rs +25 -2
  6. package/crates/omx-runtime/tests/execution.rs +219 -0
  7. package/crates/omx-runtime-core/src/dispatch.rs +96 -10
  8. package/crates/omx-runtime-core/src/engine.rs +839 -29
  9. package/crates/omx-runtime-core/src/lib.rs +8 -0
  10. package/dist/agents/__tests__/definitions.test.js +81 -0
  11. package/dist/agents/__tests__/definitions.test.js.map +1 -1
  12. package/dist/agents/__tests__/native-config.test.js +25 -0
  13. package/dist/agents/__tests__/native-config.test.js.map +1 -1
  14. package/dist/agents/native-config.d.ts +2 -1
  15. package/dist/agents/native-config.d.ts.map +1 -1
  16. package/dist/agents/native-config.js.map +1 -1
  17. package/dist/auth/hotswap.d.ts +6 -0
  18. package/dist/auth/hotswap.d.ts.map +1 -1
  19. package/dist/auth/hotswap.js +85 -15
  20. package/dist/auth/hotswap.js.map +1 -1
  21. package/dist/autopilot/__tests__/ralplan-gate.test.js +1 -1
  22. package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
  23. package/dist/cli/__tests__/auth.test.js +60 -6
  24. package/dist/cli/__tests__/auth.test.js.map +1 -1
  25. package/dist/cli/__tests__/codex-plugin-layout.test.js +80 -0
  26. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  27. package/dist/cli/__tests__/doctor-warning-copy.test.js +103 -1
  28. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  29. package/dist/cli/__tests__/exec.test.js +136 -0
  30. package/dist/cli/__tests__/exec.test.js.map +1 -1
  31. package/dist/cli/__tests__/index.test.js +258 -4
  32. package/dist/cli/__tests__/index.test.js.map +1 -1
  33. package/dist/cli/__tests__/launch-fallback.test.js +287 -1
  34. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  35. package/dist/cli/__tests__/mcp-parity.test.js +4 -1
  36. package/dist/cli/__tests__/mcp-parity.test.js.map +1 -1
  37. package/dist/cli/__tests__/native-hook-claim-journal.test.js +84 -6
  38. package/dist/cli/__tests__/native-hook-claim-journal.test.js.map +1 -1
  39. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +3 -3
  40. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
  41. package/dist/cli/__tests__/ralph.test.js +3 -3
  42. package/dist/cli/__tests__/ralph.test.js.map +1 -1
  43. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js +56 -171
  44. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js.map +1 -1
  45. package/dist/cli/__tests__/ralplan.test.js +37 -111
  46. package/dist/cli/__tests__/ralplan.test.js.map +1 -1
  47. package/dist/cli/__tests__/resume.test.js +11 -10
  48. package/dist/cli/__tests__/resume.test.js.map +1 -1
  49. package/dist/cli/__tests__/session-search-help.test.js +25 -1
  50. package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
  51. package/dist/cli/__tests__/setup-install-mode.test.js +42 -4
  52. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  53. package/dist/cli/__tests__/team.test.js +105 -7
  54. package/dist/cli/__tests__/team.test.js.map +1 -1
  55. package/dist/cli/__tests__/uninstall.test.js +46 -0
  56. package/dist/cli/__tests__/uninstall.test.js.map +1 -1
  57. package/dist/cli/__tests__/windows-popup-loop-contract.test.js +2 -1
  58. package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
  59. package/dist/cli/doctor.d.ts +3 -0
  60. package/dist/cli/doctor.d.ts.map +1 -1
  61. package/dist/cli/doctor.js +15 -2
  62. package/dist/cli/doctor.js.map +1 -1
  63. package/dist/cli/index.d.ts +4 -1
  64. package/dist/cli/index.d.ts.map +1 -1
  65. package/dist/cli/index.js +138 -38
  66. package/dist/cli/index.js.map +1 -1
  67. package/dist/cli/mcp-parity.js +2 -2
  68. package/dist/cli/mcp-parity.js.map +1 -1
  69. package/dist/cli/native-hook-claim-journal.d.ts +16 -5
  70. package/dist/cli/native-hook-claim-journal.d.ts.map +1 -1
  71. package/dist/cli/native-hook-claim-journal.js +42 -29
  72. package/dist/cli/native-hook-claim-journal.js.map +1 -1
  73. package/dist/cli/ralph.js +2 -2
  74. package/dist/cli/ralph.js.map +1 -1
  75. package/dist/cli/ralplan.d.ts +4 -8
  76. package/dist/cli/ralplan.d.ts.map +1 -1
  77. package/dist/cli/ralplan.js +19 -112
  78. package/dist/cli/ralplan.js.map +1 -1
  79. package/dist/cli/session-search.d.ts +5 -0
  80. package/dist/cli/session-search.d.ts.map +1 -1
  81. package/dist/cli/session-search.js +87 -0
  82. package/dist/cli/session-search.js.map +1 -1
  83. package/dist/cli/setup.d.ts +5 -0
  84. package/dist/cli/setup.d.ts.map +1 -1
  85. package/dist/cli/setup.js +80 -31
  86. package/dist/cli/setup.js.map +1 -1
  87. package/dist/cli/uninstall.d.ts +9 -0
  88. package/dist/cli/uninstall.d.ts.map +1 -1
  89. package/dist/cli/uninstall.js +61 -19
  90. package/dist/cli/uninstall.js.map +1 -1
  91. package/dist/compat/__tests__/rust-runtime-compat.test.js +1 -1
  92. package/dist/config/__tests__/generator-idempotent.test.js +330 -1
  93. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  94. package/dist/config/__tests__/generator-notify.test.js +2 -2
  95. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  96. package/dist/config/__tests__/models.test.js +67 -3
  97. package/dist/config/__tests__/models.test.js.map +1 -1
  98. package/dist/config/generator.d.ts +4 -20
  99. package/dist/config/generator.d.ts.map +1 -1
  100. package/dist/config/generator.js +499 -119
  101. package/dist/config/generator.js.map +1 -1
  102. package/dist/config/models.d.ts +12 -2
  103. package/dist/config/models.d.ts.map +1 -1
  104. package/dist/config/models.js +15 -1
  105. package/dist/config/models.js.map +1 -1
  106. package/dist/hooks/__tests__/agents-overlay.test.js +2 -2
  107. package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
  108. package/dist/hooks/__tests__/keyword-detector.test.js +4 -4
  109. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  110. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +238 -543
  111. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  112. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +38 -4
  113. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
  114. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +96 -9
  115. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
  116. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +531 -12
  117. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
  118. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +155 -23
  119. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  120. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +482 -32
  121. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
  122. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +68 -1
  123. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
  124. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js +7 -1
  125. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +1 -1
  126. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +43 -4
  127. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
  128. package/dist/hooks/__tests__/session.test.js +348 -2
  129. package/dist/hooks/__tests__/session.test.js.map +1 -1
  130. package/dist/hooks/agents-overlay.js +1 -1
  131. package/dist/hooks/session.d.ts +26 -2
  132. package/dist/hooks/session.d.ts.map +1 -1
  133. package/dist/hooks/session.js +169 -13
  134. package/dist/hooks/session.js.map +1 -1
  135. package/dist/leader/__tests__/contract.test.js +57 -7
  136. package/dist/leader/__tests__/contract.test.js.map +1 -1
  137. package/dist/leader/contract.d.ts +16 -0
  138. package/dist/leader/contract.d.ts.map +1 -1
  139. package/dist/leader/contract.js +79 -4
  140. package/dist/leader/contract.js.map +1 -1
  141. package/dist/mcp/__tests__/code-intel-server.test.js +31 -3
  142. package/dist/mcp/__tests__/code-intel-server.test.js.map +1 -1
  143. package/dist/mcp/__tests__/wiki-server.test.js +18 -0
  144. package/dist/mcp/__tests__/wiki-server.test.js.map +1 -1
  145. package/dist/mcp/code-intel-server.d.ts.map +1 -1
  146. package/dist/mcp/code-intel-server.js +13 -3
  147. package/dist/mcp/code-intel-server.js.map +1 -1
  148. package/dist/mcp/wiki-server.d.ts +11 -0
  149. package/dist/mcp/wiki-server.d.ts.map +1 -1
  150. package/dist/mcp/wiki-server.js +9 -2
  151. package/dist/mcp/wiki-server.js.map +1 -1
  152. package/dist/modes/__tests__/base-tmux-pane.test.js +4 -4
  153. package/dist/modes/__tests__/base-tmux-pane.test.js.map +1 -1
  154. package/dist/notifications/temp-contract.d.ts.map +1 -1
  155. package/dist/notifications/temp-contract.js +4 -0
  156. package/dist/notifications/temp-contract.js.map +1 -1
  157. package/dist/pipeline/__tests__/orchestrator.test.js +4 -4
  158. package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
  159. package/dist/pipeline/__tests__/stages.test.js +8 -8
  160. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  161. package/dist/ralplan/__tests__/consensus-gate.test.js +134 -10
  162. package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -1
  163. package/dist/ralplan/__tests__/documented-leader-preflight.test.d.ts +2 -0
  164. package/dist/ralplan/__tests__/documented-leader-preflight.test.d.ts.map +1 -0
  165. package/dist/ralplan/__tests__/documented-leader-preflight.test.js +46 -0
  166. package/dist/ralplan/__tests__/documented-leader-preflight.test.js.map +1 -0
  167. package/dist/ralplan/__tests__/runtime.test.js +8 -4
  168. package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
  169. package/dist/ralplan/consensus-gate.d.ts.map +1 -1
  170. package/dist/ralplan/consensus-gate.js +58 -50
  171. package/dist/ralplan/consensus-gate.js.map +1 -1
  172. package/dist/ralplan/documented-leader-preflight.d.ts +34 -0
  173. package/dist/ralplan/documented-leader-preflight.d.ts.map +1 -0
  174. package/dist/ralplan/documented-leader-preflight.js +58 -0
  175. package/dist/ralplan/documented-leader-preflight.js.map +1 -0
  176. package/dist/ralplan/runtime.d.ts +1 -0
  177. package/dist/ralplan/runtime.d.ts.map +1 -1
  178. package/dist/ralplan/runtime.js +11 -7
  179. package/dist/ralplan/runtime.js.map +1 -1
  180. package/dist/runtime/__tests__/bridge.test.js +42 -1
  181. package/dist/runtime/__tests__/bridge.test.js.map +1 -1
  182. package/dist/runtime/bridge.d.ts +12 -0
  183. package/dist/runtime/bridge.d.ts.map +1 -1
  184. package/dist/runtime/bridge.js +52 -1
  185. package/dist/runtime/bridge.js.map +1 -1
  186. package/dist/scripts/__tests__/codex-native-hook.test.js +2620 -487
  187. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  188. package/dist/scripts/__tests__/reasoning-artifact-contract.test.d.ts +2 -0
  189. package/dist/scripts/__tests__/reasoning-artifact-contract.test.d.ts.map +1 -0
  190. package/dist/scripts/__tests__/reasoning-artifact-contract.test.js +108 -0
  191. package/dist/scripts/__tests__/reasoning-artifact-contract.test.js.map +1 -0
  192. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js +46 -173
  193. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js.map +1 -1
  194. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js +39 -159
  195. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js.map +1 -1
  196. package/dist/scripts/__tests__/smoke-packed-install.test.js +254 -2
  197. package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
  198. package/dist/scripts/codex-native-hook.d.ts +4 -1
  199. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  200. package/dist/scripts/codex-native-hook.js +11013 -1153
  201. package/dist/scripts/codex-native-hook.js.map +1 -1
  202. package/dist/scripts/notify-fallback-watcher.js +82 -108
  203. package/dist/scripts/notify-fallback-watcher.js.map +1 -1
  204. package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
  205. package/dist/scripts/notify-hook/auto-nudge.js +74 -3
  206. package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
  207. package/dist/scripts/notify-hook/team-dispatch.d.ts +9 -0
  208. package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
  209. package/dist/scripts/notify-hook/team-dispatch.js +234 -79
  210. package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
  211. package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
  212. package/dist/scripts/notify-hook/team-leader-nudge.js +39 -13
  213. package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
  214. package/dist/scripts/notify-hook/team-tmux-guard.d.ts +12 -4
  215. package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
  216. package/dist/scripts/notify-hook/team-tmux-guard.js +365 -44
  217. package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
  218. package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
  219. package/dist/scripts/notify-hook/team-worker-stop.js +19 -18
  220. package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
  221. package/dist/scripts/notify-hook/team-worker.d.ts +3 -0
  222. package/dist/scripts/notify-hook/team-worker.d.ts.map +1 -1
  223. package/dist/scripts/notify-hook/team-worker.js +37 -28
  224. package/dist/scripts/notify-hook/team-worker.js.map +1 -1
  225. package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
  226. package/dist/scripts/notify-hook/tmux-injection.js +28 -2
  227. package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
  228. package/dist/scripts/notify-hook.js +1 -0
  229. package/dist/scripts/notify-hook.js.map +1 -1
  230. package/dist/scripts/smoke-packed-install.d.ts +48 -3
  231. package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
  232. package/dist/scripts/smoke-packed-install.js +2796 -13
  233. package/dist/scripts/smoke-packed-install.js.map +1 -1
  234. package/dist/state/__tests__/mode-state-context.test.js +54 -0
  235. package/dist/state/__tests__/mode-state-context.test.js.map +1 -1
  236. package/dist/state/__tests__/operations.test.js +2 -0
  237. package/dist/state/__tests__/operations.test.js.map +1 -1
  238. package/dist/state/mode-state-context.d.ts +5 -0
  239. package/dist/state/mode-state-context.d.ts.map +1 -1
  240. package/dist/state/mode-state-context.js +59 -0
  241. package/dist/state/mode-state-context.js.map +1 -1
  242. package/dist/subagents/__tests__/leader-bootstrap-3181.test.js +24 -245
  243. package/dist/subagents/__tests__/leader-bootstrap-3181.test.js.map +1 -1
  244. package/dist/subagents/tracker.d.ts +1 -82
  245. package/dist/subagents/tracker.d.ts.map +1 -1
  246. package/dist/subagents/tracker.js +3 -275
  247. package/dist/subagents/tracker.js.map +1 -1
  248. package/dist/team/__tests__/api-interop.test.js +91 -1
  249. package/dist/team/__tests__/api-interop.test.js.map +1 -1
  250. package/dist/team/__tests__/delivery-e2e-smoke.test.js +52 -10
  251. package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
  252. package/dist/team/__tests__/idle-nudge.test.js +45 -15
  253. package/dist/team/__tests__/idle-nudge.test.js.map +1 -1
  254. package/dist/team/__tests__/mcp-comm.test.js +93 -27
  255. package/dist/team/__tests__/mcp-comm.test.js.map +1 -1
  256. package/dist/team/__tests__/model-contract.test.js +110 -3
  257. package/dist/team/__tests__/model-contract.test.js.map +1 -1
  258. package/dist/team/__tests__/runtime-cli.test.js +9 -0
  259. package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
  260. package/dist/team/__tests__/runtime.test.js +3204 -335
  261. package/dist/team/__tests__/runtime.test.js.map +1 -1
  262. package/dist/team/__tests__/scaling.test.js +2359 -142
  263. package/dist/team/__tests__/scaling.test.js.map +1 -1
  264. package/dist/team/__tests__/state.test.js +804 -36
  265. package/dist/team/__tests__/state.test.js.map +1 -1
  266. package/dist/team/__tests__/team-ops-contract.test.js +6 -0
  267. package/dist/team/__tests__/team-ops-contract.test.js.map +1 -1
  268. package/dist/team/__tests__/tmux-session.test.js +3049 -784
  269. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  270. package/dist/team/__tests__/worker-runtime-identity.test.js +38 -2
  271. package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
  272. package/dist/team/__tests__/worktree.test.js +6 -0
  273. package/dist/team/__tests__/worktree.test.js.map +1 -1
  274. package/dist/team/api-interop.d.ts.map +1 -1
  275. package/dist/team/api-interop.js +15 -9
  276. package/dist/team/api-interop.js.map +1 -1
  277. package/dist/team/exact-pane.d.ts +23 -0
  278. package/dist/team/exact-pane.d.ts.map +1 -0
  279. package/dist/team/exact-pane.js +133 -0
  280. package/dist/team/exact-pane.js.map +1 -0
  281. package/dist/team/idle-nudge.d.ts +12 -1
  282. package/dist/team/idle-nudge.d.ts.map +1 -1
  283. package/dist/team/idle-nudge.js +41 -14
  284. package/dist/team/idle-nudge.js.map +1 -1
  285. package/dist/team/model-contract.d.ts +2 -5
  286. package/dist/team/model-contract.d.ts.map +1 -1
  287. package/dist/team/model-contract.js +59 -17
  288. package/dist/team/model-contract.js.map +1 -1
  289. package/dist/team/runtime.d.ts +1 -0
  290. package/dist/team/runtime.d.ts.map +1 -1
  291. package/dist/team/runtime.js +1244 -180
  292. package/dist/team/runtime.js.map +1 -1
  293. package/dist/team/scaling.d.ts +13 -1
  294. package/dist/team/scaling.d.ts.map +1 -1
  295. package/dist/team/scaling.js +1430 -384
  296. package/dist/team/scaling.js.map +1 -1
  297. package/dist/team/state/dispatch.d.ts.map +1 -1
  298. package/dist/team/state/dispatch.js +26 -8
  299. package/dist/team/state/dispatch.js.map +1 -1
  300. package/dist/team/state/locks.d.ts.map +1 -1
  301. package/dist/team/state/locks.js +61 -13
  302. package/dist/team/state/locks.js.map +1 -1
  303. package/dist/team/state/tasks.d.ts.map +1 -1
  304. package/dist/team/state/tasks.js +4 -3
  305. package/dist/team/state/tasks.js.map +1 -1
  306. package/dist/team/state.d.ts +67 -1
  307. package/dist/team/state.d.ts.map +1 -1
  308. package/dist/team/state.js +636 -129
  309. package/dist/team/state.js.map +1 -1
  310. package/dist/team/team-ops.d.ts +5 -0
  311. package/dist/team/team-ops.d.ts.map +1 -1
  312. package/dist/team/team-ops.js +4 -0
  313. package/dist/team/team-ops.js.map +1 -1
  314. package/dist/team/tmux-session.d.ts +123 -23
  315. package/dist/team/tmux-session.d.ts.map +1 -1
  316. package/dist/team/tmux-session.js +1250 -291
  317. package/dist/team/tmux-session.js.map +1 -1
  318. package/dist/team/worktree.d.ts.map +1 -1
  319. package/dist/team/worktree.js +4 -0
  320. package/dist/team/worktree.js.map +1 -1
  321. package/dist/utils/__tests__/file-durability.test.d.ts +2 -0
  322. package/dist/utils/__tests__/file-durability.test.d.ts.map +1 -0
  323. package/dist/utils/__tests__/file-durability.test.js +57 -0
  324. package/dist/utils/__tests__/file-durability.test.js.map +1 -0
  325. package/dist/utils/file-durability.d.ts +15 -0
  326. package/dist/utils/file-durability.d.ts.map +1 -0
  327. package/dist/utils/file-durability.js +37 -0
  328. package/dist/utils/file-durability.js.map +1 -0
  329. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js +3 -0
  330. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js.map +1 -1
  331. package/dist/wiki/lint.d.ts +4 -1
  332. package/dist/wiki/lint.d.ts.map +1 -1
  333. package/dist/wiki/lint.js +3 -2
  334. package/dist/wiki/lint.js.map +1 -1
  335. package/package.json +1 -1
  336. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  337. package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +24 -0
  338. package/plugins/oh-my-codex/skills/ralph/SKILL.md +6 -4
  339. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +3 -2
  340. package/plugins/oh-my-codex/skills/team/SKILL.md +13 -9
  341. package/skills/ralph/SKILL.md +6 -4
  342. package/skills/ralplan/SKILL.md +3 -2
  343. package/skills/team/SKILL.md +13 -9
  344. package/src/scripts/__tests__/codex-native-hook.test.ts +2826 -497
  345. package/src/scripts/__tests__/reasoning-artifact-contract.test.ts +129 -0
  346. package/src/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.ts +48 -185
  347. package/src/scripts/__tests__/role-intent-durable-recovery-3181.test.ts +44 -177
  348. package/src/scripts/__tests__/smoke-packed-install.test.ts +320 -2
  349. package/src/scripts/codex-native-hook.ts +10715 -1069
  350. package/src/scripts/notify-fallback-watcher.ts +84 -115
  351. package/src/scripts/notify-hook/auto-nudge.ts +71 -3
  352. package/src/scripts/notify-hook/team-dispatch.ts +242 -75
  353. package/src/scripts/notify-hook/team-leader-nudge.ts +40 -18
  354. package/src/scripts/notify-hook/team-tmux-guard.ts +366 -39
  355. package/src/scripts/notify-hook/team-worker-stop.ts +20 -16
  356. package/src/scripts/notify-hook/team-worker.ts +39 -26
  357. package/src/scripts/notify-hook/tmux-injection.ts +28 -2
  358. package/src/scripts/notify-hook.ts +1 -0
  359. package/src/scripts/smoke-packed-install.ts +3196 -14
  360. package/dist/subagents/__tests__/adapted-role-binding.test.d.ts +0 -2
  361. package/dist/subagents/__tests__/adapted-role-binding.test.d.ts.map +0 -1
  362. package/dist/subagents/__tests__/adapted-role-binding.test.js +0 -988
  363. package/dist/subagents/__tests__/adapted-role-binding.test.js.map +0 -1
  364. package/dist/subagents/adapted-role-binding.d.ts +0 -17
  365. package/dist/subagents/adapted-role-binding.d.ts.map +0 -1
  366. package/dist/subagents/adapted-role-binding.js +0 -75
  367. package/dist/subagents/adapted-role-binding.js.map +0 -1
@@ -1,7 +1,8 @@
1
- import { appendFile, readFile, writeFile, mkdir, rm, rename, readdir } from 'fs/promises';
2
- import { join, dirname, resolve, sep } from 'path';
1
+ import { appendFile, readFile, writeFile, mkdir, rm, rename, readdir, open, realpath } from 'fs/promises';
2
+ import { basename, join, dirname, resolve, sep } from 'path';
3
3
  import { existsSync } from 'fs';
4
4
  import { randomUUID } from 'crypto';
5
+ import { AsyncLocalStorage } from 'async_hooks';
5
6
  import { readUsableSessionState } from '../hooks/session.js';
6
7
  import { isTerminalPhase } from './orchestrator.js';
7
8
  import { computeTaskReadiness as computeTaskReadinessImpl, claimTask as claimTaskImpl, transitionTaskStatus as transitionTaskStatusImpl, releaseTaskClaim as releaseTaskClaimImpl, reclaimExpiredTaskClaim as reclaimExpiredTaskClaimImpl, listTasks as listTasksImpl, } from './state/tasks.js';
@@ -16,6 +17,20 @@ import { TEAM_NAME_SAFE_PATTERN, WORKER_NAME_SAFE_PATTERN, TASK_ID_SAFE_PATTERN,
16
17
  import { resolveCanonicalTeamStateRoot } from './state-root.js';
17
18
  import { normalizeTeamTaskCoordinationPlanForStorage } from './coordination-protocol.js';
18
19
  let renameForAtomicWrite = rename;
20
+ let openForAtomicWrite = open;
21
+ let platformForAtomicWrite = process.platform;
22
+ export function setWriteAtomicOpenForTests(fn) {
23
+ openForAtomicWrite = fn;
24
+ }
25
+ export function resetWriteAtomicOpenForTests() {
26
+ openForAtomicWrite = open;
27
+ }
28
+ export function setWriteAtomicPlatformForTests(platform) {
29
+ platformForAtomicWrite = platform;
30
+ }
31
+ export function resetWriteAtomicPlatformForTests() {
32
+ platformForAtomicWrite = process.platform;
33
+ }
19
34
  export function setWriteAtomicRenameForTests(fn) {
20
35
  renameForAtomicWrite = fn;
21
36
  }
@@ -314,6 +329,10 @@ function isTeamManifestV2(value) {
314
329
  return false;
315
330
  if (!(typeof v.hud_pane_id === 'string' || v.hud_pane_id === null))
316
331
  return false;
332
+ if (!(typeof v.leader_pane_pid === 'number' || v.leader_pane_pid === null || v.leader_pane_pid === undefined))
333
+ return false;
334
+ if (!(typeof v.hud_pane_pid === 'number' || v.hud_pane_pid === null || v.hud_pane_pid === undefined))
335
+ return false;
317
336
  if (!(typeof v.resize_hook_name === 'string' || v.resize_hook_name === null))
318
337
  return false;
319
338
  if (!(typeof v.resize_hook_target === 'string' || v.resize_hook_target === null))
@@ -326,14 +345,54 @@ function isTeamManifestV2(value) {
326
345
  return false;
327
346
  return true;
328
347
  }
329
- // Atomic write: write to {path}.tmp.{pid}, then rename
348
+ // Atomic write: write to {path}.tmp.{pid}, fsync it, rename, then fsync parent.
349
+ function isUnsupportedParentDirectorySyncError(error) {
350
+ const code = error.code;
351
+ return (code === 'EINVAL'
352
+ || code === 'ENOTSUP'
353
+ || code === 'EISDIR'
354
+ || (code === 'EPERM' && platformForAtomicWrite === 'win32'));
355
+ }
356
+ async function syncParentDirectory(path) {
357
+ let handle;
358
+ try {
359
+ handle = await openForAtomicWrite(dirname(path), 'r');
360
+ }
361
+ catch (error) {
362
+ if (isUnsupportedParentDirectorySyncError(error))
363
+ return;
364
+ throw error;
365
+ }
366
+ try {
367
+ await handle.sync();
368
+ }
369
+ catch (error) {
370
+ if (!isUnsupportedParentDirectorySyncError(error))
371
+ throw error;
372
+ }
373
+ finally {
374
+ await handle.close();
375
+ }
376
+ }
377
+ export async function removeDurableFile(path) {
378
+ await rm(path, { force: true });
379
+ await syncParentDirectory(path);
380
+ }
330
381
  export async function writeAtomic(filePath, data) {
331
382
  const parent = dirname(filePath);
332
383
  await mkdir(parent, { recursive: true });
333
384
  const tmpPath = `${filePath}.tmp.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`;
334
385
  await writeFile(tmpPath, data, 'utf8');
386
+ const handle = await openForAtomicWrite(tmpPath, 'r');
387
+ try {
388
+ await handle.sync();
389
+ }
390
+ finally {
391
+ await handle.close();
392
+ }
335
393
  try {
336
394
  await renameForAtomicWrite(tmpPath, filePath);
395
+ await syncParentDirectory(filePath);
337
396
  }
338
397
  catch (error) {
339
398
  const err = error;
@@ -350,6 +409,205 @@ export async function writeAtomic(filePath, data) {
350
409
  throw error;
351
410
  }
352
411
  }
412
+ function membershipTransactionPath(teamName, cwd) {
413
+ return join(teamDir(teamName, cwd), '.membership-task-transaction.json');
414
+ }
415
+ async function applyMembershipTransactionFiles(files, useNewBytes) {
416
+ for (const file of files) {
417
+ const bytes = useNewBytes ? file.newBytes : file.oldBytes;
418
+ if (bytes === null)
419
+ await removeDurableFile(file.path);
420
+ else
421
+ await writeAtomic(file.path, bytes);
422
+ }
423
+ }
424
+ async function validateMembershipTransactionFiles(teamName, cwd, files) {
425
+ const expectedConfigPath = resolve(teamConfigPath(teamName, cwd));
426
+ const expectedManifestPath = resolve(teamManifestV2Path(teamName, cwd));
427
+ const expectedTasksDir = resolve(teamDir(teamName, cwd), 'tasks');
428
+ const seenPaths = new Set();
429
+ let configCount = 0;
430
+ const canonicalStateRoot = await realpath(resolveTeamStateRoot(cwd));
431
+ const canonicalTeamRoot = await realpath(teamDir(teamName, cwd));
432
+ if (canonicalTeamRoot !== canonicalStateRoot && !canonicalTeamRoot.startsWith(`${canonicalStateRoot}${sep}`)) {
433
+ throw new Error(`Membership transaction team root escapes canonical state root for ${teamName}`);
434
+ }
435
+ const canonicalTasksDir = await realpath(resolve(teamDir(teamName, cwd), 'tasks'));
436
+ if (!canonicalTasksDir.startsWith(`${canonicalTeamRoot}${sep}`)) {
437
+ throw new Error(`Membership transaction tasks root escapes canonical team root for ${teamName}`);
438
+ }
439
+ for (const file of files) {
440
+ if (!file || typeof file.path !== 'string'
441
+ || (file.oldBytes !== null && typeof file.oldBytes !== 'string')
442
+ || (file.newBytes !== null && typeof file.newBytes !== 'string')) {
443
+ throw new Error(`Invalid membership transaction file entry for ${teamName}`);
444
+ }
445
+ const resolvedPath = resolve(file.path);
446
+ if (resolvedPath !== file.path || seenPaths.has(resolvedPath)) {
447
+ throw new Error(`Invalid membership transaction path for ${teamName}`);
448
+ }
449
+ seenPaths.add(resolvedPath);
450
+ let expectedParent;
451
+ if (resolvedPath === expectedConfigPath) {
452
+ configCount += 1;
453
+ expectedParent = canonicalTeamRoot;
454
+ }
455
+ else if (resolvedPath === expectedManifestPath) {
456
+ expectedParent = canonicalTeamRoot;
457
+ }
458
+ else {
459
+ if (dirname(resolvedPath) !== expectedTasksDir) {
460
+ throw new Error(`Membership transaction path escapes expected team files for ${teamName}`);
461
+ }
462
+ const match = basename(resolvedPath).match(/^task-([A-Za-z0-9_-]+)\.json$/);
463
+ if (!match || resolve(taskFilePath(teamName, match[1], cwd)) !== resolvedPath) {
464
+ throw new Error(`Invalid membership transaction task path for ${teamName}`);
465
+ }
466
+ expectedParent = canonicalTasksDir;
467
+ }
468
+ const canonicalParent = await realpath(dirname(resolvedPath));
469
+ if (canonicalParent !== expectedParent) {
470
+ throw new Error(`Membership transaction path traverses a symlink for ${teamName}`);
471
+ }
472
+ }
473
+ if (configCount !== 1) {
474
+ throw new Error(`Membership transaction must contain exactly one config path for ${teamName}`);
475
+ }
476
+ }
477
+ /**
478
+ * Resolve an interrupted membership/task commit before a reader or writer observes
479
+ * its files. Prepared transactions roll back; committed transactions roll forward.
480
+ */
481
+ export async function recoverTeamMembershipTaskTransaction(teamName, cwd, options = {}) {
482
+ const journalPath = membershipTransactionPath(teamName, cwd);
483
+ let journal;
484
+ try {
485
+ journal = JSON.parse(await readFile(journalPath, 'utf8'));
486
+ }
487
+ catch (error) {
488
+ if (error.code === 'ENOENT')
489
+ return;
490
+ throw error;
491
+ }
492
+ if (journal.schemaVersion !== 1 || (journal.phase !== 'prepared' && journal.phase !== 'committed') || !Array.isArray(journal.files)) {
493
+ throw new Error(`Invalid membership transaction journal for ${teamName}`);
494
+ }
495
+ await validateMembershipTransactionFiles(teamName, cwd, journal.files);
496
+ await applyMembershipTransactionFiles(journal.files, journal.phase === 'committed');
497
+ if (!options.retainJournal)
498
+ await removeDurableFile(journalPath);
499
+ }
500
+ /** Finalize a previously committed membership transaction after caller verification. */
501
+ export async function finalizeTeamMembershipTaskTransaction(teamName, cwd) {
502
+ const journalPath = membershipTransactionPath(teamName, cwd);
503
+ const journal = JSON.parse(await readFile(journalPath, 'utf8'));
504
+ if (journal.schemaVersion !== 1 || journal.phase !== 'committed' || !Array.isArray(journal.files)) {
505
+ throw new Error(`Cannot finalize non-committed membership transaction for ${teamName}`);
506
+ }
507
+ await validateMembershipTransactionFiles(teamName, cwd, journal.files);
508
+ await removeDurableFile(journalPath);
509
+ }
510
+ /**
511
+ * Persist config/manifest and task membership changes as an old-or-new journaled
512
+ * generation. Callers must hold the team membership barrier for the whole call.
513
+ */
514
+ export async function commitTeamMembershipTaskTransaction(teamName, cwd, transaction) {
515
+ if (!taskMembershipBarrierContext.getStore()?.has(taskMembershipBarrierKey(teamName, cwd))) {
516
+ throw new Error(`membership_transaction_barrier_required:${teamName}`);
517
+ }
518
+ const currentGeneration = await readConfigGenerationRaw(teamName, cwd);
519
+ if (currentGeneration === null)
520
+ throw new Error(`team_config_missing:${teamName}`);
521
+ if (transaction.baseGeneration !== currentGeneration) {
522
+ throw new Error(`team_membership_stale_generation:${teamName}:${transaction.baseGeneration}:${currentGeneration}`);
523
+ }
524
+ const acceptedGeneration = currentGeneration + 1;
525
+ const nextConfig = JSON.parse(transaction.config.newBytes);
526
+ nextConfig.config_generation = acceptedGeneration;
527
+ const nextManifest = transaction.manifest?.newBytes === null
528
+ ? null
529
+ : transaction.manifest
530
+ ? { ...JSON.parse(transaction.manifest.newBytes), config_generation: acceptedGeneration }
531
+ : undefined;
532
+ const files = [
533
+ ...transaction.tasks.map((task) => ({
534
+ path: taskFilePath(teamName, task.taskId, cwd),
535
+ oldBytes: task.oldBytes,
536
+ newBytes: task.newBytes,
537
+ })),
538
+ {
539
+ path: teamConfigPath(teamName, cwd),
540
+ oldBytes: transaction.config.oldBytes,
541
+ newBytes: JSON.stringify(nextConfig, null, 2),
542
+ },
543
+ ];
544
+ if (transaction.manifest) {
545
+ files.push({
546
+ path: teamManifestV2Path(teamName, cwd),
547
+ oldBytes: transaction.manifest.oldBytes,
548
+ newBytes: nextManifest === null ? null : JSON.stringify(nextManifest, null, 2),
549
+ });
550
+ }
551
+ const journalPath = membershipTransactionPath(teamName, cwd);
552
+ const journal = {
553
+ schemaVersion: 1,
554
+ // Membership rollback is itself a durable desired-state transition: an
555
+ // interruption must finish restoring the original membership, not revive
556
+ // the transient scaled-up generation.
557
+ phase: transaction.recoverToNewOnFailure ? 'committed' : 'prepared',
558
+ files,
559
+ };
560
+ await writeAtomic(journalPath, JSON.stringify(journal, null, 2));
561
+ try {
562
+ if (transaction.interruptAfterFirstTaskWrite && transaction.tasks.length > 0) {
563
+ await applyMembershipTransactionFiles(files.slice(0, 1), true);
564
+ throw new Error('injected_scale_down_interruption:after-first-task-write');
565
+ }
566
+ await applyMembershipTransactionFiles(files, true);
567
+ if (transaction.failRollbackPersistence || transaction.failRollbackPersistenceAfter) {
568
+ // Exercise the restore path after a complete new generation is visible.
569
+ // The catch below intentionally writes OLD bytes then leaves the durable
570
+ // journal in place so public entry recovery owns convergence.
571
+ throw new Error('injected_scale_down_failure:rollback-persistence-failure');
572
+ }
573
+ journal.phase = 'committed';
574
+ await writeAtomic(journalPath, JSON.stringify(journal, null, 2));
575
+ if (!transaction.retainJournalOnSuccess)
576
+ await removeDurableFile(journalPath);
577
+ }
578
+ catch (error) {
579
+ if (error instanceof Error && error.message === 'injected_scale_down_interruption:after-first-task-write')
580
+ throw error;
581
+ if (transaction.failRollbackPersistence || transaction.failRollbackPersistenceAfter) {
582
+ const target = transaction.failRollbackPersistenceAfter;
583
+ const targetIndex = target ? files.findIndex((file) => file.path === (target === 'config'
584
+ ? teamConfigPath(teamName, cwd)
585
+ : teamManifestV2Path(teamName, cwd))) : 0;
586
+ const partial = files.slice(0, Math.max(1, targetIndex + 1));
587
+ await applyMembershipTransactionFiles(partial, false);
588
+ throw error;
589
+ }
590
+ if (transaction.recoverToNewOnFailure) {
591
+ try {
592
+ await recoverTeamMembershipTaskTransaction(teamName, cwd);
593
+ }
594
+ catch {
595
+ // Keep the committed-direction journal as durable recovery authority.
596
+ }
597
+ throw error;
598
+ }
599
+ // Leave the prepared marker durable if restoring old bytes also fails. Entry
600
+ // recovery will retry until the state has converged to the old generation.
601
+ try {
602
+ await applyMembershipTransactionFiles(files, false);
603
+ await removeDurableFile(journalPath);
604
+ }
605
+ catch {
606
+ // The prepared journal is the durable recovery authority.
607
+ }
608
+ throw error;
609
+ }
610
+ }
353
611
  // Initialize team state directory + config.json
354
612
  // Creates: .omx/state/team/{name}/, workers/{worker-1}..{worker-N}/, tasks/
355
613
  // Throws if workerCount > maxWorkers (default 20)
@@ -401,12 +659,16 @@ export async function initTeamState(teamName, task, agentType, workerCount, cwd,
401
659
  workers,
402
660
  created_at: new Date().toISOString(),
403
661
  tmux_session: `omx-team-${teamName}`,
662
+ tmux_session_id: undefined,
663
+ tmux_session_created: undefined,
404
664
  next_task_id: 1,
405
665
  leader_cwd: workspace.leader_cwd,
406
666
  team_state_root: workspace.team_state_root,
407
667
  workspace_mode: workspace.workspace_mode,
408
668
  worktree_mode: workspace.worktree_mode,
409
669
  leader_pane_id: null,
670
+ leader_pane_pid: null,
671
+ hud_pane_pid: null,
410
672
  hud_pane_id: null,
411
673
  tmux_pane_owner_id: tmuxPaneOwnerId,
412
674
  resize_hook_name: null,
@@ -415,8 +677,11 @@ export async function initTeamState(teamName, task, agentType, workerCount, cwd,
415
677
  display_name: workspace.display_name,
416
678
  requested_name: workspace.requested_name,
417
679
  identity_source: workspace.identity_source,
680
+ config_generation: 0,
418
681
  };
419
- await writeAtomic(join(root, 'config.json'), JSON.stringify(config, null, 2));
682
+ await withTeamTaskBarrier(teamName, cwd, async () => {
683
+ await writeAtomic(join(root, 'config.json'), JSON.stringify(config, null, 2));
684
+ });
420
685
  await writeTeamPhase(teamName, {
421
686
  current_phase: 'team-exec',
422
687
  max_fix_attempts: 3,
@@ -438,6 +703,8 @@ export async function initTeamState(teamName, task, agentType, workerCount, cwd,
438
703
  lifecycle_profile: lifecycleProfile,
439
704
  permissions_snapshot: permissionsSnapshot,
440
705
  tmux_session: config.tmux_session,
706
+ tmux_session_id: config.tmux_session_id,
707
+ tmux_session_created: config.tmux_session_created,
441
708
  worker_count: workerCount,
442
709
  workers,
443
710
  next_task_id: 1,
@@ -447,6 +714,8 @@ export async function initTeamState(teamName, task, agentType, workerCount, cwd,
447
714
  workspace_mode: workspace.workspace_mode,
448
715
  worktree_mode: workspace.worktree_mode,
449
716
  leader_pane_id: null,
717
+ leader_pane_pid: null,
718
+ hud_pane_pid: null,
450
719
  hud_pane_id: null,
451
720
  tmux_pane_owner_id: tmuxPaneOwnerId,
452
721
  resize_hook_name: null,
@@ -460,34 +729,63 @@ export async function initTeamState(teamName, task, agentType, workerCount, cwd,
460
729
  }
461
730
  async function writeConfig(cfg, cwd) {
462
731
  const normalized = normalizeTeamConfig(cfg);
463
- const p = teamConfigPath(normalized.name, cwd);
464
- await writeAtomic(p, JSON.stringify(normalized, null, 2));
732
+ const configPath = teamConfigPath(normalized.name, cwd);
733
+ const oldConfigBytes = await readFile(configPath, 'utf8');
465
734
  // Keep v2 manifest in sync when present. Don't create it implicitly here to preserve migration behavior.
466
- const existing = await readTeamManifestV2(normalized.name, cwd);
467
- if (existing) {
468
- const merged = {
469
- ...existing,
470
- task: normalized.task,
471
- tmux_session: normalized.tmux_session,
472
- worker_count: normalized.worker_count,
473
- workers: normalized.workers,
474
- lifecycle_profile: normalized.lifecycle_profile,
475
- next_task_id: normalizeNextTaskId(normalized.next_task_id),
476
- leader_cwd: normalized.leader_cwd,
477
- team_state_root: normalized.team_state_root,
478
- workspace_mode: normalized.workspace_mode,
479
- worktree_mode: normalized.worktree_mode,
480
- leader_pane_id: normalized.leader_pane_id,
481
- hud_pane_id: normalized.hud_pane_id,
482
- tmux_pane_owner_id: normalized.tmux_pane_owner_id,
483
- resize_hook_name: normalized.resize_hook_name,
484
- resize_hook_target: normalized.resize_hook_target,
485
- next_worker_index: normalized.next_worker_index ?? existing.next_worker_index,
486
- display_name: normalized.display_name ?? existing.display_name,
487
- requested_name: normalized.requested_name ?? existing.requested_name,
488
- identity_source: normalized.identity_source ?? existing.identity_source,
489
- };
490
- await writeTeamManifestV2(merged, cwd);
735
+ const existing = await readTeamManifestV2Raw(normalized.name, cwd);
736
+ if (!existing) {
737
+ await writeAtomic(configPath, JSON.stringify(normalized, null, 2));
738
+ return;
739
+ }
740
+ const merged = {
741
+ ...existing,
742
+ task: normalized.task,
743
+ tmux_session: normalized.tmux_session,
744
+ tmux_session_id: normalized.tmux_session_id,
745
+ tmux_session_created: normalized.tmux_session_created,
746
+ worker_count: normalized.worker_count,
747
+ workers: normalized.workers,
748
+ lifecycle_profile: normalized.lifecycle_profile,
749
+ next_task_id: normalizeNextTaskId(normalized.next_task_id),
750
+ leader_cwd: normalized.leader_cwd,
751
+ team_state_root: normalized.team_state_root,
752
+ workspace_mode: normalized.workspace_mode,
753
+ worktree_mode: normalized.worktree_mode,
754
+ leader_pane_id: normalized.leader_pane_id,
755
+ leader_pane_pid: normalized.leader_pane_pid,
756
+ hud_pane_pid: normalized.hud_pane_pid,
757
+ hud_pane_id: normalized.hud_pane_id,
758
+ tmux_pane_owner_id: normalized.tmux_pane_owner_id,
759
+ resize_hook_name: normalized.resize_hook_name,
760
+ resize_hook_target: normalized.resize_hook_target,
761
+ next_worker_index: normalized.next_worker_index ?? existing.next_worker_index,
762
+ display_name: normalized.display_name ?? existing.display_name,
763
+ requested_name: normalized.requested_name ?? existing.requested_name,
764
+ identity_source: normalized.identity_source ?? existing.identity_source,
765
+ config_generation: normalized.config_generation,
766
+ };
767
+ const manifestPath = teamManifestV2Path(normalized.name, cwd);
768
+ const files = [
769
+ { path: configPath, oldBytes: oldConfigBytes, newBytes: JSON.stringify(normalized, null, 2) },
770
+ { path: manifestPath, oldBytes: await readFile(manifestPath, 'utf8'), newBytes: JSON.stringify(merged, null, 2) },
771
+ ];
772
+ const journalPath = membershipTransactionPath(normalized.name, cwd);
773
+ const journal = { schemaVersion: 1, phase: 'prepared', files };
774
+ await writeAtomic(journalPath, JSON.stringify(journal, null, 2));
775
+ try {
776
+ await applyMembershipTransactionFiles(files, true);
777
+ journal.phase = 'committed';
778
+ await writeAtomic(journalPath, JSON.stringify(journal, null, 2));
779
+ await removeDurableFile(journalPath);
780
+ }
781
+ catch (error) {
782
+ try {
783
+ await recoverTeamMembershipTaskTransaction(normalized.name, cwd);
784
+ }
785
+ catch {
786
+ // The prepared journal is the durable recovery authority.
787
+ }
788
+ throw error;
491
789
  }
492
790
  }
493
791
  function teamConfigFromManifest(manifest) {
@@ -507,14 +805,20 @@ function teamConfigFromManifest(manifest) {
507
805
  workers: manifest.workers,
508
806
  created_at: manifest.created_at,
509
807
  tmux_session: manifest.tmux_session,
808
+ tmux_session_id: manifest.tmux_session_id,
809
+ tmux_session_created: manifest.tmux_session_created,
510
810
  next_task_id: manifest.next_task_id,
511
811
  leader_cwd: manifest.leader_cwd,
512
812
  team_state_root: manifest.team_state_root,
513
813
  workspace_mode: manifest.workspace_mode,
514
814
  worktree_mode: manifest.worktree_mode,
515
815
  leader_pane_id: manifest.leader_pane_id,
816
+ leader_pane_pid: manifest.leader_pane_pid ?? null,
817
+ hud_pane_pid: manifest.hud_pane_pid ?? null,
516
818
  hud_pane_id: manifest.hud_pane_id,
517
- tmux_pane_owner_id: manifest.tmux_pane_owner_id ?? defaultTmuxPaneOwnerId(manifest.name),
819
+ tmux_pane_owner_id: typeof manifest.tmux_pane_owner_id === 'string' && manifest.tmux_pane_owner_id.trim() !== ''
820
+ ? manifest.tmux_pane_owner_id.trim()
821
+ : undefined,
518
822
  resize_hook_name: manifest.resize_hook_name,
519
823
  resize_hook_target: manifest.resize_hook_target,
520
824
  next_worker_index: manifest.next_worker_index,
@@ -525,14 +829,25 @@ function teamConfigFromManifest(manifest) {
525
829
  }
526
830
  function normalizeTeamConfig(config) {
527
831
  const workerLaunchMode = config.worker_launch_mode === 'prompt' ? 'prompt' : 'interactive';
832
+ const configGeneration = typeof config.config_generation === 'number'
833
+ && Number.isSafeInteger(config.config_generation) && config.config_generation >= 0
834
+ ? config.config_generation
835
+ : 0;
528
836
  return {
529
837
  ...config,
838
+ config_generation: configGeneration,
530
839
  lifecycle_profile: 'default',
531
840
  leader_pane_id: config.leader_pane_id ?? null,
841
+ leader_pane_pid: typeof config.leader_pane_pid === 'number' && Number.isSafeInteger(config.leader_pane_pid) && config.leader_pane_pid > 0
842
+ ? config.leader_pane_pid
843
+ : null,
844
+ hud_pane_pid: typeof config.hud_pane_pid === 'number' && Number.isSafeInteger(config.hud_pane_pid) && config.hud_pane_pid > 0
845
+ ? config.hud_pane_pid
846
+ : null,
532
847
  hud_pane_id: config.hud_pane_id ?? null,
533
848
  tmux_pane_owner_id: typeof config.tmux_pane_owner_id === 'string' && config.tmux_pane_owner_id.trim() !== ''
534
849
  ? config.tmux_pane_owner_id.trim()
535
- : defaultTmuxPaneOwnerId(config.name),
850
+ : undefined,
536
851
  resize_hook_name: config.resize_hook_name ?? null,
537
852
  resize_hook_target: config.resize_hook_target ?? null,
538
853
  worker_launch_mode: workerLaunchMode,
@@ -556,6 +871,8 @@ function teamManifestFromConfig(config) {
556
871
  lifecycle_profile: normalized.lifecycle_profile,
557
872
  permissions_snapshot: defaultPermissionsSnapshot(),
558
873
  tmux_session: normalized.tmux_session,
874
+ tmux_session_id: normalized.tmux_session_id,
875
+ tmux_session_created: normalized.tmux_session_created,
559
876
  worker_count: normalized.worker_count,
560
877
  workers: normalized.workers,
561
878
  next_task_id: normalizeNextTaskId(normalized.next_task_id),
@@ -565,6 +882,8 @@ function teamManifestFromConfig(config) {
565
882
  workspace_mode: normalized.workspace_mode,
566
883
  worktree_mode: normalized.worktree_mode,
567
884
  leader_pane_id: normalized.leader_pane_id,
885
+ leader_pane_pid: normalized.leader_pane_pid,
886
+ hud_pane_pid: normalized.hud_pane_pid,
568
887
  hud_pane_id: normalized.hud_pane_id,
569
888
  tmux_pane_owner_id: normalized.tmux_pane_owner_id,
570
889
  resize_hook_name: normalized.resize_hook_name,
@@ -573,27 +892,30 @@ function teamManifestFromConfig(config) {
573
892
  display_name: normalized.display_name,
574
893
  requested_name: normalized.requested_name,
575
894
  identity_source: normalized.identity_source,
895
+ config_generation: normalized.config_generation,
576
896
  };
577
897
  }
578
898
  export async function writeTeamManifestV2(manifest, cwd) {
579
- const normalizedPolicy = normalizeTeamPolicy(manifest.policy, {
580
- display_mode: manifest.policy?.display_mode === 'split_pane' ? 'split_pane' : 'auto',
581
- worker_launch_mode: manifest.policy?.worker_launch_mode === 'prompt' ? 'prompt' : 'interactive',
899
+ await withTeamTaskBarrier(manifest.name, cwd, async () => {
900
+ const normalizedPolicy = normalizeTeamPolicy(manifest.policy, {
901
+ display_mode: manifest.policy?.display_mode === 'split_pane' ? 'split_pane' : 'auto',
902
+ worker_launch_mode: manifest.policy?.worker_launch_mode === 'prompt' ? 'prompt' : 'interactive',
903
+ });
904
+ const normalizedGovernance = normalizeTeamGovernance(manifest.governance, manifest.policy);
905
+ const tmuxPaneOwnerId = typeof manifest.tmux_pane_owner_id === 'string' && manifest.tmux_pane_owner_id.trim() !== ''
906
+ ? manifest.tmux_pane_owner_id.trim()
907
+ : undefined;
908
+ const p = teamManifestV2Path(manifest.name, cwd);
909
+ await writeAtomic(p, JSON.stringify({
910
+ ...manifest,
911
+ tmux_pane_owner_id: tmuxPaneOwnerId,
912
+ policy: normalizedPolicy,
913
+ governance: normalizedGovernance,
914
+ lifecycle_profile: 'default',
915
+ }, null, 2));
582
916
  });
583
- const normalizedGovernance = normalizeTeamGovernance(manifest.governance, manifest.policy);
584
- const tmuxPaneOwnerId = typeof manifest.tmux_pane_owner_id === 'string' && manifest.tmux_pane_owner_id.trim() !== ''
585
- ? manifest.tmux_pane_owner_id.trim()
586
- : defaultTmuxPaneOwnerId(manifest.name);
587
- const p = teamManifestV2Path(manifest.name, cwd);
588
- await writeAtomic(p, JSON.stringify({
589
- ...manifest,
590
- tmux_pane_owner_id: tmuxPaneOwnerId,
591
- policy: normalizedPolicy,
592
- governance: normalizedGovernance,
593
- lifecycle_profile: 'default',
594
- }, null, 2));
595
917
  }
596
- export async function readTeamManifestV2(teamName, cwd) {
918
+ async function readTeamManifestV2Raw(teamName, cwd) {
597
919
  try {
598
920
  const p = teamManifestV2Path(teamName, cwd);
599
921
  if (!existsSync(p))
@@ -607,7 +929,7 @@ export async function readTeamManifestV2(teamName, cwd) {
607
929
  const legacyTeamDecomposition = legacyPolicy?.team_decomposition;
608
930
  const tmuxPaneOwnerId = typeof parsedManifest.tmux_pane_owner_id === 'string' && parsedManifest.tmux_pane_owner_id.trim() !== ''
609
931
  ? parsedManifest.tmux_pane_owner_id.trim()
610
- : defaultTmuxPaneOwnerId(parsedManifest.name);
932
+ : undefined;
611
933
  return {
612
934
  ...parsedManifest,
613
935
  tmux_pane_owner_id: tmuxPaneOwnerId,
@@ -627,6 +949,12 @@ export async function readTeamManifestV2(teamName, cwd) {
627
949
  return null;
628
950
  }
629
951
  }
952
+ export async function readTeamManifestV2(teamName, cwd) {
953
+ return await withTeamTaskBarrier(teamName, cwd, async () => {
954
+ await recoverTeamMembershipTaskTransaction(teamName, cwd);
955
+ return await readTeamManifestV2Raw(teamName, cwd);
956
+ });
957
+ }
630
958
  // Idempotent migration; keeps config.json untouched.
631
959
  export async function migrateV1ToV2(teamName, cwd) {
632
960
  const existing = await readTeamManifestV2(teamName, cwd);
@@ -683,11 +1011,23 @@ async function computeNextTaskIdFromDisk(teamName, cwd) {
683
1011
  }
684
1012
  return maxId + 1;
685
1013
  }
1014
+ async function readConfigGenerationRaw(teamName, cwd) {
1015
+ try {
1016
+ const raw = JSON.parse(await readFile(teamConfigPath(teamName, cwd), 'utf8'));
1017
+ return typeof raw.config_generation === 'number'
1018
+ && Number.isSafeInteger(raw.config_generation) && raw.config_generation >= 0
1019
+ ? raw.config_generation
1020
+ : 0;
1021
+ }
1022
+ catch {
1023
+ return null;
1024
+ }
1025
+ }
686
1026
  // Read team config
687
- export async function readTeamConfig(teamName, cwd) {
688
- const v2 = await readTeamManifestV2(teamName, cwd);
1027
+ async function readTeamConfigRaw(teamName, cwd) {
1028
+ const v2 = await readTeamManifestV2Raw(teamName, cwd);
689
1029
  if (v2)
690
- return teamConfigFromManifest(v2);
1030
+ return { ...teamConfigFromManifest(v2), config_generation: (await readConfigGenerationRaw(teamName, cwd)) ?? 0 };
691
1031
  // Attempt idempotent migration on first read.
692
1032
  const migrated = await migrateV1ToV2(teamName, cwd);
693
1033
  if (migrated)
@@ -706,6 +1046,12 @@ export async function readTeamConfig(teamName, cwd) {
706
1046
  return null;
707
1047
  }
708
1048
  }
1049
+ export async function readTeamConfig(teamName, cwd) {
1050
+ return await withTeamTaskBarrier(teamName, cwd, async () => {
1051
+ await recoverTeamMembershipTaskTransaction(teamName, cwd);
1052
+ return await readTeamConfigRaw(teamName, cwd);
1053
+ });
1054
+ }
709
1055
  // Write worker identity file
710
1056
  export async function writeWorkerIdentity(teamName, workerName, identity, cwd) {
711
1057
  const p = join(workerDir(teamName, workerName, cwd), 'identity.json');
@@ -753,7 +1099,8 @@ export async function writeWorkerStatus(teamName, workerName, status, cwd) {
753
1099
  const p = join(workerDir(teamName, workerName, cwd), 'status.json');
754
1100
  await writeAtomic(p, JSON.stringify(status, null, 2));
755
1101
  }
756
- // File-based scaling lock to prevent concurrent scale_up/scale_down operations
1102
+ // File-based scaling lock kept outside the deletable team root. Operations that
1103
+ // also mutate membership acquire this lock before the membership barrier.
757
1104
  export async function withScalingLock(teamName, cwd, fn) {
758
1105
  return await withScalingLockImpl(teamName, cwd, LOCK_STALE_MS, { teamDir, taskClaimLockDir, mailboxLockDir }, fn);
759
1106
  }
@@ -768,9 +1115,28 @@ function taskFilePath(teamName, taskId, cwd) {
768
1115
  assertPathWithinDir(p, resolveTeamStateRoot(cwd));
769
1116
  return p;
770
1117
  }
1118
+ const taskMembershipBarrierContext = new AsyncLocalStorage();
1119
+ function taskMembershipBarrierKey(teamName, cwd) {
1120
+ return `${resolve(cwd)}\0${teamName}`;
1121
+ }
771
1122
  async function withTeamLock(teamName, cwd, fn) {
772
1123
  return await withTeamLockImpl(teamName, cwd, LOCK_STALE_MS, { teamDir, taskClaimLockDir, mailboxLockDir }, fn);
773
1124
  }
1125
+ /**
1126
+ * Serializes membership snapshots with task creation and claims. The global
1127
+ * lock order is scaling, membership, then task claim locks. Operations that do
1128
+ * not scale acquire only this barrier (and then any task claim lock).
1129
+ */
1130
+ export async function withTeamTaskBarrier(teamName, cwd, fn) {
1131
+ const key = taskMembershipBarrierKey(teamName, cwd);
1132
+ if (taskMembershipBarrierContext.getStore()?.has(key))
1133
+ return await fn();
1134
+ return await withTeamLock(teamName, cwd, async () => {
1135
+ const held = new Set(taskMembershipBarrierContext.getStore() ?? []);
1136
+ held.add(key);
1137
+ return await taskMembershipBarrierContext.run(held, fn);
1138
+ });
1139
+ }
774
1140
  async function withTaskClaimLock(teamName, taskId, cwd, fn) {
775
1141
  return await withTaskClaimLockImpl(teamName, taskId, cwd, LOCK_STALE_MS, { teamDir, taskClaimLockDir, mailboxLockDir }, fn);
776
1142
  }
@@ -779,7 +1145,8 @@ async function withMailboxLock(teamName, workerName, cwd, fn) {
779
1145
  }
780
1146
  // Create a task (auto-increment ID)
781
1147
  export async function createTask(teamName, task, cwd) {
782
- return withTeamLock(teamName, cwd, async () => {
1148
+ return withTeamTaskBarrier(teamName, cwd, async () => {
1149
+ await recoverTeamMembershipTaskTransaction(teamName, cwd);
783
1150
  const cfg = await readTeamConfig(teamName, cwd);
784
1151
  if (!cfg)
785
1152
  throw new Error(`Team ${teamName} not found`);
@@ -808,7 +1175,7 @@ export async function createTask(teamName, task, cwd) {
808
1175
  });
809
1176
  }
810
1177
  // Read a task (returns null on missing/malformed)
811
- export async function readTask(teamName, taskId, cwd) {
1178
+ async function readTaskRaw(teamName, taskId, cwd) {
812
1179
  try {
813
1180
  const p = taskFilePath(teamName, taskId, cwd);
814
1181
  if (!existsSync(p))
@@ -821,98 +1188,121 @@ export async function readTask(teamName, taskId, cwd) {
821
1188
  return null;
822
1189
  }
823
1190
  }
1191
+ export async function readTask(teamName, taskId, cwd) {
1192
+ return await withTeamTaskBarrier(teamName, cwd, async () => {
1193
+ await recoverTeamMembershipTaskTransaction(teamName, cwd);
1194
+ return await readTaskRaw(teamName, taskId, cwd);
1195
+ });
1196
+ }
824
1197
  // Update a task (merge updates, atomic write)
825
1198
  export async function updateTask(teamName, taskId, updates, cwd) {
826
- const lock = await withTaskClaimLock(teamName, taskId, cwd, async () => {
827
- const existing = await readTask(teamName, taskId, cwd);
828
- if (!existing)
829
- return null;
830
- if (updates.status !== undefined && !['pending', 'blocked', 'in_progress', 'completed', 'failed'].includes(updates.status)) {
831
- throw new Error(`Invalid task status: ${updates.status}`);
832
- }
833
- const rawDeps = updates.depends_on ?? updates.blocked_by ?? existing.depends_on ?? existing.blocked_by ?? [];
834
- const normalizedDeps = Array.isArray(rawDeps) ? rawDeps : [];
835
- const merged = normalizeTask({
836
- ...normalizeTask(existing),
837
- ...updates,
838
- id: existing.id,
839
- created_at: existing.created_at,
840
- depends_on: normalizedDeps,
841
- version: Math.max(1, existing.version ?? 1) + 1,
1199
+ return await withTeamTaskBarrier(teamName, cwd, async () => {
1200
+ await recoverTeamMembershipTaskTransaction(teamName, cwd);
1201
+ const lock = await withTaskClaimLock(teamName, taskId, cwd, async () => {
1202
+ const existing = await readTaskRaw(teamName, taskId, cwd);
1203
+ if (!existing)
1204
+ return null;
1205
+ if (updates.status !== undefined && !['pending', 'blocked', 'in_progress', 'completed', 'failed'].includes(updates.status)) {
1206
+ throw new Error(`Invalid task status: ${updates.status}`);
1207
+ }
1208
+ const rawDeps = updates.depends_on ?? updates.blocked_by ?? existing.depends_on ?? existing.blocked_by ?? [];
1209
+ const normalizedDeps = Array.isArray(rawDeps) ? rawDeps : [];
1210
+ const merged = normalizeTask({
1211
+ ...normalizeTask(existing),
1212
+ ...updates,
1213
+ id: existing.id,
1214
+ created_at: existing.created_at,
1215
+ depends_on: normalizedDeps,
1216
+ version: Math.max(1, existing.version ?? 1) + 1,
1217
+ });
1218
+ await writeAtomic(taskFilePath(teamName, taskId, cwd), JSON.stringify(merged, null, 2));
1219
+ return merged;
842
1220
  });
843
- await writeAtomic(taskFilePath(teamName, taskId, cwd), JSON.stringify(merged, null, 2));
844
- return merged;
1221
+ if (!lock.ok)
1222
+ throw new Error(`Timed out acquiring task claim lock for ${teamName}/${taskId}`);
1223
+ return lock.value;
845
1224
  });
846
- if (!lock.ok) {
847
- throw new Error(`Timed out acquiring task claim lock for ${teamName}/${taskId}`);
848
- }
849
- return lock.value;
850
1225
  }
851
1226
  // List all tasks sorted by numeric ID
852
1227
  export async function listTasks(teamName, cwd) {
853
- return await listTasksImpl(teamName, cwd, {
854
- teamDir,
855
- isTeamTask,
856
- normalizeTask,
1228
+ return await withTeamTaskBarrier(teamName, cwd, async () => {
1229
+ await recoverTeamMembershipTaskTransaction(teamName, cwd);
1230
+ return await listTasksImpl(teamName, cwd, {
1231
+ teamDir,
1232
+ isTeamTask,
1233
+ normalizeTask,
1234
+ });
857
1235
  });
858
1236
  }
859
1237
  export async function computeTaskReadiness(teamName, taskId, cwd) {
860
1238
  return await computeTaskReadinessImpl(teamName, taskId, cwd, { readTask });
861
1239
  }
862
1240
  export async function claimTask(teamName, taskId, workerName, expectedVersion, cwd) {
863
- return await claimTaskImpl(taskId, workerName, expectedVersion, {
864
- teamName,
865
- cwd,
866
- readTask,
867
- readTeamConfig,
868
- withTaskClaimLock,
869
- normalizeTask,
870
- isTerminalTaskStatus,
871
- taskFilePath,
872
- writeAtomic,
1241
+ return await withTeamTaskBarrier(teamName, cwd, async () => {
1242
+ await recoverTeamMembershipTaskTransaction(teamName, cwd);
1243
+ return await claimTaskImpl(taskId, workerName, expectedVersion, {
1244
+ teamName,
1245
+ cwd,
1246
+ readTask,
1247
+ readTeamConfig,
1248
+ withTaskClaimLock,
1249
+ normalizeTask,
1250
+ isTerminalTaskStatus,
1251
+ taskFilePath,
1252
+ writeAtomic,
1253
+ });
873
1254
  });
874
1255
  }
875
1256
  export async function transitionTaskStatus(teamName, taskId, from, to, claimToken, cwd, terminalData) {
876
- return await transitionTaskStatusImpl(taskId, from, to, claimToken, terminalData, {
877
- teamName,
878
- cwd,
879
- readTask,
880
- readTeamConfig,
881
- withTaskClaimLock,
882
- normalizeTask,
883
- isTerminalTaskStatus,
884
- canTransitionTaskStatus,
885
- taskFilePath,
886
- writeAtomic,
887
- appendTeamEvent,
888
- readMonitorSnapshot,
889
- writeMonitorSnapshot,
1257
+ return await withTeamTaskBarrier(teamName, cwd, async () => {
1258
+ await recoverTeamMembershipTaskTransaction(teamName, cwd);
1259
+ return await transitionTaskStatusImpl(taskId, from, to, claimToken, terminalData, {
1260
+ teamName,
1261
+ cwd,
1262
+ readTask,
1263
+ readTeamConfig,
1264
+ withTaskClaimLock,
1265
+ normalizeTask,
1266
+ isTerminalTaskStatus,
1267
+ canTransitionTaskStatus,
1268
+ taskFilePath,
1269
+ writeAtomic,
1270
+ appendTeamEvent,
1271
+ readMonitorSnapshot,
1272
+ writeMonitorSnapshot,
1273
+ });
890
1274
  });
891
1275
  }
892
1276
  export async function releaseTaskClaim(teamName, taskId, claimToken, workerName, cwd) {
893
- return await releaseTaskClaimImpl(taskId, claimToken, workerName, {
894
- teamName,
895
- cwd,
896
- readTask,
897
- readTeamConfig,
898
- withTaskClaimLock,
899
- normalizeTask,
900
- isTerminalTaskStatus,
901
- taskFilePath,
902
- writeAtomic,
1277
+ return await withTeamTaskBarrier(teamName, cwd, async () => {
1278
+ await recoverTeamMembershipTaskTransaction(teamName, cwd);
1279
+ return await releaseTaskClaimImpl(taskId, claimToken, workerName, {
1280
+ teamName,
1281
+ cwd,
1282
+ readTask,
1283
+ readTeamConfig,
1284
+ withTaskClaimLock,
1285
+ normalizeTask,
1286
+ isTerminalTaskStatus,
1287
+ taskFilePath,
1288
+ writeAtomic,
1289
+ });
903
1290
  });
904
1291
  }
905
1292
  export async function reclaimExpiredTaskClaim(teamName, taskId, cwd) {
906
- return await reclaimExpiredTaskClaimImpl(taskId, {
907
- teamName,
908
- cwd,
909
- readTask,
910
- readTeamConfig,
911
- withTaskClaimLock,
912
- normalizeTask,
913
- isTerminalTaskStatus,
914
- taskFilePath,
915
- writeAtomic,
1293
+ return await withTeamTaskBarrier(teamName, cwd, async () => {
1294
+ await recoverTeamMembershipTaskTransaction(teamName, cwd);
1295
+ return await reclaimExpiredTaskClaimImpl(taskId, {
1296
+ teamName,
1297
+ cwd,
1298
+ readTask,
1299
+ readTeamConfig,
1300
+ withTaskClaimLock,
1301
+ normalizeTask,
1302
+ isTerminalTaskStatus,
1303
+ taskFilePath,
1304
+ writeAtomic,
1305
+ });
916
1306
  });
917
1307
  }
918
1308
  export async function appendTeamEvent(teamName, event, cwd) {
@@ -1012,6 +1402,48 @@ async function readDispatchRequests(teamName, cwd) {
1012
1402
  return [];
1013
1403
  }
1014
1404
  }
1405
+ /** Read only strictly recognized target-team legacy request records for rollback scoping. */
1406
+ function isStrictLegacyDispatchRequestForRollback(value, teamName) {
1407
+ if (!value || typeof value !== 'object' || Array.isArray(value))
1408
+ return false;
1409
+ const request = value;
1410
+ const requiredFields = [
1411
+ 'request_id', 'kind', 'team_name', 'to_worker', 'trigger_message',
1412
+ 'transport_preference', 'fallback_allowed', 'status', 'attempt_count',
1413
+ 'created_at', 'updated_at',
1414
+ ];
1415
+ if (requiredFields.some((field) => !Object.prototype.hasOwnProperty.call(request, field)))
1416
+ return false;
1417
+ return typeof request.request_id === 'string'
1418
+ && request.request_id.length > 0
1419
+ && ['inbox', 'mailbox', 'nudge'].includes(String(request.kind))
1420
+ && request.team_name === teamName
1421
+ && typeof request.to_worker === 'string'
1422
+ && request.to_worker.length > 0
1423
+ && typeof request.trigger_message === 'string'
1424
+ && ['hook_preferred_with_fallback', 'transport_direct', 'prompt_stdin'].includes(String(request.transport_preference))
1425
+ && typeof request.fallback_allowed === 'boolean'
1426
+ && ['pending', 'notified', 'delivered', 'failed'].includes(String(request.status))
1427
+ && typeof request.attempt_count === 'number'
1428
+ && Number.isFinite(request.attempt_count)
1429
+ && typeof request.created_at === 'string'
1430
+ && typeof request.updated_at === 'string';
1431
+ }
1432
+ async function readLegacyDispatchRequestsForRollback(teamName, cwd) {
1433
+ const path = dispatchRequestsPath(teamName, cwd);
1434
+ try {
1435
+ const raw = JSON.parse(await readFile(path, 'utf8'));
1436
+ if (!Array.isArray(raw) || raw.some((entry) => !isStrictLegacyDispatchRequestForRollback(entry, teamName))) {
1437
+ throw new Error('legacy dispatch rollback evidence is malformed or unrecognized');
1438
+ }
1439
+ return raw;
1440
+ }
1441
+ catch (error) {
1442
+ if (error.code === 'ENOENT')
1443
+ return [];
1444
+ throw error;
1445
+ }
1446
+ }
1015
1447
  async function writeDispatchRequests(teamName, requests, cwd) {
1016
1448
  await writeAtomic(dispatchRequestsPath(teamName, cwd), JSON.stringify(requests, null, 2));
1017
1449
  await writeBridgeDispatchCompat(teamName, requests, cwd);
@@ -1065,6 +1497,61 @@ export function resolveDispatchLockTimeoutMs(env = process.env) {
1065
1497
  async function withDispatchLock(teamName, cwd, fn) {
1066
1498
  return await withDispatchLockImpl(teamName, cwd, teamDir, dispatchLockDir, fn);
1067
1499
  }
1500
+ /** Remove worker-targeted dispatch requests under the authoritative dispatch lock. */
1501
+ export async function removeDispatchRequestsForWorkers(teamName, workerNames, cwd) {
1502
+ const names = new Set(workerNames);
1503
+ await withDispatchLock(teamName, cwd, async () => {
1504
+ // Do not derive rollback IDs from bridge-normalized root records: unscoped
1505
+ // and other-team records can share a target worker name. This team's legacy
1506
+ // file is the canonical rollback scope.
1507
+ const requests = await readLegacyDispatchRequestsForRollback(teamName, cwd);
1508
+ const legacyRequestIds = requests
1509
+ .filter((request) => names.has(request.to_worker))
1510
+ .map((request) => request.request_id);
1511
+ let removedRequestIds = legacyRequestIds;
1512
+ if (isBridgeEnabled()) {
1513
+ const stateDir = resolveBridgeStateDir(cwd);
1514
+ const bridge = getDefaultBridge(stateDir);
1515
+ const snapshot = bridge.execCommand({ command: 'CaptureSnapshot' });
1516
+ if (snapshot.event !== 'SnapshotCaptured') {
1517
+ throw new Error(`authoritative_dispatch_rollback_discovery_failed:${[...names].join(',')}`);
1518
+ }
1519
+ const scopedAuthoritativeIds = bridge.readDispatchRecordsStrict()
1520
+ .filter((record) => {
1521
+ const metadataTeam = typeof record.metadata?.team_name === 'string' ? record.metadata.team_name : '';
1522
+ return metadataTeam === teamName && names.has(record.target);
1523
+ })
1524
+ .map((record) => record.request_id);
1525
+ removedRequestIds = [...new Set([...legacyRequestIds, ...scopedAuthoritativeIds])];
1526
+ if (removedRequestIds.length === 0) {
1527
+ await writeAtomic(dispatchRequestsPath(teamName, cwd), JSON.stringify(requests, null, 2));
1528
+ return;
1529
+ }
1530
+ const removal = bridge.removeDispatchRecords(removedRequestIds);
1531
+ if (removal.event !== 'DispatchRecordsRemoved'
1532
+ || removedRequestIds.some((requestId) => !removal.request_ids.includes(requestId))) {
1533
+ throw new Error(`authoritative_dispatch_rollback_command_failed:${[...names].join(',')}`);
1534
+ }
1535
+ // Force a separate runtime command after mutation. The removal event is
1536
+ // the authoritative acknowledgement; TS must not inspect or rewrite the
1537
+ // shared root dispatch.json as a substitute for runtime verification.
1538
+ const verificationSnapshot = bridge.execCommand({ command: 'CaptureSnapshot' });
1539
+ if (verificationSnapshot.event !== 'SnapshotCaptured') {
1540
+ throw new Error(`authoritative_dispatch_rollback_verification_failed:${[...names].join(',')}`);
1541
+ }
1542
+ const remainingRemovedRecord = bridge.readDispatchRecordsStrict()
1543
+ .some((record) => removedRequestIds.includes(record.request_id));
1544
+ if (remainingRemovedRecord) {
1545
+ throw new Error(`authoritative_dispatch_rollback_verification_failed:${[...names].join(',')}`);
1546
+ }
1547
+ }
1548
+ // The legacy per-team request file is compatibility output only. Update it
1549
+ // only after the authoritative runtime confirms removal; never regenerate
1550
+ // the shared bridge dispatch.json from a local snapshot during rollback.
1551
+ const retained = requests.filter((request) => !names.has(request.to_worker));
1552
+ await writeAtomic(dispatchRequestsPath(teamName, cwd), JSON.stringify(retained, null, 2));
1553
+ });
1554
+ }
1068
1555
  export async function enqueueDispatchRequest(teamName, requestInput, cwd) {
1069
1556
  return await enqueueDispatchRequestImpl(requestInput, {
1070
1557
  teamName,
@@ -1429,10 +1916,30 @@ export async function markOwnedTeamsLeaderStopObserved(cwd, leaderSessionId, now
1429
1916
  }
1430
1917
  // === Config persistence (public wrapper) ===
1431
1918
  export async function saveTeamConfig(config, cwd) {
1432
- await writeConfig(config, cwd);
1919
+ await withTeamTaskBarrier(config.name, cwd, async () => {
1920
+ // The caller's config is a full canonical replacement; recover any
1921
+ // interrupted membership generation while holding the same barrier before
1922
+ // deriving and writing its manifest companion.
1923
+ await recoverTeamMembershipTaskTransaction(config.name, cwd);
1924
+ const currentGeneration = await readConfigGenerationRaw(config.name, cwd);
1925
+ if (currentGeneration === null)
1926
+ throw new Error(`team_config_missing:${config.name}`);
1927
+ const expectedGeneration = normalizeTeamConfig(config).config_generation;
1928
+ if (expectedGeneration !== currentGeneration) {
1929
+ throw new Error(`team_config_stale_generation:${config.name}:${expectedGeneration}:${currentGeneration}`);
1930
+ }
1931
+ config.config_generation = currentGeneration + 1;
1932
+ await writeConfig(config, cwd);
1933
+ });
1433
1934
  }
1434
- // Delete team state directory
1935
+ // Delete team state only after excluding both scaling and membership mutations.
1936
+ // The scaling lock lives outside the deletable team root, so waiters cannot
1937
+ // recreate a deleted team merely by acquiring their lock.
1435
1938
  export async function cleanupTeamState(teamName, cwd) {
1436
- await rm(teamDir(teamName, cwd), { recursive: true, force: true });
1939
+ await withScalingLock(teamName, cwd, async () => {
1940
+ await withTeamTaskBarrier(teamName, cwd, async () => {
1941
+ await rm(teamDir(teamName, cwd), { recursive: true, force: true });
1942
+ });
1943
+ });
1437
1944
  }
1438
1945
  //# sourceMappingURL=state.js.map