oh-my-codex 0.20.1 → 0.20.3

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