specweave 1.0.350 → 1.0.352

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 (282) hide show
  1. package/bin/specweave.js +9 -0
  2. package/dist/plugins/specweave-ado/lib/ado-client-v2.d.ts +5 -0
  3. package/dist/plugins/specweave-ado/lib/ado-client-v2.d.ts.map +1 -1
  4. package/dist/plugins/specweave-ado/lib/ado-client-v2.js +61 -23
  5. package/dist/plugins/specweave-ado/lib/ado-client-v2.js.map +1 -1
  6. package/dist/plugins/specweave-ado/lib/ado-duplicate-detector.d.ts.map +1 -1
  7. package/dist/plugins/specweave-ado/lib/ado-duplicate-detector.js +3 -2
  8. package/dist/plugins/specweave-ado/lib/ado-duplicate-detector.js.map +1 -1
  9. package/dist/plugins/specweave-ado/lib/ado-profile-resolver.d.ts.map +1 -1
  10. package/dist/plugins/specweave-ado/lib/ado-profile-resolver.js +2 -1
  11. package/dist/plugins/specweave-ado/lib/ado-profile-resolver.js.map +1 -1
  12. package/dist/plugins/specweave-ado/lib/ado-spec-sync.d.ts +1 -1
  13. package/dist/plugins/specweave-ado/lib/ado-spec-sync.d.ts.map +1 -1
  14. package/dist/plugins/specweave-ado/lib/ado-spec-sync.js +25 -9
  15. package/dist/plugins/specweave-ado/lib/ado-spec-sync.js.map +1 -1
  16. package/dist/plugins/specweave-ado/lib/conflict-resolver.d.ts.map +1 -1
  17. package/dist/plugins/specweave-ado/lib/conflict-resolver.js +17 -1
  18. package/dist/plugins/specweave-ado/lib/conflict-resolver.js.map +1 -1
  19. package/dist/plugins/specweave-ado/lib/per-us-sync.d.ts +3 -0
  20. package/dist/plugins/specweave-ado/lib/per-us-sync.d.ts.map +1 -1
  21. package/dist/plugins/specweave-ado/lib/per-us-sync.js +14 -1
  22. package/dist/plugins/specweave-ado/lib/per-us-sync.js.map +1 -1
  23. package/dist/plugins/specweave-github/lib/github-client-v2.d.ts.map +1 -1
  24. package/dist/plugins/specweave-github/lib/github-client-v2.js +10 -7
  25. package/dist/plugins/specweave-github/lib/github-client-v2.js.map +1 -1
  26. package/dist/plugins/specweave-github/lib/github-client.d.ts +1 -1
  27. package/dist/plugins/specweave-github/lib/github-client.d.ts.map +1 -1
  28. package/dist/plugins/specweave-github/lib/github-client.js +7 -5
  29. package/dist/plugins/specweave-github/lib/github-client.js.map +1 -1
  30. package/dist/plugins/specweave-github/lib/github-cross-repo-sync.js +13 -3
  31. package/dist/plugins/specweave-github/lib/github-cross-repo-sync.js.map +1 -1
  32. package/dist/plugins/specweave-github/lib/github-feature-sync-cli.d.ts +24 -1
  33. package/dist/plugins/specweave-github/lib/github-feature-sync-cli.d.ts.map +1 -1
  34. package/dist/plugins/specweave-github/lib/github-feature-sync-cli.js +36 -20
  35. package/dist/plugins/specweave-github/lib/github-feature-sync-cli.js.map +1 -1
  36. package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts +4 -2
  37. package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts.map +1 -1
  38. package/dist/plugins/specweave-github/lib/github-feature-sync.js +38 -9
  39. package/dist/plugins/specweave-github/lib/github-feature-sync.js.map +1 -1
  40. package/dist/plugins/specweave-github/lib/github-graphql-client.d.ts +1 -0
  41. package/dist/plugins/specweave-github/lib/github-graphql-client.d.ts.map +1 -1
  42. package/dist/plugins/specweave-github/lib/github-graphql-client.js +32 -22
  43. package/dist/plugins/specweave-github/lib/github-graphql-client.js.map +1 -1
  44. package/dist/plugins/specweave-github/lib/github-spec-frontmatter-updater.js +144 -8
  45. package/dist/plugins/specweave-github/lib/github-spec-frontmatter-updater.js.map +1 -1
  46. package/dist/plugins/specweave-github/lib/github-spec-sync.d.ts +8 -1
  47. package/dist/plugins/specweave-github/lib/github-spec-sync.d.ts.map +1 -1
  48. package/dist/plugins/specweave-github/lib/github-spec-sync.js +94 -24
  49. package/dist/plugins/specweave-github/lib/github-spec-sync.js.map +1 -1
  50. package/dist/plugins/specweave-github/lib/github-sync-orchestrator.d.ts +1 -0
  51. package/dist/plugins/specweave-github/lib/github-sync-orchestrator.d.ts.map +1 -1
  52. package/dist/plugins/specweave-github/lib/github-sync-orchestrator.js +2 -1
  53. package/dist/plugins/specweave-github/lib/github-sync-orchestrator.js.map +1 -1
  54. package/dist/plugins/specweave-github/lib/github-us-auto-closer.d.ts.map +1 -1
  55. package/dist/plugins/specweave-github/lib/github-us-auto-closer.js +25 -0
  56. package/dist/plugins/specweave-github/lib/github-us-auto-closer.js.map +1 -1
  57. package/dist/plugins/specweave-github/lib/per-us-sync.d.ts +3 -0
  58. package/dist/plugins/specweave-github/lib/per-us-sync.d.ts.map +1 -1
  59. package/dist/plugins/specweave-github/lib/per-us-sync.js +29 -9
  60. package/dist/plugins/specweave-github/lib/per-us-sync.js.map +1 -1
  61. package/dist/plugins/specweave-jira/lib/content-format-adapter.d.ts +59 -0
  62. package/dist/plugins/specweave-jira/lib/content-format-adapter.d.ts.map +1 -0
  63. package/dist/plugins/specweave-jira/lib/content-format-adapter.js +159 -0
  64. package/dist/plugins/specweave-jira/lib/content-format-adapter.js.map +1 -0
  65. package/dist/plugins/specweave-jira/lib/jira-deployment-detector.d.ts +45 -0
  66. package/dist/plugins/specweave-jira/lib/jira-deployment-detector.d.ts.map +1 -0
  67. package/dist/plugins/specweave-jira/lib/jira-deployment-detector.js +92 -0
  68. package/dist/plugins/specweave-jira/lib/jira-deployment-detector.js.map +1 -0
  69. package/dist/plugins/specweave-jira/lib/jira-duplicate-detector.d.ts.map +1 -1
  70. package/dist/plugins/specweave-jira/lib/jira-duplicate-detector.js +13 -28
  71. package/dist/plugins/specweave-jira/lib/jira-duplicate-detector.js.map +1 -1
  72. package/dist/plugins/specweave-jira/lib/jira-epic-sync.d.ts +2 -1
  73. package/dist/plugins/specweave-jira/lib/jira-epic-sync.d.ts.map +1 -1
  74. package/dist/plugins/specweave-jira/lib/jira-epic-sync.js +19 -7
  75. package/dist/plugins/specweave-jira/lib/jira-epic-sync.js.map +1 -1
  76. package/dist/plugins/specweave-jira/lib/jira-field-discovery.d.ts +47 -0
  77. package/dist/plugins/specweave-jira/lib/jira-field-discovery.d.ts.map +1 -0
  78. package/dist/plugins/specweave-jira/lib/jira-field-discovery.js +110 -0
  79. package/dist/plugins/specweave-jira/lib/jira-field-discovery.js.map +1 -0
  80. package/dist/plugins/specweave-jira/lib/jira-paginated-search.d.ts +26 -0
  81. package/dist/plugins/specweave-jira/lib/jira-paginated-search.d.ts.map +1 -0
  82. package/dist/plugins/specweave-jira/lib/jira-paginated-search.js +77 -0
  83. package/dist/plugins/specweave-jira/lib/jira-paginated-search.js.map +1 -0
  84. package/dist/plugins/specweave-jira/lib/jira-spec-commit-sync.d.ts.map +1 -1
  85. package/dist/plugins/specweave-jira/lib/jira-spec-commit-sync.js +5 -3
  86. package/dist/plugins/specweave-jira/lib/jira-spec-commit-sync.js.map +1 -1
  87. package/dist/plugins/specweave-jira/lib/jira-spec-sync.d.ts +17 -2
  88. package/dist/plugins/specweave-jira/lib/jira-spec-sync.d.ts.map +1 -1
  89. package/dist/plugins/specweave-jira/lib/jira-spec-sync.js +103 -33
  90. package/dist/plugins/specweave-jira/lib/jira-spec-sync.js.map +1 -1
  91. package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts +4 -0
  92. package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts.map +1 -1
  93. package/dist/plugins/specweave-jira/lib/jira-status-sync.js +19 -6
  94. package/dist/plugins/specweave-jira/lib/jira-status-sync.js.map +1 -1
  95. package/dist/plugins/specweave-jira/lib/metadata-paths.d.ts +29 -0
  96. package/dist/plugins/specweave-jira/lib/metadata-paths.d.ts.map +1 -0
  97. package/dist/plugins/specweave-jira/lib/metadata-paths.js +73 -0
  98. package/dist/plugins/specweave-jira/lib/metadata-paths.js.map +1 -0
  99. package/dist/plugins/specweave-jira/lib/reorganization-detector.d.ts +15 -2
  100. package/dist/plugins/specweave-jira/lib/reorganization-detector.d.ts.map +1 -1
  101. package/dist/plugins/specweave-jira/lib/reorganization-detector.js +121 -33
  102. package/dist/plugins/specweave-jira/lib/reorganization-detector.js.map +1 -1
  103. package/dist/src/cli/commands/init.d.ts.map +1 -1
  104. package/dist/src/cli/commands/init.js +23 -18
  105. package/dist/src/cli/commands/init.js.map +1 -1
  106. package/dist/src/cli/commands/sync-progress.d.ts +6 -0
  107. package/dist/src/cli/commands/sync-progress.d.ts.map +1 -1
  108. package/dist/src/cli/commands/sync-progress.js +37 -0
  109. package/dist/src/cli/commands/sync-progress.js.map +1 -1
  110. package/dist/src/cli/commands/sync-task.d.ts +16 -0
  111. package/dist/src/cli/commands/sync-task.d.ts.map +1 -0
  112. package/dist/src/cli/commands/sync-task.js +42 -0
  113. package/dist/src/cli/commands/sync-task.js.map +1 -0
  114. package/dist/src/cli/helpers/init/instruction-file-merger.js +3 -3
  115. package/dist/src/cli/helpers/init/instruction-file-merger.js.map +1 -1
  116. package/dist/src/core/hooks/LifecycleHookDispatcher.d.ts +9 -1
  117. package/dist/src/core/hooks/LifecycleHookDispatcher.d.ts.map +1 -1
  118. package/dist/src/core/hooks/LifecycleHookDispatcher.js +26 -8
  119. package/dist/src/core/hooks/LifecycleHookDispatcher.js.map +1 -1
  120. package/dist/src/core/increment/metadata-manager.d.ts +13 -0
  121. package/dist/src/core/increment/metadata-manager.d.ts.map +1 -1
  122. package/dist/src/core/increment/metadata-manager.js +144 -17
  123. package/dist/src/core/increment/metadata-manager.js.map +1 -1
  124. package/dist/src/core/increment/status-change-sync-trigger.d.ts +1 -1
  125. package/dist/src/core/increment/status-change-sync-trigger.d.ts.map +1 -1
  126. package/dist/src/core/increment/status-change-sync-trigger.js +2 -1
  127. package/dist/src/core/increment/status-change-sync-trigger.js.map +1 -1
  128. package/dist/src/core/increment/status-commands.d.ts.map +1 -1
  129. package/dist/src/core/increment/status-commands.js +33 -11
  130. package/dist/src/core/increment/status-commands.js.map +1 -1
  131. package/dist/src/core/repo-structure/repo-structure-manager.d.ts.map +1 -1
  132. package/dist/src/core/repo-structure/repo-structure-manager.js +2 -1
  133. package/dist/src/core/repo-structure/repo-structure-manager.js.map +1 -1
  134. package/dist/src/locales/de/cli.json +252 -77
  135. package/dist/src/locales/en/cli.json +7 -0
  136. package/dist/src/locales/es/cli.json +245 -3
  137. package/dist/src/locales/fr/cli.json +259 -84
  138. package/dist/src/locales/ja/cli.json +253 -78
  139. package/dist/src/locales/ko/cli.json +253 -78
  140. package/dist/src/locales/pt/cli.json +252 -77
  141. package/dist/src/locales/ru/cli.json +17 -3
  142. package/dist/src/locales/zh/cli.json +258 -83
  143. package/dist/src/sync/ado-reconciler.d.ts.map +1 -1
  144. package/dist/src/sync/ado-reconciler.js +5 -1
  145. package/dist/src/sync/ado-reconciler.js.map +1 -1
  146. package/dist/src/sync/base-reconciler.d.ts.map +1 -1
  147. package/dist/src/sync/base-reconciler.js +6 -1
  148. package/dist/src/sync/base-reconciler.js.map +1 -1
  149. package/dist/src/sync/config.d.ts +4 -0
  150. package/dist/src/sync/config.d.ts.map +1 -1
  151. package/dist/src/sync/config.js +6 -4
  152. package/dist/src/sync/config.js.map +1 -1
  153. package/dist/src/sync/external-issue-auto-creator.d.ts +3 -0
  154. package/dist/src/sync/external-issue-auto-creator.d.ts.map +1 -1
  155. package/dist/src/sync/external-issue-auto-creator.js +53 -17
  156. package/dist/src/sync/external-issue-auto-creator.js.map +1 -1
  157. package/dist/src/sync/external-item-sync-service.d.ts +9 -0
  158. package/dist/src/sync/external-item-sync-service.d.ts.map +1 -1
  159. package/dist/src/sync/external-item-sync-service.js +210 -9
  160. package/dist/src/sync/external-item-sync-service.js.map +1 -1
  161. package/dist/src/sync/github-reconciler.d.ts +30 -0
  162. package/dist/src/sync/github-reconciler.d.ts.map +1 -1
  163. package/dist/src/sync/github-reconciler.js +242 -3
  164. package/dist/src/sync/github-reconciler.js.map +1 -1
  165. package/dist/src/sync/jira-reconciler.d.ts.map +1 -1
  166. package/dist/src/sync/jira-reconciler.js +5 -1
  167. package/dist/src/sync/jira-reconciler.js.map +1 -1
  168. package/dist/src/sync/provider-router.d.ts.map +1 -1
  169. package/dist/src/sync/provider-router.js +2 -1
  170. package/dist/src/sync/provider-router.js.map +1 -1
  171. package/dist/src/sync/providers/ado.d.ts +4 -0
  172. package/dist/src/sync/providers/ado.d.ts.map +1 -1
  173. package/dist/src/sync/providers/ado.js +36 -11
  174. package/dist/src/sync/providers/ado.js.map +1 -1
  175. package/dist/src/sync/providers/github.d.ts.map +1 -1
  176. package/dist/src/sync/providers/github.js +48 -35
  177. package/dist/src/sync/providers/github.js.map +1 -1
  178. package/dist/src/sync/providers/jira.d.ts.map +1 -1
  179. package/dist/src/sync/providers/jira.js +42 -26
  180. package/dist/src/sync/providers/jira.js.map +1 -1
  181. package/dist/src/sync/status-mapper.d.ts +3 -1
  182. package/dist/src/sync/status-mapper.d.ts.map +1 -1
  183. package/dist/src/sync/status-mapper.js +10 -2
  184. package/dist/src/sync/status-mapper.js.map +1 -1
  185. package/dist/src/sync/sync-coordinator.d.ts.map +1 -1
  186. package/dist/src/sync/sync-coordinator.js +29 -19
  187. package/dist/src/sync/sync-coordinator.js.map +1 -1
  188. package/package.json +1 -1
  189. package/plugins/specweave/hooks/v2/guards/task-ac-sync-guard.sh +31 -0
  190. package/plugins/specweave/lib/vendor/core/increment/metadata-manager.d.ts +13 -0
  191. package/plugins/specweave/lib/vendor/core/increment/metadata-manager.js +144 -17
  192. package/plugins/specweave/lib/vendor/core/increment/metadata-manager.js.map +1 -1
  193. package/plugins/specweave/lib/vendor/sync/github-reconciler.d.ts +30 -0
  194. package/plugins/specweave/lib/vendor/sync/github-reconciler.js +242 -3
  195. package/plugins/specweave/lib/vendor/sync/github-reconciler.js.map +1 -1
  196. package/plugins/specweave/skills/architect/SKILL.md +2 -0
  197. package/plugins/specweave/skills/grill/SKILL.md +2 -0
  198. package/plugins/specweave/skills/team-lead/SKILL.md +43 -320
  199. package/plugins/specweave/skills/team-lead/agents/backend.md +60 -0
  200. package/plugins/specweave/skills/team-lead/agents/database.md +51 -0
  201. package/plugins/specweave/skills/team-lead/agents/frontend.md +61 -0
  202. package/plugins/specweave/skills/team-lead/agents/security.md +52 -0
  203. package/plugins/specweave/skills/team-lead/agents/testing.md +57 -0
  204. package/plugins/specweave/skills/test-aware-planner/SKILL.md +2 -0
  205. package/plugins/specweave-ado/hooks/post-task-completion.sh +2 -2
  206. package/plugins/specweave-ado/lib/ado-client-v2.js +51 -21
  207. package/plugins/specweave-ado/lib/ado-client-v2.ts +62 -23
  208. package/plugins/specweave-ado/lib/ado-duplicate-detector.js +4 -4
  209. package/plugins/specweave-ado/lib/ado-duplicate-detector.ts +4 -3
  210. package/plugins/specweave-ado/lib/ado-hierarchical-sync.js +54 -12
  211. package/plugins/specweave-ado/lib/ado-hierarchical-sync.ts +88 -18
  212. package/plugins/specweave-ado/lib/ado-profile-resolver.js +1 -1
  213. package/plugins/specweave-ado/lib/ado-profile-resolver.ts +3 -1
  214. package/plugins/specweave-ado/lib/ado-spec-sync.js +22 -9
  215. package/plugins/specweave-ado/lib/ado-spec-sync.ts +27 -9
  216. package/plugins/specweave-ado/lib/conflict-resolver.js +17 -1
  217. package/plugins/specweave-ado/lib/conflict-resolver.ts +17 -1
  218. package/plugins/specweave-ado/lib/enhanced-ado-sync.js +11 -1
  219. package/plugins/specweave-ado/lib/per-us-sync.js +8 -1
  220. package/plugins/specweave-ado/lib/per-us-sync.ts +17 -2
  221. package/plugins/specweave-github/hooks/github-auto-create-handler.sh +28 -2
  222. package/plugins/specweave-github/hooks/post-task-completion.sh +6 -3
  223. package/plugins/specweave-github/lib/enhanced-github-sync.js +35 -6
  224. package/plugins/specweave-github/lib/github-board-resolver.js +4 -4
  225. package/plugins/specweave-github/lib/github-board-resolver.ts +4 -4
  226. package/plugins/specweave-github/lib/github-client-v2.js +6 -6
  227. package/plugins/specweave-github/lib/github-client-v2.ts +11 -7
  228. package/plugins/specweave-github/lib/github-client.js +5 -4
  229. package/plugins/specweave-github/lib/github-client.ts +7 -5
  230. package/plugins/specweave-github/lib/github-cross-repo-sync.js +17 -3
  231. package/plugins/specweave-github/lib/github-cross-repo-sync.ts +16 -3
  232. package/plugins/specweave-github/lib/github-feature-sync-cli.js +20 -11
  233. package/plugins/specweave-github/lib/github-feature-sync-cli.ts +42 -20
  234. package/plugins/specweave-github/lib/github-feature-sync.js +32 -8
  235. package/plugins/specweave-github/lib/github-feature-sync.ts +41 -9
  236. package/plugins/specweave-github/lib/github-graphql-client.js +29 -20
  237. package/plugins/specweave-github/lib/github-graphql-client.ts +34 -22
  238. package/plugins/specweave-github/lib/github-hierarchical-sync.js +2 -2
  239. package/plugins/specweave-github/lib/github-hierarchical-sync.ts +2 -2
  240. package/plugins/specweave-github/lib/github-multi-project-sync.js +23 -7
  241. package/plugins/specweave-github/lib/github-multi-project-sync.ts +26 -8
  242. package/plugins/specweave-github/lib/github-spec-frontmatter-updater.js +110 -5
  243. package/plugins/specweave-github/lib/github-spec-frontmatter-updater.ts +135 -9
  244. package/plugins/specweave-github/lib/github-spec-sync.js +85 -24
  245. package/plugins/specweave-github/lib/github-spec-sync.ts +100 -26
  246. package/plugins/specweave-github/lib/github-sync-orchestrator.js +2 -1
  247. package/plugins/specweave-github/lib/github-sync-orchestrator.ts +3 -1
  248. package/plugins/specweave-github/lib/github-us-auto-closer.js +25 -0
  249. package/plugins/specweave-github/lib/github-us-auto-closer.ts +43 -0
  250. package/plugins/specweave-github/lib/per-us-sync.js +26 -11
  251. package/plugins/specweave-github/lib/per-us-sync.ts +29 -11
  252. package/plugins/specweave-jira/hooks/post-task-completion.sh +2 -1
  253. package/plugins/specweave-jira/lib/content-format-adapter.js +116 -0
  254. package/plugins/specweave-jira/lib/content-format-adapter.ts +189 -0
  255. package/plugins/specweave-jira/lib/enhanced-jira-sync.js +21 -5
  256. package/plugins/specweave-jira/lib/jira-deployment-detector.js +63 -0
  257. package/plugins/specweave-jira/lib/jira-deployment-detector.ts +113 -0
  258. package/plugins/specweave-jira/lib/jira-duplicate-detector.js +12 -29
  259. package/plugins/specweave-jira/lib/jira-duplicate-detector.ts +13 -27
  260. package/plugins/specweave-jira/lib/jira-epic-sync.js +15 -5
  261. package/plugins/specweave-jira/lib/jira-epic-sync.ts +22 -7
  262. package/plugins/specweave-jira/lib/jira-field-discovery.js +76 -0
  263. package/plugins/specweave-jira/lib/jira-field-discovery.ts +139 -0
  264. package/plugins/specweave-jira/lib/jira-hierarchical-sync.js +10 -0
  265. package/plugins/specweave-jira/lib/jira-hierarchical-sync.ts +11 -0
  266. package/plugins/specweave-jira/lib/jira-multi-project-sync.js +19 -9
  267. package/plugins/specweave-jira/lib/jira-multi-project-sync.ts +25 -14
  268. package/plugins/specweave-jira/lib/jira-paginated-search.js +55 -0
  269. package/plugins/specweave-jira/lib/jira-paginated-search.ts +108 -0
  270. package/plugins/specweave-jira/lib/jira-spec-commit-sync.js +5 -3
  271. package/plugins/specweave-jira/lib/jira-spec-commit-sync.ts +5 -3
  272. package/plugins/specweave-jira/lib/jira-spec-sync.js +102 -31
  273. package/plugins/specweave-jira/lib/jira-spec-sync.ts +123 -45
  274. package/plugins/specweave-jira/lib/jira-status-sync.js +18 -5
  275. package/plugins/specweave-jira/lib/jira-status-sync.ts +21 -6
  276. package/plugins/specweave-jira/lib/metadata-paths.js +38 -0
  277. package/plugins/specweave-jira/lib/metadata-paths.ts +73 -0
  278. package/plugins/specweave-jira/lib/reorganization-detector.js +101 -23
  279. package/plugins/specweave-jira/lib/reorganization-detector.ts +125 -35
  280. package/plugins/specweave-jira/scripts/refresh-cache.js +1 -1
  281. package/plugins/specweave-jira/scripts/refresh-cache.ts +2 -2
  282. package/plugins/specweave-jira/skills/jira-resource-validator/SKILL.md +3 -5
@@ -12,6 +12,7 @@ import * as fs from '../../../src/utils/fs-native.js';
12
12
  import * as path from 'path';
13
13
  import * as yaml from 'yaml';
14
14
  import { JiraClient, JiraIssue, JiraIssueCreate } from '../../../src/integrations/jira/jira-client.js';
15
+ import { toDescription } from './content-format-adapter.js';
15
16
 
16
17
  interface EpicFrontmatter {
17
18
  id: string;
@@ -77,11 +78,13 @@ export class JiraEpicSync {
77
78
  private client: JiraClient;
78
79
  private specsDir: string;
79
80
  private projectKey: string;
81
+ private domain: string;
80
82
 
81
- constructor(client: JiraClient, specsDir: string, projectKey: string) {
83
+ constructor(client: JiraClient, specsDir: string, projectKey: string, domain?: string) {
82
84
  this.client = client;
83
85
  this.specsDir = specsDir;
84
86
  this.projectKey = projectKey;
87
+ this.domain = domain || (client as any)['credentials']?.domain || '';
85
88
  }
86
89
 
87
90
  /**
@@ -193,11 +196,21 @@ export class JiraEpicSync {
193
196
  * Find Epic folder by ID (FS-001 or just 001)
194
197
  */
195
198
  private async findEpicFolder(epicId: string): Promise<string | null> {
196
- const normalizedId = epicId.startsWith('FS-')
199
+ const folders = await fs.readdir(this.specsDir);
200
+
201
+ // Try exact match first
202
+ for (const folder of folders) {
203
+ if (folder.startsWith(epicId)) {
204
+ return path.join(this.specsDir, folder);
205
+ }
206
+ }
207
+
208
+ // Try with project key prefix (derived from projectKey, not hardcoded FS-)
209
+ const prefix = `${this.projectKey}-`;
210
+ const normalizedId = epicId.startsWith(prefix)
197
211
  ? epicId
198
- : `FS-${epicId.padStart(3, '0')}`;
212
+ : `${prefix}${epicId.padStart(3, '0')}`;
199
213
 
200
- const folders = await fs.readdir(this.specsDir);
201
214
  for (const folder of folders) {
202
215
  if (folder.startsWith(normalizedId)) {
203
216
  return path.join(this.specsDir, folder);
@@ -274,7 +287,8 @@ export class JiraEpicSync {
274
287
  url: string;
275
288
  }> {
276
289
  const summary = `[${epic.id}] ${epic.title}`;
277
- const description = `Epic: ${epic.title}\n\nProgress: ${epic.completed_increments}/${epic.total_increments} increments (${epic.progress})\n\nPriority: ${epic.priority}\nStatus: ${epic.status}`;
290
+ const rawDescription = `Epic: ${epic.title}\n\nProgress: ${epic.completed_increments}/${epic.total_increments} increments (${epic.progress})\n\nPriority: ${epic.priority}\nStatus: ${epic.status}`;
291
+ const description = toDescription(rawDescription, this.domain) as any;
278
292
 
279
293
  const issueData: JiraIssueCreate = {
280
294
  issueType: 'Epic',
@@ -288,7 +302,7 @@ export class JiraEpicSync {
288
302
 
289
303
  return {
290
304
  key: issue.key,
291
- url: issue.self.replace('/rest/api/3/issue/', '/browse/'),
305
+ url: issue.self.replace(/\/rest\/api\/\d+\/issue\//, '/browse/'),
292
306
  };
293
307
  }
294
308
 
@@ -297,7 +311,8 @@ export class JiraEpicSync {
297
311
  */
298
312
  private async updateEpic(epicKey: string, epic: EpicFrontmatter): Promise<void> {
299
313
  const summary = `[${epic.id}] ${epic.title}`;
300
- const description = `Epic: ${epic.title}\n\nProgress: ${epic.completed_increments}/${epic.total_increments} increments (${epic.progress})\n\nPriority: ${epic.priority}\nStatus: ${epic.status}`;
314
+ const rawDescription = `Epic: ${epic.title}\n\nProgress: ${epic.completed_increments}/${epic.total_increments} increments (${epic.progress})\n\nPriority: ${epic.priority}\nStatus: ${epic.status}`;
315
+ const description = toDescription(rawDescription, this.domain) as any;
301
316
 
302
317
  await this.client.updateIssue({
303
318
  key: epicKey,
@@ -0,0 +1,76 @@
1
+ import axios from "axios";
2
+ import { getApiBaseUrl } from "./jira-deployment-detector.js";
3
+ const epicLinkFieldCache = /* @__PURE__ */ new Map();
4
+ const projectStyleCache = /* @__PURE__ */ new Map();
5
+ async function discoverEpicLinkField(domain, auth) {
6
+ const cached = epicLinkFieldCache.get(domain);
7
+ if (cached !== void 0) return cached;
8
+ try {
9
+ const baseUrl = getApiBaseUrl(domain);
10
+ const response = await axios.get(`${baseUrl}/field`, {
11
+ auth: {
12
+ username: auth.email,
13
+ password: auth.apiToken
14
+ },
15
+ headers: { Accept: "application/json" },
16
+ timeout: 1e4
17
+ });
18
+ const fields = response.data;
19
+ const epicLinkField = fields.find(
20
+ (f) => f.name === "Epic Link" || f.schema?.custom === "com.pyxis.greenhopper.jira:gh-epic-link"
21
+ );
22
+ const fieldId = epicLinkField?.id || null;
23
+ epicLinkFieldCache.set(domain, fieldId);
24
+ return fieldId;
25
+ } catch {
26
+ epicLinkFieldCache.set(domain, null);
27
+ return null;
28
+ }
29
+ }
30
+ async function detectProjectStyle(domain, projectKey, auth) {
31
+ const cacheKey = `${domain}:${projectKey}`;
32
+ const cached = projectStyleCache.get(cacheKey);
33
+ if (cached) return cached;
34
+ try {
35
+ const baseUrl = getApiBaseUrl(domain);
36
+ const response = await axios.get(`${baseUrl}/project/${projectKey}`, {
37
+ auth: {
38
+ username: auth.email,
39
+ password: auth.apiToken
40
+ },
41
+ headers: { Accept: "application/json" },
42
+ timeout: 1e4
43
+ });
44
+ const project = response.data;
45
+ const isNextGen = project.style === "next-gen" || project.simplified === true;
46
+ const style = isNextGen ? "next-gen" : "classic";
47
+ projectStyleCache.set(cacheKey, style);
48
+ return style;
49
+ } catch {
50
+ projectStyleCache.set(cacheKey, "classic");
51
+ return "classic";
52
+ }
53
+ }
54
+ async function getEpicLinkFieldForProject(domain, projectKey, auth) {
55
+ const style = await detectProjectStyle(domain, projectKey, auth);
56
+ if (style === "next-gen") {
57
+ return { field: "parent", style };
58
+ }
59
+ const epicLinkFieldId = await discoverEpicLinkField(domain, auth);
60
+ if (!epicLinkFieldId) {
61
+ throw new Error(
62
+ `Could not discover Epic Link custom field for ${domain}. Ensure the JIRA instance has the Epic Link field and API credentials have field read access.`
63
+ );
64
+ }
65
+ return { field: epicLinkFieldId, style };
66
+ }
67
+ function clearFieldDiscoveryCache() {
68
+ epicLinkFieldCache.clear();
69
+ projectStyleCache.clear();
70
+ }
71
+ export {
72
+ clearFieldDiscoveryCache,
73
+ detectProjectStyle,
74
+ discoverEpicLinkField,
75
+ getEpicLinkFieldForProject
76
+ };
@@ -0,0 +1,139 @@
1
+ /**
2
+ * JIRA Field Discovery
3
+ *
4
+ * Dynamically discovers custom field IDs (e.g., Epic Link) and
5
+ * detects project style (Next-gen vs Classic).
6
+ *
7
+ * Results are cached per domain/project to avoid repeated lookups.
8
+ *
9
+ * @module jira-field-discovery
10
+ */
11
+
12
+ import axios from 'axios';
13
+ import { getApiBaseUrl } from './jira-deployment-detector.js';
14
+
15
+ /** Cache: domain → epic link field ID */
16
+ const epicLinkFieldCache = new Map<string, string | null>();
17
+
18
+ /** Cache: projectKey → project style */
19
+ const projectStyleCache = new Map<string, 'next-gen' | 'classic'>();
20
+
21
+ /**
22
+ * Discover the Epic Link custom field ID by querying /rest/api/field.
23
+ * Searches for fields with name "Epic Link" or schema type "com.pyxis.greenhopper.jira:gh-epic-link".
24
+ *
25
+ * @returns The custom field ID (e.g., "customfield_10014") or null if not found
26
+ */
27
+ export async function discoverEpicLinkField(
28
+ domain: string,
29
+ auth: { email: string; apiToken: string }
30
+ ): Promise<string | null> {
31
+ // Check cache
32
+ const cached = epicLinkFieldCache.get(domain);
33
+ if (cached !== undefined) return cached;
34
+
35
+ try {
36
+ const baseUrl = getApiBaseUrl(domain);
37
+ const response = await axios.get(`${baseUrl}/field`, {
38
+ auth: {
39
+ username: auth.email,
40
+ password: auth.apiToken,
41
+ },
42
+ headers: { Accept: 'application/json' },
43
+ timeout: 10000,
44
+ });
45
+
46
+ const fields: any[] = response.data;
47
+
48
+ // Find Epic Link field by name or schema
49
+ const epicLinkField = fields.find(
50
+ (f: any) =>
51
+ f.name === 'Epic Link' ||
52
+ f.schema?.custom === 'com.pyxis.greenhopper.jira:gh-epic-link'
53
+ );
54
+
55
+ const fieldId = epicLinkField?.id || null;
56
+ epicLinkFieldCache.set(domain, fieldId);
57
+ return fieldId;
58
+ } catch {
59
+ epicLinkFieldCache.set(domain, null);
60
+ return null;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Detect whether a JIRA project uses Next-gen (Team-managed) or Classic (Company-managed) style.
66
+ *
67
+ * Next-gen projects use `parent` field for epic linking.
68
+ * Classic projects use a custom field (Epic Link).
69
+ */
70
+ export async function detectProjectStyle(
71
+ domain: string,
72
+ projectKey: string,
73
+ auth: { email: string; apiToken: string }
74
+ ): Promise<'next-gen' | 'classic'> {
75
+ const cacheKey = `${domain}:${projectKey}`;
76
+ const cached = projectStyleCache.get(cacheKey);
77
+ if (cached) return cached;
78
+
79
+ try {
80
+ const baseUrl = getApiBaseUrl(domain);
81
+ const response = await axios.get(`${baseUrl}/project/${projectKey}`, {
82
+ auth: {
83
+ username: auth.email,
84
+ password: auth.apiToken,
85
+ },
86
+ headers: { Accept: 'application/json' },
87
+ timeout: 10000,
88
+ });
89
+
90
+ const project = response.data;
91
+
92
+ // Next-gen projects have style: "next-gen" or projectTypeKey: "software" with simplified: true
93
+ const isNextGen =
94
+ project.style === 'next-gen' ||
95
+ project.simplified === true;
96
+
97
+ const style = isNextGen ? 'next-gen' : 'classic';
98
+ projectStyleCache.set(cacheKey, style);
99
+ return style;
100
+ } catch {
101
+ // Default to classic if detection fails
102
+ projectStyleCache.set(cacheKey, 'classic');
103
+ return 'classic';
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Get the correct field name/ID for linking an issue to an epic,
109
+ * based on project style.
110
+ *
111
+ * - Next-gen: uses `parent` field
112
+ * - Classic: uses the discovered Epic Link custom field ID
113
+ */
114
+ export async function getEpicLinkFieldForProject(
115
+ domain: string,
116
+ projectKey: string,
117
+ auth: { email: string; apiToken: string }
118
+ ): Promise<{ field: 'parent' | string; style: 'next-gen' | 'classic' }> {
119
+ const style = await detectProjectStyle(domain, projectKey, auth);
120
+
121
+ if (style === 'next-gen') {
122
+ return { field: 'parent', style };
123
+ }
124
+
125
+ const epicLinkFieldId = await discoverEpicLinkField(domain, auth);
126
+ if (!epicLinkFieldId) {
127
+ throw new Error(
128
+ `Could not discover Epic Link custom field for ${domain}. ` +
129
+ `Ensure the JIRA instance has the Epic Link field and API credentials have field read access.`
130
+ );
131
+ }
132
+ return { field: epicLinkFieldId, style };
133
+ }
134
+
135
+ /** Clear caches (for testing) */
136
+ export function clearFieldDiscoveryCache(): void {
137
+ epicLinkFieldCache.clear();
138
+ projectStyleCache.clear();
139
+ }
@@ -4,6 +4,7 @@ import {
4
4
  isCustomStrategy
5
5
  } from "../../../src/core/types/sync-profile.js";
6
6
  import { getBoardIds } from "./jira-board-resolver.js";
7
+ import { searchAllIssues } from "./jira-paginated-search.js";
7
8
  async function buildHierarchicalJQL(client, containers) {
8
9
  const clauses = [];
9
10
  for (const container of containers) {
@@ -107,6 +108,9 @@ async function fetchIssuesSimple(client, config, timeRange) {
107
108
  let jql = `project=${projectKey}`;
108
109
  jql = addTimeRangeFilter(jql, timeRange);
109
110
  console.log("\u{1F50D} Fetching issues (SIMPLE strategy):", jql);
111
+ if (typeof client.getAxiosClient === "function") {
112
+ return searchAllIssues(client.getAxiosClient(), { jql });
113
+ }
110
114
  return client.searchIssues({ jql });
111
115
  }
112
116
  async function fetchIssuesCustom(client, config, timeRange) {
@@ -116,6 +120,9 @@ async function fetchIssuesCustom(client, config, timeRange) {
116
120
  }
117
121
  const jql = addTimeRangeFilter(customQuery, timeRange);
118
122
  console.log("\u{1F50D} Fetching issues (CUSTOM strategy):", jql);
123
+ if (typeof client.getAxiosClient === "function") {
124
+ return searchAllIssues(client.getAxiosClient(), { jql });
125
+ }
119
126
  return client.searchIssues({ jql });
120
127
  }
121
128
  async function fetchIssuesFiltered(client, config, timeRange) {
@@ -126,6 +133,9 @@ async function fetchIssuesFiltered(client, config, timeRange) {
126
133
  const baseJql = await buildHierarchicalJQL(client, containers);
127
134
  const jql = addTimeRangeFilter(baseJql, timeRange);
128
135
  console.log("\u{1F50D} Fetching issues (FILTERED strategy):", jql);
136
+ if (typeof client.getAxiosClient === "function") {
137
+ return searchAllIssues(client.getAxiosClient(), { jql });
138
+ }
129
139
  return client.searchIssues({ jql });
130
140
  }
131
141
  export {
@@ -17,6 +17,7 @@ import {
17
17
  } from '../../../src/core/types/sync-profile.js';
18
18
  import { JiraClient, JiraIssue } from '../../../src/integrations/jira/jira-client.js';
19
19
  import { getBoardIds } from './jira-board-resolver.js';
20
+ import { searchAllIssues } from './jira-paginated-search.js';
20
21
 
21
22
  /**
22
23
  * Build hierarchical JQL query from containers
@@ -222,6 +223,10 @@ async function fetchIssuesSimple(
222
223
 
223
224
  console.log('🔍 Fetching issues (SIMPLE strategy):', jql);
224
225
 
226
+ // Use paginated search if client exposes axios instance, otherwise fallback
227
+ if (typeof (client as any).getAxiosClient === 'function') {
228
+ return searchAllIssues((client as any).getAxiosClient(), { jql });
229
+ }
225
230
  return client.searchIssues({ jql });
226
231
  }
227
232
 
@@ -249,6 +254,9 @@ async function fetchIssuesCustom(
249
254
 
250
255
  console.log('🔍 Fetching issues (CUSTOM strategy):', jql);
251
256
 
257
+ if (typeof (client as any).getAxiosClient === 'function') {
258
+ return searchAllIssues((client as any).getAxiosClient(), { jql });
259
+ }
252
260
  return client.searchIssues({ jql });
253
261
  }
254
262
 
@@ -279,5 +287,8 @@ async function fetchIssuesFiltered(
279
287
 
280
288
  console.log('🔍 Fetching issues (FILTERED strategy):', jql);
281
289
 
290
+ if (typeof (client as any).getAxiosClient === 'function') {
291
+ return searchAllIssues((client as any).getAxiosClient(), { jql });
292
+ }
282
293
  return client.searchIssues({ jql });
283
294
  }
@@ -5,6 +5,7 @@ import {
5
5
  mapUserStoryToProjects
6
6
  } from "../../../src/utils/project-mapper.js";
7
7
  import { parseSpecFile } from "../../../src/utils/spec-splitter.js";
8
+ import { getEpicLinkFieldForProject } from "./jira-field-discovery.js";
8
9
  class JiraMultiProjectSync {
9
10
  constructor(config) {
10
11
  this.config = config;
@@ -43,14 +44,6 @@ Please verify project keys and access permissions.`
43
44
  const results = [];
44
45
  await this.validateProjects();
45
46
  const parsedSpec = await parseSpecFile(specPath);
46
- const epicsByProject = /* @__PURE__ */ new Map();
47
- if (this.config.autoCreateEpics !== false) {
48
- for (const project of this.config.projects) {
49
- const epicResult = await this.createEpicForProject(parsedSpec, project);
50
- epicsByProject.set(project, epicResult.issueKey);
51
- results.push(epicResult);
52
- }
53
- }
54
47
  const projectStories = /* @__PURE__ */ new Map();
55
48
  for (const userStory of parsedSpec.userStories) {
56
49
  if (this.config.intelligentMapping !== false) {
@@ -85,6 +78,14 @@ Please verify project keys and access permissions.`
85
78
  }
86
79
  }
87
80
  }
81
+ const epicsByProject = /* @__PURE__ */ new Map();
82
+ if (this.config.autoCreateEpics !== false) {
83
+ for (const projectId of projectStories.keys()) {
84
+ const epicResult = await this.createEpicForProject(parsedSpec, projectId);
85
+ epicsByProject.set(projectId, epicResult.issueKey);
86
+ results.push(epicResult);
87
+ }
88
+ }
88
89
  for (const [projectId, stories] of projectStories.entries()) {
89
90
  const epicKey = epicsByProject.get(projectId);
90
91
  for (const { story, confidence } of stories) {
@@ -167,7 +168,16 @@ _Classification confidence: ${(confidence * 100).toFixed(0)}%_
167
168
  issuetype: { name: this.getIssueTypeName(itemType) }
168
169
  };
169
170
  if (epicKey) {
170
- issueData.parent = { key: epicKey };
171
+ const { field: epicField, style } = await getEpicLinkFieldForProject(
172
+ this.config.domain,
173
+ projectId,
174
+ { email: this.config.email, apiToken: this.config.apiToken }
175
+ );
176
+ if (style === "next-gen") {
177
+ issueData.parent = { key: epicKey };
178
+ } else {
179
+ issueData[epicField] = epicKey;
180
+ }
171
181
  }
172
182
  const issue = await this.client.createIssue(issueData);
173
183
  return {
@@ -23,6 +23,7 @@ import {
23
23
  mapUserStoryToProjects
24
24
  } from '../../../src/utils/project-mapper.js';
25
25
  import { parseSpecFile } from '../../../src/utils/spec-splitter.js';
26
+ import { getEpicLinkFieldForProject } from './jira-field-discovery.js';
26
27
 
27
28
  export interface JiraMultiProjectConfig {
28
29
  domain: string;
@@ -108,18 +109,7 @@ export class JiraMultiProjectSync {
108
109
  // Parse spec
109
110
  const parsedSpec = await parseSpecFile(specPath);
110
111
 
111
- // Step 1: Create epic per project (if enabled)
112
- const epicsByProject = new Map<string, string>(); // projectId → epicKey
113
-
114
- if (this.config.autoCreateEpics !== false) {
115
- for (const project of this.config.projects) {
116
- const epicResult = await this.createEpicForProject(parsedSpec, project);
117
- epicsByProject.set(project, epicResult.issueKey);
118
- results.push(epicResult);
119
- }
120
- }
121
-
122
- // Step 2: Classify user stories by project
112
+ // Step 1: Classify user stories by project FIRST (before creating epics)
123
113
  const projectStories = new Map<string, Array<{ story: UserStory; confidence: number }>>();
124
114
 
125
115
  for (const userStory of parsedSpec.userStories) {
@@ -162,6 +152,17 @@ export class JiraMultiProjectSync {
162
152
  }
163
153
  }
164
154
 
155
+ // Step 2: Create epics ONLY for projects that have classified stories
156
+ const epicsByProject = new Map<string, string>(); // projectId → epicKey
157
+
158
+ if (this.config.autoCreateEpics !== false) {
159
+ for (const projectId of projectStories.keys()) {
160
+ const epicResult = await this.createEpicForProject(parsedSpec, projectId);
161
+ epicsByProject.set(projectId, epicResult.issueKey);
162
+ results.push(epicResult);
163
+ }
164
+ }
165
+
165
166
  // Step 3: Create issues in each project
166
167
  for (const [projectId, stories] of projectStories.entries()) {
167
168
  const epicKey = epicsByProject.get(projectId);
@@ -255,9 +256,19 @@ ${confidence !== undefined ? `\n_Classification confidence: ${(confidence * 100)
255
256
  issuetype: { name: this.getIssueTypeName(itemType) }
256
257
  };
257
258
 
258
- // Link to epic if provided
259
+ // Link to epic using dynamic field discovery
259
260
  if (epicKey) {
260
- issueData.parent = { key: epicKey };
261
+ const { field: epicField, style } = await getEpicLinkFieldForProject(
262
+ this.config.domain,
263
+ projectId,
264
+ { email: this.config.email, apiToken: this.config.apiToken }
265
+ );
266
+
267
+ if (style === 'next-gen') {
268
+ issueData.parent = { key: epicKey };
269
+ } else {
270
+ issueData[epicField] = epicKey;
271
+ }
261
272
  }
262
273
 
263
274
  const issue = await this.client.createIssue(issueData);
@@ -0,0 +1,55 @@
1
+ const DEFAULT_PAGE_SIZE = 50;
2
+ const MAX_RETRIES = 3;
3
+ const BASE_DELAY_MS = 1e3;
4
+ async function searchAllIssues(client, options) {
5
+ const { jql, fields, maxResults = DEFAULT_PAGE_SIZE } = options;
6
+ const allIssues = [];
7
+ let startAt = 0;
8
+ let total = Infinity;
9
+ while (startAt < total) {
10
+ const response = await requestWithRetry(client, "/search", {
11
+ params: {
12
+ jql,
13
+ startAt,
14
+ maxResults,
15
+ ...fields ? { fields } : {}
16
+ }
17
+ });
18
+ const data = response.data;
19
+ total = data.total;
20
+ const issues = data.issues || [];
21
+ allIssues.push(...issues);
22
+ startAt += issues.length;
23
+ if (issues.length === 0) break;
24
+ }
25
+ return allIssues;
26
+ }
27
+ async function requestWithRetry(client, url, config, attempt = 0) {
28
+ try {
29
+ return await client.get(url, config);
30
+ } catch (error) {
31
+ const axiosError = error;
32
+ const status = axiosError.response?.status;
33
+ if (status === 429 && attempt < MAX_RETRIES) {
34
+ const retryAfterHeader = axiosError.response?.headers?.["retry-after"];
35
+ const retryAfterMs = retryAfterHeader ? parseInt(retryAfterHeader, 10) * 1e3 : BASE_DELAY_MS * Math.pow(2, attempt);
36
+ console.warn(
37
+ `Rate limited (429). Retry ${attempt + 1}/${MAX_RETRIES} after ${retryAfterMs}ms...`
38
+ );
39
+ await sleep(retryAfterMs);
40
+ return requestWithRetry(client, url, config, attempt + 1);
41
+ }
42
+ if (status === 429) {
43
+ throw new Error(
44
+ `JIRA rate limit exceeded after ${MAX_RETRIES} retries. Try again later or reduce request frequency.`
45
+ );
46
+ }
47
+ throw error;
48
+ }
49
+ }
50
+ function sleep(ms) {
51
+ return new Promise((resolve) => setTimeout(resolve, ms));
52
+ }
53
+ export {
54
+ searchAllIssues
55
+ };
@@ -0,0 +1,108 @@
1
+ /**
2
+ * JIRA Paginated Search with Rate-Limit Retry
3
+ *
4
+ * Provides a paginated JQL search that fetches all results
5
+ * by iterating through pages using startAt/maxResults.
6
+ *
7
+ * Includes exponential backoff retry on HTTP 429 (rate limit).
8
+ *
9
+ * @module jira-paginated-search
10
+ */
11
+
12
+ import axios, { AxiosInstance, AxiosError } from 'axios';
13
+
14
+ const DEFAULT_PAGE_SIZE = 50;
15
+ const MAX_RETRIES = 3;
16
+ const BASE_DELAY_MS = 1000;
17
+
18
+ export interface PaginatedSearchOptions {
19
+ jql: string;
20
+ fields?: string;
21
+ maxResults?: number;
22
+ }
23
+
24
+ /**
25
+ * Search all issues matching a JQL query with full pagination.
26
+ * Handles rate limiting with exponential backoff.
27
+ *
28
+ * @param client - Axios instance configured with JIRA auth
29
+ * @param options - Search options (jql, fields, maxResults per page)
30
+ * @returns All matching issues across all pages
31
+ */
32
+ export async function searchAllIssues(
33
+ client: AxiosInstance,
34
+ options: PaginatedSearchOptions
35
+ ): Promise<any[]> {
36
+ const { jql, fields, maxResults = DEFAULT_PAGE_SIZE } = options;
37
+ const allIssues: any[] = [];
38
+ let startAt = 0;
39
+ let total = Infinity;
40
+
41
+ while (startAt < total) {
42
+ const response = await requestWithRetry(client, '/search', {
43
+ params: {
44
+ jql,
45
+ startAt,
46
+ maxResults,
47
+ ...(fields ? { fields } : {}),
48
+ },
49
+ });
50
+
51
+ const data = response.data;
52
+ total = data.total;
53
+ const issues = data.issues || [];
54
+ allIssues.push(...issues);
55
+
56
+ startAt += issues.length;
57
+
58
+ // Safety: if no issues returned, break to avoid infinite loop
59
+ if (issues.length === 0) break;
60
+ }
61
+
62
+ return allIssues;
63
+ }
64
+
65
+ /**
66
+ * Make an HTTP GET request with exponential backoff retry on 429.
67
+ */
68
+ async function requestWithRetry(
69
+ client: AxiosInstance,
70
+ url: string,
71
+ config: any,
72
+ attempt: number = 0
73
+ ): Promise<any> {
74
+ try {
75
+ return await client.get(url, config);
76
+ } catch (error: any) {
77
+ const axiosError = error as AxiosError;
78
+ const status = axiosError.response?.status;
79
+
80
+ if (status === 429 && attempt < MAX_RETRIES) {
81
+ // Read Retry-After header (seconds) or use exponential backoff
82
+ const retryAfterHeader = axiosError.response?.headers?.['retry-after'];
83
+ const retryAfterMs = retryAfterHeader
84
+ ? parseInt(retryAfterHeader, 10) * 1000
85
+ : BASE_DELAY_MS * Math.pow(2, attempt);
86
+
87
+ console.warn(
88
+ `Rate limited (429). Retry ${attempt + 1}/${MAX_RETRIES} after ${retryAfterMs}ms...`
89
+ );
90
+
91
+ await sleep(retryAfterMs);
92
+ return requestWithRetry(client, url, config, attempt + 1);
93
+ }
94
+
95
+ if (status === 429) {
96
+ throw new Error(
97
+ `JIRA rate limit exceeded after ${MAX_RETRIES} retries. ` +
98
+ `Try again later or reduce request frequency.`
99
+ );
100
+ }
101
+
102
+ throw error;
103
+ }
104
+ }
105
+
106
+ function sleep(ms: number): Promise<void> {
107
+ return new Promise((resolve) => setTimeout(resolve, ms));
108
+ }
@@ -18,6 +18,8 @@ import {
18
18
  generateSmartSummary,
19
19
  formatForJira
20
20
  } from "../../../src/core/comment-builder.js";
21
+ import { readIssueKey } from "./metadata-paths.js";
22
+ import { getApiBaseUrl } from "./jira-deployment-detector.js";
21
23
  import path from "path";
22
24
  import fs from "fs/promises";
23
25
  async function syncSpecCommitsToJira(config, options) {
@@ -38,7 +40,7 @@ async function syncSpecCommitsToJira(config, options) {
38
40
  result.errors.push("No metadata.json found");
39
41
  return result;
40
42
  }
41
- const jiraIssueKey = metadata.jira?.issueKey;
43
+ const jiraIssueKey = readIssueKey(metadata);
42
44
  if (!jiraIssueKey) {
43
45
  if (verbose) {
44
46
  console.log("No JIRA issue linked to increment");
@@ -46,7 +48,7 @@ async function syncSpecCommitsToJira(config, options) {
46
48
  return result;
47
49
  }
48
50
  const client = axios.create({
49
- baseURL: `https://${config.domain}/rest/api/3`,
51
+ baseURL: getApiBaseUrl(config.domain),
50
52
  auth: {
51
53
  username: config.email,
52
54
  password: config.apiToken
@@ -194,7 +196,7 @@ Short update comment (JIRA format):`);
194
196
  async function postCommitBatchUpdate(config, incrementPath, commits, issueKey, repo, dryRun = false) {
195
197
  try {
196
198
  const client = axios.create({
197
- baseURL: `https://${config.domain}/rest/api/3`,
199
+ baseURL: getApiBaseUrl(config.domain),
198
200
  auth: {
199
201
  username: config.email,
200
202
  password: config.apiToken