oh-my-codex 0.17.3 → 0.18.1

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 (381) hide show
  1. package/Cargo.lock +13 -5
  2. package/Cargo.toml +2 -1
  3. package/README.md +44 -19
  4. package/crates/omx-api/Cargo.toml +19 -0
  5. package/crates/omx-api/src/lib.rs +2997 -0
  6. package/crates/omx-api/src/main.rs +10 -0
  7. package/crates/omx-api/tests/cli.rs +558 -0
  8. package/crates/omx-explore/src/main.rs +4 -0
  9. package/crates/omx-sparkshell/src/codex_bridge.rs +437 -123
  10. package/crates/omx-sparkshell/src/exec.rs +127 -1
  11. package/crates/omx-sparkshell/src/main.rs +829 -30
  12. package/crates/omx-sparkshell/src/prompt.rs +25 -3
  13. package/crates/omx-sparkshell/src/redaction.rs +241 -0
  14. package/crates/omx-sparkshell/tests/execution.rs +702 -237
  15. package/dist/cli/__tests__/api.test.d.ts +2 -0
  16. package/dist/cli/__tests__/api.test.d.ts.map +1 -0
  17. package/dist/cli/__tests__/api.test.js +175 -0
  18. package/dist/cli/__tests__/api.test.js.map +1 -0
  19. package/dist/cli/__tests__/ask.test.js +72 -5
  20. package/dist/cli/__tests__/ask.test.js.map +1 -1
  21. package/dist/cli/__tests__/autoresearch-goal.test.js +14 -1
  22. package/dist/cli/__tests__/autoresearch-goal.test.js.map +1 -1
  23. package/dist/cli/__tests__/codex-plugin-layout.test.js +15 -7
  24. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  25. package/dist/cli/__tests__/doctor-warning-copy.test.js +76 -3
  26. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  27. package/dist/cli/__tests__/explore.test.js +23 -0
  28. package/dist/cli/__tests__/explore.test.js.map +1 -1
  29. package/dist/cli/__tests__/index.test.js +171 -5
  30. package/dist/cli/__tests__/index.test.js.map +1 -1
  31. package/dist/cli/__tests__/install-docs-contract.test.d.ts +2 -0
  32. package/dist/cli/__tests__/install-docs-contract.test.d.ts.map +1 -0
  33. package/dist/cli/__tests__/install-docs-contract.test.js +55 -0
  34. package/dist/cli/__tests__/install-docs-contract.test.js.map +1 -0
  35. package/dist/cli/__tests__/launch-fallback.test.js +191 -0
  36. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  37. package/dist/cli/__tests__/package-bin-contract.test.js +4 -3
  38. package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
  39. package/dist/cli/__tests__/question.test.js +27 -41
  40. package/dist/cli/__tests__/question.test.js.map +1 -1
  41. package/dist/cli/__tests__/setup-install-mode.test.js +232 -35
  42. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  43. package/dist/cli/__tests__/sparkshell-cli.test.js +25 -1
  44. package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
  45. package/dist/cli/__tests__/sparkshell-packaging.test.js +1 -0
  46. package/dist/cli/__tests__/sparkshell-packaging.test.js.map +1 -1
  47. package/dist/cli/__tests__/ultragoal.test.js +227 -4
  48. package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
  49. package/dist/cli/__tests__/update.test.js +72 -1
  50. package/dist/cli/__tests__/update.test.js.map +1 -1
  51. package/dist/cli/__tests__/version-sync-contract.test.js +4 -0
  52. package/dist/cli/__tests__/version-sync-contract.test.js.map +1 -1
  53. package/dist/cli/__tests__/windows-popup-loop-contract.test.js +1 -1
  54. package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
  55. package/dist/cli/api.d.ts +26 -0
  56. package/dist/cli/api.d.ts.map +1 -0
  57. package/dist/cli/api.js +153 -0
  58. package/dist/cli/api.js.map +1 -0
  59. package/dist/cli/codex-feature-probe.d.ts +5 -0
  60. package/dist/cli/codex-feature-probe.d.ts.map +1 -1
  61. package/dist/cli/codex-feature-probe.js +13 -7
  62. package/dist/cli/codex-feature-probe.js.map +1 -1
  63. package/dist/cli/doctor.d.ts +7 -0
  64. package/dist/cli/doctor.d.ts.map +1 -1
  65. package/dist/cli/doctor.js +119 -10
  66. package/dist/cli/doctor.js.map +1 -1
  67. package/dist/cli/explore.d.ts +2 -0
  68. package/dist/cli/explore.d.ts.map +1 -1
  69. package/dist/cli/explore.js +43 -1
  70. package/dist/cli/explore.js.map +1 -1
  71. package/dist/cli/index.d.ts +12 -4
  72. package/dist/cli/index.d.ts.map +1 -1
  73. package/dist/cli/index.js +460 -87
  74. package/dist/cli/index.js.map +1 -1
  75. package/dist/cli/native-assets.d.ts +2 -1
  76. package/dist/cli/native-assets.d.ts.map +1 -1
  77. package/dist/cli/native-assets.js +1 -0
  78. package/dist/cli/native-assets.js.map +1 -1
  79. package/dist/cli/plugin-marketplace.d.ts +2 -0
  80. package/dist/cli/plugin-marketplace.d.ts.map +1 -1
  81. package/dist/cli/plugin-marketplace.js +15 -1
  82. package/dist/cli/plugin-marketplace.js.map +1 -1
  83. package/dist/cli/setup.d.ts.map +1 -1
  84. package/dist/cli/setup.js +71 -11
  85. package/dist/cli/setup.js.map +1 -1
  86. package/dist/cli/sparkshell.d.ts +7 -1
  87. package/dist/cli/sparkshell.d.ts.map +1 -1
  88. package/dist/cli/sparkshell.js +31 -4
  89. package/dist/cli/sparkshell.js.map +1 -1
  90. package/dist/cli/ultragoal.d.ts +1 -1
  91. package/dist/cli/ultragoal.d.ts.map +1 -1
  92. package/dist/cli/ultragoal.js +184 -10
  93. package/dist/cli/ultragoal.js.map +1 -1
  94. package/dist/cli/update.d.ts +2 -0
  95. package/dist/cli/update.d.ts.map +1 -1
  96. package/dist/cli/update.js +14 -3
  97. package/dist/cli/update.js.map +1 -1
  98. package/dist/compat/__tests__/doctor-contract.test.js +3 -0
  99. package/dist/compat/__tests__/doctor-contract.test.js.map +1 -1
  100. package/dist/config/__tests__/codex-feature-flags.test.js +11 -1
  101. package/dist/config/__tests__/codex-feature-flags.test.js.map +1 -1
  102. package/dist/config/__tests__/codex-hooks.test.js +19 -8
  103. package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
  104. package/dist/config/__tests__/commit-lore-guard.test.d.ts +2 -0
  105. package/dist/config/__tests__/commit-lore-guard.test.d.ts.map +1 -0
  106. package/dist/config/__tests__/commit-lore-guard.test.js +20 -0
  107. package/dist/config/__tests__/commit-lore-guard.test.js.map +1 -0
  108. package/dist/config/codex-feature-flags.d.ts +4 -0
  109. package/dist/config/codex-feature-flags.d.ts.map +1 -1
  110. package/dist/config/codex-feature-flags.js +4 -0
  111. package/dist/config/codex-feature-flags.js.map +1 -1
  112. package/dist/config/codex-hooks.js +6 -6
  113. package/dist/config/codex-hooks.js.map +1 -1
  114. package/dist/config/commit-lore-guard.d.ts +1 -0
  115. package/dist/config/commit-lore-guard.d.ts.map +1 -1
  116. package/dist/config/commit-lore-guard.js +29 -3
  117. package/dist/config/commit-lore-guard.js.map +1 -1
  118. package/dist/config/generator.d.ts +3 -1
  119. package/dist/config/generator.d.ts.map +1 -1
  120. package/dist/config/generator.js +114 -10
  121. package/dist/config/generator.js.map +1 -1
  122. package/dist/goal-workflows/codex-goal-snapshot.d.ts +1 -0
  123. package/dist/goal-workflows/codex-goal-snapshot.d.ts.map +1 -1
  124. package/dist/goal-workflows/codex-goal-snapshot.js +5 -1
  125. package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -1
  126. package/dist/hooks/__tests__/autopilot-skill-contract.test.js +10 -6
  127. package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
  128. package/dist/hooks/__tests__/best-practice-research-skill.test.d.ts +2 -0
  129. package/dist/hooks/__tests__/best-practice-research-skill.test.d.ts.map +1 -0
  130. package/dist/hooks/__tests__/best-practice-research-skill.test.js +27 -0
  131. package/dist/hooks/__tests__/best-practice-research-skill.test.js.map +1 -0
  132. package/dist/hooks/__tests__/consensus-execution-handoff.test.d.ts +1 -1
  133. package/dist/hooks/__tests__/consensus-execution-handoff.test.js +13 -11
  134. package/dist/hooks/__tests__/consensus-execution-handoff.test.js.map +1 -1
  135. package/dist/hooks/__tests__/deep-interview-contract.test.js +4 -3
  136. package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
  137. package/dist/hooks/__tests__/keyword-detector.test.js +15 -3
  138. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  139. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +6 -0
  140. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  141. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +33 -0
  142. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
  143. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js +4 -0
  144. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js.map +1 -1
  145. package/dist/hooks/extensibility/__tests__/dispatcher.test.js +26 -3
  146. package/dist/hooks/extensibility/__tests__/dispatcher.test.js.map +1 -1
  147. package/dist/hooks/extensibility/dispatcher.d.ts.map +1 -1
  148. package/dist/hooks/extensibility/dispatcher.js +29 -14
  149. package/dist/hooks/extensibility/dispatcher.js.map +1 -1
  150. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  151. package/dist/hooks/keyword-detector.js +8 -3
  152. package/dist/hooks/keyword-detector.js.map +1 -1
  153. package/dist/hooks/keyword-registry.d.ts.map +1 -1
  154. package/dist/hooks/keyword-registry.js +1 -0
  155. package/dist/hooks/keyword-registry.js.map +1 -1
  156. package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
  157. package/dist/hooks/prompt-guidance-contract.js +3 -2
  158. package/dist/hooks/prompt-guidance-contract.js.map +1 -1
  159. package/dist/hud/__tests__/hud-tmux-injection.test.js +14 -8
  160. package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
  161. package/dist/hud/__tests__/reconcile.test.js +4 -4
  162. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  163. package/dist/hud/__tests__/resource-leak-watch.test.d.ts +2 -0
  164. package/dist/hud/__tests__/resource-leak-watch.test.d.ts.map +1 -0
  165. package/dist/hud/__tests__/resource-leak-watch.test.js +28 -0
  166. package/dist/hud/__tests__/resource-leak-watch.test.js.map +1 -0
  167. package/dist/hud/__tests__/tmux.test.js +23 -18
  168. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  169. package/dist/hud/index.d.ts +1 -1
  170. package/dist/hud/index.d.ts.map +1 -1
  171. package/dist/hud/index.js +10 -4
  172. package/dist/hud/index.js.map +1 -1
  173. package/dist/hud/tmux.d.ts.map +1 -1
  174. package/dist/hud/tmux.js +9 -8
  175. package/dist/hud/tmux.js.map +1 -1
  176. package/dist/mcp/__tests__/bootstrap.test.js +75 -1
  177. package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
  178. package/dist/mcp/bootstrap.d.ts +3 -1
  179. package/dist/mcp/bootstrap.d.ts.map +1 -1
  180. package/dist/mcp/bootstrap.js +71 -2
  181. package/dist/mcp/bootstrap.js.map +1 -1
  182. package/dist/notifications/__tests__/http-client-resource.test.d.ts +2 -0
  183. package/dist/notifications/__tests__/http-client-resource.test.d.ts.map +1 -0
  184. package/dist/notifications/__tests__/http-client-resource.test.js +41 -0
  185. package/dist/notifications/__tests__/http-client-resource.test.js.map +1 -0
  186. package/dist/notifications/__tests__/verbosity.test.js +20 -0
  187. package/dist/notifications/__tests__/verbosity.test.js.map +1 -1
  188. package/dist/notifications/config.d.ts.map +1 -1
  189. package/dist/notifications/config.js +6 -3
  190. package/dist/notifications/config.js.map +1 -1
  191. package/dist/notifications/http-client.d.ts.map +1 -1
  192. package/dist/notifications/http-client.js +78 -27
  193. package/dist/notifications/http-client.js.map +1 -1
  194. package/dist/notifications/types.d.ts +2 -0
  195. package/dist/notifications/types.d.ts.map +1 -1
  196. package/dist/openclaw/__tests__/dispatcher.test.js +49 -1
  197. package/dist/openclaw/__tests__/dispatcher.test.js.map +1 -1
  198. package/dist/openclaw/dispatcher.d.ts +7 -4
  199. package/dist/openclaw/dispatcher.d.ts.map +1 -1
  200. package/dist/openclaw/dispatcher.js +32 -69
  201. package/dist/openclaw/dispatcher.js.map +1 -1
  202. package/dist/pipeline/__tests__/orchestrator.test.js +65 -3
  203. package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
  204. package/dist/pipeline/__tests__/stages.test.js +50 -5
  205. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  206. package/dist/pipeline/index.d.ts +8 -2
  207. package/dist/pipeline/index.d.ts.map +1 -1
  208. package/dist/pipeline/index.js +5 -2
  209. package/dist/pipeline/index.js.map +1 -1
  210. package/dist/pipeline/orchestrator.d.ts +5 -4
  211. package/dist/pipeline/orchestrator.d.ts.map +1 -1
  212. package/dist/pipeline/orchestrator.js +56 -15
  213. package/dist/pipeline/orchestrator.js.map +1 -1
  214. package/dist/pipeline/stages/code-review.d.ts +2 -2
  215. package/dist/pipeline/stages/code-review.d.ts.map +1 -1
  216. package/dist/pipeline/stages/code-review.js +5 -3
  217. package/dist/pipeline/stages/code-review.js.map +1 -1
  218. package/dist/pipeline/stages/deep-interview.d.ts +15 -0
  219. package/dist/pipeline/stages/deep-interview.d.ts.map +1 -0
  220. package/dist/pipeline/stages/deep-interview.js +32 -0
  221. package/dist/pipeline/stages/deep-interview.js.map +1 -0
  222. package/dist/pipeline/stages/ralph-verify.d.ts +5 -5
  223. package/dist/pipeline/stages/ralph-verify.d.ts.map +1 -1
  224. package/dist/pipeline/stages/ralph-verify.js +2 -2
  225. package/dist/pipeline/stages/ralph-verify.js.map +1 -1
  226. package/dist/pipeline/stages/ultragoal.d.ts +19 -0
  227. package/dist/pipeline/stages/ultragoal.d.ts.map +1 -0
  228. package/dist/pipeline/stages/ultragoal.js +38 -0
  229. package/dist/pipeline/stages/ultragoal.js.map +1 -0
  230. package/dist/pipeline/stages/ultraqa.d.ts +30 -0
  231. package/dist/pipeline/stages/ultraqa.d.ts.map +1 -0
  232. package/dist/pipeline/stages/ultraqa.js +46 -0
  233. package/dist/pipeline/stages/ultraqa.js.map +1 -0
  234. package/dist/pipeline/types.d.ts +8 -6
  235. package/dist/pipeline/types.d.ts.map +1 -1
  236. package/dist/pipeline/types.js +2 -2
  237. package/dist/scripts/__tests__/codex-native-hook.test.js +1488 -117
  238. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  239. package/dist/scripts/__tests__/notify-dispatcher.test.js +183 -1
  240. package/dist/scripts/__tests__/notify-dispatcher.test.js.map +1 -1
  241. package/dist/scripts/__tests__/smoke-packed-install.test.js +27 -2
  242. package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
  243. package/dist/scripts/__tests__/verify-native-agents.test.js +16 -1
  244. package/dist/scripts/__tests__/verify-native-agents.test.js.map +1 -1
  245. package/dist/scripts/build-api.d.ts +2 -0
  246. package/dist/scripts/build-api.d.ts.map +1 -0
  247. package/dist/scripts/build-api.js +44 -0
  248. package/dist/scripts/build-api.js.map +1 -0
  249. package/dist/scripts/cleanup-explore-harness.js +1 -0
  250. package/dist/scripts/cleanup-explore-harness.js.map +1 -1
  251. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  252. package/dist/scripts/codex-native-hook.js +364 -16
  253. package/dist/scripts/codex-native-hook.js.map +1 -1
  254. package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
  255. package/dist/scripts/codex-native-pre-post.js +98 -25
  256. package/dist/scripts/codex-native-pre-post.js.map +1 -1
  257. package/dist/scripts/notify-dispatcher.js +88 -0
  258. package/dist/scripts/notify-dispatcher.js.map +1 -1
  259. package/dist/scripts/notify-hook/process-runner.d.ts.map +1 -1
  260. package/dist/scripts/notify-hook/process-runner.js +39 -17
  261. package/dist/scripts/notify-hook/process-runner.js.map +1 -1
  262. package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
  263. package/dist/scripts/notify-hook/team-dispatch.js +36 -14
  264. package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
  265. package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
  266. package/dist/scripts/notify-hook/team-leader-nudge.js +26 -11
  267. package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
  268. package/dist/scripts/notify-hook/team-tmux-guard.d.ts +2 -1
  269. package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
  270. package/dist/scripts/notify-hook/team-tmux-guard.js +45 -1
  271. package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
  272. package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
  273. package/dist/scripts/notify-hook/team-worker-stop.js +27 -14
  274. package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
  275. package/dist/scripts/run-provider-advisor.js +9 -3
  276. package/dist/scripts/run-provider-advisor.js.map +1 -1
  277. package/dist/scripts/smoke-packed-install.d.ts +4 -1
  278. package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
  279. package/dist/scripts/smoke-packed-install.js +101 -1
  280. package/dist/scripts/smoke-packed-install.js.map +1 -1
  281. package/dist/scripts/sync-plugin-mirror.js +2 -2
  282. package/dist/scripts/sync-plugin-mirror.js.map +1 -1
  283. package/dist/scripts/verify-native-agents.js +2 -2
  284. package/dist/scripts/verify-native-agents.js.map +1 -1
  285. package/dist/sidecar/__tests__/resource-leak-watch.test.d.ts +2 -0
  286. package/dist/sidecar/__tests__/resource-leak-watch.test.d.ts.map +1 -0
  287. package/dist/sidecar/__tests__/resource-leak-watch.test.js +38 -0
  288. package/dist/sidecar/__tests__/resource-leak-watch.test.js.map +1 -0
  289. package/dist/sidecar/index.d.ts +1 -1
  290. package/dist/sidecar/index.d.ts.map +1 -1
  291. package/dist/sidecar/index.js +29 -12
  292. package/dist/sidecar/index.js.map +1 -1
  293. package/dist/state/__tests__/operations-ralph-phase.test.js +88 -1
  294. package/dist/state/__tests__/operations-ralph-phase.test.js.map +1 -1
  295. package/dist/state/operations.d.ts.map +1 -1
  296. package/dist/state/operations.js +11 -0
  297. package/dist/state/operations.js.map +1 -1
  298. package/dist/team/__tests__/runtime.test.js +2 -2
  299. package/dist/team/__tests__/runtime.test.js.map +1 -1
  300. package/dist/team/__tests__/tmux-session.test.js +207 -22
  301. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  302. package/dist/team/tmux-session.d.ts +1 -0
  303. package/dist/team/tmux-session.d.ts.map +1 -1
  304. package/dist/team/tmux-session.js +73 -28
  305. package/dist/team/tmux-session.js.map +1 -1
  306. package/dist/ultragoal/__tests__/artifacts.test.js +714 -10
  307. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
  308. package/dist/ultragoal/__tests__/docs-contract.test.js +57 -1
  309. package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
  310. package/dist/ultragoal/__tests__/steering-fixtures.d.ts +68 -0
  311. package/dist/ultragoal/__tests__/steering-fixtures.d.ts.map +1 -0
  312. package/dist/ultragoal/__tests__/steering-fixtures.js +259 -0
  313. package/dist/ultragoal/__tests__/steering-fixtures.js.map +1 -0
  314. package/dist/ultragoal/__tests__/steering-fixtures.test.d.ts +2 -0
  315. package/dist/ultragoal/__tests__/steering-fixtures.test.d.ts.map +1 -0
  316. package/dist/ultragoal/__tests__/steering-fixtures.test.js +65 -0
  317. package/dist/ultragoal/__tests__/steering-fixtures.test.js.map +1 -0
  318. package/dist/ultragoal/artifacts.d.ts +97 -2
  319. package/dist/ultragoal/artifacts.d.ts.map +1 -1
  320. package/dist/ultragoal/artifacts.js +811 -256
  321. package/dist/ultragoal/artifacts.js.map +1 -1
  322. package/dist/utils/__tests__/sleep-resource.test.d.ts +2 -0
  323. package/dist/utils/__tests__/sleep-resource.test.d.ts.map +1 -0
  324. package/dist/utils/__tests__/sleep-resource.test.js +39 -0
  325. package/dist/utils/__tests__/sleep-resource.test.js.map +1 -0
  326. package/dist/utils/sleep.d.ts.map +1 -1
  327. package/dist/utils/sleep.js +17 -6
  328. package/dist/utils/sleep.js.map +1 -1
  329. package/dist/verification/__tests__/ci-rust-gates.test.js +85 -10
  330. package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
  331. package/dist/verification/__tests__/explore-harness-release-workflow.test.js +1 -0
  332. package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
  333. package/package.json +5 -3
  334. package/plugins/oh-my-codex/.codex-plugin/plugin.json +4 -3
  335. package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +56 -0
  336. package/plugins/oh-my-codex/hooks/hooks.json +77 -0
  337. package/plugins/oh-my-codex/skills/autopilot/SKILL.md +77 -47
  338. package/plugins/oh-my-codex/skills/best-practice-research/SKILL.md +83 -0
  339. package/plugins/oh-my-codex/skills/cancel/SKILL.md +2 -2
  340. package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +9 -8
  341. package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +1 -1
  342. package/plugins/oh-my-codex/skills/pipeline/SKILL.md +22 -11
  343. package/plugins/oh-my-codex/skills/plan/SKILL.md +8 -8
  344. package/plugins/oh-my-codex/skills/ralph/SKILL.md +7 -0
  345. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +5 -5
  346. package/plugins/oh-my-codex/skills/team/SKILL.md +1 -1
  347. package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +38 -4
  348. package/plugins/oh-my-codex/skills/ultrawork/SKILL.md +1 -1
  349. package/prompts/planner.md +1 -1
  350. package/prompts/researcher.md +15 -10
  351. package/skills/autopilot/SKILL.md +77 -47
  352. package/skills/best-practice-research/SKILL.md +83 -0
  353. package/skills/cancel/SKILL.md +2 -2
  354. package/skills/deep-interview/SKILL.md +9 -8
  355. package/skills/omx-setup/SKILL.md +1 -1
  356. package/skills/pipeline/SKILL.md +22 -11
  357. package/skills/plan/SKILL.md +8 -8
  358. package/skills/ralph/SKILL.md +7 -0
  359. package/skills/ralplan/SKILL.md +5 -5
  360. package/skills/team/SKILL.md +1 -1
  361. package/skills/ultragoal/SKILL.md +38 -4
  362. package/skills/ultrawork/SKILL.md +1 -1
  363. package/src/scripts/__tests__/codex-native-hook.test.ts +1758 -166
  364. package/src/scripts/__tests__/notify-dispatcher.test.ts +223 -1
  365. package/src/scripts/__tests__/smoke-packed-install.test.ts +39 -2
  366. package/src/scripts/__tests__/verify-native-agents.test.ts +21 -1
  367. package/src/scripts/build-api.ts +48 -0
  368. package/src/scripts/cleanup-explore-harness.ts +1 -0
  369. package/src/scripts/codex-native-hook.ts +416 -18
  370. package/src/scripts/codex-native-pre-post.ts +119 -25
  371. package/src/scripts/notify-dispatcher.ts +97 -0
  372. package/src/scripts/notify-hook/process-runner.ts +40 -16
  373. package/src/scripts/notify-hook/team-dispatch.ts +36 -13
  374. package/src/scripts/notify-hook/team-leader-nudge.ts +25 -11
  375. package/src/scripts/notify-hook/team-tmux-guard.ts +49 -0
  376. package/src/scripts/notify-hook/team-worker-stop.ts +24 -13
  377. package/src/scripts/run-provider-advisor.ts +11 -3
  378. package/src/scripts/smoke-packed-install.ts +107 -0
  379. package/src/scripts/sync-plugin-mirror.ts +3 -3
  380. package/src/scripts/verify-native-agents.ts +2 -2
  381. package/templates/catalog-manifest.json +7 -0
@@ -2,12 +2,13 @@ use crate::error::SparkshellError;
2
2
  use crate::exec::CommandOutput;
3
3
  use crate::prompt::build_summary_prompt;
4
4
  use std::env;
5
+ use std::fs;
5
6
  use std::io::{Read, Write};
6
- use std::process::{Command, Stdio};
7
- use std::thread;
8
- use std::time::{Duration, Instant};
7
+ use std::net::{IpAddr, TcpStream, ToSocketAddrs};
8
+ use std::time::Duration;
9
9
 
10
10
  pub const DEFAULT_SUMMARY_TIMEOUT_MS: u64 = 60_000;
11
+ pub const DEFAULT_API_BASE_URL: &str = "http://127.0.0.1:14510";
11
12
  pub const DEFAULT_SPARK_MODEL: &str = "gpt-5.3-codex-spark";
12
13
  pub const DEFAULT_STANDARD_MODEL: &str = "gpt-5.4-mini";
13
14
 
@@ -63,43 +64,32 @@ pub fn summarize_output(
63
64
  let model = resolve_model();
64
65
  let fallback_model = resolve_fallback_model();
65
66
  let timeout_ms = read_summary_timeout_ms();
66
- let (stdout, stderr, status_ok) = run_codex_exec(&prompt, &model, timeout_ms)?;
67
- if !status_ok {
68
- let should_retry = fallback_model != model && should_retry_with_fallback(&stderr);
69
- if should_retry {
70
- let (fallback_stdout, fallback_stderr, fallback_ok) =
71
- run_codex_exec(&prompt, &fallback_model, timeout_ms)?;
72
- if !fallback_ok {
73
- let primary_message = if stderr.trim().is_empty() {
74
- "codex exec exited unsuccessfully".to_string()
75
- } else {
76
- stderr.trim().to_string()
77
- };
78
- let fallback_message = if fallback_stderr.trim().is_empty() {
79
- "codex exec exited unsuccessfully".to_string()
80
- } else {
81
- fallback_stderr.trim().to_string()
82
- };
83
- return Err(SparkshellError::SummaryBridge(format!(
84
- "codex exec failed for primary model `{model}` ({primary_message}) and fallback model `{fallback_model}` ({fallback_message})"
85
- )));
67
+ match request_summary(&prompt, &model, timeout_ms) {
68
+ Ok(stdout) => normalize_summary(&stdout).ok_or_else(|| {
69
+ SparkshellError::SummaryBridge(
70
+ "local API returned no valid summary sections".to_string(),
71
+ )
72
+ }),
73
+ Err(primary_error) => {
74
+ let primary_message = primary_error.to_string();
75
+ if fallback_model != model && should_retry_with_fallback(&primary_message) {
76
+ match request_summary(&prompt, &fallback_model, timeout_ms) {
77
+ Ok(fallback_stdout) => normalize_summary(&fallback_stdout).ok_or_else(|| {
78
+ SparkshellError::SummaryBridge(
79
+ "local API fallback returned no valid summary sections".to_string(),
80
+ )
81
+ }),
82
+ Err(fallback_error) => Err(SparkshellError::SummaryBridge(format!(
83
+ "local API failed for primary model `{model}` ({primary_message}) and fallback model `{fallback_model}` ({fallback_error})"
84
+ ))),
85
+ }
86
+ } else {
87
+ Err(SparkshellError::SummaryBridge(format!(
88
+ "local API summary request failed: {primary_message}"
89
+ )))
86
90
  }
87
- return normalize_summary(&fallback_stdout).ok_or_else(|| {
88
- SparkshellError::SummaryBridge(
89
- "codex exec fallback returned no valid summary sections".to_string(),
90
- )
91
- });
92
- }
93
- let message = if stderr.trim().is_empty() {
94
- "codex exec exited unsuccessfully".to_string()
95
- } else {
96
- format!("codex exec exited unsuccessfully: {}", stderr.trim())
97
- };
98
- return Err(SparkshellError::SummaryBridge(message));
91
+ }
99
92
  }
100
- normalize_summary(&stdout).ok_or_else(|| {
101
- SparkshellError::SummaryBridge("codex exec returned no valid summary sections".to_string())
102
- })
103
93
  }
104
94
 
105
95
  fn should_retry_with_fallback(stderr: &str) -> bool {
@@ -119,93 +109,371 @@ fn should_retry_with_fallback(stderr: &str) -> bool {
119
109
  .any(|needle| normalized.contains(needle))
120
110
  }
121
111
 
122
- fn run_codex_exec(
123
- prompt: &str,
124
- model: &str,
112
+ fn request_summary(prompt: &str, model: &str, timeout_ms: u64) -> Result<String, SparkshellError> {
113
+ let api_base_url = resolve_api_base_url();
114
+ let endpoint = join_api_path(&api_base_url, "/v1/responses");
115
+ let request = build_responses_request(prompt, model)?;
116
+ let response_body = post_json(&endpoint, &request, timeout_ms, resolve_api_bearer())?;
117
+ extract_output_text(&response_body).ok_or_else(|| {
118
+ SparkshellError::SummaryBridge("local API response did not include output_text".to_string())
119
+ })
120
+ }
121
+
122
+ fn resolve_api_base_url() -> String {
123
+ env::var("OMX_API_BASE_URL")
124
+ .ok()
125
+ .map(|value| value.trim().trim_end_matches('/').to_string())
126
+ .filter(|value| !value.is_empty())
127
+ .or_else(|| {
128
+ env::var("OMX_API_PORT")
129
+ .ok()
130
+ .map(|value| value.trim().to_string())
131
+ .filter(|value| !value.is_empty())
132
+ .map(|port| format!("http://127.0.0.1:{port}"))
133
+ })
134
+ .unwrap_or_else(|| DEFAULT_API_BASE_URL.to_string())
135
+ }
136
+
137
+ fn resolve_api_bearer() -> Option<String> {
138
+ env::var("OMX_API_LOCAL_BEARER")
139
+ .ok()
140
+ .map(|value| value.trim().to_string())
141
+ .filter(|value| !value.is_empty())
142
+ .or_else(|| {
143
+ let path = env::var("OMX_API_STATE_FILE")
144
+ .ok()
145
+ .filter(|value| !value.trim().is_empty())
146
+ .unwrap_or_else(|| {
147
+ env::temp_dir()
148
+ .join("omx-api-daemon.json")
149
+ .display()
150
+ .to_string()
151
+ });
152
+ fs::read_to_string(path)
153
+ .ok()
154
+ .and_then(|state| extract_json_string_field(&state, "local_bearer_token_file"))
155
+ .and_then(|token_file| fs::read_to_string(token_file).ok())
156
+ .map(|token| token.trim().to_string())
157
+ .filter(|token| !token.is_empty())
158
+ })
159
+ }
160
+
161
+ fn build_responses_request(prompt: &str, model: &str) -> Result<String, SparkshellError> {
162
+ let mut fields = vec![
163
+ format!("\"model\":{}", json_string(model)),
164
+ format!("\"input\":{}", json_string(prompt)),
165
+ "\"reasoning\":{\"effort\":\"low\"}".to_string(),
166
+ "\"stream\":false".to_string(),
167
+ ];
168
+
169
+ if let Some(path) = resolve_instructions_file() {
170
+ let instructions = fs::read_to_string(&path).map_err(|error| {
171
+ SparkshellError::SummaryBridge(format!(
172
+ "failed to read summary instructions file `{path}`: {error}"
173
+ ))
174
+ })?;
175
+ fields.push(format!("\"instructions\":{}", json_string(&instructions)));
176
+ }
177
+
178
+ Ok(format!("{{{}}}", fields.join(",")))
179
+ }
180
+
181
+ fn join_api_path(base_url: &str, path: &str) -> String {
182
+ format!(
183
+ "{}{}{}",
184
+ base_url.trim_end_matches('/'),
185
+ if path.starts_with('/') { "" } else { "/" },
186
+ path
187
+ )
188
+ }
189
+
190
+ fn post_json(
191
+ url: &str,
192
+ body: &str,
125
193
  timeout_ms: u64,
126
- ) -> Result<(String, String, bool), SparkshellError> {
127
- let mut child = Command::new("codex")
128
- .arg("exec")
129
- .arg("--model")
130
- .arg(model)
131
- .arg("--sandbox")
132
- .arg("read-only")
133
- .arg("-c")
134
- .arg("model_reasoning_effort=\"low\"")
135
- .args(resolve_instructions_file().into_iter().flat_map(|path| {
136
- [
137
- "-c".to_string(),
138
- format!("model_instructions_file=\"{}\"", escape_toml_string(&path)),
139
- ]
140
- }))
141
- .arg("--skip-git-repo-check")
142
- .arg("--color")
143
- .arg("never")
144
- .arg("-")
145
- .stdin(Stdio::piped())
146
- .stdout(Stdio::piped())
147
- .stderr(Stdio::piped())
148
- .spawn()?;
149
-
150
- let mut stdin = child
151
- .stdin
152
- .take()
153
- .ok_or_else(|| SparkshellError::SummaryBridge("failed to open codex stdin".to_string()))?;
154
- let mut stdout = child
155
- .stdout
156
- .take()
157
- .ok_or_else(|| SparkshellError::SummaryBridge("failed to open codex stdout".to_string()))?;
158
- let mut stderr = child
159
- .stderr
160
- .take()
161
- .ok_or_else(|| SparkshellError::SummaryBridge("failed to open codex stderr".to_string()))?;
162
-
163
- let prompt_owned = prompt.to_string();
164
- let stdin_writer = thread::spawn(move || stdin.write_all(prompt_owned.as_bytes()));
165
- let stdout_reader = thread::spawn(move || {
166
- let mut buffer = Vec::new();
167
- let _ = stdout.read_to_end(&mut buffer);
168
- buffer
169
- });
170
- let stderr_reader = thread::spawn(move || {
171
- let mut buffer = Vec::new();
172
- let _ = stderr.read_to_end(&mut buffer);
173
- buffer
174
- });
175
-
176
- let deadline = Instant::now() + Duration::from_millis(timeout_ms);
177
- let status = loop {
178
- if let Some(status) = child.try_wait()? {
179
- break status;
180
- }
181
- if Instant::now() >= deadline {
182
- let _ = child.kill();
183
- let _ = child.wait();
184
- let _ = stdin_writer.join();
185
- let _ = stdout_reader.join();
186
- let _ = stderr_reader.join();
187
- return Err(SparkshellError::SummaryTimeout(timeout_ms));
188
- }
189
- thread::sleep(Duration::from_millis(25));
194
+ bearer: Option<String>,
195
+ ) -> Result<String, SparkshellError> {
196
+ let parsed = parse_http_url(url)?;
197
+ let timeout = Duration::from_millis(timeout_ms);
198
+ let mut addrs = (parsed.host.as_str(), parsed.port)
199
+ .to_socket_addrs()
200
+ .map_err(|error| {
201
+ SparkshellError::SummaryBridge(format!(
202
+ "failed to resolve local API host `{}`: {error}",
203
+ parsed.host
204
+ ))
205
+ })?;
206
+ let addr = addrs.next().ok_or_else(|| {
207
+ SparkshellError::SummaryBridge(format!(
208
+ "failed to resolve local API host `{}`",
209
+ parsed.host
210
+ ))
211
+ })?;
212
+ let mut stream = TcpStream::connect_timeout(&addr, timeout)
213
+ .map_err(|error| map_api_io_error(error, timeout_ms, "local API connection failed"))?;
214
+ stream.set_read_timeout(Some(timeout)).map_err(|error| {
215
+ map_api_io_error(error, timeout_ms, "local API read timeout setup failed")
216
+ })?;
217
+ stream.set_write_timeout(Some(timeout)).map_err(|error| {
218
+ map_api_io_error(error, timeout_ms, "local API write timeout setup failed")
219
+ })?;
220
+
221
+ let host_header = if parsed.explicit_port {
222
+ format!("{}:{}", parsed.host, parsed.port)
223
+ } else {
224
+ parsed.host.clone()
190
225
  };
226
+ let auth_header = bearer
227
+ .as_deref()
228
+ .map(|token| format!("Authorization: Bearer {token}\r\n"))
229
+ .unwrap_or_default();
230
+ let request = format!(
231
+ "POST {} HTTP/1.1\r\nHost: {}\r\nContent-Type: application/json\r\nAccept: application/json\r\n{}Connection: close\r\nContent-Length: {}\r\n\r\n{}",
232
+ parsed.path, host_header, auth_header, body.len(), body
233
+ );
234
+ stream
235
+ .write_all(request.as_bytes())
236
+ .map_err(|error| map_api_io_error(error, timeout_ms, "local API write failed"))?;
237
+ stream
238
+ .flush()
239
+ .map_err(|error| map_api_io_error(error, timeout_ms, "local API flush failed"))?;
240
+
241
+ let mut raw = Vec::new();
242
+ stream
243
+ .read_to_end(&mut raw)
244
+ .map_err(|error| map_api_io_error(error, timeout_ms, "local API read failed"))?;
245
+ let response = String::from_utf8_lossy(&raw);
246
+ let (head, response_body) = response.split_once("\r\n\r\n").ok_or_else(|| {
247
+ SparkshellError::SummaryBridge("local API returned malformed HTTP response".to_string())
248
+ })?;
249
+ let status_line = head.lines().next().unwrap_or_default();
250
+ let status_code = status_line
251
+ .split_whitespace()
252
+ .nth(1)
253
+ .and_then(|value| value.parse::<u16>().ok())
254
+ .ok_or_else(|| {
255
+ SparkshellError::SummaryBridge("local API returned malformed HTTP status".to_string())
256
+ })?;
257
+ if !(200..300).contains(&status_code) {
258
+ return Err(SparkshellError::SummaryBridge(format!(
259
+ "local API returned HTTP {status_code}: {}",
260
+ response_body.trim()
261
+ )));
262
+ }
263
+ Ok(response_body.to_string())
264
+ }
265
+
266
+ fn map_api_io_error(error: std::io::Error, timeout_ms: u64, context: &str) -> SparkshellError {
267
+ if matches!(
268
+ error.kind(),
269
+ std::io::ErrorKind::TimedOut | std::io::ErrorKind::WouldBlock
270
+ ) {
271
+ SparkshellError::SummaryTimeout(timeout_ms)
272
+ } else {
273
+ SparkshellError::SummaryBridge(format!("{context}: {error}"))
274
+ }
275
+ }
276
+
277
+ #[derive(Debug)]
278
+ struct HttpUrl {
279
+ host: String,
280
+ port: u16,
281
+ path: String,
282
+ explicit_port: bool,
283
+ }
284
+
285
+ fn parse_http_url(url: &str) -> Result<HttpUrl, SparkshellError> {
286
+ let rest = url.strip_prefix("http://").ok_or_else(|| {
287
+ SparkshellError::SummaryBridge(format!("local API URL must use http://, got `{url}`"))
288
+ })?;
289
+ let (authority, path) = match rest.split_once('/') {
290
+ Some((authority, path)) => (authority, format!("/{path}")),
291
+ None => (rest, "/".to_string()),
292
+ };
293
+ let (host, port, explicit_port) = if let Some((host, port)) = authority.rsplit_once(':') {
294
+ let port = port.parse::<u16>().map_err(|_| {
295
+ SparkshellError::SummaryBridge(format!("local API URL has invalid port in `{url}`"))
296
+ })?;
297
+ (host.to_string(), port, true)
298
+ } else {
299
+ (authority.to_string(), 80, false)
300
+ };
301
+ if host.is_empty() {
302
+ return Err(SparkshellError::SummaryBridge(format!(
303
+ "local API URL has empty host in `{url}`"
304
+ )));
305
+ }
306
+ if !is_loopback_host(&host) && std::env::var_os("OMX_API_ALLOW_UNSAFE_BASE_URL").is_none() {
307
+ return Err(SparkshellError::SummaryBridge(format!(
308
+ "local API URL host `{host}` is not loopback; set OMX_API_ALLOW_UNSAFE_BASE_URL=1 only for trusted development"
309
+ )));
310
+ }
311
+ Ok(HttpUrl {
312
+ host,
313
+ port,
314
+ path,
315
+ explicit_port,
316
+ })
317
+ }
318
+
319
+ fn is_loopback_host(host: &str) -> bool {
320
+ if host == "localhost" {
321
+ return true;
322
+ }
323
+ let trimmed = host.trim_matches(['[', ']']);
324
+ trimmed
325
+ .parse::<IpAddr>()
326
+ .map(|addr| addr.is_loopback())
327
+ .unwrap_or(false)
328
+ }
329
+
330
+ fn json_string(value: &str) -> String {
331
+ let mut rendered = String::from("\"");
332
+ for ch in value.chars() {
333
+ match ch {
334
+ '\\' => rendered.push_str("\\\\"),
335
+ '"' => rendered.push_str("\\\""),
336
+ '\n' => rendered.push_str("\\n"),
337
+ '\r' => rendered.push_str("\\r"),
338
+ '\t' => rendered.push_str("\\t"),
339
+ ch if ch.is_control() => rendered.push_str(&format!("\\u{:04x}", ch as u32)),
340
+ ch => rendered.push(ch),
341
+ }
342
+ }
343
+ rendered.push('"');
344
+ rendered
345
+ }
346
+
347
+ fn extract_output_text(body: &str) -> Option<String> {
348
+ extract_json_string_field(body, "output_text").or_else(|| extract_response_output_text(body))
349
+ }
350
+
351
+ fn extract_response_output_text(body: &str) -> Option<String> {
352
+ let bytes = body.as_bytes();
353
+ let type_pattern = "\"type\"";
354
+ let mut search_start = 0;
355
+ let mut parts = Vec::new();
356
+
357
+ while let Some(relative_index) = body[search_start..].find(type_pattern) {
358
+ let type_index = search_start + relative_index;
359
+ let Some((value, value_end)) = extract_json_string_field_at(body, type_index, "type")
360
+ else {
361
+ search_start = type_index + type_pattern.len();
362
+ continue;
363
+ };
364
+ if value != "output_text" {
365
+ search_start = value_end;
366
+ continue;
367
+ }
191
368
 
192
- let _ = stdin_writer.join();
193
- let stdout_bytes = stdout_reader
194
- .join()
195
- .map_err(|_| SparkshellError::SummaryBridge("failed reading codex stdout".to_string()))?;
196
- let stderr_bytes = stderr_reader
197
- .join()
198
- .map_err(|_| SparkshellError::SummaryBridge("failed reading codex stderr".to_string()))?;
199
-
200
- Ok((
201
- String::from_utf8_lossy(&stdout_bytes).into_owned(),
202
- String::from_utf8_lossy(&stderr_bytes).into_owned(),
203
- status.success(),
204
- ))
369
+ let next_type = body[value_end..]
370
+ .find(type_pattern)
371
+ .map(|index| value_end + index)
372
+ .unwrap_or(bytes.len());
373
+ if let Some((text, text_end)) =
374
+ extract_json_string_field_before(body, value_end, next_type, "text")
375
+ {
376
+ parts.push(text);
377
+ search_start = text_end;
378
+ } else {
379
+ search_start = value_end;
380
+ }
381
+ }
382
+
383
+ if parts.is_empty() {
384
+ None
385
+ } else {
386
+ Some(parts.join(""))
387
+ }
388
+ }
389
+
390
+ fn extract_json_string_field(body: &str, field: &str) -> Option<String> {
391
+ extract_json_string_field_at(body, 0, field).map(|(value, _)| value)
392
+ }
393
+
394
+ fn extract_json_string_field_before(
395
+ body: &str,
396
+ start: usize,
397
+ end: usize,
398
+ field: &str,
399
+ ) -> Option<(String, usize)> {
400
+ extract_json_string_field_in_range(body, start, end.min(body.len()), field)
401
+ }
402
+
403
+ fn extract_json_string_field_at(body: &str, start: usize, field: &str) -> Option<(String, usize)> {
404
+ extract_json_string_field_in_range(body, start, body.len(), field)
405
+ }
406
+
407
+ fn extract_json_string_field_in_range(
408
+ body: &str,
409
+ start: usize,
410
+ end: usize,
411
+ field: &str,
412
+ ) -> Option<(String, usize)> {
413
+ let bytes = body.as_bytes();
414
+ let field_pattern = format!("\"{field}\"");
415
+ let mut search_start = start.min(body.len());
416
+ let search_end = end.min(body.len());
417
+ while search_start < search_end {
418
+ let Some(relative_index) = body[search_start..search_end].find(&field_pattern) else {
419
+ break;
420
+ };
421
+ let mut index = search_start + relative_index + field_pattern.len();
422
+ while matches!(bytes.get(index), Some(b' ' | b'\n' | b'\r' | b'\t')) {
423
+ index += 1;
424
+ }
425
+ if index >= search_end {
426
+ break;
427
+ }
428
+ if bytes.get(index) != Some(&b':') {
429
+ search_start = index;
430
+ continue;
431
+ }
432
+ index += 1;
433
+ while matches!(bytes.get(index), Some(b' ' | b'\n' | b'\r' | b'\t')) {
434
+ index += 1;
435
+ }
436
+ if index >= search_end {
437
+ break;
438
+ }
439
+ if bytes.get(index) != Some(&b'"') {
440
+ search_start = index;
441
+ continue;
442
+ }
443
+ return parse_json_string_at(body, index);
444
+ }
445
+ None
205
446
  }
206
447
 
207
- fn escape_toml_string(value: &str) -> String {
208
- value.replace('\\', "\\\\").replace('"', "\\\"")
448
+ fn parse_json_string_at(body: &str, quote_index: usize) -> Option<(String, usize)> {
449
+ let mut chars = body[quote_index + 1..].char_indices();
450
+ let mut rendered = String::new();
451
+ while let Some((offset, ch)) = chars.next() {
452
+ match ch {
453
+ '"' => return Some((rendered, quote_index + 1 + offset + ch.len_utf8())),
454
+ '\\' => match chars.next()?.1 {
455
+ '"' => rendered.push('"'),
456
+ '\\' => rendered.push('\\'),
457
+ '/' => rendered.push('/'),
458
+ 'b' => rendered.push('\u{0008}'),
459
+ 'f' => rendered.push('\u{000c}'),
460
+ 'n' => rendered.push('\n'),
461
+ 'r' => rendered.push('\r'),
462
+ 't' => rendered.push('\t'),
463
+ 'u' => {
464
+ let mut hex = String::new();
465
+ for _ in 0..4 {
466
+ hex.push(chars.next()?.1);
467
+ }
468
+ let value = u16::from_str_radix(&hex, 16).ok()?;
469
+ rendered.push(char::from_u32(value as u32)?);
470
+ }
471
+ _ => return None,
472
+ },
473
+ ch => rendered.push(ch),
474
+ }
475
+ }
476
+ None
209
477
  }
210
478
 
211
479
  fn normalize_summary(raw: &str) -> Option<String> {
@@ -289,9 +557,9 @@ fn render_section(name: &str, entries: &[String]) -> String {
289
557
  #[allow(unused_unsafe)]
290
558
  mod tests {
291
559
  use super::{
292
- normalize_summary, read_summary_timeout_ms, resolve_fallback_model,
293
- resolve_instructions_file, resolve_model, DEFAULT_SPARK_MODEL, DEFAULT_STANDARD_MODEL,
294
- DEFAULT_SUMMARY_TIMEOUT_MS,
560
+ extract_output_text, normalize_summary, parse_http_url, read_summary_timeout_ms,
561
+ resolve_fallback_model, resolve_instructions_file, resolve_model, DEFAULT_SPARK_MODEL,
562
+ DEFAULT_STANDARD_MODEL, DEFAULT_SUMMARY_TIMEOUT_MS,
295
563
  };
296
564
  use crate::test_support::env_lock;
297
565
  use std::env;
@@ -413,6 +681,33 @@ mod tests {
413
681
  }
414
682
  }
415
683
 
684
+ #[test]
685
+ fn output_text_extraction_prefers_compat_top_level_field() {
686
+ let body = r#"{"output_text":"summary: legacy\nwarnings: none","output":[{"type":"message","content":[{"type":"output_text","text":"summary: nested"}]}]}"#;
687
+ assert_eq!(
688
+ extract_output_text(body),
689
+ Some("summary: legacy\nwarnings: none".to_string())
690
+ );
691
+ }
692
+
693
+ #[test]
694
+ fn output_text_extraction_supports_responses_output_content_shape() {
695
+ let body = r#"{
696
+ "id":"resp_123",
697
+ "output":[
698
+ {"type":"reasoning","summary":[]},
699
+ {"type":"message","content":[
700
+ {"type":"output_text","annotations":[],"text":"summary: ok\n"},
701
+ {"type":"output_text","text":"warnings: none"}
702
+ ]}
703
+ ]
704
+ }"#;
705
+ assert_eq!(
706
+ extract_output_text(body),
707
+ Some("summary: ok\nwarnings: none".to_string())
708
+ );
709
+ }
710
+
416
711
  #[test]
417
712
  fn normalizes_allowed_sections_only() {
418
713
  let summary = normalize_summary(
@@ -451,4 +746,23 @@ notes: nope"
451
746
  )
452
747
  .is_none());
453
748
  }
749
+
750
+ #[test]
751
+ fn api_base_url_parser_rejects_non_loopback_hosts_by_default() {
752
+ let _guard = env_lock();
753
+ unsafe {
754
+ env::remove_var("OMX_API_ALLOW_UNSAFE_BASE_URL");
755
+ }
756
+
757
+ assert!(parse_http_url("http://127.0.0.1:14510/v1/responses").is_ok());
758
+ assert!(parse_http_url("http://localhost:14510/v1/responses").is_ok());
759
+ assert!(parse_http_url("http://example.com:14510/v1/responses")
760
+ .expect_err("non-loopback host should be rejected")
761
+ .to_string()
762
+ .contains("not loopback"));
763
+ assert!(parse_http_url("http://127.0.0.1.evil:14510/v1/responses")
764
+ .expect_err("prefix spoof host should be rejected")
765
+ .to_string()
766
+ .contains("not loopback"));
767
+ }
454
768
  }