oh-my-codex 0.7.5 → 0.8.0

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 (374) hide show
  1. package/README.de.md +314 -0
  2. package/README.es.md +295 -17
  3. package/README.fr.md +314 -0
  4. package/README.it.md +314 -0
  5. package/README.ja.md +296 -18
  6. package/README.ko.md +295 -17
  7. package/README.md +68 -3
  8. package/README.pt.md +295 -17
  9. package/README.ru.md +295 -17
  10. package/README.tr.md +314 -0
  11. package/README.vi.md +296 -18
  12. package/README.zh.md +292 -17
  13. package/dist/catalog/__tests__/generator.test.js +2 -0
  14. package/dist/catalog/__tests__/generator.test.js.map +1 -1
  15. package/dist/catalog/__tests__/schema.test.js +7 -0
  16. package/dist/catalog/__tests__/schema.test.js.map +1 -1
  17. package/dist/cli/__tests__/ask.test.d.ts +2 -0
  18. package/dist/cli/__tests__/ask.test.d.ts.map +1 -0
  19. package/dist/cli/__tests__/ask.test.js +236 -0
  20. package/dist/cli/__tests__/ask.test.js.map +1 -0
  21. package/dist/cli/__tests__/doctor-warning-copy.test.d.ts +2 -0
  22. package/dist/cli/__tests__/doctor-warning-copy.test.d.ts.map +1 -0
  23. package/dist/cli/__tests__/doctor-warning-copy.test.js +45 -0
  24. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -0
  25. package/dist/cli/__tests__/index.test.js +43 -1
  26. package/dist/cli/__tests__/index.test.js.map +1 -1
  27. package/dist/cli/__tests__/ralph-prd-deep-interview.test.d.ts +2 -0
  28. package/dist/cli/__tests__/ralph-prd-deep-interview.test.d.ts.map +1 -0
  29. package/dist/cli/__tests__/ralph-prd-deep-interview.test.js +15 -0
  30. package/dist/cli/__tests__/ralph-prd-deep-interview.test.js.map +1 -0
  31. package/dist/cli/__tests__/ralph.test.d.ts +2 -0
  32. package/dist/cli/__tests__/ralph.test.d.ts.map +1 -0
  33. package/dist/cli/__tests__/ralph.test.js +40 -0
  34. package/dist/cli/__tests__/ralph.test.js.map +1 -0
  35. package/dist/cli/__tests__/setup-scope.test.js +2 -0
  36. package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
  37. package/dist/cli/__tests__/team-decompose.test.d.ts +2 -0
  38. package/dist/cli/__tests__/team-decompose.test.d.ts.map +1 -0
  39. package/dist/cli/__tests__/team-decompose.test.js +67 -0
  40. package/dist/cli/__tests__/team-decompose.test.js.map +1 -0
  41. package/dist/cli/__tests__/version.test.d.ts +2 -0
  42. package/dist/cli/__tests__/version.test.d.ts.map +1 -0
  43. package/dist/cli/__tests__/version.test.js +21 -0
  44. package/dist/cli/__tests__/version.test.js.map +1 -0
  45. package/dist/cli/ask.d.ts +13 -0
  46. package/dist/cli/ask.d.ts.map +1 -0
  47. package/dist/cli/ask.js +174 -0
  48. package/dist/cli/ask.js.map +1 -0
  49. package/dist/cli/constants.d.ts +10 -0
  50. package/dist/cli/constants.d.ts.map +1 -0
  51. package/dist/cli/constants.js +10 -0
  52. package/dist/cli/constants.js.map +1 -0
  53. package/dist/cli/doctor.js +16 -5
  54. package/dist/cli/doctor.js.map +1 -1
  55. package/dist/cli/index.d.ts +7 -1
  56. package/dist/cli/index.d.ts.map +1 -1
  57. package/dist/cli/index.js +117 -43
  58. package/dist/cli/index.js.map +1 -1
  59. package/dist/cli/ralph.d.ts +4 -0
  60. package/dist/cli/ralph.d.ts.map +1 -1
  61. package/dist/cli/ralph.js +89 -13
  62. package/dist/cli/ralph.js.map +1 -1
  63. package/dist/cli/setup.js +1 -1
  64. package/dist/cli/setup.js.map +1 -1
  65. package/dist/cli/team.d.ts +18 -0
  66. package/dist/cli/team.d.ts.map +1 -1
  67. package/dist/cli/team.js +108 -16
  68. package/dist/cli/team.js.map +1 -1
  69. package/dist/config/generator.d.ts.map +1 -1
  70. package/dist/config/generator.js +8 -0
  71. package/dist/config/generator.js.map +1 -1
  72. package/dist/hooks/__tests__/deep-interview-contract.test.d.ts +2 -0
  73. package/dist/hooks/__tests__/deep-interview-contract.test.d.ts.map +1 -0
  74. package/dist/hooks/__tests__/deep-interview-contract.test.js +55 -0
  75. package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -0
  76. package/dist/hooks/__tests__/emulator.test.js +6 -0
  77. package/dist/hooks/__tests__/emulator.test.js.map +1 -1
  78. package/dist/hooks/__tests__/keyword-detector.test.js +44 -22
  79. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  80. package/dist/hooks/__tests__/notify-hook-session-scope.test.js +59 -0
  81. package/dist/hooks/__tests__/notify-hook-session-scope.test.js.map +1 -1
  82. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +88 -0
  83. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
  84. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +199 -0
  85. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
  86. package/dist/hooks/__tests__/notify-hook-visual-verdict.test.d.ts +11 -0
  87. package/dist/hooks/__tests__/notify-hook-visual-verdict.test.d.ts.map +1 -0
  88. package/dist/hooks/__tests__/notify-hook-visual-verdict.test.js +266 -0
  89. package/dist/hooks/__tests__/notify-hook-visual-verdict.test.js.map +1 -0
  90. package/dist/hooks/__tests__/openclaw-setup-contract.test.d.ts +2 -0
  91. package/dist/hooks/__tests__/openclaw-setup-contract.test.d.ts.map +1 -0
  92. package/dist/hooks/__tests__/openclaw-setup-contract.test.js +51 -0
  93. package/dist/hooks/__tests__/openclaw-setup-contract.test.js.map +1 -0
  94. package/dist/hooks/__tests__/pre-context-gate-skills.test.d.ts +2 -0
  95. package/dist/hooks/__tests__/pre-context-gate-skills.test.d.ts.map +1 -0
  96. package/dist/hooks/__tests__/pre-context-gate-skills.test.js +34 -0
  97. package/dist/hooks/__tests__/pre-context-gate-skills.test.js.map +1 -0
  98. package/dist/hooks/__tests__/tmux-hook-engine.test.js +36 -1
  99. package/dist/hooks/__tests__/tmux-hook-engine.test.js.map +1 -1
  100. package/dist/hooks/__tests__/visual-verdict-loop.test.d.ts +2 -0
  101. package/dist/hooks/__tests__/visual-verdict-loop.test.d.ts.map +1 -0
  102. package/dist/hooks/__tests__/visual-verdict-loop.test.js +35 -0
  103. package/dist/hooks/__tests__/visual-verdict-loop.test.js.map +1 -0
  104. package/dist/hooks/agents-overlay.d.ts.map +1 -1
  105. package/dist/hooks/agents-overlay.js +18 -16
  106. package/dist/hooks/agents-overlay.js.map +1 -1
  107. package/dist/hooks/codebase-map.d.ts.map +1 -1
  108. package/dist/hooks/codebase-map.js +6 -2
  109. package/dist/hooks/codebase-map.js.map +1 -1
  110. package/dist/hooks/emulator.d.ts.map +1 -1
  111. package/dist/hooks/emulator.js +2 -0
  112. package/dist/hooks/emulator.js.map +1 -1
  113. package/dist/hooks/extensibility/sdk.d.ts.map +1 -1
  114. package/dist/hooks/extensibility/sdk.js +2 -1
  115. package/dist/hooks/extensibility/sdk.js.map +1 -1
  116. package/dist/hooks/keyword-registry.d.ts.map +1 -1
  117. package/dist/hooks/keyword-registry.js +6 -0
  118. package/dist/hooks/keyword-registry.js.map +1 -1
  119. package/dist/hud/index.d.ts.map +1 -1
  120. package/dist/hud/index.js +2 -24
  121. package/dist/hud/index.js.map +1 -1
  122. package/dist/mcp/__tests__/team-server-cleanup.test.d.ts +2 -0
  123. package/dist/mcp/__tests__/team-server-cleanup.test.d.ts.map +1 -0
  124. package/dist/mcp/__tests__/team-server-cleanup.test.js +219 -0
  125. package/dist/mcp/__tests__/team-server-cleanup.test.js.map +1 -0
  126. package/dist/mcp/bootstrap.d.ts +1 -1
  127. package/dist/mcp/bootstrap.d.ts.map +1 -1
  128. package/dist/mcp/bootstrap.js +1 -0
  129. package/dist/mcp/bootstrap.js.map +1 -1
  130. package/dist/mcp/code-intel-server.d.ts.map +1 -1
  131. package/dist/mcp/code-intel-server.js +18 -8
  132. package/dist/mcp/code-intel-server.js.map +1 -1
  133. package/dist/mcp/memory-server.js +72 -11
  134. package/dist/mcp/memory-server.js.map +1 -1
  135. package/dist/mcp/state-paths.d.ts.map +1 -1
  136. package/dist/mcp/state-paths.js +4 -1
  137. package/dist/mcp/state-paths.js.map +1 -1
  138. package/dist/mcp/state-server.d.ts.map +1 -1
  139. package/dist/mcp/state-server.js +18 -5
  140. package/dist/mcp/state-server.js.map +1 -1
  141. package/dist/mcp/team-server.d.ts +24 -0
  142. package/dist/mcp/team-server.d.ts.map +1 -0
  143. package/dist/mcp/team-server.js +425 -0
  144. package/dist/mcp/team-server.js.map +1 -0
  145. package/dist/mcp/trace-server.d.ts.map +1 -1
  146. package/dist/mcp/trace-server.js +8 -3
  147. package/dist/mcp/trace-server.js.map +1 -1
  148. package/dist/notifications/__tests__/verbosity.test.js +35 -0
  149. package/dist/notifications/__tests__/verbosity.test.js.map +1 -1
  150. package/dist/notifications/config.d.ts.map +1 -1
  151. package/dist/notifications/config.js +12 -3
  152. package/dist/notifications/config.js.map +1 -1
  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/reply-listener.d.ts.map +1 -1
  157. package/dist/notifications/reply-listener.js +6 -2
  158. package/dist/notifications/reply-listener.js.map +1 -1
  159. package/dist/notifications/session-registry.d.ts.map +1 -1
  160. package/dist/notifications/session-registry.js +2 -2
  161. package/dist/notifications/session-registry.js.map +1 -1
  162. package/dist/notifications/tmux.d.ts.map +1 -1
  163. package/dist/notifications/tmux.js +13 -4
  164. package/dist/notifications/tmux.js.map +1 -1
  165. package/dist/notifications/types.d.ts +4 -0
  166. package/dist/notifications/types.d.ts.map +1 -1
  167. package/dist/openclaw/__tests__/index.test.js +40 -0
  168. package/dist/openclaw/__tests__/index.test.js.map +1 -1
  169. package/dist/openclaw/dispatcher.d.ts.map +1 -1
  170. package/dist/openclaw/dispatcher.js +5 -2
  171. package/dist/openclaw/dispatcher.js.map +1 -1
  172. package/dist/openclaw/index.d.ts.map +1 -1
  173. package/dist/openclaw/index.js +1 -0
  174. package/dist/openclaw/index.js.map +1 -1
  175. package/dist/openclaw/types.d.ts +2 -0
  176. package/dist/openclaw/types.d.ts.map +1 -1
  177. package/dist/ralph/__tests__/persistence.test.js +28 -1
  178. package/dist/ralph/__tests__/persistence.test.js.map +1 -1
  179. package/dist/ralph/persistence.d.ts +21 -0
  180. package/dist/ralph/persistence.d.ts.map +1 -1
  181. package/dist/ralph/persistence.js +85 -2
  182. package/dist/ralph/persistence.js.map +1 -1
  183. package/dist/state/paths.d.ts +3 -0
  184. package/dist/state/paths.d.ts.map +1 -0
  185. package/dist/state/paths.js +2 -0
  186. package/dist/state/paths.js.map +1 -0
  187. package/dist/team/__tests__/idle-nudge.test.d.ts +2 -0
  188. package/dist/team/__tests__/idle-nudge.test.d.ts.map +1 -0
  189. package/dist/team/__tests__/idle-nudge.test.js +225 -0
  190. package/dist/team/__tests__/idle-nudge.test.js.map +1 -0
  191. package/dist/team/__tests__/role-router.test.d.ts +2 -0
  192. package/dist/team/__tests__/role-router.test.d.ts.map +1 -0
  193. package/dist/team/__tests__/role-router.test.js +204 -0
  194. package/dist/team/__tests__/role-router.test.js.map +1 -0
  195. package/dist/team/__tests__/runtime-cli.test.d.ts +2 -0
  196. package/dist/team/__tests__/runtime-cli.test.d.ts.map +1 -0
  197. package/dist/team/__tests__/runtime-cli.test.js +72 -0
  198. package/dist/team/__tests__/runtime-cli.test.js.map +1 -0
  199. package/dist/team/__tests__/runtime.test.js +195 -9
  200. package/dist/team/__tests__/runtime.test.js.map +1 -1
  201. package/dist/team/__tests__/scaling.test.js +132 -2
  202. package/dist/team/__tests__/scaling.test.js.map +1 -1
  203. package/dist/team/__tests__/state-root.test.d.ts +2 -0
  204. package/dist/team/__tests__/state-root.test.d.ts.map +1 -0
  205. package/dist/team/__tests__/state-root.test.js +9 -0
  206. package/dist/team/__tests__/state-root.test.js.map +1 -0
  207. package/dist/team/__tests__/state.test.js +52 -17
  208. package/dist/team/__tests__/state.test.js.map +1 -1
  209. package/dist/team/__tests__/team-ops-contract.test.d.ts +2 -0
  210. package/dist/team/__tests__/team-ops-contract.test.d.ts.map +1 -0
  211. package/dist/team/__tests__/team-ops-contract.test.js +90 -0
  212. package/dist/team/__tests__/team-ops-contract.test.js.map +1 -0
  213. package/dist/team/__tests__/tmux-session.test.js +94 -7
  214. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  215. package/dist/team/__tests__/worker-bootstrap.test.js +59 -0
  216. package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
  217. package/dist/team/__tests__/worktree.test.js +81 -2
  218. package/dist/team/__tests__/worktree.test.js.map +1 -1
  219. package/dist/team/idle-nudge.d.ts +53 -0
  220. package/dist/team/idle-nudge.d.ts.map +1 -0
  221. package/dist/team/idle-nudge.js +140 -0
  222. package/dist/team/idle-nudge.js.map +1 -0
  223. package/dist/team/mcp-comm.d.ts +1 -1
  224. package/dist/team/mcp-comm.d.ts.map +1 -1
  225. package/dist/team/mcp-comm.js +6 -2
  226. package/dist/team/mcp-comm.js.map +1 -1
  227. package/dist/team/orchestrator.d.ts +1 -10
  228. package/dist/team/orchestrator.d.ts.map +1 -1
  229. package/dist/team/orchestrator.js +8 -0
  230. package/dist/team/orchestrator.js.map +1 -1
  231. package/dist/team/role-router.d.ts +32 -0
  232. package/dist/team/role-router.d.ts.map +1 -0
  233. package/dist/team/role-router.js +137 -0
  234. package/dist/team/role-router.js.map +1 -0
  235. package/dist/team/runtime-cli.d.ts +18 -0
  236. package/dist/team/runtime-cli.d.ts.map +1 -0
  237. package/dist/team/runtime-cli.js +244 -0
  238. package/dist/team/runtime-cli.js.map +1 -0
  239. package/dist/team/runtime.d.ts +6 -1
  240. package/dist/team/runtime.d.ts.map +1 -1
  241. package/dist/team/runtime.js +148 -60
  242. package/dist/team/runtime.js.map +1 -1
  243. package/dist/team/scaling.d.ts +1 -0
  244. package/dist/team/scaling.d.ts.map +1 -1
  245. package/dist/team/scaling.js +74 -32
  246. package/dist/team/scaling.js.map +1 -1
  247. package/dist/team/state/approvals.d.ts +25 -0
  248. package/dist/team/state/approvals.d.ts.map +1 -0
  249. package/dist/team/state/approvals.js +31 -0
  250. package/dist/team/state/approvals.js.map +1 -0
  251. package/dist/team/state/config.d.ts +2 -0
  252. package/dist/team/state/config.d.ts.map +1 -0
  253. package/dist/team/state/config.js +2 -0
  254. package/dist/team/state/config.js.map +1 -0
  255. package/dist/team/state/dispatch-lock.d.ts +3 -0
  256. package/dist/team/state/dispatch-lock.d.ts.map +1 -0
  257. package/dist/team/state/dispatch-lock.js +81 -0
  258. package/dist/team/state/dispatch-lock.js.map +1 -0
  259. package/dist/team/state/dispatch.d.ts +61 -0
  260. package/dist/team/state/dispatch.d.ts.map +1 -0
  261. package/dist/team/state/dispatch.js +158 -0
  262. package/dist/team/state/dispatch.js.map +1 -0
  263. package/dist/team/state/events.d.ts +2 -0
  264. package/dist/team/state/events.d.ts.map +1 -0
  265. package/dist/team/state/events.js +2 -0
  266. package/dist/team/state/events.js.map +1 -0
  267. package/dist/team/state/index.d.ts +11 -0
  268. package/dist/team/state/index.d.ts.map +1 -0
  269. package/dist/team/state/index.js +11 -0
  270. package/dist/team/state/index.js.map +1 -0
  271. package/dist/team/state/io.d.ts +2 -0
  272. package/dist/team/state/io.d.ts.map +1 -0
  273. package/dist/team/state/io.js +2 -0
  274. package/dist/team/state/io.js.map +1 -0
  275. package/dist/team/state/locks.d.ts +16 -0
  276. package/dist/team/state/locks.d.ts.map +1 -0
  277. package/dist/team/state/locks.js +201 -0
  278. package/dist/team/state/locks.js.map +1 -0
  279. package/dist/team/state/mailbox.d.ts +39 -0
  280. package/dist/team/state/mailbox.d.ts.map +1 -0
  281. package/dist/team/state/mailbox.js +58 -0
  282. package/dist/team/state/mailbox.js.map +1 -0
  283. package/dist/team/state/monitor.d.ts +96 -0
  284. package/dist/team/state/monitor.d.ts.map +1 -0
  285. package/dist/team/state/monitor.js +163 -0
  286. package/dist/team/state/monitor.js.map +1 -0
  287. package/dist/team/state/shutdown.d.ts +2 -0
  288. package/dist/team/state/shutdown.d.ts.map +1 -0
  289. package/dist/team/state/shutdown.js +2 -0
  290. package/dist/team/state/shutdown.js.map +1 -0
  291. package/dist/team/state/summary.d.ts +2 -0
  292. package/dist/team/state/summary.d.ts.map +1 -0
  293. package/dist/team/state/summary.js +2 -0
  294. package/dist/team/state/summary.js.map +1 -0
  295. package/dist/team/state/tasks.d.ts +49 -0
  296. package/dist/team/state/tasks.d.ts.map +1 -0
  297. package/dist/team/state/tasks.js +182 -0
  298. package/dist/team/state/tasks.js.map +1 -0
  299. package/dist/team/state/types.d.ts +281 -0
  300. package/dist/team/state/types.d.ts.map +1 -0
  301. package/dist/team/state/types.js +3 -0
  302. package/dist/team/state/types.js.map +1 -0
  303. package/dist/team/state/workers.d.ts +2 -0
  304. package/dist/team/state/workers.d.ts.map +1 -0
  305. package/dist/team/state/workers.js +2 -0
  306. package/dist/team/state/workers.js.map +1 -0
  307. package/dist/team/state-root.d.ts +5 -0
  308. package/dist/team/state-root.d.ts.map +1 -0
  309. package/dist/team/state-root.js +8 -0
  310. package/dist/team/state-root.js.map +1 -0
  311. package/dist/team/state.d.ts +6 -2
  312. package/dist/team/state.d.ts.map +1 -1
  313. package/dist/team/state.js +200 -881
  314. package/dist/team/state.js.map +1 -1
  315. package/dist/team/tmux-session.d.ts +42 -2
  316. package/dist/team/tmux-session.d.ts.map +1 -1
  317. package/dist/team/tmux-session.js +229 -74
  318. package/dist/team/tmux-session.js.map +1 -1
  319. package/dist/team/worker-bootstrap.d.ts +2 -0
  320. package/dist/team/worker-bootstrap.d.ts.map +1 -1
  321. package/dist/team/worker-bootstrap.js +47 -20
  322. package/dist/team/worker-bootstrap.js.map +1 -1
  323. package/dist/team/worktree.d.ts +5 -1
  324. package/dist/team/worktree.d.ts.map +1 -1
  325. package/dist/team/worktree.js +71 -17
  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/fixtures/ask-advisor-stub.js +12 -0
  351. package/scripts/notify-hook/log.js +5 -0
  352. package/scripts/notify-hook/team-dispatch.js +56 -1
  353. package/scripts/notify-hook/tmux-injection.js +45 -4
  354. package/scripts/notify-hook/visual-verdict.js +158 -0
  355. package/scripts/notify-hook.js +27 -0
  356. package/scripts/run-provider-advisor.js +179 -0
  357. package/scripts/tmux-hook-engine.js +24 -0
  358. package/skills/ask-claude/SKILL.md +61 -0
  359. package/skills/ask-gemini/SKILL.md +61 -0
  360. package/skills/autopilot/SKILL.md +34 -4
  361. package/skills/configure-notifications/SKILL.md +1 -1
  362. package/skills/configure-openclaw/SKILL.md +154 -157
  363. package/skills/deep-interview/SKILL.md +247 -0
  364. package/skills/doctor/SKILL.md +1 -1
  365. package/skills/help/SKILL.md +3 -3
  366. package/skills/ralph/SKILL.md +42 -11
  367. package/skills/ralplan/SKILL.md +17 -0
  368. package/skills/skill/SKILL.md +32 -32
  369. package/skills/team/SKILL.md +60 -0
  370. package/skills/visual-verdict/SKILL.md +76 -0
  371. package/skills/web-clone/SKILL.md +366 -0
  372. package/skills/worker/SKILL.md +5 -4
  373. package/templates/AGENTS.md +9 -0
  374. package/templates/catalog-manifest.json +39 -2
@@ -1,23 +1,29 @@
1
- import { appendFile, readFile, writeFile, mkdir, rm, rename, readdir, stat } from 'fs/promises';
1
+ import { appendFile, readFile, writeFile, mkdir, rm, rename, readdir } from 'fs/promises';
2
2
  import { join, dirname, resolve, sep } from 'path';
3
3
  import { existsSync } from 'fs';
4
4
  import { randomUUID } from 'crypto';
5
- import { performance } from 'perf_hooks';
6
5
  import { omxStateDir } from '../utils/paths.js';
6
+ import { computeTaskReadiness as computeTaskReadinessImpl, claimTask as claimTaskImpl, transitionTaskStatus as transitionTaskStatusImpl, releaseTaskClaim as releaseTaskClaimImpl, listTasks as listTasksImpl, } from './state/tasks.js';
7
+ import { sendDirectMessage as sendDirectMessageImpl, broadcastMessage as broadcastMessageImpl, markMessageDelivered as markMessageDeliveredImpl, markMessageNotified as markMessageNotifiedImpl, listMailboxMessages as listMailboxMessagesImpl, } from './state/mailbox.js';
8
+ import { enqueueDispatchRequest as enqueueDispatchRequestImpl, listDispatchRequests as listDispatchRequestsImpl, readDispatchRequest as readDispatchRequestImpl, transitionDispatchRequest as transitionDispatchRequestImpl, markDispatchRequestNotified as markDispatchRequestNotifiedImpl, markDispatchRequestDelivered as markDispatchRequestDeliveredImpl, normalizeDispatchRequest as normalizeDispatchRequestImpl, } from './state/dispatch.js';
9
+ import { resolveDispatchLockTimeoutMs as resolveDispatchLockTimeoutMsImpl, withDispatchLock as withDispatchLockImpl, } from './state/dispatch-lock.js';
10
+ import { writeTaskApproval as writeTaskApprovalImpl, readTaskApproval as readTaskApprovalImpl, } from './state/approvals.js';
11
+ import { getTeamSummary as getTeamSummaryImpl, readMonitorSnapshot as readMonitorSnapshotImpl, writeMonitorSnapshot as writeMonitorSnapshotImpl, readTeamPhase as readTeamPhaseImpl, writeTeamPhase as writeTeamPhaseImpl, } from './state/monitor.js';
12
+ import { withScalingLock as withScalingLockImpl, withTeamLock as withTeamLockImpl, withTaskClaimLock as withTaskClaimLockImpl, withMailboxLock as withMailboxLockImpl, } from './state/locks.js';
7
13
  import { TEAM_NAME_SAFE_PATTERN, WORKER_NAME_SAFE_PATTERN, TASK_ID_SAFE_PATTERN, TEAM_TASK_STATUSES, canTransitionTeamTaskStatus, isTerminalTeamTaskStatus, } from './contracts.js';
14
+ let renameForAtomicWrite = rename;
15
+ export function setWriteAtomicRenameForTests(fn) {
16
+ renameForAtomicWrite = fn;
17
+ }
18
+ export function resetWriteAtomicRenameForTests() {
19
+ renameForAtomicWrite = rename;
20
+ }
8
21
  export const DEFAULT_MAX_WORKERS = 20;
9
22
  export const ABSOLUTE_MAX_WORKERS = 20;
10
- const DEFAULT_CLAIM_LEASE_MS = 15 * 60 * 1000;
11
23
  const LOCK_STALE_MS = 5 * 60 * 1000;
12
24
  const DEFAULT_DISPATCH_ACK_TIMEOUT_MS = 800;
13
25
  const MIN_DISPATCH_ACK_TIMEOUT_MS = 100;
14
26
  const MAX_DISPATCH_ACK_TIMEOUT_MS = 10_000;
15
- const OMX_DISPATCH_LOCK_TIMEOUT_ENV = 'OMX_DISPATCH_LOCK_TIMEOUT_MS';
16
- const DEFAULT_DISPATCH_LOCK_TIMEOUT_MS = 15_000;
17
- const MIN_DISPATCH_LOCK_TIMEOUT_MS = 1_000;
18
- const MAX_DISPATCH_LOCK_TIMEOUT_MS = 120_000;
19
- const DISPATCH_LOCK_INITIAL_POLL_MS = 25;
20
- const DISPATCH_LOCK_MAX_POLL_MS = 500;
21
27
  function isTerminalTaskStatus(status) {
22
28
  return isTerminalTeamTaskStatus(status);
23
29
  }
@@ -41,9 +47,6 @@ function validateTaskId(taskId) {
41
47
  throw new Error(`Invalid task ID: "${taskId}". Must be a positive integer (digits only, max 20 digits).`);
42
48
  }
43
49
  }
44
- async function writeTaskClaimLockOwnerToken(ownerPath, ownerToken) {
45
- await writeFile(ownerPath, ownerToken, 'utf8');
46
- }
47
50
  function defaultLeader() {
48
51
  return {
49
52
  session_id: '',
@@ -320,12 +323,19 @@ export async function writeAtomic(filePath, data) {
320
323
  const tmpPath = `${filePath}.tmp.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`;
321
324
  await writeFile(tmpPath, data, 'utf8');
322
325
  try {
323
- await rename(tmpPath, filePath);
326
+ await renameForAtomicWrite(tmpPath, filePath);
324
327
  }
325
328
  catch (error) {
326
329
  const err = error;
327
330
  if (err.code === 'ENOENT' && existsSync(filePath)) {
328
- return;
331
+ try {
332
+ const existing = await readFile(filePath, 'utf8');
333
+ if (existing === data)
334
+ return;
335
+ }
336
+ catch {
337
+ // Preserve original ENOENT below if destination cannot be read.
338
+ }
329
339
  }
330
340
  throw error;
331
341
  }
@@ -636,15 +646,15 @@ export async function writeWorkerIdentity(teamName, workerName, identity, cwd) {
636
646
  }
637
647
  // Read worker heartbeat (returns null on missing/malformed)
638
648
  export async function readWorkerHeartbeat(teamName, workerName, cwd) {
649
+ const p = join(workerDir(teamName, workerName, cwd), 'heartbeat.json');
639
650
  try {
640
- const p = join(workerDir(teamName, workerName, cwd), 'heartbeat.json');
641
- if (!existsSync(p))
642
- return null;
643
651
  const raw = await readFile(p, 'utf8');
644
652
  const parsed = JSON.parse(raw);
645
653
  return isWorkerHeartbeat(parsed) ? parsed : null;
646
654
  }
647
- catch {
655
+ catch (error) {
656
+ if (error.code === 'ENOENT')
657
+ return null;
648
658
  return null;
649
659
  }
650
660
  }
@@ -656,11 +666,8 @@ export async function updateWorkerHeartbeat(teamName, workerName, heartbeat, cwd
656
666
  // Read worker status (returns {state:'unknown'} on missing/malformed)
657
667
  export async function readWorkerStatus(teamName, workerName, cwd) {
658
668
  const unknownStatus = { state: 'unknown', updated_at: '1970-01-01T00:00:00.000Z' };
669
+ const p = join(workerDir(teamName, workerName, cwd), 'status.json');
659
670
  try {
660
- const p = join(workerDir(teamName, workerName, cwd), 'status.json');
661
- if (!existsSync(p)) {
662
- return unknownStatus;
663
- }
664
671
  const raw = await readFile(p, 'utf8');
665
672
  const parsed = JSON.parse(raw);
666
673
  if (!isWorkerStatus(parsed)) {
@@ -668,7 +675,9 @@ export async function readWorkerStatus(teamName, workerName, cwd) {
668
675
  }
669
676
  return parsed;
670
677
  }
671
- catch {
678
+ catch (error) {
679
+ if (error.code === 'ENOENT')
680
+ return unknownStatus;
672
681
  return unknownStatus;
673
682
  }
674
683
  }
@@ -679,59 +688,7 @@ export async function writeWorkerStatus(teamName, workerName, status, cwd) {
679
688
  }
680
689
  // File-based scaling lock to prevent concurrent scale_up/scale_down operations
681
690
  export async function withScalingLock(teamName, cwd, fn) {
682
- const lockDir = join(teamDir(teamName, cwd), '.lock.scaling');
683
- const ownerPath = join(lockDir, 'owner');
684
- const ownerToken = `${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`;
685
- const deadline = Date.now() + 10_000;
686
- // Ensure parent directory exists before entering spin loop
687
- await mkdir(dirname(lockDir), { recursive: true });
688
- while (true) {
689
- try {
690
- await mkdir(lockDir);
691
- try {
692
- await writeFile(ownerPath, ownerToken, 'utf8');
693
- }
694
- catch (error) {
695
- await rm(lockDir, { recursive: true, force: true });
696
- throw error;
697
- }
698
- break;
699
- }
700
- catch (error) {
701
- const err = error;
702
- if (err.code !== 'EEXIST')
703
- throw error;
704
- try {
705
- const info = await stat(lockDir);
706
- const ageMs = Date.now() - info.mtimeMs;
707
- if (ageMs > LOCK_STALE_MS) {
708
- await rm(lockDir, { recursive: true, force: true });
709
- continue;
710
- }
711
- }
712
- catch {
713
- // best effort
714
- }
715
- if (Date.now() > deadline) {
716
- throw new Error(`Timed out acquiring scaling lock for team ${teamName}`);
717
- }
718
- await new Promise((resolve) => setTimeout(resolve, 50));
719
- }
720
- }
721
- try {
722
- return await fn();
723
- }
724
- finally {
725
- try {
726
- const currentOwner = await readFile(ownerPath, 'utf8');
727
- if (currentOwner.trim() === ownerToken) {
728
- await rm(lockDir, { recursive: true, force: true });
729
- }
730
- }
731
- catch {
732
- // best effort
733
- }
734
- }
691
+ return await withScalingLockImpl(teamName, cwd, LOCK_STALE_MS, { teamDir, taskClaimLockDir, mailboxLockDir }, fn);
735
692
  }
736
693
  // Write prompt to worker's inbox.md (atomic)
737
694
  export async function writeWorkerInbox(teamName, workerName, prompt, cwd) {
@@ -745,170 +702,13 @@ function taskFilePath(teamName, taskId, cwd) {
745
702
  return p;
746
703
  }
747
704
  async function withTeamLock(teamName, cwd, fn) {
748
- const lockDir = join(teamDir(teamName, cwd), '.lock.create-task');
749
- const ownerPath = join(lockDir, 'owner');
750
- const ownerToken = `${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`;
751
- const deadline = Date.now() + 5000;
752
- while (true) {
753
- try {
754
- await mkdir(lockDir);
755
- try {
756
- await writeFile(ownerPath, ownerToken, 'utf8');
757
- }
758
- catch (error) {
759
- await rm(lockDir, { recursive: true, force: true });
760
- throw error;
761
- }
762
- break;
763
- }
764
- catch (error) {
765
- const err = error;
766
- if (err.code !== 'EEXIST')
767
- throw error;
768
- // Best-effort stale lock recovery for crashed processes.
769
- try {
770
- const info = await stat(lockDir);
771
- const ageMs = Date.now() - info.mtimeMs;
772
- if (ageMs > LOCK_STALE_MS) {
773
- await rm(lockDir, { recursive: true, force: true });
774
- continue;
775
- }
776
- }
777
- catch {
778
- // best effort
779
- }
780
- if (Date.now() > deadline) {
781
- throw new Error(`Timed out acquiring team task lock for ${teamName}`);
782
- }
783
- await new Promise((resolve) => setTimeout(resolve, 25));
784
- }
785
- }
786
- try {
787
- return await fn();
788
- }
789
- finally {
790
- try {
791
- const currentOwner = await readFile(ownerPath, 'utf8');
792
- if (currentOwner.trim() === ownerToken) {
793
- await rm(lockDir, { recursive: true, force: true });
794
- }
795
- }
796
- catch {
797
- // best effort
798
- }
799
- }
705
+ return await withTeamLockImpl(teamName, cwd, LOCK_STALE_MS, { teamDir, taskClaimLockDir, mailboxLockDir }, fn);
800
706
  }
801
707
  async function withTaskClaimLock(teamName, taskId, cwd, fn) {
802
- const lockDir = taskClaimLockDir(teamName, taskId, cwd);
803
- const ownerPath = join(lockDir, 'owner');
804
- const ownerToken = `${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`;
805
- const staleLockMs = LOCK_STALE_MS;
806
- const deadline = Date.now() + 5000;
807
- while (true) {
808
- try {
809
- await mkdir(lockDir);
810
- break;
811
- }
812
- catch (error) {
813
- const err = error;
814
- if (err.code !== 'EEXIST')
815
- throw error;
816
- // Best-effort stale lock recovery for abandoned claim locks.
817
- try {
818
- const info = await stat(lockDir);
819
- const ageMs = Date.now() - info.mtimeMs;
820
- if (ageMs > staleLockMs) {
821
- await rm(lockDir, { recursive: true, force: true });
822
- continue;
823
- }
824
- }
825
- catch {
826
- // If stat/remove fails, fall through to conflict.
827
- }
828
- if (Date.now() > deadline)
829
- return { ok: false };
830
- await new Promise((resolve) => setTimeout(resolve, 25));
831
- }
832
- }
833
- try {
834
- try {
835
- await writeTaskClaimLockOwnerToken(ownerPath, ownerToken);
836
- }
837
- catch (error) {
838
- await rm(lockDir, { recursive: true, force: true });
839
- throw error;
840
- }
841
- return { ok: true, value: await fn() };
842
- }
843
- finally {
844
- try {
845
- const currentOwner = await readFile(ownerPath, 'utf8');
846
- if (currentOwner.trim() === ownerToken) {
847
- await rm(lockDir, { recursive: true, force: true });
848
- }
849
- }
850
- catch {
851
- // best effort
852
- }
853
- }
708
+ return await withTaskClaimLockImpl(teamName, taskId, cwd, LOCK_STALE_MS, { teamDir, taskClaimLockDir, mailboxLockDir }, fn);
854
709
  }
855
710
  async function withMailboxLock(teamName, workerName, cwd, fn) {
856
- const root = teamDir(teamName, cwd);
857
- if (!existsSync(root)) {
858
- throw new Error(`Team ${teamName} not found`);
859
- }
860
- const lockDir = mailboxLockDir(teamName, workerName, cwd);
861
- const ownerPath = join(lockDir, 'owner');
862
- const ownerToken = `${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`;
863
- const deadline = Date.now() + 5000;
864
- await mkdir(dirname(lockDir), { recursive: true });
865
- while (true) {
866
- try {
867
- await mkdir(lockDir, { recursive: false });
868
- try {
869
- await writeFile(ownerPath, ownerToken, 'utf8');
870
- }
871
- catch (error) {
872
- await rm(lockDir, { recursive: true, force: true });
873
- throw error;
874
- }
875
- break;
876
- }
877
- catch (error) {
878
- const err = error;
879
- if (err.code !== 'EEXIST')
880
- throw error;
881
- try {
882
- const info = await stat(lockDir);
883
- const ageMs = Date.now() - info.mtimeMs;
884
- if (ageMs > LOCK_STALE_MS) {
885
- await rm(lockDir, { recursive: true, force: true });
886
- continue;
887
- }
888
- }
889
- catch {
890
- // best effort
891
- }
892
- if (Date.now() > deadline) {
893
- throw new Error(`Timed out acquiring mailbox lock for ${teamName}/${workerName}`);
894
- }
895
- await new Promise((resolve) => setTimeout(resolve, 25));
896
- }
897
- }
898
- try {
899
- return await fn();
900
- }
901
- finally {
902
- try {
903
- const currentOwner = await readFile(ownerPath, 'utf8');
904
- if (currentOwner.trim() === ownerToken) {
905
- await rm(lockDir, { recursive: true, force: true });
906
- }
907
- }
908
- catch {
909
- // best effort
910
- }
911
- }
711
+ return await withMailboxLockImpl(teamName, workerName, cwd, LOCK_STALE_MS, { teamDir, taskClaimLockDir, mailboxLockDir }, fn);
912
712
  }
913
713
  // Create a task (auto-increment ID)
914
714
  export async function createTask(teamName, task, cwd) {
@@ -979,223 +779,57 @@ export async function updateTask(teamName, taskId, updates, cwd) {
979
779
  }
980
780
  // List all tasks sorted by numeric ID
981
781
  export async function listTasks(teamName, cwd) {
982
- const tasksRoot = join(teamDir(teamName, cwd), 'tasks');
983
- if (!existsSync(tasksRoot))
984
- return [];
985
- const files = await readdir(tasksRoot, { withFileTypes: true });
986
- const matched = files.flatMap((entry) => {
987
- if (!entry.isFile())
988
- return [];
989
- const m = /^task-(\d+)\.json$/.exec(entry.name);
990
- if (!m)
991
- return [];
992
- return [{ id: m[1], fileName: entry.name }];
782
+ return await listTasksImpl(teamName, cwd, {
783
+ teamDir,
784
+ isTeamTask,
785
+ normalizeTask,
993
786
  });
994
- const results = await Promise.all(matched.map(async ({ id, fileName }) => {
995
- try {
996
- const raw = await readFile(join(tasksRoot, fileName), 'utf8');
997
- const parsed = JSON.parse(raw);
998
- if (!isTeamTask(parsed))
999
- return null;
1000
- const normalized = normalizeTask(parsed);
1001
- // Ignore corrupt task files whose internal id mismatches filename.
1002
- if (normalized.id !== id)
1003
- return null;
1004
- return normalized;
1005
- }
1006
- catch {
1007
- return null;
1008
- }
1009
- }));
1010
- const tasks = [];
1011
- for (const task of results) {
1012
- if (task)
1013
- tasks.push(task);
1014
- }
1015
- tasks.sort((a, b) => Number(a.id) - Number(b.id));
1016
- return tasks;
1017
787
  }
1018
788
  export async function computeTaskReadiness(teamName, taskId, cwd) {
1019
- const task = await readTask(teamName, taskId, cwd);
1020
- if (!task)
1021
- return { ready: false, reason: 'blocked_dependency', dependencies: [] };
1022
- const deps = task.depends_on ?? task.blocked_by ?? [];
1023
- if (deps.length === 0)
1024
- return { ready: true };
1025
- const depTasks = await Promise.all(deps.map((d) => readTask(teamName, d, cwd)));
1026
- const incomplete = deps.filter((_, idx) => {
1027
- const t = depTasks[idx];
1028
- return !t || t.status !== 'completed';
1029
- });
1030
- if (incomplete.length > 0)
1031
- return { ready: false, reason: 'blocked_dependency', dependencies: incomplete };
1032
- return { ready: true };
789
+ return await computeTaskReadinessImpl(teamName, taskId, cwd, { readTask });
1033
790
  }
1034
791
  export async function claimTask(teamName, taskId, workerName, expectedVersion, cwd) {
1035
- // Validate that the claiming worker is registered in the team.
1036
- // Without this check, ghost worker IDs (non-existent workers) could claim
1037
- // tasks, breaking team state integrity and routing assumptions.
1038
- const cfg = await readTeamConfig(teamName, cwd);
1039
- if (!cfg || !cfg.workers.some((w) => w.name === workerName)) {
1040
- return { ok: false, error: 'worker_not_found' };
1041
- }
1042
- const existing = await readTask(teamName, taskId, cwd);
1043
- if (!existing)
1044
- return { ok: false, error: 'task_not_found' };
1045
- const readiness = await computeTaskReadiness(teamName, taskId, cwd);
1046
- if (!readiness.ready) {
1047
- return { ok: false, error: 'blocked_dependency', dependencies: readiness.dependencies };
1048
- }
1049
- const lock = await withTaskClaimLock(teamName, taskId, cwd, async () => {
1050
- const current = await readTask(teamName, taskId, cwd);
1051
- if (!current)
1052
- return { ok: false, error: 'task_not_found' };
1053
- const v = normalizeTask(current);
1054
- if (expectedVersion !== null && v.version !== expectedVersion) {
1055
- return { ok: false, error: 'claim_conflict' };
1056
- }
1057
- const readinessAfterLock = await computeTaskReadiness(teamName, taskId, cwd);
1058
- if (!readinessAfterLock.ready) {
1059
- return {
1060
- ok: false,
1061
- error: 'blocked_dependency',
1062
- dependencies: readinessAfterLock.dependencies,
1063
- };
1064
- }
1065
- if (isTerminalTaskStatus(v.status)) {
1066
- return { ok: false, error: 'already_terminal' };
1067
- }
1068
- if (v.status === 'in_progress') {
1069
- return { ok: false, error: 'claim_conflict' };
1070
- }
1071
- if (v.status === 'pending' || v.status === 'blocked') {
1072
- if (v.claim) {
1073
- return { ok: false, error: 'claim_conflict' };
1074
- }
1075
- if (v.owner && v.owner !== workerName) {
1076
- return { ok: false, error: 'claim_conflict' };
1077
- }
1078
- }
1079
- const claimToken = randomUUID();
1080
- const leasedUntil = new Date(Date.now() + DEFAULT_CLAIM_LEASE_MS).toISOString();
1081
- const updated = {
1082
- ...v,
1083
- status: 'in_progress',
1084
- owner: workerName,
1085
- claim: { owner: workerName, token: claimToken, leased_until: leasedUntil },
1086
- version: v.version + 1,
1087
- };
1088
- await writeAtomic(taskFilePath(teamName, taskId, cwd), JSON.stringify(updated, null, 2));
1089
- return { ok: true, task: updated, claimToken };
792
+ return await claimTaskImpl(taskId, workerName, expectedVersion, {
793
+ teamName,
794
+ cwd,
795
+ readTask,
796
+ readTeamConfig,
797
+ withTaskClaimLock,
798
+ normalizeTask,
799
+ isTerminalTaskStatus,
800
+ taskFilePath,
801
+ writeAtomic,
1090
802
  });
1091
- if (!lock.ok)
1092
- return { ok: false, error: 'claim_conflict' };
1093
- return lock.value;
1094
803
  }
1095
804
  export async function transitionTaskStatus(teamName, taskId, from, to, claimToken, cwd) {
1096
- if (!canTransitionTaskStatus(from, to)) {
1097
- return { ok: false, error: 'invalid_transition' };
1098
- }
1099
- const lock = await withTaskClaimLock(teamName, taskId, cwd, async () => {
1100
- const current = await readTask(teamName, taskId, cwd);
1101
- if (!current)
1102
- return { ok: false, error: 'task_not_found' };
1103
- const v = normalizeTask(current);
1104
- if (isTerminalTaskStatus(v.status)) {
1105
- return { ok: false, error: 'already_terminal' };
1106
- }
1107
- if (!canTransitionTaskStatus(v.status, to)) {
1108
- return { ok: false, error: 'invalid_transition' };
1109
- }
1110
- if (v.status !== from)
1111
- return { ok: false, error: 'invalid_transition' };
1112
- if (!v.owner || !v.claim || v.claim.owner !== v.owner || v.claim.token !== claimToken) {
1113
- return { ok: false, error: 'claim_conflict' };
1114
- }
1115
- if (new Date(v.claim.leased_until) <= new Date())
1116
- return { ok: false, error: 'lease_expired' };
1117
- const completedAt = new Date().toISOString();
1118
- const updated = {
1119
- ...v,
1120
- status: to,
1121
- completed_at: completedAt,
1122
- claim: undefined,
1123
- version: v.version + 1,
1124
- };
1125
- await writeAtomic(taskFilePath(teamName, taskId, cwd), JSON.stringify(updated, null, 2));
1126
- if (to === 'completed') {
1127
- await appendTeamEvent(teamName, {
1128
- type: 'task_completed',
1129
- worker: updated.owner || 'unknown',
1130
- task_id: updated.id,
1131
- message_id: null,
1132
- reason: undefined,
1133
- }, cwd);
1134
- }
1135
- else if (to === 'failed') {
1136
- await appendTeamEvent(teamName, {
1137
- type: 'task_failed',
1138
- worker: updated.owner || 'unknown',
1139
- task_id: updated.id,
1140
- message_id: null,
1141
- reason: updated.error || 'task_failed',
1142
- }, cwd);
1143
- }
1144
- return { ok: true, task: updated };
805
+ return await transitionTaskStatusImpl(taskId, from, to, claimToken, {
806
+ teamName,
807
+ cwd,
808
+ readTask,
809
+ readTeamConfig,
810
+ withTaskClaimLock,
811
+ normalizeTask,
812
+ isTerminalTaskStatus,
813
+ canTransitionTaskStatus,
814
+ taskFilePath,
815
+ writeAtomic,
816
+ appendTeamEvent,
817
+ readMonitorSnapshot,
818
+ writeMonitorSnapshot,
1145
819
  });
1146
- if (!lock.ok)
1147
- return { ok: false, error: 'claim_conflict' };
1148
- // If a task_completed event was emitted via this claim-safe path, record the task ID in
1149
- // the monitor snapshot so that emitMonitorDerivedEvents does not emit a duplicate event
1150
- // on the next monitorTeam poll (issue #161).
1151
- if (to === 'completed') {
1152
- const existingSnap = await readMonitorSnapshot(teamName, cwd);
1153
- const updatedSnap = existingSnap
1154
- ? { ...existingSnap, completedEventTaskIds: { ...(existingSnap.completedEventTaskIds ?? {}), [taskId]: true } }
1155
- : {
1156
- taskStatusById: {},
1157
- workerAliveByName: {},
1158
- workerStateByName: {},
1159
- workerTurnCountByName: {},
1160
- workerTaskIdByName: {},
1161
- mailboxNotifiedByMessageId: {},
1162
- completedEventTaskIds: { [taskId]: true },
1163
- };
1164
- await writeMonitorSnapshot(teamName, updatedSnap, cwd);
1165
- }
1166
- return lock.value;
1167
820
  }
1168
821
  export async function releaseTaskClaim(teamName, taskId, claimToken, workerName, cwd) {
1169
- const lock = await withTaskClaimLock(teamName, taskId, cwd, async () => {
1170
- const current = await readTask(teamName, taskId, cwd);
1171
- if (!current)
1172
- return { ok: false, error: 'task_not_found' };
1173
- const v = normalizeTask(current);
1174
- if (v.status === 'pending' && !v.claim && !v.owner) {
1175
- return { ok: true, task: v };
1176
- }
1177
- if (v.status === 'completed' || v.status === 'failed') {
1178
- return { ok: false, error: 'already_terminal' };
1179
- }
1180
- const leaseActive = Boolean(v.claim && new Date(v.claim.leased_until) > new Date());
1181
- const tokenMatches = Boolean(v.claim && v.claim.token === claimToken && leaseActive);
1182
- const ownerMatches = v.status === 'in_progress' && v.owner === workerName;
1183
- if (!tokenMatches && !ownerMatches) {
1184
- return { ok: false, error: 'claim_conflict' };
1185
- }
1186
- const updated = {
1187
- ...v,
1188
- status: 'pending',
1189
- owner: undefined,
1190
- claim: undefined,
1191
- version: v.version + 1,
1192
- };
1193
- await writeAtomic(taskFilePath(teamName, taskId, cwd), JSON.stringify(updated, null, 2));
1194
- return { ok: true, task: updated };
822
+ return await releaseTaskClaimImpl(taskId, claimToken, workerName, {
823
+ teamName,
824
+ cwd,
825
+ readTask,
826
+ readTeamConfig,
827
+ withTaskClaimLock,
828
+ normalizeTask,
829
+ isTerminalTaskStatus,
830
+ taskFilePath,
831
+ writeAtomic,
1195
832
  });
1196
- if (!lock.ok)
1197
- return { ok: false, error: 'claim_conflict' };
1198
- return lock.value;
1199
833
  }
1200
834
  export async function appendTeamEvent(teamName, event, cwd) {
1201
835
  const full = {
@@ -1231,44 +865,6 @@ async function writeMailbox(teamName, mailbox, cwd) {
1231
865
  const p = mailboxPath(teamName, mailbox.worker, cwd);
1232
866
  await writeAtomic(p, JSON.stringify(mailbox, null, 2));
1233
867
  }
1234
- function isDispatchKind(value) {
1235
- return value === 'inbox' || value === 'mailbox' || value === 'nudge';
1236
- }
1237
- function isDispatchStatus(value) {
1238
- return value === 'pending' || value === 'notified' || value === 'delivered' || value === 'failed';
1239
- }
1240
- function normalizeDispatchRequest(teamName, raw, nowIso = new Date().toISOString()) {
1241
- if (!isDispatchKind(raw.kind))
1242
- return null;
1243
- if (typeof raw.to_worker !== 'string' || raw.to_worker.trim() === '')
1244
- return null;
1245
- if (typeof raw.trigger_message !== 'string' || raw.trigger_message.trim() === '')
1246
- return null;
1247
- const status = isDispatchStatus(raw.status) ? raw.status : 'pending';
1248
- return {
1249
- request_id: typeof raw.request_id === 'string' && raw.request_id.trim() !== '' ? raw.request_id : randomUUID(),
1250
- kind: raw.kind,
1251
- team_name: teamName,
1252
- to_worker: raw.to_worker,
1253
- worker_index: typeof raw.worker_index === 'number' ? raw.worker_index : undefined,
1254
- pane_id: typeof raw.pane_id === 'string' && raw.pane_id !== '' ? raw.pane_id : undefined,
1255
- trigger_message: raw.trigger_message,
1256
- message_id: typeof raw.message_id === 'string' && raw.message_id !== '' ? raw.message_id : undefined,
1257
- inbox_correlation_key: typeof raw.inbox_correlation_key === 'string' && raw.inbox_correlation_key !== '' ? raw.inbox_correlation_key : undefined,
1258
- transport_preference: raw.transport_preference === 'transport_direct' || raw.transport_preference === 'prompt_stdin'
1259
- ? raw.transport_preference
1260
- : 'hook_preferred_with_fallback',
1261
- fallback_allowed: raw.fallback_allowed !== false,
1262
- status,
1263
- attempt_count: Number.isFinite(raw.attempt_count) ? Math.max(0, Math.floor(raw.attempt_count)) : 0,
1264
- created_at: typeof raw.created_at === 'string' && raw.created_at !== '' ? raw.created_at : nowIso,
1265
- updated_at: typeof raw.updated_at === 'string' && raw.updated_at !== '' ? raw.updated_at : nowIso,
1266
- notified_at: typeof raw.notified_at === 'string' && raw.notified_at !== '' ? raw.notified_at : undefined,
1267
- delivered_at: typeof raw.delivered_at === 'string' && raw.delivered_at !== '' ? raw.delivered_at : undefined,
1268
- failed_at: typeof raw.failed_at === 'string' && raw.failed_at !== '' ? raw.failed_at : undefined,
1269
- last_reason: typeof raw.last_reason === 'string' && raw.last_reason !== '' ? raw.last_reason : undefined,
1270
- };
1271
- }
1272
868
  async function readDispatchRequests(teamName, cwd) {
1273
869
  const path = dispatchRequestsPath(teamName, cwd);
1274
870
  try {
@@ -1280,7 +876,7 @@ async function readDispatchRequests(teamName, cwd) {
1280
876
  return [];
1281
877
  const nowIso = new Date().toISOString();
1282
878
  return parsed
1283
- .map((entry) => normalizeDispatchRequest(teamName, (entry ?? {}), nowIso))
879
+ .map((entry) => normalizeDispatchRequestImpl(teamName, (entry ?? {}), nowIso))
1284
880
  .filter((entry) => entry !== null);
1285
881
  }
1286
882
  catch {
@@ -1291,383 +887,158 @@ async function writeDispatchRequests(teamName, requests, cwd) {
1291
887
  await writeAtomic(dispatchRequestsPath(teamName, cwd), JSON.stringify(requests, null, 2));
1292
888
  }
1293
889
  export function resolveDispatchLockTimeoutMs(env = process.env) {
1294
- const raw = env[OMX_DISPATCH_LOCK_TIMEOUT_ENV];
1295
- if (raw === undefined || raw === '')
1296
- return DEFAULT_DISPATCH_LOCK_TIMEOUT_MS;
1297
- const parsed = Number(raw);
1298
- if (!Number.isFinite(parsed))
1299
- return DEFAULT_DISPATCH_LOCK_TIMEOUT_MS;
1300
- return Math.max(MIN_DISPATCH_LOCK_TIMEOUT_MS, Math.min(MAX_DISPATCH_LOCK_TIMEOUT_MS, Math.floor(parsed)));
890
+ return resolveDispatchLockTimeoutMsImpl(env);
1301
891
  }
1302
892
  async function withDispatchLock(teamName, cwd, fn) {
1303
- const root = teamDir(teamName, cwd);
1304
- if (!existsSync(root))
1305
- throw new Error(`Team ${teamName} not found`);
1306
- const lockDir = dispatchLockDir(teamName, cwd);
1307
- const ownerPath = join(lockDir, 'owner');
1308
- const ownerToken = `${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`;
1309
- const timeoutMs = resolveDispatchLockTimeoutMs(process.env);
1310
- const deadline = Date.now() + timeoutMs;
1311
- let pollMs = DISPATCH_LOCK_INITIAL_POLL_MS;
1312
- await mkdir(dirname(lockDir), { recursive: true });
1313
- while (true) {
1314
- try {
1315
- await mkdir(lockDir, { recursive: false });
1316
- try {
1317
- await writeFile(ownerPath, ownerToken, 'utf8');
1318
- }
1319
- catch (error) {
1320
- await rm(lockDir, { recursive: true, force: true });
1321
- throw error;
1322
- }
1323
- break;
1324
- }
1325
- catch (error) {
1326
- const err = error;
1327
- if (err.code !== 'EEXIST')
1328
- throw error;
1329
- try {
1330
- const info = await stat(lockDir);
1331
- if (Date.now() - info.mtimeMs > LOCK_STALE_MS) {
1332
- await rm(lockDir, { recursive: true, force: true });
1333
- continue;
1334
- }
1335
- }
1336
- catch {
1337
- // best effort
1338
- }
1339
- if (Date.now() > deadline) {
1340
- throw new Error(`Timed out acquiring dispatch lock for ${teamName} after ${timeoutMs}ms. ` +
1341
- `Set ${OMX_DISPATCH_LOCK_TIMEOUT_ENV} to increase (current: ${timeoutMs}ms, max: ${MAX_DISPATCH_LOCK_TIMEOUT_MS}ms).`);
1342
- }
1343
- // Exponential backoff with jitter to reduce thundering herd
1344
- const jitter = 0.5 + Math.random() * 0.5;
1345
- await new Promise((resolve) => setTimeout(resolve, Math.floor(pollMs * jitter)));
1346
- pollMs = Math.min(pollMs * 2, DISPATCH_LOCK_MAX_POLL_MS);
1347
- }
1348
- }
1349
- try {
1350
- return await fn();
1351
- }
1352
- finally {
1353
- try {
1354
- const currentOwner = await readFile(ownerPath, 'utf8');
1355
- if (currentOwner.trim() === ownerToken) {
1356
- await rm(lockDir, { recursive: true, force: true });
1357
- }
1358
- }
1359
- catch {
1360
- // best effort
1361
- }
1362
- }
1363
- }
1364
- function equivalentPendingDispatch(existing, input) {
1365
- if (existing.status !== 'pending')
1366
- return false;
1367
- if (existing.kind !== input.kind)
1368
- return false;
1369
- if (existing.to_worker !== input.to_worker)
1370
- return false;
1371
- if (input.kind === 'mailbox') {
1372
- return Boolean(input.message_id) && existing.message_id === input.message_id;
1373
- }
1374
- if (input.kind === 'inbox' && input.inbox_correlation_key) {
1375
- return existing.inbox_correlation_key === input.inbox_correlation_key;
1376
- }
1377
- return existing.trigger_message === input.trigger_message;
893
+ return await withDispatchLockImpl(teamName, cwd, teamDir, dispatchLockDir, fn);
1378
894
  }
1379
895
  export async function enqueueDispatchRequest(teamName, requestInput, cwd) {
1380
- if (!isDispatchKind(requestInput.kind))
1381
- throw new Error(`Invalid dispatch request kind: ${String(requestInput.kind)}`);
1382
- if (requestInput.kind === 'mailbox' && (!requestInput.message_id || requestInput.message_id.trim() === '')) {
1383
- throw new Error('mailbox dispatch requests require message_id');
1384
- }
1385
- validateWorkerName(requestInput.to_worker);
1386
- return await withDispatchLock(teamName, cwd, async () => {
1387
- const requests = await readDispatchRequests(teamName, cwd);
1388
- const existing = requests.find((req) => equivalentPendingDispatch(req, requestInput));
1389
- if (existing)
1390
- return { request: existing, deduped: true };
1391
- const nowIso = new Date().toISOString();
1392
- const request = normalizeDispatchRequest(teamName, {
1393
- request_id: randomUUID(),
1394
- ...requestInput,
1395
- status: 'pending',
1396
- attempt_count: 0,
1397
- created_at: nowIso,
1398
- updated_at: nowIso,
1399
- }, nowIso);
1400
- if (!request)
1401
- throw new Error('failed_to_normalize_dispatch_request');
1402
- requests.push(request);
1403
- await writeDispatchRequests(teamName, requests, cwd);
1404
- return { request, deduped: false };
896
+ return await enqueueDispatchRequestImpl(requestInput, {
897
+ teamName,
898
+ cwd,
899
+ validateWorkerName,
900
+ withDispatchLock,
901
+ readDispatchRequests,
902
+ writeDispatchRequests,
1405
903
  });
1406
904
  }
1407
905
  export async function listDispatchRequests(teamName, cwd, opts = {}) {
1408
- const requests = await readDispatchRequests(teamName, cwd);
1409
- let filtered = requests;
1410
- if (opts.status)
1411
- filtered = filtered.filter((req) => req.status === opts.status);
1412
- if (opts.kind)
1413
- filtered = filtered.filter((req) => req.kind === opts.kind);
1414
- if (opts.to_worker)
1415
- filtered = filtered.filter((req) => req.to_worker === opts.to_worker);
1416
- if (typeof opts.limit === 'number' && opts.limit > 0)
1417
- filtered = filtered.slice(0, opts.limit);
1418
- return filtered;
906
+ return await listDispatchRequestsImpl(opts, {
907
+ teamName,
908
+ cwd,
909
+ validateWorkerName,
910
+ withDispatchLock,
911
+ readDispatchRequests,
912
+ writeDispatchRequests,
913
+ });
1419
914
  }
1420
915
  export async function readDispatchRequest(teamName, requestId, cwd) {
1421
- const requests = await readDispatchRequests(teamName, cwd);
1422
- return requests.find((req) => req.request_id === requestId) ?? null;
1423
- }
1424
- function canTransitionDispatchStatus(from, to) {
1425
- if (from === to)
1426
- return true;
1427
- if (from === 'pending' && (to === 'notified' || to === 'failed'))
1428
- return true;
1429
- if (from === 'notified' && (to === 'delivered' || to === 'failed'))
1430
- return true;
1431
- return false;
916
+ return await readDispatchRequestImpl(requestId, {
917
+ teamName,
918
+ cwd,
919
+ validateWorkerName,
920
+ withDispatchLock,
921
+ readDispatchRequests,
922
+ writeDispatchRequests,
923
+ });
1432
924
  }
1433
925
  export async function transitionDispatchRequest(teamName, requestId, from, to, patch = {}, cwd) {
1434
- return await withDispatchLock(teamName, cwd, async () => {
1435
- const requests = await readDispatchRequests(teamName, cwd);
1436
- const index = requests.findIndex((req) => req.request_id === requestId);
1437
- if (index < 0)
1438
- return null;
1439
- const existing = requests[index];
1440
- if (existing.status !== from && existing.status !== to)
1441
- return null;
1442
- if (!canTransitionDispatchStatus(existing.status, to))
1443
- return null;
1444
- const nowIso = new Date().toISOString();
1445
- const nextAttemptCount = Math.max(existing.attempt_count, Number.isFinite(patch.attempt_count)
1446
- ? Math.floor(patch.attempt_count)
1447
- : (existing.status === to ? existing.attempt_count : existing.attempt_count + 1));
1448
- const next = {
1449
- ...existing,
1450
- ...patch,
1451
- status: to,
1452
- attempt_count: Math.max(0, nextAttemptCount),
1453
- updated_at: nowIso,
1454
- };
1455
- if (to === 'notified')
1456
- next.notified_at = patch.notified_at ?? nowIso;
1457
- if (to === 'delivered')
1458
- next.delivered_at = patch.delivered_at ?? nowIso;
1459
- if (to === 'failed')
1460
- next.failed_at = patch.failed_at ?? nowIso;
1461
- requests[index] = next;
1462
- await writeDispatchRequests(teamName, requests, cwd);
1463
- return next;
926
+ return await transitionDispatchRequestImpl(requestId, from, to, patch, {
927
+ teamName,
928
+ cwd,
929
+ validateWorkerName,
930
+ withDispatchLock,
931
+ readDispatchRequests,
932
+ writeDispatchRequests,
1464
933
  });
1465
934
  }
1466
935
  export async function markDispatchRequestNotified(teamName, requestId, patch = {}, cwd) {
1467
- const current = await readDispatchRequest(teamName, requestId, cwd);
1468
- if (!current)
1469
- return null;
1470
- if (current.status === 'notified' || current.status === 'delivered')
1471
- return current;
1472
- return await transitionDispatchRequest(teamName, requestId, current.status, 'notified', patch, cwd);
936
+ return await markDispatchRequestNotifiedImpl(requestId, patch, {
937
+ teamName,
938
+ cwd,
939
+ validateWorkerName,
940
+ withDispatchLock,
941
+ readDispatchRequests,
942
+ writeDispatchRequests,
943
+ });
1473
944
  }
1474
945
  export async function markDispatchRequestDelivered(teamName, requestId, patch = {}, cwd) {
1475
- const current = await readDispatchRequest(teamName, requestId, cwd);
1476
- if (!current)
1477
- return null;
1478
- if (current.status === 'delivered')
1479
- return current;
1480
- return await transitionDispatchRequest(teamName, requestId, current.status, 'delivered', patch, cwd);
946
+ return await markDispatchRequestDeliveredImpl(requestId, patch, {
947
+ teamName,
948
+ cwd,
949
+ validateWorkerName,
950
+ withDispatchLock,
951
+ readDispatchRequests,
952
+ writeDispatchRequests,
953
+ });
1481
954
  }
1482
955
  export async function sendDirectMessage(teamName, fromWorker, toWorker, body, cwd) {
1483
- const msg = {
1484
- message_id: randomUUID(),
1485
- from_worker: fromWorker,
1486
- to_worker: toWorker,
1487
- body,
1488
- created_at: new Date().toISOString(),
1489
- };
1490
- await withMailboxLock(teamName, toWorker, cwd, async () => {
1491
- const mailbox = await readMailbox(teamName, toWorker, cwd);
1492
- mailbox.messages.push(msg);
1493
- await writeMailbox(teamName, mailbox, cwd);
956
+ return await sendDirectMessageImpl(fromWorker, toWorker, body, {
957
+ teamName,
958
+ cwd,
959
+ withMailboxLock,
960
+ readMailbox,
961
+ writeMailbox,
962
+ appendTeamEvent,
963
+ readTeamConfig,
1494
964
  });
1495
- await appendTeamEvent(teamName, { type: 'message_received', worker: toWorker, task_id: undefined, message_id: msg.message_id, reason: undefined }, cwd);
1496
- return msg;
1497
965
  }
1498
966
  export async function broadcastMessage(teamName, fromWorker, body, cwd) {
1499
- const cfg = await readTeamConfig(teamName, cwd);
1500
- if (!cfg)
1501
- throw new Error(`Team ${teamName} not found`);
1502
- const targets = cfg.workers.map((w) => w.name);
1503
- const delivered = [];
1504
- for (const to of targets) {
1505
- if (to === fromWorker)
1506
- continue;
1507
- delivered.push(await sendDirectMessage(teamName, fromWorker, to, body, cwd));
1508
- }
1509
- return delivered;
967
+ return await broadcastMessageImpl(fromWorker, body, {
968
+ teamName,
969
+ cwd,
970
+ withMailboxLock,
971
+ readMailbox,
972
+ writeMailbox,
973
+ appendTeamEvent,
974
+ readTeamConfig,
975
+ });
1510
976
  }
1511
977
  export async function markMessageDelivered(teamName, workerName, messageId, cwd) {
1512
- return withMailboxLock(teamName, workerName, cwd, async () => {
1513
- const mailbox = await readMailbox(teamName, workerName, cwd);
1514
- const msg = mailbox.messages.find((m) => m.message_id === messageId);
1515
- if (!msg)
1516
- return false;
1517
- if (!msg.delivered_at) {
1518
- msg.delivered_at = new Date().toISOString();
1519
- await writeMailbox(teamName, mailbox, cwd);
1520
- }
1521
- return true;
978
+ return await markMessageDeliveredImpl(workerName, messageId, {
979
+ teamName,
980
+ cwd,
981
+ withMailboxLock,
982
+ readMailbox,
983
+ writeMailbox,
984
+ appendTeamEvent,
985
+ readTeamConfig,
1522
986
  });
1523
987
  }
1524
988
  export async function markMessageNotified(teamName, workerName, messageId, cwd) {
1525
- return withMailboxLock(teamName, workerName, cwd, async () => {
1526
- const mailbox = await readMailbox(teamName, workerName, cwd);
1527
- const msg = mailbox.messages.find((m) => m.message_id === messageId);
1528
- if (!msg)
1529
- return false;
1530
- msg.notified_at = new Date().toISOString();
1531
- await writeMailbox(teamName, mailbox, cwd);
1532
- return true;
989
+ return await markMessageNotifiedImpl(workerName, messageId, {
990
+ teamName,
991
+ cwd,
992
+ withMailboxLock,
993
+ readMailbox,
994
+ writeMailbox,
995
+ appendTeamEvent,
996
+ readTeamConfig,
1533
997
  });
1534
998
  }
1535
999
  export async function listMailboxMessages(teamName, workerName, cwd) {
1536
- const mailbox = await readMailbox(teamName, workerName, cwd);
1537
- return mailbox.messages;
1000
+ return await listMailboxMessagesImpl(workerName, {
1001
+ teamName,
1002
+ cwd,
1003
+ withMailboxLock,
1004
+ readMailbox,
1005
+ writeMailbox,
1006
+ appendTeamEvent,
1007
+ readTeamConfig,
1008
+ });
1538
1009
  }
1539
1010
  export async function writeTaskApproval(teamName, approval, cwd) {
1540
- const p = approvalPath(teamName, approval.task_id, cwd);
1541
- await writeAtomic(p, JSON.stringify(approval, null, 2));
1542
- await appendTeamEvent(teamName, {
1543
- type: 'approval_decision',
1544
- worker: approval.reviewer,
1545
- task_id: approval.task_id,
1546
- message_id: null,
1547
- reason: `${approval.status}:${approval.decision_reason}`,
1548
- }, cwd);
1011
+ await writeTaskApprovalImpl(approval, {
1012
+ teamName,
1013
+ cwd,
1014
+ approvalPath,
1015
+ writeAtomic,
1016
+ appendTeamEvent,
1017
+ });
1549
1018
  }
1550
1019
  export async function readTaskApproval(teamName, taskId, cwd) {
1551
- const p = approvalPath(teamName, taskId, cwd);
1552
- if (!existsSync(p))
1553
- return null;
1554
- try {
1555
- const raw = await readFile(p, 'utf-8');
1556
- const parsed = JSON.parse(raw);
1557
- if (parsed.task_id !== taskId)
1558
- return null;
1559
- if (!['pending', 'approved', 'rejected'].includes(parsed.status))
1560
- return null;
1561
- return parsed;
1562
- }
1563
- catch {
1564
- return null;
1565
- }
1020
+ return await readTaskApprovalImpl(taskId, {
1021
+ teamName,
1022
+ cwd,
1023
+ approvalPath,
1024
+ writeAtomic,
1025
+ appendTeamEvent,
1026
+ });
1566
1027
  }
1567
1028
  // Get team summary with aggregation and non-reporting worker detection
1568
1029
  export async function getTeamSummary(teamName, cwd) {
1569
- const summaryStartMs = performance.now();
1570
- const cfg = await readTeamConfig(teamName, cwd);
1571
- if (!cfg)
1572
- return null;
1573
- const tasksStartMs = performance.now();
1574
- const tasks = await listTasks(teamName, cwd);
1575
- const tasksLoadedMs = performance.now() - tasksStartMs;
1576
- const taskById = new Map(tasks.map((task) => [task.id, task]));
1577
- const previousSnapshot = await readSummarySnapshot(teamName, cwd);
1578
- const counts = {
1579
- total: tasks.length,
1580
- pending: 0,
1581
- blocked: 0,
1582
- in_progress: 0,
1583
- completed: 0,
1584
- failed: 0,
1585
- };
1586
- for (const t of tasks) {
1587
- if (t.status === 'pending')
1588
- counts.pending++;
1589
- else if (t.status === 'blocked')
1590
- counts.blocked++;
1591
- else if (t.status === 'in_progress')
1592
- counts.in_progress++;
1593
- else if (t.status === 'completed')
1594
- counts.completed++;
1595
- else if (t.status === 'failed')
1596
- counts.failed++;
1597
- }
1598
- const workers = cfg.workers || [];
1599
- const workerSummaries = [];
1600
- const nonReportingWorkers = [];
1601
- const nextSnapshot = {
1602
- workerTurnCountByName: {},
1603
- workerTaskByName: {},
1604
- };
1605
- const workerPollStartMs = performance.now();
1606
- const workerSignals = await Promise.all(workers.map(async (worker) => {
1607
- const [hb, status] = await Promise.all([
1608
- readWorkerHeartbeat(teamName, worker.name, cwd),
1609
- readWorkerStatus(teamName, worker.name, cwd),
1610
- ]);
1611
- return { worker, hb, status };
1612
- }));
1613
- const workersPolledMs = performance.now() - workerPollStartMs;
1614
- for (const { worker: w, hb, status } of workerSignals) {
1615
- const alive = hb?.alive ?? false;
1616
- const lastTurnAt = hb?.last_turn_at ?? null;
1617
- const currentTaskId = status.current_task_id ?? '';
1618
- const prevTaskId = previousSnapshot?.workerTaskByName[w.name] ?? '';
1619
- const prevTurnCount = previousSnapshot?.workerTurnCountByName[w.name] ?? 0;
1620
- const currentTask = currentTaskId ? taskById.get(currentTaskId) ?? null : null;
1621
- const turnsWithoutProgress = hb &&
1622
- status.state === 'working' &&
1623
- currentTask &&
1624
- (currentTask.status === 'pending' || currentTask.status === 'in_progress') &&
1625
- currentTaskId === prevTaskId
1626
- ? Math.max(0, hb.turn_count - prevTurnCount)
1627
- : 0;
1628
- if (alive && status.state === 'working' && turnsWithoutProgress > 5) {
1629
- nonReportingWorkers.push(w.name);
1630
- }
1631
- workerSummaries.push({ name: w.name, alive, lastTurnAt, turnsWithoutProgress });
1632
- nextSnapshot.workerTurnCountByName[w.name] = hb?.turn_count ?? 0;
1633
- nextSnapshot.workerTaskByName[w.name] = currentTaskId;
1634
- }
1635
- await writeSummarySnapshot(teamName, nextSnapshot, cwd);
1636
- return {
1637
- teamName: cfg.name,
1638
- workerCount: cfg.worker_count,
1639
- tasks: counts,
1640
- workers: workerSummaries,
1641
- nonReportingWorkers,
1642
- performance: {
1643
- total_ms: Number((performance.now() - summaryStartMs).toFixed(2)),
1644
- tasks_loaded_ms: Number(tasksLoadedMs.toFixed(2)),
1645
- workers_polled_ms: Number(workersPolledMs.toFixed(2)),
1646
- task_count: tasks.length,
1647
- worker_count: workers.length,
1648
- },
1649
- };
1650
- }
1651
- async function readSummarySnapshot(teamName, cwd) {
1652
- const p = summarySnapshotPath(teamName, cwd);
1653
- if (!existsSync(p))
1654
- return null;
1655
- try {
1656
- const raw = await readFile(p, 'utf8');
1657
- const parsed = JSON.parse(raw);
1658
- if (!parsed || typeof parsed !== 'object')
1659
- return null;
1660
- return {
1661
- workerTurnCountByName: parsed.workerTurnCountByName ?? {},
1662
- workerTaskByName: parsed.workerTaskByName ?? {},
1663
- };
1664
- }
1665
- catch {
1666
- return null;
1667
- }
1668
- }
1669
- async function writeSummarySnapshot(teamName, snapshot, cwd) {
1670
- await writeAtomic(summarySnapshotPath(teamName, cwd), JSON.stringify(snapshot, null, 2));
1030
+ return await getTeamSummaryImpl({
1031
+ teamName,
1032
+ cwd,
1033
+ readTeamConfig,
1034
+ listTasks,
1035
+ readWorkerHeartbeat,
1036
+ readWorkerStatus,
1037
+ summarySnapshotPath,
1038
+ monitorSnapshotPath,
1039
+ teamPhasePath,
1040
+ writeAtomic,
1041
+ });
1671
1042
  }
1672
1043
  export async function writeShutdownRequest(teamName, workerName, requestedBy, cwd) {
1673
1044
  const p = join(workerDir(teamName, workerName, cwd), 'shutdown-request.json');
@@ -1675,8 +1046,6 @@ export async function writeShutdownRequest(teamName, workerName, requestedBy, cw
1675
1046
  }
1676
1047
  export async function readShutdownAck(teamName, workerName, cwd, minUpdatedAt) {
1677
1048
  const ackPath = join(workerDir(teamName, workerName, cwd), 'shutdown-ack.json');
1678
- if (!existsSync(ackPath))
1679
- return null;
1680
1049
  try {
1681
1050
  const raw = await readFile(ackPath, 'utf-8');
1682
1051
  const parsed = JSON.parse(raw);
@@ -1690,7 +1059,9 @@ export async function readShutdownAck(teamName, workerName, cwd, minUpdatedAt) {
1690
1059
  }
1691
1060
  return parsed;
1692
1061
  }
1693
- catch {
1062
+ catch (error) {
1063
+ if (error.code === 'ENOENT')
1064
+ return null;
1694
1065
  return null;
1695
1066
  }
1696
1067
  }
@@ -1701,69 +1072,17 @@ function monitorSnapshotPath(teamName, cwd) {
1701
1072
  return join(teamDir(teamName, cwd), 'monitor-snapshot.json');
1702
1073
  }
1703
1074
  export async function readMonitorSnapshot(teamName, cwd) {
1704
- const p = monitorSnapshotPath(teamName, cwd);
1705
- if (!existsSync(p))
1706
- return null;
1707
- try {
1708
- const raw = await readFile(p, 'utf-8');
1709
- const parsed = JSON.parse(raw);
1710
- if (!parsed || typeof parsed !== 'object')
1711
- return null;
1712
- const monitorTimings = (() => {
1713
- const candidate = parsed.monitorTimings;
1714
- if (!candidate || typeof candidate !== 'object')
1715
- return undefined;
1716
- if (typeof candidate.list_tasks_ms !== 'number' ||
1717
- typeof candidate.worker_scan_ms !== 'number' ||
1718
- typeof candidate.mailbox_delivery_ms !== 'number' ||
1719
- typeof candidate.total_ms !== 'number' ||
1720
- typeof candidate.updated_at !== 'string') {
1721
- return undefined;
1722
- }
1723
- return candidate;
1724
- })();
1725
- return {
1726
- taskStatusById: parsed.taskStatusById ?? {},
1727
- workerAliveByName: parsed.workerAliveByName ?? {},
1728
- workerStateByName: parsed.workerStateByName ?? {},
1729
- workerTurnCountByName: parsed.workerTurnCountByName ?? {},
1730
- workerTaskIdByName: parsed.workerTaskIdByName ?? {},
1731
- mailboxNotifiedByMessageId: parsed.mailboxNotifiedByMessageId ?? {},
1732
- completedEventTaskIds: parsed.completedEventTaskIds ?? {},
1733
- monitorTimings,
1734
- };
1735
- }
1736
- catch {
1737
- return null;
1738
- }
1075
+ return await readMonitorSnapshotImpl(teamName, cwd, monitorSnapshotPath);
1739
1076
  }
1740
1077
  export async function writeMonitorSnapshot(teamName, snapshot, cwd) {
1741
- await writeAtomic(monitorSnapshotPath(teamName, cwd), JSON.stringify(snapshot, null, 2));
1078
+ await writeMonitorSnapshotImpl(teamName, snapshot, cwd, monitorSnapshotPath, writeAtomic);
1742
1079
  }
1743
1080
  export async function readTeamPhase(teamName, cwd) {
1744
- const p = teamPhasePath(teamName, cwd);
1745
- if (!existsSync(p))
1746
- return null;
1747
- try {
1748
- const raw = await readFile(p, 'utf-8');
1749
- const parsed = JSON.parse(raw);
1750
- if (!parsed || typeof parsed !== 'object')
1751
- return null;
1752
- const currentPhase = typeof parsed.current_phase === 'string' ? parsed.current_phase : 'team-exec';
1753
- return {
1754
- current_phase: currentPhase,
1755
- max_fix_attempts: typeof parsed.max_fix_attempts === 'number' ? parsed.max_fix_attempts : 3,
1756
- current_fix_attempt: typeof parsed.current_fix_attempt === 'number' ? parsed.current_fix_attempt : 0,
1757
- transitions: Array.isArray(parsed.transitions) ? parsed.transitions : [],
1758
- updated_at: typeof parsed.updated_at === 'string' ? parsed.updated_at : new Date().toISOString(),
1759
- };
1760
- }
1761
- catch {
1762
- return null;
1763
- }
1081
+ const phase = await readTeamPhaseImpl(teamName, cwd, teamPhasePath);
1082
+ return phase;
1764
1083
  }
1765
1084
  export async function writeTeamPhase(teamName, phaseState, cwd) {
1766
- await writeAtomic(teamPhasePath(teamName, cwd), JSON.stringify(phaseState, null, 2));
1085
+ await writeTeamPhaseImpl(teamName, phaseState, cwd, teamPhasePath, writeAtomic);
1767
1086
  }
1768
1087
  // === Config persistence (public wrapper) ===
1769
1088
  export async function saveTeamConfig(config, cwd) {