oh-my-codex 0.7.6 → 0.8.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 (376) hide show
  1. package/README.de.md +315 -0
  2. package/README.es.md +296 -17
  3. package/README.fr.md +315 -0
  4. package/README.it.md +315 -0
  5. package/README.ja.md +297 -18
  6. package/README.ko.md +296 -17
  7. package/README.md +110 -13
  8. package/README.pt.md +296 -17
  9. package/README.ru.md +296 -17
  10. package/README.tr.md +315 -0
  11. package/README.vi.md +297 -18
  12. package/README.zh-TW.md +362 -0
  13. package/README.zh.md +293 -17
  14. package/dist/catalog/__tests__/generator.test.js +2 -0
  15. package/dist/catalog/__tests__/generator.test.js.map +1 -1
  16. package/dist/catalog/__tests__/schema.test.js +7 -0
  17. package/dist/catalog/__tests__/schema.test.js.map +1 -1
  18. package/dist/cli/__tests__/ask.test.d.ts +2 -0
  19. package/dist/cli/__tests__/ask.test.d.ts.map +1 -0
  20. package/dist/cli/__tests__/ask.test.js +236 -0
  21. package/dist/cli/__tests__/ask.test.js.map +1 -0
  22. package/dist/cli/__tests__/doctor-warning-copy.test.d.ts +2 -0
  23. package/dist/cli/__tests__/doctor-warning-copy.test.d.ts.map +1 -0
  24. package/dist/cli/__tests__/doctor-warning-copy.test.js +45 -0
  25. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -0
  26. package/dist/cli/__tests__/index.test.js +85 -2
  27. package/dist/cli/__tests__/index.test.js.map +1 -1
  28. package/dist/cli/__tests__/ralph-prd-deep-interview.test.d.ts +2 -0
  29. package/dist/cli/__tests__/ralph-prd-deep-interview.test.d.ts.map +1 -0
  30. package/dist/cli/__tests__/ralph-prd-deep-interview.test.js +15 -0
  31. package/dist/cli/__tests__/ralph-prd-deep-interview.test.js.map +1 -0
  32. package/dist/cli/__tests__/ralph.test.js +19 -43
  33. package/dist/cli/__tests__/ralph.test.js.map +1 -1
  34. package/dist/cli/__tests__/setup-scope.test.js +2 -0
  35. package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
  36. package/dist/cli/__tests__/team.test.js +219 -1
  37. package/dist/cli/__tests__/team.test.js.map +1 -1
  38. package/dist/cli/__tests__/version.test.d.ts +2 -0
  39. package/dist/cli/__tests__/version.test.d.ts.map +1 -0
  40. package/dist/cli/__tests__/version.test.js +21 -0
  41. package/dist/cli/__tests__/version.test.js.map +1 -0
  42. package/dist/cli/ask.d.ts +13 -0
  43. package/dist/cli/ask.d.ts.map +1 -0
  44. package/dist/cli/ask.js +174 -0
  45. package/dist/cli/ask.js.map +1 -0
  46. package/dist/cli/constants.d.ts +10 -0
  47. package/dist/cli/constants.d.ts.map +1 -0
  48. package/dist/cli/constants.js +10 -0
  49. package/dist/cli/constants.js.map +1 -0
  50. package/dist/cli/doctor.js +16 -5
  51. package/dist/cli/doctor.js.map +1 -1
  52. package/dist/cli/index.d.ts +8 -2
  53. package/dist/cli/index.d.ts.map +1 -1
  54. package/dist/cli/index.js +150 -52
  55. package/dist/cli/index.js.map +1 -1
  56. package/dist/cli/ralph.d.ts +3 -11
  57. package/dist/cli/ralph.d.ts.map +1 -1
  58. package/dist/cli/ralph.js +64 -45
  59. package/dist/cli/ralph.js.map +1 -1
  60. package/dist/cli/setup.d.ts.map +1 -1
  61. package/dist/cli/setup.js +17 -18
  62. package/dist/cli/setup.js.map +1 -1
  63. package/dist/cli/team.d.ts.map +1 -1
  64. package/dist/cli/team.js +257 -0
  65. package/dist/cli/team.js.map +1 -1
  66. package/dist/hooks/__tests__/deep-interview-contract.test.d.ts +2 -0
  67. package/dist/hooks/__tests__/deep-interview-contract.test.d.ts.map +1 -0
  68. package/dist/hooks/__tests__/deep-interview-contract.test.js +55 -0
  69. package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -0
  70. package/dist/hooks/__tests__/emulator.test.js +6 -0
  71. package/dist/hooks/__tests__/emulator.test.js.map +1 -1
  72. package/dist/hooks/__tests__/keyword-detector.test.js +44 -22
  73. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  74. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +23 -7
  75. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
  76. package/dist/hooks/__tests__/notify-hook-session-scope.test.js +59 -0
  77. package/dist/hooks/__tests__/notify-hook-session-scope.test.js.map +1 -1
  78. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +264 -1
  79. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
  80. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +61 -1
  81. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  82. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +17 -7
  83. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
  84. package/dist/hooks/__tests__/openclaw-setup-contract.test.d.ts +2 -0
  85. package/dist/hooks/__tests__/openclaw-setup-contract.test.d.ts.map +1 -0
  86. package/dist/hooks/__tests__/openclaw-setup-contract.test.js +61 -0
  87. package/dist/hooks/__tests__/openclaw-setup-contract.test.js.map +1 -0
  88. package/dist/hooks/__tests__/pre-context-gate-skills.test.d.ts +2 -0
  89. package/dist/hooks/__tests__/pre-context-gate-skills.test.d.ts.map +1 -0
  90. package/dist/hooks/__tests__/pre-context-gate-skills.test.js +34 -0
  91. package/dist/hooks/__tests__/pre-context-gate-skills.test.js.map +1 -0
  92. package/dist/hooks/__tests__/visual-verdict-loop.test.d.ts +2 -0
  93. package/dist/hooks/__tests__/visual-verdict-loop.test.d.ts.map +1 -0
  94. package/dist/hooks/__tests__/visual-verdict-loop.test.js +35 -0
  95. package/dist/hooks/__tests__/visual-verdict-loop.test.js.map +1 -0
  96. package/dist/hooks/agents-overlay.d.ts.map +1 -1
  97. package/dist/hooks/agents-overlay.js +18 -16
  98. package/dist/hooks/agents-overlay.js.map +1 -1
  99. package/dist/hooks/codebase-map.d.ts.map +1 -1
  100. package/dist/hooks/codebase-map.js +6 -2
  101. package/dist/hooks/codebase-map.js.map +1 -1
  102. package/dist/hooks/emulator.d.ts.map +1 -1
  103. package/dist/hooks/emulator.js +2 -0
  104. package/dist/hooks/emulator.js.map +1 -1
  105. package/dist/hooks/extensibility/sdk.d.ts.map +1 -1
  106. package/dist/hooks/extensibility/sdk.js +2 -1
  107. package/dist/hooks/extensibility/sdk.js.map +1 -1
  108. package/dist/hooks/keyword-registry.d.ts.map +1 -1
  109. package/dist/hooks/keyword-registry.js +6 -0
  110. package/dist/hooks/keyword-registry.js.map +1 -1
  111. package/dist/hud/index.d.ts.map +1 -1
  112. package/dist/hud/index.js +2 -24
  113. package/dist/hud/index.js.map +1 -1
  114. package/dist/mcp/__tests__/path-traversal.test.js +9 -227
  115. package/dist/mcp/__tests__/path-traversal.test.js.map +1 -1
  116. package/dist/mcp/__tests__/state-server-schema.test.js +16 -20
  117. package/dist/mcp/__tests__/state-server-schema.test.js.map +1 -1
  118. package/dist/mcp/__tests__/state-server-team-tools.test.js +30 -487
  119. package/dist/mcp/__tests__/state-server-team-tools.test.js.map +1 -1
  120. package/dist/mcp/code-intel-server.d.ts.map +1 -1
  121. package/dist/mcp/code-intel-server.js +18 -8
  122. package/dist/mcp/code-intel-server.js.map +1 -1
  123. package/dist/mcp/memory-server.js +72 -11
  124. package/dist/mcp/memory-server.js.map +1 -1
  125. package/dist/mcp/state-paths.d.ts.map +1 -1
  126. package/dist/mcp/state-paths.js +4 -1
  127. package/dist/mcp/state-paths.js.map +1 -1
  128. package/dist/mcp/state-server.d.ts +179 -0
  129. package/dist/mcp/state-server.d.ts.map +1 -1
  130. package/dist/mcp/state-server.js +221 -1111
  131. package/dist/mcp/state-server.js.map +1 -1
  132. package/dist/mcp/team-server.d.ts.map +1 -1
  133. package/dist/mcp/team-server.js +9 -3
  134. package/dist/mcp/team-server.js.map +1 -1
  135. package/dist/mcp/trace-server.d.ts.map +1 -1
  136. package/dist/mcp/trace-server.js +8 -3
  137. package/dist/mcp/trace-server.js.map +1 -1
  138. package/dist/notifications/__tests__/dispatch-cooldown.test.d.ts +5 -0
  139. package/dist/notifications/__tests__/dispatch-cooldown.test.d.ts.map +1 -0
  140. package/dist/notifications/__tests__/dispatch-cooldown.test.js +100 -0
  141. package/dist/notifications/__tests__/dispatch-cooldown.test.js.map +1 -0
  142. package/dist/notifications/__tests__/temp-mode.test.d.ts +2 -0
  143. package/dist/notifications/__tests__/temp-mode.test.d.ts.map +1 -0
  144. package/dist/notifications/__tests__/temp-mode.test.js +172 -0
  145. package/dist/notifications/__tests__/temp-mode.test.js.map +1 -0
  146. package/dist/notifications/config.d.ts.map +1 -1
  147. package/dist/notifications/config.js +67 -7
  148. package/dist/notifications/config.js.map +1 -1
  149. package/dist/notifications/dispatch-cooldown.d.ts +36 -0
  150. package/dist/notifications/dispatch-cooldown.d.ts.map +1 -0
  151. package/dist/notifications/dispatch-cooldown.js +109 -0
  152. package/dist/notifications/dispatch-cooldown.js.map +1 -0
  153. package/dist/notifications/dispatcher.d.ts.map +1 -1
  154. package/dist/notifications/dispatcher.js +4 -4
  155. package/dist/notifications/dispatcher.js.map +1 -1
  156. package/dist/notifications/index.d.ts +5 -0
  157. package/dist/notifications/index.d.ts.map +1 -1
  158. package/dist/notifications/index.js +39 -8
  159. package/dist/notifications/index.js.map +1 -1
  160. package/dist/notifications/reply-listener.d.ts.map +1 -1
  161. package/dist/notifications/reply-listener.js +6 -2
  162. package/dist/notifications/reply-listener.js.map +1 -1
  163. package/dist/notifications/session-registry.d.ts.map +1 -1
  164. package/dist/notifications/session-registry.js +2 -2
  165. package/dist/notifications/session-registry.js.map +1 -1
  166. package/dist/notifications/temp-contract.d.ts +22 -0
  167. package/dist/notifications/temp-contract.d.ts.map +1 -0
  168. package/dist/notifications/temp-contract.js +147 -0
  169. package/dist/notifications/temp-contract.js.map +1 -0
  170. package/dist/notifications/tmux.js +2 -2
  171. package/dist/notifications/tmux.js.map +1 -1
  172. package/dist/notifications/types.d.ts +18 -0
  173. package/dist/notifications/types.d.ts.map +1 -1
  174. package/dist/openclaw/__tests__/config.test.js +81 -0
  175. package/dist/openclaw/__tests__/config.test.js.map +1 -1
  176. package/dist/openclaw/__tests__/dispatcher.test.js +40 -1
  177. package/dist/openclaw/__tests__/dispatcher.test.js.map +1 -1
  178. package/dist/openclaw/config.d.ts +4 -0
  179. package/dist/openclaw/config.d.ts.map +1 -1
  180. package/dist/openclaw/config.js +110 -16
  181. package/dist/openclaw/config.js.map +1 -1
  182. package/dist/openclaw/dispatcher.d.ts +9 -3
  183. package/dist/openclaw/dispatcher.d.ts.map +1 -1
  184. package/dist/openclaw/dispatcher.js +42 -9
  185. package/dist/openclaw/dispatcher.js.map +1 -1
  186. package/dist/openclaw/types.d.ts +5 -1
  187. package/dist/openclaw/types.d.ts.map +1 -1
  188. package/dist/ralph/__tests__/persistence.test.js +28 -1
  189. package/dist/ralph/__tests__/persistence.test.js.map +1 -1
  190. package/dist/ralph/persistence.d.ts +21 -0
  191. package/dist/ralph/persistence.d.ts.map +1 -1
  192. package/dist/ralph/persistence.js +85 -2
  193. package/dist/ralph/persistence.js.map +1 -1
  194. package/dist/state/paths.d.ts +3 -0
  195. package/dist/state/paths.d.ts.map +1 -0
  196. package/dist/state/paths.js +2 -0
  197. package/dist/state/paths.js.map +1 -0
  198. package/dist/team/__tests__/api-interop.test.d.ts +2 -0
  199. package/dist/team/__tests__/api-interop.test.d.ts.map +1 -0
  200. package/dist/team/__tests__/api-interop.test.js +1052 -0
  201. package/dist/team/__tests__/api-interop.test.js.map +1 -0
  202. package/dist/team/__tests__/idle-nudge.test.d.ts +2 -0
  203. package/dist/team/__tests__/idle-nudge.test.d.ts.map +1 -0
  204. package/dist/team/__tests__/idle-nudge.test.js +225 -0
  205. package/dist/team/__tests__/idle-nudge.test.js.map +1 -0
  206. package/dist/team/__tests__/mcp-comm.test.js +30 -0
  207. package/dist/team/__tests__/mcp-comm.test.js.map +1 -1
  208. package/dist/team/__tests__/runtime.test.js +33 -26
  209. package/dist/team/__tests__/runtime.test.js.map +1 -1
  210. package/dist/team/__tests__/state-root.test.d.ts +2 -0
  211. package/dist/team/__tests__/state-root.test.d.ts.map +1 -0
  212. package/dist/team/__tests__/state-root.test.js +9 -0
  213. package/dist/team/__tests__/state-root.test.js.map +1 -0
  214. package/dist/team/__tests__/state.test.js +52 -17
  215. package/dist/team/__tests__/state.test.js.map +1 -1
  216. package/dist/team/__tests__/team-ops-contract.test.d.ts +2 -0
  217. package/dist/team/__tests__/team-ops-contract.test.d.ts.map +1 -0
  218. package/dist/team/__tests__/team-ops-contract.test.js +90 -0
  219. package/dist/team/__tests__/team-ops-contract.test.js.map +1 -0
  220. package/dist/team/__tests__/tmux-claude-workers-demo.test.d.ts +2 -0
  221. package/dist/team/__tests__/tmux-claude-workers-demo.test.d.ts.map +1 -0
  222. package/dist/team/__tests__/tmux-claude-workers-demo.test.js +176 -0
  223. package/dist/team/__tests__/tmux-claude-workers-demo.test.js.map +1 -0
  224. package/dist/team/__tests__/tmux-session.test.js +8 -0
  225. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  226. package/dist/team/__tests__/worker-bootstrap.test.js +29 -0
  227. package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
  228. package/dist/team/__tests__/worktree.test.js +54 -1
  229. package/dist/team/__tests__/worktree.test.js.map +1 -1
  230. package/dist/team/api-interop.d.ts +19 -0
  231. package/dist/team/api-interop.d.ts.map +1 -0
  232. package/dist/team/api-interop.js +578 -0
  233. package/dist/team/api-interop.js.map +1 -0
  234. package/dist/team/mcp-comm.d.ts.map +1 -1
  235. package/dist/team/mcp-comm.js +32 -2
  236. package/dist/team/mcp-comm.js.map +1 -1
  237. package/dist/team/orchestrator.d.ts +1 -10
  238. package/dist/team/orchestrator.d.ts.map +1 -1
  239. package/dist/team/orchestrator.js +8 -0
  240. package/dist/team/orchestrator.js.map +1 -1
  241. package/dist/team/runtime-cli.js +14 -8
  242. package/dist/team/runtime-cli.js.map +1 -1
  243. package/dist/team/runtime.d.ts +2 -1
  244. package/dist/team/runtime.d.ts.map +1 -1
  245. package/dist/team/runtime.js +103 -30
  246. package/dist/team/runtime.js.map +1 -1
  247. package/dist/team/scaling.d.ts.map +1 -1
  248. package/dist/team/scaling.js +33 -12
  249. package/dist/team/scaling.js.map +1 -1
  250. package/dist/team/state/approvals.d.ts +25 -0
  251. package/dist/team/state/approvals.d.ts.map +1 -0
  252. package/dist/team/state/approvals.js +31 -0
  253. package/dist/team/state/approvals.js.map +1 -0
  254. package/dist/team/state/config.d.ts +2 -0
  255. package/dist/team/state/config.d.ts.map +1 -0
  256. package/dist/team/state/config.js +2 -0
  257. package/dist/team/state/config.js.map +1 -0
  258. package/dist/team/state/dispatch-lock.d.ts +3 -0
  259. package/dist/team/state/dispatch-lock.d.ts.map +1 -0
  260. package/dist/team/state/dispatch-lock.js +81 -0
  261. package/dist/team/state/dispatch-lock.js.map +1 -0
  262. package/dist/team/state/dispatch.d.ts +61 -0
  263. package/dist/team/state/dispatch.d.ts.map +1 -0
  264. package/dist/team/state/dispatch.js +158 -0
  265. package/dist/team/state/dispatch.js.map +1 -0
  266. package/dist/team/state/events.d.ts +2 -0
  267. package/dist/team/state/events.d.ts.map +1 -0
  268. package/dist/team/state/events.js +2 -0
  269. package/dist/team/state/events.js.map +1 -0
  270. package/dist/team/state/index.d.ts +11 -0
  271. package/dist/team/state/index.d.ts.map +1 -0
  272. package/dist/team/state/index.js +11 -0
  273. package/dist/team/state/index.js.map +1 -0
  274. package/dist/team/state/io.d.ts +2 -0
  275. package/dist/team/state/io.d.ts.map +1 -0
  276. package/dist/team/state/io.js +2 -0
  277. package/dist/team/state/io.js.map +1 -0
  278. package/dist/team/state/locks.d.ts +16 -0
  279. package/dist/team/state/locks.d.ts.map +1 -0
  280. package/dist/team/state/locks.js +201 -0
  281. package/dist/team/state/locks.js.map +1 -0
  282. package/dist/team/state/mailbox.d.ts +39 -0
  283. package/dist/team/state/mailbox.d.ts.map +1 -0
  284. package/dist/team/state/mailbox.js +58 -0
  285. package/dist/team/state/mailbox.js.map +1 -0
  286. package/dist/team/state/monitor.d.ts +96 -0
  287. package/dist/team/state/monitor.d.ts.map +1 -0
  288. package/dist/team/state/monitor.js +163 -0
  289. package/dist/team/state/monitor.js.map +1 -0
  290. package/dist/team/state/shutdown.d.ts +2 -0
  291. package/dist/team/state/shutdown.d.ts.map +1 -0
  292. package/dist/team/state/shutdown.js +2 -0
  293. package/dist/team/state/shutdown.js.map +1 -0
  294. package/dist/team/state/summary.d.ts +2 -0
  295. package/dist/team/state/summary.d.ts.map +1 -0
  296. package/dist/team/state/summary.js +2 -0
  297. package/dist/team/state/summary.js.map +1 -0
  298. package/dist/team/state/tasks.d.ts +49 -0
  299. package/dist/team/state/tasks.d.ts.map +1 -0
  300. package/dist/team/state/tasks.js +182 -0
  301. package/dist/team/state/tasks.js.map +1 -0
  302. package/dist/team/state/types.d.ts +281 -0
  303. package/dist/team/state/types.d.ts.map +1 -0
  304. package/dist/team/state/types.js +3 -0
  305. package/dist/team/state/types.js.map +1 -0
  306. package/dist/team/state/workers.d.ts +2 -0
  307. package/dist/team/state/workers.d.ts.map +1 -0
  308. package/dist/team/state/workers.js +2 -0
  309. package/dist/team/state/workers.js.map +1 -0
  310. package/dist/team/state-root.d.ts +5 -0
  311. package/dist/team/state-root.d.ts.map +1 -0
  312. package/dist/team/state-root.js +8 -0
  313. package/dist/team/state-root.js.map +1 -0
  314. package/dist/team/state.d.ts +4 -1
  315. package/dist/team/state.d.ts.map +1 -1
  316. package/dist/team/state.js +200 -881
  317. package/dist/team/state.js.map +1 -1
  318. package/dist/team/tmux-session.d.ts.map +1 -1
  319. package/dist/team/tmux-session.js +11 -10
  320. package/dist/team/tmux-session.js.map +1 -1
  321. package/dist/team/worker-bootstrap.d.ts.map +1 -1
  322. package/dist/team/worker-bootstrap.js +58 -26
  323. package/dist/team/worker-bootstrap.js.map +1 -1
  324. package/dist/team/worktree.d.ts.map +1 -1
  325. package/dist/team/worktree.js +43 -1
  326. package/dist/team/worktree.js.map +1 -1
  327. package/dist/utils/safe-json.d.ts +3 -0
  328. package/dist/utils/safe-json.d.ts.map +1 -0
  329. package/dist/utils/safe-json.js +19 -0
  330. package/dist/utils/safe-json.js.map +1 -0
  331. package/dist/utils/sleep.d.ts +3 -0
  332. package/dist/utils/sleep.d.ts.map +1 -0
  333. package/dist/utils/sleep.js +15 -0
  334. package/dist/utils/sleep.js.map +1 -0
  335. package/dist/visual/__tests__/verdict.test.d.ts +2 -0
  336. package/dist/visual/__tests__/verdict.test.d.ts.map +1 -0
  337. package/dist/visual/__tests__/verdict.test.js +81 -0
  338. package/dist/visual/__tests__/verdict.test.js.map +1 -0
  339. package/dist/visual/constants.d.ts +4 -0
  340. package/dist/visual/constants.d.ts.map +1 -0
  341. package/dist/visual/constants.js +3 -0
  342. package/dist/visual/constants.js.map +1 -0
  343. package/dist/visual/verdict.d.ts +17 -0
  344. package/dist/visual/verdict.d.ts.map +1 -0
  345. package/dist/visual/verdict.js +61 -0
  346. package/dist/visual/verdict.js.map +1 -0
  347. package/package.json +10 -3
  348. package/scripts/ask-claude.sh +17 -0
  349. package/scripts/ask-gemini.sh +14 -0
  350. package/scripts/demo-claude-workers.sh +241 -0
  351. package/scripts/demo-team-e2e.sh +179 -0
  352. package/scripts/fixtures/ask-advisor-stub.js +12 -0
  353. package/scripts/notify-hook/team-dispatch.js +234 -12
  354. package/scripts/notify-hook/team-leader-nudge.js +42 -2
  355. package/scripts/notify-hook/team-worker.js +63 -4
  356. package/scripts/notify-hook/visual-verdict.js +50 -1
  357. package/scripts/notify-hook.js +1 -0
  358. package/scripts/run-provider-advisor.js +179 -0
  359. package/skills/ask-claude/SKILL.md +61 -0
  360. package/skills/ask-gemini/SKILL.md +61 -0
  361. package/skills/autopilot/SKILL.md +32 -2
  362. package/skills/configure-notifications/SKILL.md +188 -186
  363. package/skills/deep-interview/SKILL.md +247 -0
  364. package/skills/omx-setup/SKILL.md +1 -1
  365. package/skills/ralph/SKILL.md +42 -11
  366. package/skills/ralplan/SKILL.md +17 -0
  367. package/skills/team/SKILL.md +64 -5
  368. package/skills/visual-verdict/SKILL.md +76 -0
  369. package/skills/web-clone/SKILL.md +366 -0
  370. package/skills/worker/SKILL.md +42 -11
  371. package/templates/AGENTS.md +9 -0
  372. package/templates/catalog-manifest.json +39 -18
  373. package/skills/configure-discord/SKILL.md +0 -256
  374. package/skills/configure-openclaw/SKILL.md +0 -267
  375. package/skills/configure-slack/SKILL.md +0 -226
  376. package/skills/configure-telegram/SKILL.md +0 -232
@@ -21,6 +21,76 @@ async function writeJsonAtomic(path, value) {
21
21
 
22
22
  // Keep stale-timeout semantics aligned with src/team/state.ts LOCK_STALE_MS.
23
23
  const DISPATCH_LOCK_STALE_MS = 5 * 60 * 1000;
24
+ const DEFAULT_ISSUE_DISPATCH_COOLDOWN_MS = 15 * 60 * 1000;
25
+ const ISSUE_DISPATCH_COOLDOWN_ENV = 'OMX_TEAM_DISPATCH_ISSUE_COOLDOWN_MS';
26
+ const DEFAULT_DISPATCH_TRIGGER_COOLDOWN_MS = 30 * 1000;
27
+ const DISPATCH_TRIGGER_COOLDOWN_ENV = 'OMX_TEAM_DISPATCH_TRIGGER_COOLDOWN_MS';
28
+ const LEADER_PANE_MISSING_DEFERRED_REASON = 'leader_pane_missing_deferred';
29
+ const LEADER_NOTIFICATION_DEFERRED_TYPE = 'leader_notification_deferred';
30
+
31
+ function resolveIssueDispatchCooldownMs(env = process.env) {
32
+ const raw = safeString(env[ISSUE_DISPATCH_COOLDOWN_ENV]).trim();
33
+ if (raw === '') return DEFAULT_ISSUE_DISPATCH_COOLDOWN_MS;
34
+ const parsed = Number.parseInt(raw, 10);
35
+ if (!Number.isFinite(parsed) || parsed < 0) return DEFAULT_ISSUE_DISPATCH_COOLDOWN_MS;
36
+ return parsed;
37
+ }
38
+
39
+ function resolveDispatchTriggerCooldownMs(env = process.env) {
40
+ const raw = safeString(env[DISPATCH_TRIGGER_COOLDOWN_ENV]).trim();
41
+ if (raw === '') return DEFAULT_DISPATCH_TRIGGER_COOLDOWN_MS;
42
+ const parsed = Number.parseInt(raw, 10);
43
+ if (!Number.isFinite(parsed) || parsed < 0) return DEFAULT_DISPATCH_TRIGGER_COOLDOWN_MS;
44
+ return parsed;
45
+ }
46
+
47
+ function extractIssueKey(triggerMessage) {
48
+ const match = safeString(triggerMessage).match(/\b([A-Z][A-Z0-9]+-\d+)\b/i);
49
+ return match?.[1]?.toUpperCase() || null;
50
+ }
51
+
52
+ function issueCooldownStatePath(teamDirPath) {
53
+ return join(teamDirPath, 'dispatch', 'issue-cooldown.json');
54
+ }
55
+
56
+ function triggerCooldownStatePath(teamDirPath) {
57
+ return join(teamDirPath, 'dispatch', 'trigger-cooldown.json');
58
+ }
59
+
60
+ async function readIssueCooldownState(teamDirPath) {
61
+ const fallback = { by_issue: {} };
62
+ const parsed = await readJson(issueCooldownStatePath(teamDirPath), fallback);
63
+ if (!parsed || typeof parsed !== 'object' || typeof parsed.by_issue !== 'object' || parsed.by_issue === null) {
64
+ return fallback;
65
+ }
66
+ return parsed;
67
+ }
68
+
69
+ async function readTriggerCooldownState(teamDirPath) {
70
+ const fallback = { by_trigger: {} };
71
+ const parsed = await readJson(triggerCooldownStatePath(teamDirPath), fallback);
72
+ if (!parsed || typeof parsed !== 'object' || typeof parsed.by_trigger !== 'object' || parsed.by_trigger === null) {
73
+ return fallback;
74
+ }
75
+ return parsed;
76
+ }
77
+
78
+ function normalizeTriggerKey(value) {
79
+ return safeString(value).replace(/\s+/g, ' ').trim();
80
+ }
81
+
82
+ function parseTriggerCooldownEntry(entry) {
83
+ if (typeof entry === 'number') {
84
+ return { at: entry, lastRequestId: '' };
85
+ }
86
+ if (!entry || typeof entry !== 'object') {
87
+ return { at: NaN, lastRequestId: '' };
88
+ }
89
+ return {
90
+ at: Number(entry.at),
91
+ lastRequestId: safeString(entry.last_request_id).trim(),
92
+ };
93
+ }
24
94
 
25
95
  async function withDispatchLock(teamDirPath, fn) {
26
96
  const lockDir = join(teamDirPath, 'dispatch', '.lock');
@@ -117,18 +187,15 @@ async function withMailboxLock(teamDirPath, workerName, fn) {
117
187
  }
118
188
 
119
189
  function defaultInjectTarget(request, config) {
190
+ if (request.to_worker === 'leader-fixed') {
191
+ if (config.leader_pane_id) return { type: 'pane', value: config.leader_pane_id };
192
+ return null;
193
+ }
120
194
  if (request.pane_id) return { type: 'pane', value: request.pane_id };
121
195
  if (typeof request.worker_index === 'number' && Array.isArray(config?.workers)) {
122
196
  const worker = config.workers.find((candidate) => Number(candidate?.index) === request.worker_index);
123
197
  if (worker?.pane_id) return { type: 'pane', value: worker.pane_id };
124
198
  }
125
- // Leader-fixed fallback: use config.leader_pane_id when request has no
126
- // pane_id or worker_index (leader is not a worker). Without this, leader
127
- // dispatch falls through to the session target which hits the active pane
128
- // (likely a worker). Fixes #433.
129
- if (request.to_worker === 'leader-fixed' && config.leader_pane_id) {
130
- return { type: 'pane', value: config.leader_pane_id };
131
- }
132
199
  if (typeof request.worker_index === 'number' && config.tmux_session) {
133
200
  return { type: 'pane', value: `${config.tmux_session}.${request.worker_index}` };
134
201
  }
@@ -136,6 +203,30 @@ function defaultInjectTarget(request, config) {
136
203
  return null;
137
204
  }
138
205
 
206
+ async function appendLeaderNotificationDeferredEvent({
207
+ stateDir,
208
+ teamName,
209
+ request,
210
+ reason,
211
+ nowIso,
212
+ }) {
213
+ const eventsDir = join(stateDir, 'team', teamName, 'events');
214
+ const eventsPath = join(eventsDir, 'events.ndjson');
215
+ const event = {
216
+ event_id: `leader-deferred-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
217
+ team: teamName,
218
+ type: LEADER_NOTIFICATION_DEFERRED_TYPE,
219
+ worker: request.to_worker,
220
+ to_worker: request.to_worker,
221
+ reason,
222
+ created_at: nowIso,
223
+ request_id: request.request_id,
224
+ ...(request.message_id ? { message_id: request.message_id } : {}),
225
+ };
226
+ await mkdir(eventsDir, { recursive: true }).catch(() => {});
227
+ await appendFile(eventsPath, JSON.stringify(event) + '\n').catch(() => {});
228
+ }
229
+
139
230
  function resolveWorkerCliForRequest(request, config) {
140
231
  const workers = Array.isArray(config?.workers) ? config.workers : [];
141
232
  const idx = Number.isFinite(request?.worker_index) ? Number(request.worker_index) : null;
@@ -156,6 +247,19 @@ function capturedPaneContainsTrigger(captured, trigger) {
156
247
  return normalizeCaptureText(captured).includes(normalizeCaptureText(trigger));
157
248
  }
158
249
 
250
+ function capturedPaneContainsTriggerNearTail(captured, trigger, nonEmptyTailLines = 24) {
251
+ if (!captured || !trigger) return false;
252
+ const normalizedTrigger = normalizeCaptureText(trigger);
253
+ if (!normalizedTrigger) return false;
254
+ const lines = safeString(captured)
255
+ .split('\n')
256
+ .map((line) => line.replace(/\r/g, '').trim())
257
+ .filter((line) => line.length > 0);
258
+ if (lines.length === 0) return false;
259
+ const tail = lines.slice(-Math.max(1, nonEmptyTailLines)).join(' ');
260
+ return normalizeCaptureText(tail).includes(normalizedTrigger);
261
+ }
262
+
159
263
  // Ported from src/team/tmux-session.ts:949-963 — detects active CLI task indicators.
160
264
  function paneHasActiveTask(captured) {
161
265
  const lines = safeString(captured)
@@ -163,6 +267,7 @@ function paneHasActiveTask(captured) {
163
267
  .map((line) => line.replace(/\r/g, '').trim())
164
268
  .filter((line) => line.length > 0);
165
269
  const tail = lines.slice(-40);
270
+ if (tail.some((line) => /\b\d+\s+background terminal running\b/i.test(line))) return true;
166
271
  if (tail.some((line) => /esc to interrupt/i.test(line))) return true;
167
272
  if (tail.some((line) => /\bbackground terminal running\b/i.test(line))) return true;
168
273
  if (tail.some((line) => /^•\s.+\(.+•\s*esc to interrupt\)$/i.test(line))) return true;
@@ -171,6 +276,40 @@ function paneHasActiveTask(captured) {
171
276
  return false;
172
277
  }
173
278
 
279
+ function paneIsBootstrapping(captured) {
280
+ const lines = safeString(captured)
281
+ .split('\n')
282
+ .map((line) => line.replace(/\r/g, '').trim())
283
+ .filter((line) => line.length > 0);
284
+ return lines.some((line) =>
285
+ /\b(loading|initializing|starting up)\b/i.test(line)
286
+ || /\bmodel:\s*loading\b/i.test(line)
287
+ || /\bconnecting\s+to\b/i.test(line),
288
+ );
289
+ }
290
+
291
+ function paneLooksReady(captured) {
292
+ const content = safeString(captured).trimEnd();
293
+ if (content === '') return false;
294
+
295
+ const lines = content
296
+ .split('\n')
297
+ .map((line) => line.replace(/\r/g, ''))
298
+ .map((line) => line.trimEnd())
299
+ .filter((line) => line.trim() !== '');
300
+
301
+ if (paneIsBootstrapping(content)) return false;
302
+
303
+ const lastLine = lines.length > 0 ? lines[lines.length - 1] : '';
304
+ if (/^\s*[›>❯]\s*/u.test(lastLine)) return true;
305
+
306
+ const hasCodexPromptLine = lines.some((line) => /^\s*›\s*/u.test(line));
307
+ const hasClaudePromptLine = lines.some((line) => /^\s*❯\s*/u.test(line));
308
+ if (hasCodexPromptLine || hasClaudePromptLine) return true;
309
+
310
+ return false;
311
+ }
312
+
174
313
  const INJECT_VERIFY_DELAY_MS = 250;
175
314
  const INJECT_VERIFY_ROUNDS = 3;
176
315
 
@@ -237,16 +376,29 @@ async function injectDispatchRequest(request, config, cwd) {
237
376
  for (let round = 0; round < INJECT_VERIFY_ROUNDS; round++) {
238
377
  await new Promise((r) => setTimeout(r, INJECT_VERIFY_DELAY_MS));
239
378
  try {
240
- // Primary: trigger text no longer in narrow input area
379
+ // Primary: trigger text no longer in narrow input area.
380
+ // Secondary guard: also inspect the recent non-empty tail of wide capture.
381
+ // This avoids false confirmations when Codex leaves the unsent draft just
382
+ // above a large blank area (narrow capture misses it) while still avoiding
383
+ // full-scrollback false positives.
241
384
  const narrowCap = await runProcess('tmux', verifyNarrowArgv, 2000);
242
- if (!capturedPaneContainsTrigger(narrowCap.stdout, request.trigger_message)) {
243
- return { ok: true, reason: 'tmux_send_keys_confirmed', pane: resolution.paneTarget };
244
- }
245
- // Secondary: worker is actively processing (mirrors sync path tmux-session.ts:1292-1294)
246
385
  const wideCap = await runProcess('tmux', verifyWideArgv, 2000);
386
+ // Worker is actively processing (mirrors sync path tmux-session.ts:1292-1294)
247
387
  if (paneHasActiveTask(wideCap.stdout)) {
248
388
  return { ok: true, reason: 'tmux_send_keys_confirmed_active_task', pane: resolution.paneTarget };
249
389
  }
390
+ // Do not declare success while a *worker* pane is still bootstrapping / not
391
+ // input-ready. Otherwise a pre-ready send can be marked "confirmed" and later
392
+ // appear as a stuck unsent draft once the UI finishes loading.
393
+ // Keep leader-fixed behavior unchanged to avoid regressing leader notification flow.
394
+ if (request.to_worker !== 'leader-fixed' && !paneLooksReady(wideCap.stdout)) {
395
+ continue;
396
+ }
397
+ const triggerInNarrow = capturedPaneContainsTrigger(narrowCap.stdout, request.trigger_message);
398
+ const triggerNearTail = capturedPaneContainsTriggerNearTail(wideCap.stdout, request.trigger_message);
399
+ if (!triggerInNarrow && !triggerNearTail) {
400
+ return { ok: true, reason: 'tmux_send_keys_confirmed', pane: resolution.paneTarget };
401
+ }
250
402
  } catch {
251
403
  // capture failed; fall through to retry C-m
252
404
  }
@@ -302,6 +454,8 @@ export async function drainPendingTeamDispatch({
302
454
  let processed = 0;
303
455
  let skipped = 0;
304
456
  let failed = 0;
457
+ const issueCooldownMs = resolveIssueDispatchCooldownMs();
458
+ const triggerCooldownMs = resolveDispatchTriggerCooldownMs();
305
459
 
306
460
  for (const teamName of teams) {
307
461
  if (processed >= maxPerTick) break;
@@ -315,6 +469,11 @@ export async function drainPendingTeamDispatch({
315
469
  await withDispatchLock(teamDirPath, async () => {
316
470
  const requests = await readJson(requestsPath, []);
317
471
  if (!Array.isArray(requests)) return;
472
+ const issueCooldownState = await readIssueCooldownState(teamDirPath);
473
+ const triggerCooldownState = await readTriggerCooldownState(teamDirPath);
474
+ const issueCooldownByIssue = issueCooldownState.by_issue || {};
475
+ const triggerCooldownByKey = triggerCooldownState.by_trigger || {};
476
+ const nowMs = Date.now();
318
477
 
319
478
  let mutated = false;
320
479
  for (const request of requests) {
@@ -325,7 +484,66 @@ export async function drainPendingTeamDispatch({
325
484
  continue;
326
485
  }
327
486
 
487
+ if (request.to_worker === 'leader-fixed' && !safeString(config?.leader_pane_id).trim()) {
488
+ const nowIso = new Date().toISOString();
489
+ request.updated_at = nowIso;
490
+ request.last_reason = LEADER_PANE_MISSING_DEFERRED_REASON;
491
+ request.status = 'pending';
492
+ skipped += 1;
493
+ mutated = true;
494
+ await appendDispatchLog(logsDir, {
495
+ type: 'dispatch_deferred',
496
+ team: teamName,
497
+ request_id: request.request_id,
498
+ worker: request.to_worker,
499
+ to_worker: request.to_worker,
500
+ message_id: request.message_id || null,
501
+ reason: LEADER_PANE_MISSING_DEFERRED_REASON,
502
+ status: 'pending',
503
+ tmux_injection_attempted: false,
504
+ });
505
+ await appendLeaderNotificationDeferredEvent({
506
+ stateDir,
507
+ teamName,
508
+ request,
509
+ reason: LEADER_PANE_MISSING_DEFERRED_REASON,
510
+ nowIso,
511
+ });
512
+ continue;
513
+ }
514
+
515
+ const issueKey = extractIssueKey(request.trigger_message);
516
+ if (issueCooldownMs > 0 && issueKey) {
517
+ const lastInjectedMs = Number(issueCooldownByIssue[issueKey]);
518
+ if (Number.isFinite(lastInjectedMs) && lastInjectedMs > 0 && nowMs - lastInjectedMs < issueCooldownMs) {
519
+ skipped += 1;
520
+ continue;
521
+ }
522
+ }
523
+
524
+ const triggerKey = normalizeTriggerKey(request.trigger_message);
525
+ if (triggerCooldownMs > 0 && triggerKey) {
526
+ const parsed = parseTriggerCooldownEntry(triggerCooldownByKey[triggerKey]);
527
+ const withinCooldown = Number.isFinite(parsed.at) && parsed.at > 0 && nowMs - parsed.at < triggerCooldownMs;
528
+ const sameRequestRetry = parsed.lastRequestId !== '' && parsed.lastRequestId === safeString(request.request_id).trim();
529
+ if (withinCooldown && !sameRequestRetry) {
530
+ skipped += 1;
531
+ continue;
532
+ }
533
+ }
534
+
328
535
  const result = await injector(request, config, resolve(cwd));
536
+ if (issueKey && issueCooldownMs > 0) {
537
+ issueCooldownByIssue[issueKey] = Date.now();
538
+ mutated = true;
539
+ }
540
+ if (triggerKey && triggerCooldownMs > 0) {
541
+ triggerCooldownByKey[triggerKey] = {
542
+ at: Date.now(),
543
+ last_request_id: safeString(request.request_id).trim(),
544
+ };
545
+ mutated = true;
546
+ }
329
547
  const nowIso = new Date().toISOString();
330
548
  request.attempt_count = Number.isFinite(request.attempt_count) ? Math.max(0, request.attempt_count + 1) : 1;
331
549
  request.updated_at = nowIso;
@@ -401,6 +619,10 @@ export async function drainPendingTeamDispatch({
401
619
  }
402
620
 
403
621
  if (mutated) {
622
+ issueCooldownState.by_issue = issueCooldownByIssue;
623
+ await writeJsonAtomic(issueCooldownStatePath(teamDirPath), issueCooldownState);
624
+ triggerCooldownState.by_trigger = triggerCooldownByKey;
625
+ await writeJsonAtomic(triggerCooldownStatePath(teamDirPath), triggerCooldownState);
404
626
  await writeJsonAtomic(requestsPath, requests);
405
627
  }
406
628
  });
@@ -10,6 +10,8 @@ import { readJsonIfExists, getScopedStateDirsForCurrentSession } from './state-i
10
10
  import { runProcess } from './process-runner.js';
11
11
  import { logTmuxHookEvent } from './log.js';
12
12
  import { DEFAULT_MARKER } from '../tmux-hook-engine.js';
13
+ const LEADER_PANE_MISSING_NO_INJECTION_REASON = 'leader_pane_missing_no_injection';
14
+ const LEADER_NOTIFICATION_DEFERRED_TYPE = 'leader_notification_deferred';
13
15
 
14
16
  export function resolveLeaderNudgeIntervalMs() {
15
17
  const raw = safeString(process.env.OMX_TEAM_LEADER_NUDGE_MS || '');
@@ -109,6 +111,26 @@ export async function emitTeamNudgeEvent(cwd, teamName, reason, nowIso) {
109
111
  }
110
112
  }
111
113
 
114
+ async function emitLeaderNudgeDeferredEvent(cwd, teamName, reason, nowIso) {
115
+ const eventsDir = join(cwd, '.omx', 'state', 'team', teamName, 'events');
116
+ const eventsPath = join(eventsDir, 'events.ndjson');
117
+ try {
118
+ await mkdir(eventsDir, { recursive: true });
119
+ const event = {
120
+ event_id: `leader-deferred-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
121
+ team: teamName,
122
+ type: LEADER_NOTIFICATION_DEFERRED_TYPE,
123
+ worker: 'leader-fixed',
124
+ to_worker: 'leader-fixed',
125
+ reason,
126
+ created_at: nowIso,
127
+ };
128
+ await appendFile(eventsPath, JSON.stringify(event) + '\n');
129
+ } catch {
130
+ // Best effort
131
+ }
132
+ }
133
+
112
134
  export async function maybeNudgeTeamLeader({ cwd, stateDir, logsDir, preComputedLeaderStale }) {
113
135
  const intervalMs = resolveLeaderNudgeIntervalMs();
114
136
  const idleCooldownMs = resolveLeaderAllIdleNudgeCooldownMs();
@@ -163,8 +185,8 @@ export async function maybeNudgeTeamLeader({ cwd, stateDir, logsDir, preComputed
163
185
  } catch {
164
186
  // ignore
165
187
  }
166
- const tmuxTarget = leaderPaneId || tmuxSession;
167
- if (!tmuxTarget) continue;
188
+ if (!tmuxSession && !leaderPaneId) continue;
189
+ const tmuxTarget = leaderPaneId;
168
190
 
169
191
  const paneStatus = tmuxSession
170
192
  ? await checkWorkerPanesAlive(tmuxSession)
@@ -234,6 +256,24 @@ export async function maybeNudgeTeamLeader({ cwd, stateDir, logsDir, preComputed
234
256
  const capped = text.length > 180 ? `${text.slice(0, 177)}...` : text;
235
257
  const markedText = `${capped} ${DEFAULT_MARKER}`;
236
258
 
259
+ if (!tmuxTarget) {
260
+ await emitLeaderNudgeDeferredEvent(cwd, teamName, LEADER_PANE_MISSING_NO_INJECTION_REASON, nowIso);
261
+ try {
262
+ await logTmuxHookEvent(logsDir, {
263
+ timestamp: nowIso,
264
+ type: LEADER_NOTIFICATION_DEFERRED_TYPE,
265
+ team: teamName,
266
+ worker: 'leader-fixed',
267
+ to_worker: 'leader-fixed',
268
+ reason: LEADER_PANE_MISSING_NO_INJECTION_REASON,
269
+ leader_pane_id: null,
270
+ tmux_session: tmuxSession || null,
271
+ tmux_injection_attempted: false,
272
+ });
273
+ } catch { /* ignore */ }
274
+ continue;
275
+ }
276
+
237
277
  try {
238
278
  await runProcess('tmux', ['send-keys', '-t', tmuxTarget, '-l', markedText], 3000);
239
279
  await new Promise(r => setTimeout(r, 100));
@@ -194,6 +194,43 @@ export async function readTeamWorkersForIdleCheck(stateDir, teamName) {
194
194
  }
195
195
  }
196
196
 
197
+ async function emitLeaderPaneMissingDeferred({
198
+ stateDir,
199
+ logsDir,
200
+ teamName,
201
+ workerName,
202
+ tmuxSession,
203
+ leaderPaneId,
204
+ reason = 'leader_pane_missing_no_injection',
205
+ }) {
206
+ const nowIso = new Date().toISOString();
207
+ await logTmuxHookEvent(logsDir, {
208
+ timestamp: nowIso,
209
+ type: 'leader_notification_deferred',
210
+ team: teamName,
211
+ worker: workerName,
212
+ to_worker: 'leader-fixed',
213
+ reason,
214
+ leader_pane_id: leaderPaneId || null,
215
+ tmux_session: tmuxSession || null,
216
+ tmux_injection_attempted: false,
217
+ }).catch(() => {});
218
+
219
+ const eventsDir = join(stateDir, 'team', teamName, 'events');
220
+ const eventsPath = join(eventsDir, 'events.ndjson');
221
+ await mkdir(eventsDir, { recursive: true }).catch(() => {});
222
+ const event = {
223
+ event_id: `leader-deferred-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
224
+ team: teamName,
225
+ type: 'leader_notification_deferred',
226
+ worker: workerName,
227
+ to_worker: 'leader-fixed',
228
+ reason,
229
+ created_at: nowIso,
230
+ };
231
+ await appendFile(eventsPath, JSON.stringify(event) + '\n').catch(() => {});
232
+ }
233
+
197
234
  export async function updateWorkerHeartbeat(stateDir, teamName, workerName) {
198
235
  const heartbeatPath = join(stateDir, 'team', teamName, 'workers', workerName, 'heartbeat.json');
199
236
  let turnCount = 0;
@@ -228,8 +265,6 @@ export async function maybeNotifyLeaderAllWorkersIdle({ cwd, stateDir, logsDir,
228
265
  const teamInfo = await readTeamWorkersForIdleCheck(stateDir, teamName);
229
266
  if (!teamInfo) return;
230
267
  const { workers, tmuxSession, leaderPaneId } = teamInfo;
231
- const tmuxTarget = leaderPaneId || tmuxSession;
232
- if (!tmuxTarget) return;
233
268
 
234
269
  // Check cooldown to prevent notification spam
235
270
  const idleStatePath = join(stateDir, 'team', teamName, 'all-workers-idle.json');
@@ -252,8 +287,21 @@ export async function maybeNotifyLeaderAllWorkersIdle({ cwd, stateDir, logsDir,
252
287
  );
253
288
  if (!allIdle) return;
254
289
 
290
+ if (!leaderPaneId) {
291
+ await emitLeaderPaneMissingDeferred({
292
+ stateDir,
293
+ logsDir,
294
+ teamName,
295
+ workerName,
296
+ tmuxSession,
297
+ leaderPaneId,
298
+ });
299
+ return;
300
+ }
301
+
255
302
  const N = workers.length;
256
303
  const message = `[OMX] All ${N} worker${N === 1 ? '' : 's'} idle. Ready for next instructions. ${DEFAULT_MARKER}`;
304
+ const tmuxTarget = leaderPaneId;
257
305
 
258
306
  try {
259
307
  await runProcess('tmux', ['send-keys', '-t', tmuxTarget, '-l', message], 3000);
@@ -382,8 +430,19 @@ export async function maybeNotifyLeaderWorkerIdle({ cwd, stateDir, logsDir, pars
382
430
  const teamInfo = await readTeamWorkersForIdleCheck(stateDir, teamName);
383
431
  if (!teamInfo) return;
384
432
  const { tmuxSession, leaderPaneId } = teamInfo;
385
- const tmuxTarget = leaderPaneId || tmuxSession;
386
- if (!tmuxTarget) return;
433
+
434
+ if (!leaderPaneId) {
435
+ await emitLeaderPaneMissingDeferred({
436
+ stateDir,
437
+ logsDir,
438
+ teamName,
439
+ workerName,
440
+ tmuxSession,
441
+ leaderPaneId,
442
+ });
443
+ return;
444
+ }
445
+ const tmuxTarget = leaderPaneId;
387
446
 
388
447
  // Build notification message with context
389
448
  const parts = [`[OMX] ${workerName} idle`];
@@ -25,6 +25,40 @@ const VERDICT_PATTERNS = [
25
25
  */
26
26
  const VERDICT_CANDIDATE_RE = /(?:\*\*Status\*\*\s*:|Verdict\s*:)/i;
27
27
 
28
+ function extractJsonCandidates(rawMessage) {
29
+ const message = safeString(rawMessage).trim();
30
+ if (!message) return [];
31
+
32
+ const candidates = [message];
33
+ const fencePattern = /```(?:json)?\s*([\s\S]*?)```/gi;
34
+ for (const match of message.matchAll(fencePattern)) {
35
+ const block = safeString(match[1]).trim();
36
+ if (block) candidates.push(block);
37
+ }
38
+ return candidates;
39
+ }
40
+
41
+ async function maybePersistRuntimeVisualFeedback({ cwd, output, sessionId }) {
42
+ if (!cwd || !output) return;
43
+
44
+ const candidates = extractJsonCandidates(output);
45
+ if (candidates.length === 0) return;
46
+
47
+ const { buildVisualLoopFeedback } = await import('../../dist/visual/verdict.js');
48
+ const { recordRalphVisualFeedback } = await import('../../dist/ralph/persistence.js');
49
+
50
+ for (const candidate of candidates) {
51
+ try {
52
+ const parsed = JSON.parse(candidate);
53
+ const feedback = buildVisualLoopFeedback(parsed);
54
+ await recordRalphVisualFeedback(cwd, feedback, sessionId || undefined);
55
+ return;
56
+ } catch {
57
+ // Try next candidate
58
+ }
59
+ }
60
+ }
61
+
28
62
  /**
29
63
  * Attempt to extract a structured verdict from free-form text.
30
64
  * Returns `{ verdict, raw }` on success, `null` otherwise.
@@ -50,12 +84,27 @@ export function parseVisualVerdict(text) {
50
84
  *
51
85
  * Module import failure is handled by the caller in notify-hook.js.
52
86
  */
53
- export async function maybePersistVisualVerdict({ payload, stateDir, logsDir, sessionId, turnId }) {
87
+ export async function maybePersistVisualVerdict({ cwd, payload, stateDir, logsDir, sessionId, turnId }) {
54
88
  const output = safeString(
55
89
  payload?.['last-assistant-message'] || payload?.last_assistant_message || '',
56
90
  );
57
91
  if (!output) return;
58
92
 
93
+ // Runtime visual feedback (JSON/fenced JSON) for ralph-progress persistence.
94
+ // Non-fatal and observable via warn-level structured logging.
95
+ try {
96
+ await maybePersistRuntimeVisualFeedback({ cwd, output, sessionId });
97
+ } catch (err) {
98
+ await logNotifyHookEvent(logsDir, {
99
+ timestamp: new Date().toISOString(),
100
+ level: 'warn',
101
+ type: 'visual_runtime_feedback_persist_failure',
102
+ error: err?.message || String(err),
103
+ session_id: sessionId,
104
+ turn_id: turnId,
105
+ });
106
+ }
107
+
59
108
  const parsed = parseVisualVerdict(output);
60
109
 
61
110
  if (!parsed) {
@@ -409,6 +409,7 @@ async function main() {
409
409
  try {
410
410
  const { maybePersistVisualVerdict } = await import('./notify-hook/visual-verdict.js');
411
411
  await maybePersistVisualVerdict({
412
+ cwd,
412
413
  payload,
413
414
  stateDir,
414
415
  logsDir,