light-plan 0.0.0-stage → 0.1.0

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 (963) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3181 -2
  3. package/assets/README.md +111 -0
  4. package/assets/agents/lpm-developer.md +468 -0
  5. package/assets/agents/lpm-planner.md +532 -0
  6. package/assets/harnesses/claude.yml +43 -0
  7. package/assets/harnesses/copilot.yml +54 -0
  8. package/assets/harnesses/reasonix.yml +49 -0
  9. package/assets/hcm/light-plan.yml +45 -0
  10. package/assets/skills/lpm-board-health.md +243 -0
  11. package/assets/skills/lpm-board-setup.md +354 -0
  12. package/assets/skills/lpm-delivery.md +344 -0
  13. package/assets/skills/lpm-planning.md +411 -0
  14. package/assets/skills/lpm-templates.md +144 -0
  15. package/assets/skills/lpm.md +267 -0
  16. package/dist/cli/agent-view.d.ts +27 -0
  17. package/dist/cli/agent-view.js +247 -0
  18. package/dist/cli/agent-view.js.map +1 -0
  19. package/dist/cli/commands/agent/assets.d.ts +64 -0
  20. package/dist/cli/commands/agent/assets.js +106 -0
  21. package/dist/cli/commands/agent/assets.js.map +1 -0
  22. package/dist/cli/commands/agent/glob.d.ts +25 -0
  23. package/dist/cli/commands/agent/glob.js +82 -0
  24. package/dist/cli/commands/agent/glob.js.map +1 -0
  25. package/dist/cli/commands/agent/index.d.ts +11 -0
  26. package/dist/cli/commands/agent/index.js +284 -0
  27. package/dist/cli/commands/agent/index.js.map +1 -0
  28. package/dist/cli/commands/agent/install.d.ts +59 -0
  29. package/dist/cli/commands/agent/install.js +131 -0
  30. package/dist/cli/commands/agent/install.js.map +1 -0
  31. package/dist/cli/commands/agent/mapping.d.ts +175 -0
  32. package/dist/cli/commands/agent/mapping.js +256 -0
  33. package/dist/cli/commands/agent/mapping.js.map +1 -0
  34. package/dist/cli/commands/check.d.ts +2 -0
  35. package/dist/cli/commands/check.js +81 -0
  36. package/dist/cli/commands/check.js.map +1 -0
  37. package/dist/cli/commands/comment.d.ts +2 -0
  38. package/dist/cli/commands/comment.js +104 -0
  39. package/dist/cli/commands/comment.js.map +1 -0
  40. package/dist/cli/commands/convert.d.ts +2 -0
  41. package/dist/cli/commands/convert.js +115 -0
  42. package/dist/cli/commands/convert.js.map +1 -0
  43. package/dist/cli/commands/copy.d.ts +2 -0
  44. package/dist/cli/commands/copy.js +69 -0
  45. package/dist/cli/commands/copy.js.map +1 -0
  46. package/dist/cli/commands/export.d.ts +11 -0
  47. package/dist/cli/commands/export.js +226 -0
  48. package/dist/cli/commands/export.js.map +1 -0
  49. package/dist/cli/commands/flag.d.ts +2 -0
  50. package/dist/cli/commands/flag.js +172 -0
  51. package/dist/cli/commands/flag.js.map +1 -0
  52. package/dist/cli/commands/git.d.ts +11 -0
  53. package/dist/cli/commands/git.js +357 -0
  54. package/dist/cli/commands/git.js.map +1 -0
  55. package/dist/cli/commands/hcm/bundle.d.ts +60 -0
  56. package/dist/cli/commands/hcm/bundle.js +166 -0
  57. package/dist/cli/commands/hcm/bundle.js.map +1 -0
  58. package/dist/cli/commands/hcm/index.d.ts +11 -0
  59. package/dist/cli/commands/hcm/index.js +162 -0
  60. package/dist/cli/commands/hcm/index.js.map +1 -0
  61. package/dist/cli/commands/init.d.ts +2 -0
  62. package/dist/cli/commands/init.js +81 -0
  63. package/dist/cli/commands/init.js.map +1 -0
  64. package/dist/cli/commands/insert.d.ts +2 -0
  65. package/dist/cli/commands/insert.js +70 -0
  66. package/dist/cli/commands/insert.js.map +1 -0
  67. package/dist/cli/commands/instructions.d.ts +7 -0
  68. package/dist/cli/commands/instructions.js +256 -0
  69. package/dist/cli/commands/instructions.js.map +1 -0
  70. package/dist/cli/commands/link.d.ts +2 -0
  71. package/dist/cli/commands/link.js +121 -0
  72. package/dist/cli/commands/link.js.map +1 -0
  73. package/dist/cli/commands/mcp/config.d.ts +71 -0
  74. package/dist/cli/commands/mcp/config.js +119 -0
  75. package/dist/cli/commands/mcp/config.js.map +1 -0
  76. package/dist/cli/commands/mcp/index.d.ts +9 -0
  77. package/dist/cli/commands/mcp/index.js +92 -0
  78. package/dist/cli/commands/mcp/index.js.map +1 -0
  79. package/dist/cli/commands/mcp/serve.d.ts +1 -0
  80. package/dist/cli/commands/mcp/serve.js +76 -0
  81. package/dist/cli/commands/mcp/serve.js.map +1 -0
  82. package/dist/cli/commands/mcp/setup.d.ts +1 -0
  83. package/dist/cli/commands/mcp/setup.js +83 -0
  84. package/dist/cli/commands/mcp/setup.js.map +1 -0
  85. package/dist/cli/commands/me.d.ts +2 -0
  86. package/dist/cli/commands/me.js +85 -0
  87. package/dist/cli/commands/me.js.map +1 -0
  88. package/dist/cli/commands/move.d.ts +2 -0
  89. package/dist/cli/commands/move.js +81 -0
  90. package/dist/cli/commands/move.js.map +1 -0
  91. package/dist/cli/commands/new.d.ts +2 -0
  92. package/dist/cli/commands/new.js +222 -0
  93. package/dist/cli/commands/new.js.map +1 -0
  94. package/dist/cli/commands/open.d.ts +2 -0
  95. package/dist/cli/commands/open.js +43 -0
  96. package/dist/cli/commands/open.js.map +1 -0
  97. package/dist/cli/commands/period.d.ts +10 -0
  98. package/dist/cli/commands/period.js +165 -0
  99. package/dist/cli/commands/period.js.map +1 -0
  100. package/dist/cli/commands/profile.d.ts +2 -0
  101. package/dist/cli/commands/profile.js +133 -0
  102. package/dist/cli/commands/profile.js.map +1 -0
  103. package/dist/cli/commands/queue.d.ts +2 -0
  104. package/dist/cli/commands/queue.js +582 -0
  105. package/dist/cli/commands/queue.js.map +1 -0
  106. package/dist/cli/commands/remote.d.ts +10 -0
  107. package/dist/cli/commands/remote.js +3142 -0
  108. package/dist/cli/commands/remote.js.map +1 -0
  109. package/dist/cli/commands/rm.d.ts +2 -0
  110. package/dist/cli/commands/rm.js +63 -0
  111. package/dist/cli/commands/rm.js.map +1 -0
  112. package/dist/cli/commands/set.d.ts +2 -0
  113. package/dist/cli/commands/set.js +139 -0
  114. package/dist/cli/commands/set.js.map +1 -0
  115. package/dist/cli/commands/split.d.ts +2 -0
  116. package/dist/cli/commands/split.js +90 -0
  117. package/dist/cli/commands/split.js.map +1 -0
  118. package/dist/cli/commands/task.d.ts +2 -0
  119. package/dist/cli/commands/task.js +298 -0
  120. package/dist/cli/commands/task.js.map +1 -0
  121. package/dist/cli/commands/team.d.ts +2 -0
  122. package/dist/cli/commands/team.js +114 -0
  123. package/dist/cli/commands/team.js.map +1 -0
  124. package/dist/cli/commands/template.d.ts +2 -0
  125. package/dist/cli/commands/template.js +338 -0
  126. package/dist/cli/commands/template.js.map +1 -0
  127. package/dist/cli/commands/ui.d.ts +2 -0
  128. package/dist/cli/commands/ui.js +98 -0
  129. package/dist/cli/commands/ui.js.map +1 -0
  130. package/dist/cli/commands/upstream.d.ts +2 -0
  131. package/dist/cli/commands/upstream.js +218 -0
  132. package/dist/cli/commands/upstream.js.map +1 -0
  133. package/dist/cli/context.d.ts +24 -0
  134. package/dist/cli/context.js +52 -0
  135. package/dist/cli/context.js.map +1 -0
  136. package/dist/cli/index.d.ts +2 -0
  137. package/dist/cli/index.js +208 -0
  138. package/dist/cli/index.js.map +1 -0
  139. package/dist/cli/live.d.ts +71 -0
  140. package/dist/cli/live.js +182 -0
  141. package/dist/cli/live.js.map +1 -0
  142. package/dist/cli/plan.d.ts +28 -0
  143. package/dist/cli/plan.js +76 -0
  144. package/dist/cli/plan.js.map +1 -0
  145. package/dist/cli/prompt.d.ts +60 -0
  146. package/dist/cli/prompt.js +185 -0
  147. package/dist/cli/prompt.js.map +1 -0
  148. package/dist/cli/ui.d.ts +65 -0
  149. package/dist/cli/ui.js +79 -0
  150. package/dist/cli/ui.js.map +1 -0
  151. package/dist/core/board/dependency-rollup.d.ts +54 -0
  152. package/dist/core/board/dependency-rollup.js +72 -0
  153. package/dist/core/board/dependency-rollup.js.map +1 -0
  154. package/dist/core/board/flag-rollup.d.ts +33 -0
  155. package/dist/core/board/flag-rollup.js +56 -0
  156. package/dist/core/board/flag-rollup.js.map +1 -0
  157. package/dist/core/board/index.d.ts +21 -0
  158. package/dist/core/board/index.js +22 -0
  159. package/dist/core/board/index.js.map +1 -0
  160. package/dist/core/board/load/cache.d.ts +52 -0
  161. package/dist/core/board/load/cache.js +105 -0
  162. package/dist/core/board/load/cache.js.map +1 -0
  163. package/dist/core/board/load/fields.d.ts +33 -0
  164. package/dist/core/board/load/fields.js +154 -0
  165. package/dist/core/board/load/fields.js.map +1 -0
  166. package/dist/core/board/load/scan.d.ts +39 -0
  167. package/dist/core/board/load/scan.js +88 -0
  168. package/dist/core/board/load/scan.js.map +1 -0
  169. package/dist/core/board/load/tree.d.ts +10 -0
  170. package/dist/core/board/load/tree.js +114 -0
  171. package/dist/core/board/load/tree.js.map +1 -0
  172. package/dist/core/board/load.d.ts +69 -0
  173. package/dist/core/board/load.js +204 -0
  174. package/dist/core/board/load.js.map +1 -0
  175. package/dist/core/board/query.d.ts +126 -0
  176. package/dist/core/board/query.js +291 -0
  177. package/dist/core/board/query.js.map +1 -0
  178. package/dist/core/board/registry.d.ts +51 -0
  179. package/dist/core/board/registry.js +98 -0
  180. package/dist/core/board/registry.js.map +1 -0
  181. package/dist/core/board/remote-scopes.d.ts +71 -0
  182. package/dist/core/board/remote-scopes.js +103 -0
  183. package/dist/core/board/remote-scopes.js.map +1 -0
  184. package/dist/core/board/rollup.d.ts +32 -0
  185. package/dist/core/board/rollup.js +67 -0
  186. package/dist/core/board/rollup.js.map +1 -0
  187. package/dist/core/board/scope.d.ts +81 -0
  188. package/dist/core/board/scope.js +134 -0
  189. package/dist/core/board/scope.js.map +1 -0
  190. package/dist/core/board/simulate.d.ts +138 -0
  191. package/dist/core/board/simulate.js +141 -0
  192. package/dist/core/board/simulate.js.map +1 -0
  193. package/dist/core/board/tasks/index.d.ts +25 -0
  194. package/dist/core/board/tasks/index.js +26 -0
  195. package/dist/core/board/tasks/index.js.map +1 -0
  196. package/dist/core/board/tasks/ranking.d.ts +155 -0
  197. package/dist/core/board/tasks/ranking.js +374 -0
  198. package/dist/core/board/tasks/ranking.js.map +1 -0
  199. package/dist/core/board/tasks/roster.d.ts +42 -0
  200. package/dist/core/board/tasks/roster.js +82 -0
  201. package/dist/core/board/tasks/roster.js.map +1 -0
  202. package/dist/core/board/tasks.d.ts +1 -0
  203. package/dist/core/board/tasks.js +2 -0
  204. package/dist/core/board/tasks.js.map +1 -0
  205. package/dist/core/config/index.d.ts +9 -0
  206. package/dist/core/config/index.js +10 -0
  207. package/dist/core/config/index.js.map +1 -0
  208. package/dist/core/config/lookup.d.ts +114 -0
  209. package/dist/core/config/lookup.js +221 -0
  210. package/dist/core/config/lookup.js.map +1 -0
  211. package/dist/core/config/remote-blocks.d.ts +17 -0
  212. package/dist/core/config/remote-blocks.js +47 -0
  213. package/dist/core/config/remote-blocks.js.map +1 -0
  214. package/dist/core/config/schema.d.ts +10 -0
  215. package/dist/core/config/schema.js +451 -0
  216. package/dist/core/config/schema.js.map +1 -0
  217. package/dist/core/errors.d.ts +23 -0
  218. package/dist/core/errors.js +31 -0
  219. package/dist/core/errors.js.map +1 -0
  220. package/dist/core/gitsync/hosts.d.ts +40 -0
  221. package/dist/core/gitsync/hosts.js +115 -0
  222. package/dist/core/gitsync/hosts.js.map +1 -0
  223. package/dist/core/gitsync/index.d.ts +25 -0
  224. package/dist/core/gitsync/index.js +25 -0
  225. package/dist/core/gitsync/index.js.map +1 -0
  226. package/dist/core/gitsync/integrate.d.ts +72 -0
  227. package/dist/core/gitsync/integrate.js +115 -0
  228. package/dist/core/gitsync/integrate.js.map +1 -0
  229. package/dist/core/gitsync/repo.d.ts +61 -0
  230. package/dist/core/gitsync/repo.js +213 -0
  231. package/dist/core/gitsync/repo.js.map +1 -0
  232. package/dist/core/gitsync/run.d.ts +58 -0
  233. package/dist/core/gitsync/run.js +102 -0
  234. package/dist/core/gitsync/run.js.map +1 -0
  235. package/dist/core/gitsync/status.d.ts +59 -0
  236. package/dist/core/gitsync/status.js +81 -0
  237. package/dist/core/gitsync/status.js.map +1 -0
  238. package/dist/core/gitsync/sync.d.ts +111 -0
  239. package/dist/core/gitsync/sync.js +275 -0
  240. package/dist/core/gitsync/sync.js.map +1 -0
  241. package/dist/core/index.d.ts +31 -0
  242. package/dist/core/index.js +32 -0
  243. package/dist/core/index.js.map +1 -0
  244. package/dist/core/instructions/analyze.d.ts +70 -0
  245. package/dist/core/instructions/analyze.js +275 -0
  246. package/dist/core/instructions/analyze.js.map +1 -0
  247. package/dist/core/instructions/builtin.d.ts +21 -0
  248. package/dist/core/instructions/builtin.js +133 -0
  249. package/dist/core/instructions/builtin.js.map +1 -0
  250. package/dist/core/instructions/context.d.ts +155 -0
  251. package/dist/core/instructions/context.js +209 -0
  252. package/dist/core/instructions/context.js.map +1 -0
  253. package/dist/core/instructions/index.d.ts +29 -0
  254. package/dist/core/instructions/index.js +30 -0
  255. package/dist/core/instructions/index.js.map +1 -0
  256. package/dist/core/instructions/instructions.d.ts +90 -0
  257. package/dist/core/instructions/instructions.js +179 -0
  258. package/dist/core/instructions/instructions.js.map +1 -0
  259. package/dist/core/instructions/template.d.ts +108 -0
  260. package/dist/core/instructions/template.js +252 -0
  261. package/dist/core/instructions/template.js.map +1 -0
  262. package/dist/core/model/attributes.d.ts +16 -0
  263. package/dist/core/model/attributes.js +119 -0
  264. package/dist/core/model/attributes.js.map +1 -0
  265. package/dist/core/model/index.d.ts +10 -0
  266. package/dist/core/model/index.js +11 -0
  267. package/dist/core/model/index.js.map +1 -0
  268. package/dist/core/model/links.d.ts +34 -0
  269. package/dist/core/model/links.js +114 -0
  270. package/dist/core/model/links.js.map +1 -0
  271. package/dist/core/model/profile.d.ts +31 -0
  272. package/dist/core/model/profile.js +8 -0
  273. package/dist/core/model/profile.js.map +1 -0
  274. package/dist/core/model/types.d.ts +441 -0
  275. package/dist/core/model/types.js +167 -0
  276. package/dist/core/model/types.js.map +1 -0
  277. package/dist/core/model/zod.d.ts +6 -0
  278. package/dist/core/model/zod.js +11 -0
  279. package/dist/core/model/zod.js.map +1 -0
  280. package/dist/core/operations/board-index.d.ts +45 -0
  281. package/dist/core/operations/board-index.js +147 -0
  282. package/dist/core/operations/board-index.js.map +1 -0
  283. package/dist/core/operations/claim.d.ts +69 -0
  284. package/dist/core/operations/claim.js +115 -0
  285. package/dist/core/operations/claim.js.map +1 -0
  286. package/dist/core/operations/comment.d.ts +30 -0
  287. package/dist/core/operations/comment.js +46 -0
  288. package/dist/core/operations/comment.js.map +1 -0
  289. package/dist/core/operations/create.d.ts +82 -0
  290. package/dist/core/operations/create.js +306 -0
  291. package/dist/core/operations/create.js.map +1 -0
  292. package/dist/core/operations/flag.d.ts +58 -0
  293. package/dist/core/operations/flag.js +95 -0
  294. package/dist/core/operations/flag.js.map +1 -0
  295. package/dist/core/operations/git-sync.d.ts +148 -0
  296. package/dist/core/operations/git-sync.js +341 -0
  297. package/dist/core/operations/git-sync.js.map +1 -0
  298. package/dist/core/operations/index.d.ts +44 -0
  299. package/dist/core/operations/index.js +45 -0
  300. package/dist/core/operations/index.js.map +1 -0
  301. package/dist/core/operations/init.d.ts +25 -0
  302. package/dist/core/operations/init.js +122 -0
  303. package/dist/core/operations/init.js.map +1 -0
  304. package/dist/core/operations/link.d.ts +40 -0
  305. package/dist/core/operations/link.js +131 -0
  306. package/dist/core/operations/link.js.map +1 -0
  307. package/dist/core/operations/move.d.ts +43 -0
  308. package/dist/core/operations/move.js +103 -0
  309. package/dist/core/operations/move.js.map +1 -0
  310. package/dist/core/operations/profile.d.ts +23 -0
  311. package/dist/core/operations/profile.js +26 -0
  312. package/dist/core/operations/profile.js.map +1 -0
  313. package/dist/core/operations/remotes-off.d.ts +12 -0
  314. package/dist/core/operations/remotes-off.js +67 -0
  315. package/dist/core/operations/remotes-off.js.map +1 -0
  316. package/dist/core/operations/remove.d.ts +18 -0
  317. package/dist/core/operations/remove.js +114 -0
  318. package/dist/core/operations/remove.js.map +1 -0
  319. package/dist/core/operations/retype.d.ts +25 -0
  320. package/dist/core/operations/retype.js +59 -0
  321. package/dist/core/operations/retype.js.map +1 -0
  322. package/dist/core/operations/rollup.d.ts +74 -0
  323. package/dist/core/operations/rollup.js +121 -0
  324. package/dist/core/operations/rollup.js.map +1 -0
  325. package/dist/core/operations/shared.d.ts +133 -0
  326. package/dist/core/operations/shared.js +383 -0
  327. package/dist/core/operations/shared.js.map +1 -0
  328. package/dist/core/operations/update.d.ts +43 -0
  329. package/dist/core/operations/update.js +161 -0
  330. package/dist/core/operations/update.js.map +1 -0
  331. package/dist/core/operations/user.d.ts +27 -0
  332. package/dist/core/operations/user.js +68 -0
  333. package/dist/core/operations/user.js.map +1 -0
  334. package/dist/core/profile/current.d.ts +35 -0
  335. package/dist/core/profile/current.js +31 -0
  336. package/dist/core/profile/current.js.map +1 -0
  337. package/dist/core/profile/index.d.ts +14 -0
  338. package/dist/core/profile/index.js +15 -0
  339. package/dist/core/profile/index.js.map +1 -0
  340. package/dist/core/profile/schema.d.ts +9 -0
  341. package/dist/core/profile/schema.js +100 -0
  342. package/dist/core/profile/schema.js.map +1 -0
  343. package/dist/core/storage/activity.d.ts +78 -0
  344. package/dist/core/storage/activity.js +131 -0
  345. package/dist/core/storage/activity.js.map +1 -0
  346. package/dist/core/storage/atomic.d.ts +34 -0
  347. package/dist/core/storage/atomic.js +117 -0
  348. package/dist/core/storage/atomic.js.map +1 -0
  349. package/dist/core/storage/comments.d.ts +39 -0
  350. package/dist/core/storage/comments.js +88 -0
  351. package/dist/core/storage/comments.js.map +1 -0
  352. package/dist/core/storage/document.d.ts +16 -0
  353. package/dist/core/storage/document.js +98 -0
  354. package/dist/core/storage/document.js.map +1 -0
  355. package/dist/core/storage/frontmatter.d.ts +16 -0
  356. package/dist/core/storage/frontmatter.js +33 -0
  357. package/dist/core/storage/frontmatter.js.map +1 -0
  358. package/dist/core/storage/git.d.ts +8 -0
  359. package/dist/core/storage/git.js +48 -0
  360. package/dist/core/storage/git.js.map +1 -0
  361. package/dist/core/storage/index.d.ts +27 -0
  362. package/dist/core/storage/index.js +28 -0
  363. package/dist/core/storage/index.js.map +1 -0
  364. package/dist/core/storage/local.d.ts +57 -0
  365. package/dist/core/storage/local.js +133 -0
  366. package/dist/core/storage/local.js.map +1 -0
  367. package/dist/core/storage/lock.d.ts +85 -0
  368. package/dist/core/storage/lock.js +364 -0
  369. package/dist/core/storage/lock.js.map +1 -0
  370. package/dist/core/storage/paths.d.ts +125 -0
  371. package/dist/core/storage/paths.js +202 -0
  372. package/dist/core/storage/paths.js.map +1 -0
  373. package/dist/core/storage/state.d.ts +32 -0
  374. package/dist/core/storage/state.js +71 -0
  375. package/dist/core/storage/state.js.map +1 -0
  376. package/dist/core/storage/templates.d.ts +29 -0
  377. package/dist/core/storage/templates.js +62 -0
  378. package/dist/core/storage/templates.js.map +1 -0
  379. package/dist/core/storage/views.d.ts +18 -0
  380. package/dist/core/storage/views.js +62 -0
  381. package/dist/core/storage/views.js.map +1 -0
  382. package/dist/core/validation/check.d.ts +4 -0
  383. package/dist/core/validation/check.js +22 -0
  384. package/dist/core/validation/check.js.map +1 -0
  385. package/dist/core/validation/checks/collection.d.ts +4 -0
  386. package/dist/core/validation/checks/collection.js +152 -0
  387. package/dist/core/validation/checks/collection.js.map +1 -0
  388. package/dist/core/validation/checks/counters.d.ts +3 -0
  389. package/dist/core/validation/checks/counters.js +23 -0
  390. package/dist/core/validation/checks/counters.js.map +1 -0
  391. package/dist/core/validation/checks/dependencies.d.ts +3 -0
  392. package/dist/core/validation/checks/dependencies.js +58 -0
  393. package/dist/core/validation/checks/dependencies.js.map +1 -0
  394. package/dist/core/validation/checks/gitignore.d.ts +10 -0
  395. package/dist/core/validation/checks/gitignore.js +22 -0
  396. package/dist/core/validation/checks/gitignore.js.map +1 -0
  397. package/dist/core/validation/checks/index-file.d.ts +8 -0
  398. package/dist/core/validation/checks/index-file.js +26 -0
  399. package/dist/core/validation/checks/index-file.js.map +1 -0
  400. package/dist/core/validation/checks/index.d.ts +20 -0
  401. package/dist/core/validation/checks/index.js +21 -0
  402. package/dist/core/validation/checks/index.js.map +1 -0
  403. package/dist/core/validation/checks/issues.d.ts +3 -0
  404. package/dist/core/validation/checks/issues.js +152 -0
  405. package/dist/core/validation/checks/issues.js.map +1 -0
  406. package/dist/core/validation/checks/periods.d.ts +3 -0
  407. package/dist/core/validation/checks/periods.js +89 -0
  408. package/dist/core/validation/checks/periods.js.map +1 -0
  409. package/dist/core/validation/checks/remotes.d.ts +20 -0
  410. package/dist/core/validation/checks/remotes.js +42 -0
  411. package/dist/core/validation/checks/remotes.js.map +1 -0
  412. package/dist/core/validation/checks/resources.d.ts +3 -0
  413. package/dist/core/validation/checks/resources.js +77 -0
  414. package/dist/core/validation/checks/resources.js.map +1 -0
  415. package/dist/core/validation/checks/rollup.d.ts +27 -0
  416. package/dist/core/validation/checks/rollup.js +53 -0
  417. package/dist/core/validation/checks/rollup.js.map +1 -0
  418. package/dist/core/validation/checks/squads.d.ts +3 -0
  419. package/dist/core/validation/checks/squads.js +42 -0
  420. package/dist/core/validation/checks/squads.js.map +1 -0
  421. package/dist/core/validation/checks/templates.d.ts +14 -0
  422. package/dist/core/validation/checks/templates.js +119 -0
  423. package/dist/core/validation/checks/templates.js.map +1 -0
  424. package/dist/core/validation/fix.d.ts +7 -0
  425. package/dist/core/validation/fix.js +273 -0
  426. package/dist/core/validation/fix.js.map +1 -0
  427. package/dist/core/validation/index.d.ts +7 -0
  428. package/dist/core/validation/index.js +8 -0
  429. package/dist/core/validation/index.js.map +1 -0
  430. package/dist/core/validation/shared.d.ts +47 -0
  431. package/dist/core/validation/shared.js +66 -0
  432. package/dist/core/validation/shared.js.map +1 -0
  433. package/dist/mcp/context.d.ts +70 -0
  434. package/dist/mcp/context.js +110 -0
  435. package/dist/mcp/context.js.map +1 -0
  436. package/dist/mcp/index.d.ts +49 -0
  437. package/dist/mcp/index.js +90 -0
  438. package/dist/mcp/index.js.map +1 -0
  439. package/dist/mcp/reply.d.ts +28 -0
  440. package/dist/mcp/reply.js +34 -0
  441. package/dist/mcp/reply.js.map +1 -0
  442. package/dist/mcp/tools/plan/create.d.ts +3 -0
  443. package/dist/mcp/tools/plan/create.js +128 -0
  444. package/dist/mcp/tools/plan/create.js.map +1 -0
  445. package/dist/mcp/tools/plan/delete.d.ts +3 -0
  446. package/dist/mcp/tools/plan/delete.js +35 -0
  447. package/dist/mcp/tools/plan/delete.js.map +1 -0
  448. package/dist/mcp/tools/plan/graph.d.ts +3 -0
  449. package/dist/mcp/tools/plan/graph.js +53 -0
  450. package/dist/mcp/tools/plan/graph.js.map +1 -0
  451. package/dist/mcp/tools/plan/index.d.ts +15 -0
  452. package/dist/mcp/tools/plan/index.js +16 -0
  453. package/dist/mcp/tools/plan/index.js.map +1 -0
  454. package/dist/mcp/tools/plan/link.d.ts +3 -0
  455. package/dist/mcp/tools/plan/link.js +46 -0
  456. package/dist/mcp/tools/plan/link.js.map +1 -0
  457. package/dist/mcp/tools/plan/registrar.d.ts +10 -0
  458. package/dist/mcp/tools/plan/registrar.js +24 -0
  459. package/dist/mcp/tools/plan/registrar.js.map +1 -0
  460. package/dist/mcp/tools/plan/reshape.d.ts +3 -0
  461. package/dist/mcp/tools/plan/reshape.js +95 -0
  462. package/dist/mcp/tools/plan/reshape.js.map +1 -0
  463. package/dist/mcp/tools/plan/timeline.d.ts +3 -0
  464. package/dist/mcp/tools/plan/timeline.js +68 -0
  465. package/dist/mcp/tools/plan/timeline.js.map +1 -0
  466. package/dist/mcp/tools/plan/upstream.d.ts +14 -0
  467. package/dist/mcp/tools/plan/upstream.js +69 -0
  468. package/dist/mcp/tools/plan/upstream.js.map +1 -0
  469. package/dist/mcp/tools/plan.d.ts +1 -0
  470. package/dist/mcp/tools/plan.js +2 -0
  471. package/dist/mcp/tools/plan.js.map +1 -0
  472. package/dist/mcp/tools/read.d.ts +3 -0
  473. package/dist/mcp/tools/read.js +339 -0
  474. package/dist/mcp/tools/read.js.map +1 -0
  475. package/dist/mcp/tools/remote.d.ts +3 -0
  476. package/dist/mcp/tools/remote.js +254 -0
  477. package/dist/mcp/tools/remote.js.map +1 -0
  478. package/dist/mcp/tools/templates.d.ts +12 -0
  479. package/dist/mcp/tools/templates.js +165 -0
  480. package/dist/mcp/tools/templates.js.map +1 -0
  481. package/dist/mcp/tools/work.d.ts +16 -0
  482. package/dist/mcp/tools/work.js +359 -0
  483. package/dist/mcp/tools/work.js.map +1 -0
  484. package/dist/remote/accounts.d.ts +158 -0
  485. package/dist/remote/accounts.js +207 -0
  486. package/dist/remote/accounts.js.map +1 -0
  487. package/dist/remote/adopt.d.ts +139 -0
  488. package/dist/remote/adopt.js +181 -0
  489. package/dist/remote/adopt.js.map +1 -0
  490. package/dist/remote/anchor.d.ts +35 -0
  491. package/dist/remote/anchor.js +53 -0
  492. package/dist/remote/anchor.js.map +1 -0
  493. package/dist/remote/attributes.d.ts +103 -0
  494. package/dist/remote/attributes.js +245 -0
  495. package/dist/remote/attributes.js.map +1 -0
  496. package/dist/remote/audit.d.ts +112 -0
  497. package/dist/remote/audit.js +215 -0
  498. package/dist/remote/audit.js.map +1 -0
  499. package/dist/remote/capabilities.d.ts +267 -0
  500. package/dist/remote/capabilities.js +225 -0
  501. package/dist/remote/capabilities.js.map +1 -0
  502. package/dist/remote/check.d.ts +49 -0
  503. package/dist/remote/check.js +356 -0
  504. package/dist/remote/check.js.map +1 -0
  505. package/dist/remote/comments.d.ts +86 -0
  506. package/dist/remote/comments.js +87 -0
  507. package/dist/remote/comments.js.map +1 -0
  508. package/dist/remote/config-file.d.ts +237 -0
  509. package/dist/remote/config-file.js +640 -0
  510. package/dist/remote/config-file.js.map +1 -0
  511. package/dist/remote/conflicts.d.ts +110 -0
  512. package/dist/remote/conflicts.js +188 -0
  513. package/dist/remote/conflicts.js.map +1 -0
  514. package/dist/remote/connection-catalogue.d.ts +65 -0
  515. package/dist/remote/connection-catalogue.js +66 -0
  516. package/dist/remote/connection-catalogue.js.map +1 -0
  517. package/dist/remote/connection.d.ts +84 -0
  518. package/dist/remote/connection.js +229 -0
  519. package/dist/remote/connection.js.map +1 -0
  520. package/dist/remote/coverage.d.ts +65 -0
  521. package/dist/remote/coverage.js +244 -0
  522. package/dist/remote/coverage.js.map +1 -0
  523. package/dist/remote/credentials.d.ts +95 -0
  524. package/dist/remote/credentials.js +210 -0
  525. package/dist/remote/credentials.js.map +1 -0
  526. package/dist/remote/execute.d.ts +178 -0
  527. package/dist/remote/execute.js +1245 -0
  528. package/dist/remote/execute.js.map +1 -0
  529. package/dist/remote/fingerprint.d.ts +172 -0
  530. package/dist/remote/fingerprint.js +337 -0
  531. package/dist/remote/fingerprint.js.map +1 -0
  532. package/dist/remote/fixtures.d.ts +176 -0
  533. package/dist/remote/fixtures.js +432 -0
  534. package/dist/remote/fixtures.js.map +1 -0
  535. package/dist/remote/guard.d.ts +90 -0
  536. package/dist/remote/guard.js +83 -0
  537. package/dist/remote/guard.js.map +1 -0
  538. package/dist/remote/hierarchy.d.ts +141 -0
  539. package/dist/remote/hierarchy.js +129 -0
  540. package/dist/remote/hierarchy.js.map +1 -0
  541. package/dist/remote/index.d.ts +145 -0
  542. package/dist/remote/index.js +146 -0
  543. package/dist/remote/index.js.map +1 -0
  544. package/dist/remote/inspect.d.ts +103 -0
  545. package/dist/remote/inspect.js +152 -0
  546. package/dist/remote/inspect.js.map +1 -0
  547. package/dist/remote/labels.d.ts +75 -0
  548. package/dist/remote/labels.js +137 -0
  549. package/dist/remote/labels.js.map +1 -0
  550. package/dist/remote/ladder.d.ts +145 -0
  551. package/dist/remote/ladder.js +387 -0
  552. package/dist/remote/ladder.js.map +1 -0
  553. package/dist/remote/ledger.d.ts +97 -0
  554. package/dist/remote/ledger.js +148 -0
  555. package/dist/remote/ledger.js.map +1 -0
  556. package/dist/remote/lifecycle.d.ts +175 -0
  557. package/dist/remote/lifecycle.js +274 -0
  558. package/dist/remote/lifecycle.js.map +1 -0
  559. package/dist/remote/links.d.ts +411 -0
  560. package/dist/remote/links.js +748 -0
  561. package/dist/remote/links.js.map +1 -0
  562. package/dist/remote/managed-block.d.ts +182 -0
  563. package/dist/remote/managed-block.js +406 -0
  564. package/dist/remote/managed-block.js.map +1 -0
  565. package/dist/remote/managed-comment.d.ts +104 -0
  566. package/dist/remote/managed-comment.js +92 -0
  567. package/dist/remote/managed-comment.js.map +1 -0
  568. package/dist/remote/mapping.d.ts +282 -0
  569. package/dist/remote/mapping.js +336 -0
  570. package/dist/remote/mapping.js.map +1 -0
  571. package/dist/remote/merge.d.ts +172 -0
  572. package/dist/remote/merge.js +221 -0
  573. package/dist/remote/merge.js.map +1 -0
  574. package/dist/remote/periods.d.ts +209 -0
  575. package/dist/remote/periods.js +224 -0
  576. package/dist/remote/periods.js.map +1 -0
  577. package/dist/remote/plan.d.ts +671 -0
  578. package/dist/remote/plan.js +1210 -0
  579. package/dist/remote/plan.js.map +1 -0
  580. package/dist/remote/policy.d.ts +67 -0
  581. package/dist/remote/policy.js +67 -0
  582. package/dist/remote/policy.js.map +1 -0
  583. package/dist/remote/preflight.d.ts +112 -0
  584. package/dist/remote/preflight.js +342 -0
  585. package/dist/remote/preflight.js.map +1 -0
  586. package/dist/remote/prerequisites.d.ts +113 -0
  587. package/dist/remote/prerequisites.js +161 -0
  588. package/dist/remote/prerequisites.js.map +1 -0
  589. package/dist/remote/provider.d.ts +802 -0
  590. package/dist/remote/provider.js +27 -0
  591. package/dist/remote/provider.js.map +1 -0
  592. package/dist/remote/providers/github/config.d.ts +84 -0
  593. package/dist/remote/providers/github/config.js +161 -0
  594. package/dist/remote/providers/github/config.js.map +1 -0
  595. package/dist/remote/providers/github/connector.d.ts +33 -0
  596. package/dist/remote/providers/github/connector.js +774 -0
  597. package/dist/remote/providers/github/connector.js.map +1 -0
  598. package/dist/remote/providers/github/edges.d.ts +29 -0
  599. package/dist/remote/providers/github/edges.js +56 -0
  600. package/dist/remote/providers/github/edges.js.map +1 -0
  601. package/dist/remote/providers/github/hierarchy.d.ts +21 -0
  602. package/dist/remote/providers/github/hierarchy.js +46 -0
  603. package/dist/remote/providers/github/hierarchy.js.map +1 -0
  604. package/dist/remote/providers/github/index.d.ts +26 -0
  605. package/dist/remote/providers/github/index.js +69 -0
  606. package/dist/remote/providers/github/index.js.map +1 -0
  607. package/dist/remote/providers/github/project-iteration.d.ts +71 -0
  608. package/dist/remote/providers/github/project-iteration.js +203 -0
  609. package/dist/remote/providers/github/project-iteration.js.map +1 -0
  610. package/dist/remote/providers/github/project-provision.d.ts +130 -0
  611. package/dist/remote/providers/github/project-provision.js +247 -0
  612. package/dist/remote/providers/github/project-provision.js.map +1 -0
  613. package/dist/remote/providers/github/project-status.d.ts +115 -0
  614. package/dist/remote/providers/github/project-status.js +317 -0
  615. package/dist/remote/providers/github/project-status.js.map +1 -0
  616. package/dist/remote/providers/github/projects.d.ts +181 -0
  617. package/dist/remote/providers/github/projects.js +481 -0
  618. package/dist/remote/providers/github/projects.js.map +1 -0
  619. package/dist/remote/providers/github/translator.d.ts +103 -0
  620. package/dist/remote/providers/github/translator.js +400 -0
  621. package/dist/remote/providers/github/translator.js.map +1 -0
  622. package/dist/remote/providers/jira/config.d.ts +96 -0
  623. package/dist/remote/providers/jira/config.js +200 -0
  624. package/dist/remote/providers/jira/config.js.map +1 -0
  625. package/dist/remote/providers/jira/connector.d.ts +42 -0
  626. package/dist/remote/providers/jira/connector.js +1688 -0
  627. package/dist/remote/providers/jira/connector.js.map +1 -0
  628. package/dist/remote/providers/jira/fields.d.ts +292 -0
  629. package/dist/remote/providers/jira/fields.js +463 -0
  630. package/dist/remote/providers/jira/fields.js.map +1 -0
  631. package/dist/remote/providers/jira/hierarchy.d.ts +28 -0
  632. package/dist/remote/providers/jira/hierarchy.js +60 -0
  633. package/dist/remote/providers/jira/hierarchy.js.map +1 -0
  634. package/dist/remote/providers/jira/index.d.ts +29 -0
  635. package/dist/remote/providers/jira/index.js +100 -0
  636. package/dist/remote/providers/jira/index.js.map +1 -0
  637. package/dist/remote/providers/jira/links.d.ts +41 -0
  638. package/dist/remote/providers/jira/links.js +102 -0
  639. package/dist/remote/providers/jira/links.js.map +1 -0
  640. package/dist/remote/providers/jira/sprints.d.ts +80 -0
  641. package/dist/remote/providers/jira/sprints.js +107 -0
  642. package/dist/remote/providers/jira/sprints.js.map +1 -0
  643. package/dist/remote/providers/jira/translator.d.ts +61 -0
  644. package/dist/remote/providers/jira/translator.js +382 -0
  645. package/dist/remote/providers/jira/translator.js.map +1 -0
  646. package/dist/remote/providers/jira/types.d.ts +112 -0
  647. package/dist/remote/providers/jira/types.js +160 -0
  648. package/dist/remote/providers/jira/types.js.map +1 -0
  649. package/dist/remote/providers/jira/vocabulary.d.ts +55 -0
  650. package/dist/remote/providers/jira/vocabulary.js +91 -0
  651. package/dist/remote/providers/jira/vocabulary.js.map +1 -0
  652. package/dist/remote/providers/jsonfile/config.d.ts +61 -0
  653. package/dist/remote/providers/jsonfile/config.js +83 -0
  654. package/dist/remote/providers/jsonfile/config.js.map +1 -0
  655. package/dist/remote/providers/jsonfile/connector.d.ts +34 -0
  656. package/dist/remote/providers/jsonfile/connector.js +251 -0
  657. package/dist/remote/providers/jsonfile/connector.js.map +1 -0
  658. package/dist/remote/providers/jsonfile/index.d.ts +38 -0
  659. package/dist/remote/providers/jsonfile/index.js +90 -0
  660. package/dist/remote/providers/jsonfile/index.js.map +1 -0
  661. package/dist/remote/providers/jsonfile/store.d.ts +59 -0
  662. package/dist/remote/providers/jsonfile/store.js +48 -0
  663. package/dist/remote/providers/jsonfile/store.js.map +1 -0
  664. package/dist/remote/providers/jsonfile/translator.d.ts +54 -0
  665. package/dist/remote/providers/jsonfile/translator.js +199 -0
  666. package/dist/remote/providers/jsonfile/translator.js.map +1 -0
  667. package/dist/remote/providers/linear/config.d.ts +83 -0
  668. package/dist/remote/providers/linear/config.js +140 -0
  669. package/dist/remote/providers/linear/config.js.map +1 -0
  670. package/dist/remote/providers/linear/connector.d.ts +36 -0
  671. package/dist/remote/providers/linear/connector.js +438 -0
  672. package/dist/remote/providers/linear/connector.js.map +1 -0
  673. package/dist/remote/providers/linear/estimate.d.ts +63 -0
  674. package/dist/remote/providers/linear/estimate.js +112 -0
  675. package/dist/remote/providers/linear/estimate.js.map +1 -0
  676. package/dist/remote/providers/linear/index.d.ts +31 -0
  677. package/dist/remote/providers/linear/index.js +75 -0
  678. package/dist/remote/providers/linear/index.js.map +1 -0
  679. package/dist/remote/providers/linear/labels.d.ts +51 -0
  680. package/dist/remote/providers/linear/labels.js +129 -0
  681. package/dist/remote/providers/linear/labels.js.map +1 -0
  682. package/dist/remote/providers/linear/states.d.ts +97 -0
  683. package/dist/remote/providers/linear/states.js +124 -0
  684. package/dist/remote/providers/linear/states.js.map +1 -0
  685. package/dist/remote/providers/linear/translator.d.ts +48 -0
  686. package/dist/remote/providers/linear/translator.js +343 -0
  687. package/dist/remote/providers/linear/translator.js.map +1 -0
  688. package/dist/remote/providers/linear/vocabulary.d.ts +27 -0
  689. package/dist/remote/providers/linear/vocabulary.js +42 -0
  690. package/dist/remote/providers/linear/vocabulary.js.map +1 -0
  691. package/dist/remote/provision.d.ts +45 -0
  692. package/dist/remote/provision.js +81 -0
  693. package/dist/remote/provision.js.map +1 -0
  694. package/dist/remote/pull.d.ts +52 -0
  695. package/dist/remote/pull.js +116 -0
  696. package/dist/remote/pull.js.map +1 -0
  697. package/dist/remote/readiness.d.ts +67 -0
  698. package/dist/remote/readiness.js +336 -0
  699. package/dist/remote/readiness.js.map +1 -0
  700. package/dist/remote/rebase.d.ts +157 -0
  701. package/dist/remote/rebase.js +255 -0
  702. package/dist/remote/rebase.js.map +1 -0
  703. package/dist/remote/reconcile.d.ts +115 -0
  704. package/dist/remote/reconcile.js +140 -0
  705. package/dist/remote/reconcile.js.map +1 -0
  706. package/dist/remote/redact.d.ts +70 -0
  707. package/dist/remote/redact.js +132 -0
  708. package/dist/remote/redact.js.map +1 -0
  709. package/dist/remote/registry.d.ts +32 -0
  710. package/dist/remote/registry.js +54 -0
  711. package/dist/remote/registry.js.map +1 -0
  712. package/dist/remote/remotes.d.ts +94 -0
  713. package/dist/remote/remotes.js +182 -0
  714. package/dist/remote/remotes.js.map +1 -0
  715. package/dist/remote/render.d.ts +147 -0
  716. package/dist/remote/render.js +683 -0
  717. package/dist/remote/render.js.map +1 -0
  718. package/dist/remote/report.d.ts +63 -0
  719. package/dist/remote/report.js +345 -0
  720. package/dist/remote/report.js.map +1 -0
  721. package/dist/remote/resolutions.d.ts +115 -0
  722. package/dist/remote/resolutions.js +217 -0
  723. package/dist/remote/resolutions.js.map +1 -0
  724. package/dist/remote/scaffold.d.ts +134 -0
  725. package/dist/remote/scaffold.js +370 -0
  726. package/dist/remote/scaffold.js.map +1 -0
  727. package/dist/remote/scope.d.ts +57 -0
  728. package/dist/remote/scope.js +91 -0
  729. package/dist/remote/scope.js.map +1 -0
  730. package/dist/remote/selection.d.ts +38 -0
  731. package/dist/remote/selection.js +50 -0
  732. package/dist/remote/selection.js.map +1 -0
  733. package/dist/remote/shape.d.ts +141 -0
  734. package/dist/remote/shape.js +220 -0
  735. package/dist/remote/shape.js.map +1 -0
  736. package/dist/remote/status.d.ts +104 -0
  737. package/dist/remote/status.js +238 -0
  738. package/dist/remote/status.js.map +1 -0
  739. package/dist/remote/sync.d.ts +182 -0
  740. package/dist/remote/sync.js +523 -0
  741. package/dist/remote/sync.js.map +1 -0
  742. package/dist/remote/transport/budget.d.ts +48 -0
  743. package/dist/remote/transport/budget.js +68 -0
  744. package/dist/remote/transport/budget.js.map +1 -0
  745. package/dist/remote/transport/connector.d.ts +115 -0
  746. package/dist/remote/transport/connector.js +21 -0
  747. package/dist/remote/transport/connector.js.map +1 -0
  748. package/dist/remote/transport/error.d.ts +88 -0
  749. package/dist/remote/transport/error.js +196 -0
  750. package/dist/remote/transport/error.js.map +1 -0
  751. package/dist/remote/transport/gh.d.ts +20 -0
  752. package/dist/remote/transport/gh.js +41 -0
  753. package/dist/remote/transport/gh.js.map +1 -0
  754. package/dist/remote/transport/graphql.d.ts +25 -0
  755. package/dist/remote/transport/graphql.js +81 -0
  756. package/dist/remote/transport/graphql.js.map +1 -0
  757. package/dist/remote/transport/http.d.ts +47 -0
  758. package/dist/remote/transport/http.js +195 -0
  759. package/dist/remote/transport/http.js.map +1 -0
  760. package/dist/remote/transport/index.d.ts +18 -0
  761. package/dist/remote/transport/index.js +19 -0
  762. package/dist/remote/transport/index.js.map +1 -0
  763. package/dist/remote/transport/process.d.ts +39 -0
  764. package/dist/remote/transport/process.js +121 -0
  765. package/dist/remote/transport/process.js.map +1 -0
  766. package/dist/remote/transport/rest.d.ts +22 -0
  767. package/dist/remote/transport/rest.js +57 -0
  768. package/dist/remote/transport/rest.js.map +1 -0
  769. package/dist/remote/transport/retry.d.ts +91 -0
  770. package/dist/remote/transport/retry.js +138 -0
  771. package/dist/remote/transport/retry.js.map +1 -0
  772. package/dist/remote/transport-error.d.ts +19 -0
  773. package/dist/remote/transport-error.js +22 -0
  774. package/dist/remote/transport-error.js.map +1 -0
  775. package/dist/remote/vocabulary.d.ts +126 -0
  776. package/dist/remote/vocabulary.js +57 -0
  777. package/dist/remote/vocabulary.js.map +1 -0
  778. package/dist/runner/config.d.ts +7 -0
  779. package/dist/runner/config.js +61 -0
  780. package/dist/runner/config.js.map +1 -0
  781. package/dist/runner/git.d.ts +19 -0
  782. package/dist/runner/git.js +26 -0
  783. package/dist/runner/git.js.map +1 -0
  784. package/dist/runner/index.d.ts +20 -0
  785. package/dist/runner/index.js +21 -0
  786. package/dist/runner/index.js.map +1 -0
  787. package/dist/runner/loop.d.ts +46 -0
  788. package/dist/runner/loop.js +243 -0
  789. package/dist/runner/loop.js.map +1 -0
  790. package/dist/runner/pi.d.ts +39 -0
  791. package/dist/runner/pi.js +276 -0
  792. package/dist/runner/pi.js.map +1 -0
  793. package/dist/runner/shell.d.ts +44 -0
  794. package/dist/runner/shell.js +174 -0
  795. package/dist/runner/shell.js.map +1 -0
  796. package/dist/runner/stats.d.ts +19 -0
  797. package/dist/runner/stats.js +58 -0
  798. package/dist/runner/stats.js.map +1 -0
  799. package/dist/runner/types.d.ts +218 -0
  800. package/dist/runner/types.js +37 -0
  801. package/dist/runner/types.js.map +1 -0
  802. package/dist/server/git.d.ts +8 -0
  803. package/dist/server/git.js +50 -0
  804. package/dist/server/git.js.map +1 -0
  805. package/dist/server/http/respond.d.ts +13 -0
  806. package/dist/server/http/respond.js +59 -0
  807. package/dist/server/http/respond.js.map +1 -0
  808. package/dist/server/http/router.d.ts +27 -0
  809. package/dist/server/http/router.js +48 -0
  810. package/dist/server/http/router.js.map +1 -0
  811. package/dist/server/http/static.d.ts +14 -0
  812. package/dist/server/http/static.js +55 -0
  813. package/dist/server/http/static.js.map +1 -0
  814. package/dist/server/index.d.ts +58 -0
  815. package/dist/server/index.js +124 -0
  816. package/dist/server/index.js.map +1 -0
  817. package/dist/server/routes/board.d.ts +8 -0
  818. package/dist/server/routes/board.js +19 -0
  819. package/dist/server/routes/board.js.map +1 -0
  820. package/dist/server/routes/comments.d.ts +13 -0
  821. package/dist/server/routes/comments.js +36 -0
  822. package/dist/server/routes/comments.js.map +1 -0
  823. package/dist/server/routes/flags.d.ts +16 -0
  824. package/dist/server/routes/flags.js +37 -0
  825. package/dist/server/routes/flags.js.map +1 -0
  826. package/dist/server/routes/git.d.ts +13 -0
  827. package/dist/server/routes/git.js +84 -0
  828. package/dist/server/routes/git.js.map +1 -0
  829. package/dist/server/routes/remotes.d.ts +36 -0
  830. package/dist/server/routes/remotes.js +749 -0
  831. package/dist/server/routes/remotes.js.map +1 -0
  832. package/dist/server/routes/views.d.ts +10 -0
  833. package/dist/server/routes/views.js +63 -0
  834. package/dist/server/routes/views.js.map +1 -0
  835. package/dist/server/views/schema.d.ts +2 -0
  836. package/dist/server/views/schema.js +121 -0
  837. package/dist/server/views/schema.js.map +1 -0
  838. package/dist/server/views/store.d.ts +17 -0
  839. package/dist/server/views/store.js +67 -0
  840. package/dist/server/views/store.js.map +1 -0
  841. package/dist/shared/adf.d.ts +83 -0
  842. package/dist/shared/adf.js +795 -0
  843. package/dist/shared/adf.js.map +1 -0
  844. package/dist/shared/blocking.d.ts +130 -0
  845. package/dist/shared/blocking.js +179 -0
  846. package/dist/shared/blocking.js.map +1 -0
  847. package/dist/shared/changes.d.ts +100 -0
  848. package/dist/shared/changes.js +112 -0
  849. package/dist/shared/changes.js.map +1 -0
  850. package/dist/shared/cohesion.d.ts +75 -0
  851. package/dist/shared/cohesion.js +132 -0
  852. package/dist/shared/cohesion.js.map +1 -0
  853. package/dist/shared/dependency-rollup.d.ts +91 -0
  854. package/dist/shared/dependency-rollup.js +132 -0
  855. package/dist/shared/dependency-rollup.js.map +1 -0
  856. package/dist/shared/errors.d.ts +23 -0
  857. package/dist/shared/errors.js +24 -0
  858. package/dist/shared/errors.js.map +1 -0
  859. package/dist/shared/flag-rollup.d.ts +88 -0
  860. package/dist/shared/flag-rollup.js +119 -0
  861. package/dist/shared/flag-rollup.js.map +1 -0
  862. package/dist/shared/git-sync.d.ts +90 -0
  863. package/dist/shared/git-sync.js +9 -0
  864. package/dist/shared/git-sync.js.map +1 -0
  865. package/dist/shared/index.d.ts +40 -0
  866. package/dist/shared/index.js +41 -0
  867. package/dist/shared/index.js.map +1 -0
  868. package/dist/shared/model.d.ts +218 -0
  869. package/dist/shared/model.js +95 -0
  870. package/dist/shared/model.js.map +1 -0
  871. package/dist/shared/period-query.d.ts +21 -0
  872. package/dist/shared/period-query.js +26 -0
  873. package/dist/shared/period-query.js.map +1 -0
  874. package/dist/shared/period-stance.d.ts +45 -0
  875. package/dist/shared/period-stance.js +50 -0
  876. package/dist/shared/period-stance.js.map +1 -0
  877. package/dist/shared/plans/breakdown.d.ts +66 -0
  878. package/dist/shared/plans/breakdown.js +186 -0
  879. package/dist/shared/plans/breakdown.js.map +1 -0
  880. package/dist/shared/plans/index.d.ts +21 -0
  881. package/dist/shared/plans/index.js +22 -0
  882. package/dist/shared/plans/index.js.map +1 -0
  883. package/dist/shared/plans/instantiate.d.ts +61 -0
  884. package/dist/shared/plans/instantiate.js +184 -0
  885. package/dist/shared/plans/instantiate.js.map +1 -0
  886. package/dist/shared/plans/reading.d.ts +79 -0
  887. package/dist/shared/plans/reading.js +149 -0
  888. package/dist/shared/plans/reading.js.map +1 -0
  889. package/dist/shared/plans/reparent.d.ts +54 -0
  890. package/dist/shared/plans/reparent.js +217 -0
  891. package/dist/shared/plans/reparent.js.map +1 -0
  892. package/dist/shared/plans/timeline.d.ts +93 -0
  893. package/dist/shared/plans/timeline.js +224 -0
  894. package/dist/shared/plans/timeline.js.map +1 -0
  895. package/dist/shared/plans/upstream.d.ts +67 -0
  896. package/dist/shared/plans/upstream.js +73 -0
  897. package/dist/shared/plans/upstream.js.map +1 -0
  898. package/dist/shared/remote-api.d.ts +413 -0
  899. package/dist/shared/remote-api.js +11 -0
  900. package/dist/shared/remote-api.js.map +1 -0
  901. package/dist/shared/remote-coverage.d.ts +120 -0
  902. package/dist/shared/remote-coverage.js +98 -0
  903. package/dist/shared/remote-coverage.js.map +1 -0
  904. package/dist/shared/remote-readiness.d.ts +162 -0
  905. package/dist/shared/remote-readiness.js +49 -0
  906. package/dist/shared/remote-readiness.js.map +1 -0
  907. package/dist/shared/remote-status.d.ts +268 -0
  908. package/dist/shared/remote-status.js +79 -0
  909. package/dist/shared/remote-status.js.map +1 -0
  910. package/dist/shared/rollup.d.ts +90 -0
  911. package/dist/shared/rollup.js +119 -0
  912. package/dist/shared/rollup.js.map +1 -0
  913. package/dist/shared/static.d.ts +65 -0
  914. package/dist/shared/static.js +107 -0
  915. package/dist/shared/static.js.map +1 -0
  916. package/dist/shared/template-params.d.ts +80 -0
  917. package/dist/shared/template-params.js +141 -0
  918. package/dist/shared/template-params.js.map +1 -0
  919. package/dist/shared/view.d.ts +105 -0
  920. package/dist/shared/view.js +44 -0
  921. package/dist/shared/view.js.map +1 -0
  922. package/dist/shared/work-unit.d.ts +34 -0
  923. package/dist/shared/work-unit.js +47 -0
  924. package/dist/shared/work-unit.js.map +1 -0
  925. package/dist/sync/apply.d.ts +21 -0
  926. package/dist/sync/apply.js +149 -0
  927. package/dist/sync/apply.js.map +1 -0
  928. package/dist/sync/dto.d.ts +9 -0
  929. package/dist/sync/dto.js +162 -0
  930. package/dist/sync/dto.js.map +1 -0
  931. package/dist/sync/index.d.ts +24 -0
  932. package/dist/sync/index.js +25 -0
  933. package/dist/sync/index.js.map +1 -0
  934. package/dist/sync/patch.d.ts +13 -0
  935. package/dist/sync/patch.js +212 -0
  936. package/dist/sync/patch.js.map +1 -0
  937. package/dist/sync/session.d.ts +31 -0
  938. package/dist/sync/session.js +55 -0
  939. package/dist/sync/session.js.map +1 -0
  940. package/dist/sync/static.d.ts +11 -0
  941. package/dist/sync/static.js +33 -0
  942. package/dist/sync/static.js.map +1 -0
  943. package/package.json +95 -4
  944. package/templates/blank.yml +71 -0
  945. package/templates/context/bug.md +107 -0
  946. package/templates/context/default.md +119 -0
  947. package/templates/context/epic.md +81 -0
  948. package/templates/context/feature.md +91 -0
  949. package/templates/context/research.md +87 -0
  950. package/templates/context/review.md +81 -0
  951. package/templates/context/story.md +94 -0
  952. package/templates/context/sub_task.md +108 -0
  953. package/templates/context/task.md +86 -0
  954. package/templates/context/test.md +73 -0
  955. package/templates/context/user_story.md +147 -0
  956. package/templates/kanban.yml +226 -0
  957. package/templates/scrum.yml +567 -0
  958. package/web/dist/assets/index-D0j8OnHq.js +35 -0
  959. package/web/dist/assets/index-DFL7v8aP.css +1 -0
  960. package/web/dist/index.html +13 -0
  961. package/web/dist-viewer/assets/index-Bcns6OuC.css +1 -0
  962. package/web/dist-viewer/assets/index-vVXzeiFo.js +7 -0
  963. package/web/dist-viewer/index.html +14 -0
@@ -0,0 +1,1688 @@
1
+ /**
2
+ * The Jira Cloud connector — a live connection to one Cloud site, driven by the
3
+ * executor (LP-490).
4
+ *
5
+ * Built from a validated `connection` block (`site`, `project`, `board`,
6
+ * `email`, `token`, `tls_verify`). The HTTP and Basic Auth header construction
7
+ * are owned by the wrapped client — `jira.js`, an optional *peer* dependency
8
+ * (never installed with light-plan, because Jira sync is experimental) loaded through a computed specifier so the engine still
9
+ * compiles and runs on a machine that never installed it (exactly the pattern
10
+ * `runner/pi.ts` uses for the pi SDK). The client lives entirely inside this
11
+ * file, behind the provider `Connector` contract (LP-289), so the retry/budget
12
+ * decorators and the conformance suite see an ordinary connector and nothing
13
+ * here leaks into the planners or core.
14
+ *
15
+ * TLS verification is **on by default and stays on**. A corporate MITM proxy
16
+ * with a custom root CA is handled outside this file — `NODE_EXTRA_CA_CERTS`
17
+ * or Node's `--use-system-ca` inject the root into the OS trust store that
18
+ * Node's built-in `fetch` already verifies against. `tls_verify: false` is the
19
+ * explicit, loud fallback: it warns on every build and is never the default.
20
+ *
21
+ * Three failure modes produce three different messages (LP-294's shapes):
22
+ * a site that cannot be reached (DNS/TLS/non-Jira), an account email the site
23
+ * rejects, and an API token the site rejects. The email-vs-token distinction
24
+ * is a documented heuristic on Atlassian's 401 body — both are a 401, and the
25
+ * message names the most likely fix for each.
26
+ *
27
+ * Server/Data Center is out of scope, consistent with the epic's Cloud-first
28
+ * decision: a URL that answers but has no Cloud v3 API is reported as "not
29
+ * supported" rather than as a confusing 401 or 404.
30
+ */
31
+ import { BoardError } from '../../../core/errors.js';
32
+ import { markdownToAdf } from '../../../shared/adf.js';
33
+ import { normalizeAccountMapping } from '../../accounts.js';
34
+ import { normalizePeriodMapping } from '../../periods.js';
35
+ import { parseFields } from './fields.js';
36
+ import { isClosedSprint, parseSprints, sprintByName, sprintFieldIdOf, } from './sprints.js';
37
+ import { hierarchyDepthOf, isSubtaskType } from './types.js';
38
+ /** The non-literal specifier, so `tsc` does not require jira.js at build time. */
39
+ const JIRA_PACKAGE = 'jira.js';
40
+ /** Read a Jira issue's `fields.updated` (the revision marker), or null. */
41
+ function updatedOf(record) {
42
+ const fields = record['fields'];
43
+ if (fields && typeof fields === 'object') {
44
+ const updated = fields['updated'];
45
+ if (typeof updated === 'string')
46
+ return updated;
47
+ }
48
+ return null;
49
+ }
50
+ /** The messages a 401 body may carry, as plain strings. */
51
+ /**
52
+ * A board period's dates are **inclusive calendar days** — "Sprint 1 runs from
53
+ * the 31st through the 31st" is a one-day sprint, and a board may well have
54
+ * one. A Jira sprint is a pair of *instants* and it requires the start to be
55
+ * strictly before the end, so sending the bare dates makes an inclusive
56
+ * one-day sprint into `start === end`, which Jira refuses outright
57
+ * (`startDate: The start date of a sprint must be before the end date`).
58
+ *
59
+ * So the start is the beginning of its day and the end is the *end* of its
60
+ * day. That is not a workaround for the refusal: it is what the board already
61
+ * meant. Mapping an inclusive end date onto midnight would silently shorten
62
+ * every sprint by a day, which is worse than the error.
63
+ *
64
+ * A value that already carries a time is passed through untouched — a caller
65
+ * that has been explicit is not second-guessed.
66
+ */
67
+ function sprintStart(date) {
68
+ return /^\d{4}-\d{2}-\d{2}$/.test(date.trim()) ? `${date.trim()}T00:00:00.000Z` : date;
69
+ }
70
+ function sprintEnd(date) {
71
+ return /^\d{4}-\d{2}-\d{2}$/.test(date.trim()) ? `${date.trim()}T23:59:59.000Z` : date;
72
+ }
73
+ /** A Jira issue key — `PAY-100`, `SCRUM-17` — as opposed to a numeric id. */
74
+ const ISSUE_KEY_RE = /^[A-Za-z][A-Za-z0-9_]*-\d+$/;
75
+ /**
76
+ * How to name a parent to Jira: `{ id }` for the numeric id, `{ key }` for the
77
+ * human-facing key.
78
+ *
79
+ * Chosen by the shape of the value, because both reach this code. The executor
80
+ * resolves a parent through the link store, which records Jira's **id**
81
+ * (`10016`) — and sending that under `key` is rejected with "Please select
82
+ * valid parent issue", a message that reads like a hierarchy problem and is
83
+ * not one. A caller that adopted a twin by key (`lpm remote link`) has the key.
84
+ * Neither should have to know which field the other uses.
85
+ */
86
+ function parentRef(parent) {
87
+ return ISSUE_KEY_RE.test(parent) ? { key: parent } : { id: parent };
88
+ }
89
+ function errorMessagesOf(body) {
90
+ if (body === null || typeof body !== 'object')
91
+ return [];
92
+ const record = body;
93
+ const out = [];
94
+ const messages = record['errorMessages'];
95
+ if (Array.isArray(messages)) {
96
+ out.push(...messages.filter((entry) => typeof entry === 'string'));
97
+ }
98
+ // The field-keyed map — `{ errors: { originBoardId: "The board does not
99
+ // support sprints" } }`. Jira uses it wherever the complaint is about one
100
+ // field, which is most of the Agile API, and it sits beside an *empty*
101
+ // `errorMessages`. Reading only the array threw the explanation away: a
102
+ // sprint that could not be created reported no reason at all, under a hint
103
+ // about issue types that had nothing to do with it. Jira's own words are the
104
+ // most useful thing in the error, so they are never dropped.
105
+ const errors = record['errors'];
106
+ if (errors !== null && typeof errors === 'object' && !Array.isArray(errors)) {
107
+ for (const [field, message] of Object.entries(errors)) {
108
+ if (typeof message === 'string' && message !== '')
109
+ out.push(`${field}: ${message}`);
110
+ }
111
+ }
112
+ if (out.length === 0) {
113
+ const message = record['message'];
114
+ if (typeof message === 'string')
115
+ out.push(message);
116
+ }
117
+ return out;
118
+ }
119
+ /**
120
+ * True when the 401 body suggests the *email* is the problem rather than the
121
+ * token: Atlassian's "Basic authentication with passwords is deprecated"
122
+ * message (and its relatives) fire when the credentials are read as a password
123
+ * pair, which is what a wrong account email looks like. A plain "invalid
124
+ * credentials" blames the token. This is a documented heuristic — a 401 cannot
125
+ * name the failing half, and the message names the most likely fix.
126
+ */
127
+ function bodyBlamesEmail(body) {
128
+ return errorMessagesOf(body).some((message) => /password|username|email/i.test(message));
129
+ }
130
+ /**
131
+ * True when Jira refused a write because the issue is a sub-task and the sprint
132
+ * is not its to hold.
133
+ *
134
+ * **Jira owns a sub-task's sprint**: it is always the parent's, it cannot be
135
+ * set, and it cannot be cleared either. That is not a permission or a bad
136
+ * value, it is a field that does not belong to this issue type — the same kind
137
+ * of rule as "an Epic cannot sit under an Epic", discovered the same way, by
138
+ * being refused.
139
+ *
140
+ * It matters in the *clearing* direction most, which is the one nobody
141
+ * predicts: light-plan sees a sprint on a twin whose board document has no
142
+ * period, plans an update to take it off, and Jira refuses. Nothing about that
143
+ * resolves by itself, so every push failed on the same thirteen sub-tasks for
144
+ * ever.
145
+ */
146
+ function isSubtaskSprintRefusal(error) {
147
+ const err = error;
148
+ if (err?.status !== 400)
149
+ return false;
150
+ return errorMessagesOf(err.body).some((message) => /subtask.*cannot be associated to a sprint|cannot be associated to a sprint/i.test(message));
151
+ }
152
+ /**
153
+ * True when Jira refused a write because it was given an issue type for a
154
+ * sub-task without the parent that must accompany it.
155
+ *
156
+ * The executor hands the translator the **whole document** on an update, not
157
+ * just the diff, because a label-carrying provider derives its type and state
158
+ * from it — so every Jira edit restates `issuetype` even when nothing about
159
+ * the type changed. Jira tolerates that on a standard issue and refuses it on
160
+ * a sub-task, where an `issuetype` in the body must be accompanied by a
161
+ * `parent`. The result was a permanent failure on every sub-task with any
162
+ * other field to write.
163
+ */
164
+ function isSubtaskTypeRefusal(error) {
165
+ const err = error;
166
+ if (err?.status !== 400)
167
+ return false;
168
+ return errorMessagesOf(err.body).some((message) => /sub-?task but parent issue key or id not specified/i.test(message));
169
+ }
170
+ /**
171
+ * Map a thrown jira.js error to a `BoardError` whose message and hints name the
172
+ * fix — the one place the three failure modes (site, email, token) become three
173
+ * different messages.
174
+ */
175
+ function describeFailure(error, conn, purpose) {
176
+ const err = error;
177
+ // No HTTP status but a `code` / `transient`: the request never produced a
178
+ // response — DNS, TLS, a reset socket. That is the "wrong site" case.
179
+ if (err.status === undefined && err.code !== undefined) {
180
+ const tls = /CERT|TLS|SELF_SIGNED|UNABLE_TO_VERIFY/i.test(err.code);
181
+ const hints = [
182
+ `check connection.site — it must be your Cloud site (https://<org>.atlassian.net or a Cloud custom domain)`,
183
+ ];
184
+ if (tls) {
185
+ hints.push('a corporate proxy is intercepting TLS — inject its root CA with NODE_EXTRA_CA_CERTS=<file> or node --use-system-ca');
186
+ hints.push('do not set tls_verify: false except as a last resort');
187
+ }
188
+ return new BoardError(`cannot reach the Jira site at ${conn.site} while ${purpose}`, hints);
189
+ }
190
+ if (err.status === 401) {
191
+ if (bodyBlamesEmail(err.body)) {
192
+ return new BoardError(`the Jira account email was rejected at ${conn.site}`, [
193
+ `check connection.email matches the account that owns the API token`,
194
+ 'the value must be an API token, not the account password',
195
+ `resolve it with \${JIRA_EMAIL} or: lpm remote login`,
196
+ ]);
197
+ }
198
+ return new BoardError(`the Jira API token was rejected at ${conn.site}`, [
199
+ 'revoke and regenerate it at https://id.atlassian.com/manage-profile/security/api-tokens',
200
+ 'store the new value with: lpm remote login <name> (or ${JIRA_API_TOKEN})',
201
+ ]);
202
+ }
203
+ if (err.status === 403) {
204
+ return new BoardError(`Jira refused the request (permission denied) while ${purpose}`, [
205
+ 'the credential is valid but lacks the required project permission',
206
+ 'check the account has Browse/Create/Edit issues on the project',
207
+ ]);
208
+ }
209
+ if (err.status === 400) {
210
+ const messages = errorMessagesOf(err.body);
211
+ const detail = messages.length > 0 ? `: ${messages.join('; ')}` : '';
212
+ // The hints follow the *purpose*. A 400 while creating a sprint has
213
+ // nothing to do with issue types or parents, and saying so sent somebody
214
+ // looking at the wrong half of their config for an afternoon.
215
+ if (/sprint/i.test(purpose)) {
216
+ // When Jira explained itself, its words are the diagnosis: a guess
217
+ // printed beside them is how somebody ends up debugging the wrong thing.
218
+ // (A sprint name over 30 characters and a board that cannot hold created
219
+ // sprints are both real, and they are not the same problem.) The
220
+ // candidates are offered only when it said nothing at all.
221
+ return new BoardError(`Jira refused the request while ${purpose}${detail}`, messages.length > 0
222
+ ? ['that is Jira’s own message; the sprint was not created and the push carried on without it']
223
+ : [
224
+ 'sprints can only be *created* on a company-managed Scrum board — a team-managed ("next-gen") project reports its board as type `simple`',
225
+ 'check connection.board names a Scrum board and that the account has Manage Sprints on it',
226
+ ]);
227
+ }
228
+ return new BoardError(`Jira refused the request while ${purpose}${detail}`, [
229
+ 'a type that cannot sit under its parent, or a field Jira rejects, is refused rather than coerced',
230
+ 'check the issue type and its parent against the project\u2019s actual type scheme',
231
+ ]);
232
+ }
233
+ if (err.status === 404) {
234
+ const notCloud = !conn.site.includes('.atlassian.net');
235
+ return new BoardError(`Jira answered 404 while ${purpose}`, [
236
+ ...(notCloud
237
+ ? [
238
+ `the site ${conn.site} is not a *.atlassian.net Cloud site — if it is a Jira Server/Data Center instance, it is not supported (Cloud only)`,
239
+ ]
240
+ : ['the resource may have been deleted, or the account cannot see it']),
241
+ ]);
242
+ }
243
+ if (err.status === 429) {
244
+ return new BoardError(`Jira rate-limited the request while ${purpose}`, [
245
+ 'slow the sync down and retry — the remote said so',
246
+ ]);
247
+ }
248
+ if (err.status !== undefined && err.status >= 500) {
249
+ return new BoardError(`Jira failed (HTTP ${err.status}) while ${purpose}`, [
250
+ 'retrying may succeed',
251
+ ]);
252
+ }
253
+ // A `BoardError` is already the legible, structured message (a workflow
254
+ // refusal, a missing transition field) — pass it through, never rewrap it.
255
+ if (error instanceof BoardError)
256
+ return error;
257
+ return new BoardError(`Jira rejected the request while ${purpose}`, [
258
+ err.message ?? String(error),
259
+ ]);
260
+ }
261
+ /** A Jira issue record → `ConnectorResult`, reading the post-write state. */
262
+ /** The workflow status name on a Jira issue record, or `''` when it carries none. */
263
+ function statusNameOf(record) {
264
+ const fields = (record['fields'] ?? {});
265
+ const status = (fields['status'] ?? {});
266
+ return typeof status.name === 'string' ? status.name : '';
267
+ }
268
+ function resultOf(record, site) {
269
+ const id = typeof record['id'] === 'string' ? record['id'] : String(record['id'] ?? '');
270
+ const key = typeof record['key'] === 'string' ? record['key'] : '';
271
+ const rev = updatedOf(record) ?? '';
272
+ // The link records the *browse* URL a person opens, not the REST self link:
273
+ // `https://<org>.atlassian.net/browse/PAY-418`.
274
+ const browseUrl = key ? `${site}/browse/${key}` : '';
275
+ return {
276
+ remoteId: id,
277
+ remoteKey: key || id,
278
+ remoteUrl: browseUrl,
279
+ remoteRev: rev,
280
+ record,
281
+ };
282
+ }
283
+ /** A Jira comment response → `ConnectorResult` (comment id rides along). */
284
+ function commentResult(comment, issueRemoteId, issueKey) {
285
+ const id = typeof comment['id'] === 'string' ? comment['id'] : String(comment['id'] ?? '');
286
+ return {
287
+ remoteId: issueRemoteId,
288
+ remoteKey: issueKey,
289
+ remoteUrl: '',
290
+ remoteRev: '',
291
+ commentId: id,
292
+ };
293
+ }
294
+ /** The `issuelinks` entries a Jira record carries, or an empty list. */
295
+ function issueLinksOf(record) {
296
+ const fields = record['fields'];
297
+ if (!fields || typeof fields !== 'object')
298
+ return [];
299
+ const links = fields['issuelinks'];
300
+ if (!Array.isArray(links))
301
+ return [];
302
+ return links.filter((link) => link !== null && typeof link === 'object');
303
+ }
304
+ /** The name of an issue link's type (`Blocks`, `Relates`), or ''. */
305
+ function linkTypeNameOf(link) {
306
+ const type = link['type'];
307
+ if (!type || typeof type !== 'object')
308
+ return '';
309
+ const name = type['name'];
310
+ return typeof name === 'string' ? name : '';
311
+ }
312
+ /** The remote id of a link endpoint (`outwardIssue` / `inwardIssue`), or ''. */
313
+ function endpointIdOf(endpoint) {
314
+ if (!endpoint || typeof endpoint !== 'object')
315
+ return '';
316
+ const id = endpoint['id'];
317
+ if (typeof id === 'string')
318
+ return id;
319
+ if (typeof id === 'number')
320
+ return String(id);
321
+ return '';
322
+ }
323
+ /** Case-insensitive, whitespace-tolerant status name equality. */
324
+ function nameMatches(a, b) {
325
+ return typeof a === 'string' && a.trim().toLowerCase() === b.trim().toLowerCase();
326
+ }
327
+ /** The transition reaching `target` in one hop, when the list has one. */
328
+ function singleHopTo(list, target) {
329
+ return list.find((transition) => nameMatches(transition.to?.name, target));
330
+ }
331
+ /** The statuses one transition away, for the "reachable" report. */
332
+ function singleHopReachable(list) {
333
+ const names = list
334
+ .map((transition) => transition.to?.name)
335
+ .filter((name) => typeof name === 'string' && name.length > 0);
336
+ return [...new Set(names)].sort();
337
+ }
338
+ /** The fields a transition's screen marks required, with their display names. */
339
+ function requiredFieldsOf(transition) {
340
+ const fields = transition.fields ?? {};
341
+ const required = [];
342
+ for (const [key, meta] of Object.entries(fields)) {
343
+ if (meta && typeof meta === 'object' && meta['required'] === true) {
344
+ const name = meta['name'];
345
+ required.push({ key, name: typeof name === 'string' ? name : undefined });
346
+ }
347
+ }
348
+ return required;
349
+ }
350
+ /**
351
+ * The `fields` body for a transition, answered from `transition_fields` in
352
+ * config. Returns the fields to send plus the required fields config did not
353
+ * answer — those are the ones a transition must refuse on rather than guess.
354
+ */
355
+ function fieldsFor(transition, configured) {
356
+ const missing = [];
357
+ const fields = {};
358
+ for (const field of requiredFieldsOf(transition)) {
359
+ if (Object.prototype.hasOwnProperty.call(configured, field.key)) {
360
+ fields[field.key] = configured[field.key];
361
+ }
362
+ else {
363
+ missing.push(field);
364
+ }
365
+ }
366
+ return { fields: Object.keys(fields).length > 0 ? fields : undefined, missing };
367
+ }
368
+ /** Build the workflow's status graph from a `readWorkflows` response. */
369
+ function workflowGraphOf(response) {
370
+ const nameOfRef = new Map();
371
+ for (const status of response.statuses ?? []) {
372
+ if (typeof status.statusReference === 'string' && typeof status.name === 'string') {
373
+ nameOfRef.set(status.statusReference, status.name);
374
+ }
375
+ }
376
+ const adjacency = new Map();
377
+ const display = new Map();
378
+ const addEdge = (from, to) => {
379
+ const fromKey = from.trim().toLowerCase();
380
+ const toKey = to.trim().toLowerCase();
381
+ if (fromKey === '' || toKey === '' || fromKey === toKey)
382
+ return;
383
+ display.set(fromKey, from);
384
+ display.set(toKey, to);
385
+ let set = adjacency.get(fromKey);
386
+ if (!set) {
387
+ set = new Set();
388
+ adjacency.set(fromKey, set);
389
+ }
390
+ set.add(toKey);
391
+ };
392
+ for (const workflow of response.workflows ?? []) {
393
+ for (const transition of workflow.transitions ?? []) {
394
+ const toName = typeof transition.toStatusReference === 'string'
395
+ ? nameOfRef.get(transition.toStatusReference)
396
+ : undefined;
397
+ if (!toName)
398
+ continue;
399
+ for (const link of transition.links ?? []) {
400
+ const fromName = typeof link.fromStatusReference === 'string'
401
+ ? nameOfRef.get(link.fromStatusReference)
402
+ : undefined;
403
+ if (fromName)
404
+ addEdge(fromName, toName);
405
+ }
406
+ }
407
+ }
408
+ return { adjacency, display };
409
+ }
410
+ /**
411
+ * A breadth-first path of status *names* from `fromName` to `toName` — the
412
+ * intermediate statuses plus the target, never the starting status. `null`
413
+ * when the workflow offers no path.
414
+ */
415
+ function findPath(graph, fromName, toName) {
416
+ const start = fromName.trim().toLowerCase();
417
+ const goal = toName.trim().toLowerCase();
418
+ if (start === goal)
419
+ return [toName];
420
+ const seen = new Set([start]);
421
+ const queue = [{ key: start, path: [] }];
422
+ while (queue.length > 0) {
423
+ const { key, path } = queue.shift();
424
+ for (const next of graph.adjacency.get(key) ?? []) {
425
+ if (seen.has(next))
426
+ continue;
427
+ seen.add(next);
428
+ const nextPath = [...path, next];
429
+ if (next === goal)
430
+ return nextPath.map((k) => graph.display.get(k) ?? k);
431
+ queue.push({ key: next, path: nextPath });
432
+ }
433
+ }
434
+ return null;
435
+ }
436
+ /** Every status reachable from `fromName` by any number of transitions. */
437
+ function reachableNames(graph, fromName) {
438
+ const start = fromName.trim().toLowerCase();
439
+ const seen = new Set();
440
+ const stack = [start];
441
+ while (stack.length > 0) {
442
+ const key = stack.pop();
443
+ if (seen.has(key))
444
+ continue;
445
+ seen.add(key);
446
+ for (const next of graph.adjacency.get(key) ?? []) {
447
+ if (!seen.has(next))
448
+ stack.push(next);
449
+ }
450
+ }
451
+ seen.delete(start);
452
+ return [...seen].map((key) => graph.display.get(key) ?? key).sort();
453
+ }
454
+ function isPermissionDenied(error) {
455
+ return error?.status === 403;
456
+ }
457
+ /** A transition screen's required field cannot be answered from config. */
458
+ function missingFieldsError(remoteId, target, missing) {
459
+ const list = missing
460
+ .map((field) => (field.name ? `${field.key} (${field.name})` : field.key))
461
+ .join(', ');
462
+ return new BoardError(`Cannot move ${remoteId} to "${target}": the transition requires ${missing.length === 1 ? 'field' : 'fields'} ${list}`, [
463
+ ...missing.map((field) => `set mapping.transition_fields.${field.key} to the value the workflow expects`),
464
+ 'a transition screen field must come from config — it is never guessed',
465
+ ]);
466
+ }
467
+ /** The target is reachable only through several transitions, and that is off. */
468
+ function multiHopRefusedError(remoteId, target, pathNames) {
469
+ const hops = Math.max(0, pathNames.length - 1);
470
+ return new BoardError(`Moving ${remoteId} to "${target}" needs ${hops} transition${hops === 1 ? '' : 's'}`, [
471
+ `path: ${pathNames.join(' → ')}`,
472
+ "multi-hop transitions fire automations, notify people and stamp resolutions — the decision is the operator's, not the sync's",
473
+ 'set mapping.transitions.multi_hop: true to execute this path automatically',
474
+ ]);
475
+ }
476
+ /** The target has no path from where the issue stands. */
477
+ function unreachableError(remoteId, target, reachable) {
478
+ return new BoardError(`Cannot move ${remoteId} to "${target}"`, [
479
+ ...(reachable.length > 0
480
+ ? [`reachable statuses: ${[...new Set(reachable)].join(', ')}`]
481
+ : ["no statuses are reachable from the issue's current status"]),
482
+ 'a Jira status change is a workflow transition, and the target may be unreachable from where the issue stands',
483
+ ]);
484
+ }
485
+ /** The credential can transition but not read the workflow to find a path. */
486
+ function workflowPermissionError(remoteId, target, reachable) {
487
+ return new BoardError(`Cannot move ${remoteId} to "${target}": the credential cannot read the workflow to compute a path`, [
488
+ ...(reachable.length > 0
489
+ ? [`reachable in one hop: ${reachable.join(', ')}`]
490
+ : ["no transitions are reachable from the issue's current status"]),
491
+ 'reading the workflow graph needs "View workflow" or "Administer projects" project permission',
492
+ 'grant the token that permission, or move the issue one status at a time',
493
+ ]);
494
+ }
495
+ /**
496
+ * Build a Jira connector from a validated connection block (and mapping, whose
497
+ * `transitions` / `transition_fields` shape how a status change is applied).
498
+ *
499
+ * `site` and `project` are required (the schema enforces them); `email` and
500
+ * `token` are the *resolved* secrets (the resolver filled them from the
501
+ * credential chain before this is called); `tls_verify` defaults to on.
502
+ */
503
+ export function jiraConnector(connection, mapping = {}) {
504
+ const raw = connection;
505
+ const mappingConfig = (mapping ?? {});
506
+ const multiHop = mappingConfig.transitions?.multi_hop === true;
507
+ const transitionFields = mappingConfig.transition_fields ?? {};
508
+ // Which resource attribute carries the Jira account (LP-329). `email` is the
509
+ // one value that needs a search: Jira addresses an assignee by account id,
510
+ // so an email must be resolved through the user search before it can be
511
+ // written. Any other attribute (a `jira_account_id` attribute) already holds
512
+ // the account id and is written directly, with no search.
513
+ const accountsVia = normalizeAccountMapping(mapping['accounts'])?.via;
514
+ const conn = {
515
+ site: (raw.site ?? '').trim().replace(/\/+$/, ''),
516
+ project: raw.project,
517
+ board: raw.board,
518
+ email: typeof raw.email === 'string' ? raw.email : '',
519
+ token: typeof raw.token === 'string' ? raw.token : '',
520
+ tlsVerify: raw.tls_verify !== false,
521
+ };
522
+ // TLS: verification stays on unless the operator explicitly disabled it, and
523
+ // the disable is loud — every build of the connector, i.e. every run.
524
+ if (!conn.tlsVerify) {
525
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
526
+ process.emitWarning(`Jira connection to ${conn.site}: TLS verification is DISABLED (connection.tls_verify: false). ` +
527
+ 'Traffic is not protected against interception. Prefer NODE_EXTRA_CA_CERTS=<file> or node --use-system-ca to trust your proxy root CA instead.');
528
+ }
529
+ // The client is loaded lazily, on first use: a board that declares a Jira
530
+ // remote but never syncs it pays nothing, and a missing jira.js surfaces as
531
+ // the install-command message only when something actually talks to Jira.
532
+ // The Cloud (v3) and Agile (software) surfaces share one build, so both are
533
+ // memoized together behind one import (LP-328 needs the Agile sprint API).
534
+ let clientsPromise;
535
+ function clients() {
536
+ clientsPromise ??= buildClients();
537
+ return clientsPromise;
538
+ }
539
+ async function client() {
540
+ return (await clients()).cloud;
541
+ }
542
+ async function agile() {
543
+ return (await clients()).agile;
544
+ }
545
+ async function buildClients() {
546
+ let mod;
547
+ try {
548
+ mod = await import(JIRA_PACKAGE);
549
+ }
550
+ catch (error) {
551
+ throw new BoardError('The Jira client (jira.js) is not available', [
552
+ 'Jira sync is experimental, so its client is not installed with light-plan.',
553
+ 'Install it beside light-plan: npm install -g jira.js (a global install),',
554
+ 'npm install jira.js (a project), or npx -p light-plan -p jira.js lpm … (npx).',
555
+ 'It requires Node 22 or newer.',
556
+ error.message,
557
+ ]);
558
+ }
559
+ const { createCloudClient, createAgileClient } = mod;
560
+ const config = {
561
+ host: conn.site,
562
+ auth: { type: 'basic', email: conn.email, apiToken: conn.token },
563
+ };
564
+ return { cloud: createCloudClient(config), agile: createAgileClient(config) };
565
+ }
566
+ /**
567
+ * Every status name this project's issue types can be in, fetched once and
568
+ * cached. Jira answers per issue type; the union is what the project has.
569
+ */
570
+ let projectStatusesPromise;
571
+ function projectStatuses() {
572
+ projectStatusesPromise ??= fetchProjectStatuses();
573
+ return projectStatusesPromise;
574
+ }
575
+ async function fetchProjectStatuses() {
576
+ const c = await client();
577
+ let raw;
578
+ try {
579
+ raw = await c.projects.getAllStatuses({ projectIdOrKey: conn.project });
580
+ }
581
+ catch (error) {
582
+ throw describeFailure(error, conn, `reading the statuses of ${conn.project}`);
583
+ }
584
+ if (!Array.isArray(raw))
585
+ return [];
586
+ const names = new Set();
587
+ for (const entry of raw) {
588
+ if (entry === null || typeof entry !== 'object')
589
+ continue;
590
+ const statuses = entry['statuses'];
591
+ if (!Array.isArray(statuses))
592
+ continue;
593
+ for (const status of statuses) {
594
+ if (status === null || typeof status !== 'object')
595
+ continue;
596
+ const name = status['name'];
597
+ if (typeof name === 'string' && name !== '')
598
+ names.add(name);
599
+ }
600
+ }
601
+ return [...names];
602
+ }
603
+ /**
604
+ * The project's issue types, fetched once per connector and cached — the
605
+ * *actual* scheme the type mapping is validated against (LP-324). `probe`
606
+ * and `create` share the one fetch; a board that never probes and never
607
+ * creates a sub-task pays nothing.
608
+ */
609
+ let typeSchemePromise;
610
+ function typeScheme() {
611
+ typeSchemePromise ??= fetchTypeScheme();
612
+ return typeSchemePromise;
613
+ }
614
+ async function fetchTypeScheme() {
615
+ const c = await client();
616
+ try {
617
+ const project = await c.projects.getProject({
618
+ projectIdOrKey: conn.project,
619
+ expand: 'issueTypes',
620
+ });
621
+ const raw = (project['issueTypes'] ?? []);
622
+ if (!Array.isArray(raw))
623
+ return [];
624
+ const types = [];
625
+ for (const entry of raw) {
626
+ if (entry === null || typeof entry !== 'object')
627
+ continue;
628
+ const record = entry;
629
+ const name = typeof record['name'] === 'string' ? record['name'] : '';
630
+ if (name === '')
631
+ continue;
632
+ types.push({
633
+ name,
634
+ ...(typeof record['subtask'] === 'boolean' ? { subtask: record['subtask'] } : {}),
635
+ ...(typeof record['hierarchyLevel'] === 'number'
636
+ ? { hierarchyLevel: record['hierarchyLevel'] }
637
+ : {}),
638
+ });
639
+ }
640
+ return types;
641
+ }
642
+ catch (error) {
643
+ throw describeFailure(error, conn, `reading the issue types of ${conn.project}`);
644
+ }
645
+ }
646
+ /**
647
+ * True when `typeName` names a sub-task type. Best-effort: when the scheme
648
+ * cannot be read, this returns false and Jira's own refusal is surfaced — a
649
+ * top-level sub-task is refused by Jira whether or not we saw it coming.
650
+ */
651
+ async function isSubtask(typeName) {
652
+ try {
653
+ const scheme = await typeScheme();
654
+ return scheme.some((type) => type.name === typeName && isSubtaskType(type));
655
+ }
656
+ catch {
657
+ return false;
658
+ }
659
+ }
660
+ /**
661
+ * The instance's custom fields, fetched once per connector and cached — the
662
+ * list `mapping.attributes` names resolve against (LP-327). System fields
663
+ * are dropped: the mapping carries only custom fields, so the cache stays
664
+ * small and `validateFieldMapping`'s "it has:" list names only the fields a
665
+ * mapping may point at.
666
+ */
667
+ let fieldsPromise;
668
+ function fields() {
669
+ fieldsPromise ??= fetchFields();
670
+ return fieldsPromise;
671
+ }
672
+ async function fetchFields() {
673
+ const c = await client();
674
+ try {
675
+ const raw = await c.issueFields.getFields();
676
+ return parseFields(raw).filter((field) => field.custom);
677
+ }
678
+ catch (error) {
679
+ throw describeFailure(error, conn, 'listing custom fields');
680
+ }
681
+ }
682
+ // -- sprints (LP-328) ------------------------------------------------------
683
+ // Jira's only native period container is a sprint, so a period mapping on
684
+ // this connector *is* sprint mapping — the schema already stamps
685
+ // `carrier: 'sprint'`, and reading `normalizePeriodMapping` here keeps the
686
+ // connector correct even when a test builds it with the bare
687
+ // `{ container }` spelling.
688
+ const usesSprints = normalizePeriodMapping(mapping['periods']) !== undefined;
689
+ /** The sprint custom field's per-instance id, discovered once and cached. */
690
+ let sprintFieldIdPromise;
691
+ function sprintFieldId() {
692
+ sprintFieldIdPromise ??= fetchSprintFieldId();
693
+ return sprintFieldIdPromise;
694
+ }
695
+ async function fetchSprintFieldId() {
696
+ try {
697
+ return sprintFieldIdOf(await fields());
698
+ }
699
+ catch (error) {
700
+ throw describeFailure(error, conn, 'finding the sprint field');
701
+ }
702
+ }
703
+ /** The board's sprints (every state), fetched once per connector and cached. */
704
+ let sprintsPromise;
705
+ function sprints() {
706
+ sprintsPromise ??= fetchSprints();
707
+ return sprintsPromise;
708
+ }
709
+ async function fetchSprints() {
710
+ if (conn.board === undefined)
711
+ return [];
712
+ const a = await agile();
713
+ try {
714
+ const out = [];
715
+ let startAt = 0;
716
+ for (;;) {
717
+ const page = await a.board.getAllSprints({
718
+ boardId: Number(conn.board),
719
+ startAt,
720
+ maxResults: 50,
721
+ });
722
+ out.push(...parseSprints(page));
723
+ const values = Array.isArray(page.values) ? page.values : [];
724
+ if (page.isLast === true || values.length === 0)
725
+ break;
726
+ startAt += values.length;
727
+ }
728
+ return out;
729
+ }
730
+ catch (error) {
731
+ throw describeFailure(error, conn, `listing sprints on board ${conn.board}`);
732
+ }
733
+ }
734
+ /**
735
+ * Which Agile board this project's sprints live on, asked of Jira rather than
736
+ * of the person.
737
+ *
738
+ * `connection.board` is the one connection value a reader cannot reasonably
739
+ * be expected to know: it is not the project key, it does not appear in the
740
+ * project settings, and the usual way to find it is to open a board and read
741
+ * a number out of the URL. The credential that can file an issue can also
742
+ * list the project's boards, so setup asks Jira and writes the answer down.
743
+ *
744
+ * Returns nothing at all when the board id is already set, or when the
745
+ * mapping carries no periods — there is nothing to discover in either case.
746
+ * A failure is swallowed into an empty list: discovery is an offer, and the
747
+ * push preflight is what actually holds the line.
748
+ */
749
+ async function discoverConnection() {
750
+ if (conn.board !== undefined && conn.board !== '')
751
+ return [];
752
+ if (mapping['periods'] === undefined)
753
+ return [];
754
+ try {
755
+ const a = await agile();
756
+ const page = await a.board.getAllBoards({ projectKeyOrId: conn.project, maxResults: 50 });
757
+ const values = Array.isArray(page.values) ? page.values : [];
758
+ return values.flatMap((entry) => {
759
+ const id = entry['id'];
760
+ if (typeof id !== 'number' && typeof id !== 'string')
761
+ return [];
762
+ const name = typeof entry['name'] === 'string' ? entry['name'] : `board ${String(id)}`;
763
+ const kind = typeof entry['type'] === 'string' ? ` (${entry['type']})` : '';
764
+ return [{ key: 'board', value: String(id), label: `${name}${kind}` }];
765
+ });
766
+ }
767
+ catch {
768
+ return [];
769
+ }
770
+ }
771
+ /**
772
+ * The sprint field value a create/update body should carry: `{ id }` to move
773
+ * the issue into the sprint, `{ null }` to clear it, or `{}` when the
774
+ * request carries no period.
775
+ *
776
+ * **A sprint that does not exist yet is not an error.** The board's timeline
777
+ * and the board's work are pushed independently — file the stories today,
778
+ * file the sprints when you are ready — so an issue scheduled into a sprint
779
+ * Jira has not got is filed *unscheduled*, and `unwritten` says so. The
780
+ * executor records the period as unset, and the push that files the sprint
781
+ * writes the assignment. Refusing here would make the whole timeline a
782
+ * precondition of filing a single story.
783
+ *
784
+ * A **closed** sprint is different and still refuses: the work cannot go
785
+ * there at all, and pretending otherwise would lose the scheduling silently.
786
+ * Unless the issue is *already* in it, which is a no-op rather than a move.
787
+ */
788
+ async function sprintFieldFor(request, remoteId) {
789
+ if (!usesSprints)
790
+ return { fields: {} };
791
+ if (request.period === undefined)
792
+ return { fields: {} };
793
+ const fieldId = await sprintFieldId();
794
+ if (request.period === null) {
795
+ // Clearing a sprint: without a field there is nothing to clear, and the
796
+ // issue is already out of every sprint.
797
+ return { fields: fieldId === undefined ? {} : { [fieldId]: null } };
798
+ }
799
+ // No board and no sprint field are both "this instance cannot hold the
800
+ // schedule", which is the same situation as a sprint that does not exist:
801
+ // file the work, say what was dropped, and let a later push carry it.
802
+ if (conn.board === undefined || fieldId === undefined) {
803
+ return { fields: {}, unwritten: 'period' };
804
+ }
805
+ const sprint = sprintByName(await sprints(), request.period.name);
806
+ if (sprint === undefined) {
807
+ return { fields: {}, unwritten: 'period' };
808
+ }
809
+ if (isClosedSprint(sprint)) {
810
+ // Already sitting in this closed sprint? Then nothing moves, and a title
811
+ // or body edit must not be refused for it. Only a genuine move into a
812
+ // closed sprint is refused.
813
+ if (remoteId !== undefined && (await currentSprintNameOf(remoteId, fieldId)) === sprint.name) {
814
+ return { fields: {} };
815
+ }
816
+ throw new BoardError(`Cannot move work into sprint "${sprint.name}" — it is closed`, [
817
+ 'Closed sprints are read-only; move the issue into an active or future sprint, or reopen the sprint in Jira.',
818
+ ]);
819
+ }
820
+ const numeric = Number(sprint.id);
821
+ return { fields: { [fieldId]: Number.isNaN(numeric) ? sprint.id : numeric } };
822
+ }
823
+ /** Read a record's sprint field value into a `{ name, state, starts, ends }` observation. */
824
+ function sprintObservationOf(record, fieldId) {
825
+ const fields = record['fields'];
826
+ if (!fields || typeof fields !== 'object')
827
+ return undefined;
828
+ const value = fields[fieldId];
829
+ if (value === null || value === undefined || typeof value !== 'object')
830
+ return undefined;
831
+ // Parallel sprints arrive as an array; take the first open one, else the
832
+ // first — the sync refuses ambiguity rather than inventing a winner.
833
+ const entries = Array.isArray(value) ? value : [value];
834
+ for (const entry of entries) {
835
+ if (entry === null || typeof entry !== 'object')
836
+ continue;
837
+ const sprint = entry;
838
+ if (typeof sprint['name'] !== 'string')
839
+ continue;
840
+ const observation = { name: sprint['name'] };
841
+ if (typeof sprint['state'] === 'string')
842
+ observation.state = sprint['state'];
843
+ if (typeof sprint['startDate'] === 'string')
844
+ observation.starts = sprint['startDate'];
845
+ if (typeof sprint['endDate'] === 'string')
846
+ observation.ends = sprint['endDate'];
847
+ return observation;
848
+ }
849
+ return undefined;
850
+ }
851
+ /** The name of the sprint an issue currently sits in, or `undefined`. */
852
+ async function currentSprintNameOf(remoteId, fieldId) {
853
+ const c = await client();
854
+ try {
855
+ const record = await c.issues.getIssue({ issueIdOrKey: remoteId });
856
+ return sprintObservationOf(record, fieldId)?.['name'];
857
+ }
858
+ catch {
859
+ return undefined;
860
+ }
861
+ }
862
+ /**
863
+ * Add `sprint` (name + dates) to a record so the pull resolves the period.
864
+ * Best-effort: no sprint field, no board, or no sprint on the issue simply
865
+ * stays unenriched — the carrier has nothing to recover, which is the honest
866
+ * answer rather than a failure.
867
+ */
868
+ async function enrichSprint(record) {
869
+ if (record === null)
870
+ return null;
871
+ if (!usesSprints || conn.board === undefined)
872
+ return record;
873
+ try {
874
+ const fieldId = await sprintFieldId();
875
+ if (fieldId === undefined)
876
+ return record;
877
+ const observation = sprintObservationOf(record, fieldId);
878
+ if (observation === undefined)
879
+ return record;
880
+ return { ...record, sprint: observation };
881
+ }
882
+ catch {
883
+ // Enrichment never fails a read: a missing board or a permission loss on
884
+ // the field list is not a missing issue.
885
+ return record;
886
+ }
887
+ }
888
+ /**
889
+ * The create-screen field ids, keyed by issue type name, fetched once per
890
+ * connector (LP-327). This is the metadata that answers "is this field
891
+ * writable on a create of this type" before any write; the create screen
892
+ * stands in for the edit screen too, since no issue exists to query
893
+ * `editmeta` against and Jira's default screen scheme serves one screen for
894
+ * both operations.
895
+ */
896
+ let createMetaPromise;
897
+ function createMeta() {
898
+ createMetaPromise ??= fetchCreateMeta();
899
+ return createMetaPromise;
900
+ }
901
+ async function fetchCreateMeta() {
902
+ const c = await client();
903
+ try {
904
+ const page = await c.issues.getCreateIssueMetaIssueTypes({ projectIdOrKey: conn.project });
905
+ const rawTypes = (page['issueTypes'] ?? []);
906
+ const map = new Map();
907
+ if (!Array.isArray(rawTypes))
908
+ return map;
909
+ for (const entry of rawTypes) {
910
+ if (entry === null || typeof entry !== 'object')
911
+ continue;
912
+ const record = entry;
913
+ const id = record['id'];
914
+ const name = record['name'];
915
+ if (typeof id !== 'string' || typeof name !== 'string')
916
+ continue;
917
+ const fieldsPage = await c.issues.getCreateIssueMetaIssueTypeId({
918
+ projectIdOrKey: conn.project,
919
+ issueTypeId: id,
920
+ });
921
+ const rawFields = (fieldsPage['fields'] ?? []);
922
+ const set = new Set();
923
+ if (Array.isArray(rawFields)) {
924
+ for (const rawField of rawFields) {
925
+ if (rawField === null || typeof rawField !== 'object')
926
+ continue;
927
+ const fieldRecord = rawField;
928
+ const fieldId = fieldRecord['fieldId'];
929
+ const key = fieldRecord['key'];
930
+ if (typeof fieldId === 'string')
931
+ set.add(fieldId);
932
+ if (typeof key === 'string')
933
+ set.add(key);
934
+ }
935
+ }
936
+ map.set(name, set);
937
+ }
938
+ return map;
939
+ }
940
+ catch (error) {
941
+ throw describeFailure(error, conn, 'reading the create-screen metadata');
942
+ }
943
+ }
944
+ /** Create a custom field (needs the Administer Jira global permission). */
945
+ async function createField(name, typeKey, signal) {
946
+ const c = await client();
947
+ try {
948
+ const created = await c.issueFields.createCustomField({ name, type: typeKey });
949
+ void signal;
950
+ const id = typeof created['id'] === 'string' ? created['id'] : '';
951
+ if (id === '') {
952
+ throw new BoardError(`Jira created the field "${name}" but returned no id`, [
953
+ 'The field exists but its id could not be read; re-run discovery to pick it up.',
954
+ ]);
955
+ }
956
+ return { id };
957
+ }
958
+ catch (error) {
959
+ throw describeFailure(error, conn, `creating the custom field "${name}"`);
960
+ }
961
+ }
962
+ /** Add a field to the default screen (needs the Administer Jira permission). */
963
+ async function addFieldToScreen(fieldId, signal) {
964
+ const c = await client();
965
+ try {
966
+ await c.screens.addFieldToDefaultScreen({ fieldId });
967
+ void signal;
968
+ }
969
+ catch (error) {
970
+ throw describeFailure(error, conn, `adding ${fieldId} to the screen`);
971
+ }
972
+ }
973
+ /** A Jira issue read back as its own record shape, or null when gone/unseen. */
974
+ async function get(remoteId) {
975
+ const c = await client();
976
+ try {
977
+ const issue = await c.issues.getIssue({ issueIdOrKey: remoteId });
978
+ return await enrichSprint(issue);
979
+ }
980
+ catch (error) {
981
+ const err = error;
982
+ if (err.status === 404)
983
+ return null;
984
+ throw describeFailure(error, conn, `fetching issue ${remoteId}`);
985
+ }
986
+ }
987
+ /** The fields of a request, in the shape `createIssue` / `editIssue` expect. */
988
+ function fieldsOf(request) {
989
+ const fields = {
990
+ project: { key: conn.project },
991
+ };
992
+ if (request.type !== undefined)
993
+ fields['issuetype'] = { name: request.type };
994
+ if (request.title !== undefined)
995
+ fields['summary'] = request.title;
996
+ // Jira Cloud's description is ADF, and this is the wire — the last place
997
+ // the body is anybody's but Jira's. A string arrives from the translator
998
+ // (prose) and from the executor (prose plus the managed block, composed in
999
+ // markdown); an ADF document arrives from a caller that has already done
1000
+ // the conversion, and is passed through.
1001
+ if (request.body !== undefined) {
1002
+ fields['description'] =
1003
+ typeof request.body === 'string' ? markdownToAdf(request.body) : request.body;
1004
+ }
1005
+ if (request.labels !== undefined)
1006
+ fields['labels'] = request.labels;
1007
+ if (request.parent !== undefined && request.parent !== '') {
1008
+ fields['parent'] = parentRef(request.parent);
1009
+ }
1010
+ return fields;
1011
+ }
1012
+ /**
1013
+ * The `assignee` field value a create/update should carry (LP-329). Jira
1014
+ * addresses an assignee by account id, never by email or name, so:
1015
+ *
1016
+ * - `request.assignee === null` (or an empty string, defensively) → the
1017
+ * issue goes unassigned;
1018
+ * - a non-empty value under `accounts.via: email` → resolved to an account
1019
+ * id through Jira's user search;
1020
+ * - a non-empty value under any other `via` (a `jira_account_id`
1021
+ * attribute) → the account id itself, written directly with no search.
1022
+ *
1023
+ * `undefined` means the op does not carry an assignee, so the remote side is
1024
+ * left untouched.
1025
+ */
1026
+ async function assigneeFieldFor(request) {
1027
+ if (request.assignee === undefined)
1028
+ return {};
1029
+ if (request.assignee === null || request.assignee === '') {
1030
+ return { assignee: null };
1031
+ }
1032
+ return { assignee: { id: await resolveAccount(request.assignee) } };
1033
+ }
1034
+ /**
1035
+ * Turn a `mapping.accounts.via` value into the Jira account id it names.
1036
+ *
1037
+ * Only `via: email` searches: the value is an email address and Jira assigns
1038
+ * by account id, so the user search must find the id. An empty result is the
1039
+ * documented signature of a privacy-restricted (GDPR-mode) instance, where
1040
+ * email lookups are disabled — that is refused with the `jira_account_id`
1041
+ * alternative rather than reported as "user not found". Any other `via` is
1042
+ * already the account id and is returned as-is, no search made.
1043
+ */
1044
+ async function resolveAccount(value) {
1045
+ if (accountsVia !== 'email')
1046
+ return value;
1047
+ const c = await client();
1048
+ let users;
1049
+ try {
1050
+ users = (await c.userSearch.findUsers({ query: value }));
1051
+ }
1052
+ catch (error) {
1053
+ throw describeFailure(error, conn, `resolving the Jira account for "${value}"`);
1054
+ }
1055
+ const needle = value.trim().toLowerCase();
1056
+ for (const user of users) {
1057
+ const email = user['emailAddress'];
1058
+ if (typeof email === 'string' && email.trim().toLowerCase() === needle) {
1059
+ const id = user['accountId'];
1060
+ if (typeof id === 'string' && id !== '')
1061
+ return id;
1062
+ }
1063
+ }
1064
+ throw new BoardError(`Cannot resolve "${value}" to a Jira account by email search`, [
1065
+ 'the search returned no matching account, which is what a privacy-restricted (GDPR-mode) instance does for email lookups',
1066
+ 'set mapping.accounts.via: jira_account_id and put each person\u2019s Jira account id in that attribute',
1067
+ 'the connector then assigns by account id directly, with no search',
1068
+ ]);
1069
+ }
1070
+ /** Move the issue to `target` status, resolving transitions (single- or multi-hop). */
1071
+ async function transitionTo(c, remoteId, target, current) {
1072
+ // Already there is not a move. Jira workflows rarely define a transition
1073
+ // whose `to` is the status you are in, so asking for one falls through to
1074
+ // the multi-hop path and is refused — which would make every issue whose
1075
+ // mapped status *is* the workflow's first status fail on create.
1076
+ if (current !== undefined && current.trim().toLowerCase() === target.trim().toLowerCase()) {
1077
+ return;
1078
+ }
1079
+ const transitions = await c.issues.getTransitions({
1080
+ issueIdOrKey: remoteId,
1081
+ expand: 'transitions.fields',
1082
+ });
1083
+ const list = transitions.transitions ?? [];
1084
+ // One hop: the transition whose `to` status is the target.
1085
+ const single = singleHopTo(list, target);
1086
+ if (single) {
1087
+ const { fields, missing } = fieldsFor(single, transitionFields);
1088
+ if (missing.length > 0)
1089
+ throw missingFieldsError(remoteId, target, missing);
1090
+ await c.issues.doTransition({
1091
+ issueIdOrKey: remoteId,
1092
+ transition: { id: single.id },
1093
+ ...(fields ? { fields } : {}),
1094
+ });
1095
+ return;
1096
+ }
1097
+ // No single hop. A path is computed from the workflow, which needs a read
1098
+ // the transitioning permission does not imply — the two failures are
1099
+ // different, and the messages say so.
1100
+ const issue = await c.issues.getIssue({ issueIdOrKey: remoteId });
1101
+ const issueFields = (issue['fields'] ?? {});
1102
+ const status = (issueFields['status'] ?? {});
1103
+ const project = (issueFields['project'] ?? {});
1104
+ const issuetype = (issueFields['issuetype'] ?? {});
1105
+ const currentStatusName = typeof status.name === 'string' ? status.name : '';
1106
+ const projectId = project.id === undefined ? '' : String(project.id);
1107
+ const issueTypeId = issuetype.id === undefined ? '' : String(issuetype.id);
1108
+ if (projectId === '' || issueTypeId === '') {
1109
+ throw new BoardError(`Cannot compute a multi-hop path for ${remoteId} to "${target}"`, [
1110
+ 'the issue record did not carry a project id or issue type id',
1111
+ ]);
1112
+ }
1113
+ let graph;
1114
+ try {
1115
+ const response = await c.workflows.readWorkflows({
1116
+ projectAndIssueTypes: [{ projectId, issueTypeId }],
1117
+ });
1118
+ graph = workflowGraphOf(response);
1119
+ }
1120
+ catch (error) {
1121
+ if (isPermissionDenied(error)) {
1122
+ throw workflowPermissionError(remoteId, target, singleHopReachable(list));
1123
+ }
1124
+ throw error;
1125
+ }
1126
+ const path = findPath(graph, currentStatusName, target);
1127
+ if (path === null) {
1128
+ const reachable = reachableNames(graph, currentStatusName);
1129
+ throw unreachableError(remoteId, target, reachable.length > 0 ? reachable : singleHopReachable(list));
1130
+ }
1131
+ if (!multiHop) {
1132
+ throw multiHopRefusedError(remoteId, target, [currentStatusName, ...path]);
1133
+ }
1134
+ // Execute the path one status at a time. The first hop is reachable from
1135
+ // the transitions already fetched (`list`); each later hop re-reads the
1136
+ // transitions now available from the intermediate status the issue just
1137
+ // moved to — the graph says which statuses lead where, but transition ids
1138
+ // and required fields are per-status facts.
1139
+ let available = list;
1140
+ for (let i = 0; i < path.length; i++) {
1141
+ const next = path[i];
1142
+ const step = singleHopTo(available, next);
1143
+ if (!step) {
1144
+ throw new BoardError(`Cannot move ${remoteId} to "${target}"`, [
1145
+ `the workflow path says "${next}" follows, but no transition to it is available now`,
1146
+ 'the issue may have moved, or a condition on the transition no longer holds',
1147
+ ]);
1148
+ }
1149
+ const { fields, missing } = fieldsFor(step, transitionFields);
1150
+ if (missing.length > 0)
1151
+ throw missingFieldsError(remoteId, next, missing);
1152
+ await c.issues.doTransition({
1153
+ issueIdOrKey: remoteId,
1154
+ transition: { id: step.id },
1155
+ ...(fields ? { fields } : {}),
1156
+ });
1157
+ if (i + 1 < path.length) {
1158
+ const refreshed = await c.issues.getTransitions({
1159
+ issueIdOrKey: remoteId,
1160
+ expand: 'transitions.fields',
1161
+ });
1162
+ available = refreshed.transitions ?? [];
1163
+ }
1164
+ }
1165
+ }
1166
+ return {
1167
+ name: 'jira',
1168
+ async create(request, signal) {
1169
+ const c = await client();
1170
+ try {
1171
+ // A sub-task must have a parent — Jira refuses a top-level sub-task, and
1172
+ // this local refusal names the rule before the round-trip (LP-324).
1173
+ if ((request.parent === undefined || request.parent === '') &&
1174
+ typeof request.type === 'string' &&
1175
+ (await isSubtask(request.type))) {
1176
+ throw new BoardError(`Cannot file "${request.type}" without a parent`, [
1177
+ `${request.type} is a sub-task issue type — Jira requires a parent issue for it`,
1178
+ 'place the issue under its parent before pushing, or map it to a standard type',
1179
+ ]);
1180
+ }
1181
+ const sprintField = await sprintFieldFor(request);
1182
+ const created = await c.issues.createIssue({
1183
+ fields: { ...fieldsOf(request), ...(await assigneeFieldFor(request)), ...sprintField.fields },
1184
+ });
1185
+ // **A created issue is in the workflow's first status, never the one
1186
+ // the board asked for**, because Jira has no way to set a status on
1187
+ // `POST /issue` — status moves only through a transition. So a create
1188
+ // has to do what `update` does, or a board of finished work arrives as
1189
+ // a project of "To Do" and *stays* that way: the base is recomputed
1190
+ // from the echo, Jira's first status usually maps back to more than one
1191
+ // board status (`backlog` and `ready` both claim "To Do"), the
1192
+ // ambiguous read leaves the local value standing, and both sides then
1193
+ // agree on a status the remote does not hold. No later push disagrees,
1194
+ // because there is nothing left to disagree with.
1195
+ // The post-write record: the create response carries id/key/self only,
1196
+ // so read the issue back to record the base from what Jira stored.
1197
+ let record = await c.issues.getIssue({ issueIdOrKey: created.id });
1198
+ if (request.state !== undefined && request.state !== '') {
1199
+ const before = statusNameOf(record);
1200
+ if (before !== request.state) {
1201
+ await transitionTo(c, created.id, request.state, before);
1202
+ // Read again: the base must record the status the issue ended in.
1203
+ record = await c.issues.getIssue({ issueIdOrKey: created.id });
1204
+ }
1205
+ }
1206
+ void signal;
1207
+ return {
1208
+ ...resultOf(record, conn.site),
1209
+ ...(sprintField.unwritten !== undefined ? { unwritten: [sprintField.unwritten] } : {}),
1210
+ };
1211
+ }
1212
+ catch (error) {
1213
+ throw describeFailure(error, conn, 'creating an issue');
1214
+ }
1215
+ },
1216
+ async update(remoteId, request, signal) {
1217
+ const c = await client();
1218
+ try {
1219
+ const sprintField = await sprintFieldFor(request, remoteId);
1220
+ const fields = { ...fieldsOf(request), ...(await assigneeFieldFor(request)), ...sprintField.fields };
1221
+ delete fields['project'];
1222
+ // Whether the sprint was actually written — `unwritten` has to report
1223
+ // the sprint Jira would not take, and that is only known after trying.
1224
+ let unwritten = sprintField.unwritten;
1225
+ if (Object.keys(fields).length > 0) {
1226
+ // **Jira validates one field at a time**, so recovering from a
1227
+ // refusal is a loop rather than a retry. Two of its rules are about
1228
+ // a field that was never this issue's to carry, and both surface
1229
+ // only once the earlier one is gone: drop the sprint and the next
1230
+ // attempt complains about the issue type. A single retry fixed the
1231
+ // first and failed on the second, which is how thirteen sub-tasks
1232
+ // went on failing after the sprint half was fixed.
1233
+ //
1234
+ // Anything not recognised is rethrown untouched, and the loop can
1235
+ // only ever shrink the request, so it terminates.
1236
+ const sprintKeys = Object.keys(sprintField.fields);
1237
+ let attempt = { ...fields };
1238
+ for (;;) {
1239
+ try {
1240
+ await c.issues.editIssue({ issueIdOrKey: remoteId, fields: attempt });
1241
+ break;
1242
+ }
1243
+ catch (error) {
1244
+ const next = { ...attempt };
1245
+ // A sub-task's sprint is its parent's: it can be neither set nor
1246
+ // cleared, so the field is reported unwritten rather than retried
1247
+ // for ever.
1248
+ if (isSubtaskSprintRefusal(error) && sprintKeys.some((key) => key in next)) {
1249
+ for (const key of sprintKeys)
1250
+ delete next[key];
1251
+ unwritten = 'period';
1252
+ attempt = next;
1253
+ continue;
1254
+ }
1255
+ // An `issuetype` restated on a sub-task: the executor hands the
1256
+ // translator the whole document, so every edit carries the type
1257
+ // even when nothing about it changed, and Jira wants a `parent`
1258
+ // beside it. Dropping it is only safe when the type is not
1259
+ // actually changing, so that is **checked** — a real retype fails
1260
+ // loudly with the reason instead of reporting a success that did
1261
+ // not happen.
1262
+ if (isSubtaskTypeRefusal(error) && next['issuetype'] !== undefined) {
1263
+ const current = await c.issues.getIssue({
1264
+ issueIdOrKey: remoteId,
1265
+ fields: ['issuetype'],
1266
+ });
1267
+ const currentType = (current['fields'] ?? {})['issuetype'];
1268
+ const currentName = currentType && typeof currentType === 'object'
1269
+ ? currentType.name
1270
+ : undefined;
1271
+ if (currentName !== request.type) {
1272
+ throw new BoardError(`Cannot retype ${remoteId} to "${String(request.type)}"`, [
1273
+ 'Jira needs the parent issue in the same request when an issue type changes on a sub-task',
1274
+ 'move the document out of its parent first, or change the type in Jira',
1275
+ ]);
1276
+ }
1277
+ delete next['issuetype'];
1278
+ attempt = next;
1279
+ continue;
1280
+ }
1281
+ throw error;
1282
+ }
1283
+ }
1284
+ // Everything the refusals removed is gone; nothing left to send is a
1285
+ // finished edit, not a failure.
1286
+ }
1287
+ if (request.state !== undefined && request.state !== '') {
1288
+ await transitionTo(c, remoteId, request.state);
1289
+ }
1290
+ const record = await c.issues.getIssue({ issueIdOrKey: remoteId });
1291
+ void signal;
1292
+ return {
1293
+ ...resultOf(record, conn.site),
1294
+ ...(unwritten !== undefined ? { unwritten: [unwritten] } : {}),
1295
+ };
1296
+ }
1297
+ catch (error) {
1298
+ throw describeFailure(error, conn, `updating issue ${remoteId}`);
1299
+ }
1300
+ },
1301
+ async delete(remoteId, signal) {
1302
+ const c = await client();
1303
+ try {
1304
+ await c.issues.deleteIssue({ issueIdOrKey: remoteId });
1305
+ void signal;
1306
+ // Jira deletes hard; there is no post-delete record to record.
1307
+ return { remoteId, remoteKey: remoteId, remoteUrl: '', remoteRev: '' };
1308
+ }
1309
+ catch (error) {
1310
+ throw describeFailure(error, conn, `deleting issue ${remoteId}`);
1311
+ }
1312
+ },
1313
+ get,
1314
+ async resolve(key) {
1315
+ // `PAY-123` (or a bare numeric id) → the live issue. Jira keys survive
1316
+ // lookups by id too, but the key is what a person pastes.
1317
+ const record = await get(key);
1318
+ if (record === null)
1319
+ return null;
1320
+ return resultOf(record, conn.site);
1321
+ },
1322
+ describe(record) {
1323
+ const result = resultOf(record, conn.site);
1324
+ return { remoteKey: result.remoteKey, remoteUrl: result.remoteUrl };
1325
+ },
1326
+ async list(opts) {
1327
+ const c = await client();
1328
+ const jql = `project = ${conn.project}` +
1329
+ (opts?.cursor ? ` AND updated >= ${JSON.stringify(opts.cursor)}` : '') +
1330
+ ' ORDER BY updated ASC';
1331
+ try {
1332
+ // The sprint field rides the search explicitly when sprint mapping is
1333
+ // on and a board is configured: `*all` does not reliably include it,
1334
+ // exactly as with `issuelinks` (LP-325). Without a board there is
1335
+ // nothing to enrich, and the field is left out.
1336
+ let searchFields = ['*all', 'issuelinks'];
1337
+ if (usesSprints && conn.board !== undefined) {
1338
+ const fieldId = await sprintFieldId();
1339
+ if (fieldId !== undefined)
1340
+ searchFields = ['*all', 'issuelinks', fieldId];
1341
+ }
1342
+ const records = [];
1343
+ let pages = 0;
1344
+ let nextPageToken;
1345
+ do {
1346
+ const page = await c.issueSearch.searchAndReconsileIssuesUsingJqlPost({
1347
+ jql,
1348
+ maxResults: 50,
1349
+ // `*all` does not reliably include `issuelinks` on Cloud — the
1350
+ // field is named explicitly so the pull can read native links back
1351
+ // (LP-325).
1352
+ fields: searchFields,
1353
+ ...(nextPageToken ? { nextPageToken } : {}),
1354
+ });
1355
+ records.push(...(page.issues ?? []));
1356
+ pages += 1;
1357
+ opts?.onPage?.({ page: pages, records: records.length });
1358
+ nextPageToken = page.nextPageToken ?? undefined;
1359
+ } while (nextPageToken);
1360
+ let cursor = null;
1361
+ for (const record of records) {
1362
+ const updated = updatedOf(record);
1363
+ if (updated !== null && (cursor === null || updated > cursor))
1364
+ cursor = updated;
1365
+ }
1366
+ const enriched = [];
1367
+ for (const record of records) {
1368
+ enriched.push((await enrichSprint(record)) ?? record);
1369
+ }
1370
+ return { records: enriched, cursor };
1371
+ }
1372
+ catch (error) {
1373
+ throw describeFailure(error, conn, 'listing issues');
1374
+ }
1375
+ },
1376
+ async link(dependentRemoteId, dependencyRemoteId, signal, kind) {
1377
+ const c = await client();
1378
+ try {
1379
+ if (kind === 'relates') {
1380
+ // `relates_to` → the native `Relates` link. Symmetric, so either
1381
+ // orientation holds the same meaning; the local issue is the outward
1382
+ // ("relates to") side for consistency with how it is read back.
1383
+ await c.issueLinks.linkIssues({
1384
+ outwardIssue: { id: dependentRemoteId },
1385
+ inwardIssue: { id: dependencyRemoteId },
1386
+ type: { name: 'Relates' },
1387
+ });
1388
+ }
1389
+ else {
1390
+ // `depends_on` → the dependent waits on the dependency, so the
1391
+ // dependency **blocks** the dependent.
1392
+ //
1393
+ // The field names invite the opposite arrangement, and it is wrong.
1394
+ // Measured against Jira Cloud: posting `{outwardIssue: A, inwardIssue: B}`
1395
+ // with type `Blocks` produces "**B** blocks A" — the *inward* issue
1396
+ // is the one that does the blocking. So the dependency goes in
1397
+ // `inwardIssue` and the dependent in `outwardIssue`.
1398
+ //
1399
+ // Reversed, this is silent: both sides link, the push reports
1400
+ // success, and every dependency on the board reaches Jira pointing
1401
+ // backwards — a plan that reads as its own mirror image to anybody
1402
+ // working in Jira.
1403
+ await c.issueLinks.linkIssues({
1404
+ outwardIssue: { id: dependentRemoteId },
1405
+ inwardIssue: { id: dependencyRemoteId },
1406
+ type: { name: 'Blocks' },
1407
+ });
1408
+ }
1409
+ void signal;
1410
+ return { remoteId: dependentRemoteId, remoteKey: dependentRemoteId, remoteUrl: '', remoteRev: '' };
1411
+ }
1412
+ catch (error) {
1413
+ throw describeFailure(error, conn, `linking ${dependentRemoteId} to ${dependencyRemoteId}`);
1414
+ }
1415
+ },
1416
+ async unlink(dependentRemoteId, dependencyRemoteId, signal, kind) {
1417
+ // Jira links are removed by link id, which needs a read first: the
1418
+ // connector reads the issue's `issuelinks`, finds the link between the
1419
+ // two ids, and deletes it by id. A link already gone is a no-op —
1420
+ // unlinking is idempotent (LP-325).
1421
+ const c = await client();
1422
+ try {
1423
+ const typeName = kind === 'relates' ? 'Relates' : 'Blocks';
1424
+ const record = await c.issues.getIssue({ issueIdOrKey: dependentRemoteId, fields: ['issuelinks'] });
1425
+ // An `issuelinks` entry names only the *other* end — never both — so
1426
+ // matching on a pair matched nothing, and every unlink quietly did
1427
+ // nothing while reporting success. Read from the dependent's record,
1428
+ // the dependency is the issue named on the entry; for `Blocks` it is
1429
+ // specifically the `inwardIssue` ("this issue is blocked by that
1430
+ // one"), which is also what tells a `depends_on` edge apart from its
1431
+ // own reverse.
1432
+ const match = issueLinksOf(record).find((link) => {
1433
+ if (linkTypeNameOf(link) !== typeName)
1434
+ return false;
1435
+ if (kind === 'relates') {
1436
+ const other = link['inwardIssue'] !== undefined ? link['inwardIssue'] : link['outwardIssue'];
1437
+ return endpointIdOf(other) === dependencyRemoteId;
1438
+ }
1439
+ return endpointIdOf(link['inwardIssue']) === dependencyRemoteId;
1440
+ });
1441
+ if (match === undefined) {
1442
+ // Already gone — nothing to delete.
1443
+ return { remoteId: dependentRemoteId, remoteKey: dependentRemoteId, remoteUrl: '', remoteRev: '' };
1444
+ }
1445
+ const raw = match['id'];
1446
+ const linkId = typeof raw === 'string' ? raw : typeof raw === 'number' ? String(raw) : '';
1447
+ if (linkId === '') {
1448
+ throw new BoardError(`Cannot unlink ${dependentRemoteId} from ${dependencyRemoteId}`, [
1449
+ 'the issue link carried no id to delete by',
1450
+ ]);
1451
+ }
1452
+ await c.issueLinks.deleteIssueLink({ linkId });
1453
+ void signal;
1454
+ return { remoteId: dependentRemoteId, remoteKey: dependentRemoteId, remoteUrl: '', remoteRev: '' };
1455
+ }
1456
+ catch (error) {
1457
+ throw describeFailure(error, conn, `unlinking ${dependentRemoteId} from ${dependencyRemoteId}`);
1458
+ }
1459
+ },
1460
+ async comment(remoteId, body, signal) {
1461
+ const c = await client();
1462
+ try {
1463
+ const comment = await c.issueComments.addComment({ issueIdOrKey: remoteId, body });
1464
+ void signal;
1465
+ return commentResult(comment, remoteId, remoteId);
1466
+ }
1467
+ catch (error) {
1468
+ throw describeFailure(error, conn, `commenting on ${remoteId}`);
1469
+ }
1470
+ },
1471
+ async editComment(remoteId, commentId, body, signal) {
1472
+ const c = await client();
1473
+ try {
1474
+ const comment = await c.issueComments.updateComment({ issueIdOrKey: remoteId, id: commentId, body });
1475
+ void signal;
1476
+ return commentResult(comment, remoteId, remoteId);
1477
+ }
1478
+ catch (error) {
1479
+ throw describeFailure(error, conn, `editing a comment on ${remoteId}`);
1480
+ }
1481
+ },
1482
+ async deleteComment(remoteId, commentId, signal) {
1483
+ const c = await client();
1484
+ try {
1485
+ await c.issueComments.deleteComment({ issueIdOrKey: remoteId, id: commentId });
1486
+ void signal;
1487
+ return commentResult({ id: commentId }, remoteId, remoteId);
1488
+ }
1489
+ catch (error) {
1490
+ throw describeFailure(error, conn, `deleting a comment on ${remoteId}`);
1491
+ }
1492
+ },
1493
+ /**
1494
+ * Answer a capability probe by name (LP-274). `hierarchy` reports the
1495
+ * project's native parent levels, detected from its issue types rather than
1496
+ * assumed (LP-324). `custom_fields` reports the value shapes Jira custom
1497
+ * fields can hold, in the provider's own schema vocabulary (LP-327) — the
1498
+ * name→id resolution itself is `fields()` / `preflightFields`'s job.
1499
+ */
1500
+ async probe(probeName) {
1501
+ if (probeName === 'hierarchy') {
1502
+ return hierarchyDepthOf(await typeScheme());
1503
+ }
1504
+ if (probeName === 'custom_fields') {
1505
+ return { valueTypes: ['string', 'number', 'date', 'datetime', 'option', 'array'] };
1506
+ }
1507
+ throw new BoardError(`The Jira connector does not answer probe "${probeName}"`, [
1508
+ 'Only the "hierarchy" and "custom_fields" probes are implemented.',
1509
+ ]);
1510
+ },
1511
+ /** The project's issue types, for the preflight to validate the mapping against. */
1512
+ async issueTypes() {
1513
+ return typeScheme();
1514
+ },
1515
+ /**
1516
+ * The project's own words: its issue types and every status any of its issue
1517
+ * types can reach (LP-537). `getAllStatuses` returns statuses *per issue
1518
+ * type*, so the set is the union — a status that only the Bug workflow has
1519
+ * is still a status this project has, and a mapping naming it is correct.
1520
+ *
1521
+ * Reading statuses needs only Browse Projects, unlike the workflow graph the
1522
+ * multi-hop transition path reads, so `lpm remote setup` works for the
1523
+ * ordinary syncing account.
1524
+ */
1525
+ async vocabulary() {
1526
+ const types = (await typeScheme()).map((type) => type.name);
1527
+ return { types, statuses: await projectStatuses() };
1528
+ },
1529
+ /** The instance's custom fields, for discovery and preflight (LP-327). */
1530
+ fields,
1531
+ /** The create-screen field ids keyed by issue type, for the screen check (LP-327). */
1532
+ createMeta,
1533
+ /** Create a custom field, for `provision` (LP-327). */
1534
+ createField,
1535
+ /** Add a field to the default screen, for `provision` (LP-327). */
1536
+ addFieldToScreen,
1537
+ /**
1538
+ * The people this project can assign work to.
1539
+ *
1540
+ * `findAssignableUsers` rather than a plain user search: an account that
1541
+ * exists on the instance but is not assignable *here* is a failed write
1542
+ * either way, so listing it would be an answer that reads as reassuring
1543
+ * and is not. Paged, because a large site returns 50 at a time, and
1544
+ * `active` users only — a deactivated account is not somewhere work can
1545
+ * go. An instance that refuses the call (it needs Browse Users) reports
1546
+ * the refusal rather than an empty list, which would read as "nobody".
1547
+ */
1548
+ async listUsers(signal) {
1549
+ void signal;
1550
+ const c = await client();
1551
+ const out = [];
1552
+ const seen = new Set();
1553
+ let startAt = 0;
1554
+ try {
1555
+ for (;;) {
1556
+ const page = await c.userSearch.findAssignableUsers({
1557
+ project: conn.project,
1558
+ maxResults: 50,
1559
+ startAt,
1560
+ });
1561
+ const users = Array.isArray(page) ? page : [];
1562
+ for (const user of users) {
1563
+ const id = user['accountId'];
1564
+ if (typeof id !== 'string' || id === '' || seen.has(id))
1565
+ continue;
1566
+ if (user['active'] === false)
1567
+ continue;
1568
+ seen.add(id);
1569
+ const email = user['emailAddress'];
1570
+ out.push({
1571
+ id,
1572
+ name: typeof user['displayName'] === 'string' ? user['displayName'] : id,
1573
+ ...(typeof email === 'string' && email !== '' ? { email } : {}),
1574
+ });
1575
+ }
1576
+ if (users.length < 50)
1577
+ break;
1578
+ startAt += users.length;
1579
+ }
1580
+ }
1581
+ catch (error) {
1582
+ throw describeFailure(error, conn, `listing the users assignable in ${conn.project}`);
1583
+ }
1584
+ return out;
1585
+ },
1586
+ /** Every sprint on the configured Agile board, for `provision` (LP-328). */
1587
+ async listSprints(signal) {
1588
+ void signal;
1589
+ return sprints();
1590
+ },
1591
+ /** Create a future sprint on the configured Agile board, for `provision` (LP-328). */
1592
+ async createSprint(name, starts, ends, signal) {
1593
+ if (conn.board === undefined) {
1594
+ throw new BoardError(`Cannot create sprint "${name}": no Agile board id`, [
1595
+ 'Sprints belong to an Agile board, not a project — set connection.board to the board id first.',
1596
+ ]);
1597
+ }
1598
+ const a = await agile();
1599
+ try {
1600
+ const created = await a.sprint.createSprint({
1601
+ name,
1602
+ originBoardId: Number(conn.board),
1603
+ ...(starts !== undefined ? { startDate: sprintStart(starts) } : {}),
1604
+ ...(ends !== undefined ? { endDate: sprintEnd(ends) } : {}),
1605
+ });
1606
+ void signal;
1607
+ const id = created['id'];
1608
+ if (typeof id !== 'string' && typeof id !== 'number') {
1609
+ throw new BoardError(`Jira created the sprint "${name}" but returned no id`, [
1610
+ 'The sprint exists but its id could not be read; re-run provision to pick it up.',
1611
+ ]);
1612
+ }
1613
+ // **The sprint list is cached, so the cache has just gone stale.**
1614
+ // A push creates its prerequisites *before* it writes any issue,
1615
+ // precisely so the issues can be filed into them — but the lookup that
1616
+ // resolves a period to a sprint reads this cache, which was filled
1617
+ // while the sprint did not exist yet. Without this line the sprint is
1618
+ // created and then not found, every issue in it is filed unscheduled
1619
+ // with `unwritten: 'period'`, and the schedule only lands on the
1620
+ // *next* push. Self-healing hid it: the board was always one push
1621
+ // behind on its own timeline.
1622
+ sprintsPromise = undefined;
1623
+ return { id: String(id) };
1624
+ }
1625
+ catch (error) {
1626
+ throw describeFailure(error, conn, `creating the sprint "${name}"`);
1627
+ }
1628
+ },
1629
+ discoverConnection,
1630
+ /**
1631
+ * Move an existing twin under a new parent, or out from under one.
1632
+ *
1633
+ * Without this the executor could file a parent on *create* and never
1634
+ * change it afterwards: a `reparent` op was reported "skipped — connector
1635
+ * cannot move an existing issue to a native parent", so a board whose
1636
+ * carrier changed (or whose document was reparented locally) kept the
1637
+ * shape it was first filed with, for ever.
1638
+ *
1639
+ * `null` clears the parent, which is how a document that moved to a level
1640
+ * the block carries gets its native edge taken away rather than left stale.
1641
+ */
1642
+ async reparent(remoteId, parentRemoteId, signal) {
1643
+ const c = await client();
1644
+ try {
1645
+ await c.issues.editIssue({
1646
+ issueIdOrKey: remoteId,
1647
+ fields: { parent: parentRemoteId === null ? null : parentRef(parentRemoteId) },
1648
+ });
1649
+ const record = await c.issues.getIssue({ issueIdOrKey: remoteId });
1650
+ void signal;
1651
+ return resultOf(record, conn.site);
1652
+ }
1653
+ catch (error) {
1654
+ throw describeFailure(error, conn, `re-parenting issue ${remoteId}`);
1655
+ }
1656
+ },
1657
+ async reachable() {
1658
+ const c = await client();
1659
+ try {
1660
+ const project = await c.projects.getProject({ projectIdOrKey: conn.project });
1661
+ return {
1662
+ reachable: true,
1663
+ evidence: `GET /rest/api/3/project/${conn.project} → ${typeof project['key'] === 'string' ? `project ${project['key']} reachable` : '200'}`,
1664
+ };
1665
+ }
1666
+ catch (error) {
1667
+ const err = error;
1668
+ if (err.status === 404) {
1669
+ return {
1670
+ reachable: false,
1671
+ evidence: `GET /rest/api/3/project/${conn.project} → 404`,
1672
+ };
1673
+ }
1674
+ if (err.status === undefined && err.code !== undefined) {
1675
+ return {
1676
+ reachable: false,
1677
+ evidence: `could not reach ${conn.site}: ${err.code ?? 'network error'}`,
1678
+ };
1679
+ }
1680
+ return {
1681
+ reachable: false,
1682
+ evidence: `GET /rest/api/3/project/${conn.project} → ${err.status ?? 'error'}`,
1683
+ };
1684
+ }
1685
+ },
1686
+ };
1687
+ }
1688
+ //# sourceMappingURL=connector.js.map