oh-my-codex 0.16.2 → 0.16.4

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 (340) hide show
  1. package/Cargo.lock +5 -5
  2. package/Cargo.toml +1 -1
  3. package/README.md +3 -3
  4. package/dist/catalog/__tests__/plugin-bundle-ssot.test.js +9 -0
  5. package/dist/catalog/__tests__/plugin-bundle-ssot.test.js.map +1 -1
  6. package/dist/cli/__tests__/cleanup.test.js +27 -0
  7. package/dist/cli/__tests__/cleanup.test.js.map +1 -1
  8. package/dist/cli/__tests__/codex-plugin-layout.test.js +7 -5
  9. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  10. package/dist/cli/__tests__/doctor-warning-copy.test.js +137 -6
  11. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  12. package/dist/cli/__tests__/index.test.js +303 -4
  13. package/dist/cli/__tests__/index.test.js.map +1 -1
  14. package/dist/cli/__tests__/launch-fallback.test.js +58 -0
  15. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  16. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +2 -0
  17. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
  18. package/dist/cli/__tests__/ralph.test.js +48 -0
  19. package/dist/cli/__tests__/ralph.test.js.map +1 -1
  20. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js +8 -0
  21. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js.map +1 -1
  22. package/dist/cli/__tests__/setup-install-mode.test.js +350 -27
  23. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  24. package/dist/cli/__tests__/setup-refresh.test.js +85 -3
  25. package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
  26. package/dist/cli/__tests__/setup-scope.test.js +1 -1
  27. package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
  28. package/dist/cli/__tests__/setup-skills-overwrite.test.js +2 -1
  29. package/dist/cli/__tests__/setup-skills-overwrite.test.js.map +1 -1
  30. package/dist/cli/__tests__/team.test.js +269 -0
  31. package/dist/cli/__tests__/team.test.js.map +1 -1
  32. package/dist/cli/__tests__/ultragoal.test.js +69 -0
  33. package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
  34. package/dist/cli/__tests__/uninstall.test.js +90 -6
  35. package/dist/cli/__tests__/uninstall.test.js.map +1 -1
  36. package/dist/cli/__tests__/update.test.js +109 -19
  37. package/dist/cli/__tests__/update.test.js.map +1 -1
  38. package/dist/cli/cleanup.d.ts.map +1 -1
  39. package/dist/cli/cleanup.js +8 -4
  40. package/dist/cli/cleanup.js.map +1 -1
  41. package/dist/cli/codex-feature-probe.d.ts +9 -0
  42. package/dist/cli/codex-feature-probe.d.ts.map +1 -0
  43. package/dist/cli/codex-feature-probe.js +28 -0
  44. package/dist/cli/codex-feature-probe.js.map +1 -0
  45. package/dist/cli/doctor.d.ts +1 -0
  46. package/dist/cli/doctor.d.ts.map +1 -1
  47. package/dist/cli/doctor.js +168 -16
  48. package/dist/cli/doctor.js.map +1 -1
  49. package/dist/cli/index.d.ts +9 -2
  50. package/dist/cli/index.d.ts.map +1 -1
  51. package/dist/cli/index.js +168 -20
  52. package/dist/cli/index.js.map +1 -1
  53. package/dist/cli/mcp-parity.js +8 -8
  54. package/dist/cli/mcp-parity.js.map +1 -1
  55. package/dist/cli/plugin-marketplace.d.ts +3 -0
  56. package/dist/cli/plugin-marketplace.d.ts.map +1 -1
  57. package/dist/cli/plugin-marketplace.js +88 -0
  58. package/dist/cli/plugin-marketplace.js.map +1 -1
  59. package/dist/cli/ralph.d.ts.map +1 -1
  60. package/dist/cli/ralph.js +21 -0
  61. package/dist/cli/ralph.js.map +1 -1
  62. package/dist/cli/setup-preferences.d.ts +4 -0
  63. package/dist/cli/setup-preferences.d.ts.map +1 -1
  64. package/dist/cli/setup-preferences.js +7 -0
  65. package/dist/cli/setup-preferences.js.map +1 -1
  66. package/dist/cli/setup.d.ts +5 -3
  67. package/dist/cli/setup.d.ts.map +1 -1
  68. package/dist/cli/setup.js +177 -43
  69. package/dist/cli/setup.js.map +1 -1
  70. package/dist/cli/team.d.ts.map +1 -1
  71. package/dist/cli/team.js +54 -15
  72. package/dist/cli/team.js.map +1 -1
  73. package/dist/cli/ultragoal.d.ts +1 -1
  74. package/dist/cli/ultragoal.d.ts.map +1 -1
  75. package/dist/cli/ultragoal.js +64 -5
  76. package/dist/cli/ultragoal.js.map +1 -1
  77. package/dist/cli/uninstall.d.ts +2 -0
  78. package/dist/cli/uninstall.d.ts.map +1 -1
  79. package/dist/cli/uninstall.js +76 -5
  80. package/dist/cli/uninstall.js.map +1 -1
  81. package/dist/cli/update.d.ts +10 -2
  82. package/dist/cli/update.d.ts.map +1 -1
  83. package/dist/cli/update.js +99 -5
  84. package/dist/cli/update.js.map +1 -1
  85. package/dist/config/__tests__/codex-feature-flags.test.d.ts +2 -0
  86. package/dist/config/__tests__/codex-feature-flags.test.d.ts.map +1 -0
  87. package/dist/config/__tests__/codex-feature-flags.test.js +35 -0
  88. package/dist/config/__tests__/codex-feature-flags.test.js.map +1 -0
  89. package/dist/config/__tests__/codex-hooks.test.js +188 -4
  90. package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
  91. package/dist/config/__tests__/generator-idempotent.test.js +129 -10
  92. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  93. package/dist/config/__tests__/generator-notify.test.js +148 -7
  94. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  95. package/dist/config/__tests__/wiki-config-contract.test.js +6 -3
  96. package/dist/config/__tests__/wiki-config-contract.test.js.map +1 -1
  97. package/dist/config/codex-feature-flags.d.ts +21 -0
  98. package/dist/config/codex-feature-flags.d.ts.map +1 -0
  99. package/dist/config/codex-feature-flags.js +56 -0
  100. package/dist/config/codex-feature-flags.js.map +1 -0
  101. package/dist/config/codex-hooks.d.ts +40 -4
  102. package/dist/config/codex-hooks.d.ts.map +1 -1
  103. package/dist/config/codex-hooks.js +204 -18
  104. package/dist/config/codex-hooks.js.map +1 -1
  105. package/dist/config/generator.d.ts +19 -1
  106. package/dist/config/generator.d.ts.map +1 -1
  107. package/dist/config/generator.js +319 -83
  108. package/dist/config/generator.js.map +1 -1
  109. package/dist/config/omx-first-party-mcp.d.ts +3 -1
  110. package/dist/config/omx-first-party-mcp.d.ts.map +1 -1
  111. package/dist/config/omx-first-party-mcp.js +2 -2
  112. package/dist/config/omx-first-party-mcp.js.map +1 -1
  113. package/dist/hooks/__tests__/keyword-detector.test.js +92 -2
  114. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  115. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +29 -1
  116. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  117. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +10 -0
  118. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
  119. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +1 -0
  120. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js.map +1 -1
  121. package/dist/hooks/__tests__/notify-hook-non-omx-guard.test.d.ts +2 -0
  122. package/dist/hooks/__tests__/notify-hook-non-omx-guard.test.d.ts.map +1 -0
  123. package/dist/hooks/__tests__/notify-hook-non-omx-guard.test.js +176 -0
  124. package/dist/hooks/__tests__/notify-hook-non-omx-guard.test.js.map +1 -0
  125. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js +148 -0
  126. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js.map +1 -1
  127. package/dist/hooks/__tests__/notify-hook-session-scope.test.js +3 -0
  128. package/dist/hooks/__tests__/notify-hook-session-scope.test.js.map +1 -1
  129. package/dist/hooks/__tests__/skill-catalog-hygiene.test.d.ts +2 -0
  130. package/dist/hooks/__tests__/skill-catalog-hygiene.test.d.ts.map +1 -0
  131. package/dist/hooks/__tests__/skill-catalog-hygiene.test.js +84 -0
  132. package/dist/hooks/__tests__/skill-catalog-hygiene.test.js.map +1 -0
  133. package/dist/hooks/__tests__/wiki-docs-contract.test.js +1 -2
  134. package/dist/hooks/__tests__/wiki-docs-contract.test.js.map +1 -1
  135. package/dist/hooks/agents-overlay.js +2 -2
  136. package/dist/hooks/agents-overlay.js.map +1 -1
  137. package/dist/hooks/keyword-detector.d.ts +1 -0
  138. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  139. package/dist/hooks/keyword-detector.js +7 -5
  140. package/dist/hooks/keyword-detector.js.map +1 -1
  141. package/dist/hud/__tests__/state.test.js +164 -0
  142. package/dist/hud/__tests__/state.test.js.map +1 -1
  143. package/dist/hud/state.d.ts.map +1 -1
  144. package/dist/hud/state.js +4 -5
  145. package/dist/hud/state.js.map +1 -1
  146. package/dist/mcp/__tests__/state-paths.test.js +61 -0
  147. package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
  148. package/dist/mcp/__tests__/state-server.test.js +166 -0
  149. package/dist/mcp/__tests__/state-server.test.js.map +1 -1
  150. package/dist/mcp/state-paths.d.ts.map +1 -1
  151. package/dist/mcp/state-paths.js +23 -2
  152. package/dist/mcp/state-paths.js.map +1 -1
  153. package/dist/modes/__tests__/base-session-scope.test.js +22 -0
  154. package/dist/modes/__tests__/base-session-scope.test.js.map +1 -1
  155. package/dist/modes/__tests__/base-tmux-pane.test.js +57 -26
  156. package/dist/modes/__tests__/base-tmux-pane.test.js.map +1 -1
  157. package/dist/modes/base.d.ts.map +1 -1
  158. package/dist/modes/base.js +5 -0
  159. package/dist/modes/base.js.map +1 -1
  160. package/dist/planning/__tests__/approved-execution-lifecycle-matrix.test.d.ts +2 -0
  161. package/dist/planning/__tests__/approved-execution-lifecycle-matrix.test.d.ts.map +1 -0
  162. package/dist/planning/__tests__/approved-execution-lifecycle-matrix.test.js +316 -0
  163. package/dist/planning/__tests__/approved-execution-lifecycle-matrix.test.js.map +1 -0
  164. package/dist/planning/__tests__/approved-launch-hint-lineage-matrix.test.d.ts +2 -0
  165. package/dist/planning/__tests__/approved-launch-hint-lineage-matrix.test.d.ts.map +1 -0
  166. package/dist/planning/__tests__/approved-launch-hint-lineage-matrix.test.js +481 -0
  167. package/dist/planning/__tests__/approved-launch-hint-lineage-matrix.test.js.map +1 -0
  168. package/dist/planning/__tests__/artifacts.test.js +597 -4
  169. package/dist/planning/__tests__/artifacts.test.js.map +1 -1
  170. package/dist/planning/__tests__/context-pack-status.test.js +524 -0
  171. package/dist/planning/__tests__/context-pack-status.test.js.map +1 -1
  172. package/dist/planning/__tests__/markdown-structure.test.d.ts +2 -0
  173. package/dist/planning/__tests__/markdown-structure.test.d.ts.map +1 -0
  174. package/dist/planning/__tests__/markdown-structure.test.js +459 -0
  175. package/dist/planning/__tests__/markdown-structure.test.js.map +1 -0
  176. package/dist/planning/__tests__/ready-context-pack-role-refs.test.d.ts +2 -0
  177. package/dist/planning/__tests__/ready-context-pack-role-refs.test.d.ts.map +1 -0
  178. package/dist/planning/__tests__/ready-context-pack-role-refs.test.js +612 -0
  179. package/dist/planning/__tests__/ready-context-pack-role-refs.test.js.map +1 -0
  180. package/dist/planning/artifacts.d.ts +7 -2
  181. package/dist/planning/artifacts.d.ts.map +1 -1
  182. package/dist/planning/artifacts.js +279 -26
  183. package/dist/planning/artifacts.js.map +1 -1
  184. package/dist/planning/context-pack-status.d.ts +31 -0
  185. package/dist/planning/context-pack-status.d.ts.map +1 -1
  186. package/dist/planning/context-pack-status.js +291 -25
  187. package/dist/planning/context-pack-status.js.map +1 -1
  188. package/dist/planning/markdown-structure.d.ts +20 -0
  189. package/dist/planning/markdown-structure.d.ts.map +1 -0
  190. package/dist/planning/markdown-structure.js +137 -0
  191. package/dist/planning/markdown-structure.js.map +1 -0
  192. package/dist/ralph/__tests__/completion-audit.test.d.ts +2 -0
  193. package/dist/ralph/__tests__/completion-audit.test.d.ts.map +1 -0
  194. package/dist/ralph/__tests__/completion-audit.test.js +121 -0
  195. package/dist/ralph/__tests__/completion-audit.test.js.map +1 -0
  196. package/dist/ralph/completion-audit.d.ts +8 -0
  197. package/dist/ralph/completion-audit.d.ts.map +1 -0
  198. package/dist/ralph/completion-audit.js +99 -0
  199. package/dist/ralph/completion-audit.js.map +1 -0
  200. package/dist/ralph/persistence.d.ts +1 -1
  201. package/dist/ralph/persistence.d.ts.map +1 -1
  202. package/dist/ralph/persistence.js +8 -2
  203. package/dist/ralph/persistence.js.map +1 -1
  204. package/dist/scripts/__tests__/codex-native-hook.test.js +359 -24
  205. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  206. package/dist/scripts/__tests__/notify-dispatcher.test.d.ts +2 -0
  207. package/dist/scripts/__tests__/notify-dispatcher.test.d.ts.map +1 -0
  208. package/dist/scripts/__tests__/notify-dispatcher.test.js +126 -0
  209. package/dist/scripts/__tests__/notify-dispatcher.test.js.map +1 -0
  210. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  211. package/dist/scripts/codex-native-hook.js +142 -76
  212. package/dist/scripts/codex-native-hook.js.map +1 -1
  213. package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
  214. package/dist/scripts/codex-native-pre-post.js +4 -2
  215. package/dist/scripts/codex-native-pre-post.js.map +1 -1
  216. package/dist/scripts/notify-dispatcher.d.ts +7 -0
  217. package/dist/scripts/notify-dispatcher.d.ts.map +1 -0
  218. package/dist/scripts/notify-dispatcher.js +87 -0
  219. package/dist/scripts/notify-dispatcher.js.map +1 -0
  220. package/dist/scripts/notify-fallback-watcher.js +4 -0
  221. package/dist/scripts/notify-fallback-watcher.js.map +1 -1
  222. package/dist/scripts/notify-hook/ralph-session-resume.d.ts.map +1 -1
  223. package/dist/scripts/notify-hook/ralph-session-resume.js +96 -8
  224. package/dist/scripts/notify-hook/ralph-session-resume.js.map +1 -1
  225. package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
  226. package/dist/scripts/notify-hook/state-io.js +6 -2
  227. package/dist/scripts/notify-hook/state-io.js.map +1 -1
  228. package/dist/scripts/notify-hook/visual-verdict.js +3 -3
  229. package/dist/scripts/notify-hook/visual-verdict.js.map +1 -1
  230. package/dist/scripts/notify-hook.js +127 -1
  231. package/dist/scripts/notify-hook.js.map +1 -1
  232. package/dist/state/__tests__/workflow-transition.test.js +102 -27
  233. package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
  234. package/dist/state/operations.d.ts.map +1 -1
  235. package/dist/state/operations.js +9 -3
  236. package/dist/state/operations.js.map +1 -1
  237. package/dist/state/skill-active.d.ts +7 -0
  238. package/dist/state/skill-active.d.ts.map +1 -1
  239. package/dist/state/skill-active.js +25 -8
  240. package/dist/state/skill-active.js.map +1 -1
  241. package/dist/state/workflow-transition-reconcile.d.ts +1 -0
  242. package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
  243. package/dist/state/workflow-transition-reconcile.js +22 -15
  244. package/dist/state/workflow-transition-reconcile.js.map +1 -1
  245. package/dist/state/workflow-transition.js +3 -3
  246. package/dist/state/workflow-transition.js.map +1 -1
  247. package/dist/team/__tests__/approved-execution.test.js +84 -1
  248. package/dist/team/__tests__/approved-execution.test.js.map +1 -1
  249. package/dist/team/__tests__/runtime.test.js +178 -19
  250. package/dist/team/__tests__/runtime.test.js.map +1 -1
  251. package/dist/team/__tests__/scaling.test.js +497 -2
  252. package/dist/team/__tests__/scaling.test.js.map +1 -1
  253. package/dist/team/__tests__/state-root.test.js +1 -1
  254. package/dist/team/__tests__/state-root.test.js.map +1 -1
  255. package/dist/team/__tests__/worker-bootstrap.test.js +45 -0
  256. package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
  257. package/dist/team/approved-execution.d.ts +1 -0
  258. package/dist/team/approved-execution.d.ts.map +1 -1
  259. package/dist/team/approved-execution.js +53 -0
  260. package/dist/team/approved-execution.js.map +1 -1
  261. package/dist/team/delivery-log.d.ts.map +1 -1
  262. package/dist/team/delivery-log.js +8 -1
  263. package/dist/team/delivery-log.js.map +1 -1
  264. package/dist/team/runtime.d.ts.map +1 -1
  265. package/dist/team/runtime.js +104 -18
  266. package/dist/team/runtime.js.map +1 -1
  267. package/dist/team/scaling.d.ts.map +1 -1
  268. package/dist/team/scaling.js +43 -0
  269. package/dist/team/scaling.js.map +1 -1
  270. package/dist/team/state/mailbox.d.ts +1 -0
  271. package/dist/team/state/mailbox.d.ts.map +1 -1
  272. package/dist/team/state/mailbox.js +10 -1
  273. package/dist/team/state/mailbox.js.map +1 -1
  274. package/dist/team/state-root.d.ts.map +1 -1
  275. package/dist/team/state-root.js +5 -1
  276. package/dist/team/state-root.js.map +1 -1
  277. package/dist/team/state.d.ts.map +1 -1
  278. package/dist/team/state.js +3 -7
  279. package/dist/team/state.js.map +1 -1
  280. package/dist/team/worker-bootstrap.d.ts +7 -2
  281. package/dist/team/worker-bootstrap.d.ts.map +1 -1
  282. package/dist/team/worker-bootstrap.js +17 -4
  283. package/dist/team/worker-bootstrap.js.map +1 -1
  284. package/dist/ultragoal/__tests__/artifacts.test.js +124 -1
  285. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
  286. package/dist/ultragoal/__tests__/docs-contract.test.js +21 -0
  287. package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
  288. package/dist/ultragoal/artifacts.d.ts +44 -2
  289. package/dist/ultragoal/artifacts.d.ts.map +1 -1
  290. package/dist/ultragoal/artifacts.js +197 -13
  291. package/dist/ultragoal/artifacts.js.map +1 -1
  292. package/dist/wiki/lifecycle.js +1 -1
  293. package/dist/wiki/lifecycle.js.map +1 -1
  294. package/package.json +1 -1
  295. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  296. package/plugins/oh-my-codex/.mcp.json +5 -5
  297. package/plugins/oh-my-codex/skills/analyze/SKILL.md +0 -2
  298. package/plugins/oh-my-codex/skills/autopilot/SKILL.md +2 -2
  299. package/plugins/oh-my-codex/skills/code-review/SKILL.md +1 -3
  300. package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +5 -7
  301. package/plugins/oh-my-codex/skills/doctor/SKILL.md +2 -2
  302. package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +3 -3
  303. package/plugins/oh-my-codex/skills/pipeline/SKILL.md +3 -3
  304. package/plugins/oh-my-codex/skills/plan/SKILL.md +3 -6
  305. package/plugins/oh-my-codex/skills/ralph/SKILL.md +9 -10
  306. package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +36 -3
  307. package/plugins/oh-my-codex/skills/ultraqa/SKILL.md +21 -24
  308. package/plugins/oh-my-codex/skills/ultrawork/SKILL.md +8 -8
  309. package/plugins/oh-my-codex/skills/wiki/SKILL.md +13 -13
  310. package/skills/analyze/SKILL.md +0 -2
  311. package/skills/ask-claude/SKILL.md +5 -3
  312. package/skills/ask-gemini/SKILL.md +5 -3
  313. package/skills/autopilot/SKILL.md +2 -2
  314. package/skills/code-review/SKILL.md +1 -3
  315. package/skills/deep-interview/SKILL.md +5 -7
  316. package/skills/doctor/SKILL.md +2 -2
  317. package/skills/ecomode/SKILL.md +105 -1
  318. package/skills/frontend-ui-ux/SKILL.md +4 -26
  319. package/skills/git-master/SKILL.md +2 -4
  320. package/skills/omx-setup/SKILL.md +3 -3
  321. package/skills/pipeline/SKILL.md +3 -3
  322. package/skills/plan/SKILL.md +3 -6
  323. package/skills/ralph/SKILL.md +9 -10
  324. package/skills/swarm/SKILL.md +5 -3
  325. package/skills/tdd/SKILL.md +95 -1
  326. package/skills/ultragoal/SKILL.md +36 -3
  327. package/skills/ultraqa/SKILL.md +21 -24
  328. package/skills/ultrawork/SKILL.md +8 -8
  329. package/skills/web-clone/SKILL.md +348 -1
  330. package/skills/wiki/SKILL.md +13 -13
  331. package/src/scripts/__tests__/codex-native-hook.test.ts +389 -24
  332. package/src/scripts/__tests__/notify-dispatcher.test.ts +153 -0
  333. package/src/scripts/codex-native-hook.ts +168 -64
  334. package/src/scripts/codex-native-pre-post.ts +4 -1
  335. package/src/scripts/notify-dispatcher.ts +113 -0
  336. package/src/scripts/notify-fallback-watcher.ts +6 -2
  337. package/src/scripts/notify-hook/ralph-session-resume.ts +117 -8
  338. package/src/scripts/notify-hook/state-io.ts +4 -2
  339. package/src/scripts/notify-hook/visual-verdict.ts +3 -3
  340. package/src/scripts/notify-hook.ts +119 -1
@@ -1,12 +1,14 @@
1
1
  import { describe, it } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
+ import { createHash } from 'node:crypto';
3
4
  import { execFileSync } from 'node:child_process';
4
- import { mkdtemp, rm, readFile, writeFile, mkdir, chmod } from 'fs/promises';
5
- import { join } from 'path';
5
+ import { mkdtemp, rm, readFile, writeFile, mkdir, chmod, readdir } from 'fs/promises';
6
+ import { join, relative } from 'path';
6
7
  import { tmpdir } from 'os';
7
8
  import { existsSync, readFileSync } from 'fs';
8
9
  import { initTeamState, createTask, readTask, readTeamConfig, saveTeamConfig, readWorkerStatus, writeWorkerStatus, withScalingLock, DEFAULT_MAX_WORKERS, } from '../state.js';
9
10
  import { isScalingEnabled, scaleUp, scaleDown } from '../scaling.js';
11
+ import { resolvePersistedApprovedTeamExecutionContinuityState, writePersistedApprovedTeamExecutionBinding, } from '../approved-execution.js';
10
12
  delete process.env.OMX_TEAM_STATE_ROOT;
11
13
  async function initCommittedGitRepo(cwd) {
12
14
  execFileSync('git', ['init'], { cwd, stdio: 'pipe' });
@@ -25,6 +27,245 @@ async function initRepo() {
25
27
  execFileSync('git', ['commit', '-m', 'init'], { cwd, stdio: 'ignore' });
26
28
  return cwd;
27
29
  }
30
+ function computeGitBlobSha1(content) {
31
+ const buffer = Buffer.from(content, 'utf-8');
32
+ const header = Buffer.from(`blob ${buffer.length}\0`, 'utf-8');
33
+ return createHash('sha1').update(header).update(buffer).digest('hex');
34
+ }
35
+ function canonicalContextPackRelativePath(slug) {
36
+ return `.omx/context/context-20260507T120000Z-${slug}.json`;
37
+ }
38
+ function buildContextPackOutcome(relativePackPath) {
39
+ return [
40
+ '## Context Pack Outcome',
41
+ '',
42
+ `- pack: created \`${relativePackPath}\``,
43
+ ].join('\n');
44
+ }
45
+ const BLOCKED_SCALE_UP_APPROVED_BINDING_STATES = [
46
+ 'malformed',
47
+ 'stale',
48
+ 'ambiguous',
49
+ 'missing-baseline',
50
+ 'incomplete',
51
+ 'invalid',
52
+ ];
53
+ const SCALE_UP_STATE_TEAM_SUFFIX = {
54
+ missing: 'miss',
55
+ malformed: 'mal',
56
+ stale: 'stale',
57
+ ambiguous: 'amb',
58
+ 'missing-baseline': 'mbase',
59
+ 'plan-only': 'ponly',
60
+ incomplete: 'inc',
61
+ invalid: 'inv',
62
+ ready: 'ready',
63
+ };
64
+ const SCALE_UP_APPROVED_BINDING_STATES = [
65
+ 'missing',
66
+ ...BLOCKED_SCALE_UP_APPROVED_BINDING_STATES,
67
+ 'plan-only',
68
+ 'ready',
69
+ ];
70
+ const SCALE_UP_COUNTS = [1, 2, 3];
71
+ function assertNeverScaleUpState(state) {
72
+ throw new Error(`unexpected scale-up approved binding state: ${state}`);
73
+ }
74
+ function expectedScaleUpOutcome(state) {
75
+ if (state === 'missing' || state === 'plan-only') {
76
+ return 'generic';
77
+ }
78
+ return state === 'ready' ? 'approved' : 'blocked';
79
+ }
80
+ function forbiddenScaleUpOutcomes(state) {
81
+ switch (state) {
82
+ case 'missing':
83
+ case 'plan-only':
84
+ return ['blocked', 'approved'];
85
+ case 'ready':
86
+ return ['blocked', 'generic'];
87
+ case 'malformed':
88
+ case 'stale':
89
+ case 'ambiguous':
90
+ case 'missing-baseline':
91
+ case 'incomplete':
92
+ case 'invalid':
93
+ return ['generic', 'approved'];
94
+ default:
95
+ return assertNeverScaleUpState(state);
96
+ }
97
+ }
98
+ function buildScaleUpScenarioTasks(state, count) {
99
+ return Array.from({ length: count }, (_, index) => {
100
+ const workerIndex = index + 2;
101
+ return {
102
+ subject: `Implement ${state} follow-up ${workerIndex}/${count}`,
103
+ description: `Implement ${state} follow-up ${workerIndex}/${count}`,
104
+ owner: `worker-${workerIndex}`,
105
+ };
106
+ });
107
+ }
108
+ async function writeContextPack(cwd, slug, prdPath, testSpecPath, roles) {
109
+ const contextDir = join(cwd, '.omx', 'context');
110
+ const packPath = join(cwd, canonicalContextPackRelativePath(slug));
111
+ const prdContent = await readFile(prdPath, 'utf-8');
112
+ const testSpecContent = await readFile(testSpecPath, 'utf-8');
113
+ await mkdir(contextDir, { recursive: true });
114
+ await writeFile(packPath, JSON.stringify({
115
+ slug,
116
+ basis: {
117
+ prd: {
118
+ path: relative(cwd, prdPath).replaceAll('\\', '/'),
119
+ sha1: computeGitBlobSha1(prdContent),
120
+ },
121
+ testSpecs: [{
122
+ path: relative(cwd, testSpecPath).replaceAll('\\', '/'),
123
+ sha1: computeGitBlobSha1(testSpecContent),
124
+ }],
125
+ },
126
+ entries: roles.map((role, index) => ({
127
+ path: `src/${role}-${index}.ts`,
128
+ roles: [role],
129
+ })),
130
+ }, null, 2));
131
+ }
132
+ async function writeReadyContextPack(cwd, slug, prdPath, testSpecPath) {
133
+ await writeContextPack(cwd, slug, prdPath, testSpecPath, ['scope', 'build', 'verify']);
134
+ }
135
+ async function writeSuccessfulScaleUpTmuxStub(fakeBinDir, tmuxLogPath) {
136
+ const tmuxStubPath = join(fakeBinDir, 'tmux');
137
+ await writeFile(tmuxStubPath, [
138
+ '#!/bin/sh',
139
+ 'set -eu',
140
+ `printf '%s\n' "$*" >> "${tmuxLogPath}"`,
141
+ 'case "${1:-}" in',
142
+ ' -V)',
143
+ ' echo "tmux 3.2a"',
144
+ ' ;;',
145
+ ' split-window)',
146
+ ' echo "%31"',
147
+ ' ;;',
148
+ ' list-panes)',
149
+ ' echo "42424"',
150
+ ' ;;',
151
+ ' send-keys)',
152
+ ' ;;',
153
+ ' capture-pane)',
154
+ ' echo ""',
155
+ ' ;;',
156
+ 'esac',
157
+ 'exit 0',
158
+ '',
159
+ ].join('\n'));
160
+ await chmod(tmuxStubPath, 0o755);
161
+ await writeFile(tmuxLogPath, '');
162
+ }
163
+ async function configureScaleUpTeamForDirectDispatch(teamName, cwd) {
164
+ const config = await readTeamConfig(teamName, cwd);
165
+ assert.ok(config);
166
+ if (!config) {
167
+ throw new Error(`missing team config for ${teamName}`);
168
+ }
169
+ config.tmux_session = `omx-team-${teamName}`;
170
+ config.leader_pane_id = '%11';
171
+ config.workers[0].pane_id = '%21';
172
+ await saveTeamConfig(config, cwd);
173
+ const manifestPath = join(cwd, '.omx', 'state', 'team', teamName, 'manifest.v2.json');
174
+ const manifest = JSON.parse(await readFile(manifestPath, 'utf-8'));
175
+ manifest.policy = {
176
+ ...(manifest.policy ?? {}),
177
+ dispatch_mode: 'transport_direct',
178
+ };
179
+ await writeFile(manifestPath, JSON.stringify(manifest, null, 2));
180
+ }
181
+ async function readScaleUpTmuxLogCommands(tmuxLogPath) {
182
+ const content = await readFile(tmuxLogPath, 'utf-8');
183
+ const trimmed = content.trim();
184
+ return trimmed === '' ? [] : trimmed.split('\n');
185
+ }
186
+ async function readScaleUpTaskPayloads(teamName, cwd) {
187
+ const tasksDir = join(cwd, '.omx', 'state', 'team', teamName, 'tasks');
188
+ if (!existsSync(tasksDir)) {
189
+ return [];
190
+ }
191
+ const taskFiles = (await readdir(tasksDir)).filter((entry) => entry.endsWith('.json')).sort();
192
+ return await Promise.all(taskFiles.map((entry) => readFile(join(tasksDir, entry), 'utf-8')));
193
+ }
194
+ async function readExpectedScaleUpApprovedBindingError(teamName, cwd) {
195
+ const continuity = await resolvePersistedApprovedTeamExecutionContinuityState(teamName, cwd);
196
+ if (continuity.status === 'missing') {
197
+ return null;
198
+ }
199
+ if (continuity.status === 'malformed') {
200
+ return `approved_execution_binding_malformed:${teamName}`;
201
+ }
202
+ if (continuity.status === 'ambiguous') {
203
+ return `approved_execution_binding_ambiguous:${continuity.binding.prd_path}:${continuity.binding.task}`;
204
+ }
205
+ if (continuity.status === 'stale') {
206
+ return `approved_execution_binding_stale:${continuity.binding.prd_path}:${continuity.binding.task}`;
207
+ }
208
+ return continuity.approvedHint.contextPackStatus === 'ready'
209
+ || continuity.approvedHint.contextPackStatus === 'plan-only'
210
+ ? null
211
+ : `approved_execution_binding_nonready:${continuity.approvedHint.contextPackStatus}:${continuity.binding.prd_path}:${continuity.binding.task}`;
212
+ }
213
+ async function prepareScaleUpApprovedBindingState(teamName, cwd, state) {
214
+ if (state === 'malformed') {
215
+ await writeFile(join(cwd, '.omx', 'state', 'team', teamName, 'approved-execution.json'), '{"prd_path":42}\n');
216
+ return;
217
+ }
218
+ const plansDir = join(cwd, '.omx', 'plans');
219
+ const approvedTask = `Execute ${state} scale-up handoff`;
220
+ const prdPath = join(plansDir, `prd-${state}.md`);
221
+ const testSpecPath = join(plansDir, `test-spec-${state}.md`);
222
+ await mkdir(plansDir, { recursive: true });
223
+ if (state === 'stale') {
224
+ await writePersistedApprovedTeamExecutionBinding(teamName, cwd, {
225
+ prd_path: prdPath,
226
+ task: approvedTask,
227
+ command: `omx team 1:executor "${approvedTask}"`,
228
+ });
229
+ return;
230
+ }
231
+ if (state === 'ambiguous') {
232
+ await writeFile(prdPath, [
233
+ '# Approved plan',
234
+ '',
235
+ `Launch via omx team 1:executor "${approvedTask}"`,
236
+ `Launch via omx team 2:writer "${approvedTask}"`,
237
+ ].join('\n'));
238
+ await writePersistedApprovedTeamExecutionBinding(teamName, cwd, {
239
+ prd_path: prdPath,
240
+ task: approvedTask,
241
+ });
242
+ return;
243
+ }
244
+ const prdLines = ['# Approved plan', ''];
245
+ if (state === 'incomplete' || state === 'invalid' || state === 'ready') {
246
+ prdLines.push(buildContextPackOutcome(canonicalContextPackRelativePath(state)), '');
247
+ }
248
+ prdLines.push(`Launch via omx team 1:executor "${approvedTask}"`);
249
+ await writeFile(prdPath, prdLines.join('\n'));
250
+ if (state !== 'missing-baseline') {
251
+ await writeFile(testSpecPath, `# ${state} test spec\n`);
252
+ }
253
+ if (state === 'incomplete') {
254
+ await writeContextPack(cwd, state, prdPath, testSpecPath, ['scope']);
255
+ }
256
+ if (state === 'invalid') {
257
+ await writeContextPack(cwd, state, prdPath, testSpecPath, ['scope', 'build', 'verify']);
258
+ await writeFile(testSpecPath, '# invalid drifted test spec\n');
259
+ }
260
+ if (state === 'ready') {
261
+ await writeContextPack(cwd, state, prdPath, testSpecPath, ['scope', 'build', 'verify']);
262
+ }
263
+ await writePersistedApprovedTeamExecutionBinding(teamName, cwd, {
264
+ prd_path: prdPath,
265
+ task: approvedTask,
266
+ command: `omx team 1:executor "${approvedTask}"`,
267
+ });
268
+ }
28
269
  // ── isScalingEnabled ──────────────────────────────────────────────────────────
29
270
  describe('isScalingEnabled', () => {
30
271
  it('returns false when env var is not set', () => {
@@ -320,6 +561,260 @@ describe('scaleUp', () => {
320
561
  await rm(fakeBinDir, { recursive: true, force: true });
321
562
  }
322
563
  });
564
+ it('keeps scale-up on the generic path when no approved binding is persisted', async () => {
565
+ const teamName = 'scale-up-no-approved-binding';
566
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-scale-up-no-approved-binding-'));
567
+ const fakeBinDir = await mkdtemp(join(tmpdir(), 'omx-scale-up-no-approved-binding-bin-'));
568
+ const tmuxLogPath = join(fakeBinDir, 'tmux.log');
569
+ const previousPath = process.env.PATH;
570
+ try {
571
+ await writeSuccessfulScaleUpTmuxStub(fakeBinDir, tmuxLogPath);
572
+ process.env.PATH = `${fakeBinDir}:${previousPath ?? ''}`;
573
+ await initTeamState(teamName, 'generic scale-up test', 'executor', 1, cwd);
574
+ await configureScaleUpTeamForDirectDispatch(teamName, cwd);
575
+ assert.equal(await readExpectedScaleUpApprovedBindingError(teamName, cwd), null);
576
+ const result = await scaleUp(teamName, 1, 'executor', [{ subject: 'Implement generic follow-up', description: 'Implement generic follow-up', owner: 'worker-2' }], cwd, { OMX_TEAM_SCALING_ENABLED: '1', OMX_TEAM_SKIP_READY_WAIT: '1' });
577
+ assert.equal(result.ok, true);
578
+ if (!result.ok)
579
+ return;
580
+ const inbox = await readFile(join(cwd, '.omx', 'state', 'team', teamName, 'workers', 'worker-2', 'inbox.md'), 'utf-8');
581
+ const tmuxCommands = await readScaleUpTmuxLogCommands(tmuxLogPath);
582
+ assert.match(inbox, /Implement generic follow-up/);
583
+ assert.doesNotMatch(inbox, /## Approved Handoff Context/);
584
+ assert.ok(tmuxCommands.some((command) => command.startsWith('split-window ')));
585
+ }
586
+ finally {
587
+ if (typeof previousPath === 'string')
588
+ process.env.PATH = previousPath;
589
+ else
590
+ delete process.env.PATH;
591
+ await rm(cwd, { recursive: true, force: true });
592
+ await rm(fakeBinDir, { recursive: true, force: true });
593
+ }
594
+ });
595
+ it('keeps scale-up on the generic path when the persisted binding is only plan-only followup-ready', async () => {
596
+ const teamName = 'scale-up-plan-only';
597
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-scale-up-plan-only-'));
598
+ const fakeBinDir = await mkdtemp(join(tmpdir(), 'omx-scale-up-plan-only-bin-'));
599
+ const tmuxLogPath = join(fakeBinDir, 'tmux.log');
600
+ const previousPath = process.env.PATH;
601
+ try {
602
+ await writeSuccessfulScaleUpTmuxStub(fakeBinDir, tmuxLogPath);
603
+ process.env.PATH = `${fakeBinDir}:${previousPath ?? ''}`;
604
+ await initTeamState(teamName, 'plan-only scale-up test', 'executor', 1, cwd);
605
+ await configureScaleUpTeamForDirectDispatch(teamName, cwd);
606
+ await prepareScaleUpApprovedBindingState(teamName, cwd, 'plan-only');
607
+ assert.equal(await readExpectedScaleUpApprovedBindingError(teamName, cwd), null);
608
+ const result = await scaleUp(teamName, 1, 'executor', [{ subject: 'Implement plan-only follow-up', description: 'Implement plan-only follow-up', owner: 'worker-2' }], cwd, { OMX_TEAM_SCALING_ENABLED: '1', OMX_TEAM_SKIP_READY_WAIT: '1' });
609
+ assert.equal(result.ok, true);
610
+ if (!result.ok)
611
+ return;
612
+ const inbox = await readFile(join(cwd, '.omx', 'state', 'team', teamName, 'workers', 'worker-2', 'inbox.md'), 'utf-8');
613
+ const tmuxCommands = await readScaleUpTmuxLogCommands(tmuxLogPath);
614
+ assert.match(inbox, /Implement plan-only follow-up/);
615
+ assert.doesNotMatch(inbox, /## Approved Handoff Context/);
616
+ assert.ok(tmuxCommands.some((command) => command.startsWith('split-window ')));
617
+ }
618
+ finally {
619
+ if (typeof previousPath === 'string')
620
+ process.env.PATH = previousPath;
621
+ else
622
+ delete process.env.PATH;
623
+ await rm(cwd, { recursive: true, force: true });
624
+ await rm(fakeBinDir, { recursive: true, force: true });
625
+ }
626
+ });
627
+ it('injects approved handoff context into scaled worker inboxes when the persisted binding stays ready', async () => {
628
+ const teamName = 'scale-up-approved-context';
629
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-scale-up-approved-context-'));
630
+ const fakeBinDir = await mkdtemp(join(tmpdir(), 'omx-scale-up-approved-context-bin-'));
631
+ const tmuxLogPath = join(fakeBinDir, 'tmux.log');
632
+ const previousPath = process.env.PATH;
633
+ const approvedTask = 'Execute approved issue 1410 plan';
634
+ try {
635
+ await writeSuccessfulScaleUpTmuxStub(fakeBinDir, tmuxLogPath);
636
+ process.env.PATH = `${fakeBinDir}:${previousPath ?? ''}`;
637
+ await initTeamState(teamName, 'approved scale-up test', 'executor', 1, cwd);
638
+ await configureScaleUpTeamForDirectDispatch(teamName, cwd);
639
+ const plansDir = join(cwd, '.omx', 'plans');
640
+ await mkdir(plansDir, { recursive: true });
641
+ const prdPath = join(plansDir, 'prd-issue-1410.md');
642
+ const testSpecPath = join(plansDir, 'test-spec-issue-1410.md');
643
+ await writeFile(prdPath, [
644
+ '# Approved plan',
645
+ '',
646
+ buildContextPackOutcome(canonicalContextPackRelativePath('issue-1410')),
647
+ '',
648
+ `Launch via omx team 1:executor "${approvedTask}"`,
649
+ ].join('\n'));
650
+ await writeFile(testSpecPath, '# Test spec\n');
651
+ await writeReadyContextPack(cwd, 'issue-1410', prdPath, testSpecPath);
652
+ await writeFile(join(plansDir, 'repo-context-issue-1410.md'), 'Read the approved repository slice first.\n');
653
+ await writePersistedApprovedTeamExecutionBinding(teamName, cwd, {
654
+ prd_path: prdPath,
655
+ task: approvedTask,
656
+ command: `omx team 1:executor "${approvedTask}"`,
657
+ });
658
+ assert.equal(await readExpectedScaleUpApprovedBindingError(teamName, cwd), null);
659
+ const result = await scaleUp(teamName, 1, 'executor', [{ subject: 'Implement approved follow-up', description: 'Implement approved follow-up', owner: 'worker-2' }], cwd, { OMX_TEAM_SCALING_ENABLED: '1', OMX_TEAM_SKIP_READY_WAIT: '1' });
660
+ assert.equal(result.ok, true);
661
+ if (!result.ok)
662
+ return;
663
+ const inbox = await readFile(join(cwd, '.omx', 'state', 'team', teamName, 'workers', 'worker-2', 'inbox.md'), 'utf-8');
664
+ const tmuxCommands = await readScaleUpTmuxLogCommands(tmuxLogPath);
665
+ assert.match(inbox, /## Approved Handoff Context/);
666
+ assert.ok(inbox.includes(`Approved plan: ${prdPath}`));
667
+ assert.ok(inbox.includes(`Test specs: ${testSpecPath}`));
668
+ assert.match(inbox, /Approved repository context summary source: .*repo-context-issue-1410\.md/);
669
+ assert.match(inbox, /Read the approved repository slice first\./);
670
+ assert.match(inbox, /Build refs \(read first\): src\/build-1\.ts/);
671
+ assert.match(inbox, /Verify refs: src\/verify-2\.ts/);
672
+ assert.match(inbox, /Scope refs: src\/scope-0\.ts/);
673
+ assert.doesNotMatch(inbox, /query the canonical pack|Context pack index/);
674
+ assert.ok(tmuxCommands.some((command) => command.startsWith('split-window ')));
675
+ }
676
+ finally {
677
+ if (typeof previousPath === 'string')
678
+ process.env.PATH = previousPath;
679
+ else
680
+ delete process.env.PATH;
681
+ await rm(cwd, { recursive: true, force: true });
682
+ await rm(fakeBinDir, { recursive: true, force: true });
683
+ }
684
+ });
685
+ it('proves the approved-binding scale-up model across generated state/count scenarios, including forbidden counterfactuals', async () => {
686
+ for (const state of SCALE_UP_APPROVED_BINDING_STATES) {
687
+ for (const count of SCALE_UP_COUNTS) {
688
+ const teamName = `su-model-${SCALE_UP_STATE_TEAM_SUFFIX[state]}-${count}`;
689
+ const cwd = await mkdtemp(join(tmpdir(), `omx-scale-up-model-${state}-${count}-`));
690
+ const fakeBinDir = await mkdtemp(join(tmpdir(), `omx-scale-up-model-${state}-${count}-bin-`));
691
+ const tmuxLogPath = join(fakeBinDir, 'tmux.log');
692
+ const previousPath = process.env.PATH;
693
+ try {
694
+ await writeSuccessfulScaleUpTmuxStub(fakeBinDir, tmuxLogPath);
695
+ process.env.PATH = `${fakeBinDir}:${previousPath ?? ''}`;
696
+ await initTeamState(teamName, `approved ${state} scale-up model`, 'executor', 1, cwd);
697
+ await configureScaleUpTeamForDirectDispatch(teamName, cwd);
698
+ if (state !== 'missing') {
699
+ await prepareScaleUpApprovedBindingState(teamName, cwd, state);
700
+ }
701
+ const expectedOutcome = expectedScaleUpOutcome(state);
702
+ const expectedError = await readExpectedScaleUpApprovedBindingError(teamName, cwd);
703
+ const tasks = buildScaleUpScenarioTasks(state, count);
704
+ const result = await scaleUp(teamName, count, 'executor', tasks, cwd, { OMX_TEAM_SCALING_ENABLED: '1', OMX_TEAM_SKIP_READY_WAIT: '1' });
705
+ const tmuxCommands = await readScaleUpTmuxLogCommands(tmuxLogPath);
706
+ const splitWindowCommands = tmuxCommands.filter((command) => command.startsWith('split-window '));
707
+ const inboxes = await Promise.all(tasks.map(async (task) => {
708
+ const inboxPath = join(cwd, '.omx', 'state', 'team', teamName, 'workers', task.owner, 'inbox.md');
709
+ return existsSync(inboxPath)
710
+ ? await readFile(inboxPath, 'utf-8')
711
+ : null;
712
+ }));
713
+ const approvedInboxCount = inboxes.filter((inbox) => typeof inbox === 'string' && inbox.includes('## Approved Handoff Context')).length;
714
+ assert.ok(approvedInboxCount === 0 || approvedInboxCount === tasks.length, `expected approved handoff context presence to stay consistent across all scaled workers (state=${state} count=${count})`);
715
+ const observedOutcome = !result.ok
716
+ ? 'blocked'
717
+ : approvedInboxCount === tasks.length
718
+ ? 'approved'
719
+ : 'generic';
720
+ const taskPayloads = await readScaleUpTaskPayloads(teamName, cwd);
721
+ assert.equal(observedOutcome, expectedOutcome, `state=${state} count=${count}`);
722
+ assert.equal(forbiddenScaleUpOutcomes(state).includes(observedOutcome), false, `state=${state} count=${count} produced forbidden counterfactual outcome ${observedOutcome}`);
723
+ if (expectedOutcome === 'blocked') {
724
+ assert.equal(result.ok, false);
725
+ if (result.ok) {
726
+ throw new Error(`expected blocked scale-up outcome for ${state} count=${count}`);
727
+ }
728
+ assert.equal(result.error, expectedError);
729
+ assert.deepEqual(tmuxCommands, ['-V']);
730
+ assert.deepEqual(splitWindowCommands, []);
731
+ assert.ok(inboxes.every((inbox) => inbox === null));
732
+ assert.equal(taskPayloads.some((payload) => tasks.some((task) => payload.includes(task.subject))), false);
733
+ const config = await readTeamConfig(teamName, cwd);
734
+ assert.ok(config);
735
+ if (!config) {
736
+ throw new Error(`missing team config for ${teamName}`);
737
+ }
738
+ assert.equal(config.workers.length, 1);
739
+ assert.equal(config.worker_count, 1);
740
+ assert.equal(config.next_worker_index, 2);
741
+ continue;
742
+ }
743
+ assert.equal(result.ok, true);
744
+ if (!result.ok) {
745
+ throw new Error(`expected successful scale-up outcome for ${state} count=${count}`);
746
+ }
747
+ assert.equal(result.newWorkerCount, 1 + count);
748
+ assert.equal(result.nextWorkerIndex, 2 + count);
749
+ assert.equal(splitWindowCommands.length, count);
750
+ assert.equal(expectedError, null);
751
+ assert.ok(inboxes.every((inbox) => typeof inbox === 'string'));
752
+ for (const [index, inbox] of inboxes.entries()) {
753
+ const task = tasks[index];
754
+ assert.ok(inbox.includes(task.subject), `expected inbox to include task subject ${task.subject}`);
755
+ }
756
+ assert.equal(taskPayloads.filter((payload) => tasks.some((task) => payload.includes(task.subject))).length, count);
757
+ if (expectedOutcome === 'approved') {
758
+ assert.ok(inboxes.every((inbox) => inbox.includes('## Approved Handoff Context')));
759
+ }
760
+ else {
761
+ assert.ok(inboxes.every((inbox) => !inbox.includes('## Approved Handoff Context')));
762
+ }
763
+ }
764
+ finally {
765
+ if (typeof previousPath === 'string')
766
+ process.env.PATH = previousPath;
767
+ else
768
+ delete process.env.PATH;
769
+ await rm(cwd, { recursive: true, force: true });
770
+ await rm(fakeBinDir, { recursive: true, force: true });
771
+ }
772
+ }
773
+ }
774
+ });
775
+ for (const state of BLOCKED_SCALE_UP_APPROVED_BINDING_STATES) {
776
+ it(`fails closed before worker launch when the persisted approved binding is ${state}`, async () => {
777
+ const teamName = `su-block-${SCALE_UP_STATE_TEAM_SUFFIX[state]}`;
778
+ const cwd = await mkdtemp(join(tmpdir(), `omx-scale-up-approved-${state}-`));
779
+ const fakeBinDir = await mkdtemp(join(tmpdir(), `omx-scale-up-approved-${state}-bin-`));
780
+ const tmuxLogPath = join(fakeBinDir, 'tmux.log');
781
+ const previousPath = process.env.PATH;
782
+ try {
783
+ await writeSuccessfulScaleUpTmuxStub(fakeBinDir, tmuxLogPath);
784
+ process.env.PATH = `${fakeBinDir}:${previousPath ?? ''}`;
785
+ await initTeamState(teamName, `approved ${state} scale-up test`, 'executor', 1, cwd);
786
+ await configureScaleUpTeamForDirectDispatch(teamName, cwd);
787
+ await prepareScaleUpApprovedBindingState(teamName, cwd, state);
788
+ const expectedError = await readExpectedScaleUpApprovedBindingError(teamName, cwd);
789
+ assert.ok(expectedError);
790
+ const result = await scaleUp(teamName, 1, 'executor', [{ subject: 'Implement approved follow-up', description: 'Implement approved follow-up', owner: 'worker-2' }], cwd, { OMX_TEAM_SCALING_ENABLED: '1', OMX_TEAM_SKIP_READY_WAIT: '1' });
791
+ assert.equal(result.ok, false);
792
+ if (result.ok)
793
+ return;
794
+ assert.equal(result.error, expectedError);
795
+ const config = await readTeamConfig(teamName, cwd);
796
+ assert.ok(config);
797
+ if (!config)
798
+ return;
799
+ assert.equal(config.workers.length, 1);
800
+ assert.equal(config.worker_count, 1);
801
+ assert.equal(config.next_worker_index, 2);
802
+ const taskPayloads = await readScaleUpTaskPayloads(teamName, cwd);
803
+ assert.equal(taskPayloads.some((payload) => payload.includes('Implement approved follow-up')), false);
804
+ assert.equal(existsSync(join(cwd, '.omx', 'state', 'team', teamName, 'workers', 'worker-2', 'identity.json')), false);
805
+ assert.equal(existsSync(join(cwd, '.omx', 'state', 'team', teamName, 'workers', 'worker-2', 'inbox.md')), false);
806
+ assert.deepEqual(await readScaleUpTmuxLogCommands(tmuxLogPath), ['-V']);
807
+ }
808
+ finally {
809
+ if (typeof previousPath === 'string')
810
+ process.env.PATH = previousPath;
811
+ else
812
+ delete process.env.PATH;
813
+ await rm(cwd, { recursive: true, force: true });
814
+ await rm(fakeBinDir, { recursive: true, force: true });
815
+ }
816
+ });
817
+ }
323
818
  it('uses project-scoped CODEX_HOME for scaled worker reasoning and model defaults', async () => {
324
819
  const cwd = await mkdtemp(join(tmpdir(), 'omx-scale-up-project-reasoning-'));
325
820
  const fakeBinDir = await mkdtemp(join(tmpdir(), 'omx-scale-up-project-reasoning-bin-'));