oh-my-codex 0.20.2 → 0.20.5

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 (805) hide show
  1. package/Cargo.lock +36 -18
  2. package/Cargo.toml +1 -1
  3. package/README.md +28 -2
  4. package/crates/omx-runtime/Cargo.toml +4 -0
  5. package/crates/omx-runtime/src/main.rs +415 -2
  6. package/crates/omx-runtime/tests/execution.rs +483 -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/adapt/__tests__/herdr-target.test.d.ts +2 -0
  11. package/dist/adapt/__tests__/herdr-target.test.d.ts.map +1 -0
  12. package/dist/adapt/__tests__/herdr-target.test.js +72 -0
  13. package/dist/adapt/__tests__/herdr-target.test.js.map +1 -0
  14. package/dist/adapt/contracts.d.ts +1 -1
  15. package/dist/adapt/contracts.d.ts.map +1 -1
  16. package/dist/adapt/contracts.js +1 -1
  17. package/dist/adapt/contracts.js.map +1 -1
  18. package/dist/adapt/herdr/__tests__/authority.test.d.ts +2 -0
  19. package/dist/adapt/herdr/__tests__/authority.test.d.ts.map +1 -0
  20. package/dist/adapt/herdr/__tests__/authority.test.js +49 -0
  21. package/dist/adapt/herdr/__tests__/authority.test.js.map +1 -0
  22. package/dist/adapt/herdr/__tests__/bridge.test.d.ts +2 -0
  23. package/dist/adapt/herdr/__tests__/bridge.test.d.ts.map +1 -0
  24. package/dist/adapt/herdr/__tests__/bridge.test.js +176 -0
  25. package/dist/adapt/herdr/__tests__/bridge.test.js.map +1 -0
  26. package/dist/adapt/herdr/__tests__/sanitize.test.d.ts +2 -0
  27. package/dist/adapt/herdr/__tests__/sanitize.test.d.ts.map +1 -0
  28. package/dist/adapt/herdr/__tests__/sanitize.test.js +45 -0
  29. package/dist/adapt/herdr/__tests__/sanitize.test.js.map +1 -0
  30. package/dist/adapt/herdr/__tests__/semantic.test.d.ts +2 -0
  31. package/dist/adapt/herdr/__tests__/semantic.test.d.ts.map +1 -0
  32. package/dist/adapt/herdr/__tests__/semantic.test.js +130 -0
  33. package/dist/adapt/herdr/__tests__/semantic.test.js.map +1 -0
  34. package/dist/adapt/herdr/__tests__/seq-store.test.d.ts +2 -0
  35. package/dist/adapt/herdr/__tests__/seq-store.test.d.ts.map +1 -0
  36. package/dist/adapt/herdr/__tests__/seq-store.test.js +54 -0
  37. package/dist/adapt/herdr/__tests__/seq-store.test.js.map +1 -0
  38. package/dist/adapt/herdr/__tests__/socket-live.test.d.ts +2 -0
  39. package/dist/adapt/herdr/__tests__/socket-live.test.d.ts.map +1 -0
  40. package/dist/adapt/herdr/__tests__/socket-live.test.js +104 -0
  41. package/dist/adapt/herdr/__tests__/socket-live.test.js.map +1 -0
  42. package/dist/adapt/herdr/__tests__/team-map.test.d.ts +2 -0
  43. package/dist/adapt/herdr/__tests__/team-map.test.d.ts.map +1 -0
  44. package/dist/adapt/herdr/__tests__/team-map.test.js +46 -0
  45. package/dist/adapt/herdr/__tests__/team-map.test.js.map +1 -0
  46. package/dist/adapt/herdr/__tests__/transport.test.d.ts +2 -0
  47. package/dist/adapt/herdr/__tests__/transport.test.d.ts.map +1 -0
  48. package/dist/adapt/herdr/__tests__/transport.test.js +160 -0
  49. package/dist/adapt/herdr/__tests__/transport.test.js.map +1 -0
  50. package/dist/adapt/herdr/__tests__/wiring.test.d.ts +2 -0
  51. package/dist/adapt/herdr/__tests__/wiring.test.d.ts.map +1 -0
  52. package/dist/adapt/herdr/__tests__/wiring.test.js +118 -0
  53. package/dist/adapt/herdr/__tests__/wiring.test.js.map +1 -0
  54. package/dist/adapt/herdr/authority.d.ts +34 -0
  55. package/dist/adapt/herdr/authority.d.ts.map +1 -0
  56. package/dist/adapt/herdr/authority.js +71 -0
  57. package/dist/adapt/herdr/authority.js.map +1 -0
  58. package/dist/adapt/herdr/bridge.d.ts +84 -0
  59. package/dist/adapt/herdr/bridge.d.ts.map +1 -0
  60. package/dist/adapt/herdr/bridge.js +222 -0
  61. package/dist/adapt/herdr/bridge.js.map +1 -0
  62. package/dist/adapt/herdr/sanitize.d.ts +20 -0
  63. package/dist/adapt/herdr/sanitize.d.ts.map +1 -0
  64. package/dist/adapt/herdr/sanitize.js +70 -0
  65. package/dist/adapt/herdr/sanitize.js.map +1 -0
  66. package/dist/adapt/herdr/semantic.d.ts +67 -0
  67. package/dist/adapt/herdr/semantic.d.ts.map +1 -0
  68. package/dist/adapt/herdr/semantic.js +170 -0
  69. package/dist/adapt/herdr/semantic.js.map +1 -0
  70. package/dist/adapt/herdr/seq-store.d.ts +32 -0
  71. package/dist/adapt/herdr/seq-store.d.ts.map +1 -0
  72. package/dist/adapt/herdr/seq-store.js +126 -0
  73. package/dist/adapt/herdr/seq-store.js.map +1 -0
  74. package/dist/adapt/herdr/team-map.d.ts +30 -0
  75. package/dist/adapt/herdr/team-map.d.ts.map +1 -0
  76. package/dist/adapt/herdr/team-map.js +37 -0
  77. package/dist/adapt/herdr/team-map.js.map +1 -0
  78. package/dist/adapt/herdr/transport.d.ts +115 -0
  79. package/dist/adapt/herdr/transport.d.ts.map +1 -0
  80. package/dist/adapt/herdr/transport.js +260 -0
  81. package/dist/adapt/herdr/transport.js.map +1 -0
  82. package/dist/adapt/herdr/wiring.d.ts +24 -0
  83. package/dist/adapt/herdr/wiring.d.ts.map +1 -0
  84. package/dist/adapt/herdr/wiring.js +62 -0
  85. package/dist/adapt/herdr/wiring.js.map +1 -0
  86. package/dist/adapt/herdr.d.ts +19 -0
  87. package/dist/adapt/herdr.d.ts.map +1 -0
  88. package/dist/adapt/herdr.js +115 -0
  89. package/dist/adapt/herdr.js.map +1 -0
  90. package/dist/adapt/index.d.ts +4 -0
  91. package/dist/adapt/index.d.ts.map +1 -1
  92. package/dist/adapt/index.js +48 -0
  93. package/dist/adapt/index.js.map +1 -1
  94. package/dist/adapt/registry.d.ts.map +1 -1
  95. package/dist/adapt/registry.js +11 -0
  96. package/dist/adapt/registry.js.map +1 -1
  97. package/dist/agents/__tests__/definitions.test.js +81 -0
  98. package/dist/agents/__tests__/definitions.test.js.map +1 -1
  99. package/dist/agents/__tests__/native-config.test.js +25 -0
  100. package/dist/agents/__tests__/native-config.test.js.map +1 -1
  101. package/dist/agents/native-config.d.ts +2 -1
  102. package/dist/agents/native-config.d.ts.map +1 -1
  103. package/dist/agents/native-config.js.map +1 -1
  104. package/dist/auth/__tests__/hotswap.test.js +60 -1
  105. package/dist/auth/__tests__/hotswap.test.js.map +1 -1
  106. package/dist/auth/__tests__/storage.test.js +33 -1
  107. package/dist/auth/__tests__/storage.test.js.map +1 -1
  108. package/dist/auth/hotswap.d.ts +22 -2
  109. package/dist/auth/hotswap.d.ts.map +1 -1
  110. package/dist/auth/hotswap.js +107 -22
  111. package/dist/auth/hotswap.js.map +1 -1
  112. package/dist/auth/storage.js +1 -1
  113. package/dist/auth/storage.js.map +1 -1
  114. package/dist/autopilot/__tests__/ralplan-gate.test.js +86 -1333
  115. package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
  116. package/dist/autopilot/ralplan-gate.d.ts.map +1 -1
  117. package/dist/autopilot/ralplan-gate.js +7 -6
  118. package/dist/autopilot/ralplan-gate.js.map +1 -1
  119. package/dist/cli/__tests__/agents-init.test.js +4 -23
  120. package/dist/cli/__tests__/agents-init.test.js.map +1 -1
  121. package/dist/cli/__tests__/api.test.js +61 -24
  122. package/dist/cli/__tests__/api.test.js.map +1 -1
  123. package/dist/cli/__tests__/auth.test.js +63 -7
  124. package/dist/cli/__tests__/auth.test.js.map +1 -1
  125. package/dist/cli/__tests__/codex-feature-probe.test.js +66 -1
  126. package/dist/cli/__tests__/codex-feature-probe.test.js.map +1 -1
  127. package/dist/cli/__tests__/codex-plugin-layout.test.js +150 -2
  128. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  129. package/dist/cli/__tests__/detached-leader-teardown.test.d.ts +2 -0
  130. package/dist/cli/__tests__/detached-leader-teardown.test.d.ts.map +1 -0
  131. package/dist/cli/__tests__/detached-leader-teardown.test.js +473 -0
  132. package/dist/cli/__tests__/detached-leader-teardown.test.js.map +1 -0
  133. package/dist/cli/__tests__/doctor-process-identity-readiness.test.d.ts +2 -0
  134. package/dist/cli/__tests__/doctor-process-identity-readiness.test.d.ts.map +1 -0
  135. package/dist/cli/__tests__/doctor-process-identity-readiness.test.js +103 -0
  136. package/dist/cli/__tests__/doctor-process-identity-readiness.test.js.map +1 -0
  137. package/dist/cli/__tests__/doctor-state-root-binding.test.d.ts +2 -0
  138. package/dist/cli/__tests__/doctor-state-root-binding.test.d.ts.map +1 -0
  139. package/dist/cli/__tests__/doctor-state-root-binding.test.js +139 -0
  140. package/dist/cli/__tests__/doctor-state-root-binding.test.js.map +1 -0
  141. package/dist/cli/__tests__/doctor-warning-copy.test.js +135 -1
  142. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  143. package/dist/cli/__tests__/exec.test.js +211 -0
  144. package/dist/cli/__tests__/exec.test.js.map +1 -1
  145. package/dist/cli/__tests__/index.test.js +878 -463
  146. package/dist/cli/__tests__/index.test.js.map +1 -1
  147. package/dist/cli/__tests__/launch-fallback.test.js +1290 -127
  148. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  149. package/dist/cli/__tests__/mcp-parity.test.js +8 -4
  150. package/dist/cli/__tests__/mcp-parity.test.js.map +1 -1
  151. package/dist/cli/__tests__/native-assets.test.js +398 -16
  152. package/dist/cli/__tests__/native-assets.test.js.map +1 -1
  153. package/dist/cli/__tests__/native-hook-claim-journal.test.js +132 -6
  154. package/dist/cli/__tests__/native-hook-claim-journal.test.js.map +1 -1
  155. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +3 -3
  156. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
  157. package/dist/cli/__tests__/ralph.test.js +3 -3
  158. package/dist/cli/__tests__/ralph.test.js.map +1 -1
  159. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js +49 -164
  160. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js.map +1 -1
  161. package/dist/cli/__tests__/ralplan-child-binding-3212.test.d.ts +2 -0
  162. package/dist/cli/__tests__/ralplan-child-binding-3212.test.d.ts.map +1 -0
  163. package/dist/cli/__tests__/ralplan-child-binding-3212.test.js +56 -0
  164. package/dist/cli/__tests__/ralplan-child-binding-3212.test.js.map +1 -0
  165. package/dist/cli/__tests__/ralplan.test.js +107 -109
  166. package/dist/cli/__tests__/ralplan.test.js.map +1 -1
  167. package/dist/cli/__tests__/resume.test.js +11 -10
  168. package/dist/cli/__tests__/resume.test.js.map +1 -1
  169. package/dist/cli/__tests__/session-scoped-runtime.test.js +1118 -43
  170. package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
  171. package/dist/cli/__tests__/session-search-help.test.js +25 -1
  172. package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
  173. package/dist/cli/__tests__/setup-agents-overwrite.test.js +8 -55
  174. package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
  175. package/dist/cli/__tests__/setup-install-mode.test.js +224 -7
  176. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  177. package/dist/cli/__tests__/sparkshell-cli.test.js +93 -41
  178. package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
  179. package/dist/cli/__tests__/team.test.js +150 -8
  180. package/dist/cli/__tests__/team.test.js.map +1 -1
  181. package/dist/cli/__tests__/ultragoal.test.js +117 -2
  182. package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
  183. package/dist/cli/__tests__/uninstall.test.js +95 -4
  184. package/dist/cli/__tests__/uninstall.test.js.map +1 -1
  185. package/dist/cli/__tests__/update.test.js +669 -177
  186. package/dist/cli/__tests__/update.test.js.map +1 -1
  187. package/dist/cli/__tests__/windows-popup-loop-contract.test.js +33 -2
  188. package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
  189. package/dist/cli/api.d.ts.map +1 -1
  190. package/dist/cli/api.js +9 -10
  191. package/dist/cli/api.js.map +1 -1
  192. package/dist/cli/codex-feature-probe.d.ts +16 -0
  193. package/dist/cli/codex-feature-probe.d.ts.map +1 -1
  194. package/dist/cli/codex-feature-probe.js +44 -0
  195. package/dist/cli/codex-feature-probe.js.map +1 -1
  196. package/dist/cli/codex-home.d.ts.map +1 -1
  197. package/dist/cli/codex-home.js +7 -4
  198. package/dist/cli/codex-home.js.map +1 -1
  199. package/dist/cli/doctor.d.ts +20 -0
  200. package/dist/cli/doctor.d.ts.map +1 -1
  201. package/dist/cli/doctor.js +290 -3
  202. package/dist/cli/doctor.js.map +1 -1
  203. package/dist/cli/index.d.ts +234 -12
  204. package/dist/cli/index.d.ts.map +1 -1
  205. package/dist/cli/index.js +2409 -746
  206. package/dist/cli/index.js.map +1 -1
  207. package/dist/cli/mcp-parity.js +2 -2
  208. package/dist/cli/mcp-parity.js.map +1 -1
  209. package/dist/cli/native-assets.d.ts +25 -0
  210. package/dist/cli/native-assets.d.ts.map +1 -1
  211. package/dist/cli/native-assets.js +448 -51
  212. package/dist/cli/native-assets.js.map +1 -1
  213. package/dist/cli/native-hook-claim-journal.d.ts +20 -5
  214. package/dist/cli/native-hook-claim-journal.d.ts.map +1 -1
  215. package/dist/cli/native-hook-claim-journal.js +67 -32
  216. package/dist/cli/native-hook-claim-journal.js.map +1 -1
  217. package/dist/cli/package-manager-ownership.d.ts +63 -0
  218. package/dist/cli/package-manager-ownership.d.ts.map +1 -0
  219. package/dist/cli/package-manager-ownership.js +323 -0
  220. package/dist/cli/package-manager-ownership.js.map +1 -0
  221. package/dist/cli/ralph.js +2 -2
  222. package/dist/cli/ralph.js.map +1 -1
  223. package/dist/cli/ralplan.d.ts +5 -7
  224. package/dist/cli/ralplan.d.ts.map +1 -1
  225. package/dist/cli/ralplan.js +83 -134
  226. package/dist/cli/ralplan.js.map +1 -1
  227. package/dist/cli/session-search.d.ts +5 -0
  228. package/dist/cli/session-search.d.ts.map +1 -1
  229. package/dist/cli/session-search.js +87 -0
  230. package/dist/cli/session-search.js.map +1 -1
  231. package/dist/cli/setup-preferences.d.ts +15 -0
  232. package/dist/cli/setup-preferences.d.ts.map +1 -1
  233. package/dist/cli/setup-preferences.js +27 -1
  234. package/dist/cli/setup-preferences.js.map +1 -1
  235. package/dist/cli/setup.d.ts +8 -0
  236. package/dist/cli/setup.d.ts.map +1 -1
  237. package/dist/cli/setup.js +119 -47
  238. package/dist/cli/setup.js.map +1 -1
  239. package/dist/cli/sparkshell.d.ts.map +1 -1
  240. package/dist/cli/sparkshell.js +52 -9
  241. package/dist/cli/sparkshell.js.map +1 -1
  242. package/dist/cli/ultragoal.d.ts.map +1 -1
  243. package/dist/cli/ultragoal.js +2 -2
  244. package/dist/cli/ultragoal.js.map +1 -1
  245. package/dist/cli/uninstall.d.ts +9 -0
  246. package/dist/cli/uninstall.d.ts.map +1 -1
  247. package/dist/cli/uninstall.js +71 -30
  248. package/dist/cli/uninstall.js.map +1 -1
  249. package/dist/cli/update-worker.d.ts +2 -0
  250. package/dist/cli/update-worker.d.ts.map +1 -0
  251. package/dist/cli/update-worker.js +142 -0
  252. package/dist/cli/update-worker.js.map +1 -0
  253. package/dist/cli/update.d.ts +14 -22
  254. package/dist/cli/update.d.ts.map +1 -1
  255. package/dist/cli/update.js +192 -241
  256. package/dist/cli/update.js.map +1 -1
  257. package/dist/compat/__tests__/rust-runtime-compat.test.js +1 -1
  258. package/dist/config/__tests__/generator-idempotent.test.js +503 -30
  259. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  260. package/dist/config/__tests__/generator-notify.test.js +5 -3
  261. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  262. package/dist/config/__tests__/models.test.js +67 -3
  263. package/dist/config/__tests__/models.test.js.map +1 -1
  264. package/dist/config/generator.d.ts +27 -21
  265. package/dist/config/generator.d.ts.map +1 -1
  266. package/dist/config/generator.js +610 -152
  267. package/dist/config/generator.js.map +1 -1
  268. package/dist/config/models.d.ts +12 -2
  269. package/dist/config/models.d.ts.map +1 -1
  270. package/dist/config/models.js +15 -1
  271. package/dist/config/models.js.map +1 -1
  272. package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js +25 -0
  273. package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js.map +1 -1
  274. package/dist/goal-workflows/codex-goal-snapshot.d.ts +1 -1
  275. package/dist/goal-workflows/codex-goal-snapshot.d.ts.map +1 -1
  276. package/dist/goal-workflows/codex-goal-snapshot.js +2 -0
  277. package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -1
  278. package/dist/hooks/__tests__/agents-overlay.test.js +5 -3
  279. package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
  280. package/dist/hooks/__tests__/autopilot-skill-contract.test.js +27 -4
  281. package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
  282. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.d.ts +2 -0
  283. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.d.ts.map +1 -0
  284. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.js +172 -0
  285. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.js.map +1 -0
  286. package/dist/hooks/__tests__/keyword-detector.test.js +176 -37
  287. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  288. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +402 -545
  289. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  290. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +39 -4
  291. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
  292. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +161 -26
  293. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
  294. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +27 -0
  295. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js.map +1 -1
  296. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js +10 -118
  297. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js.map +1 -1
  298. package/dist/hooks/__tests__/notify-hook-regression-205.test.js +7 -0
  299. package/dist/hooks/__tests__/notify-hook-regression-205.test.js.map +1 -1
  300. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +533 -14
  301. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
  302. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +159 -24
  303. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  304. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +482 -32
  305. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
  306. package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js +2 -1
  307. package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js.map +1 -1
  308. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +68 -1
  309. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
  310. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js +7 -1
  311. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +1 -1
  312. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +44 -4
  313. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
  314. package/dist/hooks/__tests__/prompt-guidance-contract.test.js +9 -0
  315. package/dist/hooks/__tests__/prompt-guidance-contract.test.js.map +1 -1
  316. package/dist/hooks/__tests__/session.test.js +3130 -365
  317. package/dist/hooks/__tests__/session.test.js.map +1 -1
  318. package/dist/hooks/agents-overlay.js +1 -1
  319. package/dist/hooks/agents-overlay.js.map +1 -1
  320. package/dist/hooks/extensibility/__tests__/dispatcher.test.js +108 -2
  321. package/dist/hooks/extensibility/__tests__/dispatcher.test.js.map +1 -1
  322. package/dist/hooks/extensibility/__tests__/sdk.test.js +151 -1
  323. package/dist/hooks/extensibility/__tests__/sdk.test.js.map +1 -1
  324. package/dist/hooks/extensibility/dispatcher.d.ts.map +1 -1
  325. package/dist/hooks/extensibility/dispatcher.js +20 -3
  326. package/dist/hooks/extensibility/dispatcher.js.map +1 -1
  327. package/dist/hooks/extensibility/plugin-runner.js +1 -0
  328. package/dist/hooks/extensibility/plugin-runner.js.map +1 -1
  329. package/dist/hooks/extensibility/runtime.d.ts.map +1 -1
  330. package/dist/hooks/extensibility/runtime.js.map +1 -1
  331. package/dist/hooks/extensibility/sdk/paths.d.ts +1 -1
  332. package/dist/hooks/extensibility/sdk/paths.d.ts.map +1 -1
  333. package/dist/hooks/extensibility/sdk/paths.js +2 -2
  334. package/dist/hooks/extensibility/sdk/paths.js.map +1 -1
  335. package/dist/hooks/extensibility/sdk/runtime-state.d.ts +1 -1
  336. package/dist/hooks/extensibility/sdk/runtime-state.d.ts.map +1 -1
  337. package/dist/hooks/extensibility/sdk/runtime-state.js +55 -15
  338. package/dist/hooks/extensibility/sdk/runtime-state.js.map +1 -1
  339. package/dist/hooks/extensibility/sdk.d.ts +1 -0
  340. package/dist/hooks/extensibility/sdk.d.ts.map +1 -1
  341. package/dist/hooks/extensibility/sdk.js +1 -1
  342. package/dist/hooks/extensibility/sdk.js.map +1 -1
  343. package/dist/hooks/extensibility/types.d.ts +7 -0
  344. package/dist/hooks/extensibility/types.d.ts.map +1 -1
  345. package/dist/hooks/keyword-detector.d.ts +10 -3
  346. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  347. package/dist/hooks/keyword-detector.js +116 -20
  348. package/dist/hooks/keyword-detector.js.map +1 -1
  349. package/dist/hooks/session.d.ts +204 -14
  350. package/dist/hooks/session.d.ts.map +1 -1
  351. package/dist/hooks/session.js +1500 -223
  352. package/dist/hooks/session.js.map +1 -1
  353. package/dist/hud/__tests__/hud-tmux-injection.test.js +6 -6
  354. package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
  355. package/dist/hud/__tests__/index.test.js +26 -16
  356. package/dist/hud/__tests__/index.test.js.map +1 -1
  357. package/dist/hud/__tests__/reconcile.test.js +83 -78
  358. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  359. package/dist/hud/__tests__/state.test.js +36 -31
  360. package/dist/hud/__tests__/state.test.js.map +1 -1
  361. package/dist/hud/__tests__/tmux.test.js +155 -198
  362. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  363. package/dist/hud/tmux.d.ts +42 -4
  364. package/dist/hud/tmux.d.ts.map +1 -1
  365. package/dist/hud/tmux.js +1017 -139
  366. package/dist/hud/tmux.js.map +1 -1
  367. package/dist/leader/__tests__/contract.test.js +124 -37
  368. package/dist/leader/__tests__/contract.test.js.map +1 -1
  369. package/dist/leader/contract.d.ts +17 -6
  370. package/dist/leader/contract.d.ts.map +1 -1
  371. package/dist/leader/contract.js +102 -26
  372. package/dist/leader/contract.js.map +1 -1
  373. package/dist/mcp/__tests__/code-intel-server.test.js +31 -3
  374. package/dist/mcp/__tests__/code-intel-server.test.js.map +1 -1
  375. package/dist/mcp/__tests__/hermes-bridge.test.js +17 -0
  376. package/dist/mcp/__tests__/hermes-bridge.test.js.map +1 -1
  377. package/dist/mcp/__tests__/server-lifecycle.test.js +4 -4
  378. package/dist/mcp/__tests__/state-paths.test.js +735 -4
  379. package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
  380. package/dist/mcp/__tests__/state-server.test.js +1 -1
  381. package/dist/mcp/__tests__/state-server.test.js.map +1 -1
  382. package/dist/mcp/__tests__/trace-server.test.js +2 -2
  383. package/dist/mcp/__tests__/trace-server.test.js.map +1 -1
  384. package/dist/mcp/__tests__/wiki-server.test.js +18 -0
  385. package/dist/mcp/__tests__/wiki-server.test.js.map +1 -1
  386. package/dist/mcp/code-intel-server.d.ts.map +1 -1
  387. package/dist/mcp/code-intel-server.js +13 -3
  388. package/dist/mcp/code-intel-server.js.map +1 -1
  389. package/dist/mcp/hermes-bridge.d.ts.map +1 -1
  390. package/dist/mcp/hermes-bridge.js +2 -2
  391. package/dist/mcp/hermes-bridge.js.map +1 -1
  392. package/dist/mcp/state-paths.d.ts +78 -4
  393. package/dist/mcp/state-paths.d.ts.map +1 -1
  394. package/dist/mcp/state-paths.js +543 -40
  395. package/dist/mcp/state-paths.js.map +1 -1
  396. package/dist/mcp/state-server.d.ts +4 -4
  397. package/dist/mcp/state-server.d.ts.map +1 -1
  398. package/dist/mcp/wiki-server.d.ts +11 -0
  399. package/dist/mcp/wiki-server.d.ts.map +1 -1
  400. package/dist/mcp/wiki-server.js +9 -2
  401. package/dist/mcp/wiki-server.js.map +1 -1
  402. package/dist/modes/__tests__/base-autopilot-gates.test.js +36 -7
  403. package/dist/modes/__tests__/base-autopilot-gates.test.js.map +1 -1
  404. package/dist/modes/__tests__/base-multi-state-compat.test.js +1 -1
  405. package/dist/modes/__tests__/base-multi-state-compat.test.js.map +1 -1
  406. package/dist/modes/__tests__/base-session-scope.test.js +17 -8
  407. package/dist/modes/__tests__/base-session-scope.test.js.map +1 -1
  408. package/dist/modes/__tests__/base-tmux-pane.test.js +4 -4
  409. package/dist/modes/__tests__/base-tmux-pane.test.js.map +1 -1
  410. package/dist/modes/base.d.ts +5 -5
  411. package/dist/modes/base.d.ts.map +1 -1
  412. package/dist/modes/base.js +76 -21
  413. package/dist/modes/base.js.map +1 -1
  414. package/dist/native-assets/archive.d.ts +17 -0
  415. package/dist/native-assets/archive.d.ts.map +1 -0
  416. package/dist/native-assets/archive.js +238 -0
  417. package/dist/native-assets/archive.js.map +1 -0
  418. package/dist/native-assets/policy.d.ts +42 -0
  419. package/dist/native-assets/policy.d.ts.map +1 -0
  420. package/dist/native-assets/policy.js +155 -0
  421. package/dist/native-assets/policy.js.map +1 -0
  422. package/dist/notifications/temp-contract.d.ts.map +1 -1
  423. package/dist/notifications/temp-contract.js +4 -0
  424. package/dist/notifications/temp-contract.js.map +1 -1
  425. package/dist/pipeline/__tests__/orchestrator.test.js +164 -42
  426. package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
  427. package/dist/pipeline/__tests__/stages.test.js +72 -155
  428. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  429. package/dist/pipeline/orchestrator.d.ts.map +1 -1
  430. package/dist/pipeline/orchestrator.js +124 -18
  431. package/dist/pipeline/orchestrator.js.map +1 -1
  432. package/dist/pipeline/stages/ralplan.d.ts.map +1 -1
  433. package/dist/pipeline/stages/ralplan.js +36 -8
  434. package/dist/pipeline/stages/ralplan.js.map +1 -1
  435. package/dist/ralplan/__tests__/consensus-gate.test.js +73 -1316
  436. package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -1
  437. package/dist/ralplan/__tests__/documented-leader-preflight.test.d.ts +2 -0
  438. package/dist/ralplan/__tests__/documented-leader-preflight.test.d.ts.map +1 -0
  439. package/dist/ralplan/__tests__/documented-leader-preflight.test.js +335 -0
  440. package/dist/ralplan/__tests__/documented-leader-preflight.test.js.map +1 -0
  441. package/dist/ralplan/__tests__/runtime.test.js +85 -101
  442. package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
  443. package/dist/ralplan/consensus-gate.d.ts +9 -1
  444. package/dist/ralplan/consensus-gate.d.ts.map +1 -1
  445. package/dist/ralplan/consensus-gate.js +82 -338
  446. package/dist/ralplan/consensus-gate.js.map +1 -1
  447. package/dist/ralplan/documented-leader-preflight.d.ts +41 -0
  448. package/dist/ralplan/documented-leader-preflight.d.ts.map +1 -0
  449. package/dist/ralplan/documented-leader-preflight.js +360 -0
  450. package/dist/ralplan/documented-leader-preflight.js.map +1 -0
  451. package/dist/ralplan/runtime.d.ts +2 -1
  452. package/dist/ralplan/runtime.d.ts.map +1 -1
  453. package/dist/ralplan/runtime.js +67 -168
  454. package/dist/ralplan/runtime.js.map +1 -1
  455. package/dist/runtime/__tests__/bridge.test.js +42 -1
  456. package/dist/runtime/__tests__/bridge.test.js.map +1 -1
  457. package/dist/runtime/bridge.d.ts +12 -0
  458. package/dist/runtime/bridge.d.ts.map +1 -1
  459. package/dist/runtime/bridge.js +52 -1
  460. package/dist/runtime/bridge.js.map +1 -1
  461. package/dist/runtime/run-state.d.ts +5 -1
  462. package/dist/runtime/run-state.d.ts.map +1 -1
  463. package/dist/runtime/run-state.js +15 -8
  464. package/dist/runtime/run-state.js.map +1 -1
  465. package/dist/scripts/__tests__/codex-native-hook.test.js +16323 -8978
  466. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  467. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.d.ts +2 -0
  468. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.d.ts.map +1 -0
  469. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.js +225 -0
  470. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.js.map +1 -0
  471. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.d.ts +2 -0
  472. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.d.ts.map +1 -0
  473. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.js +188 -0
  474. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.js.map +1 -0
  475. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.d.ts +2 -0
  476. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.d.ts.map +1 -0
  477. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.js +259 -0
  478. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.js.map +1 -0
  479. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.d.ts +2 -0
  480. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.d.ts.map +1 -0
  481. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.js +491 -0
  482. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.js.map +1 -0
  483. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.d.ts +2 -0
  484. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.d.ts.map +1 -0
  485. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.js +708 -0
  486. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.js.map +1 -0
  487. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.d.ts +2 -0
  488. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.d.ts.map +1 -0
  489. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.js +208 -0
  490. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.js.map +1 -0
  491. package/dist/scripts/__tests__/notify-state-io.test.js +26 -0
  492. package/dist/scripts/__tests__/notify-state-io.test.js.map +1 -1
  493. package/dist/scripts/__tests__/notify-tmux-injection.test.js +23 -1
  494. package/dist/scripts/__tests__/notify-tmux-injection.test.js.map +1 -1
  495. package/dist/scripts/__tests__/postinstall.test.js +19 -1
  496. package/dist/scripts/__tests__/postinstall.test.js.map +1 -1
  497. package/dist/scripts/__tests__/reasoning-artifact-contract.test.d.ts +2 -0
  498. package/dist/scripts/__tests__/reasoning-artifact-contract.test.d.ts.map +1 -0
  499. package/dist/scripts/__tests__/reasoning-artifact-contract.test.js +108 -0
  500. package/dist/scripts/__tests__/reasoning-artifact-contract.test.js.map +1 -0
  501. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js +63 -173
  502. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js.map +1 -1
  503. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js +74 -159
  504. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js.map +1 -1
  505. package/dist/scripts/__tests__/smoke-packed-install.test.js +404 -13
  506. package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
  507. package/dist/scripts/__tests__/verify-native-release-assets.test.d.ts +2 -0
  508. package/dist/scripts/__tests__/verify-native-release-assets.test.d.ts.map +1 -0
  509. package/dist/scripts/__tests__/verify-native-release-assets.test.js +235 -0
  510. package/dist/scripts/__tests__/verify-native-release-assets.test.js.map +1 -0
  511. package/dist/scripts/codex-native-hook.d.ts +53 -1
  512. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  513. package/dist/scripts/codex-native-hook.js +14448 -1480
  514. package/dist/scripts/codex-native-hook.js.map +1 -1
  515. package/dist/scripts/codex-native-pre-post.d.ts +9 -0
  516. package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
  517. package/dist/scripts/codex-native-pre-post.js +35 -12
  518. package/dist/scripts/codex-native-pre-post.js.map +1 -1
  519. package/dist/scripts/generate-native-release-manifest.js +80 -51
  520. package/dist/scripts/generate-native-release-manifest.js.map +1 -1
  521. package/dist/scripts/notify-fallback-watcher.js +78 -115
  522. package/dist/scripts/notify-fallback-watcher.js.map +1 -1
  523. package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
  524. package/dist/scripts/notify-hook/auto-nudge.js +74 -3
  525. package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
  526. package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
  527. package/dist/scripts/notify-hook/state-io.js +5 -0
  528. package/dist/scripts/notify-hook/state-io.js.map +1 -1
  529. package/dist/scripts/notify-hook/team-dispatch.d.ts +9 -0
  530. package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
  531. package/dist/scripts/notify-hook/team-dispatch.js +234 -79
  532. package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
  533. package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
  534. package/dist/scripts/notify-hook/team-leader-nudge.js +71 -16
  535. package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
  536. package/dist/scripts/notify-hook/team-tmux-guard.d.ts +12 -4
  537. package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
  538. package/dist/scripts/notify-hook/team-tmux-guard.js +365 -44
  539. package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
  540. package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
  541. package/dist/scripts/notify-hook/team-worker-stop.js +42 -21
  542. package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
  543. package/dist/scripts/notify-hook/team-worker.d.ts +3 -0
  544. package/dist/scripts/notify-hook/team-worker.d.ts.map +1 -1
  545. package/dist/scripts/notify-hook/team-worker.js +82 -33
  546. package/dist/scripts/notify-hook/team-worker.js.map +1 -1
  547. package/dist/scripts/notify-hook/tmux-injection.d.ts +1 -0
  548. package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
  549. package/dist/scripts/notify-hook/tmux-injection.js +32 -3
  550. package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
  551. package/dist/scripts/notify-hook.js +79 -82
  552. package/dist/scripts/notify-hook.js.map +1 -1
  553. package/dist/scripts/postinstall-advisory.d.ts +14 -0
  554. package/dist/scripts/postinstall-advisory.d.ts.map +1 -0
  555. package/dist/scripts/postinstall-advisory.js +49 -0
  556. package/dist/scripts/postinstall-advisory.js.map +1 -0
  557. package/dist/scripts/postinstall.d.ts +1 -1
  558. package/dist/scripts/postinstall.d.ts.map +1 -1
  559. package/dist/scripts/postinstall.js +1 -1
  560. package/dist/scripts/postinstall.js.map +1 -1
  561. package/dist/scripts/smoke-packed-install.d.ts +80 -4
  562. package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
  563. package/dist/scripts/smoke-packed-install.js +3162 -26
  564. package/dist/scripts/smoke-packed-install.js.map +1 -1
  565. package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -1
  566. package/dist/scripts/sync-plugin-mirror.js +16 -1
  567. package/dist/scripts/sync-plugin-mirror.js.map +1 -1
  568. package/dist/scripts/verify-native-release-assets.js +37 -23
  569. package/dist/scripts/verify-native-release-assets.js.map +1 -1
  570. package/dist/state/__tests__/mode-state-context.test.js +61 -0
  571. package/dist/state/__tests__/mode-state-context.test.js.map +1 -1
  572. package/dist/state/__tests__/operations.test.js +462 -850
  573. package/dist/state/__tests__/operations.test.js.map +1 -1
  574. package/dist/state/__tests__/skill-active.test.js +400 -2
  575. package/dist/state/__tests__/skill-active.test.js.map +1 -1
  576. package/dist/state/mode-state-context.d.ts +8 -0
  577. package/dist/state/mode-state-context.d.ts.map +1 -1
  578. package/dist/state/mode-state-context.js +73 -0
  579. package/dist/state/mode-state-context.js.map +1 -1
  580. package/dist/state/operations.d.ts +3 -0
  581. package/dist/state/operations.d.ts.map +1 -1
  582. package/dist/state/operations.js +144 -40
  583. package/dist/state/operations.js.map +1 -1
  584. package/dist/state/skill-active.d.ts +18 -1
  585. package/dist/state/skill-active.d.ts.map +1 -1
  586. package/dist/state/skill-active.js +420 -23
  587. package/dist/state/skill-active.js.map +1 -1
  588. package/dist/state/workflow-transition-reconcile.d.ts +3 -0
  589. package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
  590. package/dist/state/workflow-transition-reconcile.js +10 -4
  591. package/dist/state/workflow-transition-reconcile.js.map +1 -1
  592. package/dist/state/workflow-transition.d.ts.map +1 -1
  593. package/dist/state/workflow-transition.js +3 -1
  594. package/dist/state/workflow-transition.js.map +1 -1
  595. package/dist/subagents/__tests__/leader-bootstrap-3181.test.js +53 -247
  596. package/dist/subagents/__tests__/leader-bootstrap-3181.test.js.map +1 -1
  597. package/dist/subagents/__tests__/tracker.test.js +837 -1048
  598. package/dist/subagents/__tests__/tracker.test.js.map +1 -1
  599. package/dist/subagents/tracker.d.ts +58 -135
  600. package/dist/subagents/tracker.d.ts.map +1 -1
  601. package/dist/subagents/tracker.js +811 -632
  602. package/dist/subagents/tracker.js.map +1 -1
  603. package/dist/team/__tests__/api-interop.test.js +91 -1
  604. package/dist/team/__tests__/api-interop.test.js.map +1 -1
  605. package/dist/team/__tests__/delivery-e2e-smoke.test.js +58 -17
  606. package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
  607. package/dist/team/__tests__/idle-nudge.test.js +45 -15
  608. package/dist/team/__tests__/idle-nudge.test.js.map +1 -1
  609. package/dist/team/__tests__/mcp-comm.test.js +197 -30
  610. package/dist/team/__tests__/mcp-comm.test.js.map +1 -1
  611. package/dist/team/__tests__/model-contract.test.js +110 -3
  612. package/dist/team/__tests__/model-contract.test.js.map +1 -1
  613. package/dist/team/__tests__/notice-ledger.test.d.ts +2 -0
  614. package/dist/team/__tests__/notice-ledger.test.d.ts.map +1 -0
  615. package/dist/team/__tests__/notice-ledger.test.js +131 -0
  616. package/dist/team/__tests__/notice-ledger.test.js.map +1 -0
  617. package/dist/team/__tests__/pane-teardown-residue-e2e.test.d.ts +2 -0
  618. package/dist/team/__tests__/pane-teardown-residue-e2e.test.d.ts.map +1 -0
  619. package/dist/team/__tests__/pane-teardown-residue-e2e.test.js +410 -0
  620. package/dist/team/__tests__/pane-teardown-residue-e2e.test.js.map +1 -0
  621. package/dist/team/__tests__/phase-controller.test.js +20 -0
  622. package/dist/team/__tests__/phase-controller.test.js.map +1 -1
  623. package/dist/team/__tests__/runtime-cli.test.js +9 -0
  624. package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
  625. package/dist/team/__tests__/runtime.test.js +3963 -428
  626. package/dist/team/__tests__/runtime.test.js.map +1 -1
  627. package/dist/team/__tests__/scaling.test.js +2405 -203
  628. package/dist/team/__tests__/scaling.test.js.map +1 -1
  629. package/dist/team/__tests__/state-root.test.js +117 -2
  630. package/dist/team/__tests__/state-root.test.js.map +1 -1
  631. package/dist/team/__tests__/state.test.js +804 -36
  632. package/dist/team/__tests__/state.test.js.map +1 -1
  633. package/dist/team/__tests__/team-ops-contract.test.js +6 -0
  634. package/dist/team/__tests__/team-ops-contract.test.js.map +1 -1
  635. package/dist/team/__tests__/tmux-session.test.js +4099 -1039
  636. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  637. package/dist/team/__tests__/tmux-source-authority-realtmux.test.d.ts +2 -0
  638. package/dist/team/__tests__/tmux-source-authority-realtmux.test.d.ts.map +1 -0
  639. package/dist/team/__tests__/tmux-source-authority-realtmux.test.js +149 -0
  640. package/dist/team/__tests__/tmux-source-authority-realtmux.test.js.map +1 -0
  641. package/dist/team/__tests__/tmux-test-fixture.d.ts +69 -0
  642. package/dist/team/__tests__/tmux-test-fixture.d.ts.map +1 -1
  643. package/dist/team/__tests__/tmux-test-fixture.js +242 -15
  644. package/dist/team/__tests__/tmux-test-fixture.js.map +1 -1
  645. package/dist/team/__tests__/tmux-test-fixture.test.js +177 -1
  646. package/dist/team/__tests__/tmux-test-fixture.test.js.map +1 -1
  647. package/dist/team/__tests__/worker-runtime-identity.test.js +53 -44
  648. package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
  649. package/dist/team/__tests__/worktree.test.js +6 -0
  650. package/dist/team/__tests__/worktree.test.js.map +1 -1
  651. package/dist/team/api-interop.d.ts.map +1 -1
  652. package/dist/team/api-interop.js +15 -9
  653. package/dist/team/api-interop.js.map +1 -1
  654. package/dist/team/exact-pane.d.ts +23 -0
  655. package/dist/team/exact-pane.d.ts.map +1 -0
  656. package/dist/team/exact-pane.js +133 -0
  657. package/dist/team/exact-pane.js.map +1 -0
  658. package/dist/team/idle-nudge.d.ts +12 -1
  659. package/dist/team/idle-nudge.d.ts.map +1 -1
  660. package/dist/team/idle-nudge.js +41 -14
  661. package/dist/team/idle-nudge.js.map +1 -1
  662. package/dist/team/model-contract.d.ts +2 -5
  663. package/dist/team/model-contract.d.ts.map +1 -1
  664. package/dist/team/model-contract.js +59 -17
  665. package/dist/team/model-contract.js.map +1 -1
  666. package/dist/team/notice-ledger.d.ts +54 -0
  667. package/dist/team/notice-ledger.d.ts.map +1 -0
  668. package/dist/team/notice-ledger.js +259 -0
  669. package/dist/team/notice-ledger.js.map +1 -0
  670. package/dist/team/phase-controller.d.ts.map +1 -1
  671. package/dist/team/phase-controller.js +2 -0
  672. package/dist/team/phase-controller.js.map +1 -1
  673. package/dist/team/runtime.d.ts +4 -1
  674. package/dist/team/runtime.d.ts.map +1 -1
  675. package/dist/team/runtime.js +1527 -291
  676. package/dist/team/runtime.js.map +1 -1
  677. package/dist/team/scaling.d.ts +13 -1
  678. package/dist/team/scaling.d.ts.map +1 -1
  679. package/dist/team/scaling.js +1503 -387
  680. package/dist/team/scaling.js.map +1 -1
  681. package/dist/team/state/dispatch.d.ts.map +1 -1
  682. package/dist/team/state/dispatch.js +26 -8
  683. package/dist/team/state/dispatch.js.map +1 -1
  684. package/dist/team/state/locks.d.ts.map +1 -1
  685. package/dist/team/state/locks.js +61 -13
  686. package/dist/team/state/locks.js.map +1 -1
  687. package/dist/team/state/monitor.d.ts +10 -0
  688. package/dist/team/state/monitor.d.ts.map +1 -1
  689. package/dist/team/state/monitor.js +13 -0
  690. package/dist/team/state/monitor.js.map +1 -1
  691. package/dist/team/state/tasks.d.ts.map +1 -1
  692. package/dist/team/state/tasks.js +4 -3
  693. package/dist/team/state/tasks.js.map +1 -1
  694. package/dist/team/state/types.d.ts +15 -0
  695. package/dist/team/state/types.d.ts.map +1 -1
  696. package/dist/team/state/types.js.map +1 -1
  697. package/dist/team/state-root.d.ts +12 -0
  698. package/dist/team/state-root.d.ts.map +1 -1
  699. package/dist/team/state-root.js +177 -0
  700. package/dist/team/state-root.js.map +1 -1
  701. package/dist/team/state.d.ts +85 -1
  702. package/dist/team/state.d.ts.map +1 -1
  703. package/dist/team/state.js +672 -129
  704. package/dist/team/state.js.map +1 -1
  705. package/dist/team/team-ops.d.ts +5 -0
  706. package/dist/team/team-ops.d.ts.map +1 -1
  707. package/dist/team/team-ops.js +4 -0
  708. package/dist/team/team-ops.js.map +1 -1
  709. package/dist/team/tmux-session.d.ts +163 -27
  710. package/dist/team/tmux-session.d.ts.map +1 -1
  711. package/dist/team/tmux-session.js +1682 -376
  712. package/dist/team/tmux-session.js.map +1 -1
  713. package/dist/team/worktree.d.ts.map +1 -1
  714. package/dist/team/worktree.js +4 -0
  715. package/dist/team/worktree.js.map +1 -1
  716. package/dist/ultragoal/__tests__/artifacts.test.js +950 -2
  717. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
  718. package/dist/ultragoal/__tests__/docs-contract.test.js +1 -0
  719. package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
  720. package/dist/ultragoal/artifacts.d.ts +35 -1
  721. package/dist/ultragoal/artifacts.d.ts.map +1 -1
  722. package/dist/ultragoal/artifacts.js +232 -46
  723. package/dist/ultragoal/artifacts.js.map +1 -1
  724. package/dist/utils/__tests__/file-durability.test.d.ts +2 -0
  725. package/dist/utils/__tests__/file-durability.test.d.ts.map +1 -0
  726. package/dist/utils/__tests__/file-durability.test.js +114 -0
  727. package/dist/utils/__tests__/file-durability.test.js.map +1 -0
  728. package/dist/utils/__tests__/paths.test.js +21 -1
  729. package/dist/utils/__tests__/paths.test.js.map +1 -1
  730. package/dist/utils/file-durability.d.ts +25 -0
  731. package/dist/utils/file-durability.d.ts.map +1 -0
  732. package/dist/utils/file-durability.js +67 -0
  733. package/dist/utils/file-durability.js.map +1 -0
  734. package/dist/utils/paths.d.ts.map +1 -1
  735. package/dist/utils/paths.js +14 -6
  736. package/dist/utils/paths.js.map +1 -1
  737. package/dist/verification/__tests__/ci-rust-gates.test.js +49 -4
  738. package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
  739. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js +3 -0
  740. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js.map +1 -1
  741. package/dist/verification/__tests__/explore-harness-release-workflow.test.js +60 -0
  742. package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
  743. package/dist/verification/__tests__/native-release-manifest.test.js +157 -4
  744. package/dist/verification/__tests__/native-release-manifest.test.js.map +1 -1
  745. package/dist/wiki/lint.d.ts +4 -1
  746. package/dist/wiki/lint.d.ts.map +1 -1
  747. package/dist/wiki/lint.js +3 -2
  748. package/dist/wiki/lint.js.map +1 -1
  749. package/package.json +13 -5
  750. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  751. package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +105 -17
  752. package/plugins/oh-my-codex/skills/autopilot/SKILL.md +13 -5
  753. package/plugins/oh-my-codex/skills/cancel/SKILL.md +30 -85
  754. package/plugins/oh-my-codex/skills/ralph/SKILL.md +6 -4
  755. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +16 -17
  756. package/plugins/oh-my-codex/skills/team/SKILL.md +13 -9
  757. package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +3 -2
  758. package/skills/autopilot/SKILL.md +13 -5
  759. package/skills/cancel/SKILL.md +30 -85
  760. package/skills/ralph/SKILL.md +6 -4
  761. package/skills/ralplan/SKILL.md +16 -17
  762. package/skills/team/SKILL.md +13 -9
  763. package/skills/ultragoal/SKILL.md +3 -2
  764. package/src/scripts/__tests__/codex-native-hook.test.ts +8831 -951
  765. package/src/scripts/__tests__/issue-3293-callsite-parity.test.ts +187 -0
  766. package/src/scripts/__tests__/issue-3293-cancel-transaction.test.ts +189 -0
  767. package/src/scripts/__tests__/issue-3293-hook-owned-cancel.test.ts +187 -0
  768. package/src/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.ts +591 -0
  769. package/src/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.ts +701 -0
  770. package/src/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.ts +211 -0
  771. package/src/scripts/__tests__/notify-state-io.test.ts +29 -0
  772. package/src/scripts/__tests__/notify-tmux-injection.test.ts +26 -1
  773. package/src/scripts/__tests__/postinstall.test.ts +23 -1
  774. package/src/scripts/__tests__/reasoning-artifact-contract.test.ts +129 -0
  775. package/src/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.ts +67 -185
  776. package/src/scripts/__tests__/role-intent-durable-recovery-3181.test.ts +79 -178
  777. package/src/scripts/__tests__/smoke-packed-install.test.ts +525 -16
  778. package/src/scripts/__tests__/verify-native-release-assets.test.ts +240 -0
  779. package/src/scripts/codex-native-hook.ts +14238 -1429
  780. package/src/scripts/codex-native-pre-post.ts +43 -12
  781. package/src/scripts/generate-native-release-manifest.ts +83 -79
  782. package/src/scripts/notify-fallback-watcher.ts +91 -122
  783. package/src/scripts/notify-hook/auto-nudge.ts +71 -3
  784. package/src/scripts/notify-hook/state-io.ts +11 -0
  785. package/src/scripts/notify-hook/team-dispatch.ts +242 -75
  786. package/src/scripts/notify-hook/team-leader-nudge.ts +72 -22
  787. package/src/scripts/notify-hook/team-tmux-guard.ts +366 -39
  788. package/src/scripts/notify-hook/team-worker-stop.ts +43 -20
  789. package/src/scripts/notify-hook/team-worker.ts +82 -31
  790. package/src/scripts/notify-hook/tmux-injection.ts +33 -3
  791. package/src/scripts/notify-hook.ts +84 -82
  792. package/src/scripts/postinstall-advisory.ts +67 -0
  793. package/src/scripts/postinstall.ts +1 -1
  794. package/src/scripts/smoke-packed-install.ts +3608 -39
  795. package/src/scripts/sync-plugin-mirror.ts +17 -1
  796. package/src/scripts/verify-native-release-assets.ts +39 -31
  797. package/templates/AGENTS.md +1 -0
  798. package/dist/subagents/__tests__/adapted-role-binding.test.d.ts +0 -2
  799. package/dist/subagents/__tests__/adapted-role-binding.test.d.ts.map +0 -1
  800. package/dist/subagents/__tests__/adapted-role-binding.test.js +0 -988
  801. package/dist/subagents/__tests__/adapted-role-binding.test.js.map +0 -1
  802. package/dist/subagents/adapted-role-binding.d.ts +0 -17
  803. package/dist/subagents/adapted-role-binding.d.ts.map +0 -1
  804. package/dist/subagents/adapted-role-binding.js +0 -75
  805. package/dist/subagents/adapted-role-binding.js.map +0 -1
@@ -9,11 +9,14 @@
9
9
  * - File-based scaling lock prevents concurrent scale operations
10
10
  * - 'draining' worker status for graceful transitions during scale_down
11
11
  */
12
- import { join, resolve } from 'path';
13
- import { mkdir, rm } from 'fs/promises';
14
- import { sanitizeTeamName, isTmuxAvailable, waitForWorkerReady, dismissTrustPromptIfPresent, sendToWorker, isWorkerAlive, getWorkerPanePid, teardownWorkerPanes, buildWorkerStartupCommand, trustWorkerMiseConfigIfAvailable, writeWorkerStartupScriptCommand, resolveTeamWorkerCliForResolvedLaunchArgs, assertTeamWorkerCliPolicyCompatibility, tagPaneTeamOwner, } from './tmux-session.js';
15
- import { execFileSync, spawnSync } from 'child_process';
16
- import { teamReadConfig as readTeamConfig, teamSaveConfig as saveTeamConfig, teamWriteWorkerIdentity as writeWorkerIdentity, teamReadManifest as readTeamManifestV2, teamNormalizePolicy as normalizeTeamPolicy, teamReadWorkerStatus as readWorkerStatus, teamWriteWorkerStatus as writeWorkerStatus, teamWithScalingLock as withScalingLock, teamAppendEvent as appendTeamEvent, teamCreateTask as createStateTask, teamListTasks as listTasks, teamMarkDispatchRequestNotified as markDispatchRequestNotified, teamReadDispatchRequest as readDispatchRequest, teamTransitionDispatchRequest as transitionDispatchRequest, } from './team-ops.js';
12
+ import { randomUUID } from 'crypto';
13
+ import { dirname, isAbsolute, join, relative, resolve, sep } from 'path';
14
+ import { mkdir, readFile, realpath, rm, writeFile } from 'fs/promises';
15
+ import { existsSync } from 'fs';
16
+ import { sanitizeTeamName, isTmuxAvailable, waitForWorkerReady, dismissTrustPromptIfPresent, isWorkerAlive, buildWorkerStartupCommand, trustWorkerMiseConfigIfAvailable, writeWorkerStartupScriptCommand, resolveTeamWorkerCliForResolvedLaunchArgs, assertTeamWorkerCliPolicyCompatibility, readPaneTeamOwnerTagResult, teardownWorkerPanes, } from './tmux-session.js';
17
+ import { readExactPaneProofSync } from './exact-pane.js';
18
+ import { spawnSync } from 'child_process';
19
+ import { teamReadConfig as readTeamConfig, teamSaveConfig as saveTeamConfig, teamWriteWorkerIdentity as writeWorkerIdentity, teamReadManifest as readTeamManifestV2, teamNormalizePolicy as normalizeTeamPolicy, teamReadWorkerStatus as readWorkerStatus, teamWriteWorkerStatus as writeWorkerStatus, teamWithScalingLock as withScalingLock, teamWithTaskMembershipBarrier as withTaskMembershipBarrier, recoverTeamMembershipTaskTransaction, commitTeamMembershipTaskTransaction, finalizeTeamMembershipTaskTransaction, writeAtomic, teamRemoveDurableFile as removeDurableFile, teamAppendEvent as appendTeamEvent, teamCreateTask as createStateTask, teamListTasks as listTasks, teamReadTask as readTask, teamMarkDispatchRequestNotified as markDispatchRequestNotified, teamReadDispatchRequest as readDispatchRequest, teamTransitionDispatchRequest as transitionDispatchRequest, teamRemoveDispatchRequestsForWorkers as removeDispatchRequestsForWorkers, } from './team-ops.js';
17
20
  import { queueInboxInstruction, waitForDispatchReceipt, } from './mcp-comm.js';
18
21
  import { generateInitialInbox, buildTriggerDirective, writeWorkerRoleInstructionsFile, writeWorkerWorktreeRootAgentsFile, removeWorkerWorktreeRootAgentsFile, } from './worker-bootstrap.js';
19
22
  import { buildTeamWorkerGoalInstruction } from './goal-workflow.js';
@@ -24,8 +27,29 @@ import { resolveCodexHomeForLaunch } from '../cli/codex-home.js';
24
27
  import { parseTeamWorkerLaunchArgs, resolveTeamWorkerLaunchArgs, resolveAgentDefaultModel, resolveAgentReasoningEffort, shouldHonorAgentExactModel, TEAM_WORKER_INHERITED_MODEL_ENV, } from './model-contract.js';
25
28
  import { resolveCanonicalTeamStateRoot } from './state-root.js';
26
29
  import { ensureWorktree, planWorktreeTarget, rollbackProvisionedWorktrees, } from './worktree.js';
30
+ import { withTaskClaimLock } from './state/locks.js';
27
31
  import { buildApprovedTeamHandoffSection, resolvePersistedApprovedTeamExecutionContinuityState, } from './approved-execution.js';
28
32
  import { readPersistedTeamUltragoalContext, renderLeaderOwnedUltragoalContextSection, } from './ultragoal-context.js';
33
+ const TASK_CLAIM_LOCK_STALE_MS = 5 * 60 * 1000;
34
+ async function withTaskClaimLocks(teamName, taskIds, cwd, fn) {
35
+ const uniqueTaskIds = [...new Set(taskIds)].sort((left, right) => Number(left) - Number(right));
36
+ const teamDir = (name, lockCwd) => join(resolveCanonicalTeamStateRoot(lockCwd), 'team', name);
37
+ const locks = {
38
+ teamDir,
39
+ taskClaimLockDir: (name, taskId, lockCwd) => (join(teamDir(name, lockCwd), 'claims', `task-${taskId}.lock`)),
40
+ mailboxLockDir: (name, workerName, lockCwd) => (join(teamDir(name, lockCwd), 'mailbox', `.lock-${workerName}`)),
41
+ };
42
+ const acquire = async (index) => {
43
+ if (index >= uniqueTaskIds.length)
44
+ return await fn();
45
+ const taskId = uniqueTaskIds[index];
46
+ const locked = await withTaskClaimLock(teamName, taskId, cwd, TASK_CLAIM_LOCK_STALE_MS, locks, async () => await acquire(index + 1));
47
+ if (!locked.ok)
48
+ throw new Error(`Timed out acquiring task claim lock for ${teamName}/${taskId}`);
49
+ return locked.value;
50
+ };
51
+ return await acquire(0);
52
+ }
29
53
  // ── Environment gate ──────────────────────────────────────────────────────────
30
54
  const OMX_TEAM_SCALING_ENABLED_ENV = 'OMX_TEAM_SCALING_ENABLED';
31
55
  const WORKTREE_TRIGGER_STATE_ROOT = '$OMX_TEAM_STATE_ROOT';
@@ -36,6 +60,94 @@ export function isScalingEnabled(env = process.env) {
36
60
  const normalized = raw.trim().toLowerCase();
37
61
  return ['1', 'true', 'yes', 'on', 'enabled'].includes(normalized);
38
62
  }
63
+ const SCALE_PANE_INSTANCE_FORMAT = '#{pane_id}\t#{pane_pid}\t#{session_name}\t#{session_id}\t#{window_id}\t#{@omx_team_pane_owner_id}';
64
+ const SCALE_SPLIT_RECEIPT_FORMAT = '#{pane_id}\t#{pane_pid}\t#{session_name}\t#{session_id}\t#{window_id}';
65
+ function scaleTransactionReceipt() {
66
+ return `omx-scale-${randomUUID()}`;
67
+ }
68
+ function hasExactScaleReceipt(stdout, receipt) {
69
+ return stdout === `${receipt}\n` || stdout === `${receipt}\r\n`;
70
+ }
71
+ function runScalePaneTransaction(pane, effect, receipt = scaleTransactionReceipt()) {
72
+ const result = spawnSync('tmux', [
73
+ 'if-shell', '-F', '-t', pane.paneId, buildScalePaneAuthorityCondition(pane),
74
+ `${effect} \\; display-message -p ${quoteTmuxCommand(receipt)}`,
75
+ "display-message -p ''",
76
+ ], { encoding: 'utf-8' });
77
+ return result.status === 0 && hasExactScaleReceipt(result.stdout || '', receipt);
78
+ }
79
+ function tagScalePaneWithAuthority(pane, teamOwnerId) {
80
+ const comparisons = [
81
+ ['pane_id', pane.paneId],
82
+ ['pane_pid', String(pane.pid)],
83
+ ['session_name', pane.sessionName],
84
+ ['session_id', pane.sessionId],
85
+ ['window_id', pane.windowId],
86
+ ].map(([field, expected]) => `#{==:#{${field}},${expected}}`);
87
+ const condition = comparisons.reduce((combined, comparison) => `#{&&:${combined},${comparison}}`);
88
+ const receipt = scaleTransactionReceipt();
89
+ const result = spawnSync('tmux', [
90
+ 'if-shell', '-F', '-t', pane.paneId, condition,
91
+ `set-option -p -t ${pane.paneId} @omx_team_pane_owner_id ${quoteTmuxCommand(teamOwnerId)} \\; display-message -p ${quoteTmuxCommand(receipt)}`,
92
+ "display-message -p ''",
93
+ ], { encoding: 'utf-8' });
94
+ if (result.status !== 0 || !hasExactScaleReceipt(result.stdout || '', receipt))
95
+ return null;
96
+ return { ...pane, teamOwnerId };
97
+ }
98
+ function killScalePaneWithAuthority(pane) {
99
+ return runScalePaneTransaction(pane, `kill-pane -t ${pane.paneId}`);
100
+ }
101
+ function parseScalePaneInstance(value) {
102
+ const fields = value.trim().split('\t');
103
+ if (fields.length !== 6)
104
+ return null;
105
+ const [paneId, pidText, sessionName, sessionId, windowId, teamOwnerId] = fields;
106
+ const pid = Number(pidText);
107
+ if (!/^%[0-9]+$/.test(paneId) || !Number.isSafeInteger(pid) || pid <= 0
108
+ || !sessionName || !/^\$[0-9]+$/.test(sessionId) || !/^@[0-9]+$/.test(windowId) || !teamOwnerId)
109
+ return null;
110
+ return { paneId, pid, sessionName, sessionId, windowId, teamOwnerId };
111
+ }
112
+ function readScalePaneInstance(paneId) {
113
+ const result = spawnSync('tmux', ['display-message', '-p', '-t', paneId, SCALE_PANE_INSTANCE_FORMAT], { encoding: 'utf-8' });
114
+ return result.status === 0 ? parseScalePaneInstance(result.stdout || '') : null;
115
+ }
116
+ function quoteTmuxCommand(value) {
117
+ return `'${value.replaceAll("'", "'\\''")}'`;
118
+ }
119
+ function buildScalePaneAuthorityCondition(instance) {
120
+ const comparisons = [
121
+ ['pane_id', instance.paneId],
122
+ ['pane_pid', String(instance.pid)],
123
+ ['session_name', instance.sessionName],
124
+ ['session_id', instance.sessionId],
125
+ ['window_id', instance.windowId],
126
+ ['@omx_team_pane_owner_id', instance.teamOwnerId],
127
+ ].map(([field, expected]) => `#{==:#{${field}},${expected}}`);
128
+ return comparisons.reduce((condition, comparison) => `#{&&:${condition},${comparison}}`);
129
+ }
130
+ function splitWorkerPaneWithScaleAuthority(source, splitDirection, workerCwd, command) {
131
+ const receipt = scaleTransactionReceipt();
132
+ const splitCommand = [
133
+ 'split-window', splitDirection, '-t', source.paneId, '-d', '-P', '-F', `${SCALE_SPLIT_RECEIPT_FORMAT}\t${receipt}`,
134
+ '-c', workerCwd, command,
135
+ ].map(quoteTmuxCommand).join(' ');
136
+ const result = spawnSync('tmux', [
137
+ 'if-shell', '-F', '-t', source.paneId, buildScalePaneAuthorityCondition(source), splitCommand, "display-message -p ''",
138
+ ], { encoding: 'utf-8' });
139
+ if (result.status !== 0)
140
+ return null;
141
+ const fields = (result.stdout || '').replace(/\r?\n$/, '').split('\t');
142
+ if (fields.length !== 6 || fields[5] !== receipt)
143
+ return null;
144
+ const [paneId, pidText, sessionName, sessionId, windowId] = fields;
145
+ const pid = Number(pidText);
146
+ if (!/^%[0-9]+$/.test(paneId) || !Number.isSafeInteger(pid) || pid <= 0
147
+ || !sessionName || !sessionId || !/^\$[0-9]+$/.test(sessionId) || !/^@[0-9]+$/.test(windowId))
148
+ return null;
149
+ return { paneId, pid, sessionName, sessionId, windowId };
150
+ }
39
151
  function assertScalingEnabled(env = process.env) {
40
152
  if (!isScalingEnabled(env)) {
41
153
  throw new Error(`Dynamic scaling is disabled. Set ${OMX_TEAM_SCALING_ENABLED_ENV}=1 to enable.`);
@@ -80,6 +192,67 @@ function buildScaleUpWorkerLaunchPlans(params) {
80
192
  function resolveInstructionStateRoot(worktreePath) {
81
193
  return worktreePath ? WORKTREE_TRIGGER_STATE_ROOT : undefined;
82
194
  }
195
+ function hasScaleUpFailureInjection(env, phase) {
196
+ return env.OMX_TEAM_SCALE_UP_INJECT_FAILURE?.split(',').map((value) => value.trim()).includes(phase) ?? false;
197
+ }
198
+ function throwIfScaleUpFailureInjected(env, phase) {
199
+ if (hasScaleUpFailureInjection(env, phase)) {
200
+ throw new Error(`injected_scale_up_failure:${phase}`);
201
+ }
202
+ }
203
+ function recoverCreatedWorktreeAfterEnsureFailure(plan, worktreePathExistedBeforeEnsure, branchExistedBeforeEnsure) {
204
+ if (worktreePathExistedBeforeEnsure || !existsSync(plan.worktreePath))
205
+ return null;
206
+ const commonDir = spawnSync('git', ['rev-parse', '--git-common-dir'], {
207
+ cwd: plan.worktreePath,
208
+ encoding: 'utf-8',
209
+ windowsHide: true,
210
+ });
211
+ const repoCommonDir = spawnSync('git', ['rev-parse', '--git-common-dir'], {
212
+ cwd: plan.repoRoot,
213
+ encoding: 'utf-8',
214
+ windowsHide: true,
215
+ });
216
+ if (commonDir.status !== 0
217
+ || repoCommonDir.status !== 0
218
+ || resolve(plan.worktreePath, (commonDir.stdout || '').trim())
219
+ !== resolve(plan.repoRoot, (repoCommonDir.stdout || '').trim())) {
220
+ return null;
221
+ }
222
+ if (plan.branchName) {
223
+ const branch = spawnSync('git', ['symbolic-ref', '-q', 'HEAD'], {
224
+ cwd: plan.worktreePath,
225
+ encoding: 'utf-8',
226
+ windowsHide: true,
227
+ });
228
+ if (branch.status !== 0 || (branch.stdout || '').trim() !== `refs/heads/${plan.branchName}`)
229
+ return null;
230
+ }
231
+ else {
232
+ const head = spawnSync('git', ['rev-parse', 'HEAD'], {
233
+ cwd: plan.worktreePath,
234
+ encoding: 'utf-8',
235
+ windowsHide: true,
236
+ });
237
+ const branch = spawnSync('git', ['symbolic-ref', '-q', 'HEAD'], {
238
+ cwd: plan.worktreePath,
239
+ encoding: 'utf-8',
240
+ windowsHide: true,
241
+ });
242
+ if (head.status !== 0 || branch.status === 0 || (head.stdout || '').trim() !== plan.baseRef)
243
+ return null;
244
+ }
245
+ return {
246
+ enabled: true,
247
+ repoRoot: plan.repoRoot,
248
+ worktreePath: plan.worktreePath,
249
+ detached: plan.detached,
250
+ branchName: plan.branchName,
251
+ created: true,
252
+ reused: false,
253
+ createdBranch: Boolean(plan.branchName && !branchExistedBeforeEnsure),
254
+ };
255
+ }
83
256
  function assertUnreachableApprovedExecutionState(state) {
84
257
  throw new Error(`unreachable_scale_up_approved_execution_state:${JSON.stringify(state)}`);
85
258
  }
@@ -146,9 +319,22 @@ function resolveScaleUpWorktreeMode(config) {
146
319
  throw error;
147
320
  }
148
321
  }
149
- async function notifyWorkerPaneOutcome(sessionName, workerIndex, message, paneId, workerCli) {
322
+ async function notifyWorkerPaneOutcome(sessionName, message, paneId, expectedPanePid, expectedTeamOwnerId, expectedSessionId, expectedWindowId) {
150
323
  try {
151
- await sendToWorker(sessionName, workerIndex, message, paneId, workerCli);
324
+ if (!paneId || !expectedPanePid || !expectedTeamOwnerId || !expectedSessionId || !expectedWindowId) {
325
+ throw new Error(`scale_up_final_send_authority_missing:${paneId ?? 'unknown'}`);
326
+ }
327
+ const pane = {
328
+ paneId,
329
+ pid: expectedPanePid,
330
+ sessionName,
331
+ sessionId: expectedSessionId,
332
+ windowId: expectedWindowId,
333
+ teamOwnerId: expectedTeamOwnerId,
334
+ };
335
+ if (!runScalePaneTransaction(pane, `send-keys -t ${paneId} -l ${quoteTmuxCommand(message)} \\; send-keys -t ${paneId} C-m`)) {
336
+ throw new Error(`scale_up_final_send_authority_lost:${paneId}`);
337
+ }
152
338
  return { ok: true, transport: 'tmux_send_keys', reason: 'tmux_send_keys_sent' };
153
339
  }
154
340
  catch (error) {
@@ -177,356 +363,1082 @@ export async function scaleUp(teamName, count, agentType, tasks, cwd, env = proc
177
363
  const sanitized = sanitizeTeamName(teamName);
178
364
  const leaderCwd = resolve(cwd);
179
365
  return await withScalingLock(sanitized, leaderCwd, async () => {
180
- const config = await readTeamConfig(sanitized, leaderCwd);
181
- if (!config) {
182
- return { ok: false, error: `Team ${sanitized} not found` };
183
- }
184
- const maxWorkers = config.max_workers;
185
- const currentCount = config.workers.length;
186
- if (currentCount + count > maxWorkers) {
187
- return {
188
- ok: false,
189
- error: `Cannot add ${count} workers: would exceed max_workers (${currentCount} + ${count} > ${maxWorkers})`,
190
- };
191
- }
192
- const teamStateRoot = config.team_state_root ?? resolveCanonicalTeamStateRoot(leaderCwd);
193
- const codexHomeOverride = resolveCodexHomeForLaunch(leaderCwd, env);
194
- const launchEnv = codexHomeOverride
195
- ? { ...env, CODEX_HOME: codexHomeOverride }
196
- : env;
197
- // Build and validate every launch plan before any task, directory, worktree,
198
- // pane, process, or config mutation. The plan is the sole source of launch
199
- // policy; later task materialization must not alter it.
200
- const initialNextIndex = config.next_worker_index ?? (currentCount + 1);
201
- let workerLaunchPlans;
202
- try {
203
- const existingTasks = await listTasks(sanitized, leaderCwd);
204
- workerLaunchPlans = buildScaleUpWorkerLaunchPlans({
205
- count,
206
- nextWorkerIndex: initialNextIndex,
207
- agentType,
208
- existingTasks,
209
- incomingTasks: tasks,
210
- launchEnv,
211
- codexHomeOverride,
366
+ return await withTaskMembershipBarrier(sanitized, leaderCwd, async () => {
367
+ const config = await readTeamConfig(sanitized, leaderCwd);
368
+ if (!config) {
369
+ return { ok: false, error: `Team ${sanitized} not found` };
370
+ }
371
+ const originalConfig = JSON.parse(JSON.stringify(config));
372
+ const configPath = join(resolveCanonicalTeamStateRoot(leaderCwd), 'team', sanitized, 'config.json');
373
+ const manifestPath = join(resolveCanonicalTeamStateRoot(leaderCwd), 'team', sanitized, 'manifest.v2.json');
374
+ const originalManifestBytes = existsSync(manifestPath) ? await readFile(manifestPath, 'utf8') : null;
375
+ const maxWorkers = config.max_workers;
376
+ const currentCount = config.workers.length;
377
+ if (currentCount + count > maxWorkers) {
378
+ return {
379
+ ok: false,
380
+ error: `Cannot add ${count} workers: would exceed max_workers (${currentCount} + ${count} > ${maxWorkers})`,
381
+ };
382
+ }
383
+ const teamStateRoot = config.team_state_root ?? resolveCanonicalTeamStateRoot(leaderCwd);
384
+ const codexHomeOverride = resolveCodexHomeForLaunch(leaderCwd, env);
385
+ const launchEnv = codexHomeOverride
386
+ ? { ...env, CODEX_HOME: codexHomeOverride }
387
+ : env;
388
+ // Build and validate every launch plan before any task, directory, worktree,
389
+ // pane, process, or config mutation. The plan is the sole source of launch
390
+ // policy; later task materialization must not alter it.
391
+ const initialNextIndex = config.next_worker_index ?? (currentCount + 1);
392
+ let workerLaunchPlans;
393
+ try {
394
+ const existingTasks = await listTasks(sanitized, leaderCwd);
395
+ workerLaunchPlans = buildScaleUpWorkerLaunchPlans({
396
+ count,
397
+ nextWorkerIndex: initialNextIndex,
398
+ agentType,
399
+ existingTasks,
400
+ incomingTasks: tasks,
401
+ launchEnv,
402
+ codexHomeOverride,
403
+ });
404
+ }
405
+ catch (error) {
406
+ return {
407
+ ok: false,
408
+ error: error instanceof Error ? error.message : String(error),
409
+ };
410
+ }
411
+ let nextIndex = initialNextIndex;
412
+ const sessionName = config.tmux_session;
413
+ const manifest = await readTeamManifestV2(sanitized, leaderCwd);
414
+ const dispatchPolicy = normalizeTeamPolicy(manifest?.policy, {
415
+ display_mode: manifest?.policy?.display_mode === 'split_pane' ? 'split_pane' : 'auto',
416
+ worker_launch_mode: config.worker_launch_mode,
212
417
  });
213
- }
214
- catch (error) {
215
- return {
216
- ok: false,
217
- error: error instanceof Error ? error.message : String(error),
218
- };
219
- }
220
- let nextIndex = initialNextIndex;
221
- const sessionName = config.tmux_session;
222
- const manifest = await readTeamManifestV2(sanitized, leaderCwd);
223
- const dispatchPolicy = normalizeTeamPolicy(manifest?.policy, {
224
- display_mode: manifest?.policy?.display_mode === 'split_pane' ? 'split_pane' : 'auto',
225
- worker_launch_mode: config.worker_launch_mode,
226
- });
227
- const approvedExecutionState = await resolvePersistedApprovedTeamExecutionContinuityState(sanitized, config.leader_cwd ?? leaderCwd, config.team_state_root ?? teamStateRoot);
228
- const approvedExecutionGate = resolveScaleUpApprovedExecutionGate(sanitized, approvedExecutionState);
229
- if (!approvedExecutionGate.ok) {
230
- return approvedExecutionGate;
231
- }
232
- const persistedUltragoalContext = await readPersistedTeamUltragoalContext(sanitized, config.leader_cwd ?? leaderCwd, config.team_state_root ?? teamStateRoot);
233
- const approvedContextSection = joinContextSections(approvedExecutionGate.approvedContextSection, renderLeaderOwnedUltragoalContextSection(persistedUltragoalContext));
234
- const effectiveWorktreeMode = config.worktree_mode ?? resolveScaleUpWorktreeMode(config);
235
- if (!config.worktree_mode && effectiveWorktreeMode.enabled) {
236
- config.worktree_mode = effectiveWorktreeMode;
237
- await saveTeamConfig(config, leaderCwd);
238
- }
239
- const addedWorkers = [];
240
- const createdTaskIds = [];
241
- const rollbackScaleUp = async (error, context = {}) => {
242
- for (const w of addedWorkers) {
243
- const idx = config.workers.findIndex((worker) => worker.name === w.name);
244
- if (idx >= 0) {
245
- config.workers.splice(idx, 1);
418
+ const approvedExecutionState = await resolvePersistedApprovedTeamExecutionContinuityState(sanitized, config.leader_cwd ?? leaderCwd, config.team_state_root ?? teamStateRoot);
419
+ const approvedExecutionGate = resolveScaleUpApprovedExecutionGate(sanitized, approvedExecutionState);
420
+ if (!approvedExecutionGate.ok) {
421
+ return approvedExecutionGate;
422
+ }
423
+ const persistedUltragoalContext = await readPersistedTeamUltragoalContext(sanitized, config.leader_cwd ?? leaderCwd, config.team_state_root ?? teamStateRoot);
424
+ const approvedContextSection = joinContextSections(approvedExecutionGate.approvedContextSection, renderLeaderOwnedUltragoalContextSection(persistedUltragoalContext));
425
+ const initialSplitTargetCandidate = config.workers[config.workers.length - 1];
426
+ const initialSplitTargetWorker = initialSplitTargetCandidate?.pane_id?.trim()
427
+ ? initialSplitTargetCandidate
428
+ : undefined;
429
+ const initialSplitTarget = initialSplitTargetWorker?.pane_id ?? config.leader_pane_id ?? '';
430
+ const initialSplitTargetOwnerId = typeof config.tmux_pane_owner_id === 'string'
431
+ ? config.tmux_pane_owner_id.trim()
432
+ : '';
433
+ if (!initialSplitTargetOwnerId) {
434
+ return {
435
+ ok: false,
436
+ error: `scale_up_split_target_owner_unavailable:${initialSplitTarget}`,
437
+ };
438
+ }
439
+ let initialExpectedSplitTargetPid = initialSplitTargetWorker
440
+ ? initialSplitTargetWorker.pid
441
+ : config.leader_pane_pid;
442
+ if (typeof initialExpectedSplitTargetPid !== 'number'
443
+ || !Number.isSafeInteger(initialExpectedSplitTargetPid)
444
+ || initialExpectedSplitTargetPid <= 0) {
445
+ // Legacy configs may lack a persisted PID, but the persisted pane ID and
446
+ // canonical owner remain mandatory. Pin only the exact live pane proven
447
+ // from one global snapshot, then re-prove it after reading the owner tag
448
+ // so a recycled pane ID cannot be backfilled.
449
+ const migrationProof = readExactPaneProofSync(initialSplitTarget);
450
+ if (migrationProof.status !== 'live') {
451
+ return {
452
+ ok: false,
453
+ error: `scale_up_split_target_proof_unavailable:${initialSplitTarget}:${migrationProof.reason}`,
454
+ };
455
+ }
456
+ const migrationOwner = readPaneTeamOwnerTagResult(migrationProof.paneId);
457
+ if (migrationOwner.status !== 'value' || migrationOwner.value !== initialSplitTargetOwnerId) {
458
+ return {
459
+ ok: false,
460
+ error: `scale_up_split_target_owner_changed:${migrationProof.paneId}`,
461
+ };
462
+ }
463
+ const finalMigrationProof = readExactPaneProofSync(migrationProof.paneId);
464
+ if (finalMigrationProof.status !== 'live') {
465
+ return {
466
+ ok: false,
467
+ error: `scale_up_split_target_proof_unavailable:${migrationProof.paneId}:${finalMigrationProof.reason}`,
468
+ };
469
+ }
470
+ if (finalMigrationProof.pid !== migrationProof.pid) {
471
+ return {
472
+ ok: false,
473
+ error: `scale_up_split_target_pid_changed:${migrationProof.paneId}:${migrationProof.pid}:${finalMigrationProof.pid}`,
474
+ };
475
+ }
476
+ if (initialSplitTargetWorker) {
477
+ initialSplitTargetWorker.pid = finalMigrationProof.pid;
478
+ }
479
+ else {
480
+ config.leader_pane_pid = finalMigrationProof.pid;
246
481
  }
247
482
  try {
248
- if (w.pane_id) {
249
- execFileSync('tmux', ['kill-pane', '-t', w.pane_id], { stdio: 'pipe',
250
- windowsHide: true,
251
- });
252
- }
483
+ await saveTeamConfig(config, leaderCwd);
253
484
  }
254
- catch { }
255
- if (w.worktree_path) {
256
- await removeWorkerWorktreeRootAgentsFile(sanitized, w.name, teamStateRoot, w.worktree_path).catch(() => { });
485
+ catch (error) {
486
+ return {
487
+ ok: false,
488
+ error: `scale_up_split_target_pid_persistence_failed:${initialSplitTarget}:${error instanceof Error ? error.message : String(error)}`,
489
+ };
257
490
  }
491
+ Object.assign(originalConfig, JSON.parse(JSON.stringify(config)));
492
+ initialExpectedSplitTargetPid = finalMigrationProof.pid;
493
+ }
494
+ const initialSplitTargetProof = readExactPaneProofSync(initialSplitTarget);
495
+ if (initialSplitTargetProof.status !== 'live') {
496
+ return {
497
+ ok: false,
498
+ error: `scale_up_split_target_proof_unavailable:${initialSplitTarget}:${initialSplitTargetProof.reason}`,
499
+ };
500
+ }
501
+ if (initialSplitTargetProof.pid !== initialExpectedSplitTargetPid) {
502
+ return {
503
+ ok: false,
504
+ error: `scale_up_split_target_pid_changed:${initialSplitTarget}:${initialExpectedSplitTargetPid}:${initialSplitTargetProof.pid}`,
505
+ };
258
506
  }
259
- if (context.workerName &&
260
- context.worktreePath &&
261
- !addedWorkers.some((worker) => worker.name === context.workerName)) {
262
- await removeWorkerWorktreeRootAgentsFile(sanitized, context.workerName, teamStateRoot, context.worktreePath).catch(() => { });
507
+ const effectiveWorktreeMode = config.worktree_mode ?? resolveScaleUpWorktreeMode(config);
508
+ if (!config.worktree_mode && effectiveWorktreeMode.enabled) {
509
+ config.worktree_mode = effectiveWorktreeMode;
263
510
  }
264
- if (context.paneId) {
511
+ const addedWorkers = [];
512
+ // A scale-up pane becomes killable during rollback only after its exact
513
+ // process identity was pinned and, when Team ownership is configured, its
514
+ // owner tag command completed. A returned pane ID alone is cleanup debt,
515
+ // never authority to affect a potentially recycled pane.
516
+ const rollbackTaggedPaneOwnerIds = new Map();
517
+ const createdTaskIds = [];
518
+ const provisionedWorktrees = [];
519
+ const preparedWorkerDirectoryOwner = new Map();
520
+ const preparedStartupScriptOwner = new Map();
521
+ const runtimeDirectoryPath = join(teamStateRoot, 'team', sanitized, 'runtime');
522
+ const runtimeDirectoryExisted = existsSync(runtimeDirectoryPath);
523
+ const rollbackScaleUp = async (error, context = {}) => {
524
+ const rollbackWorkers = [...new Map([...addedWorkers, context.worker]
525
+ .filter((worker) => Boolean(worker))
526
+ .map((worker) => [worker.name, worker])).values()];
527
+ const rollbackWorkerNames = new Set(rollbackWorkers.map((worker) => worker.name));
528
+ // Persist every concrete rollback worker before pane teardown. If the
529
+ // process exits after this point, canonical config/manifest state still
530
+ // identifies each live pane and its retryable resources.
265
531
  try {
266
- execFileSync('tmux', ['kill-pane', '-t', context.paneId], { stdio: 'pipe',
267
- windowsHide: true,
532
+ await withTaskMembershipBarrier(sanitized, leaderCwd, async () => {
533
+ await recoverTeamMembershipTaskTransaction(sanitized, leaderCwd);
534
+ const currentConfigBytes = await readFile(configPath, 'utf8');
535
+ const currentManifestBytes = existsSync(manifestPath) ? await readFile(manifestPath, 'utf8') : null;
536
+ const currentConfig = JSON.parse(currentConfigBytes);
537
+ const trackedByName = new Map(currentConfig.workers.map((worker) => [worker.name, worker]));
538
+ for (const worker of rollbackWorkers)
539
+ trackedByName.set(worker.name, worker);
540
+ const trackedConfig = {
541
+ ...currentConfig,
542
+ workers: [...trackedByName.values()],
543
+ worker_count: trackedByName.size,
544
+ next_worker_index: Math.max(currentConfig.next_worker_index ?? (currentConfig.workers.length + 1), ...rollbackWorkers.map((worker) => worker.index + 1)),
545
+ };
546
+ const trackedManifestBytes = currentManifestBytes === null
547
+ ? null
548
+ : JSON.stringify({
549
+ ...JSON.parse(currentManifestBytes),
550
+ workers: trackedConfig.workers,
551
+ worker_count: trackedConfig.worker_count,
552
+ next_worker_index: trackedConfig.next_worker_index,
553
+ }, null, 2);
554
+ await commitTeamMembershipTaskTransaction(sanitized, leaderCwd, {
555
+ baseGeneration: currentConfig.config_generation ?? 0,
556
+ tasks: [],
557
+ config: { oldBytes: currentConfigBytes, newBytes: JSON.stringify(trackedConfig, null, 2) },
558
+ manifest: { oldBytes: currentManifestBytes, newBytes: trackedManifestBytes },
559
+ recoverToNewOnFailure: true,
560
+ });
561
+ const verified = await readTeamConfig(sanitized, leaderCwd);
562
+ if (!verified || rollbackWorkers.some((worker) => !verified.workers.some((entry) => entry.name === worker.name && entry.pane_id === worker.pane_id))) {
563
+ throw new Error('canonical_scale_up_rollback_tracking_verification_failed');
564
+ }
565
+ Object.assign(config, verified);
268
566
  });
269
567
  }
270
- catch { }
271
- }
272
- for (const taskId of createdTaskIds) {
273
- await rm(join(leaderCwd, '.omx', 'state', 'team', sanitized, 'tasks', `task-${taskId}.json`), { force: true }).catch(() => { });
274
- }
275
- config.worker_count = config.workers.length;
276
- config.next_worker_index = initialNextIndex;
277
- await saveTeamConfig(config, leaderCwd);
278
- return { ok: false, error };
279
- };
280
- // Persist incoming tasks only after launch policy is frozen; the resulting
281
- // task listing is used for inbox and task materialization, never launch policy.
282
- for (const task of tasks) {
283
- const createdTask = await createStateTask(sanitized, {
284
- subject: task.subject,
285
- description: task.description,
286
- status: 'pending',
287
- owner: task.owner,
288
- blocked_by: task.blocked_by,
289
- role: task.role,
290
- }, leaderCwd);
291
- createdTaskIds.push(createdTask.id);
292
- }
293
- const materializedTasks = await listTasks(sanitized, leaderCwd);
294
- for (const workerLaunchPlan of workerLaunchPlans) {
295
- const { workerIndex, workerName, runtimeRole, workerLaunchArgs, workerCli, } = workerLaunchPlan;
296
- nextIndex = workerIndex + 1;
297
- if (workerLaunchPlan.mixedTaskRoles.length > 1) {
298
- console.log(`[omx:scaling] ${workerName}: mixed task roles [${workerLaunchPlan.mixedTaskRoles.join(', ')}], falling back to ${agentType}`);
299
- }
300
- // Create worker directory
301
- const workerDirPath = join(leaderCwd, '.omx', 'state', 'team', sanitized, 'workers', workerName);
302
- await mkdir(workerDirPath, { recursive: true });
303
- const worktreeMode = effectiveWorktreeMode;
304
- const workerWorkspaceResult = worktreeMode.enabled
305
- ? ensureWorktree(planWorktreeTarget({
306
- cwd: leaderCwd,
307
- scope: 'team',
308
- mode: worktreeMode,
309
- teamName: sanitized,
310
- workerName,
311
- }))
312
- : { enabled: false };
313
- const workerWorkspace = workerWorkspaceResult.enabled ? workerWorkspaceResult : null;
314
- const workerCwd = workerWorkspace ? workerWorkspace.worktreePath : leaderCwd;
315
- // Build startup command and create tmux pane
316
- const rawRolePromptContent = await loadRolePrompt(runtimeRole, join(leaderCwd, '.codex', 'prompts'))
317
- ?? await loadRolePrompt(runtimeRole, codexPromptsDir());
318
- const resolvedWorkerModel = parseTeamWorkerLaunchArgs(workerLaunchArgs).modelOverride ?? undefined;
319
- const rolePromptContent = rawRolePromptContent
320
- ? composeRoleInstructionsForRole(runtimeRole, rawRolePromptContent, resolvedWorkerModel)
321
- : null;
322
- const teamInstructionsPath = join(leaderCwd, '.omx', 'state', 'team', sanitized, 'worker-agents.md');
323
- const instructionsFilePath = workerWorkspace
324
- ? await writeWorkerWorktreeRootAgentsFile({
325
- teamName: sanitized,
326
- workerName,
327
- workerRole: runtimeRole,
328
- rolePromptContent: rolePromptContent ?? '',
329
- teamStateRoot,
330
- leaderCwd,
331
- worktreePath: workerWorkspace.worktreePath,
332
- })
333
- : rolePromptContent
334
- ? await writeWorkerRoleInstructionsFile(sanitized, workerName, leaderCwd, teamInstructionsPath, runtimeRole, rolePromptContent)
335
- : teamInstructionsPath;
336
- const extraEnv = {
337
- OMX_TEAM_STATE_ROOT: teamStateRoot,
338
- OMX_TEAM_LEADER_CWD: leaderCwd,
339
- OMX_MODEL_INSTRUCTIONS_FILE: instructionsFilePath,
340
- ...(codexHomeOverride ? { CODEX_HOME: codexHomeOverride } : {}),
568
+ catch (trackingError) {
569
+ return { ok: false, error: `scale_up_rollback_membership_persistence_failed:${String(trackingError)}` };
570
+ }
571
+ const cleanupDebt = [];
572
+ try {
573
+ await removeDispatchRequestsForWorkers(sanitized, [...rollbackWorkerNames], leaderCwd);
574
+ }
575
+ catch (rollbackError) {
576
+ cleanupDebt.push(`authoritative_dispatch_cleanup_failed:${String(rollbackError)}`);
577
+ }
578
+ const rollbackPaneIds = [...new Set([
579
+ ...rollbackWorkers.map((worker) => worker.pane_id),
580
+ context.paneId,
581
+ ].filter((paneId) => typeof paneId === 'string' && paneId.trim().startsWith('%')))];
582
+ const resolvedPaneIds = new Set();
583
+ const unresolvedPaneIds = new Set();
584
+ const requiredTeamOwnerId = config.tmux_pane_owner_id?.trim();
585
+ const authorizedRollbackPaneIds = new Set();
586
+ const rollbackInstances = new Map();
587
+ for (const paneId of rollbackPaneIds) {
588
+ const matchingWorkers = rollbackWorkers.filter((worker) => worker.pane_id === paneId);
589
+ const worker = matchingWorkers.length === 1 ? matchingWorkers[0] : undefined;
590
+ const expectedPanePid = worker?.pid;
591
+ if (!worker || typeof expectedPanePid !== 'number' || !Number.isInteger(expectedPanePid) || expectedPanePid <= 0) {
592
+ unresolvedPaneIds.add(paneId);
593
+ cleanupDebt.push(`pane_pid_unpinned:${paneId}`);
594
+ continue;
595
+ }
596
+ if (requiredTeamOwnerId && rollbackTaggedPaneOwnerIds.get(paneId) !== requiredTeamOwnerId) {
597
+ unresolvedPaneIds.add(paneId);
598
+ cleanupDebt.push(`pane_owner_unverified:${paneId}`);
599
+ continue;
600
+ }
601
+ const expectedWindowId = worker.window_id;
602
+ const expectedSessionId = worker.session_id;
603
+ if (typeof expectedWindowId !== 'string' || !/^@[0-9]+$/.test(expectedWindowId)
604
+ || typeof expectedSessionId !== 'string' || expectedSessionId.trim() === '') {
605
+ unresolvedPaneIds.add(paneId);
606
+ cleanupDebt.push(`pane_window_unpinned:${paneId}`);
607
+ continue;
608
+ }
609
+ const rollbackInstance = readScalePaneInstance(paneId);
610
+ if (!rollbackInstance
611
+ || rollbackInstance.pid !== expectedPanePid
612
+ || rollbackInstance.sessionName !== config.tmux_session
613
+ || rollbackInstance.sessionId !== expectedSessionId
614
+ || rollbackInstance.windowId !== expectedWindowId
615
+ || (requiredTeamOwnerId && rollbackInstance.teamOwnerId !== requiredTeamOwnerId)) {
616
+ unresolvedPaneIds.add(paneId);
617
+ cleanupDebt.push(`pane_window_authority_lost:${paneId}`);
618
+ continue;
619
+ }
620
+ rollbackInstances.set(paneId, rollbackInstance);
621
+ authorizedRollbackPaneIds.add(paneId);
622
+ }
623
+ for (const paneId of authorizedRollbackPaneIds) {
624
+ const instance = rollbackInstances.get(paneId);
625
+ if (!instance || !killScalePaneWithAuthority(instance)) {
626
+ unresolvedPaneIds.add(paneId);
627
+ cleanupDebt.push(`pane_teardown_authority_lost:${paneId}`);
628
+ continue;
629
+ }
630
+ resolvedPaneIds.add(paneId);
631
+ }
632
+ const unresolvedWorkerNames = new Set(rollbackWorkers
633
+ .filter((worker) => typeof worker.pane_id === 'string' && unresolvedPaneIds.has(worker.pane_id))
634
+ .map((worker) => worker.name));
635
+ if (context.workerName && context.paneId && unresolvedPaneIds.has(context.paneId)) {
636
+ unresolvedWorkerNames.add(context.workerName);
637
+ }
638
+ try {
639
+ await withTaskMembershipBarrier(sanitized, leaderCwd, async () => {
640
+ await recoverTeamMembershipTaskTransaction(sanitized, leaderCwd);
641
+ const currentConfigBytes = await readFile(configPath, 'utf8');
642
+ const currentConfig = JSON.parse(currentConfigBytes);
643
+ const currentManifestBytes = existsSync(manifestPath) ? await readFile(manifestPath, 'utf8') : null;
644
+ const retainedWorkers = rollbackWorkers.filter((worker) => unresolvedWorkerNames.has(worker.name));
645
+ const desiredByName = new Map(originalConfig.workers.map((worker) => [worker.name, worker]));
646
+ for (const worker of retainedWorkers)
647
+ desiredByName.set(worker.name, worker);
648
+ const desiredConfig = {
649
+ ...originalConfig,
650
+ workers: [...desiredByName.values()],
651
+ worker_count: desiredByName.size,
652
+ next_worker_index: Math.max(originalConfig.next_worker_index ?? (originalConfig.workers.length + 1), ...retainedWorkers.map((worker) => worker.index + 1)),
653
+ };
654
+ const desiredManifestBytes = originalManifestBytes === null
655
+ ? null
656
+ : JSON.stringify({
657
+ ...JSON.parse(originalManifestBytes),
658
+ workers: desiredConfig.workers,
659
+ worker_count: desiredConfig.worker_count,
660
+ next_worker_index: desiredConfig.next_worker_index,
661
+ }, null, 2);
662
+ const taskChanges = [];
663
+ for (const taskId of createdTaskIds) {
664
+ const taskPath = join(teamStateRoot, 'team', sanitized, 'tasks', `task-${taskId}.json`);
665
+ const task = await readTask(sanitized, taskId, leaderCwd);
666
+ taskChanges.push({
667
+ taskId,
668
+ oldBytes: existsSync(taskPath) ? await readFile(taskPath, 'utf8') : null,
669
+ newBytes: task && unresolvedWorkerNames.has(task.owner ?? '')
670
+ ? JSON.stringify(task, null, 2)
671
+ : null,
672
+ });
673
+ }
674
+ await commitTeamMembershipTaskTransaction(sanitized, leaderCwd, {
675
+ baseGeneration: currentConfig.config_generation ?? 0,
676
+ tasks: taskChanges,
677
+ config: { oldBytes: currentConfigBytes, newBytes: JSON.stringify(desiredConfig, null, 2) },
678
+ manifest: { oldBytes: currentManifestBytes, newBytes: desiredManifestBytes },
679
+ recoverToNewOnFailure: true,
680
+ retainJournalOnSuccess: true,
681
+ failRollbackPersistence: hasScaleUpFailureInjection(env, 'rollback-membership-persistence'),
682
+ failRollbackPersistenceAfter: hasScaleUpFailureInjection(env, 'rollback-membership-config-persistence')
683
+ ? 'config'
684
+ : hasScaleUpFailureInjection(env, 'rollback-membership-manifest-persistence')
685
+ ? 'manifest'
686
+ : undefined,
687
+ });
688
+ const verified = JSON.parse(await readFile(configPath, 'utf8'));
689
+ if (!verified || retainedWorkers.some((worker) => !verified.workers.some((entry) => entry.name === worker.name && entry.pane_id === worker.pane_id))) {
690
+ throw new Error('canonical_scale_up_rollback_membership_verification_failed');
691
+ }
692
+ if (verified.workers.some((worker) => rollbackWorkerNames.has(worker.name) && !unresolvedWorkerNames.has(worker.name))) {
693
+ throw new Error('canonical_scale_up_rollback_resolved_membership_verification_failed');
694
+ }
695
+ await finalizeTeamMembershipTaskTransaction(sanitized, leaderCwd);
696
+ Object.assign(config, verified);
697
+ });
698
+ }
699
+ catch (rollbackError) {
700
+ const journalPath = join(teamStateRoot, 'team', sanitized, '.membership-task-transaction.json');
701
+ const suffix = existsSync(journalPath) ? String(rollbackError) : `no_recoverable_journal:${String(rollbackError)}`;
702
+ return { ok: false, error: `scale_up_rollback_membership_persistence_failed:${suffix}` };
703
+ }
704
+ const cleanupWorkerNames = new Set([
705
+ ...rollbackWorkers
706
+ .filter((worker) => typeof worker.pane_id !== 'string' || resolvedPaneIds.has(worker.pane_id))
707
+ .map((worker) => worker.name),
708
+ ...(context.workerName && !unresolvedWorkerNames.has(context.workerName) ? [context.workerName] : []),
709
+ ]);
710
+ try {
711
+ for (const taskId of createdTaskIds) {
712
+ const task = await readTask(sanitized, taskId, leaderCwd);
713
+ if (task && unresolvedWorkerNames.has(task.owner ?? ''))
714
+ continue;
715
+ await rm(join(teamStateRoot, 'team', sanitized, 'tasks', `task-${taskId}.json`), { force: true });
716
+ }
717
+ await Promise.all([...cleanupWorkerNames].map(async (workerName) => {
718
+ await rm(join(teamStateRoot, 'team', sanitized, 'workers', workerName), { recursive: true, force: true });
719
+ }));
720
+ for (const taskId of createdTaskIds) {
721
+ const task = await readTask(sanitized, taskId, leaderCwd);
722
+ if (task && unresolvedWorkerNames.has(task.owner ?? ''))
723
+ continue;
724
+ if (task)
725
+ throw new Error(`canonical_scale_up_rollback_task_verification_failed:${taskId}`);
726
+ }
727
+ for (const workerName of cleanupWorkerNames) {
728
+ if (existsSync(join(teamStateRoot, 'team', sanitized, 'workers', workerName))) {
729
+ throw new Error(`canonical_scale_up_rollback_worker_verification_failed:${workerName}`);
730
+ }
731
+ }
732
+ }
733
+ catch (rollbackError) {
734
+ cleanupDebt.push(`canonical_cleanup_failed:${String(rollbackError)}`);
735
+ }
736
+ try {
737
+ const contextWorkerName = context.worker?.name ?? context.workerName;
738
+ const contextWorktreePath = context.worker?.worktree_path ?? context.worktreePath;
739
+ if (contextWorkerName && contextWorktreePath && !unresolvedWorkerNames.has(contextWorkerName)) {
740
+ await removeWorkerWorktreeRootAgentsFile(sanitized, contextWorkerName, teamStateRoot, contextWorktreePath);
741
+ }
742
+ const unresolvedWorktreePaths = new Set(rollbackWorkers
743
+ .filter((worker) => unresolvedWorkerNames.has(worker.name) && typeof worker.worktree_path === 'string')
744
+ .map((worker) => resolve(worker.worktree_path)));
745
+ await rollbackProvisionedWorktrees(provisionedWorktrees.filter((worktree) => !unresolvedWorktreePaths.has(resolve(worktree.worktreePath))));
746
+ await Promise.all([...preparedWorkerDirectoryOwner.entries()]
747
+ .filter(([, workerName]) => !unresolvedWorkerNames.has(workerName))
748
+ .map(async ([workerDirPath]) => {
749
+ await rm(workerDirPath, { recursive: true, force: true });
750
+ }));
751
+ await Promise.all([...preparedStartupScriptOwner.entries()]
752
+ .filter(([, workerName]) => !unresolvedWorkerNames.has(workerName))
753
+ .map(async ([startupScriptPath]) => {
754
+ await rm(startupScriptPath, { force: true });
755
+ }));
756
+ if (!runtimeDirectoryExisted && unresolvedWorkerNames.size === 0)
757
+ await rm(runtimeDirectoryPath, { recursive: true, force: true });
758
+ }
759
+ catch (cleanupError) {
760
+ cleanupDebt.push(`resource_cleanup_failed:${String(cleanupError)}`);
761
+ }
762
+ if (cleanupDebt.length > 0) {
763
+ const reason = `scale_up_rollback_cleanup_debt:${cleanupDebt.join(';')}; retry cleanup for [${[...rollbackWorkerNames].join(',')}]`;
764
+ try {
765
+ await appendTeamEvent(sanitized, { type: 'team_leader_nudge', worker: 'leader-fixed', reason }, leaderCwd);
766
+ }
767
+ catch (eventError) {
768
+ cleanupDebt.push(`cleanup_debt_event_failed:${String(eventError)}`);
769
+ }
770
+ return { ok: false, error: `scale_up_rollback_cleanup_debt:${cleanupDebt.join(';')}` };
771
+ }
772
+ return { ok: false, error };
341
773
  };
342
- if (workerWorkspace) {
343
- extraEnv.OMX_TEAM_WORKTREE_PATH = workerWorkspace.worktreePath;
344
- if (workerWorkspace.branchName) {
345
- extraEnv.OMX_TEAM_WORKTREE_BRANCH = workerWorkspace.branchName;
346
- }
347
- extraEnv.OMX_TEAM_WORKTREE_DETACHED = workerWorkspace.detached ? '1' : '0';
348
- }
349
- trustWorkerMiseConfigIfAvailable(workerCwd);
350
- const cmd = writeWorkerStartupScriptCommand(sanitized, workerIndex, workerLaunchArgs, workerCwd, extraEnv, workerCli, undefined, runtimeRole) ?? buildWorkerStartupCommand(sanitized, workerIndex, workerLaunchArgs, workerCwd, extraEnv, workerCli, undefined, runtimeRole);
351
- // Find the right-most worker pane to split from, or fall back to leader pane.
352
- // Keep the initial split from leader horizontal to preserve the leader-left
353
- // / workers-right composition.
354
- const splitTarget = config.workers.length > 0
355
- ? (config.workers[config.workers.length - 1]?.pane_id ?? config.leader_pane_id ?? '')
356
- : (config.leader_pane_id ?? '');
357
- const splitDirection = splitTarget === (config.leader_pane_id ?? '') ? '-h' : '-v';
358
- const result = spawnSync('tmux', [
359
- 'split-window', splitDirection, '-t', splitTarget, '-d', '-P', '-F', '#{pane_id}', '-c', workerCwd, cmd,
360
- ], { encoding: 'utf-8' });
361
- if (result.status !== 0) {
362
- return await rollbackScaleUp(`Failed to create tmux pane for ${workerName}: ${(result.stderr || '').trim()}`, { workerName, worktreePath: workerWorkspace?.worktreePath });
363
- }
364
- const paneId = (result.stdout || '').trim().split('\n')[0]?.trim();
365
- if (!paneId || !paneId.startsWith('%')) {
366
- return await rollbackScaleUp(`Failed to capture pane ID for ${workerName}`, {
367
- paneId,
368
- workerName,
369
- worktreePath: workerWorkspace?.worktreePath,
370
- });
774
+ // Persist incoming tasks only after launch policy is frozen; the resulting
775
+ // task listing is used for inbox and task materialization, never launch policy.
776
+ let materializedTasks;
777
+ try {
778
+ for (const task of tasks) {
779
+ const createdTask = await createStateTask(sanitized, {
780
+ subject: task.subject,
781
+ description: task.description,
782
+ status: 'pending',
783
+ owner: task.owner,
784
+ blocked_by: task.blocked_by,
785
+ role: task.role,
786
+ }, leaderCwd);
787
+ createdTaskIds.push(createdTask.id);
788
+ }
789
+ materializedTasks = await listTasks(sanitized, leaderCwd);
371
790
  }
372
- if (config.tmux_pane_owner_id) {
791
+ catch (error) {
792
+ return await rollbackScaleUp(`scale_up_task_materialization_failed:${error instanceof Error ? error.message : String(error)}`);
793
+ }
794
+ for (const workerLaunchPlan of workerLaunchPlans) {
795
+ const { workerIndex, workerName, runtimeRole, workerLaunchArgs, workerCli, } = workerLaunchPlan;
796
+ // Freeze the split target and its identity before preparing any artifacts.
797
+ // Later proof must establish that this exact pane process still owns the
798
+ // target; a recycled pane ID must never become split authority.
799
+ const splitTargetCandidate = config.workers[config.workers.length - 1];
800
+ const splitTargetWorker = splitTargetCandidate?.pane_id?.trim() ? splitTargetCandidate : undefined;
801
+ const splitTarget = splitTargetWorker?.pane_id ?? config.leader_pane_id ?? '';
802
+ const expectedSplitTargetPid = splitTargetWorker
803
+ ? splitTargetWorker.pid
804
+ : config.leader_pane_pid;
805
+ const expectedSplitTargetOwnerId = typeof config.tmux_pane_owner_id === 'string'
806
+ ? config.tmux_pane_owner_id.trim()
807
+ : '';
808
+ if (typeof expectedSplitTargetPid !== 'number'
809
+ || !Number.isSafeInteger(expectedSplitTargetPid)
810
+ || expectedSplitTargetPid <= 0) {
811
+ return await rollbackScaleUp(`scale_up_split_target_pid_missing:${splitTarget}`);
812
+ }
813
+ if (!expectedSplitTargetOwnerId) {
814
+ return await rollbackScaleUp(`scale_up_split_target_owner_unavailable:${splitTarget}`);
815
+ }
816
+ const splitDirection = splitTarget === (config.leader_pane_id ?? '') ? '-h' : '-v';
817
+ nextIndex = workerIndex + 1;
818
+ if (workerLaunchPlan.mixedTaskRoles.length > 1) {
819
+ console.log(`[omx:scaling] ${workerName}: mixed task roles [${workerLaunchPlan.mixedTaskRoles.join(', ')}], falling back to ${agentType}`);
820
+ }
821
+ // Prepare every pre-pane artifact under the rollback boundary. A pane is
822
+ // not required for ownership: workerName and any created worktree are
823
+ // sufficient for rollback to clean a failed preparation.
824
+ const workerDirPath = join(leaderCwd, '.omx', 'state', 'team', sanitized, 'workers', workerName);
825
+ const startupScriptPath = join(teamStateRoot, 'team', sanitized, 'runtime', `worker-${workerIndex}-startup.sh`);
826
+ let workerWorkspace = null;
827
+ let workerCwd = leaderCwd;
828
+ let cmd;
829
+ let rawRolePromptContent = null;
373
830
  try {
374
- tagPaneTeamOwner(paneId, config.tmux_pane_owner_id);
831
+ preparedWorkerDirectoryOwner.set(workerDirPath, workerName);
832
+ await mkdir(workerDirPath, { recursive: true });
833
+ if (effectiveWorktreeMode.enabled) {
834
+ const worktreePlan = planWorktreeTarget({
835
+ cwd: leaderCwd,
836
+ scope: 'team',
837
+ mode: effectiveWorktreeMode,
838
+ teamName: sanitized,
839
+ workerName,
840
+ });
841
+ if (!worktreePlan.enabled)
842
+ throw new Error(`worktree_not_planned:${workerName}`);
843
+ const worktreePathExistedBeforeEnsure = existsSync(worktreePlan.worktreePath);
844
+ const branchExistedBeforeEnsure = worktreePlan.branchName
845
+ ? spawnSync('git', ['show-ref', '--verify', '--quiet', `refs/heads/${worktreePlan.branchName}`], {
846
+ cwd: worktreePlan.repoRoot,
847
+ encoding: 'utf-8',
848
+ windowsHide: true,
849
+ }).status === 0
850
+ : false;
851
+ try {
852
+ const ensuredWorkspace = ensureWorktree(worktreePlan);
853
+ throwIfScaleUpFailureInjected(env, 'worktree-ensure-post-create');
854
+ if (!ensuredWorkspace.enabled)
855
+ throw new Error(`worktree_not_provisioned:${workerName}`);
856
+ workerWorkspace = ensuredWorkspace;
857
+ }
858
+ catch (error) {
859
+ const recoveredWorkspace = recoverCreatedWorktreeAfterEnsureFailure(worktreePlan, worktreePathExistedBeforeEnsure, branchExistedBeforeEnsure);
860
+ if (recoveredWorkspace) {
861
+ workerWorkspace = recoveredWorkspace;
862
+ provisionedWorktrees.push(recoveredWorkspace);
863
+ }
864
+ throw error;
865
+ }
866
+ if (!workerWorkspace)
867
+ throw new Error(`worktree_not_provisioned:${workerName}`);
868
+ provisionedWorktrees.push(workerWorkspace);
869
+ throwIfScaleUpFailureInjected(env, 'worktree-post-create');
870
+ workerCwd = workerWorkspace.worktreePath;
871
+ }
872
+ rawRolePromptContent = await loadRolePrompt(runtimeRole, join(leaderCwd, '.codex', 'prompts'))
873
+ ?? await loadRolePrompt(runtimeRole, codexPromptsDir());
874
+ const resolvedWorkerModel = parseTeamWorkerLaunchArgs(workerLaunchArgs).modelOverride ?? undefined;
875
+ const rolePromptContent = rawRolePromptContent
876
+ ? composeRoleInstructionsForRole(runtimeRole, rawRolePromptContent, resolvedWorkerModel)
877
+ : null;
878
+ const teamInstructionsPath = join(leaderCwd, '.omx', 'state', 'team', sanitized, 'worker-agents.md');
879
+ const instructionsFilePath = workerWorkspace
880
+ ? await writeWorkerWorktreeRootAgentsFile({
881
+ teamName: sanitized,
882
+ workerName,
883
+ workerRole: runtimeRole,
884
+ rolePromptContent: rolePromptContent ?? '',
885
+ teamStateRoot,
886
+ leaderCwd,
887
+ worktreePath: workerWorkspace.worktreePath,
888
+ })
889
+ : rolePromptContent
890
+ ? await writeWorkerRoleInstructionsFile(sanitized, workerName, leaderCwd, teamInstructionsPath, runtimeRole, rolePromptContent)
891
+ : teamInstructionsPath;
892
+ const extraEnv = {
893
+ OMX_TEAM_STATE_ROOT: teamStateRoot,
894
+ OMX_TEAM_LEADER_CWD: leaderCwd,
895
+ OMX_MODEL_INSTRUCTIONS_FILE: instructionsFilePath,
896
+ ...(codexHomeOverride ? { CODEX_HOME: codexHomeOverride } : {}),
897
+ };
898
+ if (workerWorkspace) {
899
+ extraEnv.OMX_TEAM_WORKTREE_PATH = workerWorkspace.worktreePath;
900
+ if (workerWorkspace.branchName) {
901
+ extraEnv.OMX_TEAM_WORKTREE_BRANCH = workerWorkspace.branchName;
902
+ }
903
+ extraEnv.OMX_TEAM_WORKTREE_DETACHED = workerWorkspace.detached ? '1' : '0';
904
+ }
905
+ trustWorkerMiseConfigIfAvailable(workerCwd);
906
+ preparedStartupScriptOwner.set(startupScriptPath, workerName);
907
+ const startupCommand = writeWorkerStartupScriptCommand(sanitized, workerIndex, workerLaunchArgs, workerCwd, extraEnv, workerCli, undefined, runtimeRole);
908
+ cmd = startupCommand ?? buildWorkerStartupCommand(sanitized, workerIndex, workerLaunchArgs, workerCwd, extraEnv, workerCli, undefined, runtimeRole);
375
909
  }
376
910
  catch (error) {
377
- return await rollbackScaleUp(`Failed to tag tmux pane for ${workerName}: ${error instanceof Error ? error.message : String(error)}`, { paneId, workerName, worktreePath: workerWorkspace?.worktreePath });
378
- }
379
- }
380
- // Intentionally avoid forcing `select-layout tiled` here.
381
- // Tiled relayout reflows leader/HUD panes and breaks team window layout.
382
- // Get PID
383
- const panePid = getWorkerPanePid(sessionName, workerIndex, paneId);
384
- const workerInfo = {
385
- name: workerName,
386
- index: workerIndex,
387
- role: runtimeRole,
388
- worker_cli: workerCli,
389
- assigned_tasks: [],
390
- pid: panePid ?? undefined,
391
- pane_id: paneId,
392
- working_dir: workerCwd,
393
- worktree_repo_root: workerWorkspace ? workerWorkspace.repoRoot : undefined,
394
- worktree_path: workerWorkspace ? workerWorkspace.worktreePath : undefined,
395
- worktree_branch: workerWorkspace ? (workerWorkspace.branchName ?? undefined) : undefined,
396
- worktree_detached: workerWorkspace ? workerWorkspace.detached : undefined,
397
- worktree_created: workerWorkspace ? workerWorkspace.created : undefined,
398
- team_state_root: teamStateRoot,
399
- };
400
- await writeWorkerIdentity(sanitized, workerName, workerInfo, leaderCwd);
401
- // Wait for worker readiness
402
- const readyTimeoutMs = resolveWorkerReadyTimeoutMs(env);
403
- const skipReadyWait = env.OMX_TEAM_SKIP_READY_WAIT === '1';
404
- if (!skipReadyWait) {
405
- const ready = waitForWorkerReady(sessionName, workerIndex, readyTimeoutMs, paneId);
406
- if (!ready) {
407
- console.log(`[omx:scaling] Warning: worker ${workerName} did not become ready within timeout`);
408
- }
409
- }
410
- // Get assigned tasks for this worker
411
- const workerTasks = materializedTasks.filter(t => t.owner === workerName);
412
- const inbox = generateInitialInbox(workerName, sanitized, agentType, workerTasks, {
413
- teamStateRoot,
414
- leaderCwd,
415
- workerRole: runtimeRole,
416
- rolePromptContent: rawRolePromptContent ?? undefined,
417
- worktreeRootAgentsCanonical: Boolean(workerWorkspace?.worktreePath),
418
- approvedContextSection,
419
- workerGoalInstruction: buildTeamWorkerGoalInstruction(sanitized, workerName, workerTasks, { teamStateRoot }),
420
- });
421
- const triggerDirective = buildTriggerDirective(workerName, sanitized, resolveInstructionStateRoot(workerInfo.worktree_path));
422
- const queued = await queueInboxInstruction({
423
- teamName: sanitized,
424
- workerName,
425
- workerIndex,
426
- paneId,
427
- inbox,
428
- triggerMessage: triggerDirective.text,
429
- intent: triggerDirective.intent,
430
- cwd: leaderCwd,
431
- transportPreference: dispatchPolicy.dispatch_mode,
432
- fallbackAllowed: true,
433
- inboxCorrelationKey: `scale_up:${workerName}`,
434
- notify: async (_target, message) => {
435
- if (dispatchPolicy.dispatch_mode === 'hook_preferred_with_fallback') {
436
- return { ok: true, transport: 'hook', reason: 'queued_for_hook_dispatch' };
437
- }
438
- return await notifyWorkerPaneOutcome(sessionName, workerIndex, message, paneId, workerCli);
439
- },
440
- });
441
- let outcome = queued;
442
- if (dispatchPolicy.dispatch_mode === 'hook_preferred_with_fallback' && queued.request_id) {
443
- const receipt = await waitForDispatchReceipt(sanitized, queued.request_id, leaderCwd, {
444
- timeoutMs: dispatchPolicy.dispatch_ack_timeout_ms,
445
- pollMs: 50,
446
- });
447
- if (receipt && (receipt.status === 'notified' || receipt.status === 'delivered')) {
448
- outcome = { ok: true, transport: 'hook', reason: `hook_receipt_${receipt.status}`, request_id: queued.request_id };
911
+ return await rollbackScaleUp(`scale_up_worker_preparation_failed:${workerName}:${error instanceof Error ? error.message : String(error)}`, { workerName, worktreePath: workerWorkspace?.worktreePath });
449
912
  }
450
- else {
451
- const fallback = await notifyWorkerPaneOutcome(sessionName, workerIndex, triggerDirective.text, paneId, workerCli);
452
- if (receipt?.status === 'failed') {
453
- if (fallback.ok) {
454
- await transitionDispatchRequest(sanitized, queued.request_id, 'failed', 'failed', { last_reason: `fallback_confirmed_after_failed_receipt:${fallback.reason}` }, leaderCwd).catch(() => { });
455
- outcome = {
456
- ok: true,
457
- transport: fallback.transport,
458
- reason: `fallback_confirmed_after_failed_receipt:${fallback.reason}`,
459
- request_id: queued.request_id,
460
- };
913
+ // Freeze and validate the full source-pane incarnation. The server owns
914
+ // the final comparison and split, so a recycled pane ID cannot become a
915
+ // split target between client-side proof and the topology mutation.
916
+ const splitTargetProof = readExactPaneProofSync(splitTarget);
917
+ const splitTargetInstance = splitTargetProof.status === 'live'
918
+ ? readScalePaneInstance(splitTargetProof.paneId)
919
+ : null;
920
+ if (!splitTargetInstance) {
921
+ return await rollbackScaleUp(`scale_up_split_target_proof_unavailable:${splitTarget}`, { workerName, worktreePath: workerWorkspace?.worktreePath });
922
+ }
923
+ if (splitTargetInstance.pid !== expectedSplitTargetPid) {
924
+ return await rollbackScaleUp(`scale_up_split_target_pid_changed:${splitTarget}:${expectedSplitTargetPid}:${splitTargetInstance.pid}`, { workerName, worktreePath: workerWorkspace?.worktreePath });
925
+ }
926
+ if (splitTargetInstance.sessionName !== sessionName
927
+ || (config.tmux_session_id && splitTargetInstance.sessionId !== config.tmux_session_id)
928
+ || splitTargetInstance.teamOwnerId !== expectedSplitTargetOwnerId) {
929
+ return await rollbackScaleUp(`scale_up_split_target_authority_changed:${splitTargetInstance.paneId}`, { workerName, worktreePath: workerWorkspace?.worktreePath });
930
+ }
931
+ const createdPane = splitWorkerPaneWithScaleAuthority(splitTargetInstance, splitDirection, workerCwd, cmd);
932
+ if (!createdPane) {
933
+ return await rollbackScaleUp(`scale_up_split_target_authority_lost:${splitTargetInstance.paneId}`, { workerName, worktreePath: workerWorkspace?.worktreePath });
934
+ }
935
+ const paneId = createdPane.paneId;
936
+ const workerInfo = {
937
+ name: workerName,
938
+ index: workerIndex,
939
+ role: runtimeRole,
940
+ worker_cli: workerCli,
941
+ assigned_tasks: [],
942
+ pid: createdPane.pid,
943
+ pane_id: paneId,
944
+ session_id: createdPane.sessionId,
945
+ window_id: createdPane.windowId,
946
+ working_dir: workerCwd,
947
+ worktree_repo_root: workerWorkspace ? workerWorkspace.repoRoot : undefined,
948
+ worktree_path: workerWorkspace ? workerWorkspace.worktreePath : undefined,
949
+ worktree_branch: workerWorkspace ? (workerWorkspace.branchName ?? undefined) : undefined,
950
+ worktree_detached: workerWorkspace ? workerWorkspace.detached : undefined,
951
+ worktree_created: workerWorkspace ? workerWorkspace.created : undefined,
952
+ team_state_root: teamStateRoot,
953
+ };
954
+ const taggedPane = config.tmux_pane_owner_id
955
+ ? tagScalePaneWithAuthority(createdPane, config.tmux_pane_owner_id.trim())
956
+ : null;
957
+ if (!taggedPane) {
958
+ return await rollbackScaleUp(`scale_up_created_pane_owner_tag_authority_lost:${paneId}`, { paneId, worker: workerInfo });
959
+ }
960
+ rollbackTaggedPaneOwnerIds.set(paneId, taggedPane.teamOwnerId);
961
+ // Intentionally avoid forcing `select-layout tiled` here.
962
+ // Tiled relayout reflows leader/HUD panes and breaks team window layout.
963
+ try {
964
+ await writeWorkerIdentity(sanitized, workerName, workerInfo, leaderCwd);
965
+ throwIfScaleUpFailureInjected(env, 'identity');
966
+ // Wait for worker readiness
967
+ const readyTimeoutMs = resolveWorkerReadyTimeoutMs(env);
968
+ const skipReadyWait = env.OMX_TEAM_SKIP_READY_WAIT === '1';
969
+ if (!skipReadyWait) {
970
+ const ready = waitForWorkerReady(sessionName, workerIndex, readyTimeoutMs, paneId, workerInfo.pid, config.tmux_pane_owner_id ?? undefined, config.hud_pane_id ?? undefined);
971
+ if (!ready) {
972
+ console.log(`[omx:scaling] Warning: worker ${workerName} did not become ready within timeout`);
973
+ }
974
+ }
975
+ // Get assigned tasks for this worker
976
+ const workerTasks = materializedTasks.filter(t => t.owner === workerName);
977
+ const inbox = generateInitialInbox(workerName, sanitized, agentType, workerTasks, {
978
+ teamStateRoot,
979
+ leaderCwd,
980
+ workerRole: runtimeRole,
981
+ rolePromptContent: rawRolePromptContent ?? undefined,
982
+ worktreeRootAgentsCanonical: Boolean(workerWorkspace?.worktreePath),
983
+ approvedContextSection,
984
+ workerGoalInstruction: buildTeamWorkerGoalInstruction(sanitized, workerName, workerTasks, { teamStateRoot }),
985
+ });
986
+ throwIfScaleUpFailureInjected(env, 'inbox');
987
+ const triggerDirective = buildTriggerDirective(workerName, sanitized, resolveInstructionStateRoot(workerInfo.worktree_path));
988
+ const queued = await queueInboxInstruction({
989
+ teamName: sanitized,
990
+ workerName,
991
+ workerIndex,
992
+ paneId,
993
+ inbox,
994
+ triggerMessage: triggerDirective.text,
995
+ intent: triggerDirective.intent,
996
+ cwd: leaderCwd,
997
+ transportPreference: dispatchPolicy.dispatch_mode,
998
+ fallbackAllowed: true,
999
+ inboxCorrelationKey: `scale_up:${workerName}`,
1000
+ notify: async (_target, message) => {
1001
+ if (dispatchPolicy.dispatch_mode === 'hook_preferred_with_fallback') {
1002
+ return { ok: true, transport: 'hook', reason: 'queued_for_hook_dispatch' };
1003
+ }
1004
+ return await notifyWorkerPaneOutcome(sessionName, message, paneId, workerInfo.pid, config.tmux_pane_owner_id ?? undefined, workerInfo.session_id, workerInfo.window_id);
1005
+ },
1006
+ });
1007
+ let outcome = queued;
1008
+ if (dispatchPolicy.dispatch_mode === 'hook_preferred_with_fallback' && queued.request_id) {
1009
+ const receipt = await waitForDispatchReceipt(sanitized, queued.request_id, leaderCwd, {
1010
+ timeoutMs: dispatchPolicy.dispatch_ack_timeout_ms,
1011
+ pollMs: 50,
1012
+ });
1013
+ if (receipt && (receipt.status === 'notified' || receipt.status === 'delivered')) {
1014
+ outcome = { ok: true, transport: 'hook', reason: `hook_receipt_${receipt.status}`, request_id: queued.request_id };
461
1015
  }
462
1016
  else {
463
- await transitionDispatchRequest(sanitized, queued.request_id, 'failed', 'failed', { last_reason: `fallback_attempted_but_unconfirmed:${fallback.reason}` }, leaderCwd).catch(() => { });
464
- outcome = {
465
- ok: false,
466
- transport: fallback.transport,
467
- reason: `fallback_attempted_but_unconfirmed:${fallback.reason}`,
468
- request_id: queued.request_id,
469
- };
1017
+ const fallback = await notifyWorkerPaneOutcome(sessionName, triggerDirective.text, paneId, workerInfo.pid, config.tmux_pane_owner_id ?? undefined, workerInfo.session_id, workerInfo.window_id);
1018
+ if (receipt?.status === 'failed') {
1019
+ if (fallback.ok) {
1020
+ await transitionDispatchRequest(sanitized, queued.request_id, 'failed', 'failed', { last_reason: `fallback_confirmed_after_failed_receipt:${fallback.reason}` }, leaderCwd).catch(() => { });
1021
+ outcome = {
1022
+ ok: true,
1023
+ transport: fallback.transport,
1024
+ reason: `fallback_confirmed_after_failed_receipt:${fallback.reason}`,
1025
+ request_id: queued.request_id,
1026
+ };
1027
+ }
1028
+ else {
1029
+ await transitionDispatchRequest(sanitized, queued.request_id, 'failed', 'failed', { last_reason: `fallback_attempted_but_unconfirmed:${fallback.reason}` }, leaderCwd).catch(() => { });
1030
+ outcome = {
1031
+ ok: false,
1032
+ transport: fallback.transport,
1033
+ reason: `fallback_attempted_but_unconfirmed:${fallback.reason}`,
1034
+ request_id: queued.request_id,
1035
+ };
1036
+ }
1037
+ }
1038
+ else if (fallback.ok) {
1039
+ const marked = await markDispatchRequestNotified(sanitized, queued.request_id, { last_reason: `fallback_confirmed:${fallback.reason}` }, leaderCwd);
1040
+ if (!marked) {
1041
+ await transitionDispatchRequest(sanitized, queued.request_id, 'failed', 'failed', { last_reason: `fallback_confirmed_after_failed_receipt:${fallback.reason}` }, leaderCwd).catch(() => { });
1042
+ }
1043
+ outcome = {
1044
+ ok: true,
1045
+ transport: fallback.transport,
1046
+ reason: `hook_timeout_fallback_confirmed:${fallback.reason}`,
1047
+ request_id: queued.request_id,
1048
+ };
1049
+ }
1050
+ else {
1051
+ const current = await readDispatchRequest(sanitized, queued.request_id, leaderCwd);
1052
+ if (current) {
1053
+ await transitionDispatchRequest(sanitized, queued.request_id, current.status, 'failed', { last_reason: `fallback_attempted_but_unconfirmed:${fallback.reason}` }, leaderCwd).catch(() => { });
1054
+ }
1055
+ outcome = {
1056
+ ok: false,
1057
+ transport: fallback.transport,
1058
+ reason: `fallback_attempted_but_unconfirmed:${fallback.reason}`,
1059
+ request_id: queued.request_id,
1060
+ };
1061
+ }
470
1062
  }
471
1063
  }
472
- else if (fallback.ok) {
473
- const marked = await markDispatchRequestNotified(sanitized, queued.request_id, { last_reason: `fallback_confirmed:${fallback.reason}` }, leaderCwd);
474
- if (!marked) {
475
- await transitionDispatchRequest(sanitized, queued.request_id, 'failed', 'failed', { last_reason: `fallback_confirmed_after_failed_receipt:${fallback.reason}` }, leaderCwd).catch(() => { });
1064
+ // Retry dispatch once if a trust prompt is blocking the worker pane (fixes #393).
1065
+ if (!outcome.ok && dismissTrustPromptIfPresent(sessionName, workerIndex, paneId, workerInfo.pid, config.tmux_pane_owner_id ?? undefined, config.hud_pane_id ?? undefined)) {
1066
+ waitForWorkerReady(sessionName, workerIndex, readyTimeoutMs, paneId, workerInfo.pid, config.tmux_pane_owner_id ?? undefined, config.hud_pane_id ?? undefined);
1067
+ const retry = await notifyWorkerPaneOutcome(sessionName, triggerDirective.text, paneId, workerInfo.pid, config.tmux_pane_owner_id ?? undefined, workerInfo.session_id, workerInfo.window_id);
1068
+ if (retry.ok) {
1069
+ outcome = retry;
476
1070
  }
477
- outcome = {
478
- ok: true,
479
- transport: fallback.transport,
480
- reason: `hook_timeout_fallback_confirmed:${fallback.reason}`,
481
- request_id: queued.request_id,
482
- };
483
1071
  }
484
- else {
485
- const current = await readDispatchRequest(sanitized, queued.request_id, leaderCwd);
486
- if (current) {
487
- await transitionDispatchRequest(sanitized, queued.request_id, current.status, 'failed', { last_reason: `fallback_attempted_but_unconfirmed:${fallback.reason}` }, leaderCwd).catch(() => { });
488
- }
489
- outcome = {
490
- ok: false,
491
- transport: fallback.transport,
492
- reason: `fallback_attempted_but_unconfirmed:${fallback.reason}`,
493
- request_id: queued.request_id,
494
- };
1072
+ if (!outcome.ok) {
1073
+ return await rollbackScaleUp(`scale_up_dispatch_failed:${workerName}:${outcome.reason}`, {
1074
+ paneId,
1075
+ worker: workerInfo,
1076
+ });
495
1077
  }
1078
+ throwIfScaleUpFailureInjected(env, 'post-dispatch-rollback');
1079
+ addedWorkers.push(workerInfo);
1080
+ config.workers.push(workerInfo);
1081
+ config.worker_count = config.workers.length;
1082
+ config.next_worker_index = nextIndex;
1083
+ throwIfScaleUpFailureInjected(env, 'config');
1084
+ await saveTeamConfig(config, leaderCwd);
1085
+ throwIfScaleUpFailureInjected(env, 'finalization');
496
1086
  }
497
- }
498
- // Retry dispatch once if a trust prompt is blocking the worker pane (fixes #393).
499
- if (!outcome.ok && dismissTrustPromptIfPresent(sessionName, workerIndex, paneId)) {
500
- waitForWorkerReady(sessionName, workerIndex, readyTimeoutMs, paneId);
501
- const retry = await notifyWorkerPaneOutcome(sessionName, workerIndex, triggerDirective.text, paneId, workerCli);
502
- if (retry.ok) {
503
- outcome = retry;
1087
+ catch (error) {
1088
+ return await rollbackScaleUp(`scale_up_worker_materialization_failed:${workerName}:${error instanceof Error ? error.message : String(error)}`, { paneId, worker: workerInfo });
504
1089
  }
505
1090
  }
506
- if (!outcome.ok) {
507
- return await rollbackScaleUp(`scale_up_dispatch_failed:${workerName}:${outcome.reason}`, {
508
- paneId,
509
- workerName,
510
- worktreePath: workerWorkspace?.worktreePath,
511
- });
1091
+ try {
1092
+ await appendTeamEvent(sanitized, {
1093
+ type: 'team_leader_nudge',
1094
+ worker: 'leader-fixed',
1095
+ reason: `scale_up: added ${count} worker(s), new count=${config.worker_count}`,
1096
+ }, leaderCwd);
512
1097
  }
513
- addedWorkers.push(workerInfo);
514
- config.workers.push(workerInfo);
515
- config.worker_count = config.workers.length;
516
- config.next_worker_index = nextIndex;
517
- await saveTeamConfig(config, leaderCwd);
518
- }
519
- await appendTeamEvent(sanitized, {
520
- type: 'team_leader_nudge',
521
- worker: 'leader-fixed',
522
- reason: `scale_up: added ${count} worker(s), new count=${config.worker_count}`,
523
- }, leaderCwd);
524
- return {
525
- ok: true,
526
- addedWorkers,
527
- newWorkerCount: config.worker_count,
528
- nextWorkerIndex: nextIndex,
529
- };
1098
+ catch (error) {
1099
+ return await rollbackScaleUp(`scale_up_finalization_failed:${error instanceof Error ? error.message : String(error)}`);
1100
+ }
1101
+ return {
1102
+ ok: true,
1103
+ addedWorkers,
1104
+ newWorkerCount: config.worker_count,
1105
+ nextWorkerIndex: nextIndex,
1106
+ };
1107
+ });
1108
+ });
1109
+ }
1110
+ export function isSameOrInsidePath(path, root, pathSemantics = { relative, isAbsolute, sep }) {
1111
+ const relativePath = pathSemantics.relative(root, path);
1112
+ return relativePath === ''
1113
+ || (!pathSemantics.isAbsolute(relativePath)
1114
+ && relativePath !== '..'
1115
+ && !relativePath.startsWith(`..${pathSemantics.sep}`));
1116
+ }
1117
+ async function assertExistingPathParentContained(path, root) {
1118
+ const canonicalRoot = await realpath(root);
1119
+ let candidate = resolve(path);
1120
+ while (!existsSync(candidate)) {
1121
+ const parent = dirname(candidate);
1122
+ if (parent === candidate)
1123
+ throw new Error(`scale_down_cleanup_debt_path_missing_parent:${path}`);
1124
+ candidate = parent;
1125
+ }
1126
+ const canonicalExistingPath = await realpath(candidate);
1127
+ if (!isSameOrInsidePath(canonicalExistingPath, canonicalRoot)) {
1128
+ throw new Error(`scale_down_cleanup_debt_path_escape:${path}`);
1129
+ }
1130
+ }
1131
+ async function validateScaleDownCleanupResources(teamName, leaderCwd, teamStateRoot, workers) {
1132
+ const canonicalLeaderCwd = await realpath(leaderCwd);
1133
+ let repoRoot = canonicalLeaderCwd;
1134
+ const repoRootResult = spawnSync('git', ['rev-parse', '--show-toplevel'], {
1135
+ cwd: leaderCwd,
1136
+ encoding: 'utf-8',
1137
+ windowsHide: true,
1138
+ });
1139
+ const reportedRepoRoot = (repoRootResult.stdout || '').trim();
1140
+ if (repoRootResult.status === 0 && reportedRepoRoot) {
1141
+ repoRoot = await realpath(resolve(reportedRepoRoot));
1142
+ }
1143
+ const canonicalTeamStateRoot = await realpath(resolve(teamStateRoot));
1144
+ const expectedWorkerRoot = join(canonicalTeamStateRoot, 'team', teamName, 'workers');
1145
+ for (const worker of workers) {
1146
+ const expectedWorkerDirectory = join(expectedWorkerRoot, worker.name);
1147
+ await assertExistingPathParentContained(expectedWorkerDirectory, canonicalTeamStateRoot);
1148
+ if (worker.team_state_root !== undefined
1149
+ && (!isAbsolute(worker.team_state_root) || worker.team_state_root !== resolve(worker.team_state_root)
1150
+ || resolve(worker.team_state_root) !== canonicalTeamStateRoot)) {
1151
+ throw new Error(`scale_down_cleanup_debt_invalid_team_state_root:${worker.name}`);
1152
+ }
1153
+ if (worker.worktree_path === undefined) {
1154
+ if (worker.worktree_repo_root !== undefined || worker.worktree_branch !== undefined
1155
+ || worker.worktree_detached !== undefined || worker.worktree_created !== undefined) {
1156
+ throw new Error(`scale_down_cleanup_debt_invalid_worktree_metadata:${worker.name}`);
1157
+ }
1158
+ continue;
1159
+ }
1160
+ const expectedWorktreePath = join(repoRoot, '.omx', 'team', teamName, 'worktrees', worker.name);
1161
+ if (!isAbsolute(worker.worktree_path) || worker.worktree_path !== resolve(worker.worktree_path)
1162
+ || resolve(worker.worktree_path) !== expectedWorktreePath
1163
+ || (worker.worktree_repo_root !== undefined && (!isAbsolute(worker.worktree_repo_root)
1164
+ || worker.worktree_repo_root !== resolve(worker.worktree_repo_root)
1165
+ || resolve(worker.worktree_repo_root) !== repoRoot))
1166
+ || (worker.worktree_detached !== undefined && typeof worker.worktree_detached !== 'boolean')
1167
+ || (worker.worktree_created !== undefined && typeof worker.worktree_created !== 'boolean')
1168
+ || (worker.worktree_created === true && (typeof worker.worktree_repo_root !== 'string' || typeof worker.worktree_detached !== 'boolean'))) {
1169
+ throw new Error(`scale_down_cleanup_debt_invalid_worktree_target:${worker.name}`);
1170
+ }
1171
+ await assertExistingPathParentContained(expectedWorktreePath, repoRoot);
1172
+ if (existsSync(expectedWorktreePath) && await realpath(expectedWorktreePath) !== expectedWorktreePath) {
1173
+ throw new Error(`scale_down_cleanup_debt_worktree_symlink:${worker.name}`);
1174
+ }
1175
+ }
1176
+ }
1177
+ async function cleanupScaleDownResources(teamName, teamStateRoot, workers) {
1178
+ for (const worker of workers) {
1179
+ // An absent exact worktree means a prior cleanup completed before crashing.
1180
+ // Do not replay a restoration or git removal against a replacement path.
1181
+ if (worker.worktree_path && existsSync(worker.worktree_path)) {
1182
+ await removeWorkerWorktreeRootAgentsFile(teamName, worker.name, worker.team_state_root ?? teamStateRoot, worker.worktree_path);
1183
+ }
1184
+ }
1185
+ const worktrees = workers
1186
+ .filter((worker) => worker.worktree_created === true
1187
+ && typeof worker.worktree_path === 'string'
1188
+ && existsSync(worker.worktree_path)
1189
+ && typeof worker.worktree_repo_root === 'string'
1190
+ && typeof worker.worktree_detached === 'boolean')
1191
+ .map((worker) => ({
1192
+ enabled: true,
1193
+ repoRoot: worker.worktree_repo_root,
1194
+ worktreePath: worker.worktree_path,
1195
+ detached: worker.worktree_detached,
1196
+ branchName: worker.worktree_branch ?? null,
1197
+ created: true,
1198
+ reused: false,
1199
+ // Membership records do not establish branch ownership. Preserve a named
1200
+ // branch rather than deleting one that may have predated this worker.
1201
+ createdBranch: false,
1202
+ }));
1203
+ if (worktrees.length > 0)
1204
+ await rollbackProvisionedWorktrees(worktrees);
1205
+ await Promise.all(workers.map(async (worker) => {
1206
+ await rm(join(teamStateRoot, 'team', teamName, 'workers', worker.name), { recursive: true, force: true });
1207
+ }));
1208
+ }
1209
+ function asScaleDownDebtResources(debt, removedWorkerNames) {
1210
+ const resources = debt.resource_workers;
1211
+ if (!Array.isArray(resources) || resources.length !== removedWorkerNames.length)
1212
+ return null;
1213
+ const expected = new Set(removedWorkerNames);
1214
+ const seen = new Set();
1215
+ for (const worker of resources) {
1216
+ if (!worker || typeof worker.name !== 'string' || !expected.has(worker.name) || seen.has(worker.name)
1217
+ || (worker.worktree_path !== undefined && typeof worker.worktree_path !== 'string')
1218
+ || (worker.worktree_repo_root !== undefined && typeof worker.worktree_repo_root !== 'string')
1219
+ || (worker.worktree_branch !== undefined && typeof worker.worktree_branch !== 'string')
1220
+ || (worker.worktree_detached !== undefined && typeof worker.worktree_detached !== 'boolean')
1221
+ || (worker.worktree_created !== undefined && typeof worker.worktree_created !== 'boolean')
1222
+ || (worker.team_state_root !== undefined && typeof worker.team_state_root !== 'string'))
1223
+ return null;
1224
+ seen.add(worker.name);
1225
+ }
1226
+ return resources;
1227
+ }
1228
+ function asScaleDownDebtPanes(debt, removedWorkerNames, resources, leaderPaneId, hudPaneId) {
1229
+ const workers = debt.workers;
1230
+ const unresolved = debt.unresolved_panes;
1231
+ if (!Array.isArray(workers) || (unresolved !== undefined && !Array.isArray(unresolved)))
1232
+ return null;
1233
+ const expectedNames = new Set(removedWorkerNames);
1234
+ const resourceNames = new Set(resources.map((resource) => resource.name));
1235
+ const canonicalByName = new Map();
1236
+ const allPaneIds = new Set();
1237
+ const normalizedLeaderPaneId = typeof leaderPaneId === 'string' ? leaderPaneId.trim() : '';
1238
+ const normalizedHudPaneId = typeof hudPaneId === 'string' ? hudPaneId.trim() : '';
1239
+ const validate = (pane, requireCanonicalMatch) => {
1240
+ if (!pane || typeof pane.name !== 'string' || !expectedNames.has(pane.name) || !resourceNames.has(pane.name)
1241
+ || typeof pane.index !== 'number' || !Number.isInteger(pane.index) || pane.index <= 0
1242
+ || pane.index !== Number(pane.name.slice('worker-'.length))
1243
+ || typeof pane.pane_id !== 'string' || !/^%\d+$/.test(pane.pane_id)
1244
+ || pane.pane_id === normalizedLeaderPaneId || pane.pane_id === normalizedHudPaneId
1245
+ || (pane.pid !== null && (!Number.isSafeInteger(pane.pid) || pane.pid <= 0)))
1246
+ return false;
1247
+ const canonical = canonicalByName.get(pane.name);
1248
+ if (requireCanonicalMatch) {
1249
+ return canonical !== undefined
1250
+ && canonical.index === pane.index
1251
+ && canonical.pane_id === pane.pane_id
1252
+ && canonical.pid === pane.pid;
1253
+ }
1254
+ if (canonical !== undefined || allPaneIds.has(pane.pane_id))
1255
+ return false;
1256
+ canonicalByName.set(pane.name, pane);
1257
+ allPaneIds.add(pane.pane_id);
1258
+ return true;
1259
+ };
1260
+ for (const worker of workers) {
1261
+ if (!validate(worker, false))
1262
+ return null;
1263
+ }
1264
+ const unresolvedNames = new Set();
1265
+ const unresolvedPaneIds = new Set();
1266
+ for (const pane of unresolved ?? []) {
1267
+ if (unresolvedNames.has(pane.name ?? '') || unresolvedPaneIds.has(pane.pane_id)
1268
+ || !validate(pane, true))
1269
+ return null;
1270
+ unresolvedNames.add(pane.name);
1271
+ unresolvedPaneIds.add(pane.pane_id);
1272
+ }
1273
+ return unresolved && unresolved.length > 0 ? unresolved : workers;
1274
+ }
1275
+ async function hasMatchingScaleDownDebtWorkerIdentity(teamStateRoot, teamName, pane) {
1276
+ try {
1277
+ const identity = JSON.parse(await readFile(join(teamStateRoot, 'team', teamName, 'workers', pane.name ?? '', 'identity.json'), 'utf8'));
1278
+ return identity.name === pane.name
1279
+ && identity.index === pane.index
1280
+ && identity.pane_id === pane.pane_id
1281
+ && identity.pid === pane.pid;
1282
+ }
1283
+ catch {
1284
+ return false;
1285
+ }
1286
+ }
1287
+ export async function reconcileScaleDownCleanupDebt(teamName, cwd, _callerConfig) {
1288
+ const sanitized = sanitizeTeamName(teamName);
1289
+ const leaderCwd = resolve(cwd);
1290
+ return await withTaskMembershipBarrier(sanitized, leaderCwd, async () => {
1291
+ await recoverTeamMembershipTaskTransaction(sanitized, leaderCwd);
1292
+ // Authorization is derived only from the canonical state observed under the
1293
+ // membership barrier; callers may hold a stale pre-transaction config.
1294
+ const config = await readTeamConfig(sanitized, leaderCwd);
1295
+ if (!config)
1296
+ return { ok: false, error: `Team ${sanitized} not found` };
1297
+ const teamStateRoot = config.team_state_root ?? resolveCanonicalTeamStateRoot(leaderCwd);
1298
+ const cleanupDebtPath = join(teamStateRoot, 'team', sanitized, '.scale-down-cleanup-debt.json');
1299
+ if (!existsSync(cleanupDebtPath))
1300
+ return { ok: true };
1301
+ let debt;
1302
+ try {
1303
+ debt = JSON.parse(await readFile(cleanupDebtPath, 'utf8'));
1304
+ }
1305
+ catch (error) {
1306
+ return { ok: false, error: `scale_down_cleanup_debt_unreadable:${String(error)}` };
1307
+ }
1308
+ if (debt.schema_version !== 1 || debt.operation !== 'scale_down') {
1309
+ return { ok: false, error: 'scale_down_cleanup_debt_malformed' };
1310
+ }
1311
+ const removedWorkerNames = Array.isArray(debt.removed_worker_names)
1312
+ ? debt.removed_worker_names
1313
+ : (Array.isArray(debt.workers) ? debt.workers.map((worker) => worker?.name).filter((name) => typeof name === 'string') : []);
1314
+ if (removedWorkerNames.length === 0
1315
+ || removedWorkerNames.some((name) => !/^worker-\d+$/.test(name))
1316
+ || new Set(removedWorkerNames).size !== removedWorkerNames.length) {
1317
+ return { ok: false, error: 'scale_down_cleanup_debt_malformed' };
1318
+ }
1319
+ const resources = asScaleDownDebtResources(debt, removedWorkerNames);
1320
+ if (!resources)
1321
+ return { ok: false, error: 'scale_down_cleanup_debt_malformed' };
1322
+ try {
1323
+ await validateScaleDownCleanupResources(sanitized, leaderCwd, teamStateRoot, resources);
1324
+ }
1325
+ catch {
1326
+ return { ok: false, error: 'scale_down_cleanup_debt_malformed' };
1327
+ }
1328
+ const panes = asScaleDownDebtPanes(debt, removedWorkerNames, resources, config.leader_pane_id, config.hud_pane_id);
1329
+ // Validate all journal bindings before classifying or mutating recovery state.
1330
+ // A journal is evidence, never authority to target an arbitrary global pane.
1331
+ if (!panes)
1332
+ return { ok: false, error: 'scale_down_cleanup_debt_malformed' };
1333
+ const stillCanonical = removedWorkerNames.filter((name) => config.workers.some((worker) => worker.name === name));
1334
+ // A pre-commit journal is not authorization to kill or remove anything.
1335
+ if (stillCanonical.length === removedWorkerNames.length) {
1336
+ await removeDurableFile(cleanupDebtPath);
1337
+ return { ok: true };
1338
+ }
1339
+ if (stillCanonical.length > 0)
1340
+ return { ok: false, error: 'scale_down_cleanup_debt_membership_inconsistent' };
1341
+ // A cleanup journal may never reuse a pane identity held by a worker that
1342
+ // survived the committed membership update. This is checked before any
1343
+ // liveness or owner probes so malformed debt cannot cause a tmux effect.
1344
+ const survivingWorkers = config.workers.filter((worker) => !removedWorkerNames.includes(worker.name));
1345
+ if (panes.some((pane) => survivingWorkers.some((worker) => (worker.pane_id === pane.pane_id
1346
+ || (pane.pid !== null && worker.pid === pane.pid))))) {
1347
+ return { ok: false, error: 'scale_down_cleanup_debt_malformed' };
1348
+ }
1349
+ const resolvedPaneIds = new Set();
1350
+ const reasons = [];
1351
+ const expectedOwnerId = typeof config.tmux_pane_owner_id === 'string'
1352
+ ? config.tmux_pane_owner_id.trim()
1353
+ : '';
1354
+ for (const pane of panes) {
1355
+ // Legacy PID-less records can establish convergence from an authoritative
1356
+ // global absence/dead proof, but can never authorize a live-pane effect.
1357
+ const freshProof = readExactPaneProofSync(pane.pane_id);
1358
+ if (freshProof.status === 'gone') {
1359
+ resolvedPaneIds.add(pane.pane_id);
1360
+ continue;
1361
+ }
1362
+ if (freshProof.status === 'unavailable') {
1363
+ reasons.push(`${pane.pane_id}:${freshProof.reason}`);
1364
+ continue;
1365
+ }
1366
+ if (pane.pid === null) {
1367
+ reasons.push(`${pane.pane_id}:legacy_pid_missing_live`);
1368
+ continue;
1369
+ }
1370
+ if (freshProof.pid !== pane.pid) {
1371
+ reasons.push(`${pane.pane_id}:pane_pid_changed`);
1372
+ continue;
1373
+ }
1374
+ // A live debt pane must remain bound to the removed worker's recorded
1375
+ // identity; the team-wide owner token alone only proves team membership.
1376
+ if (!await hasMatchingScaleDownDebtWorkerIdentity(teamStateRoot, sanitized, pane)) {
1377
+ reasons.push(`${pane.pane_id}:worker_identity_unavailable`);
1378
+ continue;
1379
+ }
1380
+ // Owner reads are authoritative only for this canonical Team token. Missing,
1381
+ // mismatched, and unavailable tags all fail closed for a live pane effect.
1382
+ if (!expectedOwnerId) {
1383
+ reasons.push(`${pane.pane_id}:team_owner_unavailable`);
1384
+ continue;
1385
+ }
1386
+ const owner = readPaneTeamOwnerTagResult(pane.pane_id);
1387
+ if (owner.status === 'error') {
1388
+ reasons.push(`${pane.pane_id}:team_owner_unavailable`);
1389
+ continue;
1390
+ }
1391
+ if (owner.status !== 'value' || owner.value !== expectedOwnerId) {
1392
+ reasons.push(`${pane.pane_id}:team_owner_mismatch`);
1393
+ continue;
1394
+ }
1395
+ // teardownWorkerPanes re-proves this exact pane immediately before the kill
1396
+ // and pins the same durable PID through the kill confirmation.
1397
+ const teardown = await teardownWorkerPanes([pane.pane_id], {
1398
+ leaderPaneId: config.leader_pane_id,
1399
+ hudPaneId: config.hud_pane_id,
1400
+ expectedPanePids: { [pane.pane_id]: pane.pid },
1401
+ authorizePaneKill: (paneId) => {
1402
+ const currentOwner = readPaneTeamOwnerTagResult(paneId);
1403
+ return currentOwner.status === 'value' && currentOwner.value === expectedOwnerId;
1404
+ },
1405
+ });
1406
+ if (teardown.provenGonePaneIds.includes(pane.pane_id) || teardown.killedPaneIds.includes(pane.pane_id)) {
1407
+ resolvedPaneIds.add(pane.pane_id);
1408
+ continue;
1409
+ }
1410
+ reasons.push(...teardown.proofUnavailable.map((proof) => `${proof.paneId}:${proof.reason}`));
1411
+ reasons.push(...teardown.kill.failedPaneIds.map((paneId) => `${paneId}:kill_failed`));
1412
+ if (teardown.proofUnavailable.length === 0 && teardown.kill.failedPaneIds.length === 0) {
1413
+ reasons.push(`${pane.pane_id}:teardown_unresolved`);
1414
+ }
1415
+ }
1416
+ const unresolvedPanes = panes.filter((pane) => !resolvedPaneIds.has(pane.pane_id));
1417
+ if (unresolvedPanes.length > 0) {
1418
+ await writeAtomic(cleanupDebtPath, JSON.stringify({
1419
+ ...debt,
1420
+ status: 'unresolved',
1421
+ updated_at: new Date().toISOString(),
1422
+ unresolved_panes: unresolvedPanes,
1423
+ reasons,
1424
+ }, null, 2));
1425
+ return { ok: false, error: `scale_down_cleanup_debt_unresolved:${unresolvedPanes.map((pane) => pane.pane_id).join(',')}` };
1426
+ }
1427
+ try {
1428
+ await cleanupScaleDownResources(sanitized, teamStateRoot, resources);
1429
+ }
1430
+ catch (error) {
1431
+ await writeAtomic(cleanupDebtPath, JSON.stringify({
1432
+ ...debt,
1433
+ status: 'resource_cleanup_pending',
1434
+ updated_at: new Date().toISOString(),
1435
+ unresolved_panes: [],
1436
+ reasons: [`resource_cleanup_failed:${String(error)}`],
1437
+ }, null, 2));
1438
+ return { ok: false, error: `scale_down_cleanup_debt_resource_cleanup_failed:${String(error)}` };
1439
+ }
1440
+ await removeDurableFile(cleanupDebtPath);
1441
+ return { ok: true };
530
1442
  });
531
1443
  }
532
1444
  /**
@@ -542,10 +1454,17 @@ export async function scaleDown(teamName, cwd, options = {}, env = process.env)
542
1454
  const force = options.force === true;
543
1455
  const drainTimeoutMs = options.drainTimeoutMs ?? 30_000;
544
1456
  return await withScalingLock(sanitized, leaderCwd, async () => {
545
- const config = await readTeamConfig(sanitized, leaderCwd);
1457
+ const config = await withTaskMembershipBarrier(sanitized, leaderCwd, async () => {
1458
+ await recoverTeamMembershipTaskTransaction(sanitized, leaderCwd);
1459
+ return await readTeamConfig(sanitized, leaderCwd);
1460
+ });
546
1461
  if (!config) {
547
1462
  return { ok: false, error: `Team ${sanitized} not found` };
548
1463
  }
1464
+ let teamStateRoot = config.team_state_root ?? resolveCanonicalTeamStateRoot(leaderCwd);
1465
+ const priorCleanup = await reconcileScaleDownCleanupDebt(sanitized, leaderCwd, config);
1466
+ if (!priorCleanup.ok)
1467
+ return priorCleanup;
549
1468
  // Determine which workers to remove
550
1469
  let targetWorkers;
551
1470
  if (options.workerNames && options.workerNames.length > 0) {
@@ -595,6 +1514,33 @@ export async function scaleDown(teamName, cwd, options = {}, env = process.env)
595
1514
  }
596
1515
  const sessionName = config.tmux_session;
597
1516
  const removedNames = [];
1517
+ const priorWorkerStatusArtifacts = new Map();
1518
+ for (const worker of targetWorkers) {
1519
+ const statusPath = join(teamStateRoot, 'team', sanitized, 'workers', worker.name, 'status.json');
1520
+ try {
1521
+ priorWorkerStatusArtifacts.set(worker.name, { exists: true, raw: await readFile(statusPath) });
1522
+ }
1523
+ catch (error) {
1524
+ if (error.code === 'ENOENT') {
1525
+ priorWorkerStatusArtifacts.set(worker.name, { exists: false });
1526
+ }
1527
+ else {
1528
+ throw error;
1529
+ }
1530
+ }
1531
+ }
1532
+ const restorePriorWorkerStatuses = async (workers) => {
1533
+ await Promise.all(workers.map(async (worker) => {
1534
+ const statusPath = join(teamStateRoot, 'team', sanitized, 'workers', worker.name, 'status.json');
1535
+ const priorArtifact = priorWorkerStatusArtifacts.get(worker.name);
1536
+ if (priorArtifact?.exists) {
1537
+ await writeFile(statusPath, priorArtifact.raw);
1538
+ }
1539
+ else {
1540
+ await rm(statusPath, { force: true });
1541
+ }
1542
+ }));
1543
+ };
598
1544
  // Phase 1: Set workers to 'draining' status
599
1545
  for (const w of targetWorkers) {
600
1546
  const drainingStatus = {
@@ -611,69 +1557,239 @@ export async function scaleDown(teamName, cwd, options = {}, env = process.env)
611
1557
  const allDrained = await Promise.all(targetWorkers.map(async (w) => {
612
1558
  const status = await readWorkerStatus(sanitized, w.name, leaderCwd);
613
1559
  return status.state === 'idle' || status.state === 'done' ||
614
- status.state === 'draining' || !isWorkerAlive(sessionName, w.index, w.pane_id);
1560
+ status.state === 'draining' || !isWorkerAlive(sessionName, w.index, w.pane_id, w.pid, config.tmux_pane_owner_id ?? undefined, config.hud_pane_id ?? undefined);
615
1561
  }));
616
1562
  if (allDrained.every(Boolean))
617
1563
  break;
618
1564
  await new Promise(r => setTimeout(r, 2_000));
619
1565
  }
620
1566
  }
621
- // Phase 3: Kill tmux panes and remove from config
622
- const leaderPaneId = config.leader_pane_id;
623
- const hudPaneId = config.hud_pane_id;
624
- const targetPaneIds = targetWorkers
625
- .map((w) => w.pane_id)
626
- .filter((paneId) => typeof paneId === 'string' && paneId.trim().length > 0);
627
- await teardownWorkerPanes(targetPaneIds, {
628
- leaderPaneId,
629
- hudPaneId,
630
- });
631
- const detachedWorktreesToRollback = targetWorkers
632
- .filter((worker) => worker.worktree_created === true
633
- && worker.worktree_detached === true
634
- && typeof worker.worktree_repo_root === 'string'
635
- && worker.worktree_repo_root.length > 0
636
- && typeof worker.worktree_path === 'string'
637
- && worker.worktree_path.length > 0)
638
- .map((worker) => ({
639
- enabled: true,
640
- repoRoot: worker.worktree_repo_root,
641
- worktreePath: resolve(worker.worktree_path),
642
- detached: true,
643
- branchName: null,
644
- created: true,
645
- reused: false,
646
- createdBranch: false,
647
- }));
648
- if (detachedWorktreesToRollback.length > 0) {
649
- try {
650
- await rollbackProvisionedWorktrees(detachedWorktreesToRollback);
651
- }
652
- catch (error) {
653
- return { ok: false, error: `scale_down_worktree_cleanup_failed:${String(error)}` };
654
- }
1567
+ // Phase 3: acquire the membership barrier and task claim locks before any
1568
+ // pane effect. The barrier and locks remain held through the canonical
1569
+ // snapshot, exact pane teardown, and forward-recoverable commit.
1570
+ let removableWorkers = targetWorkers;
1571
+ let removableWorkerNames = new Set(removableWorkers.map((worker) => worker.name));
1572
+ let teardownFailure = null;
1573
+ try {
1574
+ await withTaskMembershipBarrier(sanitized, leaderCwd, async () => {
1575
+ await recoverTeamMembershipTaskTransaction(sanitized, leaderCwd);
1576
+ const authoritativeConfig = await readTeamConfig(sanitized, leaderCwd);
1577
+ if (!authoritativeConfig)
1578
+ throw new Error('canonical_scale_down_config_missing');
1579
+ const authoritativeWorkers = removableWorkers.map((worker) => authoritativeConfig.workers.find((candidate) => candidate.name === worker.name));
1580
+ if (authoritativeWorkers.some((worker) => !worker)) {
1581
+ throw new Error('canonical_scale_down_membership_changed');
1582
+ }
1583
+ // Capture targets from the canonical generation while the membership
1584
+ // authority is held; no caller-owned worker record authorizes a pane effect.
1585
+ removableWorkers = authoritativeWorkers;
1586
+ removableWorkerNames = new Set(removableWorkers.map((worker) => worker.name));
1587
+ teamStateRoot = authoritativeConfig.team_state_root ?? resolveCanonicalTeamStateRoot(leaderCwd);
1588
+ Object.assign(config, authoritativeConfig);
1589
+ const candidateTaskIds = (await listTasks(sanitized, leaderCwd))
1590
+ .filter((task) => task.status !== 'completed' && task.status !== 'failed')
1591
+ .map((task) => task.id);
1592
+ await withTaskClaimLocks(sanitized, candidateTaskIds, leaderCwd, async () => {
1593
+ const lockedTasks = await listTasks(sanitized, leaderCwd);
1594
+ const configPath = join(teamStateRoot, 'team', sanitized, 'config.json');
1595
+ const configSnapshot = await readFile(configPath);
1596
+ const manifestPath = join(teamStateRoot, 'team', sanitized, 'manifest.v2.json');
1597
+ const manifestSnapshot = existsSync(manifestPath) ? await readFile(manifestPath) : null;
1598
+ const reconciledTasks = lockedTasks.filter((task) => task.status !== 'completed' && task.status !== 'failed'
1599
+ && (removableWorkerNames.has(task.owner ?? '') || removableWorkerNames.has(task.claim?.owner ?? '')));
1600
+ const taskSnapshots = new Map();
1601
+ for (const task of reconciledTasks) {
1602
+ taskSnapshots.set(task.id, await readFile(join(teamStateRoot, 'team', sanitized, 'tasks', `task-${task.id}.json`)));
1603
+ }
1604
+ const postSnapshotMarker = env.OMX_TEAM_SCALE_DOWN_POST_SNAPSHOT_MARKER;
1605
+ if (postSnapshotMarker)
1606
+ await writeFile(postSnapshotMarker, 'snapshot-held\n');
1607
+ const boundaryHoldMs = Number.parseInt(env.OMX_TEAM_SCALE_DOWN_BOUNDARY_HOLD_MS ?? '', 10);
1608
+ if (Number.isFinite(boundaryHoldMs) && boundaryHoldMs > 0)
1609
+ await new Promise((resolve) => setTimeout(resolve, boundaryHoldMs));
1610
+ const currentConfig = JSON.parse(configSnapshot.toString('utf8'));
1611
+ const nextConfig = {
1612
+ ...currentConfig,
1613
+ workers: currentConfig.workers.filter((worker) => !removableWorkerNames.has(worker.name)),
1614
+ };
1615
+ nextConfig.worker_count = nextConfig.workers.length;
1616
+ const nextManifest = manifestSnapshot
1617
+ ? JSON.stringify({
1618
+ ...JSON.parse(manifestSnapshot.toString('utf8')),
1619
+ workers: nextConfig.workers,
1620
+ worker_count: nextConfig.worker_count,
1621
+ }, null, 2)
1622
+ : null;
1623
+ const membershipTransaction = {
1624
+ baseGeneration: currentConfig.config_generation ?? 0,
1625
+ tasks: reconciledTasks.map((task) => ({
1626
+ taskId: task.id,
1627
+ oldBytes: taskSnapshots.get(task.id)?.toString('utf8') ?? null,
1628
+ newBytes: JSON.stringify({
1629
+ ...task,
1630
+ owner: undefined,
1631
+ claim: undefined,
1632
+ status: task.status === 'in_progress' ? 'pending' : task.status,
1633
+ version: Math.max(1, task.version ?? 1) + 1,
1634
+ }, null, 2),
1635
+ })),
1636
+ config: {
1637
+ oldBytes: configSnapshot.toString('utf8'),
1638
+ newBytes: JSON.stringify(nextConfig, null, 2),
1639
+ },
1640
+ manifest: {
1641
+ oldBytes: manifestSnapshot?.toString('utf8') ?? null,
1642
+ newBytes: nextManifest,
1643
+ },
1644
+ interruptAfterFirstTaskWrite: env.OMX_TEAM_SCALE_DOWN_INJECT_FAILURE === 'after-first-task-write',
1645
+ failRollbackPersistence: env.OMX_TEAM_SCALE_DOWN_INJECT_FAILURE === 'rollback-persistence-failure',
1646
+ recoverToNewOnFailure: true,
1647
+ };
1648
+ const cleanupDebtPath = join(teamStateRoot, 'team', sanitized, '.scale-down-cleanup-debt.json');
1649
+ const cleanupDebtBase = {
1650
+ schema_version: 1,
1651
+ operation: 'scale_down',
1652
+ status: 'pending_teardown',
1653
+ created_at: new Date().toISOString(),
1654
+ removed_worker_names: removableWorkers.map((worker) => worker.name),
1655
+ workers: removableWorkers
1656
+ .filter((worker) => (typeof worker.pane_id === 'string' && /^%\d+$/.test(worker.pane_id)))
1657
+ .map((worker) => ({
1658
+ name: worker.name,
1659
+ index: worker.index,
1660
+ pane_id: worker.pane_id,
1661
+ pid: worker.pid ?? null,
1662
+ })),
1663
+ resource_workers: removableWorkers.map((worker) => ({
1664
+ name: worker.name,
1665
+ worktree_path: worker.worktree_path,
1666
+ worktree_repo_root: worker.worktree_repo_root,
1667
+ worktree_branch: worker.worktree_branch,
1668
+ worktree_detached: worker.worktree_detached,
1669
+ worktree_created: worker.worktree_created,
1670
+ team_state_root: worker.team_state_root,
1671
+ })),
1672
+ };
1673
+ await writeAtomic(cleanupDebtPath, JSON.stringify(cleanupDebtBase, null, 2));
1674
+ await commitTeamMembershipTaskTransaction(sanitized, leaderCwd, membershipTransaction);
1675
+ const committed = await readTeamConfig(sanitized, leaderCwd);
1676
+ if (!committed || committed.workers.some((worker) => removableWorkerNames.has(worker.name))) {
1677
+ throw new Error('canonical_scale_down_config_verification_failed');
1678
+ }
1679
+ config.workers = committed.workers;
1680
+ config.worker_count = committed.worker_count;
1681
+ for (const task of reconciledTasks) {
1682
+ const reconciled = await readTask(sanitized, task.id, leaderCwd);
1683
+ if (reconciled && (removableWorkerNames.has(reconciled.owner ?? '') || removableWorkerNames.has(reconciled.claim?.owner ?? ''))) {
1684
+ throw new Error(`canonical_scale_down_task_verification_failed:${task.id}`);
1685
+ }
1686
+ }
1687
+ // PID-less rollback records are cleanup debt, not effect authority. A
1688
+ // fresh gone proof may resolve one, but a live or unavailable proof
1689
+ // must remain debt rather than letting teardown adopt its current PID.
1690
+ const pinnedPaneWorkers = removableWorkers.filter((worker) => (typeof worker.pane_id === 'string'
1691
+ && /^%\d+$/.test(worker.pane_id)
1692
+ && typeof worker.pid === 'number'
1693
+ && Number.isSafeInteger(worker.pid)
1694
+ && worker.pid > 0));
1695
+ const unpinnedPaneWorkers = removableWorkers.filter((worker) => (typeof worker.pane_id === 'string'
1696
+ && /^%\d+$/.test(worker.pane_id)
1697
+ && !pinnedPaneWorkers.some((pinnedWorker) => pinnedWorker.name === worker.name)));
1698
+ const unpinnedResolvedPaneIds = new Set();
1699
+ const unpinnedProofDebt = [];
1700
+ for (const worker of unpinnedPaneWorkers) {
1701
+ const proof = readExactPaneProofSync(worker.pane_id);
1702
+ if (proof.status === 'gone') {
1703
+ unpinnedResolvedPaneIds.add(worker.pane_id);
1704
+ }
1705
+ else if (proof.status === 'unavailable') {
1706
+ unpinnedProofDebt.push(`${worker.pane_id}:${proof.reason}`);
1707
+ }
1708
+ else {
1709
+ unpinnedProofDebt.push(`${worker.pane_id}:legacy_pid_missing_live`);
1710
+ }
1711
+ }
1712
+ const targetPaneIds = pinnedPaneWorkers.map((worker) => worker.pane_id);
1713
+ const expectedScaleDownOwnerId = typeof config.tmux_pane_owner_id === 'string'
1714
+ ? config.tmux_pane_owner_id.trim()
1715
+ : '';
1716
+ const paneTeardown = await teardownWorkerPanes(targetPaneIds, {
1717
+ leaderPaneId: config.leader_pane_id,
1718
+ hudPaneId: config.hud_pane_id,
1719
+ expectedPanePids: Object.fromEntries(pinnedPaneWorkers.map((worker) => [worker.pane_id, worker.pid])),
1720
+ authorizePaneKill: (paneId) => {
1721
+ const currentOwner = readPaneTeamOwnerTagResult(paneId);
1722
+ return Boolean(expectedScaleDownOwnerId)
1723
+ && currentOwner.status === 'value'
1724
+ && currentOwner.value === expectedScaleDownOwnerId;
1725
+ },
1726
+ });
1727
+ const resolvedPaneIds = new Set([
1728
+ ...unpinnedResolvedPaneIds,
1729
+ ...paneTeardown.provenGonePaneIds,
1730
+ ...paneTeardown.killedPaneIds,
1731
+ ]);
1732
+ const unresolvedWorkers = removableWorkers.filter((worker) => (typeof worker.pane_id === 'string' && !resolvedPaneIds.has(worker.pane_id)));
1733
+ if (unresolvedWorkers.length > 0) {
1734
+ await restorePriorWorkerStatuses(unresolvedWorkers);
1735
+ const proofDebt = [...unpinnedProofDebt, ...paneTeardown.proofUnavailable.map((proof) => `${proof.paneId}:${proof.reason}`)];
1736
+ const killDebt = paneTeardown.kill.failedPaneIds.map((paneId) => `${paneId}:kill_failed`);
1737
+ await writeAtomic(cleanupDebtPath, JSON.stringify({
1738
+ ...cleanupDebtBase,
1739
+ status: 'unresolved',
1740
+ updated_at: new Date().toISOString(),
1741
+ unresolved_panes: unresolvedWorkers.map((worker) => ({
1742
+ name: worker.name,
1743
+ index: worker.index,
1744
+ pane_id: worker.pane_id,
1745
+ pid: worker.pid ?? null,
1746
+ })),
1747
+ reasons: [...proofDebt, ...killDebt],
1748
+ }, null, 2));
1749
+ if (paneTeardown.proofUnavailable.length > 0) {
1750
+ teardownFailure = { ok: false, error: `scale_down_pane_proof_unavailable:${proofDebt.join(',')}` };
1751
+ return;
1752
+ }
1753
+ const unresolvedDebt = unresolvedWorkers
1754
+ .map((worker) => `pane_teardown_unresolved:${worker.pane_id}`)
1755
+ .join(';');
1756
+ const failedDebt = paneTeardown.kill.failedPaneIds.length > 0
1757
+ ? `pane_teardown_failed:${paneTeardown.kill.failedPaneIds.join(',')};`
1758
+ : '';
1759
+ teardownFailure = { ok: false, error: `scale_down_cleanup_debt:${failedDebt}${unresolvedDebt}` };
1760
+ return;
1761
+ }
1762
+ await writeAtomic(cleanupDebtPath, JSON.stringify({
1763
+ ...cleanupDebtBase,
1764
+ status: 'resource_cleanup_pending',
1765
+ updated_at: new Date().toISOString(),
1766
+ unresolved_panes: [],
1767
+ }, null, 2));
1768
+ });
1769
+ });
655
1770
  }
656
- for (const w of targetWorkers) {
657
- if (w.worktree_path) {
658
- await removeWorkerWorktreeRootAgentsFile(sanitized, w.name, w.team_state_root ?? config.team_state_root ?? resolveCanonicalTeamStateRoot(leaderCwd), w.worktree_path).catch(() => { });
659
- }
660
- removedNames.push(w.name);
661
- }
662
- // Phase 4: Update config
663
- const removedSet = new Set(removedNames);
664
- config.workers = config.workers.filter(w => !removedSet.has(w.name));
665
- config.worker_count = config.workers.length;
666
- await saveTeamConfig(config, leaderCwd);
667
- await appendTeamEvent(sanitized, {
668
- type: 'team_leader_nudge',
669
- worker: 'leader-fixed',
670
- reason: `scale_down: removed ${removedNames.length} worker(s) [${removedNames.join(', ')}], new count=${config.worker_count}`,
671
- }, leaderCwd);
672
- return {
673
- ok: true,
674
- removedWorkers: removedNames,
675
- newWorkerCount: config.worker_count,
676
- };
1771
+ catch (error) {
1772
+ await restorePriorWorkerStatuses(removableWorkers);
1773
+ return { ok: false, error: `scale_down_task_reconciliation_failed:${String(error)}` };
1774
+ }
1775
+ if (teardownFailure)
1776
+ return teardownFailure;
1777
+ targetWorkers = removableWorkers;
1778
+ // Resource cleanup remains represented by the committed debt until every
1779
+ // worker directory, status, generated AGENTS file, and provisioned worktree
1780
+ // has converged. Reconciliation reacquires authority before consuming it.
1781
+ removedNames.push(...targetWorkers.map((worker) => worker.name));
1782
+ const resourceCleanup = await reconcileScaleDownCleanupDebt(sanitized, leaderCwd, config);
1783
+ if (!resourceCleanup.ok)
1784
+ return resourceCleanup;
1785
+ const reason = `scale_down: removed ${removedNames.length} worker(s) [${removedNames.join(', ')}], new count=${config.worker_count}`;
1786
+ try {
1787
+ await appendTeamEvent(sanitized, { type: 'team_leader_nudge', worker: 'leader-fixed', reason }, leaderCwd);
1788
+ }
1789
+ catch {
1790
+ // The cleanup transaction has already converged; event delivery is advisory.
1791
+ }
1792
+ return { ok: true, removedWorkers: removedNames, newWorkerCount: config.worker_count };
677
1793
  });
678
1794
  }
679
1795
  // ── Helpers ───────────────────────────────────────────────────────────────────