@herdctl/core 0.0.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 (520) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/.turbo/turbo-test.log +219 -0
  3. package/.turbo/turbo-typecheck.log +4 -0
  4. package/coverage/base.css +224 -0
  5. package/coverage/block-navigation.js +87 -0
  6. package/coverage/coverage-final.json +51 -0
  7. package/coverage/favicon.png +0 -0
  8. package/coverage/index.html +251 -0
  9. package/coverage/prettify.css +1 -0
  10. package/coverage/prettify.js +2 -0
  11. package/coverage/sort-arrow-sprite.png +0 -0
  12. package/coverage/sorter.js +210 -0
  13. package/coverage/src/config/index.html +191 -0
  14. package/coverage/src/config/index.ts.html +442 -0
  15. package/coverage/src/config/interpolate.ts.html +652 -0
  16. package/coverage/src/config/loader.ts.html +1501 -0
  17. package/coverage/src/config/merge.ts.html +823 -0
  18. package/coverage/src/config/parser.ts.html +1213 -0
  19. package/coverage/src/config/schema.ts.html +1123 -0
  20. package/coverage/src/fleet-manager/errors.ts.html +2326 -0
  21. package/coverage/src/fleet-manager/event-types.ts.html +1219 -0
  22. package/coverage/src/fleet-manager/fleet-manager.ts.html +7030 -0
  23. package/coverage/src/fleet-manager/index.html +206 -0
  24. package/coverage/src/fleet-manager/index.ts.html +469 -0
  25. package/coverage/src/fleet-manager/job-manager.ts.html +2074 -0
  26. package/coverage/src/fleet-manager/job-queue.ts.html +2479 -0
  27. package/coverage/src/fleet-manager/types.ts.html +2602 -0
  28. package/coverage/src/index.html +116 -0
  29. package/coverage/src/index.ts.html +181 -0
  30. package/coverage/src/runner/errors.ts.html +1006 -0
  31. package/coverage/src/runner/index.html +191 -0
  32. package/coverage/src/runner/index.ts.html +256 -0
  33. package/coverage/src/runner/job-executor.ts.html +1429 -0
  34. package/coverage/src/runner/message-processor.ts.html +1150 -0
  35. package/coverage/src/runner/sdk-adapter.ts.html +658 -0
  36. package/coverage/src/runner/types.ts.html +559 -0
  37. package/coverage/src/scheduler/errors.ts.html +388 -0
  38. package/coverage/src/scheduler/index.html +206 -0
  39. package/coverage/src/scheduler/index.ts.html +244 -0
  40. package/coverage/src/scheduler/interval.ts.html +652 -0
  41. package/coverage/src/scheduler/schedule-runner.ts.html +1411 -0
  42. package/coverage/src/scheduler/schedule-state.ts.html +718 -0
  43. package/coverage/src/scheduler/scheduler.ts.html +1795 -0
  44. package/coverage/src/scheduler/types.ts.html +733 -0
  45. package/coverage/src/state/directory.ts.html +736 -0
  46. package/coverage/src/state/errors.ts.html +376 -0
  47. package/coverage/src/state/fleet-state.ts.html +937 -0
  48. package/coverage/src/state/index.html +221 -0
  49. package/coverage/src/state/index.ts.html +322 -0
  50. package/coverage/src/state/job-metadata.ts.html +1420 -0
  51. package/coverage/src/state/job-output.ts.html +1033 -0
  52. package/coverage/src/state/schemas/fleet-state.ts.html +445 -0
  53. package/coverage/src/state/schemas/index.html +176 -0
  54. package/coverage/src/state/schemas/index.ts.html +286 -0
  55. package/coverage/src/state/schemas/job-metadata.ts.html +628 -0
  56. package/coverage/src/state/schemas/job-output.ts.html +616 -0
  57. package/coverage/src/state/schemas/session-info.ts.html +361 -0
  58. package/coverage/src/state/session.ts.html +844 -0
  59. package/coverage/src/state/types.ts.html +262 -0
  60. package/coverage/src/state/utils/atomic.ts.html +748 -0
  61. package/coverage/src/state/utils/index.html +146 -0
  62. package/coverage/src/state/utils/index.ts.html +103 -0
  63. package/coverage/src/state/utils/reads.ts.html +1621 -0
  64. package/coverage/src/work-sources/adapters/github.ts.html +3583 -0
  65. package/coverage/src/work-sources/adapters/index.html +131 -0
  66. package/coverage/src/work-sources/adapters/index.ts.html +277 -0
  67. package/coverage/src/work-sources/errors.ts.html +298 -0
  68. package/coverage/src/work-sources/index.html +176 -0
  69. package/coverage/src/work-sources/index.ts.html +529 -0
  70. package/coverage/src/work-sources/manager.ts.html +1324 -0
  71. package/coverage/src/work-sources/registry.ts.html +619 -0
  72. package/coverage/src/work-sources/types.ts.html +568 -0
  73. package/dist/config/__tests__/agent.test.d.ts +2 -0
  74. package/dist/config/__tests__/agent.test.d.ts.map +1 -0
  75. package/dist/config/__tests__/agent.test.js +752 -0
  76. package/dist/config/__tests__/agent.test.js.map +1 -0
  77. package/dist/config/__tests__/interpolate.test.d.ts +2 -0
  78. package/dist/config/__tests__/interpolate.test.d.ts.map +1 -0
  79. package/dist/config/__tests__/interpolate.test.js +509 -0
  80. package/dist/config/__tests__/interpolate.test.js.map +1 -0
  81. package/dist/config/__tests__/loader.test.d.ts +2 -0
  82. package/dist/config/__tests__/loader.test.d.ts.map +1 -0
  83. package/dist/config/__tests__/loader.test.js +631 -0
  84. package/dist/config/__tests__/loader.test.js.map +1 -0
  85. package/dist/config/__tests__/merge.test.d.ts +2 -0
  86. package/dist/config/__tests__/merge.test.d.ts.map +1 -0
  87. package/dist/config/__tests__/merge.test.js +672 -0
  88. package/dist/config/__tests__/merge.test.js.map +1 -0
  89. package/dist/config/__tests__/parser.test.d.ts +2 -0
  90. package/dist/config/__tests__/parser.test.d.ts.map +1 -0
  91. package/dist/config/__tests__/parser.test.js +476 -0
  92. package/dist/config/__tests__/parser.test.js.map +1 -0
  93. package/dist/config/__tests__/schema.test.d.ts +2 -0
  94. package/dist/config/__tests__/schema.test.d.ts.map +1 -0
  95. package/dist/config/__tests__/schema.test.js +776 -0
  96. package/dist/config/__tests__/schema.test.js.map +1 -0
  97. package/dist/config/index.d.ts +11 -0
  98. package/dist/config/index.d.ts.map +1 -0
  99. package/dist/config/index.js +26 -0
  100. package/dist/config/index.js.map +1 -0
  101. package/dist/config/interpolate.d.ts +76 -0
  102. package/dist/config/interpolate.d.ts.map +1 -0
  103. package/dist/config/interpolate.js +143 -0
  104. package/dist/config/interpolate.js.map +1 -0
  105. package/dist/config/loader.d.ts +147 -0
  106. package/dist/config/loader.d.ts.map +1 -0
  107. package/dist/config/loader.js +336 -0
  108. package/dist/config/loader.js.map +1 -0
  109. package/dist/config/merge.d.ts +84 -0
  110. package/dist/config/merge.d.ts.map +1 -0
  111. package/dist/config/merge.js +138 -0
  112. package/dist/config/merge.js.map +1 -0
  113. package/dist/config/parser.d.ts +143 -0
  114. package/dist/config/parser.d.ts.map +1 -0
  115. package/dist/config/parser.js +316 -0
  116. package/dist/config/parser.js.map +1 -0
  117. package/dist/config/schema.d.ts +1906 -0
  118. package/dist/config/schema.d.ts.map +1 -0
  119. package/dist/config/schema.js +268 -0
  120. package/dist/config/schema.js.map +1 -0
  121. package/dist/fleet-manager/__tests__/coverage.test.d.ts +13 -0
  122. package/dist/fleet-manager/__tests__/coverage.test.d.ts.map +1 -0
  123. package/dist/fleet-manager/__tests__/coverage.test.js +2282 -0
  124. package/dist/fleet-manager/__tests__/coverage.test.js.map +1 -0
  125. package/dist/fleet-manager/__tests__/errors.test.d.ts +7 -0
  126. package/dist/fleet-manager/__tests__/errors.test.d.ts.map +1 -0
  127. package/dist/fleet-manager/__tests__/errors.test.js +557 -0
  128. package/dist/fleet-manager/__tests__/errors.test.js.map +1 -0
  129. package/dist/fleet-manager/__tests__/event-helpers.test.d.ts +7 -0
  130. package/dist/fleet-manager/__tests__/event-helpers.test.d.ts.map +1 -0
  131. package/dist/fleet-manager/__tests__/event-helpers.test.js +368 -0
  132. package/dist/fleet-manager/__tests__/event-helpers.test.js.map +1 -0
  133. package/dist/fleet-manager/__tests__/integration.test.d.ts +11 -0
  134. package/dist/fleet-manager/__tests__/integration.test.d.ts.map +1 -0
  135. package/dist/fleet-manager/__tests__/integration.test.js +949 -0
  136. package/dist/fleet-manager/__tests__/integration.test.js.map +1 -0
  137. package/dist/fleet-manager/__tests__/job-control.test.d.ts +7 -0
  138. package/dist/fleet-manager/__tests__/job-control.test.d.ts.map +1 -0
  139. package/dist/fleet-manager/__tests__/job-control.test.js +215 -0
  140. package/dist/fleet-manager/__tests__/job-control.test.js.map +1 -0
  141. package/dist/fleet-manager/__tests__/job-manager.test.d.ts +7 -0
  142. package/dist/fleet-manager/__tests__/job-manager.test.d.ts.map +1 -0
  143. package/dist/fleet-manager/__tests__/job-manager.test.js +659 -0
  144. package/dist/fleet-manager/__tests__/job-manager.test.js.map +1 -0
  145. package/dist/fleet-manager/__tests__/job-queue.test.d.ts +5 -0
  146. package/dist/fleet-manager/__tests__/job-queue.test.d.ts.map +1 -0
  147. package/dist/fleet-manager/__tests__/job-queue.test.js +315 -0
  148. package/dist/fleet-manager/__tests__/job-queue.test.js.map +1 -0
  149. package/dist/fleet-manager/__tests__/reload.test.d.ts +7 -0
  150. package/dist/fleet-manager/__tests__/reload.test.d.ts.map +1 -0
  151. package/dist/fleet-manager/__tests__/reload.test.js +609 -0
  152. package/dist/fleet-manager/__tests__/reload.test.js.map +1 -0
  153. package/dist/fleet-manager/__tests__/status-queries.test.d.ts +7 -0
  154. package/dist/fleet-manager/__tests__/status-queries.test.d.ts.map +1 -0
  155. package/dist/fleet-manager/__tests__/status-queries.test.js +488 -0
  156. package/dist/fleet-manager/__tests__/status-queries.test.js.map +1 -0
  157. package/dist/fleet-manager/__tests__/trigger.test.d.ts +7 -0
  158. package/dist/fleet-manager/__tests__/trigger.test.d.ts.map +1 -0
  159. package/dist/fleet-manager/__tests__/trigger.test.js +471 -0
  160. package/dist/fleet-manager/__tests__/trigger.test.js.map +1 -0
  161. package/dist/fleet-manager/errors.d.ts +407 -0
  162. package/dist/fleet-manager/errors.d.ts.map +1 -0
  163. package/dist/fleet-manager/errors.js +569 -0
  164. package/dist/fleet-manager/errors.js.map +1 -0
  165. package/dist/fleet-manager/event-types.d.ts +302 -0
  166. package/dist/fleet-manager/event-types.d.ts.map +1 -0
  167. package/dist/fleet-manager/event-types.js +9 -0
  168. package/dist/fleet-manager/event-types.js.map +1 -0
  169. package/dist/fleet-manager/fleet-manager.d.ts +699 -0
  170. package/dist/fleet-manager/fleet-manager.d.ts.map +1 -0
  171. package/dist/fleet-manager/fleet-manager.js +1906 -0
  172. package/dist/fleet-manager/fleet-manager.js.map +1 -0
  173. package/dist/fleet-manager/index.d.ts +17 -0
  174. package/dist/fleet-manager/index.d.ts.map +1 -0
  175. package/dist/fleet-manager/index.js +29 -0
  176. package/dist/fleet-manager/index.js.map +1 -0
  177. package/dist/fleet-manager/job-manager.d.ts +271 -0
  178. package/dist/fleet-manager/job-manager.d.ts.map +1 -0
  179. package/dist/fleet-manager/job-manager.js +443 -0
  180. package/dist/fleet-manager/job-manager.js.map +1 -0
  181. package/dist/fleet-manager/job-queue.d.ts +422 -0
  182. package/dist/fleet-manager/job-queue.d.ts.map +1 -0
  183. package/dist/fleet-manager/job-queue.js +448 -0
  184. package/dist/fleet-manager/job-queue.js.map +1 -0
  185. package/dist/fleet-manager/types.d.ts +680 -0
  186. package/dist/fleet-manager/types.d.ts.map +1 -0
  187. package/dist/fleet-manager/types.js +8 -0
  188. package/dist/fleet-manager/types.js.map +1 -0
  189. package/dist/index.d.ts +20 -0
  190. package/dist/index.d.ts.map +1 -0
  191. package/dist/index.js +26 -0
  192. package/dist/index.js.map +1 -0
  193. package/dist/runner/__tests__/errors.test.d.ts +2 -0
  194. package/dist/runner/__tests__/errors.test.d.ts.map +1 -0
  195. package/dist/runner/__tests__/errors.test.js +264 -0
  196. package/dist/runner/__tests__/errors.test.js.map +1 -0
  197. package/dist/runner/__tests__/job-executor.test.d.ts +2 -0
  198. package/dist/runner/__tests__/job-executor.test.d.ts.map +1 -0
  199. package/dist/runner/__tests__/job-executor.test.js +1345 -0
  200. package/dist/runner/__tests__/job-executor.test.js.map +1 -0
  201. package/dist/runner/__tests__/message-processor.test.d.ts +2 -0
  202. package/dist/runner/__tests__/message-processor.test.d.ts.map +1 -0
  203. package/dist/runner/__tests__/message-processor.test.js +768 -0
  204. package/dist/runner/__tests__/message-processor.test.js.map +1 -0
  205. package/dist/runner/__tests__/sdk-adapter.test.d.ts +2 -0
  206. package/dist/runner/__tests__/sdk-adapter.test.d.ts.map +1 -0
  207. package/dist/runner/__tests__/sdk-adapter.test.js +554 -0
  208. package/dist/runner/__tests__/sdk-adapter.test.js.map +1 -0
  209. package/dist/runner/errors.d.ts +121 -0
  210. package/dist/runner/errors.d.ts.map +1 -0
  211. package/dist/runner/errors.js +212 -0
  212. package/dist/runner/errors.js.map +1 -0
  213. package/dist/runner/index.d.ts +12 -0
  214. package/dist/runner/index.d.ts.map +1 -0
  215. package/dist/runner/index.js +15 -0
  216. package/dist/runner/index.js.map +1 -0
  217. package/dist/runner/job-executor.d.ts +98 -0
  218. package/dist/runner/job-executor.d.ts.map +1 -0
  219. package/dist/runner/job-executor.js +333 -0
  220. package/dist/runner/job-executor.js.map +1 -0
  221. package/dist/runner/message-processor.d.ts +45 -0
  222. package/dist/runner/message-processor.d.ts.map +1 -0
  223. package/dist/runner/message-processor.js +294 -0
  224. package/dist/runner/message-processor.js.map +1 -0
  225. package/dist/runner/sdk-adapter.d.ts +60 -0
  226. package/dist/runner/sdk-adapter.d.ts.map +1 -0
  227. package/dist/runner/sdk-adapter.js +138 -0
  228. package/dist/runner/sdk-adapter.js.map +1 -0
  229. package/dist/runner/types.d.ts +135 -0
  230. package/dist/runner/types.d.ts.map +1 -0
  231. package/dist/runner/types.js +7 -0
  232. package/dist/runner/types.js.map +1 -0
  233. package/dist/scheduler/__tests__/errors.test.d.ts +2 -0
  234. package/dist/scheduler/__tests__/errors.test.d.ts.map +1 -0
  235. package/dist/scheduler/__tests__/errors.test.js +101 -0
  236. package/dist/scheduler/__tests__/errors.test.js.map +1 -0
  237. package/dist/scheduler/__tests__/interval.test.d.ts +2 -0
  238. package/dist/scheduler/__tests__/interval.test.d.ts.map +1 -0
  239. package/dist/scheduler/__tests__/interval.test.js +419 -0
  240. package/dist/scheduler/__tests__/interval.test.js.map +1 -0
  241. package/dist/scheduler/__tests__/schedule-runner.test.d.ts +2 -0
  242. package/dist/scheduler/__tests__/schedule-runner.test.d.ts.map +1 -0
  243. package/dist/scheduler/__tests__/schedule-runner.test.js +634 -0
  244. package/dist/scheduler/__tests__/schedule-runner.test.js.map +1 -0
  245. package/dist/scheduler/__tests__/schedule-state.test.d.ts +2 -0
  246. package/dist/scheduler/__tests__/schedule-state.test.d.ts.map +1 -0
  247. package/dist/scheduler/__tests__/schedule-state.test.js +572 -0
  248. package/dist/scheduler/__tests__/schedule-state.test.js.map +1 -0
  249. package/dist/scheduler/__tests__/scheduler.test.d.ts +2 -0
  250. package/dist/scheduler/__tests__/scheduler.test.d.ts.map +1 -0
  251. package/dist/scheduler/__tests__/scheduler.test.js +987 -0
  252. package/dist/scheduler/__tests__/scheduler.test.js.map +1 -0
  253. package/dist/scheduler/errors.d.ts +61 -0
  254. package/dist/scheduler/errors.d.ts.map +1 -0
  255. package/dist/scheduler/errors.js +81 -0
  256. package/dist/scheduler/errors.js.map +1 -0
  257. package/dist/scheduler/index.d.ts +13 -0
  258. package/dist/scheduler/index.d.ts.map +1 -0
  259. package/dist/scheduler/index.js +17 -0
  260. package/dist/scheduler/index.js.map +1 -0
  261. package/dist/scheduler/interval.d.ts +64 -0
  262. package/dist/scheduler/interval.d.ts.map +1 -0
  263. package/dist/scheduler/interval.js +139 -0
  264. package/dist/scheduler/interval.js.map +1 -0
  265. package/dist/scheduler/schedule-runner.d.ts +149 -0
  266. package/dist/scheduler/schedule-runner.d.ts.map +1 -0
  267. package/dist/scheduler/schedule-runner.js +277 -0
  268. package/dist/scheduler/schedule-runner.js.map +1 -0
  269. package/dist/scheduler/schedule-state.d.ts +105 -0
  270. package/dist/scheduler/schedule-state.d.ts.map +1 -0
  271. package/dist/scheduler/schedule-state.js +151 -0
  272. package/dist/scheduler/schedule-state.js.map +1 -0
  273. package/dist/scheduler/scheduler.d.ts +138 -0
  274. package/dist/scheduler/scheduler.d.ts.map +1 -0
  275. package/dist/scheduler/scheduler.js +423 -0
  276. package/dist/scheduler/scheduler.js.map +1 -0
  277. package/dist/scheduler/types.d.ts +160 -0
  278. package/dist/scheduler/types.d.ts.map +1 -0
  279. package/dist/scheduler/types.js +8 -0
  280. package/dist/scheduler/types.js.map +1 -0
  281. package/dist/state/__tests__/directory.test.d.ts +2 -0
  282. package/dist/state/__tests__/directory.test.d.ts.map +1 -0
  283. package/dist/state/__tests__/directory.test.js +414 -0
  284. package/dist/state/__tests__/directory.test.js.map +1 -0
  285. package/dist/state/__tests__/fleet-state.test.d.ts +2 -0
  286. package/dist/state/__tests__/fleet-state.test.d.ts.map +1 -0
  287. package/dist/state/__tests__/fleet-state.test.js +696 -0
  288. package/dist/state/__tests__/fleet-state.test.js.map +1 -0
  289. package/dist/state/__tests__/job-metadata-schema.test.d.ts +2 -0
  290. package/dist/state/__tests__/job-metadata-schema.test.d.ts.map +1 -0
  291. package/dist/state/__tests__/job-metadata-schema.test.js +329 -0
  292. package/dist/state/__tests__/job-metadata-schema.test.js.map +1 -0
  293. package/dist/state/__tests__/job-metadata.test.d.ts +2 -0
  294. package/dist/state/__tests__/job-metadata.test.d.ts.map +1 -0
  295. package/dist/state/__tests__/job-metadata.test.js +667 -0
  296. package/dist/state/__tests__/job-metadata.test.js.map +1 -0
  297. package/dist/state/__tests__/job-output.test.d.ts +2 -0
  298. package/dist/state/__tests__/job-output.test.d.ts.map +1 -0
  299. package/dist/state/__tests__/job-output.test.js +672 -0
  300. package/dist/state/__tests__/job-output.test.js.map +1 -0
  301. package/dist/state/__tests__/session-schema.test.d.ts +2 -0
  302. package/dist/state/__tests__/session-schema.test.d.ts.map +1 -0
  303. package/dist/state/__tests__/session-schema.test.js +323 -0
  304. package/dist/state/__tests__/session-schema.test.js.map +1 -0
  305. package/dist/state/__tests__/session.test.d.ts +2 -0
  306. package/dist/state/__tests__/session.test.d.ts.map +1 -0
  307. package/dist/state/__tests__/session.test.js +468 -0
  308. package/dist/state/__tests__/session.test.js.map +1 -0
  309. package/dist/state/directory.d.ts +42 -0
  310. package/dist/state/directory.d.ts.map +1 -0
  311. package/dist/state/directory.js +170 -0
  312. package/dist/state/directory.js.map +1 -0
  313. package/dist/state/errors.d.ts +44 -0
  314. package/dist/state/errors.d.ts.map +1 -0
  315. package/dist/state/errors.js +82 -0
  316. package/dist/state/errors.js.map +1 -0
  317. package/dist/state/fleet-state.d.ts +126 -0
  318. package/dist/state/fleet-state.d.ts.map +1 -0
  319. package/dist/state/fleet-state.js +196 -0
  320. package/dist/state/fleet-state.js.map +1 -0
  321. package/dist/state/index.d.ts +21 -0
  322. package/dist/state/index.d.ts.map +1 -0
  323. package/dist/state/index.js +30 -0
  324. package/dist/state/index.js.map +1 -0
  325. package/dist/state/job-metadata.d.ts +151 -0
  326. package/dist/state/job-metadata.d.ts.map +1 -0
  327. package/dist/state/job-metadata.js +287 -0
  328. package/dist/state/job-metadata.js.map +1 -0
  329. package/dist/state/job-output.d.ts +116 -0
  330. package/dist/state/job-output.d.ts.map +1 -0
  331. package/dist/state/job-output.js +218 -0
  332. package/dist/state/job-output.js.map +1 -0
  333. package/dist/state/schemas/__tests__/job-output.test.d.ts +2 -0
  334. package/dist/state/schemas/__tests__/job-output.test.d.ts.map +1 -0
  335. package/dist/state/schemas/__tests__/job-output.test.js +279 -0
  336. package/dist/state/schemas/__tests__/job-output.test.js.map +1 -0
  337. package/dist/state/schemas/fleet-state.d.ts +249 -0
  338. package/dist/state/schemas/fleet-state.d.ts.map +1 -0
  339. package/dist/state/schemas/fleet-state.js +97 -0
  340. package/dist/state/schemas/fleet-state.js.map +1 -0
  341. package/dist/state/schemas/index.d.ts +10 -0
  342. package/dist/state/schemas/index.d.ts.map +1 -0
  343. package/dist/state/schemas/index.js +10 -0
  344. package/dist/state/schemas/index.js.map +1 -0
  345. package/dist/state/schemas/job-metadata.d.ts +118 -0
  346. package/dist/state/schemas/job-metadata.d.ts.map +1 -0
  347. package/dist/state/schemas/job-metadata.js +123 -0
  348. package/dist/state/schemas/job-metadata.js.map +1 -0
  349. package/dist/state/schemas/job-output.d.ts +291 -0
  350. package/dist/state/schemas/job-output.d.ts.map +1 -0
  351. package/dist/state/schemas/job-output.js +132 -0
  352. package/dist/state/schemas/job-output.js.map +1 -0
  353. package/dist/state/schemas/session-info.d.ts +65 -0
  354. package/dist/state/schemas/session-info.d.ts.map +1 -0
  355. package/dist/state/schemas/session-info.js +58 -0
  356. package/dist/state/schemas/session-info.js.map +1 -0
  357. package/dist/state/session.d.ts +92 -0
  358. package/dist/state/session.d.ts.map +1 -0
  359. package/dist/state/session.js +173 -0
  360. package/dist/state/session.js.map +1 -0
  361. package/dist/state/types.d.ts +54 -0
  362. package/dist/state/types.d.ts.map +1 -0
  363. package/dist/state/types.js +18 -0
  364. package/dist/state/types.js.map +1 -0
  365. package/dist/state/utils/__tests__/atomic.test.d.ts +2 -0
  366. package/dist/state/utils/__tests__/atomic.test.d.ts.map +1 -0
  367. package/dist/state/utils/__tests__/atomic.test.js +537 -0
  368. package/dist/state/utils/__tests__/atomic.test.js.map +1 -0
  369. package/dist/state/utils/__tests__/reads.test.d.ts +2 -0
  370. package/dist/state/utils/__tests__/reads.test.d.ts.map +1 -0
  371. package/dist/state/utils/__tests__/reads.test.js +792 -0
  372. package/dist/state/utils/__tests__/reads.test.js.map +1 -0
  373. package/dist/state/utils/atomic.d.ts +89 -0
  374. package/dist/state/utils/atomic.d.ts.map +1 -0
  375. package/dist/state/utils/atomic.js +157 -0
  376. package/dist/state/utils/atomic.js.map +1 -0
  377. package/dist/state/utils/index.d.ts +6 -0
  378. package/dist/state/utils/index.d.ts.map +1 -0
  379. package/dist/state/utils/index.js +6 -0
  380. package/dist/state/utils/index.js.map +1 -0
  381. package/dist/state/utils/reads.d.ts +196 -0
  382. package/dist/state/utils/reads.d.ts.map +1 -0
  383. package/dist/state/utils/reads.js +346 -0
  384. package/dist/state/utils/reads.js.map +1 -0
  385. package/dist/work-sources/__tests__/github.test.d.ts +2 -0
  386. package/dist/work-sources/__tests__/github.test.d.ts.map +1 -0
  387. package/dist/work-sources/__tests__/github.test.js +1334 -0
  388. package/dist/work-sources/__tests__/github.test.js.map +1 -0
  389. package/dist/work-sources/__tests__/manager.test.d.ts +2 -0
  390. package/dist/work-sources/__tests__/manager.test.d.ts.map +1 -0
  391. package/dist/work-sources/__tests__/manager.test.js +424 -0
  392. package/dist/work-sources/__tests__/manager.test.js.map +1 -0
  393. package/dist/work-sources/__tests__/registry.test.d.ts +2 -0
  394. package/dist/work-sources/__tests__/registry.test.d.ts.map +1 -0
  395. package/dist/work-sources/__tests__/registry.test.js +381 -0
  396. package/dist/work-sources/__tests__/registry.test.js.map +1 -0
  397. package/dist/work-sources/__tests__/types.test.d.ts +2 -0
  398. package/dist/work-sources/__tests__/types.test.d.ts.map +1 -0
  399. package/dist/work-sources/__tests__/types.test.js +406 -0
  400. package/dist/work-sources/__tests__/types.test.js.map +1 -0
  401. package/dist/work-sources/adapters/github.d.ts +290 -0
  402. package/dist/work-sources/adapters/github.d.ts.map +1 -0
  403. package/dist/work-sources/adapters/github.js +803 -0
  404. package/dist/work-sources/adapters/github.js.map +1 -0
  405. package/dist/work-sources/adapters/index.d.ts +10 -0
  406. package/dist/work-sources/adapters/index.d.ts.map +1 -0
  407. package/dist/work-sources/adapters/index.js +31 -0
  408. package/dist/work-sources/adapters/index.js.map +1 -0
  409. package/dist/work-sources/errors.d.ts +40 -0
  410. package/dist/work-sources/errors.d.ts.map +1 -0
  411. package/dist/work-sources/errors.js +54 -0
  412. package/dist/work-sources/errors.js.map +1 -0
  413. package/dist/work-sources/index.d.ts +105 -0
  414. package/dist/work-sources/index.d.ts.map +1 -0
  415. package/dist/work-sources/index.js +24 -0
  416. package/dist/work-sources/index.js.map +1 -0
  417. package/dist/work-sources/manager.d.ts +370 -0
  418. package/dist/work-sources/manager.d.ts.map +1 -0
  419. package/dist/work-sources/manager.js +61 -0
  420. package/dist/work-sources/manager.js.map +1 -0
  421. package/dist/work-sources/registry.d.ts +128 -0
  422. package/dist/work-sources/registry.d.ts.map +1 -0
  423. package/dist/work-sources/registry.js +132 -0
  424. package/dist/work-sources/registry.js.map +1 -0
  425. package/dist/work-sources/types.d.ts +127 -0
  426. package/dist/work-sources/types.d.ts.map +1 -0
  427. package/dist/work-sources/types.js +8 -0
  428. package/dist/work-sources/types.js.map +1 -0
  429. package/package.json +23 -0
  430. package/src/config/__tests__/agent.test.ts +864 -0
  431. package/src/config/__tests__/interpolate.test.ts +644 -0
  432. package/src/config/__tests__/loader.test.ts +784 -0
  433. package/src/config/__tests__/merge.test.ts +751 -0
  434. package/src/config/__tests__/parser.test.ts +533 -0
  435. package/src/config/__tests__/schema.test.ts +873 -0
  436. package/src/config/index.ts +119 -0
  437. package/src/config/interpolate.ts +189 -0
  438. package/src/config/loader.ts +472 -0
  439. package/src/config/merge.ts +246 -0
  440. package/src/config/parser.ts +376 -0
  441. package/src/config/schema.ts +346 -0
  442. package/src/fleet-manager/__tests__/coverage.test.ts +2869 -0
  443. package/src/fleet-manager/__tests__/errors.test.ts +660 -0
  444. package/src/fleet-manager/__tests__/event-helpers.test.ts +448 -0
  445. package/src/fleet-manager/__tests__/integration.test.ts +1209 -0
  446. package/src/fleet-manager/__tests__/job-control.test.ts +283 -0
  447. package/src/fleet-manager/__tests__/job-manager.test.ts +869 -0
  448. package/src/fleet-manager/__tests__/job-queue.test.ts +401 -0
  449. package/src/fleet-manager/__tests__/reload.test.ts +751 -0
  450. package/src/fleet-manager/__tests__/status-queries.test.ts +595 -0
  451. package/src/fleet-manager/__tests__/trigger.test.ts +601 -0
  452. package/src/fleet-manager/errors.ts +747 -0
  453. package/src/fleet-manager/event-types.ts +378 -0
  454. package/src/fleet-manager/fleet-manager.ts +2315 -0
  455. package/src/fleet-manager/index.ts +128 -0
  456. package/src/fleet-manager/job-manager.ts +663 -0
  457. package/src/fleet-manager/job-queue.ts +798 -0
  458. package/src/fleet-manager/types.ts +839 -0
  459. package/src/index.ts +32 -0
  460. package/src/runner/__tests__/errors.test.ts +382 -0
  461. package/src/runner/__tests__/job-executor.test.ts +1708 -0
  462. package/src/runner/__tests__/message-processor.test.ts +960 -0
  463. package/src/runner/__tests__/sdk-adapter.test.ts +626 -0
  464. package/src/runner/errors.ts +307 -0
  465. package/src/runner/index.ts +57 -0
  466. package/src/runner/job-executor.ts +448 -0
  467. package/src/runner/message-processor.ts +355 -0
  468. package/src/runner/sdk-adapter.ts +191 -0
  469. package/src/runner/types.ts +158 -0
  470. package/src/scheduler/__tests__/errors.test.ts +159 -0
  471. package/src/scheduler/__tests__/interval.test.ts +515 -0
  472. package/src/scheduler/__tests__/schedule-runner.test.ts +798 -0
  473. package/src/scheduler/__tests__/schedule-state.test.ts +671 -0
  474. package/src/scheduler/__tests__/scheduler.test.ts +1280 -0
  475. package/src/scheduler/errors.ts +101 -0
  476. package/src/scheduler/index.ts +53 -0
  477. package/src/scheduler/interval.ts +189 -0
  478. package/src/scheduler/schedule-runner.ts +442 -0
  479. package/src/scheduler/schedule-state.ts +211 -0
  480. package/src/scheduler/scheduler.ts +570 -0
  481. package/src/scheduler/types.ts +216 -0
  482. package/src/state/__tests__/directory.test.ts +595 -0
  483. package/src/state/__tests__/fleet-state.test.ts +868 -0
  484. package/src/state/__tests__/job-metadata-schema.test.ts +414 -0
  485. package/src/state/__tests__/job-metadata.test.ts +831 -0
  486. package/src/state/__tests__/job-output.test.ts +856 -0
  487. package/src/state/__tests__/session-schema.test.ts +378 -0
  488. package/src/state/__tests__/session.test.ts +604 -0
  489. package/src/state/directory.ts +217 -0
  490. package/src/state/errors.ts +97 -0
  491. package/src/state/fleet-state.ts +284 -0
  492. package/src/state/index.ts +79 -0
  493. package/src/state/job-metadata.ts +445 -0
  494. package/src/state/job-output.ts +316 -0
  495. package/src/state/schemas/__tests__/job-output.test.ts +338 -0
  496. package/src/state/schemas/fleet-state.ts +120 -0
  497. package/src/state/schemas/index.ts +67 -0
  498. package/src/state/schemas/job-metadata.ts +181 -0
  499. package/src/state/schemas/job-output.ts +177 -0
  500. package/src/state/schemas/session-info.ts +92 -0
  501. package/src/state/session.ts +253 -0
  502. package/src/state/types.ts +59 -0
  503. package/src/state/utils/__tests__/atomic.test.ts +723 -0
  504. package/src/state/utils/__tests__/reads.test.ts +1071 -0
  505. package/src/state/utils/atomic.ts +221 -0
  506. package/src/state/utils/index.ts +6 -0
  507. package/src/state/utils/reads.ts +512 -0
  508. package/src/work-sources/__tests__/github.test.ts +1800 -0
  509. package/src/work-sources/__tests__/manager.test.ts +529 -0
  510. package/src/work-sources/__tests__/registry.test.ts +477 -0
  511. package/src/work-sources/__tests__/types.test.ts +479 -0
  512. package/src/work-sources/adapters/github.ts +1166 -0
  513. package/src/work-sources/adapters/index.ts +64 -0
  514. package/src/work-sources/errors.ts +71 -0
  515. package/src/work-sources/index.ts +148 -0
  516. package/src/work-sources/manager.ts +413 -0
  517. package/src/work-sources/registry.ts +178 -0
  518. package/src/work-sources/types.ts +161 -0
  519. package/tsconfig.json +9 -0
  520. package/vitest.config.ts +19 -0
@@ -0,0 +1,1906 @@
1
+ /**
2
+ * FleetManager class for library consumers
3
+ *
4
+ * Provides a simple, high-level API to initialize and run a fleet of agents
5
+ * with minimal configuration. Handles config loading, state directory setup,
6
+ * and scheduler orchestration internally.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * import { FleetManager } from '@herdctl/core';
11
+ *
12
+ * const manager = new FleetManager({
13
+ * configPath: './herdctl.yaml',
14
+ * stateDir: './.herdctl',
15
+ * });
16
+ *
17
+ * await manager.initialize();
18
+ * await manager.start();
19
+ *
20
+ * // Later...
21
+ * await manager.stop();
22
+ * ```
23
+ */
24
+ import { EventEmitter } from "node:events";
25
+ import { resolve } from "node:path";
26
+ import { loadConfig, ConfigNotFoundError, ConfigError, } from "../config/index.js";
27
+ import { initStateDirectory, createJob, getJob, updateJob, listJobs } from "../state/index.js";
28
+ import { Scheduler } from "../scheduler/index.js";
29
+ import { join } from "node:path";
30
+ import { FleetManagerStateError, FleetManagerConfigError, FleetManagerStateDirError, FleetManagerShutdownError, AgentNotFoundError, ScheduleNotFoundError, ConcurrencyLimitError, InvalidStateError,
31
+ // Job control errors (US-6)
32
+ JobCancelError, JobForkError, JobNotFoundError, } from "./errors.js";
33
+ import { readFleetState } from "../state/fleet-state.js";
34
+ // =============================================================================
35
+ // Constants
36
+ // =============================================================================
37
+ /**
38
+ * Default check interval in milliseconds (1 second)
39
+ */
40
+ const DEFAULT_CHECK_INTERVAL = 1000;
41
+ // =============================================================================
42
+ // Default Logger
43
+ // =============================================================================
44
+ /**
45
+ * Create a default console-based logger
46
+ */
47
+ function createDefaultLogger() {
48
+ return {
49
+ debug: (message) => console.debug(`[fleet-manager] ${message}`),
50
+ info: (message) => console.info(`[fleet-manager] ${message}`),
51
+ warn: (message) => console.warn(`[fleet-manager] ${message}`),
52
+ error: (message) => console.error(`[fleet-manager] ${message}`),
53
+ };
54
+ }
55
+ // =============================================================================
56
+ // FleetManager Class
57
+ // =============================================================================
58
+ /**
59
+ * FleetManager provides a simple API to manage a fleet of agents
60
+ *
61
+ * This class is the primary entry point for library consumers who want to
62
+ * run herdctl programmatically. It handles:
63
+ *
64
+ * - Configuration loading and validation
65
+ * - State directory initialization
66
+ * - Scheduler lifecycle management
67
+ * - Event emission for monitoring
68
+ *
69
+ * ## Lifecycle
70
+ *
71
+ * 1. **Construction**: Create with options (configPath, stateDir)
72
+ * 2. **Initialize**: Call `initialize()` to load config and prepare state
73
+ * 3. **Start**: Call `start()` to begin scheduler and process schedules
74
+ * 4. **Stop**: Call `stop()` to gracefully shut down
75
+ *
76
+ * ## Events
77
+ *
78
+ * The FleetManager emits events for monitoring:
79
+ * - `initialized` - After successful initialization
80
+ * - `started` - When the scheduler starts running
81
+ * - `stopped` - When the scheduler stops
82
+ * - `error` - When an error occurs
83
+ * - `schedule:trigger` - When a schedule triggers an agent
84
+ * - `schedule:complete` - When an agent run completes
85
+ * - `schedule:error` - When an agent run fails
86
+ *
87
+ * ## Typed Events (US-2)
88
+ *
89
+ * The FleetManager also supports strongly-typed events via TypeScript:
90
+ * - `config:reloaded` - When configuration is hot-reloaded
91
+ * - `agent:started` - When an agent is started
92
+ * - `agent:stopped` - When an agent is stopped
93
+ * - `schedule:triggered` - When a schedule triggers (with payload)
94
+ * - `schedule:skipped` - When a schedule is skipped
95
+ * - `job:created` - When a job is created
96
+ * - `job:output` - When a job produces output
97
+ * - `job:completed` - When a job completes successfully
98
+ * - `job:failed` - When a job fails
99
+ *
100
+ * @example
101
+ * ```typescript
102
+ * // Subscribe to typed events
103
+ * manager.on('job:created', (payload) => {
104
+ * console.log(`Job ${payload.job.id} created for ${payload.agentName}`);
105
+ * });
106
+ *
107
+ * manager.on('job:output', (payload) => {
108
+ * process.stdout.write(payload.output);
109
+ * });
110
+ *
111
+ * manager.on('job:completed', (payload) => {
112
+ * console.log(`Job completed in ${payload.durationSeconds}s`);
113
+ * });
114
+ * ```
115
+ */
116
+ export class FleetManager extends EventEmitter {
117
+ // Configuration
118
+ configPath;
119
+ stateDir;
120
+ logger;
121
+ checkInterval;
122
+ // Internal state
123
+ status = "uninitialized";
124
+ config = null;
125
+ stateDirInfo = null;
126
+ scheduler = null;
127
+ // Timing info
128
+ initializedAt = null;
129
+ startedAt = null;
130
+ stoppedAt = null;
131
+ lastError = null;
132
+ /**
133
+ * Create a new FleetManager instance
134
+ *
135
+ * @param options - Configuration options
136
+ *
137
+ * @example
138
+ * ```typescript
139
+ * // Minimal configuration
140
+ * const manager = new FleetManager({
141
+ * configPath: './herdctl.yaml',
142
+ * stateDir: './.herdctl',
143
+ * });
144
+ *
145
+ * // With custom logger
146
+ * const manager = new FleetManager({
147
+ * configPath: './herdctl.yaml',
148
+ * stateDir: './.herdctl',
149
+ * logger: myLogger,
150
+ * checkInterval: 5000, // 5 seconds
151
+ * });
152
+ * ```
153
+ */
154
+ constructor(options) {
155
+ super();
156
+ this.configPath = options.configPath;
157
+ this.stateDir = resolve(options.stateDir);
158
+ this.logger = options.logger ?? createDefaultLogger();
159
+ this.checkInterval = options.checkInterval ?? DEFAULT_CHECK_INTERVAL;
160
+ }
161
+ // ===========================================================================
162
+ // Public State Accessors
163
+ // ===========================================================================
164
+ /**
165
+ * Get the current fleet manager state
166
+ *
167
+ * This provides a snapshot of the fleet manager's current status and
168
+ * configuration for monitoring purposes.
169
+ */
170
+ get state() {
171
+ return {
172
+ status: this.status,
173
+ initializedAt: this.initializedAt,
174
+ startedAt: this.startedAt,
175
+ stoppedAt: this.stoppedAt,
176
+ agentCount: this.config?.agents.length ?? 0,
177
+ lastError: this.lastError,
178
+ };
179
+ }
180
+ /**
181
+ * Get the loaded configuration
182
+ *
183
+ * @returns The resolved configuration, or null if not initialized
184
+ */
185
+ getConfig() {
186
+ return this.config;
187
+ }
188
+ /**
189
+ * Get the loaded agents
190
+ *
191
+ * @returns Array of resolved agents, or empty array if not initialized
192
+ */
193
+ getAgents() {
194
+ return this.config?.agents ?? [];
195
+ }
196
+ // ===========================================================================
197
+ // Fleet Status Query Methods (US-3)
198
+ // ===========================================================================
199
+ /**
200
+ * Get overall fleet status
201
+ *
202
+ * Returns a comprehensive snapshot of the fleet state including:
203
+ * - Current state and uptime
204
+ * - Agent counts (total, idle, running, error)
205
+ * - Job counts
206
+ * - Scheduler information
207
+ *
208
+ * This method works whether the fleet is running or stopped.
209
+ *
210
+ * @returns A consistent FleetStatus snapshot
211
+ *
212
+ * @example
213
+ * ```typescript
214
+ * const status = await manager.getFleetStatus();
215
+ * console.log(`Fleet: ${status.state}`);
216
+ * console.log(`Uptime: ${status.uptimeSeconds}s`);
217
+ * console.log(`Running jobs: ${status.counts.runningJobs}`);
218
+ * ```
219
+ */
220
+ async getFleetStatus() {
221
+ // Get agent info to compute counts
222
+ const agentInfoList = await this.getAgentInfo();
223
+ // Compute counts from agent info
224
+ const counts = this.computeFleetCounts(agentInfoList);
225
+ // Compute uptime
226
+ let uptimeSeconds = null;
227
+ if (this.startedAt) {
228
+ const startTime = new Date(this.startedAt).getTime();
229
+ const endTime = this.stoppedAt
230
+ ? new Date(this.stoppedAt).getTime()
231
+ : Date.now();
232
+ uptimeSeconds = Math.floor((endTime - startTime) / 1000);
233
+ }
234
+ // Get scheduler state
235
+ const schedulerState = this.scheduler?.getState();
236
+ return {
237
+ state: this.status,
238
+ uptimeSeconds,
239
+ initializedAt: this.initializedAt,
240
+ startedAt: this.startedAt,
241
+ stoppedAt: this.stoppedAt,
242
+ counts,
243
+ scheduler: {
244
+ status: schedulerState?.status ?? "stopped",
245
+ checkCount: schedulerState?.checkCount ?? 0,
246
+ triggerCount: schedulerState?.triggerCount ?? 0,
247
+ lastCheckAt: schedulerState?.lastCheckAt ?? null,
248
+ checkIntervalMs: this.checkInterval,
249
+ },
250
+ lastError: this.lastError,
251
+ };
252
+ }
253
+ /**
254
+ * Get information about all configured agents
255
+ *
256
+ * Returns detailed information for each agent including:
257
+ * - Current status and job information
258
+ * - Schedule details with runtime state
259
+ * - Configuration details
260
+ *
261
+ * This method works whether the fleet is running or stopped.
262
+ *
263
+ * @returns Array of AgentInfo objects with current state
264
+ *
265
+ * @example
266
+ * ```typescript
267
+ * const agents = await manager.getAgentInfo();
268
+ * for (const agent of agents) {
269
+ * console.log(`${agent.name}: ${agent.status}`);
270
+ * console.log(` Schedules: ${agent.scheduleCount}`);
271
+ * }
272
+ * ```
273
+ */
274
+ async getAgentInfo() {
275
+ const agents = this.config?.agents ?? [];
276
+ // Read fleet state for runtime information
277
+ const fleetState = await this.readFleetStateSnapshot();
278
+ return agents.map((agent) => {
279
+ const agentState = fleetState.agents[agent.name];
280
+ return this.buildAgentInfo(agent, agentState);
281
+ });
282
+ }
283
+ /**
284
+ * Get information about a specific agent by name
285
+ *
286
+ * Returns detailed information for the specified agent including:
287
+ * - Current status and job information
288
+ * - Schedule details with runtime state
289
+ * - Configuration details
290
+ *
291
+ * This method works whether the fleet is running or stopped.
292
+ *
293
+ * @param name - The agent name to look up
294
+ * @returns AgentInfo for the specified agent
295
+ * @throws {AgentNotFoundError} If no agent with that name exists
296
+ *
297
+ * @example
298
+ * ```typescript
299
+ * const agent = await manager.getAgentInfoByName('my-agent');
300
+ * console.log(`Agent: ${agent.name}`);
301
+ * console.log(`Status: ${agent.status}`);
302
+ * console.log(`Running: ${agent.runningCount}/${agent.maxConcurrent}`);
303
+ * ```
304
+ */
305
+ async getAgentInfoByName(name) {
306
+ const agents = this.config?.agents ?? [];
307
+ const agent = agents.find((a) => a.name === name);
308
+ if (!agent) {
309
+ throw new AgentNotFoundError(name);
310
+ }
311
+ // Read fleet state for runtime information
312
+ const fleetState = await this.readFleetStateSnapshot();
313
+ const agentState = fleetState.agents[name];
314
+ return this.buildAgentInfo(agent, agentState);
315
+ }
316
+ // ===========================================================================
317
+ // Private Status Query Helpers
318
+ // ===========================================================================
319
+ /**
320
+ * Read fleet state from disk for status queries
321
+ *
322
+ * This provides a consistent snapshot of the fleet state.
323
+ */
324
+ async readFleetStateSnapshot() {
325
+ if (!this.stateDirInfo) {
326
+ // Not initialized yet, return empty state
327
+ return { fleet: {}, agents: {} };
328
+ }
329
+ return await readFleetState(this.stateDirInfo.stateFile, {
330
+ logger: { warn: this.logger.warn },
331
+ });
332
+ }
333
+ /**
334
+ * Build AgentInfo from configuration and state
335
+ */
336
+ buildAgentInfo(agent, agentState) {
337
+ // Build schedule info
338
+ const schedules = this.buildScheduleInfoList(agent, agentState);
339
+ // Get running count from scheduler or state
340
+ const runningCount = this.scheduler?.getRunningJobCount(agent.name) ?? 0;
341
+ // Determine workspace path
342
+ let workspace;
343
+ if (typeof agent.workspace === "string") {
344
+ workspace = agent.workspace;
345
+ }
346
+ else if (agent.workspace?.root) {
347
+ workspace = agent.workspace.root;
348
+ }
349
+ return {
350
+ name: agent.name,
351
+ description: agent.description,
352
+ status: agentState?.status ?? "idle",
353
+ currentJobId: agentState?.current_job ?? null,
354
+ lastJobId: agentState?.last_job ?? null,
355
+ maxConcurrent: agent.instances?.max_concurrent ?? 1,
356
+ runningCount,
357
+ errorMessage: agentState?.error_message ?? null,
358
+ scheduleCount: schedules.length,
359
+ schedules,
360
+ model: agent.model,
361
+ workspace,
362
+ };
363
+ }
364
+ /**
365
+ * Build schedule info list from agent configuration and state
366
+ */
367
+ buildScheduleInfoList(agent, agentState) {
368
+ if (!agent.schedules) {
369
+ return [];
370
+ }
371
+ return Object.entries(agent.schedules).map(([name, schedule]) => {
372
+ const scheduleState = agentState?.schedules?.[name];
373
+ return {
374
+ name,
375
+ agentName: agent.name,
376
+ type: schedule.type,
377
+ interval: schedule.interval,
378
+ expression: schedule.expression,
379
+ status: scheduleState?.status ?? "idle",
380
+ lastRunAt: scheduleState?.last_run_at ?? null,
381
+ nextRunAt: scheduleState?.next_run_at ?? null,
382
+ lastError: scheduleState?.last_error ?? null,
383
+ };
384
+ });
385
+ }
386
+ /**
387
+ * Compute fleet counts from agent info list
388
+ */
389
+ computeFleetCounts(agentInfoList) {
390
+ let idleAgents = 0;
391
+ let runningAgents = 0;
392
+ let errorAgents = 0;
393
+ let totalSchedules = 0;
394
+ let runningSchedules = 0;
395
+ let runningJobs = 0;
396
+ for (const agent of agentInfoList) {
397
+ switch (agent.status) {
398
+ case "idle":
399
+ idleAgents++;
400
+ break;
401
+ case "running":
402
+ runningAgents++;
403
+ break;
404
+ case "error":
405
+ errorAgents++;
406
+ break;
407
+ }
408
+ totalSchedules += agent.scheduleCount;
409
+ runningJobs += agent.runningCount;
410
+ for (const schedule of agent.schedules) {
411
+ if (schedule.status === "running") {
412
+ runningSchedules++;
413
+ }
414
+ }
415
+ }
416
+ return {
417
+ totalAgents: agentInfoList.length,
418
+ idleAgents,
419
+ runningAgents,
420
+ errorAgents,
421
+ totalSchedules,
422
+ runningSchedules,
423
+ runningJobs,
424
+ };
425
+ }
426
+ // ===========================================================================
427
+ // Schedule Management Methods (US-7)
428
+ // ===========================================================================
429
+ /**
430
+ * Get all schedules across all agents
431
+ *
432
+ * Returns a list of all configured schedules with their current state,
433
+ * including next trigger times.
434
+ *
435
+ * @returns Array of ScheduleInfo objects with current state
436
+ *
437
+ * @example
438
+ * ```typescript
439
+ * const schedules = await manager.getSchedules();
440
+ * for (const schedule of schedules) {
441
+ * console.log(`${schedule.agentName}/${schedule.name}: ${schedule.status}`);
442
+ * console.log(` Next run: ${schedule.nextRunAt}`);
443
+ * }
444
+ * ```
445
+ */
446
+ async getSchedules() {
447
+ const agents = this.config?.agents ?? [];
448
+ const fleetState = await this.readFleetStateSnapshot();
449
+ const allSchedules = [];
450
+ for (const agent of agents) {
451
+ const agentState = fleetState.agents[agent.name];
452
+ const schedules = this.buildScheduleInfoList(agent, agentState);
453
+ allSchedules.push(...schedules);
454
+ }
455
+ return allSchedules;
456
+ }
457
+ /**
458
+ * Get a specific schedule by agent name and schedule name
459
+ *
460
+ * @param agentName - The name of the agent
461
+ * @param scheduleName - The name of the schedule
462
+ * @returns The schedule info with current state
463
+ * @throws {AgentNotFoundError} If the agent doesn't exist
464
+ * @throws {ScheduleNotFoundError} If the schedule doesn't exist
465
+ *
466
+ * @example
467
+ * ```typescript
468
+ * const schedule = await manager.getSchedule('my-agent', 'hourly');
469
+ * console.log(`Status: ${schedule.status}`);
470
+ * console.log(`Last run: ${schedule.lastRunAt}`);
471
+ * console.log(`Next run: ${schedule.nextRunAt}`);
472
+ * ```
473
+ */
474
+ async getSchedule(agentName, scheduleName) {
475
+ const agents = this.config?.agents ?? [];
476
+ const agent = agents.find((a) => a.name === agentName);
477
+ if (!agent) {
478
+ throw new AgentNotFoundError(agentName, {
479
+ availableAgents: agents.map((a) => a.name),
480
+ });
481
+ }
482
+ if (!agent.schedules || !(scheduleName in agent.schedules)) {
483
+ const availableSchedules = agent.schedules
484
+ ? Object.keys(agent.schedules)
485
+ : [];
486
+ throw new ScheduleNotFoundError(agentName, scheduleName, {
487
+ availableSchedules,
488
+ });
489
+ }
490
+ const fleetState = await this.readFleetStateSnapshot();
491
+ const agentState = fleetState.agents[agentName];
492
+ const schedule = agent.schedules[scheduleName];
493
+ const scheduleState = agentState?.schedules?.[scheduleName];
494
+ return {
495
+ name: scheduleName,
496
+ agentName,
497
+ type: schedule.type,
498
+ interval: schedule.interval,
499
+ expression: schedule.expression,
500
+ status: scheduleState?.status ?? "idle",
501
+ lastRunAt: scheduleState?.last_run_at ?? null,
502
+ nextRunAt: scheduleState?.next_run_at ?? null,
503
+ lastError: scheduleState?.last_error ?? null,
504
+ };
505
+ }
506
+ /**
507
+ * Enable a disabled schedule
508
+ *
509
+ * Enables a schedule that was previously disabled, allowing it to trigger
510
+ * again on its configured interval. The enabled state is persisted to the
511
+ * state directory and survives restarts.
512
+ *
513
+ * @param agentName - The name of the agent
514
+ * @param scheduleName - The name of the schedule
515
+ * @returns The updated schedule info
516
+ * @throws {AgentNotFoundError} If the agent doesn't exist
517
+ * @throws {ScheduleNotFoundError} If the schedule doesn't exist
518
+ *
519
+ * @example
520
+ * ```typescript
521
+ * // Enable a previously disabled schedule
522
+ * const schedule = await manager.enableSchedule('my-agent', 'hourly');
523
+ * console.log(`Schedule status: ${schedule.status}`); // 'idle'
524
+ * ```
525
+ */
526
+ async enableSchedule(agentName, scheduleName) {
527
+ // Validate the agent and schedule exist
528
+ const agents = this.config?.agents ?? [];
529
+ const agent = agents.find((a) => a.name === agentName);
530
+ if (!agent) {
531
+ throw new AgentNotFoundError(agentName, {
532
+ availableAgents: agents.map((a) => a.name),
533
+ });
534
+ }
535
+ if (!agent.schedules || !(scheduleName in agent.schedules)) {
536
+ const availableSchedules = agent.schedules
537
+ ? Object.keys(agent.schedules)
538
+ : [];
539
+ throw new ScheduleNotFoundError(agentName, scheduleName, {
540
+ availableSchedules,
541
+ });
542
+ }
543
+ // Update schedule state to enabled (idle)
544
+ const { updateScheduleState } = await import("../scheduler/schedule-state.js");
545
+ await updateScheduleState(this.stateDir, agentName, scheduleName, { status: "idle" }, { logger: { warn: this.logger.warn } });
546
+ this.logger.info(`Enabled schedule ${agentName}/${scheduleName}`);
547
+ // Return the updated schedule info
548
+ return this.getSchedule(agentName, scheduleName);
549
+ }
550
+ /**
551
+ * Disable a schedule
552
+ *
553
+ * Disables a schedule, preventing it from triggering on its configured
554
+ * interval. The schedule remains in the configuration but won't run until
555
+ * re-enabled. The disabled state is persisted to the state directory and
556
+ * survives restarts.
557
+ *
558
+ * @param agentName - The name of the agent
559
+ * @param scheduleName - The name of the schedule
560
+ * @returns The updated schedule info
561
+ * @throws {AgentNotFoundError} If the agent doesn't exist
562
+ * @throws {ScheduleNotFoundError} If the schedule doesn't exist
563
+ *
564
+ * @example
565
+ * ```typescript
566
+ * // Disable a schedule temporarily
567
+ * const schedule = await manager.disableSchedule('my-agent', 'hourly');
568
+ * console.log(`Schedule status: ${schedule.status}`); // 'disabled'
569
+ *
570
+ * // Later, re-enable it
571
+ * await manager.enableSchedule('my-agent', 'hourly');
572
+ * ```
573
+ */
574
+ async disableSchedule(agentName, scheduleName) {
575
+ // Validate the agent and schedule exist
576
+ const agents = this.config?.agents ?? [];
577
+ const agent = agents.find((a) => a.name === agentName);
578
+ if (!agent) {
579
+ throw new AgentNotFoundError(agentName, {
580
+ availableAgents: agents.map((a) => a.name),
581
+ });
582
+ }
583
+ if (!agent.schedules || !(scheduleName in agent.schedules)) {
584
+ const availableSchedules = agent.schedules
585
+ ? Object.keys(agent.schedules)
586
+ : [];
587
+ throw new ScheduleNotFoundError(agentName, scheduleName, {
588
+ availableSchedules,
589
+ });
590
+ }
591
+ // Update schedule state to disabled
592
+ const { updateScheduleState } = await import("../scheduler/schedule-state.js");
593
+ await updateScheduleState(this.stateDir, agentName, scheduleName, { status: "disabled" }, { logger: { warn: this.logger.warn } });
594
+ this.logger.info(`Disabled schedule ${agentName}/${scheduleName}`);
595
+ // Return the updated schedule info
596
+ return this.getSchedule(agentName, scheduleName);
597
+ }
598
+ // ===========================================================================
599
+ // Lifecycle Methods
600
+ // ===========================================================================
601
+ /**
602
+ * Initialize the fleet manager
603
+ *
604
+ * This method:
605
+ * 1. Loads and validates the configuration file
606
+ * 2. Initializes the state directory structure
607
+ * 3. Prepares the scheduler (but does not start it)
608
+ *
609
+ * After initialization, the fleet manager is ready to start.
610
+ *
611
+ * @throws {FleetManagerStateError} If already initialized or running
612
+ * @throws {FleetManagerConfigError} If configuration is invalid or not found
613
+ * @throws {FleetManagerStateDirError} If state directory cannot be created
614
+ *
615
+ * @example
616
+ * ```typescript
617
+ * const manager = new FleetManager({ ... });
618
+ * await manager.initialize();
619
+ * console.log(`Loaded ${manager.state.agentCount} agents`);
620
+ * ```
621
+ */
622
+ async initialize() {
623
+ // Validate current state
624
+ if (this.status !== "uninitialized" && this.status !== "stopped") {
625
+ throw new FleetManagerStateError("initialize", this.status, ["uninitialized", "stopped"]);
626
+ }
627
+ this.logger.info("Initializing fleet manager...");
628
+ try {
629
+ // Load configuration
630
+ this.logger.debug(this.configPath
631
+ ? `Loading config from: ${this.configPath}`
632
+ : "Auto-discovering config...");
633
+ this.config = await this.loadConfiguration();
634
+ this.logger.info(`Loaded ${this.config.agents.length} agent(s) from config`);
635
+ // Initialize state directory
636
+ this.logger.debug(`Initializing state directory: ${this.stateDir}`);
637
+ this.stateDirInfo = await this.initializeStateDir();
638
+ this.logger.debug("State directory initialized");
639
+ // Create scheduler (but don't start it)
640
+ this.scheduler = new Scheduler({
641
+ stateDir: this.stateDir,
642
+ checkInterval: this.checkInterval,
643
+ logger: this.logger,
644
+ onTrigger: (info) => this.handleScheduleTrigger(info),
645
+ });
646
+ // Update state
647
+ this.status = "initialized";
648
+ this.initializedAt = new Date().toISOString();
649
+ this.lastError = null;
650
+ this.logger.info("Fleet manager initialized successfully");
651
+ this.emit("initialized");
652
+ }
653
+ catch (error) {
654
+ this.status = "error";
655
+ this.lastError = error instanceof Error ? error.message : String(error);
656
+ this.emit("error", error instanceof Error ? error : new Error(String(error)));
657
+ throw error;
658
+ }
659
+ }
660
+ /**
661
+ * Start the fleet manager
662
+ *
663
+ * This begins the scheduler, which will:
664
+ * 1. Check agent schedules at the configured interval
665
+ * 2. Trigger agents when their schedules are due
666
+ * 3. Track schedule state in the state directory
667
+ *
668
+ * @throws {FleetManagerStateError} If not initialized
669
+ *
670
+ * @example
671
+ * ```typescript
672
+ * await manager.initialize();
673
+ * await manager.start();
674
+ *
675
+ * // The manager is now running and processing schedules
676
+ * manager.on('schedule:trigger', (agent, schedule) => {
677
+ * console.log(`Triggered ${agent}/${schedule}`);
678
+ * });
679
+ * ```
680
+ */
681
+ async start() {
682
+ // Validate current state
683
+ if (this.status !== "initialized") {
684
+ throw new FleetManagerStateError("start", this.status, "initialized");
685
+ }
686
+ this.logger.info("Starting fleet manager...");
687
+ this.status = "starting";
688
+ try {
689
+ // Start the scheduler with loaded agents
690
+ const agents = this.config.agents;
691
+ // Start scheduler in background (don't await the loop)
692
+ this.startSchedulerAsync(agents);
693
+ // Update state
694
+ this.status = "running";
695
+ this.startedAt = new Date().toISOString();
696
+ this.stoppedAt = null;
697
+ this.logger.info("Fleet manager started");
698
+ this.emit("started");
699
+ }
700
+ catch (error) {
701
+ this.status = "error";
702
+ this.lastError = error instanceof Error ? error.message : String(error);
703
+ this.emit("error", error instanceof Error ? error : new Error(String(error)));
704
+ throw error;
705
+ }
706
+ }
707
+ /**
708
+ * Stop the fleet manager gracefully
709
+ *
710
+ * This will:
711
+ * 1. Signal the scheduler to stop accepting new triggers
712
+ * 2. Wait for running jobs to complete (with timeout)
713
+ * 3. If timeout is reached and cancelOnTimeout is true, cancel remaining jobs
714
+ * 4. Persist all state before shutdown completes
715
+ * 5. Emit 'stopped' event when complete
716
+ *
717
+ * @param options - Stop options for controlling shutdown behavior
718
+ * @throws {FleetManagerShutdownError} If shutdown times out and cancelOnTimeout is false
719
+ *
720
+ * @example
721
+ * ```typescript
722
+ * // Normal shutdown - wait for jobs with default 30s timeout
723
+ * await manager.stop();
724
+ *
725
+ * // Shutdown with custom timeout
726
+ * await manager.stop({ timeout: 60000 });
727
+ *
728
+ * // Shutdown without waiting for jobs (not recommended)
729
+ * await manager.stop({ waitForJobs: false });
730
+ *
731
+ * // Cancel jobs if they don't complete in time
732
+ * await manager.stop({
733
+ * timeout: 30000,
734
+ * cancelOnTimeout: true,
735
+ * cancelTimeout: 10000,
736
+ * });
737
+ * ```
738
+ */
739
+ async stop(options) {
740
+ if (this.status !== "running" && this.status !== "starting") {
741
+ this.logger.debug(`Stop called but status is '${this.status}', ignoring`);
742
+ return;
743
+ }
744
+ const waitForJobs = options?.waitForJobs ?? true;
745
+ const timeout = options?.timeout ?? 30000;
746
+ const cancelOnTimeout = options?.cancelOnTimeout ?? false;
747
+ const cancelTimeout = options?.cancelTimeout ?? 10000;
748
+ this.logger.info("Stopping fleet manager...");
749
+ this.status = "stopping";
750
+ try {
751
+ // Stop the scheduler - don't wait for jobs here, we'll handle it ourselves
752
+ // This stops new triggers from being accepted
753
+ if (this.scheduler) {
754
+ try {
755
+ await this.scheduler.stop({
756
+ waitForJobs,
757
+ timeout,
758
+ });
759
+ }
760
+ catch (error) {
761
+ // Check if it's a scheduler shutdown timeout
762
+ if (error instanceof Error && error.name === "SchedulerShutdownError") {
763
+ if (cancelOnTimeout) {
764
+ // Cancel all running jobs
765
+ this.logger.info("Timeout reached, cancelling running jobs...");
766
+ await this.cancelRunningJobs(cancelTimeout);
767
+ }
768
+ else {
769
+ // Re-throw the error
770
+ this.status = "error";
771
+ this.lastError = error.message;
772
+ throw new FleetManagerShutdownError(error.message, {
773
+ timedOut: true,
774
+ cause: error,
775
+ });
776
+ }
777
+ }
778
+ else {
779
+ throw error;
780
+ }
781
+ }
782
+ }
783
+ // Persist fleet state before completing shutdown
784
+ await this.persistShutdownState();
785
+ // Update state
786
+ this.status = "stopped";
787
+ this.stoppedAt = new Date().toISOString();
788
+ this.logger.info("Fleet manager stopped");
789
+ this.emit("stopped");
790
+ }
791
+ catch (error) {
792
+ this.status = "error";
793
+ this.lastError = error instanceof Error ? error.message : String(error);
794
+ throw error;
795
+ }
796
+ }
797
+ /**
798
+ * Reload configuration without restarting the fleet
799
+ *
800
+ * This method provides hot configuration reload capability:
801
+ * 1. Loads and validates the new configuration
802
+ * 2. If validation fails, keeps the old configuration (fails gracefully)
803
+ * 3. Running jobs continue with their original configuration
804
+ * 4. New jobs will use the new configuration
805
+ * 5. Updates the scheduler with new agent definitions and schedules
806
+ * 6. Emits a 'config:reloaded' event with a list of changes
807
+ *
808
+ * @returns The reload result with change details
809
+ * @throws {InvalidStateError} If the fleet manager is not initialized
810
+ * @throws {FleetManagerConfigError} If the new configuration is invalid (re-thrown after logging)
811
+ *
812
+ * @example
813
+ * ```typescript
814
+ * // Reload configuration
815
+ * const result = await manager.reload();
816
+ * console.log(`Reloaded with ${result.changes.length} changes`);
817
+ *
818
+ * // Subscribe to reload events
819
+ * manager.on('config:reloaded', (payload) => {
820
+ * console.log(`Config reloaded: ${payload.changes.length} changes`);
821
+ * for (const change of payload.changes) {
822
+ * console.log(` ${change.type} ${change.category}: ${change.name}`);
823
+ * }
824
+ * });
825
+ * ```
826
+ */
827
+ async reload() {
828
+ // Validate state - must be at least initialized
829
+ if (this.status === "uninitialized") {
830
+ throw new InvalidStateError("reload", this.status, ["initialized", "starting", "running", "stopping", "stopped"]);
831
+ }
832
+ this.logger.info("Reloading configuration...");
833
+ // Store old config for comparison
834
+ const oldConfig = this.config;
835
+ // Try to load new configuration
836
+ let newConfig;
837
+ try {
838
+ newConfig = await this.loadConfiguration();
839
+ }
840
+ catch (error) {
841
+ // Log the error but don't update config - fail gracefully
842
+ this.logger.error(`Failed to reload configuration: ${error instanceof Error ? error.message : String(error)}`);
843
+ this.logger.info("Keeping existing configuration");
844
+ // Re-throw so caller knows reload failed
845
+ throw error;
846
+ }
847
+ // Compute changes between old and new config
848
+ const changes = this.computeConfigChanges(oldConfig, newConfig);
849
+ // Update the stored configuration
850
+ this.config = newConfig;
851
+ // Update the scheduler with new agents (if scheduler exists and is running)
852
+ if (this.scheduler) {
853
+ this.scheduler.setAgents(newConfig.agents);
854
+ this.logger.debug(`Updated scheduler with ${newConfig.agents.length} agents`);
855
+ }
856
+ const timestamp = new Date().toISOString();
857
+ // Build the reload payload
858
+ const payload = {
859
+ agentCount: newConfig.agents.length,
860
+ agentNames: newConfig.agents.map((a) => a.name),
861
+ configPath: newConfig.configPath,
862
+ changes,
863
+ timestamp,
864
+ };
865
+ // Emit the config:reloaded event
866
+ this.emit("config:reloaded", payload);
867
+ this.logger.info(`Configuration reloaded: ${newConfig.agents.length} agents, ${changes.length} changes`);
868
+ return payload;
869
+ }
870
+ /**
871
+ * Compute the list of changes between old and new configuration
872
+ */
873
+ computeConfigChanges(oldConfig, newConfig) {
874
+ const changes = [];
875
+ const oldAgents = oldConfig?.agents ?? [];
876
+ const newAgents = newConfig.agents;
877
+ const oldAgentNames = new Set(oldAgents.map((a) => a.name));
878
+ const newAgentNames = new Set(newAgents.map((a) => a.name));
879
+ // Find added agents
880
+ for (const agent of newAgents) {
881
+ if (!oldAgentNames.has(agent.name)) {
882
+ changes.push({
883
+ type: "added",
884
+ category: "agent",
885
+ name: agent.name,
886
+ details: agent.description,
887
+ });
888
+ // Also add all schedules for new agents
889
+ if (agent.schedules) {
890
+ for (const scheduleName of Object.keys(agent.schedules)) {
891
+ changes.push({
892
+ type: "added",
893
+ category: "schedule",
894
+ name: `${agent.name}/${scheduleName}`,
895
+ });
896
+ }
897
+ }
898
+ }
899
+ }
900
+ // Find removed agents
901
+ for (const agent of oldAgents) {
902
+ if (!newAgentNames.has(agent.name)) {
903
+ changes.push({
904
+ type: "removed",
905
+ category: "agent",
906
+ name: agent.name,
907
+ });
908
+ // Also mark all schedules as removed
909
+ if (agent.schedules) {
910
+ for (const scheduleName of Object.keys(agent.schedules)) {
911
+ changes.push({
912
+ type: "removed",
913
+ category: "schedule",
914
+ name: `${agent.name}/${scheduleName}`,
915
+ });
916
+ }
917
+ }
918
+ }
919
+ }
920
+ // Find modified agents and schedules
921
+ for (const newAgent of newAgents) {
922
+ const oldAgent = oldAgents.find((a) => a.name === newAgent.name);
923
+ if (!oldAgent) {
924
+ continue; // Already handled as "added"
925
+ }
926
+ // Check for agent-level modifications
927
+ const agentModified = this.isAgentModified(oldAgent, newAgent);
928
+ if (agentModified) {
929
+ changes.push({
930
+ type: "modified",
931
+ category: "agent",
932
+ name: newAgent.name,
933
+ details: agentModified,
934
+ });
935
+ }
936
+ // Check for schedule changes
937
+ const oldScheduleNames = new Set(oldAgent.schedules ? Object.keys(oldAgent.schedules) : []);
938
+ const newScheduleNames = new Set(newAgent.schedules ? Object.keys(newAgent.schedules) : []);
939
+ // Added schedules
940
+ for (const scheduleName of newScheduleNames) {
941
+ if (!oldScheduleNames.has(scheduleName)) {
942
+ changes.push({
943
+ type: "added",
944
+ category: "schedule",
945
+ name: `${newAgent.name}/${scheduleName}`,
946
+ });
947
+ }
948
+ }
949
+ // Removed schedules
950
+ for (const scheduleName of oldScheduleNames) {
951
+ if (!newScheduleNames.has(scheduleName)) {
952
+ changes.push({
953
+ type: "removed",
954
+ category: "schedule",
955
+ name: `${newAgent.name}/${scheduleName}`,
956
+ });
957
+ }
958
+ }
959
+ // Modified schedules
960
+ for (const scheduleName of newScheduleNames) {
961
+ if (oldScheduleNames.has(scheduleName)) {
962
+ const oldSchedule = oldAgent.schedules[scheduleName];
963
+ const newSchedule = newAgent.schedules[scheduleName];
964
+ if (this.isScheduleModified(oldSchedule, newSchedule)) {
965
+ changes.push({
966
+ type: "modified",
967
+ category: "schedule",
968
+ name: `${newAgent.name}/${scheduleName}`,
969
+ details: this.getScheduleModificationDetails(oldSchedule, newSchedule),
970
+ });
971
+ }
972
+ }
973
+ }
974
+ }
975
+ return changes;
976
+ }
977
+ /**
978
+ * Check if an agent configuration has been modified
979
+ * Returns a description of what changed, or null if not modified
980
+ */
981
+ isAgentModified(oldAgent, newAgent) {
982
+ const modifications = [];
983
+ // Check key properties
984
+ if (oldAgent.description !== newAgent.description) {
985
+ modifications.push("description");
986
+ }
987
+ if (oldAgent.model !== newAgent.model) {
988
+ modifications.push("model");
989
+ }
990
+ if (oldAgent.max_turns !== newAgent.max_turns) {
991
+ modifications.push("max_turns");
992
+ }
993
+ if (oldAgent.system_prompt !== newAgent.system_prompt) {
994
+ modifications.push("system_prompt");
995
+ }
996
+ // Check workspace
997
+ const oldWorkspace = typeof oldAgent.workspace === "string"
998
+ ? oldAgent.workspace
999
+ : oldAgent.workspace?.root;
1000
+ const newWorkspace = typeof newAgent.workspace === "string"
1001
+ ? newAgent.workspace
1002
+ : newAgent.workspace?.root;
1003
+ if (oldWorkspace !== newWorkspace) {
1004
+ modifications.push("workspace");
1005
+ }
1006
+ // Check instances
1007
+ const oldMaxConcurrent = oldAgent.instances?.max_concurrent ?? 1;
1008
+ const newMaxConcurrent = newAgent.instances?.max_concurrent ?? 1;
1009
+ if (oldMaxConcurrent !== newMaxConcurrent) {
1010
+ modifications.push("max_concurrent");
1011
+ }
1012
+ return modifications.length > 0 ? modifications.join(", ") : null;
1013
+ }
1014
+ /**
1015
+ * Check if a schedule configuration has been modified
1016
+ */
1017
+ isScheduleModified(oldSchedule, newSchedule) {
1018
+ return (oldSchedule.type !== newSchedule.type ||
1019
+ oldSchedule.interval !== newSchedule.interval ||
1020
+ oldSchedule.expression !== newSchedule.expression ||
1021
+ oldSchedule.prompt !== newSchedule.prompt);
1022
+ }
1023
+ /**
1024
+ * Get a description of what changed in a schedule
1025
+ */
1026
+ getScheduleModificationDetails(oldSchedule, newSchedule) {
1027
+ const details = [];
1028
+ if (oldSchedule.type !== newSchedule.type) {
1029
+ details.push(`type: ${oldSchedule.type} → ${newSchedule.type}`);
1030
+ }
1031
+ if (oldSchedule.interval !== newSchedule.interval) {
1032
+ details.push(`interval: ${oldSchedule.interval ?? "none"} → ${newSchedule.interval ?? "none"}`);
1033
+ }
1034
+ if (oldSchedule.expression !== newSchedule.expression) {
1035
+ details.push(`expression: ${oldSchedule.expression ?? "none"} → ${newSchedule.expression ?? "none"}`);
1036
+ }
1037
+ if (oldSchedule.prompt !== newSchedule.prompt) {
1038
+ details.push("prompt changed");
1039
+ }
1040
+ return details.join("; ");
1041
+ }
1042
+ /**
1043
+ * Cancel all running jobs during shutdown
1044
+ *
1045
+ * @param cancelTimeout - Timeout for each job cancellation
1046
+ */
1047
+ async cancelRunningJobs(cancelTimeout) {
1048
+ const jobsDir = join(this.stateDir, "jobs");
1049
+ // Get all running jobs from the fleet status
1050
+ const agentInfoList = await this.getAgentInfo();
1051
+ const runningJobIds = [];
1052
+ for (const agent of agentInfoList) {
1053
+ if (agent.currentJobId) {
1054
+ runningJobIds.push(agent.currentJobId);
1055
+ }
1056
+ }
1057
+ if (runningJobIds.length === 0) {
1058
+ this.logger.debug("No running jobs to cancel");
1059
+ return;
1060
+ }
1061
+ this.logger.info(`Cancelling ${runningJobIds.length} running job(s)...`);
1062
+ // Cancel all jobs in parallel
1063
+ const cancelPromises = runningJobIds.map(async (jobId) => {
1064
+ try {
1065
+ const result = await this.cancelJob(jobId, { timeout: cancelTimeout });
1066
+ this.logger.debug(`Cancelled job ${jobId}: ${result.terminationType}`);
1067
+ }
1068
+ catch (error) {
1069
+ this.logger.warn(`Failed to cancel job ${jobId}: ${error.message}`);
1070
+ }
1071
+ });
1072
+ await Promise.all(cancelPromises);
1073
+ this.logger.info("All jobs cancelled");
1074
+ }
1075
+ /**
1076
+ * Persist shutdown state to ensure all state is saved before completing
1077
+ */
1078
+ async persistShutdownState() {
1079
+ if (!this.stateDirInfo) {
1080
+ return;
1081
+ }
1082
+ // Persist fleet state
1083
+ const { writeFleetState } = await import("../state/fleet-state.js");
1084
+ // Read current state and update with stopped status
1085
+ const currentState = await this.readFleetStateSnapshot();
1086
+ // Update fleet-level state
1087
+ const updatedState = {
1088
+ ...currentState,
1089
+ fleet: {
1090
+ ...currentState.fleet,
1091
+ stoppedAt: new Date().toISOString(),
1092
+ },
1093
+ };
1094
+ try {
1095
+ await writeFleetState(this.stateDirInfo.stateFile, updatedState);
1096
+ this.logger.debug("Fleet state persisted");
1097
+ }
1098
+ catch (error) {
1099
+ this.logger.warn(`Failed to persist fleet state: ${error.message}`);
1100
+ }
1101
+ }
1102
+ // ===========================================================================
1103
+ // Manual Triggering (US-5)
1104
+ // ===========================================================================
1105
+ /**
1106
+ * Manually trigger an agent outside its normal schedule
1107
+ *
1108
+ * This method allows you to trigger an agent on-demand for testing or
1109
+ * handling urgent situations. You can optionally specify a schedule to use
1110
+ * for configuration (prompt, work source, etc.) or pass runtime options
1111
+ * to override defaults.
1112
+ *
1113
+ * @param agentName - Name of the agent to trigger
1114
+ * @param scheduleName - Optional schedule name to use for configuration
1115
+ * @param options - Optional runtime options to override defaults
1116
+ * @returns The created job information
1117
+ * @throws {InvalidStateError} If the fleet manager is not initialized
1118
+ * @throws {AgentNotFoundError} If the agent doesn't exist
1119
+ * @throws {ScheduleNotFoundError} If the specified schedule doesn't exist
1120
+ * @throws {ConcurrencyLimitError} If the agent is at capacity and bypassConcurrencyLimit is false
1121
+ *
1122
+ * @example
1123
+ * ```typescript
1124
+ * // Trigger with agent defaults
1125
+ * const job = await manager.trigger('my-agent');
1126
+ *
1127
+ * // Trigger a specific schedule
1128
+ * const job = await manager.trigger('my-agent', 'hourly');
1129
+ *
1130
+ * // Trigger with custom prompt
1131
+ * const job = await manager.trigger('my-agent', undefined, {
1132
+ * prompt: 'Review the latest security updates',
1133
+ * });
1134
+ *
1135
+ * // Force trigger even at capacity
1136
+ * const job = await manager.trigger('my-agent', undefined, {
1137
+ * bypassConcurrencyLimit: true,
1138
+ * });
1139
+ * ```
1140
+ */
1141
+ async trigger(agentName, scheduleName, options) {
1142
+ // Validate state - must be at least initialized
1143
+ if (this.status === "uninitialized") {
1144
+ throw new InvalidStateError("trigger", this.status, ["initialized", "running", "stopped"]);
1145
+ }
1146
+ // Find the agent
1147
+ const agents = this.config?.agents ?? [];
1148
+ const agent = agents.find((a) => a.name === agentName);
1149
+ if (!agent) {
1150
+ throw new AgentNotFoundError(agentName, {
1151
+ availableAgents: agents.map((a) => a.name),
1152
+ });
1153
+ }
1154
+ // If a schedule name is provided, validate it exists
1155
+ let schedule;
1156
+ if (scheduleName) {
1157
+ if (!agent.schedules || !(scheduleName in agent.schedules)) {
1158
+ const availableSchedules = agent.schedules
1159
+ ? Object.keys(agent.schedules)
1160
+ : [];
1161
+ throw new ScheduleNotFoundError(agentName, scheduleName, {
1162
+ availableSchedules,
1163
+ });
1164
+ }
1165
+ schedule = agent.schedules[scheduleName];
1166
+ }
1167
+ // Check concurrency limits unless bypassed
1168
+ if (!options?.bypassConcurrencyLimit) {
1169
+ const maxConcurrent = agent.instances?.max_concurrent ?? 1;
1170
+ const runningCount = this.scheduler?.getRunningJobCount(agentName) ?? 0;
1171
+ if (runningCount >= maxConcurrent) {
1172
+ throw new ConcurrencyLimitError(agentName, runningCount, maxConcurrent);
1173
+ }
1174
+ }
1175
+ // Determine the prompt to use (priority: options > schedule > agent default)
1176
+ const prompt = options?.prompt ?? schedule?.prompt ?? undefined;
1177
+ // Create the job
1178
+ const jobsDir = join(this.stateDir, "jobs");
1179
+ const job = await createJob(jobsDir, {
1180
+ agent: agentName,
1181
+ trigger_type: "manual",
1182
+ schedule: scheduleName ?? null,
1183
+ prompt: prompt ?? null,
1184
+ });
1185
+ const timestamp = new Date().toISOString();
1186
+ this.logger.info(`Manually triggered ${agentName}${scheduleName ? `/${scheduleName}` : ""} - job ${job.id}`);
1187
+ // Emit job:created event
1188
+ this.emit("job:created", {
1189
+ job,
1190
+ agentName,
1191
+ scheduleName: scheduleName ?? null,
1192
+ timestamp,
1193
+ });
1194
+ // Build and return the result
1195
+ const result = {
1196
+ jobId: job.id,
1197
+ agentName,
1198
+ scheduleName: scheduleName ?? null,
1199
+ startedAt: job.started_at,
1200
+ prompt,
1201
+ };
1202
+ return result;
1203
+ }
1204
+ // ===========================================================================
1205
+ // Job Control (US-6)
1206
+ // ===========================================================================
1207
+ /**
1208
+ * Cancel a running job gracefully
1209
+ *
1210
+ * This method cancels a running job by first sending SIGTERM to allow
1211
+ * graceful shutdown. If the job doesn't terminate within the timeout,
1212
+ * it will be forcefully killed with SIGKILL.
1213
+ *
1214
+ * @param jobId - ID of the job to cancel
1215
+ * @param options - Optional cancellation options
1216
+ * @param options.timeout - Time in ms to wait for graceful shutdown (default: 10000)
1217
+ * @returns Result of the cancellation operation
1218
+ * @throws {InvalidStateError} If the fleet manager is not initialized
1219
+ * @throws {JobNotFoundError} If the job doesn't exist
1220
+ *
1221
+ * @example
1222
+ * ```typescript
1223
+ * // Cancel with default timeout
1224
+ * const result = await manager.cancelJob('job-2024-01-15-abc123');
1225
+ * console.log(`Job cancelled: ${result.terminationType}`);
1226
+ *
1227
+ * // Cancel with custom timeout
1228
+ * const result = await manager.cancelJob('job-2024-01-15-abc123', {
1229
+ * timeout: 30000, // 30 seconds
1230
+ * });
1231
+ * ```
1232
+ */
1233
+ async cancelJob(jobId, options) {
1234
+ // Validate state - must be at least initialized
1235
+ if (this.status === "uninitialized") {
1236
+ throw new InvalidStateError("cancelJob", this.status, ["initialized", "running", "stopped"]);
1237
+ }
1238
+ const jobsDir = join(this.stateDir, "jobs");
1239
+ const timeout = options?.timeout ?? 10000; // Default 10 seconds
1240
+ // Get the job to verify it exists and check its status
1241
+ const job = await getJob(jobsDir, jobId, { logger: this.logger });
1242
+ if (!job) {
1243
+ throw new JobNotFoundError(jobId);
1244
+ }
1245
+ const timestamp = new Date().toISOString();
1246
+ let terminationType;
1247
+ let durationSeconds;
1248
+ // If job is already not running, return early
1249
+ if (job.status !== "running" && job.status !== "pending") {
1250
+ this.logger.info(`Job ${jobId} is already ${job.status}, no cancellation needed`);
1251
+ terminationType = 'already_stopped';
1252
+ // Calculate duration if we have finished_at
1253
+ if (job.finished_at) {
1254
+ const startTime = new Date(job.started_at).getTime();
1255
+ const endTime = new Date(job.finished_at).getTime();
1256
+ durationSeconds = Math.round((endTime - startTime) / 1000);
1257
+ }
1258
+ return {
1259
+ jobId,
1260
+ success: true,
1261
+ terminationType,
1262
+ canceledAt: timestamp,
1263
+ };
1264
+ }
1265
+ // Calculate duration
1266
+ const startTime = new Date(job.started_at).getTime();
1267
+ const endTime = new Date(timestamp).getTime();
1268
+ durationSeconds = Math.round((endTime - startTime) / 1000);
1269
+ // Try to cancel via the scheduler if it has process tracking
1270
+ // For now, we'll update the job status directly since we don't have
1271
+ // direct process control yet. In a full implementation, this would
1272
+ // send SIGTERM to the process, wait, then SIGKILL if needed.
1273
+ // Note: The scheduler/executor would need to track job processes
1274
+ // and provide an API to cancel them. For this implementation,
1275
+ // we'll update the job state and emit events, assuming the executor
1276
+ // monitors the job status and handles cancellation.
1277
+ this.logger.info(`Cancelling job ${jobId} for agent ${job.agent}`);
1278
+ // Update job status to cancelled
1279
+ try {
1280
+ await updateJob(jobsDir, jobId, {
1281
+ status: "cancelled",
1282
+ exit_reason: "cancelled",
1283
+ finished_at: timestamp,
1284
+ });
1285
+ // Assume graceful termination for now
1286
+ // In a full implementation, this would be determined by whether
1287
+ // the process responded to SIGTERM or required SIGKILL
1288
+ terminationType = 'graceful';
1289
+ }
1290
+ catch (error) {
1291
+ this.logger.error(`Failed to update job status: ${error.message}`);
1292
+ throw new JobCancelError(jobId, 'process_error', {
1293
+ cause: error,
1294
+ });
1295
+ }
1296
+ // Emit job:cancelled event
1297
+ const updatedJob = await getJob(jobsDir, jobId, { logger: this.logger });
1298
+ if (updatedJob) {
1299
+ this.emit("job:cancelled", {
1300
+ job: updatedJob,
1301
+ agentName: job.agent,
1302
+ terminationType,
1303
+ durationSeconds,
1304
+ timestamp,
1305
+ });
1306
+ }
1307
+ this.logger.info(`Job ${jobId} cancelled (${terminationType}) after ${durationSeconds}s`);
1308
+ return {
1309
+ jobId,
1310
+ success: true,
1311
+ terminationType,
1312
+ canceledAt: timestamp,
1313
+ };
1314
+ }
1315
+ /**
1316
+ * Fork a job to create a new job based on an existing one
1317
+ *
1318
+ * This method creates a new job that is based on an existing job's
1319
+ * configuration. The new job will have the same agent and can optionally
1320
+ * have modifications applied (different prompt, schedule, etc.).
1321
+ *
1322
+ * If the original job has a session ID, the new job will fork from that
1323
+ * session, preserving conversation context.
1324
+ *
1325
+ * @param jobId - ID of the job to fork
1326
+ * @param modifications - Optional modifications to apply to the forked job
1327
+ * @returns Result of the fork operation including the new job ID
1328
+ * @throws {InvalidStateError} If the fleet manager is not initialized
1329
+ * @throws {JobNotFoundError} If the original job doesn't exist
1330
+ * @throws {JobForkError} If the job cannot be forked (e.g., no session)
1331
+ *
1332
+ * @example
1333
+ * ```typescript
1334
+ * // Fork with same configuration
1335
+ * const result = await manager.forkJob('job-2024-01-15-abc123');
1336
+ * console.log(`Forked to: ${result.jobId}`);
1337
+ *
1338
+ * // Fork with modified prompt
1339
+ * const result = await manager.forkJob('job-2024-01-15-abc123', {
1340
+ * prompt: 'Continue the previous task but focus on testing',
1341
+ * });
1342
+ *
1343
+ * // Fork with different schedule
1344
+ * const result = await manager.forkJob('job-2024-01-15-abc123', {
1345
+ * schedule: 'nightly',
1346
+ * });
1347
+ * ```
1348
+ */
1349
+ async forkJob(jobId, modifications) {
1350
+ // Validate state - must be at least initialized
1351
+ if (this.status === "uninitialized") {
1352
+ throw new InvalidStateError("forkJob", this.status, ["initialized", "running", "stopped"]);
1353
+ }
1354
+ const jobsDir = join(this.stateDir, "jobs");
1355
+ // Get the original job
1356
+ const originalJob = await getJob(jobsDir, jobId, { logger: this.logger });
1357
+ if (!originalJob) {
1358
+ throw new JobForkError(jobId, 'job_not_found');
1359
+ }
1360
+ // Verify the agent exists in config
1361
+ const agents = this.config?.agents ?? [];
1362
+ const agent = agents.find((a) => a.name === originalJob.agent);
1363
+ if (!agent) {
1364
+ throw new JobForkError(jobId, 'agent_not_found', {
1365
+ message: `Agent "${originalJob.agent}" for job "${jobId}" not found in current configuration`,
1366
+ });
1367
+ }
1368
+ // Determine the prompt to use (priority: modifications > original job)
1369
+ const prompt = modifications?.prompt ?? originalJob.prompt ?? undefined;
1370
+ // Determine the schedule to use
1371
+ const scheduleName = modifications?.schedule ?? originalJob.schedule ?? undefined;
1372
+ // Create the new job
1373
+ const timestamp = new Date().toISOString();
1374
+ const newJob = await createJob(jobsDir, {
1375
+ agent: originalJob.agent,
1376
+ trigger_type: "fork",
1377
+ schedule: scheduleName ?? null,
1378
+ prompt: prompt ?? null,
1379
+ forked_from: jobId,
1380
+ });
1381
+ this.logger.info(`Forked job ${jobId} to new job ${newJob.id} for agent ${originalJob.agent}`);
1382
+ // Emit job:created event
1383
+ this.emit("job:created", {
1384
+ job: newJob,
1385
+ agentName: originalJob.agent,
1386
+ scheduleName: scheduleName ?? undefined,
1387
+ timestamp,
1388
+ });
1389
+ // Emit job:forked event
1390
+ this.emit("job:forked", {
1391
+ job: newJob,
1392
+ originalJob,
1393
+ agentName: originalJob.agent,
1394
+ timestamp,
1395
+ });
1396
+ return {
1397
+ jobId: newJob.id,
1398
+ forkedFromJobId: jobId,
1399
+ agentName: originalJob.agent,
1400
+ startedAt: newJob.started_at,
1401
+ prompt,
1402
+ };
1403
+ }
1404
+ // ===========================================================================
1405
+ // Log Streaming (US-11)
1406
+ // ===========================================================================
1407
+ /**
1408
+ * Stream all fleet logs as an async iterable
1409
+ *
1410
+ * Provides a unified stream of logs from all sources in the fleet including
1411
+ * agents, jobs, and the scheduler. Logs can be filtered by level and optionally
1412
+ * by agent or job.
1413
+ *
1414
+ * For completed jobs, this will replay their history (if includeHistory is true)
1415
+ * before streaming new logs from running jobs.
1416
+ *
1417
+ * @param options - Options for filtering and configuring the stream
1418
+ * @returns An async iterable of LogEntry objects
1419
+ *
1420
+ * @example
1421
+ * ```typescript
1422
+ * // Stream all info+ logs
1423
+ * for await (const log of manager.streamLogs()) {
1424
+ * console.log(`[${log.level}] ${log.message}`);
1425
+ * }
1426
+ *
1427
+ * // Stream only errors for a specific agent
1428
+ * for await (const log of manager.streamLogs({
1429
+ * level: 'error',
1430
+ * agentName: 'my-agent',
1431
+ * })) {
1432
+ * console.error(log.message);
1433
+ * }
1434
+ * ```
1435
+ */
1436
+ async *streamLogs(options) {
1437
+ const level = options?.level ?? "info";
1438
+ const includeHistory = options?.includeHistory ?? true;
1439
+ const historyLimit = options?.historyLimit ?? 1000;
1440
+ const agentFilter = options?.agentName;
1441
+ const jobFilter = options?.jobId;
1442
+ const jobsDir = join(this.stateDir, "jobs");
1443
+ const { readJobOutputAll } = await import("../state/job-output.js");
1444
+ // Replay historical logs if requested
1445
+ if (includeHistory) {
1446
+ // Get jobs to replay history from
1447
+ const jobsResult = await listJobs(jobsDir, agentFilter ? { agent: agentFilter } : {}, { logger: this.logger });
1448
+ // Filter by job ID if specified
1449
+ let jobs = jobsResult.jobs;
1450
+ if (jobFilter) {
1451
+ jobs = jobs.filter((j) => j.id === jobFilter);
1452
+ }
1453
+ // Sort by started_at ascending to replay in chronological order
1454
+ jobs.sort((a, b) => new Date(a.started_at).getTime() - new Date(b.started_at).getTime());
1455
+ let yielded = 0;
1456
+ for (const job of jobs) {
1457
+ if (yielded >= historyLimit)
1458
+ break;
1459
+ // Read job output and convert to log entries
1460
+ const output = await readJobOutputAll(jobsDir, job.id, {
1461
+ skipInvalidLines: true,
1462
+ logger: this.logger,
1463
+ });
1464
+ for (const msg of output) {
1465
+ if (yielded >= historyLimit)
1466
+ break;
1467
+ const logEntry = this.jobOutputToLogEntry(job, msg);
1468
+ if (this.shouldYieldLog(logEntry, level, agentFilter, jobFilter)) {
1469
+ yield logEntry;
1470
+ yielded++;
1471
+ }
1472
+ }
1473
+ }
1474
+ }
1475
+ // For running jobs, subscribe to job:output events
1476
+ const outputQueue = [];
1477
+ let resolveWait = null;
1478
+ let stopped = false;
1479
+ const outputHandler = (payload) => {
1480
+ if (stopped)
1481
+ return;
1482
+ const logEntry = {
1483
+ timestamp: payload.timestamp,
1484
+ level: "info",
1485
+ source: "job",
1486
+ agentName: payload.agentName,
1487
+ jobId: payload.jobId,
1488
+ message: payload.output,
1489
+ };
1490
+ if (this.shouldYieldLog(logEntry, level, agentFilter, jobFilter)) {
1491
+ outputQueue.push(logEntry);
1492
+ if (resolveWait) {
1493
+ resolveWait();
1494
+ resolveWait = null;
1495
+ }
1496
+ }
1497
+ };
1498
+ this.on("job:output", outputHandler);
1499
+ try {
1500
+ // Yield queued entries as they arrive
1501
+ while (!stopped) {
1502
+ while (outputQueue.length > 0) {
1503
+ const entry = outputQueue.shift();
1504
+ yield entry;
1505
+ }
1506
+ // Wait for more entries
1507
+ await new Promise((resolve) => {
1508
+ resolveWait = resolve;
1509
+ // Add timeout to prevent hanging forever
1510
+ setTimeout(resolve, 1000);
1511
+ });
1512
+ }
1513
+ }
1514
+ finally {
1515
+ stopped = true;
1516
+ this.off("job:output", outputHandler);
1517
+ }
1518
+ }
1519
+ /**
1520
+ * Stream output from a specific job as an async iterable
1521
+ *
1522
+ * Provides a stream of log entries for a specific job. For completed jobs,
1523
+ * this will replay the job's history and then complete. For running jobs,
1524
+ * it will continue streaming until the job completes.
1525
+ *
1526
+ * @param jobId - The ID of the job to stream output from
1527
+ * @returns An async iterable of LogEntry objects
1528
+ * @throws {JobNotFoundError} If the job doesn't exist
1529
+ *
1530
+ * @example
1531
+ * ```typescript
1532
+ * // Stream job output
1533
+ * for await (const log of manager.streamJobOutput('job-2024-01-15-abc123')) {
1534
+ * console.log(`[${log.level}] ${log.message}`);
1535
+ * }
1536
+ * ```
1537
+ */
1538
+ async *streamJobOutput(jobId) {
1539
+ const jobsDir = join(this.stateDir, "jobs");
1540
+ // Verify job exists
1541
+ const job = await getJob(jobsDir, jobId, { logger: this.logger });
1542
+ if (!job) {
1543
+ throw new JobNotFoundError(jobId);
1544
+ }
1545
+ const { readJobOutputAll, getJobOutputPath } = await import("../state/job-output.js");
1546
+ const { watch } = await import("node:fs");
1547
+ const { stat } = await import("node:fs/promises");
1548
+ const { createReadStream } = await import("node:fs");
1549
+ const { createInterface } = await import("node:readline");
1550
+ const outputPath = getJobOutputPath(jobsDir, jobId);
1551
+ // First, replay all existing output
1552
+ const existingOutput = await readJobOutputAll(jobsDir, jobId, {
1553
+ skipInvalidLines: true,
1554
+ logger: this.logger,
1555
+ });
1556
+ for (const msg of existingOutput) {
1557
+ yield this.jobOutputToLogEntry(job, msg);
1558
+ }
1559
+ // If job is already completed, we're done
1560
+ if (job.status !== "running" && job.status !== "pending") {
1561
+ return;
1562
+ }
1563
+ // For running jobs, watch for new output
1564
+ const outputQueue = [];
1565
+ let resolveWait = null;
1566
+ let stopped = false;
1567
+ let lastReadPosition = 0;
1568
+ // Get current file position
1569
+ try {
1570
+ const stats = await stat(outputPath);
1571
+ lastReadPosition = stats.size;
1572
+ }
1573
+ catch {
1574
+ // File doesn't exist yet
1575
+ }
1576
+ // Watch for file changes
1577
+ let watcher = null;
1578
+ try {
1579
+ watcher = watch(outputPath, async (eventType) => {
1580
+ if (stopped || eventType !== "change")
1581
+ return;
1582
+ try {
1583
+ const currentStats = await stat(outputPath);
1584
+ if (currentStats.size > lastReadPosition) {
1585
+ // Read new content
1586
+ const fileStream = createReadStream(outputPath, {
1587
+ encoding: "utf-8",
1588
+ start: lastReadPosition,
1589
+ });
1590
+ const rl = createInterface({
1591
+ input: fileStream,
1592
+ crlfDelay: Infinity,
1593
+ });
1594
+ for await (const line of rl) {
1595
+ if (stopped)
1596
+ break;
1597
+ const trimmedLine = line.trim();
1598
+ if (trimmedLine === "")
1599
+ continue;
1600
+ try {
1601
+ const parsed = JSON.parse(trimmedLine);
1602
+ const logEntry = this.jobOutputToLogEntry(job, parsed);
1603
+ outputQueue.push(logEntry);
1604
+ if (resolveWait) {
1605
+ resolveWait();
1606
+ resolveWait = null;
1607
+ }
1608
+ }
1609
+ catch {
1610
+ // Skip malformed lines
1611
+ }
1612
+ }
1613
+ rl.close();
1614
+ fileStream.destroy();
1615
+ lastReadPosition = currentStats.size;
1616
+ }
1617
+ }
1618
+ catch (err) {
1619
+ this.logger.warn(`Error reading output file: ${err.message}`);
1620
+ }
1621
+ });
1622
+ }
1623
+ catch {
1624
+ // Can't watch file - might not exist yet
1625
+ }
1626
+ // Poll for job completion
1627
+ const checkJobComplete = async () => {
1628
+ const currentJob = await getJob(jobsDir, jobId, { logger: this.logger });
1629
+ return (!currentJob ||
1630
+ (currentJob.status !== "running" && currentJob.status !== "pending"));
1631
+ };
1632
+ try {
1633
+ // Yield queued entries as they arrive
1634
+ while (!stopped) {
1635
+ while (outputQueue.length > 0) {
1636
+ const entry = outputQueue.shift();
1637
+ yield entry;
1638
+ }
1639
+ // Check if job is complete
1640
+ if (await checkJobComplete()) {
1641
+ stopped = true;
1642
+ break;
1643
+ }
1644
+ // Wait for more entries
1645
+ await new Promise((resolve) => {
1646
+ resolveWait = resolve;
1647
+ setTimeout(resolve, 1000);
1648
+ });
1649
+ }
1650
+ }
1651
+ finally {
1652
+ stopped = true;
1653
+ if (watcher) {
1654
+ watcher.close();
1655
+ }
1656
+ }
1657
+ }
1658
+ /**
1659
+ * Stream logs for a specific agent as an async iterable
1660
+ *
1661
+ * Provides a stream of log entries for all jobs belonging to a specific agent.
1662
+ * For completed jobs, this will replay their history. For running jobs, it
1663
+ * will continue streaming until the iterator is stopped.
1664
+ *
1665
+ * @param agentName - The name of the agent to stream logs for
1666
+ * @returns An async iterable of LogEntry objects
1667
+ * @throws {AgentNotFoundError} If the agent doesn't exist in the configuration
1668
+ *
1669
+ * @example
1670
+ * ```typescript
1671
+ * // Stream all logs for an agent
1672
+ * for await (const log of manager.streamAgentLogs('my-agent')) {
1673
+ * console.log(`[${log.jobId}] ${log.message}`);
1674
+ * }
1675
+ * ```
1676
+ */
1677
+ async *streamAgentLogs(agentName) {
1678
+ // Verify agent exists
1679
+ const agents = this.config?.agents ?? [];
1680
+ const agent = agents.find((a) => a.name === agentName);
1681
+ if (!agent) {
1682
+ throw new AgentNotFoundError(agentName, {
1683
+ availableAgents: agents.map((a) => a.name),
1684
+ });
1685
+ }
1686
+ // Delegate to streamLogs with agent filter
1687
+ yield* this.streamLogs({
1688
+ agentName,
1689
+ includeHistory: true,
1690
+ });
1691
+ }
1692
+ // ===========================================================================
1693
+ // Log Streaming Helpers (US-11)
1694
+ // ===========================================================================
1695
+ /**
1696
+ * Convert a job output message to a LogEntry
1697
+ */
1698
+ jobOutputToLogEntry(job, msg) {
1699
+ // Determine log level based on message type
1700
+ let level = "info";
1701
+ if (msg.type === "error") {
1702
+ level = "error";
1703
+ }
1704
+ else if (msg.type === "system") {
1705
+ level = "debug";
1706
+ }
1707
+ return {
1708
+ timestamp: msg.timestamp ?? new Date().toISOString(),
1709
+ level,
1710
+ source: "job",
1711
+ agentName: job.agent,
1712
+ jobId: job.id,
1713
+ scheduleName: job.schedule ?? undefined,
1714
+ message: msg.content ?? "",
1715
+ data: { type: msg.type },
1716
+ };
1717
+ }
1718
+ /**
1719
+ * Determine if a log entry should be yielded based on filters
1720
+ */
1721
+ shouldYieldLog(entry, minLevel, agentFilter, jobFilter) {
1722
+ // Check log level
1723
+ const levelOrder = {
1724
+ debug: 0,
1725
+ info: 1,
1726
+ warn: 2,
1727
+ error: 3,
1728
+ };
1729
+ if (levelOrder[entry.level] < levelOrder[minLevel]) {
1730
+ return false;
1731
+ }
1732
+ // Check agent filter
1733
+ if (agentFilter && entry.agentName !== agentFilter) {
1734
+ return false;
1735
+ }
1736
+ // Check job filter
1737
+ if (jobFilter && entry.jobId !== jobFilter) {
1738
+ return false;
1739
+ }
1740
+ return true;
1741
+ }
1742
+ // ===========================================================================
1743
+ // Private Helper Methods
1744
+ // ===========================================================================
1745
+ /**
1746
+ * Load configuration with proper error handling
1747
+ */
1748
+ async loadConfiguration() {
1749
+ try {
1750
+ return await loadConfig(this.configPath);
1751
+ }
1752
+ catch (error) {
1753
+ if (error instanceof ConfigNotFoundError) {
1754
+ throw new FleetManagerConfigError(`Configuration file not found. ${error.message}`, this.configPath, { cause: error });
1755
+ }
1756
+ if (error instanceof ConfigError) {
1757
+ throw new FleetManagerConfigError(`Invalid configuration: ${error.message}`, this.configPath, { cause: error });
1758
+ }
1759
+ throw new FleetManagerConfigError(`Failed to load configuration: ${error instanceof Error ? error.message : String(error)}`, this.configPath, { cause: error instanceof Error ? error : undefined });
1760
+ }
1761
+ }
1762
+ /**
1763
+ * Initialize state directory with proper error handling
1764
+ */
1765
+ async initializeStateDir() {
1766
+ try {
1767
+ return await initStateDirectory({ path: this.stateDir });
1768
+ }
1769
+ catch (error) {
1770
+ throw new FleetManagerStateDirError(`Failed to initialize state directory: ${error instanceof Error ? error.message : String(error)}`, this.stateDir, { cause: error instanceof Error ? error : undefined });
1771
+ }
1772
+ }
1773
+ /**
1774
+ * Start the scheduler asynchronously (don't block on the loop)
1775
+ */
1776
+ startSchedulerAsync(agents) {
1777
+ // Start the scheduler loop in the background
1778
+ // The scheduler.start() method runs the loop and returns when stopped
1779
+ this.scheduler.start(agents).catch((error) => {
1780
+ // Only handle errors if we're still supposed to be running
1781
+ if (this.status === "running" || this.status === "starting") {
1782
+ this.logger.error(`Scheduler error: ${error instanceof Error ? error.message : String(error)}`);
1783
+ this.status = "error";
1784
+ this.lastError = error instanceof Error ? error.message : String(error);
1785
+ this.emit("error", error instanceof Error ? error : new Error(String(error)));
1786
+ }
1787
+ });
1788
+ }
1789
+ /**
1790
+ * Handle schedule trigger callback from scheduler
1791
+ */
1792
+ async handleScheduleTrigger(info) {
1793
+ const { agent, scheduleName, schedule } = info;
1794
+ const timestamp = new Date().toISOString();
1795
+ this.logger.info(`Triggering ${agent.name}/${scheduleName}`);
1796
+ // Emit legacy event for backwards compatibility
1797
+ this.emit("schedule:trigger", agent.name, scheduleName);
1798
+ // Emit new typed event with full payload
1799
+ this.emit("schedule:triggered", {
1800
+ agentName: agent.name,
1801
+ scheduleName,
1802
+ schedule,
1803
+ timestamp,
1804
+ });
1805
+ try {
1806
+ // For now, just log the trigger
1807
+ // In future PRDs, this will actually run the agent via the runner
1808
+ this.logger.debug(`Schedule ${scheduleName} triggered for agent ${agent.name} ` +
1809
+ `(type: ${schedule.type}, prompt: ${schedule.prompt ?? "default"})`);
1810
+ // Emit legacy completion event for backwards compatibility
1811
+ this.emit("schedule:complete", agent.name, scheduleName);
1812
+ }
1813
+ catch (error) {
1814
+ const err = error instanceof Error ? error : new Error(String(error));
1815
+ this.logger.error(`Error in ${agent.name}/${scheduleName}: ${err.message}`);
1816
+ // Emit legacy error event for backwards compatibility
1817
+ this.emit("schedule:error", agent.name, scheduleName, err);
1818
+ throw error;
1819
+ }
1820
+ }
1821
+ // ===========================================================================
1822
+ // Event Emission Helpers (US-2)
1823
+ // ===========================================================================
1824
+ /**
1825
+ * Emit a config:reloaded event
1826
+ *
1827
+ * Called when configuration is hot-reloaded.
1828
+ */
1829
+ emitConfigReloaded(payload) {
1830
+ this.emit("config:reloaded", payload);
1831
+ }
1832
+ /**
1833
+ * Emit an agent:started event
1834
+ *
1835
+ * Called when an agent is started/registered with the fleet.
1836
+ */
1837
+ emitAgentStarted(payload) {
1838
+ this.emit("agent:started", payload);
1839
+ }
1840
+ /**
1841
+ * Emit an agent:stopped event
1842
+ *
1843
+ * Called when an agent is stopped/unregistered from the fleet.
1844
+ */
1845
+ emitAgentStopped(payload) {
1846
+ this.emit("agent:stopped", payload);
1847
+ }
1848
+ /**
1849
+ * Emit a schedule:skipped event
1850
+ *
1851
+ * Called when a schedule check is skipped (already running, disabled, etc.).
1852
+ */
1853
+ emitScheduleSkipped(payload) {
1854
+ this.emit("schedule:skipped", payload);
1855
+ }
1856
+ /**
1857
+ * Emit a job:created event
1858
+ *
1859
+ * Called when a new job is created.
1860
+ */
1861
+ emitJobCreated(payload) {
1862
+ this.emit("job:created", payload);
1863
+ }
1864
+ /**
1865
+ * Emit a job:output event
1866
+ *
1867
+ * Called when a job produces output during execution.
1868
+ * This enables real-time streaming of output to UIs.
1869
+ */
1870
+ emitJobOutput(payload) {
1871
+ this.emit("job:output", payload);
1872
+ }
1873
+ /**
1874
+ * Emit a job:completed event
1875
+ *
1876
+ * Called when a job completes successfully.
1877
+ */
1878
+ emitJobCompleted(payload) {
1879
+ this.emit("job:completed", payload);
1880
+ }
1881
+ /**
1882
+ * Emit a job:failed event
1883
+ *
1884
+ * Called when a job fails.
1885
+ */
1886
+ emitJobFailed(payload) {
1887
+ this.emit("job:failed", payload);
1888
+ }
1889
+ /**
1890
+ * Emit a job:cancelled event (US-6)
1891
+ *
1892
+ * Called when a job is cancelled.
1893
+ */
1894
+ emitJobCancelled(payload) {
1895
+ this.emit("job:cancelled", payload);
1896
+ }
1897
+ /**
1898
+ * Emit a job:forked event (US-6)
1899
+ *
1900
+ * Called when a job is forked to create a new job.
1901
+ */
1902
+ emitJobForked(payload) {
1903
+ this.emit("job:forked", payload);
1904
+ }
1905
+ }
1906
+ //# sourceMappingURL=fleet-manager.js.map