@amsterdamdatalabs/enact-factory 0.1.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 (644) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +566 -0
  3. package/dist/adapters/agenticLoop.d.ts +90 -0
  4. package/dist/adapters/agenticLoop.d.ts.map +1 -0
  5. package/dist/adapters/agenticLoop.js +219 -0
  6. package/dist/adapters/agenticLoop.js.map +1 -0
  7. package/dist/adapters/base.d.ts +16 -0
  8. package/dist/adapters/base.d.ts.map +1 -0
  9. package/dist/adapters/base.js +135 -0
  10. package/dist/adapters/base.js.map +1 -0
  11. package/dist/adapters/claude.d.ts +13 -0
  12. package/dist/adapters/claude.d.ts.map +1 -0
  13. package/dist/adapters/claude.js +318 -0
  14. package/dist/adapters/claude.js.map +1 -0
  15. package/dist/adapters/codex.d.ts +14 -0
  16. package/dist/adapters/codex.d.ts.map +1 -0
  17. package/dist/adapters/codex.js +366 -0
  18. package/dist/adapters/codex.js.map +1 -0
  19. package/dist/adapters/cryptoQuantAdapter.d.ts +85 -0
  20. package/dist/adapters/cryptoQuantAdapter.d.ts.map +1 -0
  21. package/dist/adapters/cryptoQuantAdapter.js +238 -0
  22. package/dist/adapters/cryptoQuantAdapter.js.map +1 -0
  23. package/dist/adapters/cursor.d.ts +13 -0
  24. package/dist/adapters/cursor.d.ts.map +1 -0
  25. package/dist/adapters/cursor.js +300 -0
  26. package/dist/adapters/cursor.js.map +1 -0
  27. package/dist/adapters/envPath.d.ts +20 -0
  28. package/dist/adapters/envPath.d.ts.map +1 -0
  29. package/dist/adapters/envPath.js +49 -0
  30. package/dist/adapters/envPath.js.map +1 -0
  31. package/dist/adapters/hermes.d.ts +13 -0
  32. package/dist/adapters/hermes.d.ts.map +1 -0
  33. package/dist/adapters/hermes.js +283 -0
  34. package/dist/adapters/hermes.js.map +1 -0
  35. package/dist/adapters/index.d.ts +18 -0
  36. package/dist/adapters/index.d.ts.map +1 -0
  37. package/dist/adapters/index.js +56 -0
  38. package/dist/adapters/index.js.map +1 -0
  39. package/dist/adapters/opencode.d.ts +13 -0
  40. package/dist/adapters/opencode.d.ts.map +1 -0
  41. package/dist/adapters/opencode.js +282 -0
  42. package/dist/adapters/opencode.js.map +1 -0
  43. package/dist/adapters/processRegistry.d.ts +38 -0
  44. package/dist/adapters/processRegistry.d.ts.map +1 -0
  45. package/dist/adapters/processRegistry.js +147 -0
  46. package/dist/adapters/processRegistry.js.map +1 -0
  47. package/dist/adapters/responses.d.ts +16 -0
  48. package/dist/adapters/responses.d.ts.map +1 -0
  49. package/dist/adapters/responses.js +244 -0
  50. package/dist/adapters/responses.js.map +1 -0
  51. package/dist/adapters/streamBuffer.d.ts +59 -0
  52. package/dist/adapters/streamBuffer.d.ts.map +1 -0
  53. package/dist/adapters/streamBuffer.js +123 -0
  54. package/dist/adapters/streamBuffer.js.map +1 -0
  55. package/dist/adapters/tools.d.ts +30 -0
  56. package/dist/adapters/tools.d.ts.map +1 -0
  57. package/dist/adapters/tools.js +219 -0
  58. package/dist/adapters/tools.js.map +1 -0
  59. package/dist/adapters/types.d.ts +82 -0
  60. package/dist/adapters/types.d.ts.map +1 -0
  61. package/dist/adapters/types.js +6 -0
  62. package/dist/adapters/types.js.map +1 -0
  63. package/dist/agents/agentBus.d.ts +160 -0
  64. package/dist/agents/agentBus.d.ts.map +1 -0
  65. package/dist/agents/agentBus.js +350 -0
  66. package/dist/agents/agentBus.js.map +1 -0
  67. package/dist/agents/agentPair.d.ts +215 -0
  68. package/dist/agents/agentPair.d.ts.map +1 -0
  69. package/dist/agents/agentPair.js +456 -0
  70. package/dist/agents/agentPair.js.map +1 -0
  71. package/dist/agents/auditor.d.ts +27 -0
  72. package/dist/agents/auditor.d.ts.map +1 -0
  73. package/dist/agents/auditor.js +238 -0
  74. package/dist/agents/auditor.js.map +1 -0
  75. package/dist/agents/cliStreamParser.d.ts +18 -0
  76. package/dist/agents/cliStreamParser.d.ts.map +1 -0
  77. package/dist/agents/cliStreamParser.js +156 -0
  78. package/dist/agents/cliStreamParser.js.map +1 -0
  79. package/dist/agents/documenter.d.ts +31 -0
  80. package/dist/agents/documenter.d.ts.map +1 -0
  81. package/dist/agents/documenter.js +286 -0
  82. package/dist/agents/documenter.js.map +1 -0
  83. package/dist/agents/draftAnalyzer.d.ts +50 -0
  84. package/dist/agents/draftAnalyzer.d.ts.map +1 -0
  85. package/dist/agents/draftAnalyzer.js +289 -0
  86. package/dist/agents/draftAnalyzer.js.map +1 -0
  87. package/dist/agents/evaluator.d.ts +61 -0
  88. package/dist/agents/evaluator.d.ts.map +1 -0
  89. package/dist/agents/evaluator.js +338 -0
  90. package/dist/agents/evaluator.js.map +1 -0
  91. package/dist/agents/executor.d.ts +33 -0
  92. package/dist/agents/executor.d.ts.map +1 -0
  93. package/dist/agents/executor.js +130 -0
  94. package/dist/agents/executor.js.map +1 -0
  95. package/dist/agents/index.d.ts +10 -0
  96. package/dist/agents/index.d.ts.map +1 -0
  97. package/dist/agents/index.js +10 -0
  98. package/dist/agents/index.js.map +1 -0
  99. package/dist/agents/pairMetrics.d.ts +63 -0
  100. package/dist/agents/pairMetrics.d.ts.map +1 -0
  101. package/dist/agents/pairMetrics.js +232 -0
  102. package/dist/agents/pairMetrics.js.map +1 -0
  103. package/dist/agents/pairPipeline.d.ts +184 -0
  104. package/dist/agents/pairPipeline.d.ts.map +1 -0
  105. package/dist/agents/pairPipeline.js +934 -0
  106. package/dist/agents/pairPipeline.js.map +1 -0
  107. package/dist/agents/pairWebhook.d.ts +59 -0
  108. package/dist/agents/pairWebhook.d.ts.map +1 -0
  109. package/dist/agents/pairWebhook.js +242 -0
  110. package/dist/agents/pairWebhook.js.map +1 -0
  111. package/dist/agents/pipelineFormat.d.ts +8 -0
  112. package/dist/agents/pipelineFormat.d.ts.map +1 -0
  113. package/dist/agents/pipelineFormat.js +65 -0
  114. package/dist/agents/pipelineFormat.js.map +1 -0
  115. package/dist/agents/pipelineGuards.d.ts +23 -0
  116. package/dist/agents/pipelineGuards.d.ts.map +1 -0
  117. package/dist/agents/pipelineGuards.js +257 -0
  118. package/dist/agents/pipelineGuards.js.map +1 -0
  119. package/dist/agents/reviewer.d.ts +37 -0
  120. package/dist/agents/reviewer.d.ts.map +1 -0
  121. package/dist/agents/reviewer.js +214 -0
  122. package/dist/agents/reviewer.js.map +1 -0
  123. package/dist/agents/skillDocumenter.d.ts +23 -0
  124. package/dist/agents/skillDocumenter.d.ts.map +1 -0
  125. package/dist/agents/skillDocumenter.js +219 -0
  126. package/dist/agents/skillDocumenter.js.map +1 -0
  127. package/dist/agents/tester.d.ts +37 -0
  128. package/dist/agents/tester.d.ts.map +1 -0
  129. package/dist/agents/tester.js +309 -0
  130. package/dist/agents/tester.js.map +1 -0
  131. package/dist/automation/autonomousRunner.d.ts +145 -0
  132. package/dist/automation/autonomousRunner.d.ts.map +1 -0
  133. package/dist/automation/autonomousRunner.js +1272 -0
  134. package/dist/automation/autonomousRunner.js.map +1 -0
  135. package/dist/automation/dailyReporter.d.ts +26 -0
  136. package/dist/automation/dailyReporter.d.ts.map +1 -0
  137. package/dist/automation/dailyReporter.js +130 -0
  138. package/dist/automation/dailyReporter.js.map +1 -0
  139. package/dist/automation/index.d.ts +5 -0
  140. package/dist/automation/index.d.ts.map +1 -0
  141. package/dist/automation/index.js +5 -0
  142. package/dist/automation/index.js.map +1 -0
  143. package/dist/automation/longRunningMonitor.d.ts +26 -0
  144. package/dist/automation/longRunningMonitor.d.ts.map +1 -0
  145. package/dist/automation/longRunningMonitor.js +356 -0
  146. package/dist/automation/longRunningMonitor.js.map +1 -0
  147. package/dist/automation/prOwnership.d.ts +18 -0
  148. package/dist/automation/prOwnership.d.ts.map +1 -0
  149. package/dist/automation/prOwnership.js +61 -0
  150. package/dist/automation/prOwnership.js.map +1 -0
  151. package/dist/automation/runnerExecution.d.ts +57 -0
  152. package/dist/automation/runnerExecution.d.ts.map +1 -0
  153. package/dist/automation/runnerExecution.js +701 -0
  154. package/dist/automation/runnerExecution.js.map +1 -0
  155. package/dist/automation/runnerState.d.ts +170 -0
  156. package/dist/automation/runnerState.d.ts.map +1 -0
  157. package/dist/automation/runnerState.js +496 -0
  158. package/dist/automation/runnerState.js.map +1 -0
  159. package/dist/automation/runnerTypes.d.ts +57 -0
  160. package/dist/automation/runnerTypes.d.ts.map +1 -0
  161. package/dist/automation/runnerTypes.js +5 -0
  162. package/dist/automation/runnerTypes.js.map +1 -0
  163. package/dist/automation/scheduler.d.ts +75 -0
  164. package/dist/automation/scheduler.d.ts.map +1 -0
  165. package/dist/automation/scheduler.js +402 -0
  166. package/dist/automation/scheduler.js.map +1 -0
  167. package/dist/azdo/azdo.d.ts +70 -0
  168. package/dist/azdo/azdo.d.ts.map +1 -0
  169. package/dist/azdo/azdo.js +328 -0
  170. package/dist/azdo/azdo.js.map +1 -0
  171. package/dist/azdo/index.d.ts +3 -0
  172. package/dist/azdo/index.d.ts.map +1 -0
  173. package/dist/azdo/index.js +3 -0
  174. package/dist/azdo/index.js.map +1 -0
  175. package/dist/azdo/projectUpdater.d.ts +13 -0
  176. package/dist/azdo/projectUpdater.d.ts.map +1 -0
  177. package/dist/azdo/projectUpdater.js +155 -0
  178. package/dist/azdo/projectUpdater.js.map +1 -0
  179. package/dist/azureDevOps/client.d.ts +75 -0
  180. package/dist/azureDevOps/client.d.ts.map +1 -0
  181. package/dist/azureDevOps/client.js +150 -0
  182. package/dist/azureDevOps/client.js.map +1 -0
  183. package/dist/azureDevOps/hierarchy.d.ts +119 -0
  184. package/dist/azureDevOps/hierarchy.d.ts.map +1 -0
  185. package/dist/azureDevOps/hierarchy.js +470 -0
  186. package/dist/azureDevOps/hierarchy.js.map +1 -0
  187. package/dist/azureDevOps/mapper.d.ts +101 -0
  188. package/dist/azureDevOps/mapper.d.ts.map +1 -0
  189. package/dist/azureDevOps/mapper.js +438 -0
  190. package/dist/azureDevOps/mapper.js.map +1 -0
  191. package/dist/azureDevOps/stateMapping.d.ts +15 -0
  192. package/dist/azureDevOps/stateMapping.d.ts.map +1 -0
  193. package/dist/azureDevOps/stateMapping.js +141 -0
  194. package/dist/azureDevOps/stateMapping.js.map +1 -0
  195. package/dist/cli/authHandler.d.ts +13 -0
  196. package/dist/cli/authHandler.d.ts.map +1 -0
  197. package/dist/cli/authHandler.js +70 -0
  198. package/dist/cli/authHandler.js.map +1 -0
  199. package/dist/cli/checkHandler.d.ts +27 -0
  200. package/dist/cli/checkHandler.d.ts.map +1 -0
  201. package/dist/cli/checkHandler.js +560 -0
  202. package/dist/cli/checkHandler.js.map +1 -0
  203. package/dist/cli/daemon.d.ts +30 -0
  204. package/dist/cli/daemon.d.ts.map +1 -0
  205. package/dist/cli/daemon.js +141 -0
  206. package/dist/cli/daemon.js.map +1 -0
  207. package/dist/cli/factoryCommands.d.ts +3 -0
  208. package/dist/cli/factoryCommands.d.ts.map +1 -0
  209. package/dist/cli/factoryCommands.js +165 -0
  210. package/dist/cli/factoryCommands.js.map +1 -0
  211. package/dist/cli/promptHandler.d.ts +13 -0
  212. package/dist/cli/promptHandler.d.ts.map +1 -0
  213. package/dist/cli/promptHandler.js +193 -0
  214. package/dist/cli/promptHandler.js.map +1 -0
  215. package/dist/cli.d.ts +3 -0
  216. package/dist/cli.d.ts.map +1 -0
  217. package/dist/cli.js +320 -0
  218. package/dist/cli.js.map +1 -0
  219. package/dist/core/agentLifecycle.d.ts +322 -0
  220. package/dist/core/agentLifecycle.d.ts.map +1 -0
  221. package/dist/core/agentLifecycle.js +230 -0
  222. package/dist/core/agentLifecycle.js.map +1 -0
  223. package/dist/core/areaMapping.d.ts +9 -0
  224. package/dist/core/areaMapping.d.ts.map +1 -0
  225. package/dist/core/areaMapping.js +37 -0
  226. package/dist/core/areaMapping.js.map +1 -0
  227. package/dist/core/config.d.ts +469 -0
  228. package/dist/core/config.d.ts.map +1 -0
  229. package/dist/core/config.js +780 -0
  230. package/dist/core/config.js.map +1 -0
  231. package/dist/core/dashboardContract.d.ts +204 -0
  232. package/dist/core/dashboardContract.d.ts.map +1 -0
  233. package/dist/core/dashboardContract.js +205 -0
  234. package/dist/core/dashboardContract.js.map +1 -0
  235. package/dist/core/devopsModel.d.ts +138 -0
  236. package/dist/core/devopsModel.d.ts.map +1 -0
  237. package/dist/core/devopsModel.js +137 -0
  238. package/dist/core/devopsModel.js.map +1 -0
  239. package/dist/core/envFile.d.ts +11 -0
  240. package/dist/core/envFile.d.ts.map +1 -0
  241. package/dist/core/envFile.js +104 -0
  242. package/dist/core/envFile.js.map +1 -0
  243. package/dist/core/eventHub.d.ts +220 -0
  244. package/dist/core/eventHub.d.ts.map +1 -0
  245. package/dist/core/eventHub.js +136 -0
  246. package/dist/core/eventHub.js.map +1 -0
  247. package/dist/core/index.d.ts +8 -0
  248. package/dist/core/index.d.ts.map +1 -0
  249. package/dist/core/index.js +7 -0
  250. package/dist/core/index.js.map +1 -0
  251. package/dist/core/laneExecutionState.d.ts +29 -0
  252. package/dist/core/laneExecutionState.d.ts.map +1 -0
  253. package/dist/core/laneExecutionState.js +18 -0
  254. package/dist/core/laneExecutionState.js.map +1 -0
  255. package/dist/core/laneStatus.d.ts +49 -0
  256. package/dist/core/laneStatus.d.ts.map +1 -0
  257. package/dist/core/laneStatus.js +153 -0
  258. package/dist/core/laneStatus.js.map +1 -0
  259. package/dist/core/prSidecar.d.ts +96 -0
  260. package/dist/core/prSidecar.d.ts.map +1 -0
  261. package/dist/core/prSidecar.js +33 -0
  262. package/dist/core/prSidecar.js.map +1 -0
  263. package/dist/core/runtimeConfig.d.ts +6 -0
  264. package/dist/core/runtimeConfig.d.ts.map +1 -0
  265. package/dist/core/runtimeConfig.js +24 -0
  266. package/dist/core/runtimeConfig.js.map +1 -0
  267. package/dist/core/scmProvider.d.ts +19 -0
  268. package/dist/core/scmProvider.d.ts.map +1 -0
  269. package/dist/core/scmProvider.js +38 -0
  270. package/dist/core/scmProvider.js.map +1 -0
  271. package/dist/core/service.d.ts +10 -0
  272. package/dist/core/service.d.ts.map +1 -0
  273. package/dist/core/service.js +297 -0
  274. package/dist/core/service.js.map +1 -0
  275. package/dist/core/traceCollector.d.ts +105 -0
  276. package/dist/core/traceCollector.d.ts.map +1 -0
  277. package/dist/core/traceCollector.js +141 -0
  278. package/dist/core/traceCollector.js.map +1 -0
  279. package/dist/core/types.d.ts +432 -0
  280. package/dist/core/types.d.ts.map +1 -0
  281. package/dist/core/types.js +2 -0
  282. package/dist/core/types.js.map +1 -0
  283. package/dist/core/workItemMapper.d.ts +39 -0
  284. package/dist/core/workItemMapper.d.ts.map +1 -0
  285. package/dist/core/workItemMapper.js +427 -0
  286. package/dist/core/workItemMapper.js.map +1 -0
  287. package/dist/core/workItemModel.d.ts +120 -0
  288. package/dist/core/workItemModel.d.ts.map +1 -0
  289. package/dist/core/workItemModel.js +104 -0
  290. package/dist/core/workItemModel.js.map +1 -0
  291. package/dist/core/workItemPayload.d.ts +195 -0
  292. package/dist/core/workItemPayload.d.ts.map +1 -0
  293. package/dist/core/workItemPayload.js +24 -0
  294. package/dist/core/workItemPayload.js.map +1 -0
  295. package/dist/core/workspaceConfig.d.ts +57 -0
  296. package/dist/core/workspaceConfig.d.ts.map +1 -0
  297. package/dist/core/workspaceConfig.js +184 -0
  298. package/dist/core/workspaceConfig.js.map +1 -0
  299. package/dist/doctor.d.ts +18 -0
  300. package/dist/doctor.d.ts.map +1 -0
  301. package/dist/doctor.js +34 -0
  302. package/dist/doctor.js.map +1 -0
  303. package/dist/factory/activeSkill.d.ts +11 -0
  304. package/dist/factory/activeSkill.d.ts.map +1 -0
  305. package/dist/factory/activeSkill.js +44 -0
  306. package/dist/factory/activeSkill.js.map +1 -0
  307. package/dist/factory/assignment.d.ts +54 -0
  308. package/dist/factory/assignment.d.ts.map +1 -0
  309. package/dist/factory/assignment.js +94 -0
  310. package/dist/factory/assignment.js.map +1 -0
  311. package/dist/factory/auditLog.d.ts +10 -0
  312. package/dist/factory/auditLog.d.ts.map +1 -0
  313. package/dist/factory/auditLog.js +38 -0
  314. package/dist/factory/auditLog.js.map +1 -0
  315. package/dist/factory/closureRequirements.d.ts +12 -0
  316. package/dist/factory/closureRequirements.d.ts.map +1 -0
  317. package/dist/factory/closureRequirements.js +30 -0
  318. package/dist/factory/closureRequirements.js.map +1 -0
  319. package/dist/factory/delegationPrompt.d.ts +3 -0
  320. package/dist/factory/delegationPrompt.d.ts.map +1 -0
  321. package/dist/factory/delegationPrompt.js +16 -0
  322. package/dist/factory/delegationPrompt.js.map +1 -0
  323. package/dist/factory/http.d.ts +3 -0
  324. package/dist/factory/http.d.ts.map +1 -0
  325. package/dist/factory/http.js +555 -0
  326. package/dist/factory/http.js.map +1 -0
  327. package/dist/factory/lifecyclePushMap.d.ts +4 -0
  328. package/dist/factory/lifecyclePushMap.d.ts.map +1 -0
  329. package/dist/factory/lifecyclePushMap.js +7 -0
  330. package/dist/factory/lifecyclePushMap.js.map +1 -0
  331. package/dist/factory/missions.d.ts +125 -0
  332. package/dist/factory/missions.d.ts.map +1 -0
  333. package/dist/factory/missions.js +304 -0
  334. package/dist/factory/missions.js.map +1 -0
  335. package/dist/factory/mode.d.ts +9 -0
  336. package/dist/factory/mode.d.ts.map +1 -0
  337. package/dist/factory/mode.js +30 -0
  338. package/dist/factory/mode.js.map +1 -0
  339. package/dist/factory/operatorActiveSkill.d.ts +15 -0
  340. package/dist/factory/operatorActiveSkill.d.ts.map +1 -0
  341. package/dist/factory/operatorActiveSkill.js +95 -0
  342. package/dist/factory/operatorActiveSkill.js.map +1 -0
  343. package/dist/factory/paseoDispatcher.d.ts +52 -0
  344. package/dist/factory/paseoDispatcher.d.ts.map +1 -0
  345. package/dist/factory/paseoDispatcher.js +122 -0
  346. package/dist/factory/paseoDispatcher.js.map +1 -0
  347. package/dist/factory/paseoLifecycle.d.ts +32 -0
  348. package/dist/factory/paseoLifecycle.d.ts.map +1 -0
  349. package/dist/factory/paseoLifecycle.js +260 -0
  350. package/dist/factory/paseoLifecycle.js.map +1 -0
  351. package/dist/factory/paths.d.ts +31 -0
  352. package/dist/factory/paths.d.ts.map +1 -0
  353. package/dist/factory/paths.js +139 -0
  354. package/dist/factory/paths.js.map +1 -0
  355. package/dist/factory/progressWatchdog.d.ts +58 -0
  356. package/dist/factory/progressWatchdog.d.ts.map +1 -0
  357. package/dist/factory/progressWatchdog.js +160 -0
  358. package/dist/factory/progressWatchdog.js.map +1 -0
  359. package/dist/factory/roster.d.ts +59 -0
  360. package/dist/factory/roster.d.ts.map +1 -0
  361. package/dist/factory/roster.js +116 -0
  362. package/dist/factory/roster.js.map +1 -0
  363. package/dist/factory/runtime.d.ts +44 -0
  364. package/dist/factory/runtime.d.ts.map +1 -0
  365. package/dist/factory/runtime.js +238 -0
  366. package/dist/factory/runtime.js.map +1 -0
  367. package/dist/factory/sync.d.ts +29 -0
  368. package/dist/factory/sync.d.ts.map +1 -0
  369. package/dist/factory/sync.js +77 -0
  370. package/dist/factory/sync.js.map +1 -0
  371. package/dist/factory/workitemQueues.d.ts +37 -0
  372. package/dist/factory/workitemQueues.d.ts.map +1 -0
  373. package/dist/factory/workitemQueues.js +99 -0
  374. package/dist/factory/workitemQueues.js.map +1 -0
  375. package/dist/factory/workitemTriage.d.ts +9 -0
  376. package/dist/factory/workitemTriage.d.ts.map +1 -0
  377. package/dist/factory/workitemTriage.js +81 -0
  378. package/dist/factory/workitemTriage.js.map +1 -0
  379. package/dist/hooks.d.ts +18 -0
  380. package/dist/hooks.d.ts.map +1 -0
  381. package/dist/hooks.js +96 -0
  382. package/dist/hooks.js.map +1 -0
  383. package/dist/index.d.ts +3 -0
  384. package/dist/index.d.ts.map +1 -0
  385. package/dist/index.js +90 -0
  386. package/dist/index.js.map +1 -0
  387. package/dist/install/agentCatalog.d.ts +7 -0
  388. package/dist/install/agentCatalog.d.ts.map +1 -0
  389. package/dist/install/agentCatalog.js +28 -0
  390. package/dist/install/agentCatalog.js.map +1 -0
  391. package/dist/install/bundlePaths.d.ts +10 -0
  392. package/dist/install/bundlePaths.d.ts.map +1 -0
  393. package/dist/install/bundlePaths.js +30 -0
  394. package/dist/install/bundlePaths.js.map +1 -0
  395. package/dist/install/codex.d.ts +43 -0
  396. package/dist/install/codex.d.ts.map +1 -0
  397. package/dist/install/codex.js +207 -0
  398. package/dist/install/codex.js.map +1 -0
  399. package/dist/install/enactHome.d.ts +37 -0
  400. package/dist/install/enactHome.d.ts.map +1 -0
  401. package/dist/install/enactHome.js +152 -0
  402. package/dist/install/enactHome.js.map +1 -0
  403. package/dist/install/plugins.d.ts +115 -0
  404. package/dist/install/plugins.d.ts.map +1 -0
  405. package/dist/install/plugins.js +259 -0
  406. package/dist/install/plugins.js.map +1 -0
  407. package/dist/install/setup.d.ts +33 -0
  408. package/dist/install/setup.d.ts.map +1 -0
  409. package/dist/install/setup.js +167 -0
  410. package/dist/install/setup.js.map +1 -0
  411. package/dist/locale/en.d.ts +3 -0
  412. package/dist/locale/en.d.ts.map +1 -0
  413. package/dist/locale/en.js +435 -0
  414. package/dist/locale/en.js.map +1 -0
  415. package/dist/locale/index.d.ts +28 -0
  416. package/dist/locale/index.d.ts.map +1 -0
  417. package/dist/locale/index.js +84 -0
  418. package/dist/locale/index.js.map +1 -0
  419. package/dist/locale/prompts/en.d.ts +3 -0
  420. package/dist/locale/prompts/en.d.ts.map +1 -0
  421. package/dist/locale/prompts/en.js +254 -0
  422. package/dist/locale/prompts/en.js.map +1 -0
  423. package/dist/locale/types.d.ts +433 -0
  424. package/dist/locale/types.d.ts.map +1 -0
  425. package/dist/locale/types.js +5 -0
  426. package/dist/locale/types.js.map +1 -0
  427. package/dist/mcp/server.d.ts +489 -0
  428. package/dist/mcp/server.d.ts.map +1 -0
  429. package/dist/mcp/server.js +597 -0
  430. package/dist/mcp/server.js.map +1 -0
  431. package/dist/orchestration/decisionEngine.d.ts +175 -0
  432. package/dist/orchestration/decisionEngine.d.ts.map +1 -0
  433. package/dist/orchestration/decisionEngine.js +471 -0
  434. package/dist/orchestration/decisionEngine.js.map +1 -0
  435. package/dist/orchestration/index.d.ts +5 -0
  436. package/dist/orchestration/index.d.ts.map +1 -0
  437. package/dist/orchestration/index.js +5 -0
  438. package/dist/orchestration/index.js.map +1 -0
  439. package/dist/orchestration/workItemParser.d.ts +67 -0
  440. package/dist/orchestration/workItemParser.d.ts.map +1 -0
  441. package/dist/orchestration/workItemParser.js +560 -0
  442. package/dist/orchestration/workItemParser.js.map +1 -0
  443. package/dist/orchestration/workItemScheduler.d.ts +141 -0
  444. package/dist/orchestration/workItemScheduler.d.ts.map +1 -0
  445. package/dist/orchestration/workItemScheduler.js +317 -0
  446. package/dist/orchestration/workItemScheduler.js.map +1 -0
  447. package/dist/orchestration/workflow.d.ts +145 -0
  448. package/dist/orchestration/workflow.d.ts.map +1 -0
  449. package/dist/orchestration/workflow.js +301 -0
  450. package/dist/orchestration/workflow.js.map +1 -0
  451. package/dist/providers/codexSessions.d.ts +93 -0
  452. package/dist/providers/codexSessions.d.ts.map +1 -0
  453. package/dist/providers/codexSessions.js +366 -0
  454. package/dist/providers/codexSessions.js.map +1 -0
  455. package/dist/registry/bsDetector.d.ts +24 -0
  456. package/dist/registry/bsDetector.d.ts.map +1 -0
  457. package/dist/registry/bsDetector.js +276 -0
  458. package/dist/registry/bsDetector.js.map +1 -0
  459. package/dist/registry/entityScanner.d.ts +36 -0
  460. package/dist/registry/entityScanner.d.ts.map +1 -0
  461. package/dist/registry/entityScanner.js +693 -0
  462. package/dist/registry/entityScanner.js.map +1 -0
  463. package/dist/registry/index.d.ts +9 -0
  464. package/dist/registry/index.d.ts.map +1 -0
  465. package/dist/registry/index.js +13 -0
  466. package/dist/registry/index.js.map +1 -0
  467. package/dist/registry/schema.d.ts +307 -0
  468. package/dist/registry/schema.d.ts.map +1 -0
  469. package/dist/registry/schema.js +139 -0
  470. package/dist/registry/schema.js.map +1 -0
  471. package/dist/registry/sqliteStore.d.ts +101 -0
  472. package/dist/registry/sqliteStore.d.ts.map +1 -0
  473. package/dist/registry/sqliteStore.js +688 -0
  474. package/dist/registry/sqliteStore.js.map +1 -0
  475. package/dist/registry/workItemBridge.d.ts +8 -0
  476. package/dist/registry/workItemBridge.d.ts.map +1 -0
  477. package/dist/registry/workItemBridge.js +30 -0
  478. package/dist/registry/workItemBridge.js.map +1 -0
  479. package/dist/runners/cliRunner.d.ts +11 -0
  480. package/dist/runners/cliRunner.d.ts.map +1 -0
  481. package/dist/runners/cliRunner.js +193 -0
  482. package/dist/runners/cliRunner.js.map +1 -0
  483. package/dist/support/apiCache.d.ts +85 -0
  484. package/dist/support/apiCache.d.ts.map +1 -0
  485. package/dist/support/apiCache.js +163 -0
  486. package/dist/support/apiCache.js.map +1 -0
  487. package/dist/support/chat.d.ts +3 -0
  488. package/dist/support/chat.d.ts.map +1 -0
  489. package/dist/support/chat.js +305 -0
  490. package/dist/support/chat.js.map +1 -0
  491. package/dist/support/chatBackend.d.ts +25 -0
  492. package/dist/support/chatBackend.d.ts.map +1 -0
  493. package/dist/support/chatBackend.js +289 -0
  494. package/dist/support/chatBackend.js.map +1 -0
  495. package/dist/support/chatTui.d.ts +3 -0
  496. package/dist/support/chatTui.d.ts.map +1 -0
  497. package/dist/support/chatTui.js +1082 -0
  498. package/dist/support/chatTui.js.map +1 -0
  499. package/dist/support/costTracker.d.ts +29 -0
  500. package/dist/support/costTracker.d.ts.map +1 -0
  501. package/dist/support/costTracker.js +113 -0
  502. package/dist/support/costTracker.js.map +1 -0
  503. package/dist/support/dashboardHtml.d.ts +5 -0
  504. package/dist/support/dashboardHtml.d.ts.map +1 -0
  505. package/dist/support/dashboardHtml.js +2629 -0
  506. package/dist/support/dashboardHtml.js.map +1 -0
  507. package/dist/support/dev.d.ts +55 -0
  508. package/dist/support/dev.d.ts.map +1 -0
  509. package/dist/support/dev.js +298 -0
  510. package/dist/support/dev.js.map +1 -0
  511. package/dist/support/editParser.d.ts +37 -0
  512. package/dist/support/editParser.d.ts.map +1 -0
  513. package/dist/support/editParser.js +365 -0
  514. package/dist/support/editParser.js.map +1 -0
  515. package/dist/support/ghosttyThemeCatalog.generated.d.ts +2 -0
  516. package/dist/support/ghosttyThemeCatalog.generated.d.ts.map +1 -0
  517. package/dist/support/ghosttyThemeCatalog.generated.js +11116 -0
  518. package/dist/support/ghosttyThemeCatalog.generated.js.map +1 -0
  519. package/dist/support/gitStatus.d.ts +21 -0
  520. package/dist/support/gitStatus.d.ts.map +1 -0
  521. package/dist/support/gitStatus.js +108 -0
  522. package/dist/support/gitStatus.js.map +1 -0
  523. package/dist/support/gitTracker.d.ts +30 -0
  524. package/dist/support/gitTracker.d.ts.map +1 -0
  525. package/dist/support/gitTracker.js +143 -0
  526. package/dist/support/gitTracker.js.map +1 -0
  527. package/dist/support/index.d.ts +12 -0
  528. package/dist/support/index.d.ts.map +1 -0
  529. package/dist/support/index.js +12 -0
  530. package/dist/support/index.js.map +1 -0
  531. package/dist/support/planner.d.ts +64 -0
  532. package/dist/support/planner.d.ts.map +1 -0
  533. package/dist/support/planner.js +396 -0
  534. package/dist/support/planner.js.map +1 -0
  535. package/dist/support/projectMapper.d.ts +46 -0
  536. package/dist/support/projectMapper.d.ts.map +1 -0
  537. package/dist/support/projectMapper.js +273 -0
  538. package/dist/support/projectMapper.js.map +1 -0
  539. package/dist/support/pty-helper.py +117 -0
  540. package/dist/support/quotaTracker.d.ts +29 -0
  541. package/dist/support/quotaTracker.d.ts.map +1 -0
  542. package/dist/support/quotaTracker.js +89 -0
  543. package/dist/support/quotaTracker.js.map +1 -0
  544. package/dist/support/rateLimiter.d.ts +101 -0
  545. package/dist/support/rateLimiter.d.ts.map +1 -0
  546. package/dist/support/rateLimiter.js +219 -0
  547. package/dist/support/rateLimiter.js.map +1 -0
  548. package/dist/support/rollback.d.ts +61 -0
  549. package/dist/support/rollback.d.ts.map +1 -0
  550. package/dist/support/rollback.js +329 -0
  551. package/dist/support/rollback.js.map +1 -0
  552. package/dist/support/sharedShell.d.ts +17 -0
  553. package/dist/support/sharedShell.d.ts.map +1 -0
  554. package/dist/support/sharedShell.js +439 -0
  555. package/dist/support/sharedShell.js.map +1 -0
  556. package/dist/support/stuckDetector.d.ts +68 -0
  557. package/dist/support/stuckDetector.d.ts.map +1 -0
  558. package/dist/support/stuckDetector.js +174 -0
  559. package/dist/support/stuckDetector.js.map +1 -0
  560. package/dist/support/terminalBridge.d.ts +18 -0
  561. package/dist/support/terminalBridge.d.ts.map +1 -0
  562. package/dist/support/terminalBridge.js +553 -0
  563. package/dist/support/terminalBridge.js.map +1 -0
  564. package/dist/support/timeWindow.d.ts +60 -0
  565. package/dist/support/timeWindow.d.ts.map +1 -0
  566. package/dist/support/timeWindow.js +236 -0
  567. package/dist/support/timeWindow.js.map +1 -0
  568. package/dist/support/uiThemes.d.ts +44 -0
  569. package/dist/support/uiThemes.d.ts.map +1 -0
  570. package/dist/support/uiThemes.js +290 -0
  571. package/dist/support/uiThemes.js.map +1 -0
  572. package/dist/support/web.d.ts +29 -0
  573. package/dist/support/web.d.ts.map +1 -0
  574. package/dist/support/web.js +1097 -0
  575. package/dist/support/web.js.map +1 -0
  576. package/dist/support/worktreeManager.d.ts +20 -0
  577. package/dist/support/worktreeManager.d.ts.map +1 -0
  578. package/dist/support/worktreeManager.js +140 -0
  579. package/dist/support/worktreeManager.js.map +1 -0
  580. package/dist/task_state_model.py +55 -0
  581. package/dist/workItemState/store.d.ts +122 -0
  582. package/dist/workItemState/store.d.ts.map +1 -0
  583. package/dist/workItemState/store.js +438 -0
  584. package/dist/workItemState/store.js.map +1 -0
  585. package/dist/workItems/azdoBridge.d.ts +42 -0
  586. package/dist/workItems/azdoBridge.d.ts.map +1 -0
  587. package/dist/workItems/azdoBridge.js +143 -0
  588. package/dist/workItems/azdoBridge.js.map +1 -0
  589. package/dist/workItems/azdoSyncRuntime.d.ts +28 -0
  590. package/dist/workItems/azdoSyncRuntime.d.ts.map +1 -0
  591. package/dist/workItems/azdoSyncRuntime.js +158 -0
  592. package/dist/workItems/azdoSyncRuntime.js.map +1 -0
  593. package/dist/workItems/azureDevOpsSync.d.ts +128 -0
  594. package/dist/workItems/azureDevOpsSync.d.ts.map +1 -0
  595. package/dist/workItems/azureDevOpsSync.js +748 -0
  596. package/dist/workItems/azureDevOpsSync.js.map +1 -0
  597. package/dist/workItems/helpers.d.ts +11 -0
  598. package/dist/workItems/helpers.d.ts.map +1 -0
  599. package/dist/workItems/helpers.js +17 -0
  600. package/dist/workItems/helpers.js.map +1 -0
  601. package/dist/workItems/index.d.ts +21 -0
  602. package/dist/workItems/index.d.ts.map +1 -0
  603. package/dist/workItems/index.js +89 -0
  604. package/dist/workItems/index.js.map +1 -0
  605. package/dist/workItems/localWorkItemFetcher.d.ts +55 -0
  606. package/dist/workItems/localWorkItemFetcher.d.ts.map +1 -0
  607. package/dist/workItems/localWorkItemFetcher.js +209 -0
  608. package/dist/workItems/localWorkItemFetcher.js.map +1 -0
  609. package/dist/workItems/migrations/001_rename_workItem_to_work_item.sql +10 -0
  610. package/dist/workItems/postgresStore.d.ts +78 -0
  611. package/dist/workItems/postgresStore.d.ts.map +1 -0
  612. package/dist/workItems/postgresStore.js +937 -0
  613. package/dist/workItems/postgresStore.js.map +1 -0
  614. package/dist/workItems/schema.d.ts +257 -0
  615. package/dist/workItems/schema.d.ts.map +1 -0
  616. package/dist/workItems/schema.js +176 -0
  617. package/dist/workItems/schema.js.map +1 -0
  618. package/dist/workItems/sqliteStore.d.ts +124 -0
  619. package/dist/workItems/sqliteStore.d.ts.map +1 -0
  620. package/dist/workItems/sqliteStore.js +713 -0
  621. package/dist/workItems/sqliteStore.js.map +1 -0
  622. package/dist/workItems/workItemBoardHtml.d.ts +5 -0
  623. package/dist/workItems/workItemBoardHtml.d.ts.map +1 -0
  624. package/dist/workItems/workItemBoardHtml.js +2192 -0
  625. package/dist/workItems/workItemBoardHtml.js.map +1 -0
  626. package/package.json +99 -0
  627. package/templates/AGENTS.md +432 -0
  628. package/templates/BOOT.md +25 -0
  629. package/templates/BOOTSTRAP.md +50 -0
  630. package/templates/CHANGELOG_AUDIT.md +74 -0
  631. package/templates/HEARTBEAT.md +86 -0
  632. package/templates/IDENTITY.md +27 -0
  633. package/templates/PR_LAND.md +75 -0
  634. package/templates/PR_REVIEW.md +97 -0
  635. package/templates/SOUL.dev.md +52 -0
  636. package/templates/SOUL.md +81 -0
  637. package/templates/TOOLS.md +52 -0
  638. package/templates/USER.md +22 -0
  639. package/templates/WORKITEM_ANALYSIS.md +31 -0
  640. package/templates/agents/executor.md +26 -0
  641. package/templates/agents/plan.md +22 -0
  642. package/templates/agents/ralph.md +37 -0
  643. package/templates/agents/review.md +22 -0
  644. package/templates/agents/team.md +39 -0
@@ -0,0 +1,748 @@
1
+ // ============================================
2
+ // EnactFactory - Azure DevOps Heartbeat Sync Orchestrator
3
+ // Pull-down from AzDO, push-up local changes, stale detection, SSE events.
4
+ // ============================================
5
+ import { execFile } from 'node:child_process';
6
+ import { promisify } from 'node:util';
7
+ import { AzdoApiError, createAzdoClient } from '../azureDevOps/client.js';
8
+ import { DEFAULT_FIELD_NAMES, azdoToWorkItemRecord, azdoToWorkItemPayload, payloadToAzdoPatches, azdoToArea } from '../azureDevOps/mapper.js';
9
+ import { loadConfig } from '../core/config.js';
10
+ import { provisionHierarchy } from '../azureDevOps/hierarchy.js';
11
+ import { DEFAULT_AZDO_TERMINAL_STATES, localStatusToAzdoState, resolveTerminalAzdoStates, } from '../azureDevOps/stateMapping.js';
12
+ import { resolveProjectConfigs } from '../core/runtimeConfig.js';
13
+ import { EventEmitter } from 'node:events';
14
+ import { drainPushQueue } from '../factory/workitemQueues.js';
15
+ const execFileAsync = promisify(execFile);
16
+ /**
17
+ * Drains the workitem-push-queue at `root` by calling pushLocalChange for
18
+ * each pending entry. Entries that succeed are marked drained. Entries that
19
+ * exhaust all attempts remain in the queue and are reported in result.errors.
20
+ *
21
+ * Exponential backoff: delay = backoffMs * 2^attempt (capped at 30 000 ms).
22
+ */
23
+ export async function drainWorkItemPushQueue(root, context, options = {}) {
24
+ const maxAttempts = options.maxAttemptsPerEntry ?? 3;
25
+ const backoffMs = options.backoffMs ?? 1000;
26
+ const errors = [];
27
+ // Build an inline pushLocalChange equivalent using the provided context.
28
+ const { client, store, config, events } = context;
29
+ const fieldNames = { ...DEFAULT_FIELD_NAMES, ...config.fieldNames };
30
+ async function pushEntry(workItemId) {
31
+ const local = await store.getWorkItem(workItemId);
32
+ if (!local) {
33
+ console.warn(`[AzdoSync] drainPushQueue: item ${workItemId} not found in local store — skipping`);
34
+ return;
35
+ }
36
+ const numericId = parseInt(workItemId, 10);
37
+ if (isNaN(numericId)) {
38
+ console.warn(`[AzdoSync] drainPushQueue: ${workItemId} is not a numeric AzDO ID — skipping`);
39
+ return;
40
+ }
41
+ // Source the patch from local intent (stored payload + local title), not a
42
+ // fresh AzDO fetch — otherwise we re-derive from AzDO's current state and
43
+ // clobber the local edit we're draining.
44
+ const patches = await buildLocalPushPatches(store, fieldNames, workItemId, local, context.config);
45
+ try {
46
+ await withExponentialBackoff(() => client.updateWorkItem(numericId, patches), 3, 1000, 30000);
47
+ }
48
+ catch (err) {
49
+ if (err instanceof AzdoApiError && err.status !== 429 && err.status !== 503) {
50
+ console.error(`[AzdoSync] drainPushQueue push failed for ${workItemId}:`, err instanceof Error ? err.message : String(err));
51
+ events.emit('azdo.push.failed', { workItemId, status: err.status, message: err.message });
52
+ // Non-retriable — surface as resolved so we don't re-queue indefinitely.
53
+ return;
54
+ }
55
+ throw err;
56
+ }
57
+ }
58
+ const queueResult = await drainPushQueue(root, async (entry) => {
59
+ let lastErr;
60
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
61
+ try {
62
+ await pushEntry(entry.workItemId);
63
+ return; // success → drainPushQueue marks this entry drained
64
+ }
65
+ catch (err) {
66
+ lastErr = err;
67
+ if (attempt < maxAttempts - 1) {
68
+ await new Promise((r) => setTimeout(r, backoffMs * Math.pow(2, attempt)));
69
+ }
70
+ }
71
+ }
72
+ // All attempts exhausted — record for forensics and rethrow so
73
+ // drainPushQueue does NOT mark this entry drained.
74
+ errors.push({
75
+ entryId: entry.id,
76
+ workItemId: entry.workItemId,
77
+ attempts: maxAttempts,
78
+ lastError: lastErr instanceof Error ? lastErr.message : String(lastErr),
79
+ });
80
+ throw lastErr;
81
+ });
82
+ return { drained: queueResult.drained, errors };
83
+ }
84
+ // ---------------------------------------------------------------------------
85
+ // Internal helpers
86
+ // ---------------------------------------------------------------------------
87
+ const BATCH_SIZE = 200;
88
+ // Default Scrum terminal states — open = not in these. PBI/Bug (the Scrum
89
+ // process template) end their lifecycle at exactly 'Done' or 'Removed'.
90
+ // Overridable per-workspace via AzdoSyncConfig.terminalStates.
91
+ export const AZDO_TERMINAL_STATES = DEFAULT_AZDO_TERMINAL_STATES;
92
+ function buildWiql(project, areaFilter, changedSince, terminalStates = AZDO_TERMINAL_STATES) {
93
+ // Board scope: only executable work items (Product Backlog Item, Bug).
94
+ // Epic/Feature are planning containers — they live in AzDO for hierarchy
95
+ // and breadcrumbs but are out of scope for the Factory board.
96
+ let wiql = `SELECT [System.Id] FROM WorkItems` +
97
+ ` WHERE [System.TeamProject] = '${project}'` +
98
+ ` AND [System.WorkItemType] IN ('Product Backlog Item', 'Bug')`;
99
+ // Open items only — exclude terminal states so the fetch stays bounded at
100
+ // scale and the board mirrors only live work. Applied in both modes.
101
+ const terminalList = terminalStates.map((s) => `'${s}'`).join(', ');
102
+ wiql += ` AND [System.State] NOT IN (${terminalList})`;
103
+ if (areaFilter) {
104
+ wiql += ` AND [System.AreaPath] UNDER '${areaFilter}'`;
105
+ }
106
+ if (changedSince) {
107
+ // Incremental mode: only items touched since the watermark, ordered by
108
+ // ChangedDate ASC so the watermark can advance monotonically.
109
+ //
110
+ // AzDO WIQL requires DATE precision for [System.ChangedDate] comparisons —
111
+ // supplying a full ISO timestamp (e.g. "2026-06-16T00:24:05.860Z") causes
112
+ // HTTP 400: "You cannot supply a time with the date when running a query
113
+ // using date precision". Truncate to YYYY-MM-DD.
114
+ //
115
+ // Date-precision re-scans from midnight of the watermark's date, so items
116
+ // changed earlier the same day are re-fetched on the next tick. That is
117
+ // safe: mergeRecord is idempotent (no-op when local is already current) and
118
+ // the stored watermark stays full-ISO so the monotonic advance is preserved.
119
+ const azdoDate = new Date(changedSince).toISOString().slice(0, 10);
120
+ wiql += ` AND [System.ChangedDate] >= '${azdoDate}'`;
121
+ wiql += ' ORDER BY [System.ChangedDate] ASC';
122
+ }
123
+ else {
124
+ // Full mode: stable id ordering over the complete set.
125
+ wiql += ' ORDER BY [System.Id]';
126
+ }
127
+ return wiql;
128
+ }
129
+ async function sleep(ms) {
130
+ return new Promise((resolve) => setTimeout(resolve, ms));
131
+ }
132
+ function extractParentId(workItem) {
133
+ const relation = workItem.relations?.find((entry) => entry.rel === 'System.LinkTypes.Hierarchy-Reverse');
134
+ if (!relation?.url)
135
+ return null;
136
+ const match = relation.url.match(/workItems\/(\d+)/i);
137
+ return match ? match[1] : null;
138
+ }
139
+ async function withExponentialBackoff(fn, maxAttempts, baseDelayMs, capMs) {
140
+ let delay = baseDelayMs;
141
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
142
+ try {
143
+ return await fn();
144
+ }
145
+ catch (err) {
146
+ if (attempt === maxAttempts)
147
+ throw err;
148
+ // Only retry on 429 / 503
149
+ if (err instanceof AzdoApiError && err.status !== 429 && err.status !== 503) {
150
+ throw err;
151
+ }
152
+ await sleep(Math.min(delay, capMs));
153
+ delay *= 2;
154
+ }
155
+ }
156
+ // Unreachable — satisfies TS
157
+ throw new Error('withExponentialBackoff: exhausted attempts');
158
+ }
159
+ function mergeRecord(local, azdoRecord, azdoChangedDate, payloadFields) {
160
+ // Conflict rule: if local was updated after AzDO's changedDate, keep local for AzDO-owned fields
161
+ const localUpdated = new Date(local.updatedAt).getTime();
162
+ const azdoChanged = new Date(azdoChangedDate).getTime();
163
+ if (localUpdated > azdoChanged) {
164
+ // Local is newer — keep it as-is
165
+ return { merged: {}, changed: false };
166
+ }
167
+ // AzDO is newer — accept AzDO-owned fields
168
+ const patch = {};
169
+ let changed = false;
170
+ // Flat fields that azdoToWorkItemRecord produces.
171
+ const fields = [
172
+ 'title', 'description', 'status', 'priority',
173
+ ];
174
+ for (const field of fields) {
175
+ const azdoVal = azdoRecord[field];
176
+ if (azdoVal !== undefined && local[field] !== azdoVal) {
177
+ patch[field] = azdoVal;
178
+ changed = true;
179
+ }
180
+ }
181
+ // Payload-derived fields (area, kind, assignee) — these ARE DB columns but are
182
+ // not produced by azdoToWorkItemRecord. Merge them here when AzDO is canonical.
183
+ if (payloadFields) {
184
+ // area: merge only when AzDO has a real sub-area (non-null). A null area
185
+ // means the item lives at the project root — don't clobber an existing local area.
186
+ if (payloadFields.area !== null && payloadFields.area !== undefined &&
187
+ local.area !== payloadFields.area) {
188
+ patch.area = payloadFields.area;
189
+ changed = true;
190
+ }
191
+ // kind: always present on synced PBIs/Bugs; update if it changed upstream.
192
+ if (payloadFields.kind !== undefined && local.kind !== payloadFields.kind) {
193
+ patch.kind = payloadFields.kind;
194
+ changed = true;
195
+ }
196
+ // assignee: null means unassigned — propagate that too (clear the local assignee).
197
+ const azdoAssignee = payloadFields.assignee ?? null;
198
+ const localAssignee = local.assignee ?? null;
199
+ if (localAssignee !== azdoAssignee) {
200
+ // Use explicit `null` (not `undefined`) so updateWorkItem writes NULL and
201
+ // actually clears the column. `undefined` would be treated as "leave unchanged"
202
+ // by the Postgres store (and dropped by the `{...existing, ...patch}` spread),
203
+ // so the local assignee would never clear when AzDO unassigns the item.
204
+ patch['assignee'] = azdoAssignee;
205
+ changed = true;
206
+ }
207
+ }
208
+ if (changed) {
209
+ // Stamp the REAL AzDO ChangedDate, not NOW() — otherwise every merged row
210
+ // looks "just now" on the dashboard even though AzDO changed it minutes ago.
211
+ patch.updatedAt = azdoChangedDate;
212
+ }
213
+ return { merged: patch, changed };
214
+ }
215
+ // Canonical assignee re-applied on every drained push (mirrors azdoBridge.ts).
216
+ // Replicated inline — importing from azdoBridge would create an import cycle.
217
+ const AZDO_DEFAULT_ASSIGNEE = 'Tarun Rana <tarun.rana@amsterdamdatalabs.com>';
218
+ /**
219
+ * Build AzDO patches to push a LOCAL change up, sourced from local intent
220
+ * rather than a fresh AzDO fetch. Re-fetching AzDO and re-deriving patches
221
+ * from its current state clobbers the very edit we're trying to push.
222
+ *
223
+ * Strategy: take the last-known stored payload (which carries the
224
+ * AzDO-canonical area/kind from the previous pull), overlay the local record's
225
+ * authoritative title, then OVERRIDE System.State with the value derived from
226
+ * `local.status`. The stored payload's layeredState is the PRE-edit state, so
227
+ * for a queued status change (e.g. a failed soft-delete cancel) trusting it
228
+ * would push a stale non-Removed state and the delete would not stick — the
229
+ * row resurrects on the next full pull. We therefore de-dup any System.State
230
+ * patch the payload produced and append exactly one built from local.status.
231
+ * AssignedTo is (re)applied to honor the same canonical-assignee guarantee as
232
+ * the direct bridge pushes.
233
+ */
234
+ async function buildLocalPushPatches(store, fieldNames, workItemId, local, syncConfig) {
235
+ // Base patches: payload-derived (title/area/booleans) or title-only fallback.
236
+ let base;
237
+ const storedJson = await store.getPayloadJson(workItemId);
238
+ let parsed = null;
239
+ if (storedJson) {
240
+ try {
241
+ parsed = JSON.parse(storedJson);
242
+ }
243
+ catch {
244
+ // Corrupt stored payload — fall through to the title-only fallback.
245
+ }
246
+ }
247
+ if (parsed) {
248
+ const overlaid = { ...parsed, title: local.title };
249
+ base = payloadToAzdoPatches(overlaid, fieldNames);
250
+ }
251
+ else {
252
+ base = [{ op: 'replace', path: `/fields/${fieldNames.title}`, value: local.title }];
253
+ }
254
+ // Drop any System.State patch the payload emitted — its value is the stale
255
+ // pre-edit state. The authoritative state comes from local.status below.
256
+ const statePath = `/fields/${fieldNames.state}`;
257
+ base = base.filter((p) => p.path !== statePath);
258
+ // Append the single authoritative System.State + the canonical AssignedTo.
259
+ base.push({ op: 'add', path: statePath, value: localStatusToAzdoState(local.status, syncConfig) });
260
+ base.push({ op: 'add', path: `/fields/${fieldNames.assignedTo}`, value: AZDO_DEFAULT_ASSIGNEE });
261
+ return base;
262
+ }
263
+ // ---------------------------------------------------------------------------
264
+ // fetchToken factories (exported for service.ts bootstrap)
265
+ // ---------------------------------------------------------------------------
266
+ export function makePATFetcher(patEnvVar) {
267
+ return async () => {
268
+ const pat = process.env[patEnvVar];
269
+ if (!pat) {
270
+ throw new Error(`AzDO PAT environment variable "${patEnvVar}" is not set`);
271
+ }
272
+ return `Basic ${Buffer.from(':' + pat).toString('base64')}`;
273
+ };
274
+ }
275
+ export function makeAzCliTokenFetcher(tenant) {
276
+ return async () => {
277
+ const args = [
278
+ 'account', 'get-access-token',
279
+ '--resource', '499b84ac-1321-427f-aa17-267ca6975798',
280
+ ];
281
+ if (tenant) {
282
+ args.push('--tenant', tenant);
283
+ }
284
+ args.push('--query', 'accessToken', '-o', 'tsv');
285
+ const { stdout } = await execFileAsync('az', args);
286
+ return stdout.trim();
287
+ };
288
+ }
289
+ // ---------------------------------------------------------------------------
290
+ // AzdoSyncContext builder — shared by the service and MCP tool
291
+ // ---------------------------------------------------------------------------
292
+ /**
293
+ * Build an AzdoSyncContext from workspace config.
294
+ * Organization / project come from config.toml; PAT remains a secret env var.
295
+ */
296
+ export function buildAzdoSyncContextFromConfig(store) {
297
+ const loadedConfig = loadConfig();
298
+ const azdoCfg = loadedConfig.azureDevOps;
299
+ if (!azdoCfg?.enabled) {
300
+ throw new Error('factory_workitem_drain_push_queue: Azure DevOps sync is disabled in config.toml');
301
+ }
302
+ const fetchToken = azdoCfg.authStrategy === 'pat'
303
+ ? makePATFetcher(azdoCfg.patEnvVar ?? 'AZDO_PAT')
304
+ : makeAzCliTokenFetcher('amsterdamdatalabs.com');
305
+ const client = createAzdoClient({ organization: azdoCfg.organization, project: azdoCfg.project, fetchToken });
306
+ const events = new EventEmitter();
307
+ const syncConfig = {
308
+ enabled: true,
309
+ organization: azdoCfg.organization,
310
+ project: azdoCfg.project,
311
+ intervalMs: azdoCfg.intervalMs,
312
+ authStrategy: azdoCfg.authStrategy,
313
+ patEnvVar: azdoCfg.patEnvVar,
314
+ fieldNames: azdoCfg.fieldNames,
315
+ areaFilter: azdoCfg.areaFilter,
316
+ azdoStateToStatus: azdoCfg.azdoStateToStatus,
317
+ statusToAzdoState: azdoCfg.statusToAzdoState,
318
+ terminalStates: azdoCfg.terminalStates,
319
+ };
320
+ return { client, store, config: syncConfig, events, fetchToken };
321
+ }
322
+ export function createAzdoSyncService(deps, options) {
323
+ const { client, store, config, events } = deps;
324
+ const drainAfterSync = options?.drainPushQueueAfterSync !== false;
325
+ const root = options?.root;
326
+ const fieldNames = { ...DEFAULT_FIELD_NAMES, ...config.fieldNames };
327
+ // Incremental-sync watermark key. Scoped to org/project/areaFilter so a
328
+ // workspace that re-points at a different project/area starts fresh instead
329
+ // of inheriting a stale ChangedDate cursor.
330
+ const watermarkKey = `azdo:watermark:${config.organization}/${config.project}:${config.areaFilter ?? ''}`;
331
+ // Single source of truth for "what counts as terminal". Drives both the
332
+ // open-only WIQL filter and the reconcile's closed-item detection so they
333
+ // can't drift. Defaults to AZDO_TERMINAL_STATES when config omits it.
334
+ const terminalStates = resolveTerminalAzdoStates(config);
335
+ const terminalStateSet = new Set(terminalStates);
336
+ // State
337
+ let intervalHandle = null;
338
+ let inFlightSync = null;
339
+ let started = false;
340
+ let lastSuccessAt = null;
341
+ let lastErrorAt = null;
342
+ let lastError;
343
+ let wasStale = false;
344
+ // ---------------------------------------------------------------------------
345
+ // Stale detection
346
+ // ---------------------------------------------------------------------------
347
+ function checkStale() {
348
+ const now = Date.now();
349
+ const threshold = config.intervalMs * 3;
350
+ const isStale = lastSuccessAt === null || (now - lastSuccessAt) > threshold;
351
+ if (isStale && !wasStale) {
352
+ wasStale = true;
353
+ events.emit('azdo.sync.stale', { at: now });
354
+ }
355
+ else if (!isStale && wasStale) {
356
+ wasStale = false;
357
+ events.emit('azdo.sync.healthy', { at: now });
358
+ }
359
+ }
360
+ // ---------------------------------------------------------------------------
361
+ // syncOnce — pull-down
362
+ // ---------------------------------------------------------------------------
363
+ async function syncOnce(opts) {
364
+ const t0 = Date.now();
365
+ const result = { pulled: 0, pushed: 0, conflicts: 0, errors: [], durationMs: 0 };
366
+ const mode = opts?.mode ?? 'incremental';
367
+ try {
368
+ // Incremental mode reads the stored watermark and only pulls items whose
369
+ // System.ChangedDate is at or after it. The very first incremental run
370
+ // (no watermark yet) falls through to a full scan so we don't miss the
371
+ // existing backlog. Full mode always scans the complete id set.
372
+ const currentWatermark = await store.getSyncMeta(watermarkKey);
373
+ const effectiveMode = mode === 'incremental' && !currentWatermark ? 'full' : mode;
374
+ const changedSince = effectiveMode === 'incremental' ? currentWatermark ?? undefined : undefined;
375
+ const wiql = buildWiql(config.project, config.areaFilter, changedSince, terminalStates);
376
+ const ids = await client.queryByWiql(wiql);
377
+ // Batch IDs in groups of BATCH_SIZE
378
+ const batches = [];
379
+ for (let i = 0; i < ids.length; i += BATCH_SIZE) {
380
+ batches.push(ids.slice(i, i + BATCH_SIZE));
381
+ }
382
+ // Track every AzDO id we see this heartbeat so we can prune local
383
+ // rows whose upstream item has been deleted in AzDO. Prune only runs in
384
+ // full mode (incremental never sees the complete id set).
385
+ const azdoIdsSeen = new Set();
386
+ // Highest System.ChangedDate observed across processed items this run.
387
+ // After a successful run we advance the watermark to (max − 60s) so a
388
+ // small clock-skew window is re-scanned next time; re-processing is
389
+ // idempotent because create/merge key on the AzDO id PK.
390
+ let maxChangedMs = 0;
391
+ // ALL open executable items (PBI/Bug) this run, each carrying its CURRENT
392
+ // upstream parent id. Provisioning self-heals wrong parentage, so it needs
393
+ // every executable item (not just empty-parent orphans) to detect and fix
394
+ // mis-parented ones. Collected in full mode only; provisioned after the
395
+ // hydration loop + reconcile.
396
+ const hierItems = [];
397
+ for (const batch of batches) {
398
+ const azdoItems = await client.getWorkItems(batch);
399
+ const parentIds = Array.from(new Set(azdoItems
400
+ .map((item) => extractParentId(item))
401
+ .filter((id) => Boolean(id))));
402
+ const parentItems = parentIds.length > 0
403
+ ? await client.getWorkItems(parentIds.map((id) => Number(id)))
404
+ : [];
405
+ const parentMap = new Map(parentItems.map((item) => [String(item.id), item]));
406
+ const grandparentIds = Array.from(new Set(parentItems
407
+ .map((item) => extractParentId(item))
408
+ .filter((id) => Boolean(id))));
409
+ const grandparentItems = grandparentIds.length > 0
410
+ ? await client.getWorkItems(grandparentIds.map((id) => Number(id)))
411
+ : [];
412
+ const grandparentMap = new Map(grandparentItems.map((item) => [String(item.id), item]));
413
+ for (const azdo of azdoItems) {
414
+ const itemId = String(azdo.id);
415
+ azdoIdsSeen.add(itemId);
416
+ try {
417
+ const azdoRecord = azdoToWorkItemRecord(azdo, config);
418
+ const azdoChangedDate = String(azdo.fields['System.ChangedDate'] ?? new Date().toISOString());
419
+ const changedMs = new Date(azdoChangedDate).getTime();
420
+ if (Number.isFinite(changedMs) && changedMs > maxChangedMs) {
421
+ maxChangedMs = changedMs;
422
+ }
423
+ const payload = azdoToWorkItemPayload(azdo, fieldNames, config);
424
+ const parentId = extractParentId(azdo);
425
+ // Collect EVERY open executable item (Backlog item / Bug) for the
426
+ // hierarchy self-heal (full mode only), carrying its CURRENT parent
427
+ // id (null if none). Provisioning needs all of them — not just
428
+ // empty-parent orphans — to detect and re-parent mis-parented items.
429
+ // Epics/Features are never returned by the WIQL, but we still guard
430
+ // on kind. kind/area come from the payload/mapper (the record mapper
431
+ // doesn't emit them); area is the real value or omitted (no fabrication).
432
+ if (effectiveMode === 'full' &&
433
+ (payload.kind === 'Backlog item' || payload.kind === 'Bug')) {
434
+ const itemArea = azdoToArea(azdo);
435
+ hierItems.push({
436
+ id: itemId,
437
+ kind: payload.kind,
438
+ ...(itemArea !== null ? { area: itemArea } : {}),
439
+ currentParentId: parentId,
440
+ });
441
+ }
442
+ const parent = parentId ? parentMap.get(parentId) : null;
443
+ const grandparentId = parent ? extractParentId(parent) : null;
444
+ const grandparent = grandparentId ? grandparentMap.get(grandparentId) : null;
445
+ payload.parentId = parentId;
446
+ if (payload.azdoFields) {
447
+ payload.azdoFields.parentId = parentId;
448
+ payload.azdoFields.parentTitle = parent ? String(parent.fields[fieldNames.title] ?? '') || null : null;
449
+ payload.azdoFields.parentWorkItemType = parent ? String(parent.fields[fieldNames.workItemType] ?? '') || null : null;
450
+ payload.azdoFields.parentStartDate = parent ? String(parent.fields[fieldNames.startDate] ?? '') || null : null;
451
+ payload.azdoFields.parentTargetDate = parent ? String(parent.fields[fieldNames.targetDate] ?? '') || null : null;
452
+ payload.azdoFields.grandparentId = grandparentId;
453
+ payload.azdoFields.grandparentTitle = grandparent ? String(grandparent.fields[fieldNames.title] ?? '') || null : null;
454
+ payload.azdoFields.grandparentWorkItemType = grandparent ? String(grandparent.fields[fieldNames.workItemType] ?? '') || null : null;
455
+ payload.azdoFields.grandparentStartDate = grandparent ? String(grandparent.fields[fieldNames.startDate] ?? '') || null : null;
456
+ payload.azdoFields.grandparentTargetDate = grandparent ? String(grandparent.fields[fieldNames.targetDate] ?? '') || null : null;
457
+ }
458
+ const payloadJson = JSON.stringify(payload);
459
+ const local = await store.getWorkItem(itemId);
460
+ if (!local) {
461
+ // Insert with the AzDO numeric id as the local PK; tag as 'azdo'
462
+ // so future heartbeats and pruning can identify mirrored rows.
463
+ //
464
+ // Forward the REAL AzDO values for every content field the store
465
+ // now persists. Phase 1 removed the store's fabricated defaults,
466
+ // so anything we omit lands as NULL. createdAt/updatedAt come from
467
+ // the AzDO record (real System.CreatedDate / System.ChangedDate) —
468
+ // omitting them used to make the store fall back to NOW(), which is
469
+ // exactly the "just now" dashboard bug.
470
+ //
471
+ // kind/area/review/resolution/assignee are not on azdoRecord (the
472
+ // record mapper only covers the flat columns) — derive them from
473
+ // the payload/mapper helpers. `area` is taken from azdoToArea so a
474
+ // genuinely root-only path stays null rather than being fabricated
475
+ // as 'Enact/Factory'.
476
+ const area = azdoToArea(azdo);
477
+ await store.createWorkItem({
478
+ id: itemId,
479
+ projectId: azdoRecord.projectId ?? config.project,
480
+ title: azdoRecord.title ?? '',
481
+ description: azdoRecord.description,
482
+ status: azdoRecord.status,
483
+ priority: azdoRecord.priority ?? undefined,
484
+ source: 'azdo',
485
+ kind: payload.kind,
486
+ ...(area !== null ? { area } : {}),
487
+ reviewState: payload.layeredState.review,
488
+ resolution: payload.layeredState.resolution,
489
+ ...(payload.azdoFields?.assignedTo ? { assignee: payload.azdoFields.assignedTo } : {}),
490
+ createdAt: azdoRecord.createdAt,
491
+ updatedAt: azdoChangedDate,
492
+ ...(azdoRecord.closedAt !== undefined ? { closedAt: azdoRecord.closedAt } : {}),
493
+ });
494
+ await store.setPayloadJson(itemId, payloadJson);
495
+ result.pulled++;
496
+ events.emit('workitem.updated', { id: itemId, action: 'created' });
497
+ }
498
+ else {
499
+ const { merged, changed } = mergeRecord(local, azdoRecord, azdoChangedDate, {
500
+ area: azdoToArea(azdo),
501
+ kind: payload.kind,
502
+ assignee: payload.azdoFields?.assignedTo ?? null,
503
+ });
504
+ if (changed) {
505
+ await store.updateWorkItem(itemId, merged);
506
+ result.pulled++;
507
+ events.emit('workitem.updated', { id: itemId, action: 'updated' });
508
+ }
509
+ else if (new Date(local.updatedAt).getTime() > new Date(azdoChangedDate).getTime()) {
510
+ result.conflicts++;
511
+ }
512
+ // Always refresh the payload mirror — even if record fields
513
+ // didn't change, AzDO custom fields (operator lane, lifecycle
514
+ // booleans, etc.) may have been edited upstream.
515
+ await store.setPayloadJson(itemId, payloadJson);
516
+ }
517
+ }
518
+ catch (err) {
519
+ const msg = err instanceof Error ? err.message : String(err);
520
+ result.errors.push({ workItemId: itemId, message: msg });
521
+ console.warn(`[AzdoSync] Error processing item ${itemId}: ${msg}`);
522
+ }
523
+ }
524
+ }
525
+ // Reconcile AzDO-sourced rows that are open locally but absent from the
526
+ // open upstream set — full mode only. Incremental never queries the
527
+ // complete id set, so its "not returned" set would be every unchanged
528
+ // item (heavy + wrong). Rather than blind-deleting (the old pruneOrphans
529
+ // behavior), we RE-FETCH only the small diff to confirm true state before
530
+ // removing anything — a transient fetch error must never cause an
531
+ // erroneous prune.
532
+ if (effectiveMode === 'full') {
533
+ const TERMINAL_LOCAL_STATUSES = new Set(['done', 'cancelled', 'duplicate']);
534
+ // Local OPEN azdo rows: source='azdo' and not in a terminal local
535
+ // status. Page through the LOCAL store (fixed page size, advance offset
536
+ // until a short page) so a board with >1 page of open rows can't
537
+ // silently skip prune candidates. This read is local-only; only the
538
+ // candidate diff is later fetched from AzDO.
539
+ const LOCAL_PAGE_SIZE = 1000;
540
+ const localOpenIds = [];
541
+ for (let offset = 0;; offset += LOCAL_PAGE_SIZE) {
542
+ const page = await store.listWorkItems({ source: 'azdo', limit: LOCAL_PAGE_SIZE, offset });
543
+ for (const r of page.workItems) {
544
+ if (!TERMINAL_LOCAL_STATUSES.has(r.status)) {
545
+ localOpenIds.push(r.id);
546
+ }
547
+ }
548
+ if (page.workItems.length < LOCAL_PAGE_SIZE)
549
+ break;
550
+ }
551
+ // Candidates: open locally but NOT in the open upstream set this run.
552
+ const candidates = localOpenIds.filter((id) => !azdoIdsSeen.has(id));
553
+ let prunedCount = 0;
554
+ if (candidates.length > 0) {
555
+ for (let i = 0; i < candidates.length; i += BATCH_SIZE) {
556
+ const batch = candidates.slice(i, i + BATCH_SIZE);
557
+ let fetched;
558
+ try {
559
+ fetched = await withExponentialBackoff(() => client.getWorkItems(batch.map(Number)), 3, 1000, 30000);
560
+ }
561
+ catch (err) {
562
+ // Transient fetch failure — KEEP this batch (safer than deleting
563
+ // on an error). The next reconcile retries it.
564
+ console.warn(`[AzdoSync] Reconcile fetch failed for ${batch.length} candidate(s) — keeping them: ` +
565
+ (err instanceof Error ? err.message : String(err)));
566
+ continue;
567
+ }
568
+ const fetchedMap = new Map(fetched.map((item) => [String(item.id), item]));
569
+ for (const id of batch) {
570
+ const item = fetchedMap.get(id);
571
+ if (!item) {
572
+ // Hard-deleted upstream — confirmed gone.
573
+ await store.deleteWorkItem(id);
574
+ prunedCount++;
575
+ continue;
576
+ }
577
+ // Present upstream: read its true System.State. Terminal check
578
+ // uses the SAME configured set as the WIQL open-filter so the two
579
+ // can't drift.
580
+ const upstreamState = String(item.fields[fieldNames.state] ?? '').trim();
581
+ const isTerminal = terminalStateSet.has(upstreamState);
582
+ if (isTerminal) {
583
+ // Closed upstream — leave the open board.
584
+ await store.deleteWorkItem(id);
585
+ prunedCount++;
586
+ }
587
+ // else: still OPEN upstream (WIQL eventual-consistency lag) — KEEP
588
+ // it; the next full reconcile will handle it.
589
+ }
590
+ }
591
+ }
592
+ if (prunedCount > 0) {
593
+ events.emit('azdo.sync.pruned', { count: prunedCount });
594
+ }
595
+ }
596
+ // Hierarchy self-heal (full mode only): provision Epics/Features, link
597
+ // unparented items, and re-parent mis-parented ones. Idempotent
598
+ // (find-or-create + KV cache) and self-limiting — once correct, the next
599
+ // pull shows the right parent so they land in `alreadyCorrect`. Failure
600
+ // here must NEVER crash the sync tick, hence its own try/catch.
601
+ if (effectiveMode === 'full' && hierItems.length > 0) {
602
+ try {
603
+ const projects = await resolveProjectConfigs(loadConfig(), store);
604
+ const hier = await provisionHierarchy({ client, store, organization: config.organization, project: config.project, projects }, hierItems);
605
+ if (hier.epicsCreated || hier.featuresCreated || hier.linked || hier.reparented) {
606
+ events.emit('azdo.sync.hierarchy', hier);
607
+ console.log(`[AzdoSync] Hierarchy: +${hier.epicsCreated} epics, +${hier.featuresCreated} features, ${hier.linked} linked, ${hier.reparented} reparented, ${hier.alreadyCorrect} ok, ${hier.skipped} skipped, ${hier.errors.length} errors`);
608
+ }
609
+ }
610
+ catch (err) {
611
+ console.error('[AzdoSync] Hierarchy provisioning failed (sync continues):', err instanceof Error ? err.message : String(err));
612
+ }
613
+ }
614
+ // Advance the watermark to (maxChangedDate − 60s) so the next incremental
615
+ // run re-scans a short overlap window (absorbs clock skew). Only advance
616
+ // forward — never rewind past a watermark a concurrent/prior run set.
617
+ if (maxChangedMs > 0) {
618
+ const nextWatermark = new Date(maxChangedMs - 60_000).toISOString();
619
+ const prevMs = currentWatermark ? new Date(currentWatermark).getTime() : 0;
620
+ if (!Number.isFinite(prevMs) || new Date(nextWatermark).getTime() > prevMs) {
621
+ await store.setSyncMeta(watermarkKey, nextWatermark);
622
+ }
623
+ }
624
+ lastSuccessAt = Date.now();
625
+ lastError = undefined;
626
+ checkStale();
627
+ result.durationMs = Date.now() - t0;
628
+ events.emit('azdo.sync.tick', result);
629
+ return result;
630
+ }
631
+ catch (err) {
632
+ lastErrorAt = Date.now();
633
+ lastError = err instanceof Error ? err.message : String(err);
634
+ checkStale();
635
+ result.durationMs = Date.now() - t0;
636
+ throw err;
637
+ }
638
+ }
639
+ // ---------------------------------------------------------------------------
640
+ // pushLocalChange — push-up a single work item
641
+ // ---------------------------------------------------------------------------
642
+ async function pushLocalChange(workItemId) {
643
+ const local = await store.getWorkItem(workItemId);
644
+ if (!local) {
645
+ console.warn(`[AzdoSync] pushLocalChange: item ${workItemId} not found in local store`);
646
+ return;
647
+ }
648
+ const numericId = parseInt(workItemId, 10);
649
+ if (isNaN(numericId)) {
650
+ console.warn(`[AzdoSync] pushLocalChange: ${workItemId} is not a numeric AzDO ID`);
651
+ return;
652
+ }
653
+ // Build patches from the LOCAL intent, not a fresh AzDO fetch. Re-fetching
654
+ // AzDO and re-deriving patches from its current state would overwrite the
655
+ // very local edit we're trying to push.
656
+ const patches = await buildLocalPushPatches(store, fieldNames, workItemId, local, config);
657
+ try {
658
+ await withExponentialBackoff(() => client.updateWorkItem(numericId, patches), 3, 1000, 30000);
659
+ }
660
+ catch (err) {
661
+ if (err instanceof AzdoApiError && err.status !== 429 && err.status !== 503) {
662
+ console.error(`[AzdoSync] Push failed for ${workItemId}:`, err.message);
663
+ events.emit('azdo.push.failed', { workItemId, status: err.status, message: err.message });
664
+ }
665
+ else {
666
+ throw err;
667
+ }
668
+ }
669
+ }
670
+ // ---------------------------------------------------------------------------
671
+ // start / stop
672
+ // ---------------------------------------------------------------------------
673
+ async function start() {
674
+ if (!config.enabled) {
675
+ return;
676
+ }
677
+ if (started) {
678
+ return;
679
+ }
680
+ started = true;
681
+ // Boot: a FULL sync seeds the complete backlog and establishes the
682
+ // watermark before the incremental heartbeat takes over.
683
+ inFlightSync = syncOnce({ mode: 'full' }).catch((err) => {
684
+ console.error('[AzdoSync] Initial sync failed:', err);
685
+ return { pulled: 0, pushed: 0, conflicts: 0, errors: [], durationMs: 0 };
686
+ });
687
+ await inFlightSync;
688
+ inFlightSync = null;
689
+ // Periodic FULL reconcile cadence: roughly every 15 minutes. A full pass
690
+ // re-scans the complete id set and prunes orphans (deletes that an
691
+ // incremental ChangedDate query can never observe). Every other tick is a
692
+ // cheap incremental pull.
693
+ const FULL_RECONCILE_MS = 900_000; // 15 minutes
694
+ const fullEveryNthTick = Math.max(1, Math.round(FULL_RECONCILE_MS / config.intervalMs));
695
+ let tick = 0;
696
+ intervalHandle = setInterval(() => {
697
+ tick += 1;
698
+ const mode = tick % fullEveryNthTick === 0 ? 'full' : 'incremental';
699
+ inFlightSync = syncOnce({ mode }).then(async () => {
700
+ if (drainAfterSync && root) {
701
+ const ctx = { client, store, config, events, fetchToken: deps.fetchToken };
702
+ try {
703
+ const drainResult = await drainWorkItemPushQueue(root, ctx);
704
+ console.log(`[AzdoSync] Drain complete: ${drainResult.drained} drained, ${drainResult.errors.length} errors`);
705
+ if (drainResult.errors.length > 0) {
706
+ console.warn('[AzdoSync] Drain errors:', drainResult.errors.map((e) => `${e.workItemId}(${e.lastError})`).join(', '));
707
+ }
708
+ }
709
+ catch (err) {
710
+ console.error('[AzdoSync] Drain threw unexpectedly (tick continues):', err instanceof Error ? err.message : String(err));
711
+ }
712
+ }
713
+ return { pulled: 0, pushed: 0, conflicts: 0, errors: [], durationMs: 0 };
714
+ }).catch((err) => {
715
+ console.error('[AzdoSync] Periodic sync failed:', err);
716
+ return { pulled: 0, pushed: 0, conflicts: 0, errors: [], durationMs: 0 };
717
+ }).finally(() => {
718
+ inFlightSync = null;
719
+ });
720
+ }, config.intervalMs);
721
+ }
722
+ async function stop() {
723
+ if (intervalHandle !== null) {
724
+ clearInterval(intervalHandle);
725
+ intervalHandle = null;
726
+ }
727
+ started = false;
728
+ // Wait for any in-flight sync to settle
729
+ if (inFlightSync) {
730
+ await inFlightSync.catch(() => undefined);
731
+ inFlightSync = null;
732
+ }
733
+ }
734
+ function getStatus() {
735
+ const now = Date.now();
736
+ const threshold = config.intervalMs * 3;
737
+ const isStale = lastSuccessAt === null || (now - lastSuccessAt) > threshold;
738
+ return {
739
+ lastSuccessAt,
740
+ lastErrorAt,
741
+ lastError,
742
+ running: started,
743
+ isStale,
744
+ };
745
+ }
746
+ return { start, stop, syncOnce, pushLocalChange, getStatus };
747
+ }
748
+ //# sourceMappingURL=azureDevOpsSync.js.map