@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,1166 @@
1
+ /**
2
+ * GitHub Work Source Adapter
3
+ *
4
+ * Provides work items from GitHub Issues. Uses labels to identify
5
+ * work that is ready for agents and to track in-progress work.
6
+ *
7
+ * Includes robust handling of GitHub API edge cases:
8
+ * - Exponential backoff for rate limit errors (HTTP 403 with X-RateLimit-Remaining: 0)
9
+ * - Rate limit status detection and surfacing
10
+ * - Network error retries (max 3 attempts)
11
+ * - Graceful handling of 404 errors (issue deleted/moved)
12
+ * - PAT scope validation
13
+ * - Warnings for approaching rate limit (< 100 remaining)
14
+ */
15
+
16
+ import type { WorkSourceAdapter } from "../index.js";
17
+ import type {
18
+ FetchOptions,
19
+ FetchResult,
20
+ ClaimResult,
21
+ WorkResult,
22
+ ReleaseOptions,
23
+ ReleaseResult,
24
+ WorkItem,
25
+ WorkItemPriority,
26
+ } from "../types.js";
27
+ import type { WorkSourceConfig } from "../registry.js";
28
+ import { WorkSourceError } from "../errors.js";
29
+
30
+ // =============================================================================
31
+ // Rate Limit Types
32
+ // =============================================================================
33
+
34
+ /**
35
+ * GitHub API rate limit information extracted from response headers
36
+ */
37
+ export interface RateLimitInfo {
38
+ /** Maximum number of requests allowed per hour */
39
+ limit: number;
40
+ /** Number of requests remaining in current window */
41
+ remaining: number;
42
+ /** Unix timestamp when rate limit resets */
43
+ reset: number;
44
+ /** Resource type (core, search, graphql, etc.) */
45
+ resource: string;
46
+ }
47
+
48
+ /**
49
+ * Options for rate limit warning callback
50
+ */
51
+ export interface RateLimitWarningOptions {
52
+ /** Threshold below which warnings are triggered (default: 100) */
53
+ warningThreshold?: number;
54
+ /** Callback invoked when rate limit is approaching */
55
+ onWarning?: (info: RateLimitInfo) => void;
56
+ }
57
+
58
+ // =============================================================================
59
+ // Retry Configuration
60
+ // =============================================================================
61
+
62
+ /**
63
+ * Configuration for retry behavior
64
+ */
65
+ export interface RetryOptions {
66
+ /** Maximum number of retry attempts (default: 3) */
67
+ maxRetries?: number;
68
+ /** Base delay in milliseconds for exponential backoff (default: 1000) */
69
+ baseDelayMs?: number;
70
+ /** Maximum delay in milliseconds (default: 30000) */
71
+ maxDelayMs?: number;
72
+ /** Jitter factor (0-1) to randomize delays (default: 0.1) */
73
+ jitterFactor?: number;
74
+ }
75
+
76
+ // =============================================================================
77
+ // Types
78
+ // =============================================================================
79
+
80
+ /**
81
+ * Configuration specific to GitHub work source
82
+ */
83
+ export interface GitHubWorkSourceConfig extends WorkSourceConfig {
84
+ type: "github";
85
+ /** GitHub repository owner */
86
+ owner?: string;
87
+ /** GitHub repository name */
88
+ repo?: string;
89
+ /** GitHub API token (defaults to GITHUB_TOKEN env var) */
90
+ token?: string;
91
+ /** GitHub API base URL (defaults to https://api.github.com) */
92
+ apiBaseUrl?: string;
93
+ /** Label configuration for work item states */
94
+ labels?: {
95
+ /** Label that marks issues as ready for agent work (default: "ready") */
96
+ ready?: string;
97
+ /** Label applied when an agent claims the issue (default: "agent-working") */
98
+ in_progress?: string;
99
+ };
100
+ /** Labels to exclude from fetched issues (default: ["blocked", "wip"]) */
101
+ exclude_labels?: string[];
102
+ /** Re-add ready label when releasing work on failure (default: true) */
103
+ cleanup_on_failure?: boolean;
104
+ /** Retry configuration for handling transient failures */
105
+ retry?: RetryOptions;
106
+ /** Rate limit warning configuration */
107
+ rateLimitWarning?: RateLimitWarningOptions;
108
+ }
109
+
110
+ /**
111
+ * GitHub API issue response structure
112
+ */
113
+ export interface GitHubIssue {
114
+ number: number;
115
+ title: string;
116
+ body: string | null;
117
+ html_url: string;
118
+ state: "open" | "closed";
119
+ labels: Array<{ name: string }>;
120
+ created_at: string;
121
+ updated_at: string;
122
+ assignee: { login: string } | null;
123
+ assignees: Array<{ login: string }>;
124
+ milestone: { title: string; number: number } | null;
125
+ user: { login: string } | null;
126
+ }
127
+
128
+ /**
129
+ * GitHub API error response
130
+ */
131
+ interface GitHubErrorResponse {
132
+ message: string;
133
+ documentation_url?: string;
134
+ }
135
+
136
+ // =============================================================================
137
+ // Errors
138
+ // =============================================================================
139
+
140
+ /**
141
+ * Error thrown when GitHub API requests fail
142
+ */
143
+ export class GitHubAPIError extends WorkSourceError {
144
+ /** HTTP status code from the API response */
145
+ public readonly statusCode?: number;
146
+ /** The API endpoint that was called */
147
+ public readonly endpoint?: string;
148
+ /** Rate limit information if available */
149
+ public readonly rateLimitInfo?: RateLimitInfo;
150
+ /** Whether this error was caused by rate limiting */
151
+ public readonly isRateLimitError: boolean;
152
+ /** Timestamp when rate limit resets (for rate limit errors) */
153
+ public readonly rateLimitResetAt?: Date;
154
+
155
+ constructor(
156
+ message: string,
157
+ options?: {
158
+ cause?: Error;
159
+ statusCode?: number;
160
+ endpoint?: string;
161
+ rateLimitInfo?: RateLimitInfo;
162
+ isRateLimitError?: boolean;
163
+ }
164
+ ) {
165
+ super(message, options);
166
+ this.name = "GitHubAPIError";
167
+ this.statusCode = options?.statusCode;
168
+ this.endpoint = options?.endpoint;
169
+ this.rateLimitInfo = options?.rateLimitInfo;
170
+ this.isRateLimitError = options?.isRateLimitError ?? false;
171
+ if (options?.rateLimitInfo?.reset) {
172
+ this.rateLimitResetAt = new Date(options.rateLimitInfo.reset * 1000);
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Check if this error is retryable (rate limit or network error)
178
+ */
179
+ isRetryable(): boolean {
180
+ // Rate limit errors are retryable after waiting
181
+ if (this.isRateLimitError) {
182
+ return true;
183
+ }
184
+ // Network errors (no status code) are retryable
185
+ if (this.statusCode === undefined) {
186
+ return true;
187
+ }
188
+ // Server errors (5xx) are retryable
189
+ if (this.statusCode >= 500 && this.statusCode < 600) {
190
+ return true;
191
+ }
192
+ // 408 Request Timeout is retryable
193
+ if (this.statusCode === 408) {
194
+ return true;
195
+ }
196
+ return false;
197
+ }
198
+
199
+ /**
200
+ * Check if this is a not found error (404)
201
+ */
202
+ isNotFound(): boolean {
203
+ return this.statusCode === 404;
204
+ }
205
+
206
+ /**
207
+ * Check if this is a permission error (403 without rate limit)
208
+ */
209
+ isPermissionDenied(): boolean {
210
+ return this.statusCode === 403 && !this.isRateLimitError;
211
+ }
212
+
213
+ /**
214
+ * Get time in milliseconds until rate limit resets
215
+ */
216
+ getTimeUntilReset(): number | undefined {
217
+ if (!this.rateLimitResetAt) {
218
+ return undefined;
219
+ }
220
+ const now = Date.now();
221
+ const resetTime = this.rateLimitResetAt.getTime();
222
+ return Math.max(0, resetTime - now);
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Error thrown when PAT validation fails
228
+ */
229
+ export class GitHubAuthError extends WorkSourceError {
230
+ /** The scopes that were found */
231
+ public readonly foundScopes: string[];
232
+ /** The scopes that were required */
233
+ public readonly requiredScopes: string[];
234
+ /** The missing scopes */
235
+ public readonly missingScopes: string[];
236
+
237
+ constructor(
238
+ message: string,
239
+ options: {
240
+ cause?: Error;
241
+ foundScopes: string[];
242
+ requiredScopes: string[];
243
+ }
244
+ ) {
245
+ super(message, options);
246
+ this.name = "GitHubAuthError";
247
+ this.foundScopes = options.foundScopes;
248
+ this.requiredScopes = options.requiredScopes;
249
+ this.missingScopes = options.requiredScopes.filter(
250
+ (s) => !options.foundScopes.includes(s)
251
+ );
252
+ }
253
+ }
254
+
255
+ // =============================================================================
256
+ // Default Configuration
257
+ // =============================================================================
258
+
259
+ const DEFAULT_LABELS = {
260
+ ready: "ready",
261
+ in_progress: "agent-working",
262
+ };
263
+
264
+ const DEFAULT_EXCLUDE_LABELS = ["blocked", "wip"];
265
+
266
+ const DEFAULT_API_BASE_URL = "https://api.github.com";
267
+
268
+ /** Default retry options */
269
+ const DEFAULT_RETRY_OPTIONS: Required<RetryOptions> = {
270
+ maxRetries: 3,
271
+ baseDelayMs: 1000,
272
+ maxDelayMs: 30000,
273
+ jitterFactor: 0.1,
274
+ };
275
+
276
+ /** Default rate limit warning threshold */
277
+ const DEFAULT_RATE_LIMIT_WARNING_THRESHOLD = 100;
278
+
279
+ /** Required scopes for full GitHub adapter functionality */
280
+ const REQUIRED_SCOPES = ["repo"] as const;
281
+
282
+ // =============================================================================
283
+ // Utility Functions
284
+ // =============================================================================
285
+
286
+ /**
287
+ * Extract rate limit information from GitHub API response headers
288
+ */
289
+ export function extractRateLimitInfo(headers: Headers): RateLimitInfo | undefined {
290
+ const remaining = headers.get("X-RateLimit-Remaining");
291
+ const limit = headers.get("X-RateLimit-Limit");
292
+ const reset = headers.get("X-RateLimit-Reset");
293
+ const resource = headers.get("X-RateLimit-Resource");
294
+
295
+ if (remaining === null || limit === null || reset === null) {
296
+ return undefined;
297
+ }
298
+
299
+ return {
300
+ remaining: parseInt(remaining, 10),
301
+ limit: parseInt(limit, 10),
302
+ reset: parseInt(reset, 10),
303
+ resource: resource ?? "core",
304
+ };
305
+ }
306
+
307
+ /**
308
+ * Check if a response indicates rate limiting
309
+ */
310
+ export function isRateLimitResponse(response: Response): boolean {
311
+ // GitHub returns 403 with X-RateLimit-Remaining: 0 for rate limits
312
+ // Also check for 429 which some GitHub endpoints may return
313
+ if (response.status !== 403 && response.status !== 429) {
314
+ return false;
315
+ }
316
+
317
+ const remaining = response.headers.get("X-RateLimit-Remaining");
318
+ if (remaining === null) {
319
+ return response.status === 429;
320
+ }
321
+
322
+ return parseInt(remaining, 10) === 0;
323
+ }
324
+
325
+ /**
326
+ * Calculate delay with exponential backoff and jitter
327
+ */
328
+ export function calculateBackoffDelay(
329
+ attempt: number,
330
+ options: Required<RetryOptions>,
331
+ rateLimitResetMs?: number
332
+ ): number {
333
+ // If we have rate limit reset info, use that as the delay
334
+ if (rateLimitResetMs !== undefined && rateLimitResetMs > 0) {
335
+ // Add a small buffer (1 second) to ensure the limit has reset
336
+ return Math.min(rateLimitResetMs + 1000, options.maxDelayMs);
337
+ }
338
+
339
+ // Calculate exponential backoff: baseDelay * 2^attempt
340
+ const exponentialDelay = options.baseDelayMs * Math.pow(2, attempt);
341
+
342
+ // Cap at max delay
343
+ const cappedDelay = Math.min(exponentialDelay, options.maxDelayMs);
344
+
345
+ // Add jitter to prevent thundering herd
346
+ const jitter = cappedDelay * options.jitterFactor * Math.random();
347
+
348
+ return Math.floor(cappedDelay + jitter);
349
+ }
350
+
351
+ /**
352
+ * Sleep for a specified number of milliseconds
353
+ */
354
+ function sleep(ms: number): Promise<void> {
355
+ return new Promise((resolve) => setTimeout(resolve, ms));
356
+ }
357
+
358
+ // =============================================================================
359
+ // GitHub Adapter Implementation
360
+ // =============================================================================
361
+
362
+ /**
363
+ * Work source adapter for GitHub Issues
364
+ *
365
+ * Fetches issues with a specific label and allows claiming/completing them.
366
+ * Uses the GitHub REST API to manage issue labels and comments.
367
+ */
368
+ export class GitHubWorkSourceAdapter implements WorkSourceAdapter {
369
+ public readonly type = "github" as const;
370
+
371
+ private readonly config: GitHubWorkSourceConfig;
372
+ private readonly labels: { ready: string; in_progress: string };
373
+ private readonly excludeLabels: string[];
374
+ private readonly apiBaseUrl: string;
375
+ private readonly retryOptions: Required<RetryOptions>;
376
+ private readonly rateLimitWarningThreshold: number;
377
+ private readonly onRateLimitWarning?: (info: RateLimitInfo) => void;
378
+
379
+ /** Last known rate limit info, updated after each request */
380
+ private _lastRateLimitInfo?: RateLimitInfo;
381
+
382
+ constructor(config: GitHubWorkSourceConfig) {
383
+ this.config = config;
384
+ this.labels = {
385
+ ready: config.labels?.ready ?? DEFAULT_LABELS.ready,
386
+ in_progress: config.labels?.in_progress ?? DEFAULT_LABELS.in_progress,
387
+ };
388
+ this.excludeLabels = config.exclude_labels ?? DEFAULT_EXCLUDE_LABELS;
389
+ this.apiBaseUrl = config.apiBaseUrl ?? DEFAULT_API_BASE_URL;
390
+ this.retryOptions = {
391
+ ...DEFAULT_RETRY_OPTIONS,
392
+ ...config.retry,
393
+ };
394
+ this.rateLimitWarningThreshold =
395
+ config.rateLimitWarning?.warningThreshold ?? DEFAULT_RATE_LIMIT_WARNING_THRESHOLD;
396
+ this.onRateLimitWarning = config.rateLimitWarning?.onWarning;
397
+ }
398
+
399
+ /**
400
+ * Get the last known rate limit information
401
+ */
402
+ get lastRateLimitInfo(): RateLimitInfo | undefined {
403
+ return this._lastRateLimitInfo;
404
+ }
405
+
406
+ /**
407
+ * Get the GitHub API token from config or environment
408
+ */
409
+ private getToken(): string | undefined {
410
+ return this.config.token ?? process.env.GITHUB_TOKEN;
411
+ }
412
+
413
+ /**
414
+ * Get required owner and repo from config
415
+ */
416
+ private getOwnerRepo(): { owner: string; repo: string } {
417
+ const { owner, repo } = this.config;
418
+ if (!owner || !repo) {
419
+ throw new GitHubAPIError(
420
+ "GitHub adapter requires 'owner' and 'repo' configuration"
421
+ );
422
+ }
423
+ return { owner, repo };
424
+ }
425
+
426
+ /**
427
+ * Validate PAT has required scopes for GitHub adapter functionality
428
+ *
429
+ * Makes a request to check the token scopes and verifies
430
+ * that the required scopes (repo) are present.
431
+ *
432
+ * @throws GitHubAuthError if required scopes are missing
433
+ * @throws GitHubAPIError if the validation request fails
434
+ */
435
+ async validateToken(): Promise<{ scopes: string[]; valid: boolean }> {
436
+ const token = this.getToken();
437
+
438
+ if (!token) {
439
+ throw new GitHubAuthError(
440
+ "No GitHub token configured. Set GITHUB_TOKEN environment variable or provide token in config.",
441
+ {
442
+ foundScopes: [],
443
+ requiredScopes: [...REQUIRED_SCOPES],
444
+ }
445
+ );
446
+ }
447
+
448
+ const headers: Record<string, string> = {
449
+ Accept: "application/vnd.github+json",
450
+ "X-GitHub-Api-Version": "2022-11-28",
451
+ Authorization: `Bearer ${token}`,
452
+ };
453
+
454
+ const url = `${this.apiBaseUrl}/user`;
455
+
456
+ try {
457
+ const response = await fetch(url, { method: "GET", headers });
458
+
459
+ // Extract scopes from response header
460
+ const scopesHeader = response.headers.get("X-OAuth-Scopes");
461
+ const scopes = scopesHeader
462
+ ? scopesHeader.split(",").map((s) => s.trim()).filter((s) => s.length > 0)
463
+ : [];
464
+
465
+ // Update rate limit info
466
+ const rateLimitInfo = extractRateLimitInfo(response.headers);
467
+ if (rateLimitInfo) {
468
+ this._lastRateLimitInfo = rateLimitInfo;
469
+ this.checkRateLimitWarning(rateLimitInfo);
470
+ }
471
+
472
+ if (!response.ok) {
473
+ if (response.status === 401) {
474
+ throw new GitHubAuthError(
475
+ "Invalid GitHub token. The token may be expired or revoked.",
476
+ {
477
+ foundScopes: scopes,
478
+ requiredScopes: [...REQUIRED_SCOPES],
479
+ }
480
+ );
481
+ }
482
+ throw new GitHubAPIError(
483
+ `GitHub API error: ${response.status} ${response.statusText}`,
484
+ { statusCode: response.status, endpoint: "/user" }
485
+ );
486
+ }
487
+
488
+ // Check for required scopes
489
+ const hasRequiredScopes = REQUIRED_SCOPES.every((required) =>
490
+ scopes.some((scope) => scope === required || scope.startsWith(`${required}:`))
491
+ );
492
+
493
+ if (!hasRequiredScopes) {
494
+ throw new GitHubAuthError(
495
+ `GitHub token is missing required scopes. Found: [${scopes.join(", ")}], Required: [${REQUIRED_SCOPES.join(", ")}]`,
496
+ {
497
+ foundScopes: scopes,
498
+ requiredScopes: [...REQUIRED_SCOPES],
499
+ }
500
+ );
501
+ }
502
+
503
+ return { scopes, valid: true };
504
+ } catch (error) {
505
+ if (error instanceof GitHubAuthError || error instanceof GitHubAPIError) {
506
+ throw error;
507
+ }
508
+ throw new GitHubAPIError(
509
+ `Failed to validate GitHub token: ${error instanceof Error ? error.message : String(error)}`,
510
+ { cause: error instanceof Error ? error : undefined, endpoint: "/user" }
511
+ );
512
+ }
513
+ }
514
+
515
+ /**
516
+ * Check rate limit and invoke warning callback if below threshold
517
+ */
518
+ private checkRateLimitWarning(info: RateLimitInfo): void {
519
+ if (info.remaining < this.rateLimitWarningThreshold && this.onRateLimitWarning) {
520
+ this.onRateLimitWarning(info);
521
+ }
522
+ }
523
+
524
+ /**
525
+ * Make an authenticated request to the GitHub API with retry logic
526
+ *
527
+ * Implements:
528
+ * - Exponential backoff for rate limit errors (HTTP 403 with X-RateLimit-Remaining: 0)
529
+ * - Network error retries (max 3 attempts by default)
530
+ * - Rate limit status tracking and warnings
531
+ */
532
+ private async apiRequest<T>(
533
+ method: string,
534
+ endpoint: string,
535
+ body?: unknown
536
+ ): Promise<T> {
537
+ const token = this.getToken();
538
+ const headers: Record<string, string> = {
539
+ Accept: "application/vnd.github+json",
540
+ "X-GitHub-Api-Version": "2022-11-28",
541
+ };
542
+
543
+ if (token) {
544
+ headers["Authorization"] = `Bearer ${token}`;
545
+ }
546
+
547
+ const url = `${this.apiBaseUrl}${endpoint}`;
548
+ let lastError: GitHubAPIError | undefined;
549
+
550
+ for (let attempt = 0; attempt <= this.retryOptions.maxRetries; attempt++) {
551
+ try {
552
+ const response = await fetch(url, {
553
+ method,
554
+ headers,
555
+ body: body ? JSON.stringify(body) : undefined,
556
+ });
557
+
558
+ // Extract and store rate limit info from every response
559
+ const rateLimitInfo = extractRateLimitInfo(response.headers);
560
+ if (rateLimitInfo) {
561
+ this._lastRateLimitInfo = rateLimitInfo;
562
+ this.checkRateLimitWarning(rateLimitInfo);
563
+ }
564
+
565
+ if (!response.ok) {
566
+ // Check for rate limiting
567
+ const isRateLimit = isRateLimitResponse(response);
568
+
569
+ let errorMessage = `GitHub API error: ${response.status} ${response.statusText}`;
570
+ try {
571
+ const errorBody = (await response.json()) as GitHubErrorResponse;
572
+ if (errorBody.message) {
573
+ errorMessage = `GitHub API error: ${errorBody.message}`;
574
+ }
575
+ } catch {
576
+ // Ignore JSON parse errors
577
+ }
578
+
579
+ const error = new GitHubAPIError(errorMessage, {
580
+ statusCode: response.status,
581
+ endpoint,
582
+ rateLimitInfo,
583
+ isRateLimitError: isRateLimit,
584
+ });
585
+
586
+ // Only retry on retryable errors
587
+ if (error.isRetryable() && attempt < this.retryOptions.maxRetries) {
588
+ lastError = error;
589
+ const delay = calculateBackoffDelay(
590
+ attempt,
591
+ this.retryOptions,
592
+ error.getTimeUntilReset()
593
+ );
594
+ await sleep(delay);
595
+ continue;
596
+ }
597
+
598
+ throw error;
599
+ }
600
+
601
+ // Handle 204 No Content responses
602
+ if (response.status === 204) {
603
+ return undefined as T;
604
+ }
605
+
606
+ return (await response.json()) as T;
607
+ } catch (error) {
608
+ // Re-throw GitHubAPIError as-is (already handled above)
609
+ if (error instanceof GitHubAPIError) {
610
+ throw error;
611
+ }
612
+
613
+ // Network errors - wrap and potentially retry
614
+ const networkError = new GitHubAPIError(
615
+ `Failed to connect to GitHub API: ${error instanceof Error ? error.message : String(error)}`,
616
+ { cause: error instanceof Error ? error : undefined, endpoint }
617
+ );
618
+
619
+ if (networkError.isRetryable() && attempt < this.retryOptions.maxRetries) {
620
+ lastError = networkError;
621
+ const delay = calculateBackoffDelay(attempt, this.retryOptions);
622
+ await sleep(delay);
623
+ continue;
624
+ }
625
+
626
+ throw networkError;
627
+ }
628
+ }
629
+
630
+ // Should not reach here, but throw last error if we do
631
+ throw lastError ?? new GitHubAPIError("Unknown error during API request", { endpoint });
632
+ }
633
+
634
+ /**
635
+ * Make an authenticated request to the GitHub API with retry logic, returning both data and headers
636
+ *
637
+ * This variant returns the response headers along with the data, useful for
638
+ * pagination (Link header) and other response metadata.
639
+ */
640
+ private async apiRequestWithHeaders<T>(
641
+ method: string,
642
+ endpoint: string,
643
+ body?: unknown
644
+ ): Promise<{ data: T; headers: Headers }> {
645
+ const token = this.getToken();
646
+ const headers: Record<string, string> = {
647
+ Accept: "application/vnd.github+json",
648
+ "X-GitHub-Api-Version": "2022-11-28",
649
+ };
650
+
651
+ if (token) {
652
+ headers["Authorization"] = `Bearer ${token}`;
653
+ }
654
+
655
+ const url = `${this.apiBaseUrl}${endpoint}`;
656
+ let lastError: GitHubAPIError | undefined;
657
+
658
+ for (let attempt = 0; attempt <= this.retryOptions.maxRetries; attempt++) {
659
+ try {
660
+ const response = await fetch(url, {
661
+ method,
662
+ headers,
663
+ body: body ? JSON.stringify(body) : undefined,
664
+ });
665
+
666
+ // Extract and store rate limit info from every response
667
+ const rateLimitInfo = extractRateLimitInfo(response.headers);
668
+ if (rateLimitInfo) {
669
+ this._lastRateLimitInfo = rateLimitInfo;
670
+ this.checkRateLimitWarning(rateLimitInfo);
671
+ }
672
+
673
+ if (!response.ok) {
674
+ // Check for rate limiting
675
+ const isRateLimit = isRateLimitResponse(response);
676
+
677
+ let errorMessage = `GitHub API error: ${response.status} ${response.statusText}`;
678
+ try {
679
+ const errorBody = (await response.json()) as GitHubErrorResponse;
680
+ if (errorBody.message) {
681
+ errorMessage = `GitHub API error: ${errorBody.message}`;
682
+ }
683
+ } catch {
684
+ // Ignore JSON parse errors
685
+ }
686
+
687
+ const error = new GitHubAPIError(errorMessage, {
688
+ statusCode: response.status,
689
+ endpoint,
690
+ rateLimitInfo,
691
+ isRateLimitError: isRateLimit,
692
+ });
693
+
694
+ // Only retry on retryable errors
695
+ if (error.isRetryable() && attempt < this.retryOptions.maxRetries) {
696
+ lastError = error;
697
+ const delay = calculateBackoffDelay(
698
+ attempt,
699
+ this.retryOptions,
700
+ error.getTimeUntilReset()
701
+ );
702
+ await sleep(delay);
703
+ continue;
704
+ }
705
+
706
+ throw error;
707
+ }
708
+
709
+ // Handle 204 No Content responses
710
+ if (response.status === 204) {
711
+ return { data: undefined as T, headers: response.headers };
712
+ }
713
+
714
+ const data = (await response.json()) as T;
715
+ return { data, headers: response.headers };
716
+ } catch (error) {
717
+ // Re-throw GitHubAPIError as-is (already handled above)
718
+ if (error instanceof GitHubAPIError) {
719
+ throw error;
720
+ }
721
+
722
+ // Network errors - wrap and potentially retry
723
+ const networkError = new GitHubAPIError(
724
+ `Failed to connect to GitHub API: ${error instanceof Error ? error.message : String(error)}`,
725
+ { cause: error instanceof Error ? error : undefined, endpoint }
726
+ );
727
+
728
+ if (networkError.isRetryable() && attempt < this.retryOptions.maxRetries) {
729
+ lastError = networkError;
730
+ const delay = calculateBackoffDelay(attempt, this.retryOptions);
731
+ await sleep(delay);
732
+ continue;
733
+ }
734
+
735
+ throw networkError;
736
+ }
737
+ }
738
+
739
+ // Should not reach here, but throw last error if we do
740
+ throw lastError ?? new GitHubAPIError("Unknown error during API request", { endpoint });
741
+ }
742
+
743
+ /**
744
+ * Extract the issue number from a work item ID
745
+ */
746
+ private parseWorkItemId(workItemId: string): number {
747
+ // Work item IDs are formatted as "github-{issueNumber}"
748
+ const match = workItemId.match(/^github-(\d+)$/);
749
+ if (!match) {
750
+ throw new GitHubAPIError(
751
+ `Invalid work item ID format: "${workItemId}". Expected "github-{number}"`
752
+ );
753
+ }
754
+ return parseInt(match[1], 10);
755
+ }
756
+
757
+ /**
758
+ * Convert a GitHub issue to a WorkItem
759
+ */
760
+ private issueToWorkItem(issue: GitHubIssue): WorkItem {
761
+ const labelNames = issue.labels.map((l) => l.name);
762
+
763
+ return {
764
+ id: `github-${issue.number}`,
765
+ source: "github",
766
+ externalId: String(issue.number),
767
+ title: issue.title,
768
+ description: issue.body ?? "",
769
+ priority: this.inferPriority(labelNames),
770
+ labels: labelNames,
771
+ metadata: {
772
+ state: issue.state,
773
+ assignee: issue.assignee?.login ?? null,
774
+ assignees: issue.assignees.map((a) => a.login),
775
+ milestone: issue.milestone
776
+ ? {
777
+ title: issue.milestone.title,
778
+ number: issue.milestone.number,
779
+ }
780
+ : null,
781
+ author: issue.user?.login ?? null,
782
+ },
783
+ url: issue.html_url,
784
+ createdAt: new Date(issue.created_at),
785
+ updatedAt: new Date(issue.updated_at),
786
+ };
787
+ }
788
+
789
+ /**
790
+ * Infer priority from issue labels
791
+ */
792
+ private inferPriority(labels: string[]): WorkItemPriority {
793
+ const lowerLabels = labels.map((l) => l.toLowerCase());
794
+
795
+ if (
796
+ lowerLabels.some(
797
+ (l) => l.includes("critical") || l.includes("p0") || l.includes("urgent")
798
+ )
799
+ ) {
800
+ return "critical";
801
+ }
802
+ if (
803
+ lowerLabels.some(
804
+ (l) => l.includes("high") || l.includes("p1") || l.includes("important")
805
+ )
806
+ ) {
807
+ return "high";
808
+ }
809
+ if (lowerLabels.some((l) => l.includes("low") || l.includes("p3"))) {
810
+ return "low";
811
+ }
812
+ return "medium";
813
+ }
814
+
815
+ /**
816
+ * Build the query string for fetching issues
817
+ */
818
+ private buildIssueQuery(options?: FetchOptions): string {
819
+ const params = new URLSearchParams();
820
+
821
+ // Always filter by ready label
822
+ params.set("labels", this.labels.ready);
823
+
824
+ // Sort by creation date (oldest first for FIFO)
825
+ params.set("sort", "created");
826
+ params.set("direction", "asc");
827
+
828
+ // State filter - only open issues unless claimed are included
829
+ params.set("state", "open");
830
+
831
+ // Pagination
832
+ if (options?.limit) {
833
+ params.set("per_page", String(Math.min(options.limit, 100)));
834
+ } else {
835
+ params.set("per_page", "30");
836
+ }
837
+
838
+ if (options?.cursor) {
839
+ params.set("page", options.cursor);
840
+ }
841
+
842
+ return params.toString();
843
+ }
844
+
845
+ /**
846
+ * Extract pagination cursor from Link header
847
+ */
848
+ private extractNextPage(linkHeader: string | null): string | undefined {
849
+ if (!linkHeader) return undefined;
850
+
851
+ // Parse Link header format: <url>; rel="next", <url>; rel="last"
852
+ const links = linkHeader.split(",");
853
+ for (const link of links) {
854
+ const match = link.match(/<[^>]*[?&]page=(\d+)[^>]*>;\s*rel="next"/);
855
+ if (match) {
856
+ return match[1];
857
+ }
858
+ }
859
+ return undefined;
860
+ }
861
+
862
+ /**
863
+ * Fetch available work items from GitHub Issues
864
+ *
865
+ * Queries issues with the ready label, excludes issues with blocked/wip labels,
866
+ * and returns them sorted by creation date (oldest first).
867
+ *
868
+ * Includes retry logic for transient failures and rate limit handling.
869
+ */
870
+ async fetchAvailableWork(options?: FetchOptions): Promise<FetchResult> {
871
+ const { owner, repo } = this.getOwnerRepo();
872
+ const query = this.buildIssueQuery(options);
873
+ const endpoint = `/repos/${owner}/${repo}/issues?${query}`;
874
+
875
+ const { data: issues, headers } = await this.apiRequestWithHeaders<GitHubIssue[]>(
876
+ "GET",
877
+ endpoint
878
+ );
879
+
880
+ // Filter out issues with excluded labels and issues that are already claimed
881
+ const filteredIssues = issues.filter((issue) => {
882
+ const labelNames = issue.labels.map((l) => l.name.toLowerCase());
883
+
884
+ // Exclude issues with any of the exclude labels
885
+ const hasExcludedLabel = this.excludeLabels.some((excluded) =>
886
+ labelNames.includes(excluded.toLowerCase())
887
+ );
888
+ if (hasExcludedLabel) return false;
889
+
890
+ // Unless includeClaimed is true, exclude issues with in_progress label
891
+ if (!options?.includeClaimed) {
892
+ const hasInProgressLabel = labelNames.includes(
893
+ this.labels.in_progress.toLowerCase()
894
+ );
895
+ if (hasInProgressLabel) return false;
896
+ }
897
+
898
+ // Apply additional label filters if specified
899
+ if (options?.labels && options.labels.length > 0) {
900
+ const hasAllLabels = options.labels.every((required) =>
901
+ labelNames.includes(required.toLowerCase())
902
+ );
903
+ if (!hasAllLabels) return false;
904
+ }
905
+
906
+ return true;
907
+ });
908
+
909
+ // Filter by priority if specified
910
+ let workItems = filteredIssues.map((issue) => this.issueToWorkItem(issue));
911
+ if (options?.priority && options.priority.length > 0) {
912
+ workItems = workItems.filter((item) =>
913
+ options.priority!.includes(item.priority)
914
+ );
915
+ }
916
+
917
+ // Extract pagination info from Link header
918
+ const linkHeader = headers.get("Link");
919
+ const nextCursor = this.extractNextPage(linkHeader);
920
+
921
+ return {
922
+ items: workItems,
923
+ nextCursor,
924
+ };
925
+ }
926
+
927
+ /**
928
+ * Claim a work item by adding the in-progress label and removing the ready label
929
+ */
930
+ async claimWork(workItemId: string): Promise<ClaimResult> {
931
+ const { owner, repo } = this.getOwnerRepo();
932
+ const issueNumber = this.parseWorkItemId(workItemId);
933
+
934
+ try {
935
+ // First, get the current issue to verify it exists and is claimable
936
+ const issue = await this.apiRequest<GitHubIssue>(
937
+ "GET",
938
+ `/repos/${owner}/${repo}/issues/${issueNumber}`
939
+ );
940
+
941
+ if (!issue) {
942
+ return {
943
+ success: false,
944
+ reason: "not_found",
945
+ message: `Issue #${issueNumber} not found`,
946
+ };
947
+ }
948
+
949
+ if (issue.state === "closed") {
950
+ return {
951
+ success: false,
952
+ reason: "invalid_state",
953
+ message: `Issue #${issueNumber} is closed`,
954
+ };
955
+ }
956
+
957
+ const labelNames = issue.labels.map((l) => l.name.toLowerCase());
958
+
959
+ // Check if already claimed
960
+ if (labelNames.includes(this.labels.in_progress.toLowerCase())) {
961
+ return {
962
+ success: false,
963
+ reason: "already_claimed",
964
+ message: `Issue #${issueNumber} is already claimed`,
965
+ };
966
+ }
967
+
968
+ // Add the in-progress label
969
+ await this.apiRequest<void>(
970
+ "POST",
971
+ `/repos/${owner}/${repo}/issues/${issueNumber}/labels`,
972
+ { labels: [this.labels.in_progress] }
973
+ );
974
+
975
+ // Remove the ready label (if present)
976
+ if (labelNames.includes(this.labels.ready.toLowerCase())) {
977
+ try {
978
+ await this.apiRequest<void>(
979
+ "DELETE",
980
+ `/repos/${owner}/${repo}/issues/${issueNumber}/labels/${encodeURIComponent(this.labels.ready)}`
981
+ );
982
+ } catch {
983
+ // Ignore errors removing the ready label - it might not exist
984
+ }
985
+ }
986
+
987
+ // Fetch the updated issue
988
+ const updatedIssue = await this.apiRequest<GitHubIssue>(
989
+ "GET",
990
+ `/repos/${owner}/${repo}/issues/${issueNumber}`
991
+ );
992
+
993
+ return {
994
+ success: true,
995
+ workItem: this.issueToWorkItem(updatedIssue),
996
+ };
997
+ } catch (error) {
998
+ if (error instanceof GitHubAPIError) {
999
+ if (error.statusCode === 404) {
1000
+ return {
1001
+ success: false,
1002
+ reason: "not_found",
1003
+ message: `Issue #${issueNumber} not found`,
1004
+ };
1005
+ }
1006
+ if (error.statusCode === 403) {
1007
+ return {
1008
+ success: false,
1009
+ reason: "permission_denied",
1010
+ message: `Permission denied for issue #${issueNumber}`,
1011
+ };
1012
+ }
1013
+ return {
1014
+ success: false,
1015
+ reason: "source_error",
1016
+ message: error.message,
1017
+ };
1018
+ }
1019
+ return {
1020
+ success: false,
1021
+ reason: "source_error",
1022
+ message: `Failed to claim issue: ${error instanceof Error ? error.message : String(error)}`,
1023
+ };
1024
+ }
1025
+ }
1026
+
1027
+ /**
1028
+ * Complete a work item by posting a comment with the result and optionally closing the issue
1029
+ */
1030
+ async completeWork(workItemId: string, result: WorkResult): Promise<void> {
1031
+ const { owner, repo } = this.getOwnerRepo();
1032
+ const issueNumber = this.parseWorkItemId(workItemId);
1033
+
1034
+ // Build the completion comment
1035
+ const outcomeEmoji =
1036
+ result.outcome === "success"
1037
+ ? "✅"
1038
+ : result.outcome === "partial"
1039
+ ? "⚠️"
1040
+ : "❌";
1041
+
1042
+ let commentBody = `## ${outcomeEmoji} Work Completed\n\n`;
1043
+ commentBody += `**Outcome:** ${result.outcome}\n\n`;
1044
+ commentBody += `**Summary:** ${result.summary}\n`;
1045
+
1046
+ if (result.details) {
1047
+ commentBody += `\n### Details\n\n${result.details}\n`;
1048
+ }
1049
+
1050
+ if (result.artifacts && result.artifacts.length > 0) {
1051
+ commentBody += `\n### Artifacts\n\n`;
1052
+ for (const artifact of result.artifacts) {
1053
+ commentBody += `- ${artifact}\n`;
1054
+ }
1055
+ }
1056
+
1057
+ if (result.error) {
1058
+ commentBody += `\n### Error\n\n\`\`\`\n${result.error}\n\`\`\`\n`;
1059
+ }
1060
+
1061
+ // Post the comment
1062
+ await this.apiRequest<void>(
1063
+ "POST",
1064
+ `/repos/${owner}/${repo}/issues/${issueNumber}/comments`,
1065
+ { body: commentBody }
1066
+ );
1067
+
1068
+ // Remove the in-progress label
1069
+ try {
1070
+ await this.apiRequest<void>(
1071
+ "DELETE",
1072
+ `/repos/${owner}/${repo}/issues/${issueNumber}/labels/${encodeURIComponent(this.labels.in_progress)}`
1073
+ );
1074
+ } catch {
1075
+ // Ignore errors removing the label
1076
+ }
1077
+
1078
+ // Close the issue if the outcome was success
1079
+ if (result.outcome === "success") {
1080
+ await this.apiRequest<void>(
1081
+ "PATCH",
1082
+ `/repos/${owner}/${repo}/issues/${issueNumber}`,
1083
+ { state: "closed", state_reason: "completed" }
1084
+ );
1085
+ }
1086
+ }
1087
+
1088
+ /**
1089
+ * Release a claimed work item by removing in-progress label and re-adding ready label
1090
+ */
1091
+ async releaseWork(
1092
+ workItemId: string,
1093
+ options?: ReleaseOptions
1094
+ ): Promise<ReleaseResult> {
1095
+ const { owner, repo } = this.getOwnerRepo();
1096
+ const issueNumber = this.parseWorkItemId(workItemId);
1097
+
1098
+ try {
1099
+ // Add a comment if requested
1100
+ if (options?.addComment && options?.reason) {
1101
+ await this.apiRequest<void>(
1102
+ "POST",
1103
+ `/repos/${owner}/${repo}/issues/${issueNumber}/comments`,
1104
+ { body: `⏸️ **Work Released**\n\nReason: ${options.reason}` }
1105
+ );
1106
+ }
1107
+
1108
+ // Remove the in-progress label
1109
+ try {
1110
+ await this.apiRequest<void>(
1111
+ "DELETE",
1112
+ `/repos/${owner}/${repo}/issues/${issueNumber}/labels/${encodeURIComponent(this.labels.in_progress)}`
1113
+ );
1114
+ } catch {
1115
+ // Ignore errors removing the label
1116
+ }
1117
+
1118
+ // Re-add the ready label if cleanup_on_failure is true (default)
1119
+ if (this.config.cleanup_on_failure !== false) {
1120
+ await this.apiRequest<void>(
1121
+ "POST",
1122
+ `/repos/${owner}/${repo}/issues/${issueNumber}/labels`,
1123
+ { labels: [this.labels.ready] }
1124
+ );
1125
+ }
1126
+
1127
+ return { success: true };
1128
+ } catch (error) {
1129
+ return {
1130
+ success: false,
1131
+ message: `Failed to release issue: ${error instanceof Error ? error.message : String(error)}`,
1132
+ };
1133
+ }
1134
+ }
1135
+
1136
+ /**
1137
+ * Get a specific work item by ID
1138
+ */
1139
+ async getWork(workItemId: string): Promise<WorkItem | undefined> {
1140
+ const { owner, repo } = this.getOwnerRepo();
1141
+ const issueNumber = this.parseWorkItemId(workItemId);
1142
+
1143
+ try {
1144
+ const issue = await this.apiRequest<GitHubIssue>(
1145
+ "GET",
1146
+ `/repos/${owner}/${repo}/issues/${issueNumber}`
1147
+ );
1148
+
1149
+ return this.issueToWorkItem(issue);
1150
+ } catch (error) {
1151
+ if (error instanceof GitHubAPIError && error.statusCode === 404) {
1152
+ return undefined;
1153
+ }
1154
+ throw error;
1155
+ }
1156
+ }
1157
+ }
1158
+
1159
+ /**
1160
+ * Factory function for creating GitHub adapters
1161
+ */
1162
+ export function createGitHubAdapter(
1163
+ config: WorkSourceConfig
1164
+ ): WorkSourceAdapter {
1165
+ return new GitHubWorkSourceAdapter(config as GitHubWorkSourceConfig);
1166
+ }