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