@iloom/cli 0.9.2 → 0.10.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 (220) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +159 -40
  3. package/dist/{BranchNamingService-K6XNWQ6C.js → BranchNamingService-ECJHBB67.js} +2 -2
  4. package/dist/ClaudeContextManager-QXX6ZFST.js +14 -0
  5. package/dist/ClaudeService-NJNK2SUH.js +13 -0
  6. package/dist/{GitHubService-TGWJN4V4.js → GitHubService-MEHKHUQP.js} +4 -4
  7. package/dist/IssueTrackerFactory-NG53YX5S.js +14 -0
  8. package/dist/{LoomLauncher-73NXL2CL.js → LoomLauncher-L64HHS3T.js} +9 -9
  9. package/dist/{MetadataManager-W3C54UYT.js → MetadataManager-5QZSTKNN.js} +2 -2
  10. package/dist/{ProjectCapabilityDetector-N5L7T4IY.js → ProjectCapabilityDetector-5KSYUTBJ.js} +3 -3
  11. package/dist/{PromptTemplateManager-36YLQRHP.js → PromptTemplateManager-DULSVRRE.js} +2 -2
  12. package/dist/README.md +159 -40
  13. package/dist/{SettingsManager-AW3JTJHD.js → SettingsManager-BQDQA3FK.js} +4 -2
  14. package/dist/agents/iloom-artifact-reviewer.md +11 -0
  15. package/dist/agents/iloom-code-reviewer.md +14 -0
  16. package/dist/agents/iloom-issue-analyze-and-plan.md +55 -12
  17. package/dist/agents/iloom-issue-analyzer.md +49 -6
  18. package/dist/agents/iloom-issue-complexity-evaluator.md +47 -6
  19. package/dist/agents/iloom-issue-enhancer.md +86 -7
  20. package/dist/agents/iloom-issue-implementer.md +48 -7
  21. package/dist/agents/iloom-issue-planner.md +115 -62
  22. package/dist/{build-THZI572G.js → build-5GO3XW26.js} +9 -9
  23. package/dist/{chunk-NUACL52E.js → chunk-3D7WQM7I.js} +2 -2
  24. package/dist/chunk-4232AHNQ.js +35 -0
  25. package/dist/chunk-4232AHNQ.js.map +1 -0
  26. package/dist/{chunk-QN47QVBX.js → chunk-4WJNIR5O.js} +1 -1
  27. package/dist/chunk-4WJNIR5O.js.map +1 -0
  28. package/dist/{chunk-A7NJF73J.js → chunk-5MWV33NN.js} +4 -4
  29. package/dist/{chunk-3I4ONZRT.js → chunk-6EU6TCF6.js} +10 -10
  30. package/dist/chunk-6EU6TCF6.js.map +1 -0
  31. package/dist/{chunk-CWRI4JC3.js → chunk-FB47TIJG.js} +29 -11
  32. package/dist/chunk-FB47TIJG.js.map +1 -0
  33. package/dist/chunk-HEXKPKCK.js +1396 -0
  34. package/dist/chunk-HEXKPKCK.js.map +1 -0
  35. package/dist/{chunk-KAYXR544.js → chunk-J5S7DFYC.js} +2 -2
  36. package/dist/{chunk-ULSWCPQG.js → chunk-JO2LZ6EQ.js} +476 -5
  37. package/dist/chunk-JO2LZ6EQ.js.map +1 -0
  38. package/dist/{chunk-KBEIQP4G.js → chunk-KB64WNBZ.js} +43 -3
  39. package/dist/chunk-KB64WNBZ.js.map +1 -0
  40. package/dist/{chunk-OFDN5NKS.js → chunk-KXDRI47U.js} +69 -12
  41. package/dist/chunk-KXDRI47U.js.map +1 -0
  42. package/dist/{chunk-R4YWBGY6.js → chunk-LXLMMXXY.js} +54 -14
  43. package/dist/chunk-LXLMMXXY.js.map +1 -0
  44. package/dist/{chunk-AR5QKYNE.js → chunk-MNHZB4Z2.js} +4 -4
  45. package/dist/{chunk-TL72BGP6.js → chunk-MORRVYPT.js} +2 -2
  46. package/dist/{chunk-KJTVU3HZ.js → chunk-NRSWLOAZ.js} +8 -8
  47. package/dist/chunk-NRSWLOAZ.js.map +1 -0
  48. package/dist/{chunk-FO5GGFOV.js → chunk-ONQYPICO.js} +13 -5
  49. package/dist/chunk-ONQYPICO.js.map +1 -0
  50. package/dist/{chunk-7ZEHSSUP.js → chunk-P4O6EH46.js} +4 -4
  51. package/dist/chunk-QZWEJVWV.js +207 -0
  52. package/dist/chunk-QZWEJVWV.js.map +1 -0
  53. package/dist/chunk-RSYT7MVI.js +202 -0
  54. package/dist/chunk-RSYT7MVI.js.map +1 -0
  55. package/dist/{chunk-Z2TWEXR7.js → chunk-RYWFS37M.js} +6 -6
  56. package/dist/chunk-RYWFS37M.js.map +1 -0
  57. package/dist/{chunk-B7U6OKUR.js → chunk-SF2P22EE.js} +11 -3
  58. package/dist/chunk-SF2P22EE.js.map +1 -0
  59. package/dist/{chunk-6IIL5M2L.js → chunk-SN3SQCFK.js} +10 -8
  60. package/dist/{chunk-6IIL5M2L.js.map → chunk-SN3SQCFK.js.map} +1 -1
  61. package/dist/{chunk-SOSQILHO.js → chunk-UD3WJDIV.js} +92 -82
  62. package/dist/chunk-UD3WJDIV.js.map +1 -0
  63. package/dist/{chunk-KXGQYLFZ.js → chunk-UKBAJ2QQ.js} +61 -7
  64. package/dist/chunk-UKBAJ2QQ.js.map +1 -0
  65. package/dist/{chunk-W6DP5RVR.js → chunk-UVD4CZKS.js} +3 -3
  66. package/dist/chunk-UWGVCXRF.js +207 -0
  67. package/dist/chunk-UWGVCXRF.js.map +1 -0
  68. package/dist/{chunk-NWMORW3U.js → chunk-VECNX6VX.js} +2 -2
  69. package/dist/{chunk-4CO6KG5S.js → chunk-VG45TUYK.js} +53 -7
  70. package/dist/{chunk-4CO6KG5S.js.map → chunk-VG45TUYK.js.map} +1 -1
  71. package/dist/{chunk-TC7APDKU.js → chunk-VGGST52X.js} +2 -2
  72. package/dist/{chunk-4LKGCFGG.js → chunk-WWKOVDWC.js} +2 -2
  73. package/dist/{chunk-YKFCCV6S.js → chunk-WY4QBK43.js} +7 -7
  74. package/dist/chunk-WY4QBK43.js.map +1 -0
  75. package/dist/chunk-Y4YZTHZE.js +73 -0
  76. package/dist/chunk-Y4YZTHZE.js.map +1 -0
  77. package/dist/{chunk-VOGGLPG5.js → chunk-YQ57ORTV.js} +14 -1
  78. package/dist/chunk-YQ57ORTV.js.map +1 -0
  79. package/dist/{chunk-RI2YL6TK.js → chunk-YYAKPQBT.js} +65 -18
  80. package/dist/chunk-YYAKPQBT.js.map +1 -0
  81. package/dist/{chunk-IZIYLYPK.js → chunk-ZEWU5PZK.js} +2 -2
  82. package/dist/{chunk-VPTAX5TR.js → chunk-ZHPNZC75.js} +12 -12
  83. package/dist/chunk-ZHPNZC75.js.map +1 -0
  84. package/dist/{chunk-DGG2VY7B.js → chunk-ZW2LKWWE.js} +9 -9
  85. package/dist/chunk-ZW2LKWWE.js.map +1 -0
  86. package/dist/{claude-TP2QO3BU.js → claude-P3NQR6IJ.js} +2 -2
  87. package/dist/{cleanup-PJRIFFU4.js → cleanup-6UCPVMFG.js} +81 -32
  88. package/dist/cleanup-6UCPVMFG.js.map +1 -0
  89. package/dist/cli.js +638 -349
  90. package/dist/cli.js.map +1 -1
  91. package/dist/{commit-IVP3M4HG.js → commit-L3EPY5QG.js} +21 -20
  92. package/dist/commit-L3EPY5QG.js.map +1 -0
  93. package/dist/{compile-R2J65HBQ.js → compile-ZS4HYRX5.js} +9 -9
  94. package/dist/{contribute-VDZXHK5Y.js → contribute-ORDDQGSL.js} +14 -6
  95. package/dist/contribute-ORDDQGSL.js.map +1 -0
  96. package/dist/{dev-server-7F622OEO.js → dev-server-FYZ2AQIH.js} +29 -15
  97. package/dist/dev-server-FYZ2AQIH.js.map +1 -0
  98. package/dist/{feedback-E7VET7CL.js → feedback-TMBXSCM5.js} +15 -15
  99. package/dist/{git-2QDQ2X2S.js → git-ET64COO3.js} +4 -4
  100. package/dist/hooks/iloom-hook.js +15 -0
  101. package/dist/ignite-CGOV3TD4.js +1393 -0
  102. package/dist/ignite-CGOV3TD4.js.map +1 -0
  103. package/dist/index.d.ts +382 -53
  104. package/dist/index.js +1167 -36
  105. package/dist/index.js.map +1 -1
  106. package/dist/{init-676DHF6R.js → init-GFQ5W7GK.js} +57 -21
  107. package/dist/init-GFQ5W7GK.js.map +1 -0
  108. package/dist/{issues-PJSOLOBJ.js → issues-T4ZZSPEG.js} +61 -20
  109. package/dist/issues-T4ZZSPEG.js.map +1 -0
  110. package/dist/{lint-CJM7BAIM.js → lint-6TQXDZ3T.js} +9 -9
  111. package/dist/mcp/issue-management-server.js +2471 -256
  112. package/dist/mcp/issue-management-server.js.map +1 -1
  113. package/dist/mcp/recap-server.js +144 -21
  114. package/dist/mcp/recap-server.js.map +1 -1
  115. package/dist/{neon-helpers-VVFFTLXE.js → neon-helpers-CQN2PB4S.js} +3 -3
  116. package/dist/neon-helpers-CQN2PB4S.js.map +1 -0
  117. package/dist/{open-544H7JF5.js → open-5QZGXQRF.js} +15 -15
  118. package/dist/open-5QZGXQRF.js.map +1 -0
  119. package/dist/{plan-Q7ELXDLC.js → plan-U7ZQWLFY.js} +41 -25
  120. package/dist/plan-U7ZQWLFY.js.map +1 -0
  121. package/dist/{projects-LH362JZQ.js → projects-2UOXFLNZ.js} +4 -4
  122. package/dist/prompts/CLAUDE.md +62 -0
  123. package/dist/prompts/init-prompt.txt +347 -26
  124. package/dist/prompts/issue-prompt.txt +427 -54
  125. package/dist/prompts/plan-prompt.txt +97 -16
  126. package/dist/prompts/pr-prompt.txt +44 -1
  127. package/dist/prompts/regular-prompt.txt +42 -1
  128. package/dist/prompts/session-summary-prompt.txt +14 -0
  129. package/dist/prompts/swarm-orchestrator-prompt.txt +437 -0
  130. package/dist/{rebase-YND35CIE.js → rebase-DWIB77KV.js} +10 -10
  131. package/dist/{recap-3W7COH7D.js → recap-MX63HAKV.js} +47 -19
  132. package/dist/recap-MX63HAKV.js.map +1 -0
  133. package/dist/{run-QUXJKDQQ.js → run-O3TFNQFC.js} +15 -15
  134. package/dist/run-O3TFNQFC.js.map +1 -0
  135. package/dist/schema/package-iloom.schema.json +58 -0
  136. package/dist/schema/settings.schema.json +115 -15
  137. package/dist/{shell-QGECBLST.js → shell-G6VC2CYR.js} +14 -7
  138. package/dist/shell-G6VC2CYR.js.map +1 -0
  139. package/dist/{summary-G2T4452H.js → summary-FWHAX55O.js} +27 -25
  140. package/dist/summary-FWHAX55O.js.map +1 -0
  141. package/dist/{test-EA5NQFDC.js → test-F7JNJZYP.js} +9 -9
  142. package/dist/{test-git-M7LSLEFL.js → test-git-BTAOIUE2.js} +4 -4
  143. package/dist/test-jira-CHYNV33F.js +96 -0
  144. package/dist/test-jira-CHYNV33F.js.map +1 -0
  145. package/dist/{test-prefix-64NAAUON.js → test-prefix-Q6TFSU6F.js} +4 -4
  146. package/dist/{test-webserver-OK6Z5FJM.js → test-webserver-EONCG7E7.js} +6 -6
  147. package/dist/{vscode-AR5NNXXI.js → vscode-VA5X4P25.js} +7 -7
  148. package/package.json +5 -1
  149. package/dist/ClaudeContextManager-HR5JQKAI.js +0 -14
  150. package/dist/ClaudeService-TK7FMC2X.js +0 -13
  151. package/dist/chunk-3I4ONZRT.js.map +0 -1
  152. package/dist/chunk-B7U6OKUR.js.map +0 -1
  153. package/dist/chunk-CWRI4JC3.js.map +0 -1
  154. package/dist/chunk-DGG2VY7B.js.map +0 -1
  155. package/dist/chunk-FJDRTVJX.js +0 -520
  156. package/dist/chunk-FJDRTVJX.js.map +0 -1
  157. package/dist/chunk-FO5GGFOV.js.map +0 -1
  158. package/dist/chunk-KBEIQP4G.js.map +0 -1
  159. package/dist/chunk-KJTVU3HZ.js.map +0 -1
  160. package/dist/chunk-KXGQYLFZ.js.map +0 -1
  161. package/dist/chunk-OFDN5NKS.js.map +0 -1
  162. package/dist/chunk-QN47QVBX.js.map +0 -1
  163. package/dist/chunk-R4YWBGY6.js.map +0 -1
  164. package/dist/chunk-RI2YL6TK.js.map +0 -1
  165. package/dist/chunk-SOSQILHO.js.map +0 -1
  166. package/dist/chunk-ULSWCPQG.js.map +0 -1
  167. package/dist/chunk-VOGGLPG5.js.map +0 -1
  168. package/dist/chunk-VPTAX5TR.js.map +0 -1
  169. package/dist/chunk-WHI5KEOX.js +0 -121
  170. package/dist/chunk-WHI5KEOX.js.map +0 -1
  171. package/dist/chunk-YKFCCV6S.js.map +0 -1
  172. package/dist/chunk-Z2TWEXR7.js.map +0 -1
  173. package/dist/cleanup-PJRIFFU4.js.map +0 -1
  174. package/dist/commit-IVP3M4HG.js.map +0 -1
  175. package/dist/contribute-VDZXHK5Y.js.map +0 -1
  176. package/dist/dev-server-7F622OEO.js.map +0 -1
  177. package/dist/ignite-IW35CDBD.js +0 -784
  178. package/dist/ignite-IW35CDBD.js.map +0 -1
  179. package/dist/init-676DHF6R.js.map +0 -1
  180. package/dist/issues-PJSOLOBJ.js.map +0 -1
  181. package/dist/open-544H7JF5.js.map +0 -1
  182. package/dist/plan-Q7ELXDLC.js.map +0 -1
  183. package/dist/recap-3W7COH7D.js.map +0 -1
  184. package/dist/run-QUXJKDQQ.js.map +0 -1
  185. package/dist/shell-QGECBLST.js.map +0 -1
  186. package/dist/summary-G2T4452H.js.map +0 -1
  187. /package/dist/{BranchNamingService-K6XNWQ6C.js.map → BranchNamingService-ECJHBB67.js.map} +0 -0
  188. /package/dist/{ClaudeContextManager-HR5JQKAI.js.map → ClaudeContextManager-QXX6ZFST.js.map} +0 -0
  189. /package/dist/{ClaudeService-TK7FMC2X.js.map → ClaudeService-NJNK2SUH.js.map} +0 -0
  190. /package/dist/{GitHubService-TGWJN4V4.js.map → GitHubService-MEHKHUQP.js.map} +0 -0
  191. /package/dist/{MetadataManager-W3C54UYT.js.map → IssueTrackerFactory-NG53YX5S.js.map} +0 -0
  192. /package/dist/{LoomLauncher-73NXL2CL.js.map → LoomLauncher-L64HHS3T.js.map} +0 -0
  193. /package/dist/{ProjectCapabilityDetector-N5L7T4IY.js.map → MetadataManager-5QZSTKNN.js.map} +0 -0
  194. /package/dist/{PromptTemplateManager-36YLQRHP.js.map → ProjectCapabilityDetector-5KSYUTBJ.js.map} +0 -0
  195. /package/dist/{SettingsManager-AW3JTJHD.js.map → PromptTemplateManager-DULSVRRE.js.map} +0 -0
  196. /package/dist/{claude-TP2QO3BU.js.map → SettingsManager-BQDQA3FK.js.map} +0 -0
  197. /package/dist/{build-THZI572G.js.map → build-5GO3XW26.js.map} +0 -0
  198. /package/dist/{chunk-NUACL52E.js.map → chunk-3D7WQM7I.js.map} +0 -0
  199. /package/dist/{chunk-A7NJF73J.js.map → chunk-5MWV33NN.js.map} +0 -0
  200. /package/dist/{chunk-KAYXR544.js.map → chunk-J5S7DFYC.js.map} +0 -0
  201. /package/dist/{chunk-AR5QKYNE.js.map → chunk-MNHZB4Z2.js.map} +0 -0
  202. /package/dist/{chunk-TL72BGP6.js.map → chunk-MORRVYPT.js.map} +0 -0
  203. /package/dist/{chunk-7ZEHSSUP.js.map → chunk-P4O6EH46.js.map} +0 -0
  204. /package/dist/{chunk-W6DP5RVR.js.map → chunk-UVD4CZKS.js.map} +0 -0
  205. /package/dist/{chunk-NWMORW3U.js.map → chunk-VECNX6VX.js.map} +0 -0
  206. /package/dist/{chunk-TC7APDKU.js.map → chunk-VGGST52X.js.map} +0 -0
  207. /package/dist/{chunk-4LKGCFGG.js.map → chunk-WWKOVDWC.js.map} +0 -0
  208. /package/dist/{chunk-IZIYLYPK.js.map → chunk-ZEWU5PZK.js.map} +0 -0
  209. /package/dist/{git-2QDQ2X2S.js.map → claude-P3NQR6IJ.js.map} +0 -0
  210. /package/dist/{compile-R2J65HBQ.js.map → compile-ZS4HYRX5.js.map} +0 -0
  211. /package/dist/{feedback-E7VET7CL.js.map → feedback-TMBXSCM5.js.map} +0 -0
  212. /package/dist/{neon-helpers-VVFFTLXE.js.map → git-ET64COO3.js.map} +0 -0
  213. /package/dist/{lint-CJM7BAIM.js.map → lint-6TQXDZ3T.js.map} +0 -0
  214. /package/dist/{projects-LH362JZQ.js.map → projects-2UOXFLNZ.js.map} +0 -0
  215. /package/dist/{rebase-YND35CIE.js.map → rebase-DWIB77KV.js.map} +0 -0
  216. /package/dist/{test-EA5NQFDC.js.map → test-F7JNJZYP.js.map} +0 -0
  217. /package/dist/{test-git-M7LSLEFL.js.map → test-git-BTAOIUE2.js.map} +0 -0
  218. /package/dist/{test-prefix-64NAAUON.js.map → test-prefix-Q6TFSU6F.js.map} +0 -0
  219. /package/dist/{test-webserver-OK6Z5FJM.js.map → test-webserver-EONCG7E7.js.map} +0 -0
  220. /package/dist/{vscode-AR5NNXXI.js.map → vscode-VA5X4P25.js.map} +0 -0
package/dist/index.js CHANGED
@@ -475,6 +475,7 @@ var init_logger = __esm({
475
475
  var SettingsManager_exports = {};
476
476
  __export(SettingsManager_exports, {
477
477
  AgentSettingsSchema: () => AgentSettingsSchema,
478
+ BaseAgentSettingsSchema: () => BaseAgentSettingsSchema,
478
479
  CapabilitiesSettingsSchema: () => CapabilitiesSettingsSchema,
479
480
  CapabilitiesSettingsSchemaNoDefaults: () => CapabilitiesSettingsSchemaNoDefaults,
480
481
  DatabaseProvidersSettingsSchema: () => DatabaseProvidersSettingsSchema,
@@ -495,13 +496,30 @@ import path2 from "path";
495
496
  import os from "os";
496
497
  import { z } from "zod";
497
498
  import deepmerge from "deepmerge";
498
- var PROJECT_CAPABILITIES, AgentSettingsSchema, SpinAgentSettingsSchema, PlanCommandSettingsSchema, SummarySettingsSchema, WorkflowPermissionSchema, WorkflowPermissionSchemaNoDefaults, WorkflowsSettingsSchema, WorkflowsSettingsSchemaNoDefaults, CapabilitiesSettingsSchema, CapabilitiesSettingsSchemaNoDefaults, NeonSettingsSchema, DatabaseProvidersSettingsSchema, IloomSettingsSchema, IloomSettingsSchemaNoDefaults, SettingsManager;
499
+ function redactSensitiveFields(obj) {
500
+ if (obj === null || obj === void 0) return obj;
501
+ if (typeof obj !== "object") return obj;
502
+ if (Array.isArray(obj)) return obj.map(redactSensitiveFields);
503
+ const sensitiveKeys = ["apitoken", "token", "secret", "password"];
504
+ const result = {};
505
+ for (const [key, value] of Object.entries(obj)) {
506
+ const lowerKey = key.toLowerCase();
507
+ if (sensitiveKeys.some((s) => lowerKey.includes(s)) && typeof value === "string") {
508
+ result[key] = "[REDACTED]";
509
+ } else if (typeof value === "object" && value !== null) {
510
+ result[key] = redactSensitiveFields(value);
511
+ } else {
512
+ result[key] = value;
513
+ }
514
+ }
515
+ return result;
516
+ }
517
+ var BaseAgentSettingsSchema, AgentSettingsSchema, SpinAgentSettingsSchema, PlanCommandSettingsSchema, SummarySettingsSchema, WorkflowPermissionSchema, WorkflowPermissionSchemaNoDefaults, WorkflowsSettingsSchema, WorkflowsSettingsSchemaNoDefaults, CapabilitiesSettingsSchema, CapabilitiesSettingsSchemaNoDefaults, NeonSettingsSchema, DatabaseProvidersSettingsSchema, IloomSettingsSchema, IloomSettingsSchemaNoDefaults, SettingsManager;
499
518
  var init_SettingsManager = __esm({
500
519
  "src/lib/SettingsManager.ts"() {
501
520
  "use strict";
502
521
  init_logger();
503
- PROJECT_CAPABILITIES = ["cli", "web"];
504
- AgentSettingsSchema = z.object({
522
+ BaseAgentSettingsSchema = z.object({
505
523
  model: z.enum(["sonnet", "opus", "haiku"]).optional().describe("Claude model shorthand: sonnet, opus, or haiku"),
506
524
  enabled: z.boolean().optional().describe("Whether this agent is enabled. Defaults to true."),
507
525
  providers: z.record(
@@ -510,6 +528,9 @@ var init_SettingsManager = __esm({
510
528
  ).optional().describe('Map of review providers to model names. Keys: claude, gemini, codex. Values: model name strings (e.g., "sonnet", "gemini-3-pro-preview", "gpt-5.2-codex")'),
511
529
  review: z.boolean().optional().describe("Whether artifacts from this agent should be reviewed before posting (defaults to false)")
512
530
  });
531
+ AgentSettingsSchema = BaseAgentSettingsSchema.extend({
532
+ agents: z.record(z.string(), BaseAgentSettingsSchema).optional().describe("Nested per-agent model overrides for swarm mode. Configure under agents.iloom-swarm-worker.agents.<agent-name>.model to set a different model for phase agents when running inside swarm workers. Fallback chain: swarm-specific agent model > explicit swarm worker model > base agent model. Only meaningful under the iloom-swarm-worker agent entry.")
533
+ });
513
534
  SpinAgentSettingsSchema = z.object({
514
535
  model: z.enum(["sonnet", "opus", "haiku"]).default("opus").describe("Claude model shorthand for spin orchestrator")
515
536
  });
@@ -550,19 +571,17 @@ var init_SettingsManager = __esm({
550
571
  regular: WorkflowPermissionSchemaNoDefaults.optional()
551
572
  }).optional();
552
573
  CapabilitiesSettingsSchema = z.object({
553
- capabilities: z.array(z.enum(PROJECT_CAPABILITIES)).optional().describe("Explicitly declared project capabilities (auto-detected if not specified)"),
554
574
  web: z.object({
555
575
  basePort: z.number().min(1, "Base port must be >= 1").max(65535, "Base port must be <= 65535").optional().describe("Base port for web workspace port calculations (default: 3000)")
556
- }).optional(),
576
+ }).optional().describe('Web dev server settings. To declare a project as a web project, add "web" to the capabilities array in .iloom/package.iloom.json or .iloom/package.iloom.local.json.'),
557
577
  database: z.object({
558
578
  databaseUrlEnvVarName: z.string().min(1, "Database URL variable name cannot be empty").regex(/^[A-Z_][A-Z0-9_]*$/, "Must be valid env var name (uppercase, underscores)").optional().default("DATABASE_URL").describe("Name of environment variable for database connection URL")
559
579
  }).optional()
560
580
  }).optional();
561
581
  CapabilitiesSettingsSchemaNoDefaults = z.object({
562
- capabilities: z.array(z.enum(PROJECT_CAPABILITIES)).optional().describe("Explicitly declared project capabilities (auto-detected if not specified)"),
563
582
  web: z.object({
564
583
  basePort: z.number().min(1, "Base port must be >= 1").max(65535, "Base port must be <= 65535").optional().describe("Base port for web workspace port calculations (default: 3000)")
565
- }).optional(),
584
+ }).optional().describe('Web dev server settings. To declare a project as a web project, add "web" to the capabilities array in .iloom/package.iloom.json or .iloom/package.iloom.local.json.'),
566
585
  database: z.object({
567
586
  databaseUrlEnvVarName: z.string().min(1, "Database URL variable name cannot be empty").regex(/^[A-Z_][A-Z0-9_]*$/, "Must be valid env var name (uppercase, underscores)").optional().describe("Name of environment variable for database connection URL")
568
587
  }).optional()
@@ -606,7 +625,7 @@ var init_SettingsManager = __esm({
606
625
  copyGitIgnoredPatterns: z.array(z.string().min(1, "Pattern cannot be empty")).optional().describe(`Glob patterns for gitignored files to copy to looms (e.g., ["*.db", "data/*.sqlite"]). Great for local dbs and large test data files that are too big to commit to git. Note: .env (dotenv-flow) files, iloom's and claude's local settings are automatically copied and do not need to be specified here.`),
607
626
  workflows: WorkflowsSettingsSchema.describe("Per-workflow-type permission configurations"),
608
627
  agents: z.record(z.string(), AgentSettingsSchema).optional().nullable().describe(
609
- "Per-agent configuration overrides. Available agents: iloom-issue-analyzer (analyzes issues), iloom-issue-planner (creates implementation plans), iloom-issue-analyze-and-plan (combined analysis and planning), iloom-issue-complexity-evaluator (evaluates complexity), iloom-issue-enhancer (enhances issue descriptions), iloom-issue-implementer (implements code changes), iloom-code-reviewer (reviews code changes against requirements), iloom-artifact-reviewer (reviews artifacts before posting)"
628
+ 'Per-agent configuration overrides. Available agents: iloom-issue-analyzer (analyzes issues), iloom-issue-planner (creates implementation plans), iloom-issue-analyze-and-plan (combined analysis and planning), iloom-issue-complexity-evaluator (evaluates complexity), iloom-issue-enhancer (enhances issue descriptions), iloom-issue-implementer (implements code changes), iloom-code-reviewer (reviews code changes against requirements), iloom-artifact-reviewer (reviews artifacts before posting), iloom-swarm-worker (swarm worker agent, dynamically generated). The iloom-swarm-worker agent supports a nested "agents" sub-record for configuring phase agent models specifically in swarm mode.'
610
629
  ),
611
630
  spin: SpinAgentSettingsSchema.optional().describe(
612
631
  "Spin orchestrator configuration. Model defaults to opus when not configured."
@@ -621,7 +640,7 @@ var init_SettingsManager = __esm({
621
640
  databaseProviders: DatabaseProvidersSettingsSchema.describe("Database provider configurations"),
622
641
  issueManagement: z.object({
623
642
  // SYNC: If this default changes, update displayDefaultsBox() in src/utils/first-run-setup.ts
624
- provider: z.enum(["github", "linear"]).optional().default("github").describe("Issue tracker provider (github, linear)"),
643
+ provider: z.enum(["github", "linear", "jira"]).optional().default("github").describe("Issue tracker provider (github, linear, jira)"),
625
644
  github: z.object({
626
645
  remote: z.string().min(1, "Remote name cannot be empty").describe("Git remote name to use for GitHub operations")
627
646
  }).optional(),
@@ -629,6 +648,17 @@ var init_SettingsManager = __esm({
629
648
  teamId: z.string().min(1, "Team ID cannot be empty").describe('Linear team identifier (e.g., "ENG", "PLAT")'),
630
649
  branchFormat: z.string().optional().describe("Branch naming template for Linear issues"),
631
650
  apiToken: z.string().optional().describe("Linear API token (lin_api_...). SECURITY: Store in settings.local.json only, never commit to source control.")
651
+ }).optional(),
652
+ jira: z.object({
653
+ host: z.string().min(1, "Jira host cannot be empty").describe('Jira instance URL (e.g., "https://yourcompany.atlassian.net")'),
654
+ username: z.string().min(1, "Jira username/email cannot be empty").describe("Jira username or email address"),
655
+ apiToken: z.string().optional().describe("Jira API token. SECURITY: Store in settings.local.json only, never commit to source control. Generate at: https://id.atlassian.com/manage-profile/security/api-tokens"),
656
+ projectKey: z.string().min(1, "Project key cannot be empty").describe('Jira project key (e.g., "PROJ", "ENG")'),
657
+ boardId: z.string().optional().describe("Jira board ID for sprint/workflow operations (optional)"),
658
+ transitionMappings: z.record(z.string(), z.string()).optional().describe('Map iloom states to Jira transition names (e.g., {"In Review": "Start Review"})'),
659
+ defaultIssueType: z.string().min(1).optional().default("Task").describe('Default Jira issue type name for creating issues (e.g., "Task", "Story", "Bug")'),
660
+ defaultSubtaskType: z.string().min(1).optional().default("Subtask").describe('Default Jira issue type name for creating subtasks/child issues (e.g., "Subtask", "Sub-task")'),
661
+ doneStatuses: z.array(z.string()).optional().default(["Done"]).describe('Status names to exclude from issue lists (e.g., ["Done", "Closed", "Verify"])')
632
662
  }).optional()
633
663
  }).optional().describe("Issue management configuration"),
634
664
  mergeBehavior: z.object({
@@ -637,6 +667,9 @@ var init_SettingsManager = __esm({
637
667
  remote: z.string().optional(),
638
668
  autoCommitPush: z.boolean().optional().describe(
639
669
  "Auto-commit and push after code review in draft PR mode. Defaults to true when mode is github-draft-pr."
670
+ ),
671
+ openBrowserOnFinish: z.boolean().default(true).describe(
672
+ "Open the PR in the default browser after finishing in github-pr or github-draft-pr mode. Use --no-browser flag to override."
640
673
  )
641
674
  }).optional().describe("Merge behavior configuration: local (merge locally), github-pr (create PR), or github-draft-pr (create draft PR at start, mark ready on finish)"),
642
675
  ide: z.object({
@@ -690,7 +723,7 @@ var init_SettingsManager = __esm({
690
723
  copyGitIgnoredPatterns: z.array(z.string().min(1, "Pattern cannot be empty")).optional().describe(`Glob patterns for gitignored files to copy to looms (e.g., ["*.db", "data/*.sqlite"]). Great for local dbs and large test data files that are too big to commit to git. Note: .env (dotenv-flow) files, iloom's and claude's local settings are automatically copied and do not need to be specified here.`),
691
724
  workflows: WorkflowsSettingsSchemaNoDefaults.describe("Per-workflow-type permission configurations"),
692
725
  agents: z.record(z.string(), AgentSettingsSchema).optional().nullable().describe(
693
- "Per-agent configuration overrides. Available agents: iloom-issue-analyzer (analyzes issues), iloom-issue-planner (creates implementation plans), iloom-issue-analyze-and-plan (combined analysis and planning), iloom-issue-complexity-evaluator (evaluates complexity), iloom-issue-enhancer (enhances issue descriptions), iloom-issue-implementer (implements code changes), iloom-code-reviewer (reviews code changes against requirements), iloom-artifact-reviewer (reviews artifacts before posting)"
726
+ 'Per-agent configuration overrides. Available agents: iloom-issue-analyzer (analyzes issues), iloom-issue-planner (creates implementation plans), iloom-issue-analyze-and-plan (combined analysis and planning), iloom-issue-complexity-evaluator (evaluates complexity), iloom-issue-enhancer (enhances issue descriptions), iloom-issue-implementer (implements code changes), iloom-code-reviewer (reviews code changes against requirements), iloom-artifact-reviewer (reviews artifacts before posting), iloom-swarm-worker (swarm worker agent, dynamically generated). The iloom-swarm-worker agent supports a nested "agents" sub-record for configuring phase agent models specifically in swarm mode.'
694
727
  ),
695
728
  spin: z.object({
696
729
  model: z.enum(["sonnet", "opus", "haiku"]).optional()
@@ -706,7 +739,7 @@ var init_SettingsManager = __esm({
706
739
  capabilities: CapabilitiesSettingsSchemaNoDefaults.describe("Project capability configurations"),
707
740
  databaseProviders: DatabaseProvidersSettingsSchema.describe("Database provider configurations"),
708
741
  issueManagement: z.object({
709
- provider: z.enum(["github", "linear"]).optional().describe("Issue tracker provider (github, linear)"),
742
+ provider: z.enum(["github", "linear", "jira"]).optional().describe("Issue tracker provider (github, linear, jira)"),
710
743
  github: z.object({
711
744
  remote: z.string().min(1, "Remote name cannot be empty").describe("Git remote name to use for GitHub operations")
712
745
  }).optional(),
@@ -714,6 +747,17 @@ var init_SettingsManager = __esm({
714
747
  teamId: z.string().min(1, "Team ID cannot be empty").describe('Linear team identifier (e.g., "ENG", "PLAT")'),
715
748
  branchFormat: z.string().optional().describe("Branch naming template for Linear issues"),
716
749
  apiToken: z.string().optional().describe("Linear API token (lin_api_...). SECURITY: Store in settings.local.json only, never commit to source control.")
750
+ }).optional(),
751
+ jira: z.object({
752
+ host: z.string().min(1, "Jira host cannot be empty").describe('Jira instance URL (e.g., "https://yourcompany.atlassian.net")'),
753
+ username: z.string().min(1, "Jira username/email cannot be empty").describe("Jira username or email address"),
754
+ apiToken: z.string().optional().describe("Jira API token. SECURITY: Store in settings.local.json only, never commit to source control. Generate at: https://id.atlassian.com/manage-profile/security/api-tokens"),
755
+ projectKey: z.string().min(1, "Project key cannot be empty").describe('Jira project key (e.g., "PROJ", "ENG")'),
756
+ boardId: z.string().optional().describe("Jira board ID for sprint/workflow operations (optional)"),
757
+ transitionMappings: z.record(z.string(), z.string()).optional().describe('Map iloom states to Jira transition names (e.g., {"In Review": "Start Review"})'),
758
+ defaultIssueType: z.string().min(1).optional().describe('Default Jira issue type name for creating issues (e.g., "Task", "Story", "Bug")'),
759
+ defaultSubtaskType: z.string().min(1).optional().describe('Default Jira issue type name for creating subtasks/child issues (e.g., "Subtask", "Sub-task")'),
760
+ doneStatuses: z.array(z.string()).optional().default(["Done"]).describe('Status names to exclude from issue lists (e.g., ["Done", "Closed", "Verify"])')
717
761
  }).optional()
718
762
  }).optional().describe("Issue management configuration"),
719
763
  mergeBehavior: z.object({
@@ -721,6 +765,9 @@ var init_SettingsManager = __esm({
721
765
  remote: z.string().optional(),
722
766
  autoCommitPush: z.boolean().optional().describe(
723
767
  "Auto-commit and push after code review in draft PR mode. Defaults to true when mode is github-draft-pr."
768
+ ),
769
+ openBrowserOnFinish: z.boolean().optional().describe(
770
+ "Open the PR in the default browser after finishing in github-pr or github-draft-pr mode. Use --no-browser flag to override."
724
771
  )
725
772
  }).optional().describe("Merge behavior configuration: local (merge locally), github-pr (create PR), or github-draft-pr (create draft PR at start, mark ready on finish)"),
726
773
  ide: z.object({
@@ -757,19 +804,19 @@ var init_SettingsManager = __esm({
757
804
  const root = this.getProjectRoot(projectRoot);
758
805
  const globalSettings = await this.loadGlobalSettingsFile();
759
806
  const globalSettingsPath = this.getGlobalSettingsPath();
760
- logger.debug(`\u{1F30D} Global settings from ${globalSettingsPath}:`, JSON.stringify(globalSettings, null, 2));
807
+ logger.debug(`\u{1F30D} Global settings from ${globalSettingsPath}:`, JSON.stringify(redactSensitiveFields(globalSettings), null, 2));
761
808
  const baseSettings = await this.loadSettingsFile(root, "settings.json");
762
809
  const baseSettingsPath = path2.join(root, ".iloom", "settings.json");
763
- logger.debug(`\u{1F4C4} Base settings from ${baseSettingsPath}:`, JSON.stringify(baseSettings, null, 2));
810
+ logger.debug(`\u{1F4C4} Base settings from ${baseSettingsPath}:`, JSON.stringify(redactSensitiveFields(baseSettings), null, 2));
764
811
  const localSettings = await this.loadSettingsFile(root, "settings.local.json");
765
812
  const localSettingsPath = path2.join(root, ".iloom", "settings.local.json");
766
- logger.debug(`\u{1F4C4} Local settings from ${localSettingsPath}:`, JSON.stringify(localSettings, null, 2));
813
+ logger.debug(`\u{1F4C4} Local settings from ${localSettingsPath}:`, JSON.stringify(redactSensitiveFields(localSettings), null, 2));
767
814
  let merged = this.mergeSettings(this.mergeSettings(globalSettings, baseSettings), localSettings);
768
- logger.debug("\u{1F504} After merging global + base + local settings:", JSON.stringify(merged, null, 2));
815
+ logger.debug("\u{1F504} After merging global + base + local settings:", JSON.stringify(redactSensitiveFields(merged), null, 2));
769
816
  if (cliOverrides && Object.keys(cliOverrides).length > 0) {
770
- logger.debug("\u2699\uFE0F CLI overrides to apply:", JSON.stringify(cliOverrides, null, 2));
817
+ logger.debug("\u2699\uFE0F CLI overrides to apply:", JSON.stringify(redactSensitiveFields(cliOverrides), null, 2));
771
818
  merged = this.mergeSettings(merged, cliOverrides);
772
- logger.debug("\u{1F504} After applying CLI overrides:", JSON.stringify(merged, null, 2));
819
+ logger.debug("\u{1F504} After applying CLI overrides:", JSON.stringify(redactSensitiveFields(merged), null, 2));
773
820
  }
774
821
  try {
775
822
  const finalSettings = IloomSettingsSchema.parse(merged);
@@ -792,7 +839,7 @@ Note: CLI overrides were applied. Check your --set arguments.`);
792
839
  * Log the final merged configuration for debugging
793
840
  */
794
841
  logFinalConfiguration(settings) {
795
- logger.debug("\u{1F4CB} Final merged configuration:", JSON.stringify(settings, null, 2));
842
+ logger.debug("\u{1F4CB} Final merged configuration:", JSON.stringify(redactSensitiveFields(settings), null, 2));
796
843
  }
797
844
  /**
798
845
  * Load and parse a single settings file
@@ -1253,6 +1300,7 @@ var MetadataManager = class {
1253
1300
  branchName: data.branchName ?? null,
1254
1301
  worktreePath: data.worktreePath ?? null,
1255
1302
  issueType: data.issueType ?? null,
1303
+ issueKey: data.issueKey ?? null,
1256
1304
  issue_numbers: data.issue_numbers ?? [],
1257
1305
  pr_numbers: data.pr_numbers ?? [],
1258
1306
  issueTracker: data.issueTracker ?? null,
@@ -1264,7 +1312,12 @@ var MetadataManager = class {
1264
1312
  draftPrNumber: data.draftPrNumber ?? null,
1265
1313
  oneShot: data.oneShot ?? null,
1266
1314
  capabilities: data.capabilities ?? [],
1267
- parentLoom: data.parentLoom ?? null
1315
+ state: data.state ?? null,
1316
+ childIssueNumbers: data.childIssueNumbers ?? [],
1317
+ parentLoom: data.parentLoom ?? null,
1318
+ childIssues: data.childIssues ?? [],
1319
+ dependencyMap: data.dependencyMap ?? {},
1320
+ mcpConfigPath: data.mcpConfigPath ?? null
1268
1321
  };
1269
1322
  }
1270
1323
  /**
@@ -1317,6 +1370,7 @@ var MetadataManager = class {
1317
1370
  branchName: input.branchName,
1318
1371
  worktreePath: input.worktreePath,
1319
1372
  issueType: input.issueType,
1373
+ ...input.issueKey && { issueKey: input.issueKey },
1320
1374
  issue_numbers: input.issue_numbers,
1321
1375
  pr_numbers: input.pr_numbers,
1322
1376
  issueTracker: input.issueTracker,
@@ -1328,7 +1382,12 @@ var MetadataManager = class {
1328
1382
  capabilities: input.capabilities,
1329
1383
  ...input.draftPrNumber && { draftPrNumber: input.draftPrNumber },
1330
1384
  ...input.oneShot && { oneShot: input.oneShot },
1331
- ...input.parentLoom && { parentLoom: input.parentLoom }
1385
+ ...input.state && { state: input.state },
1386
+ ...input.childIssueNumbers && input.childIssueNumbers.length > 0 && { childIssueNumbers: input.childIssueNumbers },
1387
+ ...input.parentLoom && { parentLoom: input.parentLoom },
1388
+ ...input.childIssues && input.childIssues.length > 0 && { childIssues: input.childIssues },
1389
+ ...input.dependencyMap && Object.keys(input.dependencyMap).length > 0 && { dependencyMap: input.dependencyMap },
1390
+ ...input.mcpConfigPath && { mcpConfigPath: input.mcpConfigPath }
1332
1391
  };
1333
1392
  const filePath = this.getFilePath(worktreePath);
1334
1393
  await fs.writeFile(filePath, JSON.stringify(content, null, 2), { mode: 420 });
@@ -1404,6 +1463,34 @@ var MetadataManager = class {
1404
1463
  }
1405
1464
  return results;
1406
1465
  }
1466
+ /**
1467
+ * Update existing metadata for a worktree by merging new fields
1468
+ *
1469
+ * Reads the existing metadata file, merges the provided updates,
1470
+ * and writes back. Only provided fields are overwritten.
1471
+ *
1472
+ * @param worktreePath - Absolute path to the worktree
1473
+ * @param updates - Partial metadata fields to merge
1474
+ */
1475
+ async updateMetadata(worktreePath, updates) {
1476
+ try {
1477
+ const filePath = this.getFilePath(worktreePath);
1478
+ if (!await fs.pathExists(filePath)) {
1479
+ getLogger().warn(`No metadata file to update for worktree: ${worktreePath}`);
1480
+ return;
1481
+ }
1482
+ const content = await fs.readFile(filePath, "utf8");
1483
+ const data = JSON.parse(content);
1484
+ const merged = { ...data, ...updates };
1485
+ await fs.writeFile(filePath, JSON.stringify(merged, null, 2), { mode: 420 });
1486
+ getLogger().debug(`Metadata updated for worktree: ${worktreePath}`);
1487
+ } catch (error) {
1488
+ getLogger().warn(
1489
+ `Failed to update metadata for worktree: ${error instanceof Error ? error.message : String(error)}`
1490
+ );
1491
+ throw error;
1492
+ }
1493
+ }
1407
1494
  /**
1408
1495
  * Delete metadata for a worktree (spec section 3.3)
1409
1496
  *
@@ -2769,6 +2856,65 @@ async function createIssue(title, body, options) {
2769
2856
  url: issueUrl
2770
2857
  };
2771
2858
  }
2859
+ async function getIssueNodeId(issueNumber, repo) {
2860
+ logger.debug("Fetching GitHub issue node ID", { issueNumber, repo });
2861
+ const args = ["issue", "view", String(issueNumber), "--json", "id"];
2862
+ if (repo) {
2863
+ args.push("--repo", repo);
2864
+ }
2865
+ const result = await executeGhCommand(args);
2866
+ return result.id;
2867
+ }
2868
+ async function getSubIssues(issueNumber, repo) {
2869
+ var _a, _b;
2870
+ logger.debug("Fetching GitHub sub-issues", { issueNumber, repo });
2871
+ const parentNodeId = await getIssueNodeId(issueNumber, repo);
2872
+ const query = `
2873
+ query getSubIssues($parentId: ID!) {
2874
+ node(id: $parentId) {
2875
+ ... on Issue {
2876
+ subIssues(first: 100) {
2877
+ nodes {
2878
+ number
2879
+ title
2880
+ url
2881
+ state
2882
+ }
2883
+ }
2884
+ }
2885
+ }
2886
+ }
2887
+ `;
2888
+ try {
2889
+ const result = await executeGhCommand([
2890
+ "api",
2891
+ "graphql",
2892
+ "-H",
2893
+ "GraphQL-Features: sub_issues",
2894
+ "-f",
2895
+ `query=${query}`,
2896
+ "-F",
2897
+ `parentId=${parentNodeId}`
2898
+ ]);
2899
+ const subIssues = ((_b = (_a = result.data.node) == null ? void 0 : _a.subIssues) == null ? void 0 : _b.nodes) ?? [];
2900
+ return subIssues.map((issue) => ({
2901
+ id: String(issue.number),
2902
+ title: issue.title,
2903
+ url: issue.url,
2904
+ state: issue.state.toLowerCase()
2905
+ }));
2906
+ } catch (error) {
2907
+ if (error instanceof Error) {
2908
+ const errorMessage = error.message;
2909
+ const stderr = "stderr" in error ? error.stderr ?? "" : "";
2910
+ const combinedError = `${errorMessage} ${stderr}`;
2911
+ if (combinedError.includes("sub_issues") || combinedError.includes("null")) {
2912
+ return [];
2913
+ }
2914
+ }
2915
+ throw error;
2916
+ }
2917
+ }
2772
2918
 
2773
2919
  // src/utils/prompt.ts
2774
2920
  init_logger();
@@ -2951,6 +3097,14 @@ var GitHubService = class {
2951
3097
  const issue = await fetchGhIssue(issueNumber, repo);
2952
3098
  return issue.url;
2953
3099
  }
3100
+ async getChildIssues(parentIdentifier, repo) {
3101
+ const issueNum = parseInt(parentIdentifier, 10);
3102
+ if (isNaN(issueNum)) {
3103
+ getLogger().warn(`Invalid GitHub issue number: ${parentIdentifier}`);
3104
+ return [];
3105
+ }
3106
+ return getSubIssues(issueNum, repo);
3107
+ }
2954
3108
  // GitHub Projects integration
2955
3109
  async moveIssueToInProgress(issueNumber) {
2956
3110
  getLogger().info("Moving issue to In Progress in GitHub Projects", {
@@ -2986,7 +3140,48 @@ var GitHubService = class {
2986
3140
  await this.updateIssueStatusInProject(project, issueNumber, owner);
2987
3141
  }
2988
3142
  }
2989
- async updateIssueStatusInProject(project, issueNumber, owner) {
3143
+ // GitHub Projects integration - move to Ready for Review
3144
+ async moveIssueToReadyForReview(issueNumber) {
3145
+ getLogger().info("Moving issue to Ready for Review in GitHub Projects", {
3146
+ issueNumber
3147
+ });
3148
+ if (!await hasProjectScope()) {
3149
+ getLogger().warn("Missing project scope in GitHub CLI auth");
3150
+ throw new GitHubError(
3151
+ "MISSING_SCOPE" /* MISSING_SCOPE */,
3152
+ "GitHub CLI lacks project scope. Run: gh auth refresh -s project"
3153
+ );
3154
+ }
3155
+ let owner;
3156
+ try {
3157
+ const repoInfo = await executeGhCommand(["repo", "view", "--json", "owner,name"]);
3158
+ owner = repoInfo.owner.login;
3159
+ } catch (error) {
3160
+ getLogger().warn("Could not determine repository info", { error });
3161
+ return;
3162
+ }
3163
+ let projects;
3164
+ try {
3165
+ projects = await fetchProjectList(owner);
3166
+ } catch (error) {
3167
+ getLogger().warn("Could not fetch projects", { owner, error });
3168
+ return;
3169
+ }
3170
+ if (!projects.length) {
3171
+ getLogger().warn("No projects found", { owner });
3172
+ return;
3173
+ }
3174
+ for (const project of projects) {
3175
+ await this.updateIssueStatusInProject(
3176
+ project,
3177
+ issueNumber,
3178
+ owner,
3179
+ ["Ready for Review", "In Review", "Review"],
3180
+ "Ready for Review"
3181
+ );
3182
+ }
3183
+ }
3184
+ async updateIssueStatusInProject(project, issueNumber, owner, statusNames = ["In Progress", "In progress"], logLabel = "In Progress") {
2990
3185
  var _a;
2991
3186
  let items;
2992
3187
  try {
@@ -3017,11 +3212,13 @@ var GitHubService = class {
3017
3212
  getLogger().debug("No Status field found in project", { projectNumber: project.number });
3018
3213
  return;
3019
3214
  }
3020
- const inProgressOption = (_a = statusField.options) == null ? void 0 : _a.find(
3021
- (o) => o.name === "In Progress" || o.name === "In progress"
3215
+ const targetOption = (_a = statusField.options) == null ? void 0 : _a.find(
3216
+ (o) => statusNames.some(
3217
+ (name) => o.name.toLowerCase() === name.toLowerCase()
3218
+ )
3022
3219
  );
3023
- if (!inProgressOption) {
3024
- getLogger().debug("No In Progress option found in Status field", { projectNumber: project.number });
3220
+ if (!targetOption) {
3221
+ getLogger().debug(`No ${logLabel} option found in Status field`, { projectNumber: project.number });
3025
3222
  return;
3026
3223
  }
3027
3224
  try {
@@ -3029,16 +3226,21 @@ var GitHubService = class {
3029
3226
  item.id,
3030
3227
  project.id,
3031
3228
  statusField.id,
3032
- inProgressOption.id
3229
+ targetOption.id
3033
3230
  );
3034
3231
  getLogger().info("Updated issue status in project", {
3035
3232
  issueNumber,
3036
- projectNumber: project.number
3233
+ projectNumber: project.number,
3234
+ status: logLabel
3037
3235
  });
3038
3236
  } catch (error) {
3039
3237
  getLogger().debug("Could not update project item", { item: item.id, error });
3040
3238
  }
3041
3239
  }
3240
+ // Identifier normalization - GitHub identifiers are numeric, just stringify
3241
+ normalizeIdentifier(identifier) {
3242
+ return String(identifier);
3243
+ }
3042
3244
  // Utility methods
3043
3245
  extractContext(entity) {
3044
3246
  if ("branch" in entity) {
@@ -3247,6 +3449,35 @@ async function updateLinearIssueState(identifier, stateName) {
3247
3449
  handleLinearError(error, "updateLinearIssueState");
3248
3450
  }
3249
3451
  }
3452
+ async function getLinearChildIssues(identifier, options) {
3453
+ try {
3454
+ logger.debug(`Fetching child issues for Linear issue: ${identifier}`);
3455
+ const client = createLinearClient(options == null ? void 0 : options.apiToken);
3456
+ const issue = await client.issue(identifier);
3457
+ if (!issue) {
3458
+ throw new LinearServiceError("NOT_FOUND", `Linear issue ${identifier} not found`);
3459
+ }
3460
+ const children = await issue.children({ first: 100 });
3461
+ const results = await Promise.all(
3462
+ children.nodes.map(async (child) => {
3463
+ const stateObj = await child.state;
3464
+ const state = (stateObj == null ? void 0 : stateObj.name) ?? "unknown";
3465
+ return {
3466
+ id: child.identifier,
3467
+ title: child.title,
3468
+ url: child.url,
3469
+ state
3470
+ };
3471
+ })
3472
+ );
3473
+ return results;
3474
+ } catch (error) {
3475
+ if (error instanceof LinearServiceError) {
3476
+ throw error;
3477
+ }
3478
+ handleLinearError(error, "getLinearChildIssues");
3479
+ }
3480
+ }
3250
3481
 
3251
3482
  // src/lib/LinearService.ts
3252
3483
  var LinearService = class {
@@ -3360,6 +3591,15 @@ var LinearService = class {
3360
3591
  const issue = await this.fetchIssue(identifier);
3361
3592
  return issue.url;
3362
3593
  }
3594
+ /**
3595
+ * Fetch child issues of a Linear parent issue
3596
+ * @param parentIdentifier - Linear issue identifier (e.g., "ENG-123")
3597
+ * @param _repo - Repository (unused for Linear)
3598
+ * @returns Array of child issues
3599
+ */
3600
+ async getChildIssues(parentIdentifier, _repo) {
3601
+ return getLinearChildIssues(parentIdentifier, this.config.apiToken ? { apiToken: this.config.apiToken } : void 0);
3602
+ }
3363
3603
  /**
3364
3604
  * Move a Linear issue to "In Progress" state
3365
3605
  * @param identifier - Linear issue identifier
@@ -3369,6 +3609,23 @@ var LinearService = class {
3369
3609
  getLogger().info(`Moving Linear issue ${identifier} to In Progress`);
3370
3610
  await updateLinearIssueState(String(identifier), "In Progress");
3371
3611
  }
3612
+ /**
3613
+ * Move a Linear issue to "In Review" state
3614
+ * @param identifier - Linear issue identifier
3615
+ * @throws LinearServiceError if state update fails
3616
+ */
3617
+ async moveIssueToReadyForReview(identifier) {
3618
+ getLogger().info(`Moving Linear issue ${identifier} to In Review`);
3619
+ await updateLinearIssueState(String(identifier), "In Review");
3620
+ }
3621
+ /**
3622
+ * Normalize identifier to canonical form (uppercase for Linear keys)
3623
+ * @param identifier - Linear issue identifier (e.g., "eng-123" or "ENG-123")
3624
+ * @returns Uppercase identifier (e.g., "ENG-123")
3625
+ */
3626
+ normalizeIdentifier(identifier) {
3627
+ return String(identifier).toUpperCase();
3628
+ }
3372
3629
  /**
3373
3630
  * Extract issue context for AI prompts
3374
3631
  * @param entity - Issue (Linear doesn't have PRs)
@@ -3400,6 +3657,844 @@ ${issue.body}`;
3400
3657
  }
3401
3658
  };
3402
3659
 
3660
+ // src/lib/providers/jira/JiraApiClient.ts
3661
+ import https from "https";
3662
+
3663
+ // src/lib/providers/jira/AdfMarkdownConverter.ts
3664
+ import { Parser } from "extended-markdown-adf-parser";
3665
+ var parser = new Parser();
3666
+ function sanitizeCodeMarks(node) {
3667
+ var _a;
3668
+ if ((_a = node.marks) == null ? void 0 : _a.some((mark) => mark.type === "code")) {
3669
+ node.marks = [{ type: "code" }];
3670
+ }
3671
+ if (node.content && Array.isArray(node.content)) {
3672
+ node.content = node.content.map((child) => sanitizeCodeMarks(child));
3673
+ }
3674
+ return node;
3675
+ }
3676
+ var BLOCK_LEVEL_TYPES = /* @__PURE__ */ new Set([
3677
+ "paragraph",
3678
+ "bulletList",
3679
+ "orderedList",
3680
+ "codeBlock",
3681
+ "heading",
3682
+ "blockquote",
3683
+ "rule",
3684
+ "mediaGroup",
3685
+ "nestedExpand",
3686
+ "panel",
3687
+ "table",
3688
+ "taskList",
3689
+ "decisionList",
3690
+ "mediaSingle"
3691
+ ]);
3692
+ function wrapTableCellContent(node) {
3693
+ if (node.content && Array.isArray(node.content)) {
3694
+ node.content = node.content.map((child) => wrapTableCellContent(child));
3695
+ }
3696
+ if (node.type !== "tableCell" && node.type !== "tableHeader") {
3697
+ return node;
3698
+ }
3699
+ if (!node.content || node.content.length === 0) {
3700
+ return node;
3701
+ }
3702
+ const allInline = node.content.every((child) => !BLOCK_LEVEL_TYPES.has(child.type));
3703
+ if (allInline) {
3704
+ node.content = [{ type: "paragraph", content: node.content }];
3705
+ } else {
3706
+ const newContent = [];
3707
+ let inlineRun = [];
3708
+ for (const child of node.content) {
3709
+ if (BLOCK_LEVEL_TYPES.has(child.type)) {
3710
+ if (inlineRun.length > 0) {
3711
+ newContent.push({ type: "paragraph", content: inlineRun });
3712
+ inlineRun = [];
3713
+ }
3714
+ newContent.push(child);
3715
+ } else {
3716
+ inlineRun.push(child);
3717
+ }
3718
+ }
3719
+ if (inlineRun.length > 0) {
3720
+ newContent.push({ type: "paragraph", content: inlineRun });
3721
+ }
3722
+ node.content = newContent;
3723
+ }
3724
+ return node;
3725
+ }
3726
+ var taskIdCounter = 0;
3727
+ function getCanonicalPlainText(text) {
3728
+ const miniAdf = parser.markdownToAdf(text);
3729
+ return getPlainText(miniAdf);
3730
+ }
3731
+ function extractCheckboxBlocks(markdown) {
3732
+ var _a, _b;
3733
+ const lines = markdown.split("\n");
3734
+ const blocks = [];
3735
+ let i = 0;
3736
+ while (i < lines.length) {
3737
+ const bulletLines = [];
3738
+ let blockIndent = null;
3739
+ while (i < lines.length) {
3740
+ const line = lines[i] ?? "";
3741
+ const checkboxMatch = line.match(/^(\s*)[-*+] \[([ xX])\] (.*)$/);
3742
+ if (checkboxMatch) {
3743
+ const indent = ((_a = checkboxMatch[1]) == null ? void 0 : _a.length) ?? 0;
3744
+ if (blockIndent === null) {
3745
+ blockIndent = indent;
3746
+ } else if (indent !== blockIndent) {
3747
+ break;
3748
+ }
3749
+ const state = checkboxMatch[2] === " " ? "TODO" : "DONE";
3750
+ bulletLines.push({ isCheckbox: true, state, rawText: checkboxMatch[3] ?? "" });
3751
+ i++;
3752
+ } else if (line.match(/^\s*[-*+] /)) {
3753
+ const indentMatch = line.match(/^(\s*)/);
3754
+ const indent = ((_b = indentMatch == null ? void 0 : indentMatch[1]) == null ? void 0 : _b.length) ?? 0;
3755
+ if (blockIndent === null) {
3756
+ blockIndent = indent;
3757
+ } else if (indent !== blockIndent) {
3758
+ break;
3759
+ }
3760
+ bulletLines.push({ isCheckbox: false, state: null, rawText: "" });
3761
+ i++;
3762
+ } else if (bulletLines.length > 0 && line.match(/^\s/) && line.trim() !== "") {
3763
+ const lastItem = bulletLines[bulletLines.length - 1];
3764
+ if (lastItem) {
3765
+ lastItem.rawText += "\n" + line.trim();
3766
+ }
3767
+ i++;
3768
+ } else {
3769
+ break;
3770
+ }
3771
+ }
3772
+ if (bulletLines.length > 0) {
3773
+ const allCheckboxes = bulletLines.every((l) => l.isCheckbox);
3774
+ if (allCheckboxes) {
3775
+ blocks.push({
3776
+ states: bulletLines.map((l) => l.state),
3777
+ texts: bulletLines.map((l) => getCanonicalPlainText(l.rawText))
3778
+ });
3779
+ }
3780
+ } else {
3781
+ i++;
3782
+ }
3783
+ }
3784
+ return blocks;
3785
+ }
3786
+ function getPlainText(node) {
3787
+ if (node.type === "text" && node.text !== void 0) return node.text;
3788
+ if (!node.content) return "";
3789
+ return node.content.map(getPlainText).join("");
3790
+ }
3791
+ function convertCheckboxesToTaskList(node, blocks) {
3792
+ const cursor = { index: 0 };
3793
+ return convertCheckboxesRecursive(node, blocks, cursor);
3794
+ }
3795
+ function convertCheckboxesRecursive(node, blocks, cursor) {
3796
+ var _a;
3797
+ if (node.type === "bulletList" && node.content && node.content.length > 0 && cursor.index < blocks.length) {
3798
+ const block = blocks[cursor.index];
3799
+ if (!block) return node;
3800
+ if (node.content.length === block.states.length) {
3801
+ const plaintexts = node.content.map((listItem) => getPlainText(listItem));
3802
+ const matches = plaintexts.every((text, i) => text === block.texts[i]);
3803
+ if (matches) {
3804
+ const allSimple = node.content.every((item) => {
3805
+ var _a2, _b;
3806
+ return ((_a2 = item.content) == null ? void 0 : _a2.length) === 1 && ((_b = item.content[0]) == null ? void 0 : _b.type) === "paragraph";
3807
+ });
3808
+ if (!allSimple) {
3809
+ cursor.index++;
3810
+ return node;
3811
+ }
3812
+ cursor.index++;
3813
+ node.type = "taskList";
3814
+ node.attrs = { localId: `tasklist-${++taskIdCounter}` };
3815
+ for (const [i, listItem] of node.content.entries()) {
3816
+ listItem.type = "taskItem";
3817
+ listItem.attrs = {
3818
+ localId: `task-${++taskIdCounter}`,
3819
+ state: block.states[i]
3820
+ };
3821
+ const firstChild = (_a = listItem.content) == null ? void 0 : _a[0];
3822
+ if ((firstChild == null ? void 0 : firstChild.type) === "paragraph" && firstChild.content) {
3823
+ listItem.content = firstChild.content;
3824
+ }
3825
+ }
3826
+ return node;
3827
+ }
3828
+ }
3829
+ }
3830
+ if (node.content && Array.isArray(node.content)) {
3831
+ node.content = node.content.map((child) => convertCheckboxesRecursive(child, blocks, cursor));
3832
+ }
3833
+ return node;
3834
+ }
3835
+ function convertDetailsToExpandSyntax(markdown) {
3836
+ if (!markdown) return markdown;
3837
+ let previousText = "";
3838
+ let currentText = markdown;
3839
+ while (previousText !== currentText) {
3840
+ previousText = currentText;
3841
+ currentText = currentText.replace(
3842
+ /<details[^>]*>\s*<summary[^>]*>([\s\S]*?)<\/summary>([\s\S]*?)<\/details>/gi,
3843
+ (_match, summary, content) => {
3844
+ const cleanSummary = summary.trim().replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
3845
+ let cleanContent = content.trim();
3846
+ cleanContent = cleanContent.replace(/\n{3,}/g, "\n\n");
3847
+ if (cleanContent) {
3848
+ return `~~~expand title="${cleanSummary}"
3849
+ ${cleanContent}
3850
+ ~~~`;
3851
+ } else {
3852
+ return `~~~expand title="${cleanSummary}"
3853
+ ~~~`;
3854
+ }
3855
+ }
3856
+ );
3857
+ }
3858
+ return currentText;
3859
+ }
3860
+ function adfToMarkdown(adf) {
3861
+ if (!adf) return "";
3862
+ if (typeof adf === "string") return adf;
3863
+ return parser.adfToMarkdown(adf);
3864
+ }
3865
+ function markdownToAdf(markdown) {
3866
+ if (!markdown) {
3867
+ return { type: "doc", version: 1, content: [] };
3868
+ }
3869
+ taskIdCounter = 0;
3870
+ const checkboxBlocks = extractCheckboxBlocks(markdown);
3871
+ const preprocessed = convertDetailsToExpandSyntax(markdown);
3872
+ const adf = parser.markdownToAdf(preprocessed);
3873
+ let result = sanitizeCodeMarks(adf);
3874
+ result = wrapTableCellContent(result);
3875
+ result = convertCheckboxesToTaskList(result, checkboxBlocks);
3876
+ return result;
3877
+ }
3878
+
3879
+ // src/lib/providers/jira/JiraApiClient.ts
3880
+ var JiraApiClient = class {
3881
+ constructor(config) {
3882
+ this.baseUrl = `${config.host.replace(/\/$/, "")}/rest/api/3`;
3883
+ const credentials = Buffer.from(`${config.username}:${config.apiToken}`).toString("base64");
3884
+ this.authHeader = `Basic ${credentials}`;
3885
+ }
3886
+ /**
3887
+ * Make an HTTP request to Jira API
3888
+ */
3889
+ async request(method, endpoint, body) {
3890
+ const url = new URL(`${this.baseUrl}${endpoint}`);
3891
+ getLogger().debug(`Jira API ${method} request`, { url: url.toString() });
3892
+ if (body) {
3893
+ getLogger().debug("Jira API request body", JSON.stringify(body, null, 2));
3894
+ }
3895
+ return new Promise((resolve, reject) => {
3896
+ const options = {
3897
+ hostname: url.hostname,
3898
+ port: url.port || 443,
3899
+ path: url.pathname + url.search,
3900
+ method,
3901
+ headers: {
3902
+ "Authorization": this.authHeader,
3903
+ "Accept": "application/json",
3904
+ "Content-Type": "application/json"
3905
+ }
3906
+ };
3907
+ const req = https.request({ ...options, timeout: 3e4 }, (res) => {
3908
+ const chunks = [];
3909
+ res.on("data", (chunk) => {
3910
+ chunks.push(chunk);
3911
+ });
3912
+ res.on("end", () => {
3913
+ var _a;
3914
+ const data = Buffer.concat(chunks).toString("utf8");
3915
+ if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
3916
+ let errorDetail = data;
3917
+ try {
3918
+ const parsed = JSON.parse(data);
3919
+ const parts = [];
3920
+ if ((_a = parsed.errorMessages) == null ? void 0 : _a.length) {
3921
+ parts.push(`messages: ${parsed.errorMessages.join(", ")}`);
3922
+ }
3923
+ if (parsed.errors && Object.keys(parsed.errors).length) {
3924
+ parts.push(`field errors: ${JSON.stringify(parsed.errors)}`);
3925
+ }
3926
+ if (parts.length) {
3927
+ errorDetail = parts.join("; ");
3928
+ }
3929
+ } catch {
3930
+ }
3931
+ reject(new Error(`Jira API error (${res.statusCode}): ${errorDetail}`));
3932
+ return;
3933
+ }
3934
+ if (res.statusCode === 204 || !data) {
3935
+ resolve({});
3936
+ return;
3937
+ }
3938
+ try {
3939
+ resolve(JSON.parse(data));
3940
+ } catch (error) {
3941
+ reject(new Error(`Failed to parse Jira API response: ${error}`));
3942
+ }
3943
+ });
3944
+ });
3945
+ req.on("timeout", () => {
3946
+ req.destroy();
3947
+ reject(new Error("Jira API request timed out after 30 seconds"));
3948
+ });
3949
+ req.on("error", (error) => {
3950
+ reject(new Error(`Jira API request failed: ${error.message}`));
3951
+ });
3952
+ if (body) {
3953
+ req.write(JSON.stringify(body));
3954
+ }
3955
+ req.end();
3956
+ });
3957
+ }
3958
+ /**
3959
+ * Make a GET request to Jira API
3960
+ */
3961
+ async get(endpoint) {
3962
+ return this.request("GET", endpoint);
3963
+ }
3964
+ /**
3965
+ * Make a POST request to Jira API
3966
+ */
3967
+ async post(endpoint, body) {
3968
+ return this.request("POST", endpoint, body);
3969
+ }
3970
+ /**
3971
+ * Make a PUT request to Jira API
3972
+ */
3973
+ async put(endpoint, body) {
3974
+ return this.request("PUT", endpoint, body);
3975
+ }
3976
+ /**
3977
+ * Make a DELETE request to Jira API
3978
+ */
3979
+ async delete(endpoint) {
3980
+ await this.request("DELETE", endpoint);
3981
+ }
3982
+ /**
3983
+ * Fetch an issue by key (e.g., "PROJ-123")
3984
+ */
3985
+ async getIssue(issueKey) {
3986
+ return this.get(`/issue/${issueKey}`);
3987
+ }
3988
+ /**
3989
+ * Add a comment to an issue
3990
+ * Accepts Markdown content which is converted to ADF for Jira
3991
+ */
3992
+ async addComment(issueKey, body) {
3993
+ const adfBody = markdownToAdf(body);
3994
+ getLogger().debug("Adding comment to Jira issue", { issueKey, bodyLength: body.length });
3995
+ return this.post(`/issue/${issueKey}/comment`, {
3996
+ body: adfBody
3997
+ });
3998
+ }
3999
+ /**
4000
+ * Get all comments for an issue
4001
+ */
4002
+ async getComments(issueKey) {
4003
+ const response = await this.get(`/issue/${issueKey}/comment?maxResults=5000`);
4004
+ if (response.total > response.comments.length) {
4005
+ getLogger().warn(`Comments truncated for issue ${issueKey}: returned ${response.comments.length} of ${response.total} total comments`);
4006
+ }
4007
+ return response.comments;
4008
+ }
4009
+ /**
4010
+ * Update a comment on an issue
4011
+ * Accepts Markdown content which is converted to ADF for Jira
4012
+ */
4013
+ async updateComment(issueKey, commentId, body) {
4014
+ return this.put(`/issue/${issueKey}/comment/${commentId}`, {
4015
+ body: markdownToAdf(body)
4016
+ });
4017
+ }
4018
+ /**
4019
+ * Get available transitions for an issue
4020
+ */
4021
+ async getTransitions(issueKey) {
4022
+ const response = await this.get(`/issue/${issueKey}/transitions`);
4023
+ return response.transitions;
4024
+ }
4025
+ /**
4026
+ * Transition an issue to a new state
4027
+ */
4028
+ async transitionIssue(issueKey, transitionId) {
4029
+ await this.post(`/issue/${issueKey}/transitions`, {
4030
+ transition: {
4031
+ id: transitionId
4032
+ }
4033
+ });
4034
+ }
4035
+ /**
4036
+ * Create a new issue
4037
+ * Accepts Markdown description which is converted to ADF for Jira
4038
+ */
4039
+ async createIssue(projectKey, summary, description, issueType = "Task") {
4040
+ return this.post("/issue", {
4041
+ fields: {
4042
+ project: {
4043
+ key: projectKey
4044
+ },
4045
+ summary,
4046
+ description: markdownToAdf(description),
4047
+ issuetype: {
4048
+ name: issueType
4049
+ }
4050
+ }
4051
+ });
4052
+ }
4053
+ /**
4054
+ * Update an issue's fields (summary, description)
4055
+ * @param issueKey - Jira issue key (e.g., "PROJ-123")
4056
+ * @param fields - Fields to update
4057
+ */
4058
+ async updateIssue(issueKey, fields) {
4059
+ const updateFields = {};
4060
+ if (fields.summary !== void 0) {
4061
+ updateFields.summary = fields.summary;
4062
+ }
4063
+ if (fields.description !== void 0) {
4064
+ updateFields.description = markdownToAdf(fields.description);
4065
+ }
4066
+ await this.put(`/issue/${issueKey}`, { fields: updateFields });
4067
+ }
4068
+ /**
4069
+ * Create an issue with a parent (subtask or child issue)
4070
+ * Accepts Markdown description which is converted to ADF for Jira
4071
+ */
4072
+ async createIssueWithParent(projectKey, summary, description, parentKey, issueType = "Subtask") {
4073
+ return this.post("/issue", {
4074
+ fields: {
4075
+ project: {
4076
+ key: projectKey
4077
+ },
4078
+ summary,
4079
+ description: markdownToAdf(description),
4080
+ issuetype: {
4081
+ name: issueType
4082
+ },
4083
+ parent: {
4084
+ key: parentKey
4085
+ }
4086
+ }
4087
+ });
4088
+ }
4089
+ /**
4090
+ * Create an issue link (dependency/relationship between issues)
4091
+ * @param inwardKey - The issue key for the inward side (e.g., the blocked issue)
4092
+ * @param outwardKey - The issue key for the outward side (e.g., the blocking issue)
4093
+ * @param linkType - The link type name (e.g., "Blocks")
4094
+ */
4095
+ async createIssueLink(inwardKey, outwardKey, linkType) {
4096
+ await this.post("/issueLink", {
4097
+ type: {
4098
+ name: linkType
4099
+ },
4100
+ inwardIssue: {
4101
+ key: inwardKey
4102
+ },
4103
+ outwardIssue: {
4104
+ key: outwardKey
4105
+ }
4106
+ });
4107
+ }
4108
+ /**
4109
+ * Delete an issue link by ID
4110
+ */
4111
+ async deleteIssueLink(linkId) {
4112
+ await this.delete(`/issueLink/${linkId}`);
4113
+ }
4114
+ /**
4115
+ * Search issues using JQL
4116
+ * Automatically paginates through all results up to MAX_SEARCH_RESULTS.
4117
+ */
4118
+ async searchIssues(jql) {
4119
+ const MAX_SEARCH_RESULTS = 5e3;
4120
+ const allIssues = [];
4121
+ let nextPageToken;
4122
+ const maxResults = 100;
4123
+ while (allIssues.length < MAX_SEARCH_RESULTS) {
4124
+ const body = {
4125
+ jql,
4126
+ maxResults,
4127
+ fields: [
4128
+ "summary",
4129
+ "description",
4130
+ "status",
4131
+ "issuetype",
4132
+ "project",
4133
+ "assignee",
4134
+ "reporter",
4135
+ "labels",
4136
+ "created",
4137
+ "updated",
4138
+ "issuelinks",
4139
+ "parent"
4140
+ ]
4141
+ };
4142
+ if (nextPageToken) {
4143
+ body.nextPageToken = nextPageToken;
4144
+ }
4145
+ const response = await this.post(
4146
+ "/search/jql",
4147
+ body
4148
+ );
4149
+ allIssues.push(...response.issues);
4150
+ if (!response.nextPageToken || response.issues.length === 0) {
4151
+ break;
4152
+ }
4153
+ nextPageToken = response.nextPageToken;
4154
+ }
4155
+ if (allIssues.length >= MAX_SEARCH_RESULTS) {
4156
+ getLogger().warn(`Search results truncated at ${MAX_SEARCH_RESULTS} issues. The query matched more results than the safety cap allows.`, { jql, returnedCount: allIssues.length });
4157
+ }
4158
+ return allIssues;
4159
+ }
4160
+ /**
4161
+ * Test connection to Jira API
4162
+ */
4163
+ async testConnection() {
4164
+ try {
4165
+ await this.get("/myself");
4166
+ return true;
4167
+ } catch (error) {
4168
+ const message = error instanceof Error ? error.message : String(error);
4169
+ if (message.includes("Jira API error (401)") || message.includes("Jira API error (403)")) {
4170
+ getLogger().error("Jira connection test failed: authentication error", { error });
4171
+ return false;
4172
+ }
4173
+ throw error;
4174
+ }
4175
+ }
4176
+ };
4177
+
4178
+ // src/lib/providers/jira/JiraIssueTracker.ts
4179
+ var JiraIssueTracker = class {
4180
+ constructor(config, options) {
4181
+ this.providerName = "jira";
4182
+ this.supportsPullRequests = false;
4183
+ this.config = config;
4184
+ this.client = new JiraApiClient({
4185
+ host: config.host,
4186
+ username: config.username,
4187
+ apiToken: config.apiToken
4188
+ });
4189
+ this.prompter = (options == null ? void 0 : options.prompter) ?? promptConfirmation;
4190
+ }
4191
+ /**
4192
+ * Normalize identifier to canonical uppercase form
4193
+ * Jira issue keys are case-sensitive in the API (must be uppercase)
4194
+ */
4195
+ normalizeIdentifier(identifier) {
4196
+ return String(identifier).toUpperCase();
4197
+ }
4198
+ /**
4199
+ * Detect input type from user input
4200
+ * Jira issues follow pattern: PROJECTKEY-123 (case-insensitive)
4201
+ */
4202
+ async detectInputType(input) {
4203
+ const jiraPattern = /^([A-Z][A-Z0-9]+)-(\d+)$/i;
4204
+ const match = input.match(jiraPattern);
4205
+ if (!match) {
4206
+ return { type: "unknown", identifier: null, rawInput: input };
4207
+ }
4208
+ const issueKey = this.normalizeIdentifier(input);
4209
+ getLogger().debug("Checking if input is a Jira issue", { issueKey });
4210
+ try {
4211
+ await this.client.getIssue(issueKey);
4212
+ return { type: "issue", identifier: issueKey, rawInput: input };
4213
+ } catch (error) {
4214
+ if (error instanceof Error && (/404/.test(error.message) || /not found/i.test(error.message))) {
4215
+ getLogger().debug("Issue not found", { issueKey, error });
4216
+ return { type: "unknown", identifier: null, rawInput: input };
4217
+ }
4218
+ throw error;
4219
+ }
4220
+ }
4221
+ /**
4222
+ * Fetch issue details
4223
+ */
4224
+ async fetchIssue(identifier) {
4225
+ const issueKey = this.normalizeIdentifier(identifier);
4226
+ getLogger().debug("Fetching Jira issue", { issueKey });
4227
+ const jiraIssue = await this.client.getIssue(issueKey);
4228
+ return this.mapJiraIssueToIssue(jiraIssue);
4229
+ }
4230
+ /**
4231
+ * Check if issue exists (silent validation)
4232
+ */
4233
+ async isValidIssue(identifier) {
4234
+ try {
4235
+ return await this.fetchIssue(identifier);
4236
+ } catch (error) {
4237
+ if (error instanceof Error && (/404/.test(error.message) || /not found/i.test(error.message))) {
4238
+ getLogger().debug("Issue validation failed: not found", { identifier, error });
4239
+ return false;
4240
+ }
4241
+ throw error;
4242
+ }
4243
+ }
4244
+ /**
4245
+ * Validate issue state
4246
+ * Note: Jira doesn't have a simple "closed" state - depends on workflow
4247
+ */
4248
+ async validateIssueState(issue) {
4249
+ getLogger().debug("Jira issue state", { issueKey: issue.number, state: issue.state });
4250
+ if (issue.state === "closed") {
4251
+ const shouldContinue = await this.prompter(
4252
+ `Issue ${issue.number} is in a completed state. Continue anyway?`
4253
+ );
4254
+ if (!shouldContinue) {
4255
+ throw new Error("User cancelled due to completed issue");
4256
+ }
4257
+ }
4258
+ }
4259
+ /**
4260
+ * Create a new issue
4261
+ */
4262
+ async createIssue(title, body, _repository, _labels) {
4263
+ getLogger().debug("Creating Jira issue", { title, projectKey: this.config.projectKey });
4264
+ const jiraIssue = await this.client.createIssue(
4265
+ this.config.projectKey,
4266
+ title,
4267
+ body,
4268
+ this.config.defaultIssueType
4269
+ );
4270
+ return {
4271
+ number: jiraIssue.key,
4272
+ url: `${this.config.host}/browse/${jiraIssue.key}`
4273
+ };
4274
+ }
4275
+ /**
4276
+ * Get issue URL
4277
+ */
4278
+ async getIssueUrl(identifier) {
4279
+ const issueKey = this.normalizeIdentifier(identifier);
4280
+ return `${this.config.host}/browse/${issueKey}`;
4281
+ }
4282
+ /**
4283
+ * Move issue to "In Progress" state
4284
+ * Uses configured transition mapping or default transition name
4285
+ */
4286
+ async moveIssueToInProgress(identifier) {
4287
+ var _a;
4288
+ const issueKey = this.normalizeIdentifier(identifier);
4289
+ getLogger().debug("Moving Jira issue to In Progress", { issueKey });
4290
+ const transitions = await this.client.getTransitions(issueKey);
4291
+ const transitionName = ((_a = this.config.transitionMappings) == null ? void 0 : _a["In Progress"]) ?? this.findTransitionByName(transitions, ["In Progress", "Start Progress", "Start"]);
4292
+ if (!transitionName) {
4293
+ throw new Error(
4294
+ `Could not find "In Progress" transition for ${issueKey}. Available transitions: ${transitions.map((t) => t.name).join(", ")}. Configure custom mapping in settings.json: issueManagement.jira.transitionMappings`
4295
+ );
4296
+ }
4297
+ const transition = transitions.find((t) => t.name === transitionName);
4298
+ if (!transition) {
4299
+ throw new Error(`Transition "${transitionName}" not found`);
4300
+ }
4301
+ await this.client.transitionIssue(issueKey, transition.id);
4302
+ getLogger().info("Issue transitioned successfully", { issueKey, transition: transitionName });
4303
+ }
4304
+ /**
4305
+ * Move issue to "Ready for Review" state
4306
+ * Uses configured transition mapping or default transition name
4307
+ */
4308
+ async moveIssueToReadyForReview(identifier) {
4309
+ var _a;
4310
+ const issueKey = this.normalizeIdentifier(identifier);
4311
+ getLogger().debug("Moving Jira issue to Ready for Review", { issueKey });
4312
+ const transitions = await this.client.getTransitions(issueKey);
4313
+ const transitionName = ((_a = this.config.transitionMappings) == null ? void 0 : _a["Ready for Review"]) ?? this.findTransitionByName(transitions, ["Ready for Review", "In Review", "Code Review", "Review"]);
4314
+ if (!transitionName) {
4315
+ throw new Error(
4316
+ `Could not find "Ready for Review" transition for ${issueKey}. Available transitions: ${transitions.map((t) => t.name).join(", ")}. Configure custom mapping in settings.json: issueManagement.jira.transitionMappings`
4317
+ );
4318
+ }
4319
+ const transition = transitions.find((t) => t.name === transitionName);
4320
+ if (!transition) {
4321
+ throw new Error(`Transition "${transitionName}" not found`);
4322
+ }
4323
+ await this.client.transitionIssue(issueKey, transition.id);
4324
+ getLogger().info("Issue transitioned to Ready for Review", { issueKey, transition: transitionName });
4325
+ }
4326
+ /**
4327
+ * Close an issue by transitioning to "Done" state
4328
+ * Uses configured transition mapping or default transition names
4329
+ */
4330
+ async closeIssue(identifier) {
4331
+ var _a;
4332
+ const issueKey = this.normalizeIdentifier(identifier);
4333
+ getLogger().debug("Closing Jira issue", { issueKey });
4334
+ const transitions = await this.client.getTransitions(issueKey);
4335
+ const transitionName = ((_a = this.config.transitionMappings) == null ? void 0 : _a["Done"]) ?? this.findTransitionByName(transitions, ["Done", "Close", "Closed", "Resolve", "Resolved"]);
4336
+ if (!transitionName) {
4337
+ throw new Error(
4338
+ `Could not find "Done" transition for ${issueKey}. Available transitions: ${transitions.map((t) => t.name).join(", ")}. Configure custom mapping in settings.json: issueManagement.jira.transitionMappings`
4339
+ );
4340
+ }
4341
+ const transition = transitions.find((t) => t.name === transitionName);
4342
+ if (!transition) {
4343
+ throw new Error(`Transition "${transitionName}" not found`);
4344
+ }
4345
+ await this.client.transitionIssue(issueKey, transition.id);
4346
+ getLogger().info("Issue closed successfully", { issueKey, transition: transitionName });
4347
+ }
4348
+ /**
4349
+ * Reopen an issue by transitioning back to an open state
4350
+ * Uses configured transition mapping or default transition names
4351
+ */
4352
+ async reopenIssue(identifier) {
4353
+ var _a;
4354
+ const issueKey = this.normalizeIdentifier(identifier);
4355
+ getLogger().debug("Reopening Jira issue", { issueKey });
4356
+ const transitions = await this.client.getTransitions(issueKey);
4357
+ const transitionName = ((_a = this.config.transitionMappings) == null ? void 0 : _a["Reopen"]) ?? this.findTransitionByName(transitions, ["Reopen", "To Do", "Open", "Backlog"]);
4358
+ if (!transitionName) {
4359
+ throw new Error(
4360
+ `Could not find "Reopen" transition for ${issueKey}. Available transitions: ${transitions.map((t) => t.name).join(", ")}. Configure custom mapping in settings.json: issueManagement.jira.transitionMappings`
4361
+ );
4362
+ }
4363
+ const transition = transitions.find((t) => t.name === transitionName);
4364
+ if (!transition) {
4365
+ throw new Error(`Transition "${transitionName}" not found`);
4366
+ }
4367
+ await this.client.transitionIssue(issueKey, transition.id);
4368
+ getLogger().info("Issue reopened successfully", { issueKey, transition: transitionName });
4369
+ }
4370
+ /**
4371
+ * Extract context from issue for AI prompts
4372
+ */
4373
+ extractContext(entity) {
4374
+ return `Issue: ${entity.number}
4375
+ Title: ${entity.title}
4376
+ Status: ${entity.state}
4377
+ URL: ${entity.url}
4378
+
4379
+ Description:
4380
+ ${entity.body}
4381
+
4382
+ ${entity.labels.length > 0 ? `Labels: ${entity.labels.join(", ")}` : ""}
4383
+ ${entity.assignees.length > 0 ? `Assignees: ${entity.assignees.join(", ")}` : ""}`;
4384
+ }
4385
+ /**
4386
+ * Fetch child issues of a Jira parent issue using JQL
4387
+ * @param parentIdentifier - Jira issue key (e.g., "PROJ-123")
4388
+ * @param _repo - Repository (unused for Jira)
4389
+ * @returns Array of child issues
4390
+ */
4391
+ async getChildIssues(parentIdentifier, _repo) {
4392
+ const parentKey = this.normalizeIdentifier(parentIdentifier);
4393
+ const jiraKeyPattern = /^[A-Z][A-Z0-9]+-\d+$/;
4394
+ if (!jiraKeyPattern.test(parentKey)) {
4395
+ getLogger().warn(`Invalid Jira issue key format: ${parentKey}`);
4396
+ return [];
4397
+ }
4398
+ const issues = await this.client.searchIssues(`parent = ${parentKey}`);
4399
+ return issues.map((issue) => ({
4400
+ id: issue.key,
4401
+ title: issue.fields.summary,
4402
+ url: `${this.config.host}/browse/${issue.key}`,
4403
+ state: issue.fields.status.name.toLowerCase()
4404
+ }));
4405
+ }
4406
+ /**
4407
+ * Get issue details (alias for fetchIssue for MCP compatibility)
4408
+ */
4409
+ async getIssue(identifier) {
4410
+ return this.fetchIssue(identifier);
4411
+ }
4412
+ /**
4413
+ * Get all comments for an issue
4414
+ */
4415
+ async getComments(identifier) {
4416
+ const issueKey = this.normalizeIdentifier(identifier);
4417
+ getLogger().debug("Fetching Jira comments", { issueKey });
4418
+ const comments = await this.client.getComments(issueKey);
4419
+ return comments.map((comment) => ({
4420
+ id: comment.id,
4421
+ body: adfToMarkdown(comment.body),
4422
+ author: comment.author,
4423
+ createdAt: comment.created,
4424
+ updatedAt: comment.updated
4425
+ }));
4426
+ }
4427
+ /**
4428
+ * Add a comment to an issue
4429
+ */
4430
+ async addComment(identifier, body) {
4431
+ const issueKey = this.normalizeIdentifier(identifier);
4432
+ getLogger().debug("Adding Jira comment", { issueKey });
4433
+ const comment = await this.client.addComment(issueKey, body);
4434
+ return { id: comment.id };
4435
+ }
4436
+ /**
4437
+ * Update an existing comment
4438
+ */
4439
+ async updateComment(identifier, commentId, body) {
4440
+ const issueKey = this.normalizeIdentifier(identifier);
4441
+ getLogger().debug("Updating Jira comment", { issueKey, commentId });
4442
+ await this.client.updateComment(issueKey, commentId, body);
4443
+ }
4444
+ /**
4445
+ * Get the underlying API client (for direct API access by MCP provider)
4446
+ */
4447
+ getApiClient() {
4448
+ return this.client;
4449
+ }
4450
+ /**
4451
+ * Get configuration (for MCP provider)
4452
+ */
4453
+ getConfig() {
4454
+ return this.config;
4455
+ }
4456
+ /**
4457
+ * Map Jira API issue to generic Issue type
4458
+ */
4459
+ mapJiraIssueToIssue(jiraIssue) {
4460
+ const description = adfToMarkdown(jiraIssue.fields.description);
4461
+ return {
4462
+ id: jiraIssue.id,
4463
+ key: jiraIssue.key,
4464
+ number: jiraIssue.key,
4465
+ title: jiraIssue.fields.summary,
4466
+ body: description,
4467
+ state: this.mapJiraStatusToState(jiraIssue.fields.status.name),
4468
+ labels: jiraIssue.fields.labels,
4469
+ assignees: jiraIssue.fields.assignee ? [jiraIssue.fields.assignee.displayName] : [],
4470
+ assignee: jiraIssue.fields.assignee,
4471
+ author: jiraIssue.fields.reporter,
4472
+ url: `${this.config.host}/browse/${jiraIssue.key}`,
4473
+ issueType: jiraIssue.fields.issuetype.name,
4474
+ status: jiraIssue.fields.status.name
4475
+ };
4476
+ }
4477
+ mapJiraStatusToState(statusName) {
4478
+ const normalized = statusName.toLowerCase();
4479
+ const closedStatuses = ["done", "closed", "resolved", "cancelled", "canceled"];
4480
+ return closedStatuses.includes(normalized) ? "closed" : "open";
4481
+ }
4482
+ /**
4483
+ * Find a transition by name, trying multiple possible names
4484
+ */
4485
+ findTransitionByName(transitions, names) {
4486
+ for (const name of names) {
4487
+ const transition = transitions.find(
4488
+ (t) => t.name.toLowerCase() === name.toLowerCase()
4489
+ );
4490
+ if (transition) {
4491
+ return transition.name;
4492
+ }
4493
+ }
4494
+ return null;
4495
+ }
4496
+ };
4497
+
3403
4498
  // src/lib/IssueTrackerFactory.ts
3404
4499
  var IssueTrackerFactory = class {
3405
4500
  /**
@@ -3411,7 +4506,7 @@ var IssueTrackerFactory = class {
3411
4506
  * @throws Error if provider type is not supported
3412
4507
  */
3413
4508
  static create(settings) {
3414
- var _a, _b;
4509
+ var _a, _b, _c;
3415
4510
  const provider = ((_a = settings.issueManagement) == null ? void 0 : _a.provider) ?? "github";
3416
4511
  getLogger().debug(`IssueTrackerFactory: Creating tracker for provider "${provider}"`);
3417
4512
  getLogger().debug(`IssueTrackerFactory: issueManagement settings:`, JSON.stringify(settings.issueManagement, null, 2));
@@ -3434,6 +4529,32 @@ var IssueTrackerFactory = class {
3434
4529
  getLogger().debug(`IssueTrackerFactory: Creating LinearService with config:`, JSON.stringify(linearConfig, null, 2));
3435
4530
  return new LinearService(linearConfig);
3436
4531
  }
4532
+ case "jira": {
4533
+ const jiraSettings = (_c = settings.issueManagement) == null ? void 0 : _c.jira;
4534
+ if (!(jiraSettings == null ? void 0 : jiraSettings.host)) {
4535
+ throw new Error("Jira host is required. Configure issueManagement.jira.host in .iloom/settings.json");
4536
+ }
4537
+ if (!(jiraSettings == null ? void 0 : jiraSettings.username)) {
4538
+ throw new Error("Jira username is required. Configure issueManagement.jira.username in .iloom/settings.json");
4539
+ }
4540
+ if (!(jiraSettings == null ? void 0 : jiraSettings.apiToken)) {
4541
+ throw new Error("Jira API token is required. Configure issueManagement.jira.apiToken in .iloom/settings.local.json");
4542
+ }
4543
+ if (!(jiraSettings == null ? void 0 : jiraSettings.projectKey)) {
4544
+ throw new Error("Jira project key is required. Configure issueManagement.jira.projectKey in .iloom/settings.json");
4545
+ }
4546
+ const jiraConfig = {
4547
+ host: jiraSettings.host,
4548
+ username: jiraSettings.username,
4549
+ apiToken: jiraSettings.apiToken,
4550
+ projectKey: jiraSettings.projectKey
4551
+ };
4552
+ if (jiraSettings.transitionMappings) {
4553
+ jiraConfig.transitionMappings = jiraSettings.transitionMappings;
4554
+ }
4555
+ getLogger().debug(`IssueTrackerFactory: Creating JiraIssueTracker for host: ${jiraSettings.host}`);
4556
+ return new JiraIssueTracker(jiraConfig);
4557
+ }
3437
4558
  default:
3438
4559
  throw new Error(`Unsupported issue tracker provider: ${provider}`);
3439
4560
  }
@@ -3945,7 +5066,7 @@ function parseJsonStreamOutput(output) {
3945
5066
  }
3946
5067
  }
3947
5068
  async function launchClaude(prompt, options = {}) {
3948
- const { model, permissionMode, addDir, headless = false, appendSystemPrompt, mcpConfig, allowedTools, disallowedTools, agents, sessionId, noSessionPersistence, outputFormat, verbose, jsonMode } = options;
5069
+ const { model, permissionMode, addDir, headless = false, appendSystemPrompt, mcpConfig, allowedTools, disallowedTools, agents, sessionId, noSessionPersistence, outputFormat, verbose, jsonMode, env: extraEnv } = options;
3949
5070
  const log = getLogger();
3950
5071
  const args = [];
3951
5072
  if (headless) {
@@ -3989,6 +5110,7 @@ async function launchClaude(prompt, options = {}) {
3989
5110
  if (noSessionPersistence && headless) {
3990
5111
  args.push("--no-session-persistence");
3991
5112
  }
5113
+ const claudeEnv = { ...process.env, CLAUDECODE: "0" };
3992
5114
  try {
3993
5115
  if (headless) {
3994
5116
  const isDebugMode = logger.isDebugEnabled();
@@ -3999,6 +5121,8 @@ async function launchClaude(prompt, options = {}) {
3999
5121
  ...addDir && { cwd: addDir },
4000
5122
  // Run Claude in the worktree directory
4001
5123
  verbose: isDebugMode,
5124
+ env: { ...claudeEnv, ...extraEnv },
5125
+ // CLAUDECODE=0 + any extra env vars
4002
5126
  ...isDebugMode && { stdio: ["pipe", "pipe", "pipe"] }
4003
5127
  // Enable streaming in debug mode
4004
5128
  };
@@ -4055,7 +5179,9 @@ async function launchClaude(prompt, options = {}) {
4055
5179
  // Capture stderr to detect session conflicts
4056
5180
  timeout: 0,
4057
5181
  // Disable timeout
4058
- verbose: logger.isDebugEnabled()
5182
+ verbose: logger.isDebugEnabled(),
5183
+ env: { ...claudeEnv, ...extraEnv }
5184
+ // CLAUDECODE=0 + any extra env vars
4059
5185
  });
4060
5186
  return;
4061
5187
  } catch (interactiveError) {
@@ -4075,7 +5201,8 @@ async function launchClaude(prompt, options = {}) {
4075
5201
  ...addDir && { cwd: addDir },
4076
5202
  stdio: "inherit",
4077
5203
  timeout: 0,
4078
- verbose: logger.isDebugEnabled()
5204
+ verbose: logger.isDebugEnabled(),
5205
+ env: claudeEnv
4079
5206
  });
4080
5207
  return;
4081
5208
  }
@@ -4103,6 +5230,7 @@ async function launchClaude(prompt, options = {}) {
4103
5230
  timeout: 0,
4104
5231
  ...addDir && { cwd: addDir },
4105
5232
  verbose: isDebugMode,
5233
+ env: claudeEnv,
4106
5234
  ...isDebugMode && { stdio: ["pipe", "pipe", "pipe"] }
4107
5235
  };
4108
5236
  const subprocess = execa4("claude", resumeArgs, execaOptions);
@@ -4155,7 +5283,8 @@ async function launchClaude(prompt, options = {}) {
4155
5283
  ...addDir && { cwd: addDir },
4156
5284
  stdio: "inherit",
4157
5285
  timeout: 0,
4158
- verbose: logger.isDebugEnabled()
5286
+ verbose: logger.isDebugEnabled(),
5287
+ env: claudeEnv
4159
5288
  });
4160
5289
  return;
4161
5290
  }
@@ -4330,6 +5459,8 @@ var ClaudeService = class {
4330
5459
  if (port !== void 0) {
4331
5460
  variables.PORT = port;
4332
5461
  }
5462
+ const isVscodeMode = process.env.ILOOM_VSCODE === "1";
5463
+ variables.IS_VSCODE_MODE = isVscodeMode;
4333
5464
  const prompt = await this.templateManager.getPrompt(type, variables);
4334
5465
  const permissionMode = this.getPermissionModeForWorkflow(type);
4335
5466
  if (permissionMode === "bypassPermissions") {
@@ -4395,11 +5526,11 @@ var ClaudeContextManager = class {
4395
5526
  if (!context.workspacePath) {
4396
5527
  throw new Error("Workspace path is required");
4397
5528
  }
4398
- if (context.type === "issue" && typeof context.identifier !== "number") {
4399
- throw new Error("Issue identifier must be a number");
5529
+ if (context.type === "issue" && context.identifier === void 0) {
5530
+ throw new Error("Issue identifier is required");
4400
5531
  }
4401
- if (context.type === "pr" && typeof context.identifier !== "number") {
4402
- throw new Error("PR identifier must be a number");
5532
+ if (context.type === "pr" && context.identifier === void 0) {
5533
+ throw new Error("PR identifier is required");
4403
5534
  }
4404
5535
  logger.debug("Context prepared", { context });
4405
5536
  }