@iloom/cli 0.9.2 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +160 -41
  3. package/dist/{BranchNamingService-K6XNWQ6C.js → BranchNamingService-25KSZAEM.js} +2 -2
  4. package/dist/ClaudeContextManager-66GR4BGM.js +14 -0
  5. package/dist/ClaudeService-7KM5NA5Z.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-TDLZSYG2.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-YOE2SIPG.js} +2 -2
  12. package/dist/README.md +160 -41
  13. package/dist/{SettingsManager-AW3JTJHD.js → SettingsManager-FNKCOZMQ.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-VHGEMXBA.js} +9 -9
  23. package/dist/chunk-4232AHNQ.js +35 -0
  24. package/dist/chunk-4232AHNQ.js.map +1 -0
  25. package/dist/chunk-4E7LCFUG.js +24 -0
  26. package/dist/chunk-4E7LCFUG.js.map +1 -0
  27. package/dist/{chunk-AR5QKYNE.js → chunk-4FGEGQW4.js} +4 -4
  28. package/dist/{chunk-R4YWBGY6.js → chunk-5FJWO4IT.js} +67 -22
  29. package/dist/chunk-5FJWO4IT.js.map +1 -0
  30. package/dist/{chunk-VPTAX5TR.js → chunk-5RPBYK5Q.js} +35 -30
  31. package/dist/chunk-5RPBYK5Q.js.map +1 -0
  32. package/dist/{chunk-YKFCCV6S.js → chunk-63QWFWH3.js} +7 -7
  33. package/dist/chunk-63QWFWH3.js.map +1 -0
  34. package/dist/{chunk-RI2YL6TK.js → chunk-7VHJNVLF.js} +80 -23
  35. package/dist/chunk-7VHJNVLF.js.map +1 -0
  36. package/dist/{chunk-B7U6OKUR.js → chunk-C6HNNJIV.js} +11 -3
  37. package/dist/chunk-C6HNNJIV.js.map +1 -0
  38. package/dist/{chunk-A7NJF73J.js → chunk-CVCTIDDK.js} +4 -4
  39. package/dist/{chunk-Z2TWEXR7.js → chunk-E6KOWMKA.js} +6 -6
  40. package/dist/chunk-E6KOWMKA.js.map +1 -0
  41. package/dist/{chunk-3I4ONZRT.js → chunk-EVPZFV3K.js} +10 -10
  42. package/dist/chunk-EVPZFV3K.js.map +1 -0
  43. package/dist/{chunk-IZIYLYPK.js → chunk-G5V75JD5.js} +2 -2
  44. package/dist/chunk-GRISNU6G.js +651 -0
  45. package/dist/chunk-GRISNU6G.js.map +1 -0
  46. package/dist/chunk-HEXKPKCK.js +1396 -0
  47. package/dist/chunk-HEXKPKCK.js.map +1 -0
  48. package/dist/{chunk-TC7APDKU.js → chunk-I5T677EA.js} +2 -2
  49. package/dist/{chunk-KBEIQP4G.js → chunk-KB64WNBZ.js} +43 -3
  50. package/dist/chunk-KB64WNBZ.js.map +1 -0
  51. package/dist/{chunk-NWMORW3U.js → chunk-KIK2ZFAL.js} +2 -2
  52. package/dist/{chunk-CWRI4JC3.js → chunk-KKV5WH5M.js} +30 -31
  53. package/dist/chunk-KKV5WH5M.js.map +1 -0
  54. package/dist/{chunk-DGG2VY7B.js → chunk-KVHIAWVT.js} +9 -9
  55. package/dist/chunk-KVHIAWVT.js.map +1 -0
  56. package/dist/{chunk-OFDN5NKS.js → chunk-KXDRI47U.js} +69 -12
  57. package/dist/chunk-KXDRI47U.js.map +1 -0
  58. package/dist/{chunk-NUACL52E.js → chunk-LLHXQS3C.js} +2 -2
  59. package/dist/chunk-LUKXJSRI.js +73 -0
  60. package/dist/chunk-LUKXJSRI.js.map +1 -0
  61. package/dist/{chunk-TL72BGP6.js → chunk-MORRVYPT.js} +2 -2
  62. package/dist/chunk-OTGH2HRS.js +1427 -0
  63. package/dist/chunk-OTGH2HRS.js.map +1 -0
  64. package/dist/{chunk-7ZEHSSUP.js → chunk-P4O6EH46.js} +4 -4
  65. package/dist/{chunk-KAYXR544.js → chunk-QVLPWNE3.js} +2 -2
  66. package/dist/chunk-QZWEJVWV.js +207 -0
  67. package/dist/chunk-QZWEJVWV.js.map +1 -0
  68. package/dist/chunk-RJ3VBUFK.js +781 -0
  69. package/dist/chunk-RJ3VBUFK.js.map +1 -0
  70. package/dist/chunk-RSYT7MVI.js +202 -0
  71. package/dist/chunk-RSYT7MVI.js.map +1 -0
  72. package/dist/{chunk-6IIL5M2L.js → chunk-S7PZA6IV.js} +10 -8
  73. package/dist/{chunk-6IIL5M2L.js.map → chunk-S7PZA6IV.js.map} +1 -1
  74. package/dist/chunk-SKSYYBCU.js +229 -0
  75. package/dist/chunk-SKSYYBCU.js.map +1 -0
  76. package/dist/{chunk-ULSWCPQG.js → chunk-SWSJWA2S.js} +476 -5
  77. package/dist/chunk-SWSJWA2S.js.map +1 -0
  78. package/dist/{chunk-KXGQYLFZ.js → chunk-UKBAJ2QQ.js} +61 -7
  79. package/dist/chunk-UKBAJ2QQ.js.map +1 -0
  80. package/dist/{chunk-FO5GGFOV.js → chunk-UR5DGNUO.js} +71 -9
  81. package/dist/chunk-UR5DGNUO.js.map +1 -0
  82. package/dist/{chunk-QN47QVBX.js → chunk-UUEW5KWB.js} +1 -1
  83. package/dist/chunk-UUEW5KWB.js.map +1 -0
  84. package/dist/{chunk-4CO6KG5S.js → chunk-VG45TUYK.js} +53 -7
  85. package/dist/{chunk-4CO6KG5S.js.map → chunk-VG45TUYK.js.map} +1 -1
  86. package/dist/{chunk-4LKGCFGG.js → chunk-WWKOVDWC.js} +2 -2
  87. package/dist/{chunk-KJTVU3HZ.js → chunk-WXIM2WS7.js} +8 -8
  88. package/dist/chunk-WXIM2WS7.js.map +1 -0
  89. package/dist/{chunk-VOGGLPG5.js → chunk-YQ57ORTV.js} +14 -1
  90. package/dist/chunk-YQ57ORTV.js.map +1 -0
  91. package/dist/{chunk-SOSQILHO.js → chunk-ZNMPGMHY.js} +44 -797
  92. package/dist/chunk-ZNMPGMHY.js.map +1 -0
  93. package/dist/{claude-TP2QO3BU.js → claude-7GGEWVEM.js} +2 -2
  94. package/dist/{cleanup-PJRIFFU4.js → cleanup-6PVAC4NI.js} +85 -34
  95. package/dist/cleanup-6PVAC4NI.js.map +1 -0
  96. package/dist/cli.js +630 -801
  97. package/dist/cli.js.map +1 -1
  98. package/dist/{commit-IVP3M4HG.js → commit-FZR5XDQG.js} +26 -23
  99. package/dist/commit-FZR5XDQG.js.map +1 -0
  100. package/dist/{compile-R2J65HBQ.js → compile-7ALJHZ4N.js} +9 -9
  101. package/dist/{contribute-VDZXHK5Y.js → contribute-5GKLK3BQ.js} +14 -6
  102. package/dist/contribute-5GKLK3BQ.js.map +1 -0
  103. package/dist/{dev-server-7F622OEO.js → dev-server-7SMIB7OF.js} +29 -15
  104. package/dist/dev-server-7SMIB7OF.js.map +1 -0
  105. package/dist/{feedback-E7VET7CL.js → feedback-G2GJFN2F.js} +18 -16
  106. package/dist/{feedback-E7VET7CL.js.map → feedback-G2GJFN2F.js.map} +1 -1
  107. package/dist/{git-2QDQ2X2S.js → git-GTLKAZRJ.js} +4 -4
  108. package/dist/hooks/iloom-hook.js +15 -0
  109. package/dist/ignite-H2O5Y5A2.js +34 -0
  110. package/dist/ignite-H2O5Y5A2.js.map +1 -0
  111. package/dist/index.d.ts +482 -58
  112. package/dist/index.js +1340 -44
  113. package/dist/index.js.map +1 -1
  114. package/dist/{init-676DHF6R.js → init-32YOKXRL.js} +57 -21
  115. package/dist/init-32YOKXRL.js.map +1 -0
  116. package/dist/{issues-PJSOLOBJ.js → issues-4UUAQ5K6.js} +61 -20
  117. package/dist/issues-4UUAQ5K6.js.map +1 -0
  118. package/dist/{lint-CJM7BAIM.js → lint-AAN2NZWG.js} +9 -9
  119. package/dist/mcp/harness-server.js +140 -0
  120. package/dist/mcp/harness-server.js.map +1 -0
  121. package/dist/mcp/issue-management-server.js +2599 -262
  122. package/dist/mcp/issue-management-server.js.map +1 -1
  123. package/dist/mcp/recap-server.js +144 -21
  124. package/dist/mcp/recap-server.js.map +1 -1
  125. package/dist/{neon-helpers-VVFFTLXE.js → neon-helpers-CQN2PB4S.js} +3 -3
  126. package/dist/neon-helpers-CQN2PB4S.js.map +1 -0
  127. package/dist/{open-544H7JF5.js → open-FXWW3VI4.js} +15 -15
  128. package/dist/open-FXWW3VI4.js.map +1 -0
  129. package/dist/{plan-Q7ELXDLC.js → plan-RQ5FPIGF.js} +358 -40
  130. package/dist/plan-RQ5FPIGF.js.map +1 -0
  131. package/dist/{projects-LH362JZQ.js → projects-2UOXFLNZ.js} +4 -4
  132. package/dist/prompts/CLAUDE.md +62 -0
  133. package/dist/prompts/init-prompt.txt +430 -34
  134. package/dist/prompts/issue-prompt.txt +473 -54
  135. package/dist/prompts/plan-prompt.txt +140 -19
  136. package/dist/prompts/pr-prompt.txt +44 -1
  137. package/dist/prompts/regular-prompt.txt +42 -1
  138. package/dist/prompts/session-summary-prompt.txt +14 -0
  139. package/dist/prompts/swarm-orchestrator-prompt.txt +464 -0
  140. package/dist/{rebase-YND35CIE.js → rebase-6NVLX5V7.js} +21 -12
  141. package/dist/rebase-6NVLX5V7.js.map +1 -0
  142. package/dist/{recap-3W7COH7D.js → recap-OMBOKJST.js} +47 -19
  143. package/dist/recap-OMBOKJST.js.map +1 -0
  144. package/dist/{run-QUXJKDQQ.js → run-BBXLRIZB.js} +15 -15
  145. package/dist/run-BBXLRIZB.js.map +1 -0
  146. package/dist/schema/package-iloom.schema.json +58 -0
  147. package/dist/schema/settings.schema.json +149 -15
  148. package/dist/{shell-QGECBLST.js → shell-RF7LTND5.js} +14 -7
  149. package/dist/shell-RF7LTND5.js.map +1 -0
  150. package/dist/{summary-G2T4452H.js → summary-WTQZ7XG2.js} +27 -25
  151. package/dist/summary-WTQZ7XG2.js.map +1 -0
  152. package/dist/{test-EA5NQFDC.js → test-SGO6I5Z7.js} +9 -9
  153. package/dist/{test-git-M7LSLEFL.js → test-git-XM4TM65W.js} +4 -4
  154. package/dist/test-jira-LDTOYFSD.js +96 -0
  155. package/dist/test-jira-LDTOYFSD.js.map +1 -0
  156. package/dist/{test-prefix-64NAAUON.js → test-prefix-GBO37XCN.js} +4 -4
  157. package/dist/{test-webserver-OK6Z5FJM.js → test-webserver-NZ3JTVLL.js} +6 -6
  158. package/dist/{vscode-AR5NNXXI.js → vscode-6XUGHJKL.js} +7 -7
  159. package/package.json +5 -1
  160. package/dist/ClaudeContextManager-HR5JQKAI.js +0 -14
  161. package/dist/ClaudeService-TK7FMC2X.js +0 -13
  162. package/dist/chunk-3I4ONZRT.js.map +0 -1
  163. package/dist/chunk-B7U6OKUR.js.map +0 -1
  164. package/dist/chunk-CWRI4JC3.js.map +0 -1
  165. package/dist/chunk-DGG2VY7B.js.map +0 -1
  166. package/dist/chunk-FJDRTVJX.js +0 -520
  167. package/dist/chunk-FJDRTVJX.js.map +0 -1
  168. package/dist/chunk-FO5GGFOV.js.map +0 -1
  169. package/dist/chunk-KBEIQP4G.js.map +0 -1
  170. package/dist/chunk-KJTVU3HZ.js.map +0 -1
  171. package/dist/chunk-KXGQYLFZ.js.map +0 -1
  172. package/dist/chunk-OFDN5NKS.js.map +0 -1
  173. package/dist/chunk-QN47QVBX.js.map +0 -1
  174. package/dist/chunk-R4YWBGY6.js.map +0 -1
  175. package/dist/chunk-RI2YL6TK.js.map +0 -1
  176. package/dist/chunk-SOSQILHO.js.map +0 -1
  177. package/dist/chunk-ULSWCPQG.js.map +0 -1
  178. package/dist/chunk-VOGGLPG5.js.map +0 -1
  179. package/dist/chunk-VPTAX5TR.js.map +0 -1
  180. package/dist/chunk-W6DP5RVR.js +0 -101
  181. package/dist/chunk-W6DP5RVR.js.map +0 -1
  182. package/dist/chunk-WHI5KEOX.js +0 -121
  183. package/dist/chunk-WHI5KEOX.js.map +0 -1
  184. package/dist/chunk-YKFCCV6S.js.map +0 -1
  185. package/dist/chunk-Z2TWEXR7.js.map +0 -1
  186. package/dist/cleanup-PJRIFFU4.js.map +0 -1
  187. package/dist/commit-IVP3M4HG.js.map +0 -1
  188. package/dist/contribute-VDZXHK5Y.js.map +0 -1
  189. package/dist/dev-server-7F622OEO.js.map +0 -1
  190. package/dist/ignite-IW35CDBD.js +0 -784
  191. package/dist/ignite-IW35CDBD.js.map +0 -1
  192. package/dist/init-676DHF6R.js.map +0 -1
  193. package/dist/issues-PJSOLOBJ.js.map +0 -1
  194. package/dist/open-544H7JF5.js.map +0 -1
  195. package/dist/plan-Q7ELXDLC.js.map +0 -1
  196. package/dist/rebase-YND35CIE.js.map +0 -1
  197. package/dist/recap-3W7COH7D.js.map +0 -1
  198. package/dist/run-QUXJKDQQ.js.map +0 -1
  199. package/dist/shell-QGECBLST.js.map +0 -1
  200. package/dist/summary-G2T4452H.js.map +0 -1
  201. /package/dist/{BranchNamingService-K6XNWQ6C.js.map → BranchNamingService-25KSZAEM.js.map} +0 -0
  202. /package/dist/{ClaudeContextManager-HR5JQKAI.js.map → ClaudeContextManager-66GR4BGM.js.map} +0 -0
  203. /package/dist/{ClaudeService-TK7FMC2X.js.map → ClaudeService-7KM5NA5Z.js.map} +0 -0
  204. /package/dist/{GitHubService-TGWJN4V4.js.map → GitHubService-MEHKHUQP.js.map} +0 -0
  205. /package/dist/{MetadataManager-W3C54UYT.js.map → IssueTrackerFactory-NG53YX5S.js.map} +0 -0
  206. /package/dist/{LoomLauncher-73NXL2CL.js.map → LoomLauncher-TDLZSYG2.js.map} +0 -0
  207. /package/dist/{ProjectCapabilityDetector-N5L7T4IY.js.map → MetadataManager-5QZSTKNN.js.map} +0 -0
  208. /package/dist/{PromptTemplateManager-36YLQRHP.js.map → ProjectCapabilityDetector-5KSYUTBJ.js.map} +0 -0
  209. /package/dist/{SettingsManager-AW3JTJHD.js.map → PromptTemplateManager-YOE2SIPG.js.map} +0 -0
  210. /package/dist/{claude-TP2QO3BU.js.map → SettingsManager-FNKCOZMQ.js.map} +0 -0
  211. /package/dist/{build-THZI572G.js.map → build-VHGEMXBA.js.map} +0 -0
  212. /package/dist/{chunk-AR5QKYNE.js.map → chunk-4FGEGQW4.js.map} +0 -0
  213. /package/dist/{chunk-A7NJF73J.js.map → chunk-CVCTIDDK.js.map} +0 -0
  214. /package/dist/{chunk-IZIYLYPK.js.map → chunk-G5V75JD5.js.map} +0 -0
  215. /package/dist/{chunk-TC7APDKU.js.map → chunk-I5T677EA.js.map} +0 -0
  216. /package/dist/{chunk-NWMORW3U.js.map → chunk-KIK2ZFAL.js.map} +0 -0
  217. /package/dist/{chunk-NUACL52E.js.map → chunk-LLHXQS3C.js.map} +0 -0
  218. /package/dist/{chunk-TL72BGP6.js.map → chunk-MORRVYPT.js.map} +0 -0
  219. /package/dist/{chunk-7ZEHSSUP.js.map → chunk-P4O6EH46.js.map} +0 -0
  220. /package/dist/{chunk-KAYXR544.js.map → chunk-QVLPWNE3.js.map} +0 -0
  221. /package/dist/{chunk-4LKGCFGG.js.map → chunk-WWKOVDWC.js.map} +0 -0
  222. /package/dist/{git-2QDQ2X2S.js.map → claude-7GGEWVEM.js.map} +0 -0
  223. /package/dist/{compile-R2J65HBQ.js.map → compile-7ALJHZ4N.js.map} +0 -0
  224. /package/dist/{neon-helpers-VVFFTLXE.js.map → git-GTLKAZRJ.js.map} +0 -0
  225. /package/dist/{lint-CJM7BAIM.js.map → lint-AAN2NZWG.js.map} +0 -0
  226. /package/dist/{projects-LH362JZQ.js.map → projects-2UOXFLNZ.js.map} +0 -0
  227. /package/dist/{test-EA5NQFDC.js.map → test-SGO6I5Z7.js.map} +0 -0
  228. /package/dist/{test-git-M7LSLEFL.js.map → test-git-XM4TM65W.js.map} +0 -0
  229. /package/dist/{test-prefix-64NAAUON.js.map → test-prefix-GBO37XCN.js.map} +0 -0
  230. /package/dist/{test-webserver-OK6Z5FJM.js.map → test-webserver-NZ3JTVLL.js.map} +0 -0
  231. /package/dist/{vscode-AR5NNXXI.js.map → vscode-6XUGHJKL.js.map} +0 -0
@@ -0,0 +1,1427 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ buildDependencyMap,
4
+ fetchChildIssueDetails
5
+ } from "./chunk-QZWEJVWV.js";
6
+ import {
7
+ IssueManagementProviderFactory
8
+ } from "./chunk-SWSJWA2S.js";
9
+ import {
10
+ getWorkspacePort
11
+ } from "./chunk-LLHXQS3C.js";
12
+ import {
13
+ installDependencies
14
+ } from "./chunk-WWKOVDWC.js";
15
+ import {
16
+ TelemetryService
17
+ } from "./chunk-RSYT7MVI.js";
18
+ import {
19
+ GitWorktreeManager
20
+ } from "./chunk-I5T677EA.js";
21
+ import {
22
+ FirstRunManager
23
+ } from "./chunk-Q7POFB5Q.js";
24
+ import {
25
+ generateAndWriteMcpConfigFile,
26
+ generateIssueManagementMcpConfig,
27
+ generateRecapMcpConfig
28
+ } from "./chunk-SKSYYBCU.js";
29
+ import {
30
+ AgentManager
31
+ } from "./chunk-C6HNNJIV.js";
32
+ import {
33
+ launchClaude
34
+ } from "./chunk-UR5DGNUO.js";
35
+ import {
36
+ PromptTemplateManager,
37
+ buildReviewTemplateVariables
38
+ } from "./chunk-UUEW5KWB.js";
39
+ import {
40
+ extractSettingsOverrides
41
+ } from "./chunk-GYCR2LOU.js";
42
+ import {
43
+ extractIssueNumber,
44
+ findMainWorktreePathWithSettings,
45
+ generateWorktreePath,
46
+ getWorktreeRoot,
47
+ isValidGitRepo
48
+ } from "./chunk-4FGEGQW4.js";
49
+ import {
50
+ SettingsManager
51
+ } from "./chunk-7VHJNVLF.js";
52
+ import {
53
+ MetadataManager
54
+ } from "./chunk-KB64WNBZ.js";
55
+ import {
56
+ IssueTrackerFactory
57
+ } from "./chunk-UKBAJ2QQ.js";
58
+ import {
59
+ getLogger,
60
+ withLogger
61
+ } from "./chunk-6MLEBAYZ.js";
62
+ import {
63
+ createStderrLogger,
64
+ logger
65
+ } from "./chunk-VT4PDUYT.js";
66
+
67
+ // src/commands/ignite.ts
68
+ import path4 from "path";
69
+ import fs4 from "fs-extra";
70
+ import { readFile } from "fs/promises";
71
+
72
+ // src/lib/ClaudeHookManager.ts
73
+ import os from "os";
74
+ import path from "path";
75
+ import fs from "fs-extra";
76
+ import { parse, modify, applyEdits } from "jsonc-parser";
77
+ import { fileURLToPath } from "url";
78
+ import { accessSync } from "fs";
79
+ var ClaudeHookManager = class {
80
+ constructor() {
81
+ this.claudeDir = path.join(os.homedir(), ".claude");
82
+ this.hooksDir = path.join(this.claudeDir, "hooks");
83
+ this.settingsPath = path.join(this.claudeDir, "settings.json");
84
+ const currentFileUrl = import.meta.url;
85
+ const currentFilePath = fileURLToPath(currentFileUrl);
86
+ const distDir = path.dirname(currentFilePath);
87
+ let templateDir = path.join(distDir, "hooks");
88
+ let currentDir = distDir;
89
+ while (currentDir !== path.dirname(currentDir)) {
90
+ const candidatePath = path.join(currentDir, "hooks");
91
+ try {
92
+ accessSync(candidatePath);
93
+ templateDir = candidatePath;
94
+ break;
95
+ } catch {
96
+ currentDir = path.dirname(currentDir);
97
+ }
98
+ }
99
+ this.templateDir = templateDir;
100
+ logger.debug("ClaudeHookManager initialized", {
101
+ claudeDir: this.claudeDir,
102
+ hooksDir: this.hooksDir,
103
+ settingsPath: this.settingsPath,
104
+ templateDir: this.templateDir
105
+ });
106
+ }
107
+ /**
108
+ * Install Claude hooks for VSCode integration
109
+ *
110
+ * This is idempotent - safe to call on every spin.
111
+ * Installs hook script to ~/.claude/hooks/ and merges
112
+ * hook configuration into ~/.claude/settings.json
113
+ */
114
+ async installHooks() {
115
+ try {
116
+ await fs.ensureDir(this.hooksDir);
117
+ await this.installHookScript();
118
+ await this.mergeHookConfig();
119
+ logger.debug("Claude hooks installed successfully");
120
+ } catch (error) {
121
+ logger.warn(
122
+ `Failed to install Claude hooks: ${error instanceof Error ? error.message : "Unknown error"}`
123
+ );
124
+ }
125
+ }
126
+ /**
127
+ * Check if hooks are already installed
128
+ */
129
+ async isHooksInstalled() {
130
+ try {
131
+ const hookScriptPath = path.join(this.hooksDir, "iloom-hook.js");
132
+ if (!await fs.pathExists(hookScriptPath)) {
133
+ return false;
134
+ }
135
+ if (!await fs.pathExists(this.settingsPath)) {
136
+ return false;
137
+ }
138
+ const content = await fs.readFile(this.settingsPath, "utf8");
139
+ const errors = [];
140
+ const settings = parse(content, errors, { allowTrailingComma: true });
141
+ if (errors.length > 0 || !(settings == null ? void 0 : settings.hooks)) {
142
+ return false;
143
+ }
144
+ return Array.isArray(settings.hooks.SessionStart);
145
+ } catch {
146
+ return false;
147
+ }
148
+ }
149
+ /**
150
+ * Install the hook script from bundled templates
151
+ * Skips write if destination already has identical content
152
+ */
153
+ async installHookScript() {
154
+ const sourcePath = path.join(this.templateDir, "iloom-hook.js");
155
+ const destPath = path.join(this.hooksDir, "iloom-hook.js");
156
+ if (!await fs.pathExists(sourcePath)) {
157
+ throw new Error(`Hook template not found at ${sourcePath}`);
158
+ }
159
+ if (await fs.pathExists(destPath)) {
160
+ const [sourceContent, destContent] = await Promise.all([
161
+ fs.readFile(sourcePath, "utf8"),
162
+ fs.readFile(destPath, "utf8")
163
+ ]);
164
+ if (sourceContent === destContent) {
165
+ logger.debug("Hook script already up to date, skipping");
166
+ return;
167
+ }
168
+ }
169
+ await fs.copyFile(sourcePath, destPath);
170
+ logger.debug("Hook script installed", { sourcePath, destPath });
171
+ }
172
+ /**
173
+ * Merge hook configuration into settings.json
174
+ * Preserves existing user hooks and comments
175
+ */
176
+ async mergeHookConfig() {
177
+ var _a, _b;
178
+ await fs.ensureDir(this.claudeDir);
179
+ let existingContent = "{}";
180
+ let existingSettings = {};
181
+ if (await fs.pathExists(this.settingsPath)) {
182
+ existingContent = await fs.readFile(this.settingsPath, "utf8");
183
+ const errors = [];
184
+ existingSettings = parse(existingContent, errors, { allowTrailingComma: true });
185
+ if (errors.length > 0) {
186
+ logger.warn("Existing settings.json has parse errors, will attempt to merge anyway");
187
+ }
188
+ }
189
+ const ourHooks = this.getHookConfig();
190
+ const mergedHooks = { ...existingSettings.hooks ?? {} };
191
+ let hooksAdded = false;
192
+ for (const [eventName, eventConfigs] of Object.entries(ourHooks)) {
193
+ const existing = mergedHooks[eventName] ?? [];
194
+ const ourConfig = eventConfigs[0];
195
+ const ourCommand = (_b = (_a = ourConfig == null ? void 0 : ourConfig.hooks) == null ? void 0 : _a[0]) == null ? void 0 : _b.command;
196
+ const existingConfigIndex = existing.findIndex(
197
+ (config) => {
198
+ var _a2;
199
+ return (_a2 = config.hooks) == null ? void 0 : _a2.some((h) => h.command === ourCommand);
200
+ }
201
+ );
202
+ if (existingConfigIndex === -1) {
203
+ mergedHooks[eventName] = [...existing, ...eventConfigs];
204
+ hooksAdded = true;
205
+ } else {
206
+ const existingConfig = existing[existingConfigIndex];
207
+ const ourMatcher = ourConfig == null ? void 0 : ourConfig.matcher;
208
+ if (existingConfig && ourMatcher !== void 0 && existingConfig.matcher !== ourMatcher) {
209
+ existing[existingConfigIndex] = {
210
+ ...existingConfig,
211
+ matcher: ourMatcher
212
+ };
213
+ hooksAdded = true;
214
+ }
215
+ }
216
+ }
217
+ if (!hooksAdded) {
218
+ logger.debug("All hooks already registered, skipping settings.json update");
219
+ return;
220
+ }
221
+ let content;
222
+ if (existingContent.includes("//") || existingContent.includes("/*")) {
223
+ let modifiedContent = existingContent;
224
+ const edits = modify(modifiedContent, ["hooks"], mergedHooks, {});
225
+ content = applyEdits(modifiedContent, edits);
226
+ } else {
227
+ const updatedSettings = {
228
+ ...existingSettings,
229
+ hooks: mergedHooks
230
+ };
231
+ content = JSON.stringify(updatedSettings, null, 2) + "\n";
232
+ }
233
+ const tempPath = `${this.settingsPath}.tmp`;
234
+ await fs.writeFile(tempPath, content, "utf8");
235
+ await fs.rename(tempPath, this.settingsPath);
236
+ logger.debug("Hook configuration merged into settings.json");
237
+ }
238
+ /**
239
+ * Get the hook configuration to register
240
+ *
241
+ * Each event maps to a hook that runs iloom-hook.js
242
+ */
243
+ getHookConfig() {
244
+ const hookCommand = `node ${path.join(this.hooksDir, "iloom-hook.js")}`;
245
+ return {
246
+ Notification: [
247
+ { hooks: [{ type: "command", command: hookCommand }] }
248
+ ],
249
+ Stop: [
250
+ { hooks: [{ type: "command", command: hookCommand }] }
251
+ ],
252
+ SubagentStop: [
253
+ { hooks: [{ type: "command", command: hookCommand }] }
254
+ ],
255
+ PermissionRequest: [
256
+ { matcher: "*", hooks: [{ type: "command", command: hookCommand, timeout: 86400 }] }
257
+ ],
258
+ PreToolUse: [
259
+ { matcher: "*", hooks: [{ type: "command", command: hookCommand }] }
260
+ ],
261
+ PostToolUse: [
262
+ { matcher: "*", hooks: [{ type: "command", command: hookCommand }] }
263
+ ],
264
+ SessionStart: [
265
+ { matcher: "*", hooks: [{ type: "command", command: hookCommand }] }
266
+ ],
267
+ SessionEnd: [
268
+ { hooks: [{ type: "command", command: hookCommand }] }
269
+ ],
270
+ UserPromptSubmit: [
271
+ { hooks: [{ type: "command", command: hookCommand }] }
272
+ ]
273
+ };
274
+ }
275
+ };
276
+
277
+ // src/lib/SwarmSetupService.ts
278
+ import path2 from "path";
279
+ import fs2 from "fs-extra";
280
+ var SwarmSetupService = class {
281
+ constructor(gitWorktree, metadataManager, agentManager, settingsManager, templateManager) {
282
+ this.gitWorktree = gitWorktree;
283
+ this.metadataManager = metadataManager;
284
+ this.agentManager = agentManager;
285
+ this.settingsManager = settingsManager;
286
+ this.templateManager = templateManager;
287
+ }
288
+ /**
289
+ * Create child worktrees for each child issue, branched off the epic branch.
290
+ * Writes iloom-metadata.json for each child with state: 'pending' and parentLoom.
291
+ * Generates and writes per-loom MCP config file for each child.
292
+ *
293
+ * Uses standard iloom naming conventions via generateWorktreePath().
294
+ *
295
+ * @param childIssues - Array of child issues from epic metadata
296
+ * @param epicBranch - The epic branch name (base branch for children)
297
+ * @param epicWorktreePath - Path to the epic worktree
298
+ * @param mainWorktreePath - Path to the main worktree (project root)
299
+ * @param epicIssueNumber - The parent epic issue number
300
+ * @param issueTrackerName - The issue tracker provider name (e.g., 'github')
301
+ * @param settings - Optional settings for MCP config generation
302
+ * @returns Array of results for each child worktree creation
303
+ */
304
+ async createChildWorktrees(childIssues, epicBranch, epicWorktreePath, mainWorktreePath, epicIssueNumber, issueTrackerName, settings) {
305
+ return Promise.all(childIssues.map(async (child) => {
306
+ try {
307
+ const rawId = child.number.replace(/^#/, "");
308
+ const safeId = rawId.replace(/[^a-zA-Z0-9-_]/g, "-");
309
+ const childBranch = `issue/${safeId}`;
310
+ const childWorktreePath = generateWorktreePath(
311
+ childBranch,
312
+ mainWorktreePath
313
+ );
314
+ getLogger().info(`Creating child worktree for ${child.number}: ${childWorktreePath}...`);
315
+ await this.gitWorktree.createWorktree({
316
+ path: childWorktreePath,
317
+ branch: childBranch,
318
+ createBranch: true,
319
+ baseBranch: epicBranch
320
+ });
321
+ const metadataInput = {
322
+ description: child.title,
323
+ branchName: childBranch,
324
+ worktreePath: childWorktreePath,
325
+ issueType: "issue",
326
+ issue_numbers: [rawId],
327
+ pr_numbers: [],
328
+ issueTracker: issueTrackerName,
329
+ colorHex: "#808080",
330
+ sessionId: "",
331
+ // No session - not launching Claude directly
332
+ projectPath: mainWorktreePath,
333
+ issueUrls: { [rawId]: child.url },
334
+ prUrls: {},
335
+ capabilities: [],
336
+ state: "pending",
337
+ parentLoom: {
338
+ type: "epic",
339
+ identifier: epicIssueNumber,
340
+ branchName: epicBranch,
341
+ worktreePath: epicWorktreePath
342
+ }
343
+ };
344
+ try {
345
+ await this.metadataManager.writeMetadata(childWorktreePath, metadataInput);
346
+ } catch (metaError) {
347
+ getLogger().warn(`Metadata write failed for ${child.number}, cleaning up worktree...`);
348
+ try {
349
+ await this.gitWorktree.removeWorktree(childWorktreePath, { force: true });
350
+ } catch {
351
+ getLogger().debug(`Could not clean up worktree at ${childWorktreePath}`);
352
+ }
353
+ throw metaError;
354
+ }
355
+ try {
356
+ const childMetadata = await this.metadataManager.readMetadata(childWorktreePath);
357
+ if (childMetadata) {
358
+ const providerName = IssueTrackerFactory.getProviderName(
359
+ settings ?? await this.settingsManager.loadSettings()
360
+ );
361
+ const mcpConfigPath = await generateAndWriteMcpConfigFile(
362
+ childWorktreePath,
363
+ childMetadata,
364
+ providerName,
365
+ settings
366
+ );
367
+ await this.metadataManager.updateMetadata(childWorktreePath, { mcpConfigPath });
368
+ const claudeDir = path2.join(childWorktreePath, ".claude");
369
+ await fs2.ensureDir(claudeDir);
370
+ await fs2.writeFile(
371
+ path2.join(claudeDir, "iloom-swarm-mcp-config-path"),
372
+ mcpConfigPath,
373
+ "utf-8"
374
+ );
375
+ getLogger().debug(`Wrote MCP config for ${child.number}: ${mcpConfigPath}`);
376
+ }
377
+ } catch (error) {
378
+ getLogger().warn(
379
+ `Failed to write MCP config for child ${child.number}: ${error instanceof Error ? error.message : "Unknown error"}`
380
+ );
381
+ }
382
+ try {
383
+ await installDependencies(childWorktreePath, true, true);
384
+ } catch (error) {
385
+ getLogger().warn(
386
+ `Failed to install dependencies in child worktree ${child.number}: ${error instanceof Error ? error.message : "Unknown error"}`
387
+ );
388
+ }
389
+ getLogger().success(`Created child worktree for ${child.number}`);
390
+ return {
391
+ issueId: rawId,
392
+ worktreePath: childWorktreePath,
393
+ branch: childBranch,
394
+ success: true
395
+ };
396
+ } catch (error) {
397
+ const rawId = child.number.replace(/^#/, "");
398
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
399
+ getLogger().warn(`Failed to create child worktree for ${child.number}: ${errorMessage}`);
400
+ return {
401
+ issueId: rawId,
402
+ worktreePath: "",
403
+ branch: "",
404
+ success: false,
405
+ error: errorMessage
406
+ };
407
+ }
408
+ }));
409
+ }
410
+ /**
411
+ * Render swarm-mode agent templates to the epic worktree's .claude/agents/ directory.
412
+ *
413
+ * Phase agent files are written WITHOUT frontmatter (prompt body only) because they are
414
+ * loaded via `--append-system-prompt-file` which does not parse YAML frontmatter.
415
+ * Model and tools metadata is extracted from the agent config and returned separately
416
+ * for use as CLI flags in `claude -p` commands.
417
+ */
418
+ async renderSwarmAgents(epicWorktreePath) {
419
+ var _a, _b;
420
+ const claudeAgentsDir = path2.join(epicWorktreePath, ".claude", "agents");
421
+ await fs2.ensureDir(claudeAgentsDir);
422
+ const settings = await this.settingsManager.loadSettings();
423
+ const templateVariables = {
424
+ SWARM_MODE: true
425
+ };
426
+ const agents = await this.agentManager.loadAgents(settings, templateVariables);
427
+ const defaultSwarmModels = {
428
+ "iloom-issue-analyzer": "opus",
429
+ "iloom-issue-analyze-and-plan": "opus",
430
+ "iloom-issue-planner": "sonnet",
431
+ "iloom-issue-implementer": "sonnet",
432
+ "iloom-issue-enhancer": "sonnet",
433
+ "iloom-code-reviewer": "sonnet",
434
+ "iloom-issue-complexity-evaluator": "haiku"
435
+ };
436
+ for (const [agentName, agentConfig] of Object.entries(agents)) {
437
+ const userSwarmModel = (_b = (_a = settings == null ? void 0 : settings.agents) == null ? void 0 : _a[agentName]) == null ? void 0 : _b.swarmModel;
438
+ if (userSwarmModel) {
439
+ agents[agentName] = { ...agentConfig, model: userSwarmModel };
440
+ } else if (defaultSwarmModels[agentName]) {
441
+ agents[agentName] = { ...agentConfig, model: defaultSwarmModels[agentName] };
442
+ }
443
+ }
444
+ const renderedFiles = [];
445
+ const metadata = {};
446
+ for (const [agentName, agentConfig] of Object.entries(agents)) {
447
+ const swarmFileName = agentName.startsWith("iloom-") ? `iloom-swarm-${agentName.slice("iloom-".length)}.md` : `iloom-swarm-${agentName}.md`;
448
+ const agentKey = swarmFileName.replace(".md", "");
449
+ metadata[agentKey] = {
450
+ model: agentConfig.model,
451
+ ...agentConfig.tools && { tools: agentConfig.tools }
452
+ };
453
+ const outputPath = path2.join(claudeAgentsDir, swarmFileName);
454
+ await fs2.writeFile(outputPath, agentConfig.prompt + "\n", "utf-8");
455
+ renderedFiles.push(swarmFileName);
456
+ getLogger().debug(`Rendered swarm agent: ${swarmFileName}`);
457
+ }
458
+ getLogger().success(`Rendered ${renderedFiles.length} swarm agents to ${claudeAgentsDir}`);
459
+ return { renderedFiles, metadata };
460
+ }
461
+ /**
462
+ * Render the swarm worker agent file to the epic worktree's .claude/agents/ directory.
463
+ *
464
+ * This creates an agent file at `.claude/agents/iloom-swarm-worker.md` containing
465
+ * the full iloom workflow instructions (rendered from issue-prompt.txt with SWARM_MODE=true).
466
+ * The orchestrator spawns children with `subagent_type: "iloom-swarm-worker"` so these
467
+ * instructions become the agent's system prompt (high authority), rather than arriving
468
+ * as a skill invocation (low authority user message).
469
+ *
470
+ * The agent file is shared across all children. Issue-specific context (number, title,
471
+ * worktree path, body) is provided per-child via the Task prompt from the orchestrator.
472
+ */
473
+ async renderSwarmWorkerAgent(epicWorktreePath, agentMetadata) {
474
+ var _a, _b, _c, _d, _e;
475
+ const agentsDir = path2.join(epicWorktreePath, ".claude", "agents");
476
+ const agentOutputPath = path2.join(agentsDir, "iloom-swarm-worker.md");
477
+ await fs2.ensureDir(agentsDir);
478
+ try {
479
+ const settings = await this.settingsManager.loadSettings();
480
+ const providerType = ((_a = settings == null ? void 0 : settings.issueManagement) == null ? void 0 : _a.provider) ?? "github";
481
+ const issuePrefix = IssueManagementProviderFactory.create(providerType, settings ?? void 0).issuePrefix;
482
+ const subAgentTimeoutMinutes = ((_c = (_b = settings == null ? void 0 : settings.agents) == null ? void 0 : _b["iloom-swarm-worker"]) == null ? void 0 : _c.subAgentTimeout) ?? 10;
483
+ const subAgentTimeoutMs = subAgentTimeoutMinutes * 60 * 1e3;
484
+ const variables = {
485
+ SWARM_MODE: true,
486
+ ONE_SHOT_MODE: true,
487
+ EPIC_WORKTREE_PATH: epicWorktreePath,
488
+ ISSUE_PREFIX: issuePrefix,
489
+ SWARM_SUB_AGENT_TIMEOUT_MS: subAgentTimeoutMs,
490
+ ...agentMetadata && { SWARM_AGENT_METADATA: JSON.stringify(agentMetadata) },
491
+ ...buildReviewTemplateVariables(settings == null ? void 0 : settings.agents)
492
+ };
493
+ const agentBody = await this.templateManager.getPrompt("issue", variables);
494
+ const workerModel = ((_e = (_d = settings == null ? void 0 : settings.agents) == null ? void 0 : _d["iloom-swarm-worker"]) == null ? void 0 : _e.model) ?? "opus";
495
+ const frontmatter = [
496
+ "---",
497
+ "name: iloom-swarm-worker",
498
+ "description: Swarm worker agent that implements a child issue following the full iloom workflow.",
499
+ `model: ${workerModel}`,
500
+ "---"
501
+ ].join("\n");
502
+ const content = `${frontmatter}
503
+
504
+ ${agentBody}
505
+ `;
506
+ await fs2.writeFile(agentOutputPath, content, "utf-8");
507
+ getLogger().success(`Rendered swarm worker agent to ${agentOutputPath}`);
508
+ return true;
509
+ } catch (error) {
510
+ getLogger().warn(
511
+ `Failed to render swarm worker agent: ${error instanceof Error ? error.message : "Unknown error"}`
512
+ );
513
+ return false;
514
+ }
515
+ }
516
+ /**
517
+ * Copy .claude/agents/ from the epic worktree to each child worktree.
518
+ *
519
+ * Child workers need local access to agent files (used via --append-system-prompt-file).
520
+ * Without this copy, child worktrees lack the rendered agent files since they only
521
+ * exist in the epic worktree after renderSwarmAgents/renderSwarmWorkerAgent.
522
+ */
523
+ async copyAgentsToChildWorktrees(epicWorktreePath, childWorktrees) {
524
+ const sourceDir = path2.join(epicWorktreePath, ".claude", "agents");
525
+ if (!await fs2.pathExists(sourceDir)) {
526
+ getLogger().warn("No .claude/agents/ directory in epic worktree to copy");
527
+ return;
528
+ }
529
+ const successfulChildren = childWorktrees.filter((c) => c.success);
530
+ await Promise.all(successfulChildren.map(async (child) => {
531
+ try {
532
+ const targetDir = path2.join(child.worktreePath, ".claude", "agents");
533
+ await fs2.copy(sourceDir, targetDir, { overwrite: true });
534
+ getLogger().debug(`Copied .claude/agents/ to ${child.worktreePath}`);
535
+ } catch (error) {
536
+ getLogger().warn(
537
+ `Failed to copy agents to child worktree ${child.issueId}: ${error instanceof Error ? error.message : "Unknown error"}`
538
+ );
539
+ }
540
+ }));
541
+ getLogger().success(`Copied agents to ${successfulChildren.length} child worktrees`);
542
+ }
543
+ /**
544
+ * Run the full swarm setup: child worktrees, agents, and worker agent.
545
+ *
546
+ * The epic worktree already exists (created by `il start`).
547
+ */
548
+ async setupSwarm(epicIssueNumber, epicBranch, epicWorktreePath, childIssues, mainWorktreePath, issueTrackerName, settings) {
549
+ const childWorktrees = await this.createChildWorktrees(
550
+ childIssues,
551
+ epicBranch,
552
+ epicWorktreePath,
553
+ mainWorktreePath,
554
+ epicIssueNumber,
555
+ issueTrackerName,
556
+ settings
557
+ );
558
+ const { renderedFiles: agentsRendered, metadata: agentMetadata } = await this.renderSwarmAgents(epicWorktreePath);
559
+ const workerAgentRendered = await this.renderSwarmWorkerAgent(
560
+ epicWorktreePath,
561
+ agentMetadata
562
+ );
563
+ await this.copyAgentsToChildWorktrees(epicWorktreePath, childWorktrees);
564
+ const successCount = childWorktrees.filter((c) => c.success).length;
565
+ const failCount = childWorktrees.filter((c) => !c.success).length;
566
+ getLogger().success(
567
+ `Swarm setup complete: ${successCount} child worktrees` + (failCount > 0 ? ` (${failCount} failed)` : "")
568
+ );
569
+ return {
570
+ epicWorktreePath,
571
+ epicBranch,
572
+ childWorktrees,
573
+ agentsRendered,
574
+ workerAgentRendered
575
+ };
576
+ }
577
+ };
578
+
579
+ // src/utils/language-detector.ts
580
+ import fs3 from "fs-extra";
581
+ import path3 from "path";
582
+ import fg from "fast-glob";
583
+ async function detectProjectLanguage(projectPath) {
584
+ try {
585
+ const packageJsonPath = path3.join(projectPath, "package.json");
586
+ if (await fs3.pathExists(packageJsonPath)) {
587
+ try {
588
+ const pkg = await fs3.readJson(packageJsonPath);
589
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
590
+ return "typescript" in allDeps ? "typescript" : "javascript";
591
+ } catch {
592
+ return "javascript";
593
+ }
594
+ }
595
+ if (await fs3.pathExists(path3.join(projectPath, "Cargo.toml"))) {
596
+ return "rust";
597
+ }
598
+ if (await fs3.pathExists(path3.join(projectPath, "go.mod"))) {
599
+ return "go";
600
+ }
601
+ for (const file of ["pyproject.toml", "setup.py", "requirements.txt"]) {
602
+ if (await fs3.pathExists(path3.join(projectPath, file))) {
603
+ return "python";
604
+ }
605
+ }
606
+ if (await fs3.pathExists(path3.join(projectPath, "Gemfile"))) {
607
+ return "ruby";
608
+ }
609
+ for (const file of ["pom.xml", "build.gradle", "build.gradle.kts"]) {
610
+ if (await fs3.pathExists(path3.join(projectPath, file))) {
611
+ return "java";
612
+ }
613
+ }
614
+ const csprojFiles = await fg("*.csproj", { cwd: projectPath, dot: false });
615
+ if (csprojFiles.length > 0) {
616
+ return "csharp";
617
+ }
618
+ return "unknown";
619
+ } catch {
620
+ return "unknown";
621
+ }
622
+ }
623
+
624
+ // src/commands/ignite.ts
625
+ var WorktreeValidationError = class extends Error {
626
+ constructor(message, suggestion) {
627
+ super(message);
628
+ this.suggestion = suggestion;
629
+ this.name = "WorktreeValidationError";
630
+ }
631
+ };
632
+ var IgniteCommand = class {
633
+ constructor(templateManager, gitWorktreeManager, agentManager, settingsManager, firstRunManager, hookManager) {
634
+ this.templateManager = templateManager ?? new PromptTemplateManager();
635
+ this.gitWorktreeManager = gitWorktreeManager ?? new GitWorktreeManager();
636
+ this.agentManager = agentManager ?? new AgentManager();
637
+ this.settingsManager = settingsManager ?? new SettingsManager();
638
+ this.firstRunManager = firstRunManager ?? new FirstRunManager("spin");
639
+ this.hookManager = hookManager ?? new ClaudeHookManager();
640
+ }
641
+ /**
642
+ * Validate that we're not running from the main worktree
643
+ * @param workspacePath - Optional explicit workspace path; defaults to process.cwd()
644
+ * @throws WorktreeValidationError if running from main worktree
645
+ */
646
+ async validateNotMainWorktree(workspacePath) {
647
+ const currentDir = workspacePath ?? process.cwd();
648
+ const isGitRepo = await isValidGitRepo(currentDir);
649
+ if (!isGitRepo) {
650
+ return;
651
+ }
652
+ const worktreeRoot = await getWorktreeRoot(currentDir);
653
+ if (!worktreeRoot) {
654
+ return;
655
+ }
656
+ const worktrees = await this.gitWorktreeManager.listWorktrees();
657
+ const currentWorktree = worktrees.find((wt) => wt.path === worktreeRoot);
658
+ if (!currentWorktree) {
659
+ return;
660
+ }
661
+ const isMain = await this.gitWorktreeManager.isMainWorktree(currentWorktree, this.settingsManager);
662
+ if (isMain) {
663
+ throw new WorktreeValidationError(
664
+ "You cannot run the command from the main worktree.",
665
+ "Navigate to a feature worktree created by 'il start <issue>' and run 'il spin' from there."
666
+ );
667
+ }
668
+ }
669
+ /**
670
+ * Main entry point for spin command
671
+ * @param oneShot - One-shot automation mode
672
+ * @param printOptions - Print mode options for headless/CI execution
673
+ * @param skipCleanup - Skip cleanup after execution
674
+ * @param workspacePath - Optional explicit workspace path for programmatic invocation (avoids process.chdir())
675
+ */
676
+ async execute(oneShot, printOptions, skipCleanup, workspacePath) {
677
+ var _a, _b;
678
+ this.printOptions = printOptions;
679
+ const isJsonMode = (((_a = this.printOptions) == null ? void 0 : _a.json) ?? false) || (((_b = this.printOptions) == null ? void 0 : _b.jsonStream) ?? false);
680
+ if (isJsonMode) {
681
+ const jsonLogger = createStderrLogger();
682
+ return withLogger(jsonLogger, () => this.executeInternal(oneShot, skipCleanup, workspacePath));
683
+ }
684
+ return this.executeInternal(oneShot, skipCleanup, workspacePath);
685
+ }
686
+ /**
687
+ * Internal execution method (separated for withLogger wrapping)
688
+ */
689
+ async executeInternal(oneShot, skipCleanup, workspacePath) {
690
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r;
691
+ process.env.ILOOM = "1";
692
+ try {
693
+ await this.validateNotMainWorktree(workspacePath);
694
+ } catch (error) {
695
+ if (error instanceof WorktreeValidationError) {
696
+ logger.error(error.message);
697
+ logger.info(error.suggestion);
698
+ throw error;
699
+ }
700
+ throw error;
701
+ }
702
+ try {
703
+ logger.info("\u{1F680} Your loom is spinning up, please wait...");
704
+ const isFirstRun = await this.firstRunManager.isFirstRun();
705
+ if (isFirstRun) {
706
+ logger.success("Welcome to iloom! Preparing first-time experience...");
707
+ }
708
+ await this.hookManager.installHooks();
709
+ const context = await this.detectWorkspaceContext(workspacePath);
710
+ logger.debug("Auto-detected workspace context", { context });
711
+ this.logDetectedContext(context);
712
+ logger.info("\u{1F4DD} Loading prompt template and preparing Claude...");
713
+ const metadataManager = new MetadataManager();
714
+ const metadata = await metadataManager.readMetadata(context.workspacePath);
715
+ const draftPrNumber = (metadata == null ? void 0 : metadata.draftPrNumber) ?? void 0;
716
+ const draftPrUrl = draftPrNumber && ((_a = metadata == null ? void 0 : metadata.prUrls) == null ? void 0 : _a[String(draftPrNumber)]) ? metadata.prUrls[String(draftPrNumber)] : void 0;
717
+ if (((_b = metadata == null ? void 0 : metadata.parentLoom) == null ? void 0 : _b.type) === "epic" && metadata.issueType !== "epic") {
718
+ throw new WorktreeValidationError(
719
+ "Cannot run il spin in a child worktree of an epic loom. The swarm orchestrator manages agent execution for these issues.",
720
+ "Run il spin from the parent epic worktree instead to launch the swarm orchestrator."
721
+ );
722
+ }
723
+ const storedOneShot = (metadata == null ? void 0 : metadata.oneShot) ?? "default";
724
+ const isHeadlessForOneShot = ((_c = this.printOptions) == null ? void 0 : _c.print) ?? false;
725
+ const effectiveOneShot = isHeadlessForOneShot ? "noReview" : oneShot ?? storedOneShot;
726
+ if (!this.settings) {
727
+ const cliOverrides = extractSettingsOverrides();
728
+ this.settings = await this.settingsManager.loadSettings(void 0, cliOverrides);
729
+ }
730
+ try {
731
+ const hasNeon = !!((_e = (_d = this.settings) == null ? void 0 : _d.databaseProviders) == null ? void 0 : _e.neon);
732
+ const language = await detectProjectLanguage(context.workspacePath);
733
+ TelemetryService.getInstance().track("session.started", {
734
+ has_neon: hasNeon,
735
+ language
736
+ });
737
+ } catch (error) {
738
+ logger.debug(`Telemetry session.started tracking failed: ${error instanceof Error ? error.message : error}`);
739
+ }
740
+ if (((_f = metadata == null ? void 0 : metadata.capabilities) == null ? void 0 : _f.includes("web")) && context.branchName) {
741
+ const basePort = ((_i = (_h = (_g = this.settings) == null ? void 0 : _g.capabilities) == null ? void 0 : _h.web) == null ? void 0 : _i.basePort) ?? 3e3;
742
+ context.port = await getWorkspacePort({
743
+ basePort,
744
+ worktreePath: context.workspacePath,
745
+ worktreeBranch: context.branchName
746
+ });
747
+ logger.info(`\u{1F310} Development server port: ${context.port}`);
748
+ }
749
+ const isEpicLoom = metadata && metadata.issue_numbers.length > 0 && ((((_j = metadata.childIssues) == null ? void 0 : _j.length) ?? 0) > 0 || metadata.issueType === "epic");
750
+ if (isEpicLoom && this.settings) {
751
+ await this.fetchAndStoreEpicChildData(metadataManager, metadata, context.workspacePath, this.settings);
752
+ }
753
+ if (isEpicLoom && this.settings) {
754
+ const freshMetadata = await metadataManager.readMetadata(context.workspacePath);
755
+ if (freshMetadata && freshMetadata.childIssues.length > 0) {
756
+ await this.executeSwarmMode(
757
+ freshMetadata,
758
+ context.workspacePath,
759
+ context.branchName ?? "",
760
+ metadataManager,
761
+ skipCleanup
762
+ );
763
+ return;
764
+ }
765
+ }
766
+ const variables = this.buildTemplateVariables(context, effectiveOneShot, draftPrNumber, draftPrUrl);
767
+ if (isFirstRun) {
768
+ variables.FIRST_TIME_USER = true;
769
+ variables.README_CONTENT = await this.loadReadmeContent();
770
+ variables.SETTINGS_SCHEMA_CONTENT = await this.loadSettingsSchemaContent();
771
+ }
772
+ const systemInstructions = await this.templateManager.getPrompt(context.type, variables);
773
+ const userPrompt = this.buildUserPrompt(effectiveOneShot);
774
+ const model = this.settingsManager.getSpinModel(this.settings);
775
+ let permissionMode = this.getPermissionModeForWorkflow(context.type);
776
+ if (effectiveOneShot === "bypassPermissions") {
777
+ permissionMode = "bypassPermissions";
778
+ }
779
+ if (permissionMode === "bypassPermissions") {
780
+ logger.warn(
781
+ "\u26A0\uFE0F WARNING: Using bypassPermissions mode - Claude will execute all tool calls without confirmation. This can be dangerous. Use with caution."
782
+ );
783
+ }
784
+ const sessionId = metadata == null ? void 0 : metadata.sessionId;
785
+ if (!sessionId) {
786
+ throw new Error("No session ID found in loom metadata. This loom may need to be recreated with `il start`.");
787
+ }
788
+ logger.debug("Using session ID from metadata", { sessionId });
789
+ const isHeadless = ((_k = this.printOptions) == null ? void 0 : _k.print) ?? false;
790
+ const claudeOptions = {
791
+ headless: isHeadless,
792
+ addDir: context.workspacePath,
793
+ sessionId
794
+ // Enable Claude Code session resume
795
+ };
796
+ if (model !== void 0) {
797
+ claudeOptions.model = model;
798
+ }
799
+ if (isHeadless) {
800
+ permissionMode = "bypassPermissions";
801
+ }
802
+ if (permissionMode !== void 0 && permissionMode !== "default") {
803
+ claudeOptions.permissionMode = permissionMode;
804
+ }
805
+ if (((_l = this.printOptions) == null ? void 0 : _l.outputFormat) !== void 0) {
806
+ claudeOptions.outputFormat = this.printOptions.outputFormat;
807
+ }
808
+ if (((_m = this.printOptions) == null ? void 0 : _m.verbose) !== void 0) {
809
+ claudeOptions.verbose = this.printOptions.verbose;
810
+ }
811
+ if ((_n = this.printOptions) == null ? void 0 : _n.json) {
812
+ claudeOptions.jsonMode = "json";
813
+ claudeOptions.outputFormat = "stream-json";
814
+ } else if ((_o = this.printOptions) == null ? void 0 : _o.jsonStream) {
815
+ claudeOptions.jsonMode = "stream";
816
+ claudeOptions.outputFormat = "stream-json";
817
+ }
818
+ if (context.branchName !== void 0) {
819
+ claudeOptions.branchName = context.branchName;
820
+ }
821
+ let mcpConfig;
822
+ let allowedTools;
823
+ let disallowedTools;
824
+ if (context.type === "issue" || context.type === "pr") {
825
+ try {
826
+ const provider = this.settings ? IssueTrackerFactory.getProviderName(this.settings) : "github";
827
+ mcpConfig = await generateIssueManagementMcpConfig(context.type, void 0, provider, this.settings, draftPrNumber);
828
+ logger.debug("Generated MCP configuration for issue management", { provider, draftPrNumber });
829
+ const baseTools = [
830
+ "mcp__issue_management__get_issue",
831
+ "mcp__issue_management__get_comment",
832
+ "mcp__issue_management__create_comment",
833
+ "mcp__issue_management__update_comment",
834
+ "mcp__issue_management__create_issue",
835
+ "mcp__issue_management__close_issue",
836
+ "mcp__issue_management__reopen_issue",
837
+ "mcp__issue_management__edit_issue",
838
+ "mcp__recap__add_entry",
839
+ "mcp__recap__get_recap",
840
+ "mcp__recap__add_artifact",
841
+ "mcp__recap__set_complexity",
842
+ "mcp__recap__set_loom_state",
843
+ "mcp__recap__get_loom_state"
844
+ ];
845
+ allowedTools = context.type === "pr" ? [...baseTools, "mcp__issue_management__get_pr", "mcp__issue_management__get_review_comments", "mcp__recap__set_goal"] : baseTools;
846
+ disallowedTools = ["Bash(gh api:*), Bash(gh issue comment:*)"];
847
+ logger.debug("Configured tool filtering for issue/PR workflow", { allowedTools, disallowedTools });
848
+ } catch (error) {
849
+ logger.warn(`Failed to generate MCP config: ${error instanceof Error ? error.message : "Unknown error"}`);
850
+ }
851
+ } else {
852
+ allowedTools = [
853
+ "mcp__recap__set_goal",
854
+ "mcp__recap__add_entry",
855
+ "mcp__recap__get_recap",
856
+ "mcp__recap__set_complexity",
857
+ "mcp__recap__set_loom_state",
858
+ "mcp__recap__get_loom_state"
859
+ ];
860
+ logger.debug("Configured tool filtering for regular workflow", { allowedTools });
861
+ }
862
+ try {
863
+ if (!metadata) {
864
+ throw new Error("No loom metadata found for this workspace");
865
+ }
866
+ const recapMcpConfig = generateRecapMcpConfig(context.workspacePath, metadata);
867
+ if (mcpConfig) {
868
+ mcpConfig.push(...recapMcpConfig);
869
+ } else {
870
+ mcpConfig = recapMcpConfig;
871
+ }
872
+ logger.debug("Generated MCP configuration for recap server");
873
+ } catch (error) {
874
+ logger.warn(`Failed to generate recap MCP config: ${error instanceof Error ? error.message : "Unknown error"}`);
875
+ }
876
+ let agents;
877
+ try {
878
+ if (((_p = this.settings) == null ? void 0 : _p.agents) && Object.keys(this.settings.agents).length > 0) {
879
+ logger.debug("Loaded project settings", {
880
+ agentOverrides: Object.keys(this.settings.agents)
881
+ });
882
+ }
883
+ const loadedAgents = await this.agentManager.loadAgents(
884
+ this.settings,
885
+ variables,
886
+ ["*.md", "!iloom-framework-detector.md"]
887
+ );
888
+ agents = this.agentManager.formatForCli(loadedAgents);
889
+ logger.debug("Loaded agent configurations", {
890
+ agentCount: Object.keys(agents).length,
891
+ agentNames: Object.keys(agents)
892
+ });
893
+ } catch (error) {
894
+ logger.warn(`Failed to load agents: ${error instanceof Error ? error.message : "Unknown error"}`);
895
+ }
896
+ logger.debug("Launching Claude in current terminal", {
897
+ type: context.type,
898
+ model,
899
+ permissionMode,
900
+ workspacePath: context.workspacePath,
901
+ hasMcpConfig: !!mcpConfig
902
+ });
903
+ logger.info(isHeadless ? "\u2728 Launching Claude in headless mode..." : "\u2728 Launching Claude in current terminal...");
904
+ const claudeResult = await launchClaude(userPrompt, {
905
+ ...claudeOptions,
906
+ appendSystemPrompt: systemInstructions,
907
+ ...mcpConfig && { mcpConfig },
908
+ ...allowedTools && { allowedTools },
909
+ ...disallowedTools && { disallowedTools },
910
+ ...agents && { agents }
911
+ });
912
+ if ((_q = this.printOptions) == null ? void 0 : _q.json) {
913
+ console.log(JSON.stringify({
914
+ success: true,
915
+ output: claudeResult ?? ""
916
+ }));
917
+ }
918
+ if (isFirstRun) {
919
+ await this.firstRunManager.markAsRun();
920
+ }
921
+ } catch (error) {
922
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
923
+ if ((_r = this.printOptions) == null ? void 0 : _r.json) {
924
+ console.log(JSON.stringify({
925
+ success: false,
926
+ error: errorMessage
927
+ }));
928
+ } else {
929
+ logger.error(`Failed to launch Claude: ${errorMessage}`);
930
+ }
931
+ throw error;
932
+ }
933
+ }
934
+ /**
935
+ * Log user-friendly information about detected context
936
+ */
937
+ logDetectedContext(context) {
938
+ if (context.type === "issue") {
939
+ logger.info(`\u{1F3AF} Detected issue workflow: Issue #${context.issueNumber}`);
940
+ } else if (context.type === "pr") {
941
+ logger.info(`\u{1F504} Detected PR workflow: PR #${context.prNumber}`);
942
+ } else {
943
+ logger.info("\u{1F31F} Detected regular workflow");
944
+ }
945
+ if (context.branchName) {
946
+ logger.info(`\u{1F33F} Working on branch: ${context.branchName}`);
947
+ }
948
+ if (context.port) {
949
+ logger.info(`\u{1F310} Development server port: ${context.port}`);
950
+ }
951
+ }
952
+ /**
953
+ * Build template variables from context
954
+ */
955
+ buildTemplateVariables(context, oneShot, draftPrNumber, draftPrUrl) {
956
+ var _a, _b, _c, _d, _e;
957
+ const variables = {
958
+ WORKSPACE_PATH: context.workspacePath
959
+ };
960
+ if (context.issueNumber !== void 0) {
961
+ variables.ISSUE_NUMBER = context.issueNumber;
962
+ }
963
+ if (context.prNumber !== void 0) {
964
+ variables.PR_NUMBER = context.prNumber;
965
+ }
966
+ if (context.title !== void 0) {
967
+ if (context.type === "issue") {
968
+ variables.ISSUE_TITLE = context.title;
969
+ } else if (context.type === "pr") {
970
+ variables.PR_TITLE = context.title;
971
+ }
972
+ }
973
+ if (context.port !== void 0) {
974
+ variables.PORT = context.port;
975
+ }
976
+ if (oneShot === "noReview" || oneShot === "bypassPermissions") {
977
+ variables.ONE_SHOT_MODE = true;
978
+ } else {
979
+ variables.INTERACTIVE_MODE = true;
980
+ }
981
+ Object.assign(variables, buildReviewTemplateVariables((_a = this.settings) == null ? void 0 : _a.agents));
982
+ if (draftPrNumber !== void 0) {
983
+ variables.DRAFT_PR_MODE = true;
984
+ variables.DRAFT_PR_NUMBER = draftPrNumber;
985
+ if (draftPrUrl) {
986
+ variables.DRAFT_PR_URL = draftPrUrl;
987
+ }
988
+ const autoCommitPushEnabled = ((_c = (_b = this.settings) == null ? void 0 : _b.mergeBehavior) == null ? void 0 : _c.autoCommitPush) !== false;
989
+ variables.AUTO_COMMIT_PUSH = autoCommitPushEnabled;
990
+ const remote = ((_e = (_d = this.settings) == null ? void 0 : _d.mergeBehavior) == null ? void 0 : _e.remote) ?? "origin";
991
+ if (!/^[a-zA-Z0-9_-]+$/.test(remote)) {
992
+ throw new Error(`Invalid git remote name: "${remote}". Remote names can only contain alphanumeric characters, underscores, and hyphens.`);
993
+ }
994
+ variables.GIT_REMOTE = remote;
995
+ } else if (context.type === "regular") {
996
+ variables.STANDARD_BRANCH_MODE = true;
997
+ } else {
998
+ variables.STANDARD_ISSUE_MODE = true;
999
+ }
1000
+ const isVscodeMode = process.env.ILOOM_VSCODE === "1";
1001
+ variables.IS_VSCODE_MODE = isVscodeMode;
1002
+ return variables;
1003
+ }
1004
+ /**
1005
+ * Get the appropriate permission mode for a workflow type
1006
+ * Same logic as ClaudeService.getPermissionModeForWorkflow()
1007
+ */
1008
+ getPermissionModeForWorkflow(type) {
1009
+ var _a;
1010
+ if ((_a = this.settings) == null ? void 0 : _a.workflows) {
1011
+ const workflowConfig = type === "issue" ? this.settings.workflows.issue : type === "pr" ? this.settings.workflows.pr : this.settings.workflows.regular;
1012
+ if (workflowConfig == null ? void 0 : workflowConfig.permissionMode) {
1013
+ return workflowConfig.permissionMode;
1014
+ }
1015
+ }
1016
+ if (type === "issue") {
1017
+ return "acceptEdits";
1018
+ }
1019
+ return "default";
1020
+ }
1021
+ /**
1022
+ * Auto-detect workspace context from current directory and git branch
1023
+ *
1024
+ * Detection priority:
1025
+ * 1. Directory name patterns (_pr_N, issue-N)
1026
+ * 2. Git branch name patterns
1027
+ * 3. Fallback to 'regular' workflow
1028
+ *
1029
+ * This leverages the same logic as FinishCommand.autoDetectFromCurrentDirectory()
1030
+ */
1031
+ async detectWorkspaceContext(workspacePath) {
1032
+ const workspacePath_ = workspacePath ?? process.cwd();
1033
+ const currentDir = path4.basename(workspacePath_);
1034
+ const prPattern = /_pr_(\d+)$/;
1035
+ const prMatch = currentDir.match(prPattern);
1036
+ if (prMatch == null ? void 0 : prMatch[1]) {
1037
+ const prNumber = parseInt(prMatch[1], 10);
1038
+ logger.debug(`Auto-detected PR #${prNumber} from directory: ${currentDir}`);
1039
+ return this.buildContextForPR(prNumber, workspacePath_);
1040
+ }
1041
+ const issueNumber = extractIssueNumber(currentDir);
1042
+ if (issueNumber !== null) {
1043
+ logger.debug(`Auto-detected issue #${issueNumber} from directory: ${currentDir}`);
1044
+ return this.buildContextForIssue(issueNumber, workspacePath_);
1045
+ }
1046
+ try {
1047
+ const repoInfo = await this.gitWorktreeManager.getRepoInfo();
1048
+ const currentBranch = repoInfo.currentBranch;
1049
+ if (currentBranch) {
1050
+ const branchIssueNumber = extractIssueNumber(currentBranch);
1051
+ if (branchIssueNumber !== null) {
1052
+ logger.debug(`Auto-detected issue #${branchIssueNumber} from branch: ${currentBranch}`);
1053
+ return this.buildContextForIssue(branchIssueNumber, workspacePath_, currentBranch);
1054
+ }
1055
+ }
1056
+ } catch (error) {
1057
+ logger.debug("Could not detect from git branch", { error });
1058
+ }
1059
+ logger.debug("No specific context detected, using regular workflow");
1060
+ return this.buildContextForRegular(workspacePath_);
1061
+ }
1062
+ /**
1063
+ * Build context for issue workflow
1064
+ */
1065
+ async buildContextForIssue(issueNumber, workspacePath, branchName) {
1066
+ if (!branchName) {
1067
+ try {
1068
+ const repoInfo = await this.gitWorktreeManager.getRepoInfo();
1069
+ branchName = repoInfo.currentBranch ?? void 0;
1070
+ } catch {
1071
+ }
1072
+ }
1073
+ const context = {
1074
+ type: "issue",
1075
+ issueNumber,
1076
+ workspacePath,
1077
+ headless: false
1078
+ // Interactive mode
1079
+ };
1080
+ if (branchName !== void 0) {
1081
+ context.branchName = branchName;
1082
+ }
1083
+ return context;
1084
+ }
1085
+ /**
1086
+ * Build context for PR workflow
1087
+ */
1088
+ async buildContextForPR(prNumber, workspacePath) {
1089
+ let branchName;
1090
+ try {
1091
+ const repoInfo = await this.gitWorktreeManager.getRepoInfo();
1092
+ branchName = repoInfo.currentBranch ?? void 0;
1093
+ } catch {
1094
+ }
1095
+ const context = {
1096
+ type: "pr",
1097
+ prNumber,
1098
+ workspacePath,
1099
+ headless: false
1100
+ // Interactive mode
1101
+ };
1102
+ if (branchName !== void 0) {
1103
+ context.branchName = branchName;
1104
+ }
1105
+ return context;
1106
+ }
1107
+ /**
1108
+ * Build context for regular workflow
1109
+ */
1110
+ async buildContextForRegular(workspacePath) {
1111
+ let branchName;
1112
+ try {
1113
+ const repoInfo = await this.gitWorktreeManager.getRepoInfo();
1114
+ branchName = repoInfo.currentBranch ?? void 0;
1115
+ } catch {
1116
+ }
1117
+ const context = {
1118
+ type: "regular",
1119
+ workspacePath,
1120
+ headless: false
1121
+ // Interactive mode
1122
+ };
1123
+ if (branchName !== void 0) {
1124
+ context.branchName = branchName;
1125
+ }
1126
+ return context;
1127
+ }
1128
+ /**
1129
+ * Fetch and store epic child issue data and dependency map in metadata
1130
+ *
1131
+ * Called during spin setup for epic looms. Fetches child issue details
1132
+ * and dependency relationships from the issue tracker, then persists
1133
+ * them in the loom metadata for use by the orchestrator.
1134
+ */
1135
+ async fetchAndStoreEpicChildData(metadataManager, metadata, worktreePath, settings) {
1136
+ const parentIssueNumber = metadata.issue_numbers[0];
1137
+ if (!parentIssueNumber) return;
1138
+ logger.info("Fetching child issue data for epic...");
1139
+ try {
1140
+ const issueTracker = IssueTrackerFactory.create(settings);
1141
+ const childIssueDetails = await fetchChildIssueDetails(
1142
+ parentIssueNumber,
1143
+ issueTracker
1144
+ );
1145
+ if (childIssueDetails.length === 0) {
1146
+ logger.debug("No child issues found for epic");
1147
+ return;
1148
+ }
1149
+ const childIds = childIssueDetails.map((child) => child.number.replace(/^#/, ""));
1150
+ const dependencyMap = await buildDependencyMap(childIds, settings);
1151
+ await metadataManager.updateMetadata(worktreePath, {
1152
+ childIssues: childIssueDetails,
1153
+ dependencyMap
1154
+ });
1155
+ logger.info(`Stored ${childIssueDetails.length} child issues and dependency map in metadata`);
1156
+ } catch (error) {
1157
+ logger.warn(`Failed to fetch epic child data: ${error instanceof Error ? error.message : String(error)}`);
1158
+ }
1159
+ }
1160
+ /**
1161
+ * Execute swarm mode for an epic loom.
1162
+ *
1163
+ * Creates child worktrees, renders swarm agents/skill, builds the
1164
+ * orchestrator prompt, and launches Claude with agent teams enabled.
1165
+ */
1166
+ async executeSwarmMode(metadata, epicWorktreePath, epicBranch, metadataManager, skipCleanup) {
1167
+ var _a, _b, _c;
1168
+ if (!this.settings) {
1169
+ throw new Error("Settings not loaded. Cannot enter swarm mode.");
1170
+ }
1171
+ const settings = this.settings;
1172
+ const epicIssueNumber = metadata.issue_numbers[0];
1173
+ if (!epicIssueNumber) {
1174
+ throw new Error("Epic loom has no issue number in metadata");
1175
+ }
1176
+ logger.info("Epic loom detected - entering swarm mode...");
1177
+ const mainWorktreePath = await findMainWorktreePathWithSettings();
1178
+ const providerName = IssueTrackerFactory.getProviderName(settings);
1179
+ const swarmSetup = new SwarmSetupService(
1180
+ this.gitWorktreeManager,
1181
+ metadataManager,
1182
+ this.agentManager,
1183
+ this.settingsManager,
1184
+ this.templateManager
1185
+ );
1186
+ try {
1187
+ const epicMcpConfigPath = await generateAndWriteMcpConfigFile(
1188
+ epicWorktreePath,
1189
+ metadata,
1190
+ providerName,
1191
+ settings
1192
+ );
1193
+ await metadataManager.updateMetadata(epicWorktreePath, { mcpConfigPath: epicMcpConfigPath });
1194
+ const epicClaudeDir = path4.join(epicWorktreePath, ".claude");
1195
+ await fs4.ensureDir(epicClaudeDir);
1196
+ await fs4.writeFile(
1197
+ path4.join(epicClaudeDir, "iloom-swarm-mcp-config-path"),
1198
+ epicMcpConfigPath,
1199
+ "utf-8"
1200
+ );
1201
+ logger.debug("Wrote MCP config for epic loom", { epicMcpConfigPath });
1202
+ } catch (error) {
1203
+ logger.warn(`Failed to write MCP config for epic loom: ${error instanceof Error ? error.message : "Unknown error"}`);
1204
+ }
1205
+ const mcpConfigs = [];
1206
+ try {
1207
+ const issueMcpConfigs = await generateIssueManagementMcpConfig(
1208
+ "issue",
1209
+ void 0,
1210
+ providerName,
1211
+ settings
1212
+ );
1213
+ mcpConfigs.push(...issueMcpConfigs);
1214
+ } catch (error) {
1215
+ logger.warn(`Failed to generate issue management MCP config: ${error instanceof Error ? error.message : "Unknown error"}`);
1216
+ }
1217
+ try {
1218
+ const recapMcpConfigs = generateRecapMcpConfig(epicWorktreePath, metadata);
1219
+ mcpConfigs.push(...recapMcpConfigs);
1220
+ } catch (error) {
1221
+ logger.warn(`Failed to generate recap MCP config: ${error instanceof Error ? error.message : "Unknown error"}`);
1222
+ }
1223
+ const swarmResult = await swarmSetup.setupSwarm(
1224
+ epicIssueNumber,
1225
+ epicBranch,
1226
+ epicWorktreePath,
1227
+ metadata.childIssues,
1228
+ mainWorktreePath,
1229
+ providerName,
1230
+ settings
1231
+ );
1232
+ const successfulWorktrees = swarmResult.childWorktrees.filter((c) => c.success);
1233
+ const worktreeMap = new Map(successfulWorktrees.map((cw) => [cw.issueId, cw]));
1234
+ const childIssuesData = metadata.childIssues.filter((ci) => worktreeMap.has(ci.number.replace(/^#/, ""))).map((ci) => {
1235
+ const rawId = ci.number.replace(/^#/, "");
1236
+ const wt = worktreeMap.get(rawId);
1237
+ return {
1238
+ number: rawId,
1239
+ title: ci.title,
1240
+ body: ci.body,
1241
+ worktreePath: (wt == null ? void 0 : wt.worktreePath) ?? "",
1242
+ branchName: (wt == null ? void 0 : wt.branch) ?? ""
1243
+ };
1244
+ });
1245
+ const epicMetadataPath = metadataManager.getMetadataFilePath(epicWorktreePath);
1246
+ const issuePrefix = providerName === "github" ? "#" : "";
1247
+ const variables = {
1248
+ EPIC_ISSUE_NUMBER: epicIssueNumber,
1249
+ EPIC_WORKTREE_PATH: epicWorktreePath,
1250
+ EPIC_METADATA_PATH: epicMetadataPath,
1251
+ CHILD_ISSUES: JSON.stringify(childIssuesData, null, 2),
1252
+ DEPENDENCY_MAP: JSON.stringify(metadata.dependencyMap, null, 2),
1253
+ ISSUE_PREFIX: issuePrefix,
1254
+ ...skipCleanup && { NO_CLEANUP: true }
1255
+ };
1256
+ const draftPrNumber = metadata.draftPrNumber ?? void 0;
1257
+ if (draftPrNumber !== void 0) {
1258
+ variables.DRAFT_PR_MODE = true;
1259
+ variables.DRAFT_PR_NUMBER = draftPrNumber;
1260
+ const draftPrUrl = (_a = metadata.prUrls) == null ? void 0 : _a[String(draftPrNumber)];
1261
+ if (draftPrUrl) {
1262
+ variables.DRAFT_PR_URL = draftPrUrl;
1263
+ }
1264
+ const autoCommitPushEnabled = ((_b = settings.mergeBehavior) == null ? void 0 : _b.autoCommitPush) !== false;
1265
+ variables.AUTO_COMMIT_PUSH = autoCommitPushEnabled;
1266
+ const remote = ((_c = settings.mergeBehavior) == null ? void 0 : _c.remote) ?? "origin";
1267
+ if (!/^[a-zA-Z0-9_-]+$/.test(remote)) {
1268
+ throw new Error(`Invalid git remote name: "${remote}". Remote names can only contain alphanumeric characters, underscores, and hyphens.`);
1269
+ }
1270
+ variables.GIT_REMOTE = remote;
1271
+ }
1272
+ const orchestratorPrompt = await this.templateManager.getPrompt("swarm-orchestrator", variables);
1273
+ const allowedTools = [
1274
+ "mcp__issue_management__get_issue",
1275
+ "mcp__issue_management__get_comment",
1276
+ "mcp__issue_management__create_comment",
1277
+ "mcp__issue_management__update_comment",
1278
+ "mcp__issue_management__create_issue",
1279
+ "mcp__issue_management__close_issue",
1280
+ "mcp__issue_management__reopen_issue",
1281
+ "mcp__issue_management__edit_issue",
1282
+ "mcp__recap__add_entry",
1283
+ "mcp__recap__get_recap",
1284
+ "mcp__recap__add_artifact",
1285
+ "mcp__recap__set_complexity",
1286
+ "mcp__recap__set_loom_state",
1287
+ "mcp__recap__get_loom_state"
1288
+ ];
1289
+ const model = this.settingsManager.getSpinModel(settings, "swarm");
1290
+ logger.info("Launching swarm orchestrator...");
1291
+ logger.info(` Model: ${model ?? "default"}`);
1292
+ logger.info(` Permission mode: bypassPermissions`);
1293
+ logger.info(` Agent teams: enabled`);
1294
+ logger.info(` Child worktrees: ${successfulWorktrees.length}`);
1295
+ let agents;
1296
+ try {
1297
+ const loadedAgents = await this.agentManager.loadAgents(
1298
+ settings,
1299
+ variables,
1300
+ ["*.md", "!iloom-framework-detector.md"]
1301
+ );
1302
+ agents = this.agentManager.formatForCli(loadedAgents);
1303
+ } catch (error) {
1304
+ logger.warn(`Failed to load agents: ${error instanceof Error ? error.message : "Unknown error"}`);
1305
+ }
1306
+ const swarmStartTime = Date.now();
1307
+ try {
1308
+ TelemetryService.getInstance().track("swarm.started", {
1309
+ child_count: successfulWorktrees.length,
1310
+ tracker: providerName
1311
+ });
1312
+ } catch (error) {
1313
+ logger.debug(`Telemetry swarm.started tracking failed: ${error instanceof Error ? error.message : error}`);
1314
+ }
1315
+ await launchClaude(
1316
+ `You are the swarm orchestrator for epic #${epicIssueNumber}. Begin by reading your system prompt instructions and executing the workflow.`,
1317
+ {
1318
+ model,
1319
+ permissionMode: "bypassPermissions",
1320
+ addDir: epicWorktreePath,
1321
+ headless: false,
1322
+ ...metadata.sessionId && { sessionId: metadata.sessionId },
1323
+ appendSystemPrompt: orchestratorPrompt,
1324
+ mcpConfig: mcpConfigs,
1325
+ allowedTools,
1326
+ ...agents && { agents },
1327
+ env: {
1328
+ CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: "1",
1329
+ ILOOM_SWARM: "1",
1330
+ ENABLE_TOOL_SEARCH: "auto:30"
1331
+ }
1332
+ }
1333
+ );
1334
+ try {
1335
+ const swarmEndTime = Date.now();
1336
+ let succeeded = 0;
1337
+ let failed = 0;
1338
+ for (const child of successfulWorktrees) {
1339
+ const childMeta = await metadataManager.readMetadata(child.worktreePath);
1340
+ const isSuccess = (childMeta == null ? void 0 : childMeta.state) === "done";
1341
+ if (isSuccess) {
1342
+ succeeded++;
1343
+ } else {
1344
+ failed++;
1345
+ }
1346
+ const parsed = (childMeta == null ? void 0 : childMeta.created_at) ? Date.parse(childMeta.created_at) : NaN;
1347
+ const childCreatedAt = Number.isNaN(parsed) ? swarmStartTime : parsed;
1348
+ const childDuration = Math.max(0, Math.round((swarmEndTime - childCreatedAt) / 6e4));
1349
+ TelemetryService.getInstance().track("swarm.child_completed", {
1350
+ success: isSuccess,
1351
+ duration_minutes: childDuration
1352
+ });
1353
+ }
1354
+ TelemetryService.getInstance().track("swarm.completed", {
1355
+ total_children: successfulWorktrees.length,
1356
+ succeeded,
1357
+ failed,
1358
+ duration_minutes: Math.round((swarmEndTime - swarmStartTime) / 6e4)
1359
+ });
1360
+ } catch (error) {
1361
+ logger.debug(`Telemetry swarm completion tracking failed: ${error instanceof Error ? error.message : error}`);
1362
+ }
1363
+ }
1364
+ /**
1365
+ * Build user prompt based on one-shot mode
1366
+ */
1367
+ buildUserPrompt(oneShot = "default") {
1368
+ if (oneShot === "noReview" || oneShot === "bypassPermissions") {
1369
+ return "Guide the user through the iloom workflow! The user has requested you move through the workflow without awaiting confirmation. This supersedes any other guidance.";
1370
+ }
1371
+ return "Guide the user through the iloom workflow!";
1372
+ }
1373
+ /**
1374
+ * Load README.md content for first-time users
1375
+ * Walks up from dist directory to find README.md in project root
1376
+ */
1377
+ async loadReadmeContent() {
1378
+ try {
1379
+ let currentDir = path4.dirname(new URL(import.meta.url).pathname);
1380
+ while (currentDir !== path4.dirname(currentDir)) {
1381
+ const readmePath = path4.join(currentDir, "README.md");
1382
+ try {
1383
+ const content = await readFile(readmePath, "utf-8");
1384
+ logger.debug("Loaded README.md for first-time user", { readmePath });
1385
+ return content;
1386
+ } catch {
1387
+ currentDir = path4.dirname(currentDir);
1388
+ }
1389
+ }
1390
+ logger.debug("README.md not found, returning empty string");
1391
+ return "";
1392
+ } catch (error) {
1393
+ logger.debug(`Failed to load README.md: ${error}`);
1394
+ return "";
1395
+ }
1396
+ }
1397
+ /**
1398
+ * Load settings schema content for first-time users
1399
+ * Walks up from dist directory to find .iloom/README.md
1400
+ */
1401
+ async loadSettingsSchemaContent() {
1402
+ try {
1403
+ let currentDir = path4.dirname(new URL(import.meta.url).pathname);
1404
+ while (currentDir !== path4.dirname(currentDir)) {
1405
+ const schemaPath = path4.join(currentDir, ".iloom", "README.md");
1406
+ try {
1407
+ const content = await readFile(schemaPath, "utf-8");
1408
+ logger.debug("Loaded .iloom/README.md for first-time user", { schemaPath });
1409
+ return content;
1410
+ } catch {
1411
+ currentDir = path4.dirname(currentDir);
1412
+ }
1413
+ }
1414
+ logger.debug(".iloom/README.md not found, returning empty string");
1415
+ return "";
1416
+ } catch (error) {
1417
+ logger.debug(`Failed to load .iloom/README.md: ${error}`);
1418
+ return "";
1419
+ }
1420
+ }
1421
+ };
1422
+
1423
+ export {
1424
+ WorktreeValidationError,
1425
+ IgniteCommand
1426
+ };
1427
+ //# sourceMappingURL=chunk-OTGH2HRS.js.map