@iloom/cli 0.1.19 → 0.3.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 (176) hide show
  1. package/README.md +290 -30
  2. package/dist/BranchNamingService-3OQPRSWT.js +13 -0
  3. package/dist/ClaudeContextManager-MUQSDY2E.js +13 -0
  4. package/dist/ClaudeService-HG4VQ7AW.js +12 -0
  5. package/dist/GitHubService-EBOETDIW.js +11 -0
  6. package/dist/{LoomLauncher-UMMLPIZO.js → LoomLauncher-FLEMBCSQ.js} +64 -33
  7. package/dist/LoomLauncher-FLEMBCSQ.js.map +1 -0
  8. package/dist/ProjectCapabilityDetector-34LU7JJ4.js +9 -0
  9. package/dist/{PromptTemplateManager-WII75TKH.js → PromptTemplateManager-A52RUAMS.js} +2 -2
  10. package/dist/README.md +290 -30
  11. package/dist/{SettingsManager-SKLUVE3K.js → SettingsManager-WHHFGSL7.js} +12 -4
  12. package/dist/SettingsMigrationManager-AGIIIPDQ.js +10 -0
  13. package/dist/agents/iloom-issue-analyze-and-plan.md +125 -35
  14. package/dist/agents/iloom-issue-analyzer.md +284 -32
  15. package/dist/agents/iloom-issue-complexity-evaluator.md +40 -21
  16. package/dist/agents/iloom-issue-enhancer.md +69 -48
  17. package/dist/agents/iloom-issue-implementer.md +36 -25
  18. package/dist/agents/iloom-issue-planner.md +35 -24
  19. package/dist/agents/iloom-issue-reviewer.md +62 -9
  20. package/dist/chunk-3KATJIKO.js +55 -0
  21. package/dist/chunk-3KATJIKO.js.map +1 -0
  22. package/dist/{chunk-JXQXSC45.js → chunk-3RUPPQRG.js} +1 -18
  23. package/dist/chunk-3RUPPQRG.js.map +1 -0
  24. package/dist/{chunk-PR7FKQBG.js → chunk-47KSHUCR.js} +3 -3
  25. package/dist/chunk-47KSHUCR.js.map +1 -0
  26. package/dist/{chunk-IO4WFTL2.js → chunk-4HHRTA7Q.js} +3 -3
  27. package/dist/{chunk-IO4WFTL2.js.map → chunk-4HHRTA7Q.js.map} +1 -1
  28. package/dist/chunk-5EF7Z346.js +1987 -0
  29. package/dist/chunk-5EF7Z346.js.map +1 -0
  30. package/dist/{chunk-VVH3ANF2.js → chunk-AWOFAD5O.js} +12 -12
  31. package/dist/chunk-AWOFAD5O.js.map +1 -0
  32. package/dist/{chunk-DEPYQRRB.js → chunk-C5QCTEQK.js} +2 -2
  33. package/dist/{chunk-CWR2SANQ.js → chunk-EBISESAP.js} +1 -1
  34. package/dist/{chunk-ELFT36PV.js → chunk-FIAT22G7.js} +4 -16
  35. package/dist/chunk-FIAT22G7.js.map +1 -0
  36. package/dist/{chunk-ZWXJBSUW.js → chunk-G2IEYOLQ.js} +11 -38
  37. package/dist/chunk-G2IEYOLQ.js.map +1 -0
  38. package/dist/{chunk-ZMNQBJUI.js → chunk-IP7SMKIF.js} +61 -22
  39. package/dist/chunk-IP7SMKIF.js.map +1 -0
  40. package/dist/{chunk-JNKJ7NJV.js → chunk-JKXJ7BGL.js} +6 -2
  41. package/dist/{chunk-JNKJ7NJV.js.map → chunk-JKXJ7BGL.js.map} +1 -1
  42. package/dist/{chunk-LAPY6NAE.js → chunk-JQFO7QQN.js} +68 -12
  43. package/dist/{chunk-LAPY6NAE.js.map → chunk-JQFO7QQN.js.map} +1 -1
  44. package/dist/{SettingsMigrationManager-MTQIMI54.js → chunk-KLBYVHPK.js} +3 -2
  45. package/dist/{chunk-KOCQAD2E.js → chunk-MAVL6PJF.js} +26 -3
  46. package/dist/chunk-MAVL6PJF.js.map +1 -0
  47. package/dist/{chunk-USVVV3FP.js → chunk-MKWYLDFK.js} +5 -5
  48. package/dist/chunk-ML3NRPNB.js +396 -0
  49. package/dist/chunk-ML3NRPNB.js.map +1 -0
  50. package/dist/{chunk-FXV24OYZ.js → chunk-PA6Q6AWM.js} +24 -4
  51. package/dist/chunk-PA6Q6AWM.js.map +1 -0
  52. package/dist/chunk-RO26VS3W.js +444 -0
  53. package/dist/chunk-RO26VS3W.js.map +1 -0
  54. package/dist/{chunk-PV3GAXQO.js → chunk-VAYCCUXW.js} +72 -2
  55. package/dist/{chunk-PV3GAXQO.js.map → chunk-VAYCCUXW.js.map} +1 -1
  56. package/dist/{chunk-SPYPLHMK.js → chunk-VU3QMIP2.js} +34 -2
  57. package/dist/chunk-VU3QMIP2.js.map +1 -0
  58. package/dist/{chunk-PVAVNJKS.js → chunk-WEN5C5DM.js} +10 -1
  59. package/dist/chunk-WEN5C5DM.js.map +1 -0
  60. package/dist/{chunk-PXZBAC2M.js → chunk-XXV3UFZL.js} +4 -4
  61. package/dist/{chunk-PXZBAC2M.js.map → chunk-XXV3UFZL.js.map} +1 -1
  62. package/dist/{chunk-RSRO7564.js → chunk-ZE74H5BR.js} +28 -3
  63. package/dist/chunk-ZE74H5BR.js.map +1 -0
  64. package/dist/{chunk-GZP4UGGM.js → chunk-ZM3CFL5L.js} +2 -2
  65. package/dist/{chunk-BLCTGFZN.js → chunk-ZT3YZB4K.js} +3 -4
  66. package/dist/chunk-ZT3YZB4K.js.map +1 -0
  67. package/dist/{claude-7LUVDZZ4.js → claude-GOP6PFC7.js} +2 -2
  68. package/dist/{cleanup-ZHROIBSQ.js → cleanup-7RWLBSLE.js} +86 -25
  69. package/dist/cleanup-7RWLBSLE.js.map +1 -0
  70. package/dist/cli.js +2513 -64
  71. package/dist/cli.js.map +1 -1
  72. package/dist/{contribute-3MQJ3XAQ.js → contribute-BS2L4FZR.js} +9 -6
  73. package/dist/{contribute-3MQJ3XAQ.js.map → contribute-BS2L4FZR.js.map} +1 -1
  74. package/dist/{feedback-ZOUCCHN4.js → feedback-N4ECWIPF.js} +15 -14
  75. package/dist/{feedback-ZOUCCHN4.js.map → feedback-N4ECWIPF.js.map} +1 -1
  76. package/dist/{git-OUYMVYJX.js → git-TDXKRTXM.js} +4 -2
  77. package/dist/{ignite-HICLZEYU.js → ignite-VM64QO3J.js} +32 -27
  78. package/dist/ignite-VM64QO3J.js.map +1 -0
  79. package/dist/index.d.ts +379 -45
  80. package/dist/index.js +1241 -448
  81. package/dist/index.js.map +1 -1
  82. package/dist/{init-UMKNHNV5.js → init-G3T64SC4.js} +104 -40
  83. package/dist/init-G3T64SC4.js.map +1 -0
  84. package/dist/mcp/issue-management-server.js +934 -0
  85. package/dist/mcp/issue-management-server.js.map +1 -0
  86. package/dist/{neon-helpers-ZVIRPKCI.js → neon-helpers-WPUACUVC.js} +3 -3
  87. package/dist/{open-ETZUFSE4.js → open-KXDXEKRZ.js} +39 -36
  88. package/dist/open-KXDXEKRZ.js.map +1 -0
  89. package/dist/{prompt-ANTQWHUF.js → prompt-7INJ7YRU.js} +4 -2
  90. package/dist/prompt-7INJ7YRU.js.map +1 -0
  91. package/dist/prompts/init-prompt.txt +563 -91
  92. package/dist/prompts/issue-prompt.txt +27 -27
  93. package/dist/{rebase-KBWFDZCN.js → rebase-Q7GMM7EI.js} +6 -6
  94. package/dist/{remote-GJEZWRCC.js → remote-VUNCQZ6J.js} +5 -2
  95. package/dist/remote-VUNCQZ6J.js.map +1 -0
  96. package/dist/{run-4SVQ3WEU.js → run-PAWJJCSX.js} +39 -36
  97. package/dist/run-PAWJJCSX.js.map +1 -0
  98. package/dist/schema/settings.schema.json +74 -0
  99. package/dist/{terminal-3D6TUAKJ.js → terminal-BIRBZ4AZ.js} +2 -2
  100. package/dist/terminal-BIRBZ4AZ.js.map +1 -0
  101. package/dist/{test-git-MKZATGZN.js → test-git-3WDLNQCA.js} +3 -3
  102. package/dist/{test-prefix-ZNLWDI3K.js → test-prefix-EVGAWAJW.js} +3 -3
  103. package/dist/{test-tabs-JRKY3QMM.js → test-tabs-RXDBZ6J7.js} +2 -2
  104. package/dist/{test-webserver-M2I3EV4J.js → test-webserver-DAHONWCS.js} +4 -4
  105. package/dist/test-webserver-DAHONWCS.js.map +1 -0
  106. package/package.json +2 -1
  107. package/dist/ClaudeContextManager-JKR4WGNU.js +0 -13
  108. package/dist/ClaudeService-55DQGB7T.js +0 -12
  109. package/dist/GitHubService-LWP4GKGH.js +0 -11
  110. package/dist/LoomLauncher-UMMLPIZO.js.map +0 -1
  111. package/dist/add-issue-X56V3XPB.js +0 -69
  112. package/dist/add-issue-X56V3XPB.js.map +0 -1
  113. package/dist/chunk-BLCTGFZN.js.map +0 -1
  114. package/dist/chunk-ELFT36PV.js.map +0 -1
  115. package/dist/chunk-FXV24OYZ.js.map +0 -1
  116. package/dist/chunk-H4E4THUZ.js +0 -55
  117. package/dist/chunk-H4E4THUZ.js.map +0 -1
  118. package/dist/chunk-H5LDRGVK.js +0 -642
  119. package/dist/chunk-H5LDRGVK.js.map +0 -1
  120. package/dist/chunk-JXQXSC45.js.map +0 -1
  121. package/dist/chunk-KOCQAD2E.js.map +0 -1
  122. package/dist/chunk-PR7FKQBG.js.map +0 -1
  123. package/dist/chunk-PVAVNJKS.js.map +0 -1
  124. package/dist/chunk-Q2KYPAH2.js +0 -545
  125. package/dist/chunk-Q2KYPAH2.js.map +0 -1
  126. package/dist/chunk-RSRO7564.js.map +0 -1
  127. package/dist/chunk-SPYPLHMK.js.map +0 -1
  128. package/dist/chunk-VCMMAFXQ.js +0 -54
  129. package/dist/chunk-VCMMAFXQ.js.map +0 -1
  130. package/dist/chunk-VVH3ANF2.js.map +0 -1
  131. package/dist/chunk-VYQLLHZ7.js +0 -239
  132. package/dist/chunk-VYQLLHZ7.js.map +0 -1
  133. package/dist/chunk-ZMNQBJUI.js.map +0 -1
  134. package/dist/chunk-ZWXJBSUW.js.map +0 -1
  135. package/dist/cleanup-ZHROIBSQ.js.map +0 -1
  136. package/dist/enhance-VGWUX474.js +0 -176
  137. package/dist/enhance-VGWUX474.js.map +0 -1
  138. package/dist/finish-QJSK6Z7J.js +0 -1355
  139. package/dist/finish-QJSK6Z7J.js.map +0 -1
  140. package/dist/ignite-HICLZEYU.js.map +0 -1
  141. package/dist/init-UMKNHNV5.js.map +0 -1
  142. package/dist/mcp/chunk-6SDFJ42P.js +0 -62
  143. package/dist/mcp/chunk-6SDFJ42P.js.map +0 -1
  144. package/dist/mcp/claude-YHHHLSXH.js +0 -249
  145. package/dist/mcp/claude-YHHHLSXH.js.map +0 -1
  146. package/dist/mcp/color-QS5BFCNN.js +0 -168
  147. package/dist/mcp/color-QS5BFCNN.js.map +0 -1
  148. package/dist/mcp/github-comment-server.js +0 -168
  149. package/dist/mcp/github-comment-server.js.map +0 -1
  150. package/dist/mcp/terminal-SDCMDVD7.js +0 -202
  151. package/dist/mcp/terminal-SDCMDVD7.js.map +0 -1
  152. package/dist/open-ETZUFSE4.js.map +0 -1
  153. package/dist/run-4SVQ3WEU.js.map +0 -1
  154. package/dist/start-CT2ZEFP2.js +0 -983
  155. package/dist/start-CT2ZEFP2.js.map +0 -1
  156. package/dist/test-webserver-M2I3EV4J.js.map +0 -1
  157. /package/dist/{ClaudeContextManager-JKR4WGNU.js.map → BranchNamingService-3OQPRSWT.js.map} +0 -0
  158. /package/dist/{ClaudeService-55DQGB7T.js.map → ClaudeContextManager-MUQSDY2E.js.map} +0 -0
  159. /package/dist/{GitHubService-LWP4GKGH.js.map → ClaudeService-HG4VQ7AW.js.map} +0 -0
  160. /package/dist/{PromptTemplateManager-WII75TKH.js.map → GitHubService-EBOETDIW.js.map} +0 -0
  161. /package/dist/{SettingsManager-SKLUVE3K.js.map → ProjectCapabilityDetector-34LU7JJ4.js.map} +0 -0
  162. /package/dist/{claude-7LUVDZZ4.js.map → PromptTemplateManager-A52RUAMS.js.map} +0 -0
  163. /package/dist/{git-OUYMVYJX.js.map → SettingsManager-WHHFGSL7.js.map} +0 -0
  164. /package/dist/{neon-helpers-ZVIRPKCI.js.map → SettingsMigrationManager-AGIIIPDQ.js.map} +0 -0
  165. /package/dist/{chunk-DEPYQRRB.js.map → chunk-C5QCTEQK.js.map} +0 -0
  166. /package/dist/{chunk-CWR2SANQ.js.map → chunk-EBISESAP.js.map} +0 -0
  167. /package/dist/{SettingsMigrationManager-MTQIMI54.js.map → chunk-KLBYVHPK.js.map} +0 -0
  168. /package/dist/{chunk-USVVV3FP.js.map → chunk-MKWYLDFK.js.map} +0 -0
  169. /package/dist/{chunk-GZP4UGGM.js.map → chunk-ZM3CFL5L.js.map} +0 -0
  170. /package/dist/{prompt-ANTQWHUF.js.map → claude-GOP6PFC7.js.map} +0 -0
  171. /package/dist/{remote-GJEZWRCC.js.map → git-TDXKRTXM.js.map} +0 -0
  172. /package/dist/{terminal-3D6TUAKJ.js.map → neon-helpers-WPUACUVC.js.map} +0 -0
  173. /package/dist/{rebase-KBWFDZCN.js.map → rebase-Q7GMM7EI.js.map} +0 -0
  174. /package/dist/{test-git-MKZATGZN.js.map → test-git-3WDLNQCA.js.map} +0 -0
  175. /package/dist/{test-prefix-ZNLWDI3K.js.map → test-prefix-EVGAWAJW.js.map} +0 -0
  176. /package/dist/{test-tabs-JRKY3QMM.js.map → test-tabs-RXDBZ6J7.js.map} +0 -0
package/dist/index.js CHANGED
@@ -147,18 +147,23 @@ var SettingsManager_exports = {};
147
147
  __export(SettingsManager_exports, {
148
148
  AgentSettingsSchema: () => AgentSettingsSchema,
149
149
  CapabilitiesSettingsSchema: () => CapabilitiesSettingsSchema,
150
+ CapabilitiesSettingsSchemaNoDefaults: () => CapabilitiesSettingsSchemaNoDefaults,
150
151
  DatabaseProvidersSettingsSchema: () => DatabaseProvidersSettingsSchema,
151
152
  IloomSettingsSchema: () => IloomSettingsSchema,
153
+ IloomSettingsSchemaNoDefaults: () => IloomSettingsSchemaNoDefaults,
152
154
  NeonSettingsSchema: () => NeonSettingsSchema,
153
155
  SettingsManager: () => SettingsManager,
154
156
  WorkflowPermissionSchema: () => WorkflowPermissionSchema,
155
- WorkflowsSettingsSchema: () => WorkflowsSettingsSchema
157
+ WorkflowPermissionSchemaNoDefaults: () => WorkflowPermissionSchemaNoDefaults,
158
+ WorkflowsSettingsSchema: () => WorkflowsSettingsSchema,
159
+ WorkflowsSettingsSchemaNoDefaults: () => WorkflowsSettingsSchemaNoDefaults
156
160
  });
157
161
  import { readFile } from "fs/promises";
158
162
  import path from "path";
163
+ import os from "os";
159
164
  import { z } from "zod";
160
165
  import deepmerge from "deepmerge";
161
- var AgentSettingsSchema, WorkflowPermissionSchema, WorkflowsSettingsSchema, CapabilitiesSettingsSchema, NeonSettingsSchema, DatabaseProvidersSettingsSchema, IloomSettingsSchema, SettingsManager;
166
+ var AgentSettingsSchema, WorkflowPermissionSchema, WorkflowPermissionSchemaNoDefaults, WorkflowsSettingsSchema, WorkflowsSettingsSchemaNoDefaults, CapabilitiesSettingsSchema, CapabilitiesSettingsSchemaNoDefaults, NeonSettingsSchema, DatabaseProvidersSettingsSchema, IloomSettingsSchema, IloomSettingsSchemaNoDefaults, SettingsManager;
162
167
  var init_SettingsManager = __esm({
163
168
  "src/lib/SettingsManager.ts"() {
164
169
  "use strict";
@@ -175,11 +180,24 @@ var init_SettingsManager = __esm({
175
180
  startAiAgent: z.boolean().default(true).describe("Launch Claude AI agent when starting this workflow type"),
176
181
  startTerminal: z.boolean().default(false).describe("Launch terminal window without dev server when starting this workflow type")
177
182
  });
183
+ WorkflowPermissionSchemaNoDefaults = z.object({
184
+ permissionMode: z.enum(["plan", "acceptEdits", "bypassPermissions", "default"]).optional().describe("Permission mode for Claude CLI in this workflow type"),
185
+ noVerify: z.boolean().optional().describe("Skip pre-commit hooks (--no-verify) when committing during finish workflow"),
186
+ startIde: z.boolean().optional().describe("Launch IDE (code) when starting this workflow type"),
187
+ startDevServer: z.boolean().optional().describe("Launch development server when starting this workflow type"),
188
+ startAiAgent: z.boolean().optional().describe("Launch Claude AI agent when starting this workflow type"),
189
+ startTerminal: z.boolean().optional().describe("Launch terminal window without dev server when starting this workflow type")
190
+ });
178
191
  WorkflowsSettingsSchema = z.object({
179
192
  issue: WorkflowPermissionSchema.optional(),
180
193
  pr: WorkflowPermissionSchema.optional(),
181
194
  regular: WorkflowPermissionSchema.optional()
182
195
  }).optional();
196
+ WorkflowsSettingsSchemaNoDefaults = z.object({
197
+ issue: WorkflowPermissionSchemaNoDefaults.optional(),
198
+ pr: WorkflowPermissionSchemaNoDefaults.optional(),
199
+ regular: WorkflowPermissionSchemaNoDefaults.optional()
200
+ }).optional();
183
201
  CapabilitiesSettingsSchema = z.object({
184
202
  web: z.object({
185
203
  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)")
@@ -188,6 +206,14 @@ var init_SettingsManager = __esm({
188
206
  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")
189
207
  }).optional()
190
208
  }).optional();
209
+ CapabilitiesSettingsSchemaNoDefaults = z.object({
210
+ web: z.object({
211
+ 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)")
212
+ }).optional(),
213
+ database: z.object({
214
+ 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")
215
+ }).optional()
216
+ }).optional();
191
217
  NeonSettingsSchema = z.object({
192
218
  projectId: z.string().min(1).regex(/^[a-zA-Z0-9-]+$/, "Neon project ID must contain only letters, numbers, and hyphens").describe('Neon project ID found in your project URL (e.g., "fantastic-fox-3566354")'),
193
219
  parentBranch: z.string().min(1).describe("Branch from which new database branches are created")
@@ -199,6 +225,9 @@ var init_SettingsManager = __esm({
199
225
  }).optional();
200
226
  IloomSettingsSchema = z.object({
201
227
  mainBranch: z.string().min(1, "Settings 'mainBranch' cannot be empty").optional().describe("Name of the main/primary branch for the repository"),
228
+ sourceEnvOnStart: z.boolean().default(false).describe(
229
+ "Source .env file when launching terminal processes (Claude, dev server, terminal). NOTE TO AI AGENTS: Before making changes to settings files, YOU MUST OFFER to check .env compatibility. WARNING: Unquoted values in .env files may break sourcing in bash/zsh shells. Before enabling, verify your .env file does not contain unquoted special characters (e.g., database URLs with ?, &, or other shell metacharacters). Shell compatibility issues may cause processes to fail or behave unexpectedly."
230
+ ),
202
231
  worktreePrefix: z.string().optional().refine(
203
232
  (val) => {
204
233
  if (val === void 0) return true;
@@ -228,28 +257,107 @@ var init_SettingsManager = __esm({
228
257
  capabilities: CapabilitiesSettingsSchema.describe("Project capability configurations"),
229
258
  databaseProviders: DatabaseProvidersSettingsSchema.describe("Database provider configurations"),
230
259
  issueManagement: z.object({
260
+ provider: z.enum(["github", "linear"]).optional().default("github").describe("Issue tracker provider (github, linear)"),
261
+ github: z.object({
262
+ remote: z.string().min(1, "Remote name cannot be empty").describe("Git remote name to use for GitHub operations")
263
+ }).optional(),
264
+ linear: z.object({
265
+ teamId: z.string().min(1, "Team ID cannot be empty").describe('Linear team identifier (e.g., "ENG", "PLAT")'),
266
+ branchFormat: z.string().optional().describe("Branch naming template for Linear issues"),
267
+ apiToken: z.string().optional().describe("Linear API token (lin_api_...). SECURITY: Store in settings.local.json only, never commit to source control.")
268
+ }).optional()
269
+ }).optional().describe("Issue management configuration"),
270
+ mergeBehavior: z.object({
271
+ mode: z.enum(["local", "github-pr"]).default("local"),
272
+ remote: z.string().optional()
273
+ }).optional().describe("Merge behavior configuration: local (merge locally) or github-pr (create PR)"),
274
+ ide: z.object({
275
+ type: z.enum(["vscode", "cursor", "webstorm", "sublime", "intellij", "windsurf"]).default("vscode").describe(
276
+ "IDE to launch when starting a loom. Options: vscode (Visual Studio Code), cursor (Cursor AI editor), webstorm (JetBrains WebStorm), sublime (Sublime Text), intellij (JetBrains IntelliJ IDEA), windsurf (Windsurf editor)."
277
+ )
278
+ }).optional().describe(
279
+ "IDE configuration for workspace launches. Controls which editor opens when you start a loom. Supports VSCode, Cursor, WebStorm, Sublime Text, IntelliJ, and Windsurf. Note: Color synchronization (title bar colors) only works with VSCode-compatible editors (vscode, cursor, windsurf)."
280
+ )
281
+ });
282
+ IloomSettingsSchemaNoDefaults = z.object({
283
+ mainBranch: z.string().min(1, "Settings 'mainBranch' cannot be empty").optional().describe("Name of the main/primary branch for the repository"),
284
+ sourceEnvOnStart: z.boolean().optional().describe(
285
+ "Source .env file when launching terminal processes (Claude, dev server, terminal). NOTE TO AI AGENTS: Before making changes to settings files, YOU MUST OFFER to check .env compatibility. WARNING: Unquoted values in .env files may break sourcing in bash/zsh shells. Before enabling, verify your .env file does not contain unquoted special characters (e.g., database URLs with ?, &, or other shell metacharacters). Shell compatibility issues may cause processes to fail or behave unexpectedly."
286
+ ),
287
+ worktreePrefix: z.string().optional().refine(
288
+ (val) => {
289
+ if (val === void 0) return true;
290
+ if (val === "") return true;
291
+ const allowedChars = /^[a-zA-Z0-9\-_/]+$/;
292
+ if (!allowedChars.test(val)) return false;
293
+ if (/^[-_/]+$/.test(val)) return false;
294
+ const segments = val.split("/");
295
+ for (const segment of segments) {
296
+ if (segment && /^[-_]+$/.test(segment)) {
297
+ return false;
298
+ }
299
+ }
300
+ return true;
301
+ },
302
+ {
303
+ message: "worktreePrefix contains invalid characters. Only alphanumeric characters, hyphens (-), underscores (_), and forward slashes (/) are allowed. Use forward slashes for nested directories."
304
+ }
305
+ ).describe(
306
+ "Prefix for worktree directories. Empty string disables prefix. Defaults to <repo-name>-looms if not set."
307
+ ),
308
+ protectedBranches: z.array(z.string().min(1, "Protected branch name cannot be empty")).optional().describe('List of branches that cannot be deleted (defaults to [mainBranch, "main", "master", "develop"])'),
309
+ workflows: WorkflowsSettingsSchemaNoDefaults.describe("Per-workflow-type permission configurations"),
310
+ agents: z.record(z.string(), AgentSettingsSchema).optional().nullable().describe(
311
+ "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-issue-reviewer (reviews code changes against requirements)"
312
+ ),
313
+ capabilities: CapabilitiesSettingsSchemaNoDefaults.describe("Project capability configurations"),
314
+ databaseProviders: DatabaseProvidersSettingsSchema.describe("Database provider configurations"),
315
+ issueManagement: z.object({
316
+ provider: z.enum(["github", "linear"]).optional().describe("Issue tracker provider (github, linear)"),
231
317
  github: z.object({
232
318
  remote: z.string().min(1, "Remote name cannot be empty").describe("Git remote name to use for GitHub operations")
319
+ }).optional(),
320
+ linear: z.object({
321
+ teamId: z.string().min(1, "Team ID cannot be empty").describe('Linear team identifier (e.g., "ENG", "PLAT")'),
322
+ branchFormat: z.string().optional().describe("Branch naming template for Linear issues"),
323
+ apiToken: z.string().optional().describe("Linear API token (lin_api_...). SECURITY: Store in settings.local.json only, never commit to source control.")
233
324
  }).optional()
234
- }).optional().describe("Issue management configuration")
325
+ }).optional().describe("Issue management configuration"),
326
+ mergeBehavior: z.object({
327
+ mode: z.enum(["local", "github-pr"]).optional(),
328
+ remote: z.string().optional()
329
+ }).optional().describe("Merge behavior configuration: local (merge locally) or github-pr (create PR)"),
330
+ ide: z.object({
331
+ type: z.enum(["vscode", "cursor", "webstorm", "sublime", "intellij", "windsurf"]).optional().describe(
332
+ "IDE to launch when starting a loom. Options: vscode (Visual Studio Code), cursor (Cursor AI editor), webstorm (JetBrains WebStorm), sublime (Sublime Text), intellij (JetBrains IntelliJ IDEA), windsurf (Windsurf editor)."
333
+ )
334
+ }).optional().describe(
335
+ "IDE configuration for workspace launches. Controls which editor opens when you start a loom. Supports VSCode, Cursor, WebStorm, Sublime Text, IntelliJ, and Windsurf. Note: Color synchronization (title bar colors) only works with VSCode-compatible editors (vscode, cursor, windsurf)."
336
+ )
235
337
  });
236
338
  SettingsManager = class {
237
339
  /**
238
- * Load settings from <PROJECT_ROOT>/.iloom/settings.json and settings.local.json
239
- * Merges settings.local.json over settings.json with priority
240
- * CLI overrides have highest priority if provided
241
- * Returns empty object if both files don't exist (not an error)
340
+ * Load settings from global, project, and local sources with proper precedence
341
+ * Merge hierarchy (lowest to highest priority):
342
+ * 1. Global settings (~/.config/iloom-ai/settings.json)
343
+ * 2. Project settings (<PROJECT_ROOT>/.iloom/settings.json)
344
+ * 3. Local settings (<PROJECT_ROOT>/.iloom/settings.local.json)
345
+ * 4. CLI overrides (--set flags)
346
+ * Returns empty object if all files don't exist (not an error)
242
347
  */
243
348
  async loadSettings(projectRoot, cliOverrides) {
244
349
  const root = this.getProjectRoot(projectRoot);
350
+ const globalSettings = await this.loadGlobalSettingsFile();
351
+ const globalSettingsPath = this.getGlobalSettingsPath();
352
+ logger.debug(`\u{1F30D} Global settings from ${globalSettingsPath}:`, JSON.stringify(globalSettings, null, 2));
245
353
  const baseSettings = await this.loadSettingsFile(root, "settings.json");
246
354
  const baseSettingsPath = path.join(root, ".iloom", "settings.json");
247
355
  logger.debug(`\u{1F4C4} Base settings from ${baseSettingsPath}:`, JSON.stringify(baseSettings, null, 2));
248
356
  const localSettings = await this.loadSettingsFile(root, "settings.local.json");
249
357
  const localSettingsPath = path.join(root, ".iloom", "settings.local.json");
250
358
  logger.debug(`\u{1F4C4} Local settings from ${localSettingsPath}:`, JSON.stringify(localSettings, null, 2));
251
- let merged = this.mergeSettings(baseSettings, localSettings);
252
- logger.debug("\u{1F504} After merging base + local settings:", JSON.stringify(merged, null, 2));
359
+ let merged = this.mergeSettings(this.mergeSettings(globalSettings, baseSettings), localSettings);
360
+ logger.debug("\u{1F504} After merging global + base + local settings:", JSON.stringify(merged, null, 2));
253
361
  if (cliOverrides && Object.keys(cliOverrides).length > 0) {
254
362
  logger.debug("\u2699\uFE0F CLI overrides to apply:", JSON.stringify(cliOverrides, null, 2));
255
363
  merged = this.mergeSettings(merged, cliOverrides);
@@ -281,6 +389,7 @@ Note: CLI overrides were applied. Check your --set arguments.`);
281
389
  /**
282
390
  * Load and parse a single settings file
283
391
  * Returns empty object if file doesn't exist (not an error)
392
+ * Uses non-defaulting schema to prevent polluting partial settings with defaults before merge
284
393
  */
285
394
  async loadSettingsFile(projectRoot, filename) {
286
395
  const settingsPath = path.join(projectRoot, ".iloom", filename);
@@ -294,16 +403,13 @@ Note: CLI overrides were applied. Check your --set arguments.`);
294
403
  `Failed to parse settings file at ${settingsPath}: ${error instanceof Error ? error.message : "Invalid JSON"}`
295
404
  );
296
405
  }
297
- try {
298
- const validated = IloomSettingsSchema.strict().parse(parsed);
299
- return validated;
300
- } catch (error) {
301
- if (error instanceof z.ZodError) {
302
- const errorMsg = this.formatAllZodErrors(error, filename);
303
- throw errorMsg;
304
- }
305
- throw error;
406
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
407
+ throw new Error(
408
+ `Settings validation failed at ${filename}:
409
+ - root: Expected object, received ${typeof parsed}`
410
+ );
306
411
  }
412
+ return parsed;
307
413
  } catch (error) {
308
414
  if (error.code === "ENOENT") {
309
415
  logger.debug(`No settings file found at ${settingsPath}, using defaults`);
@@ -357,6 +463,57 @@ ${errorMessages.join("\n")}`
357
463
  getProjectRoot(projectRoot) {
358
464
  return projectRoot ?? process.cwd();
359
465
  }
466
+ /**
467
+ * Get global config directory path (~/.config/iloom-ai)
468
+ */
469
+ getGlobalConfigDir() {
470
+ return path.join(os.homedir(), ".config", "iloom-ai");
471
+ }
472
+ /**
473
+ * Get global settings file path (~/.config/iloom-ai/settings.json)
474
+ */
475
+ getGlobalSettingsPath() {
476
+ return path.join(this.getGlobalConfigDir(), "settings.json");
477
+ }
478
+ /**
479
+ * Load and parse global settings file
480
+ * Returns empty object if file doesn't exist (not an error)
481
+ * Warns but returns empty object on validation/parse errors (graceful degradation)
482
+ */
483
+ async loadGlobalSettingsFile() {
484
+ const settingsPath = this.getGlobalSettingsPath();
485
+ try {
486
+ const content = await readFile(settingsPath, "utf-8");
487
+ let parsed;
488
+ try {
489
+ parsed = JSON.parse(content);
490
+ } catch (error) {
491
+ logger.warn(
492
+ `Failed to parse global settings file at ${settingsPath}: ${error instanceof Error ? error.message : "Invalid JSON"}. Ignoring global settings.`
493
+ );
494
+ return {};
495
+ }
496
+ try {
497
+ const validated = IloomSettingsSchemaNoDefaults.strict().parse(parsed);
498
+ return validated;
499
+ } catch (error) {
500
+ if (error instanceof z.ZodError) {
501
+ const errorMsg = this.formatAllZodErrors(error, "global settings");
502
+ logger.warn(`${errorMsg.message}. Ignoring global settings.`);
503
+ } else {
504
+ logger.warn(`Validation error in global settings: ${error instanceof Error ? error.message : "Unknown error"}. Ignoring global settings.`);
505
+ }
506
+ return {};
507
+ }
508
+ } catch (error) {
509
+ if (error.code === "ENOENT") {
510
+ logger.debug(`No global settings file found at ${settingsPath}`);
511
+ return {};
512
+ }
513
+ logger.warn(`Error reading global settings file at ${settingsPath}: ${error instanceof Error ? error.message : "Unknown error"}. Ignoring global settings.`);
514
+ return {};
515
+ }
516
+ }
360
517
  /**
361
518
  * Get effective protected branches list with mainBranch always included
362
519
  *
@@ -392,7 +549,7 @@ __export(terminal_exports, {
392
549
  openMultipleTerminalWindows: () => openMultipleTerminalWindows,
393
550
  openTerminalWindow: () => openTerminalWindow
394
551
  });
395
- import { execa as execa2 } from "execa";
552
+ import { execa as execa3 } from "execa";
396
553
  import { existsSync } from "fs";
397
554
  function detectPlatform() {
398
555
  const platform = process.platform;
@@ -413,10 +570,13 @@ async function openTerminalWindow(options) {
413
570
  `Terminal window launching not yet supported on ${platform}. Currently only macOS is supported.`
414
571
  );
415
572
  }
416
- const applescript = buildAppleScript(options);
573
+ const hasITerm2 = await detectITerm2();
574
+ const applescript = hasITerm2 ? buildITerm2SingleTabScript(options) : buildAppleScript(options);
417
575
  try {
418
- await execa2("osascript", ["-e", applescript]);
419
- await execa2("osascript", ["-e", 'tell application "Terminal" to activate']);
576
+ await execa3("osascript", ["-e", applescript]);
577
+ if (!hasITerm2) {
578
+ await execa3("osascript", ["-e", 'tell application "Terminal" to activate']);
579
+ }
420
580
  } catch (error) {
421
581
  throw new Error(
422
582
  `Failed to open terminal window: ${error instanceof Error ? error.message : "Unknown error"}`
@@ -465,6 +625,28 @@ function escapePathForAppleScript(path5) {
465
625
  function escapeForAppleScript(command) {
466
626
  return command.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
467
627
  }
628
+ function buildITerm2SingleTabScript(options) {
629
+ const command = buildCommandSequence(options);
630
+ let script = 'tell application id "com.googlecode.iterm2"\n';
631
+ script += " create window with default profile\n";
632
+ script += " set s1 to current session of current window\n\n";
633
+ if (options.backgroundColor) {
634
+ const { r, g, b } = options.backgroundColor;
635
+ script += ` set background color of s1 to {${Math.round(r * 257)}, ${Math.round(g * 257)}, ${Math.round(b * 257)}}
636
+ `;
637
+ }
638
+ script += ` tell s1 to write text "${escapeForAppleScript(command)}"
639
+
640
+ `;
641
+ if (options.title) {
642
+ script += ` set name of s1 to "${escapeForAppleScript(options.title)}"
643
+
644
+ `;
645
+ }
646
+ script += " activate\n";
647
+ script += "end tell";
648
+ return script;
649
+ }
468
650
  function buildCommandSequence(options) {
469
651
  const {
470
652
  workspacePath,
@@ -561,7 +743,7 @@ async function openMultipleTerminalWindows(optionsArray) {
561
743
  if (hasITerm2) {
562
744
  const applescript = buildITerm2MultiTabScript(optionsArray);
563
745
  try {
564
- await execa2("osascript", ["-e", applescript]);
746
+ await execa3("osascript", ["-e", applescript]);
565
747
  } catch (error) {
566
748
  throw new Error(
567
749
  `Failed to open iTerm2 window: ${error instanceof Error ? error.message : "Unknown error"}`
@@ -600,7 +782,7 @@ __export(color_exports, {
600
782
  rgbToHex: () => rgbToHex,
601
783
  saturateColor: () => saturateColor
602
784
  });
603
- import { createHash } from "crypto";
785
+ import { createHash as createHash2 } from "crypto";
604
786
  function getColorPalette() {
605
787
  return [
606
788
  // First 10 colors preserved for backward compatibility
@@ -707,7 +889,7 @@ function hexToRgb(hex) {
707
889
  return { r, g, b };
708
890
  }
709
891
  function generateColorFromBranchName(branchName) {
710
- const hash = createHash("sha256").update(branchName).digest("hex");
892
+ const hash = createHash2("sha256").update(branchName).digest("hex");
711
893
  const hashPrefix = hash.slice(0, 8);
712
894
  const palette = getColorPalette();
713
895
  const hashAsInt = parseInt(hashPrefix, 16);
@@ -759,349 +941,98 @@ var init_color = __esm({
759
941
  }
760
942
  });
761
943
 
762
- // src/utils/claude.ts
763
- var claude_exports = {};
764
- __export(claude_exports, {
765
- detectClaudeCli: () => detectClaudeCli,
766
- generateBranchName: () => generateBranchName,
767
- getClaudeVersion: () => getClaudeVersion,
768
- launchClaude: () => launchClaude,
769
- launchClaudeInNewTerminalWindow: () => launchClaudeInNewTerminalWindow
770
- });
771
- import { execa as execa3 } from "execa";
772
- import { existsSync as existsSync2 } from "fs";
773
- import { join } from "path";
774
- async function detectClaudeCli() {
775
- try {
776
- await execa3("command", ["-v", "claude"], {
777
- shell: true,
778
- timeout: 5e3
779
- });
780
- return true;
781
- } catch (error) {
782
- logger.debug("Claude CLI not available", { error });
783
- return false;
784
- }
785
- }
786
- async function getClaudeVersion() {
944
+ // src/lib/WorkspaceManager.ts
945
+ var WorkspaceManager = class {
946
+ // TODO: Implement in Issue #6
947
+ };
948
+
949
+ // src/lib/GitWorktreeManager.ts
950
+ import path3 from "path";
951
+ import fs from "fs-extra";
952
+
953
+ // src/utils/git.ts
954
+ init_logger();
955
+ import path2 from "path";
956
+ import { execa } from "execa";
957
+ async function executeGitCommand(args, options) {
787
958
  try {
788
- const result = await execa3("claude", ["--version"], {
789
- timeout: 5e3
959
+ const result = await execa("git", args, {
960
+ cwd: (options == null ? void 0 : options.cwd) ?? process.cwd(),
961
+ timeout: (options == null ? void 0 : options.timeout) ?? 3e4,
962
+ encoding: "utf8",
963
+ stdio: (options == null ? void 0 : options.stdio) ?? "pipe",
964
+ verbose: logger.isDebugEnabled()
790
965
  });
791
- return result.stdout.trim();
966
+ return result.stdout;
792
967
  } catch (error) {
793
- logger.warn("Failed to get Claude version", { error });
794
- return null;
968
+ const execaError = error;
969
+ const stderr = execaError.stderr ?? execaError.message ?? "Unknown Git error";
970
+ throw new Error(`Git command failed: ${stderr}`);
795
971
  }
796
972
  }
797
- function parseJsonStreamOutput(output) {
798
- try {
799
- const lines = output.split("\n").filter((line) => line.trim());
800
- let lastResult = "";
801
- for (const line of lines) {
802
- try {
803
- const jsonObj = JSON.parse(line);
804
- if (jsonObj && typeof jsonObj === "object" && jsonObj.type === "result" && "result" in jsonObj) {
805
- lastResult = jsonObj.result;
806
- }
807
- } catch {
973
+ function parseWorktreeList(output, defaultBranch) {
974
+ var _a, _b;
975
+ const worktrees = [];
976
+ const lines = output.trim().split("\n");
977
+ let i = 0;
978
+ while (i < lines.length) {
979
+ const pathLine = lines[i];
980
+ if (!(pathLine == null ? void 0 : pathLine.startsWith("worktree "))) {
981
+ i++;
982
+ continue;
983
+ }
984
+ const pathMatch = pathLine.match(/^worktree (.+)$/);
985
+ if (!pathMatch) {
986
+ i++;
987
+ continue;
988
+ }
989
+ let branch = "";
990
+ let commit = "";
991
+ let detached = false;
992
+ let bare = false;
993
+ let locked = false;
994
+ let lockReason;
995
+ i++;
996
+ while (i < lines.length && !((_a = lines[i]) == null ? void 0 : _a.startsWith("worktree "))) {
997
+ const line = (_b = lines[i]) == null ? void 0 : _b.trim();
998
+ if (!line) {
999
+ i++;
808
1000
  continue;
809
1001
  }
1002
+ if (line === "bare") {
1003
+ bare = true;
1004
+ branch = defaultBranch ?? "main";
1005
+ } else if (line === "detached") {
1006
+ detached = true;
1007
+ branch = "HEAD";
1008
+ } else if (line.startsWith("locked")) {
1009
+ locked = true;
1010
+ const lockMatch = line.match(/^locked (.+)$/);
1011
+ lockReason = lockMatch == null ? void 0 : lockMatch[1];
1012
+ branch = branch || "unknown";
1013
+ } else if (line.startsWith("HEAD ")) {
1014
+ const commitMatch = line.match(/^HEAD ([a-f0-9]+)/);
1015
+ if (commitMatch) {
1016
+ commit = commitMatch[1] ?? "";
1017
+ }
1018
+ } else if (line.startsWith("branch ")) {
1019
+ const branchMatch = line.match(/^branch refs\/heads\/(.+)$/);
1020
+ branch = (branchMatch == null ? void 0 : branchMatch[1]) ?? line.replace("branch ", "");
1021
+ }
1022
+ i++;
810
1023
  }
811
- return lastResult || output;
812
- } catch {
813
- return output;
814
- }
815
- }
816
- async function launchClaude(prompt, options = {}) {
817
- const { model, permissionMode, addDir, headless = false, appendSystemPrompt, mcpConfig, allowedTools, disallowedTools, agents } = options;
818
- const args = [];
819
- if (headless) {
820
- args.push("-p");
821
- args.push("--output-format", "stream-json");
822
- args.push("--verbose");
823
- }
824
- if (model) {
825
- args.push("--model", model);
826
- }
827
- if (permissionMode && permissionMode !== "default") {
828
- args.push("--permission-mode", permissionMode);
829
- }
830
- if (addDir) {
831
- args.push("--add-dir", addDir);
832
- }
833
- args.push("--add-dir", "/tmp");
834
- if (appendSystemPrompt) {
835
- args.push("--append-system-prompt", appendSystemPrompt);
836
- }
837
- if (mcpConfig && mcpConfig.length > 0) {
838
- for (const config of mcpConfig) {
839
- args.push("--mcp-config", JSON.stringify(config));
1024
+ const worktree = {
1025
+ path: pathMatch[1] ?? "",
1026
+ branch,
1027
+ commit,
1028
+ bare,
1029
+ detached,
1030
+ locked
1031
+ };
1032
+ if (lockReason !== void 0) {
1033
+ worktree.lockReason = lockReason;
840
1034
  }
841
- }
842
- if (allowedTools && allowedTools.length > 0) {
843
- args.push("--allowed-tools", ...allowedTools);
844
- }
845
- if (disallowedTools && disallowedTools.length > 0) {
846
- args.push("--disallowed-tools", ...disallowedTools);
847
- }
848
- if (agents) {
849
- args.push("--agents", JSON.stringify(agents));
850
- }
851
- try {
852
- if (headless) {
853
- const isDebugMode = logger.isDebugEnabled();
854
- const execaOptions = {
855
- input: prompt,
856
- timeout: 0,
857
- // Disable timeout for long responses
858
- ...addDir && { cwd: addDir },
859
- // Run Claude in the worktree directory
860
- verbose: isDebugMode,
861
- ...isDebugMode && { stdio: ["pipe", "pipe", "pipe"] }
862
- // Enable streaming in debug mode
863
- };
864
- const subprocess = execa3("claude", args, execaOptions);
865
- const isJsonStreamFormat = args.includes("--output-format") && args.includes("stream-json");
866
- let outputBuffer = "";
867
- let isStreaming = false;
868
- let isFirstProgress = true;
869
- if (subprocess.stdout && typeof subprocess.stdout.on === "function") {
870
- isStreaming = true;
871
- subprocess.stdout.on("data", (chunk) => {
872
- const text = chunk.toString();
873
- outputBuffer += text;
874
- if (isDebugMode) {
875
- process.stdout.write(text);
876
- } else {
877
- if (isFirstProgress) {
878
- process.stdout.write("\u{1F916} .");
879
- isFirstProgress = false;
880
- } else {
881
- process.stdout.write(".");
882
- }
883
- }
884
- });
885
- }
886
- const result = await subprocess;
887
- if (isStreaming) {
888
- const rawOutput = outputBuffer.trim();
889
- if (!isDebugMode) {
890
- process.stdout.write("\n");
891
- }
892
- return isJsonStreamFormat ? parseJsonStreamOutput(rawOutput) : rawOutput;
893
- } else {
894
- if (isDebugMode) {
895
- process.stdout.write(result.stdout);
896
- if (result.stdout && !result.stdout.endsWith("\n")) {
897
- process.stdout.write("\n");
898
- }
899
- } else {
900
- process.stdout.write("\u{1F916} .");
901
- process.stdout.write("\n");
902
- }
903
- const rawOutput = result.stdout.trim();
904
- return isJsonStreamFormat ? parseJsonStreamOutput(rawOutput) : rawOutput;
905
- }
906
- } else {
907
- await execa3("claude", [...args, "--", prompt], {
908
- ...addDir && { cwd: addDir },
909
- stdio: "inherit",
910
- // Let user interact directly in current terminal
911
- timeout: 0,
912
- // Disable timeout
913
- verbose: logger.isDebugEnabled()
914
- });
915
- return;
916
- }
917
- } catch (error) {
918
- const execaError = error;
919
- const errorMessage = execaError.stderr ?? execaError.message ?? "Unknown Claude CLI error";
920
- throw new Error(`Claude CLI error: ${errorMessage}`);
921
- }
922
- }
923
- async function launchClaudeInNewTerminalWindow(_prompt, options) {
924
- const { workspacePath, branchName, oneShot = "default", port, setArguments, executablePath } = options;
925
- if (!workspacePath) {
926
- throw new Error("workspacePath is required for terminal window launch");
927
- }
928
- const { openTerminalWindow: openTerminalWindow2 } = await Promise.resolve().then(() => (init_terminal(), terminal_exports));
929
- const executable = executablePath ?? "iloom";
930
- let launchCommand = `${executable} spin`;
931
- if (oneShot !== "default") {
932
- launchCommand += ` --one-shot=${oneShot}`;
933
- }
934
- if (setArguments && setArguments.length > 0) {
935
- for (const setArg of setArguments) {
936
- launchCommand += ` --set ${setArg}`;
937
- }
938
- }
939
- let backgroundColor;
940
- if (branchName) {
941
- try {
942
- const { generateColorFromBranchName: generateColorFromBranchName2 } = await Promise.resolve().then(() => (init_color(), color_exports));
943
- const colorData = generateColorFromBranchName2(branchName);
944
- backgroundColor = colorData.rgb;
945
- } catch (error) {
946
- logger.warn(
947
- `Failed to generate terminal color: ${error instanceof Error ? error.message : "Unknown error"}`
948
- );
949
- }
950
- }
951
- const hasEnvFile = existsSync2(join(workspacePath, ".env"));
952
- await openTerminalWindow2({
953
- workspacePath,
954
- command: launchCommand,
955
- ...backgroundColor && { backgroundColor },
956
- includeEnvSetup: hasEnvFile,
957
- // source .env only if it exists
958
- ...port !== void 0 && { port, includePortExport: true }
959
- });
960
- }
961
- async function generateBranchName(issueTitle, issueNumber, model = "haiku") {
962
- try {
963
- const isAvailable = await detectClaudeCli();
964
- if (!isAvailable) {
965
- logger.warn("Claude CLI not available, using fallback branch name");
966
- return `feat/issue-${issueNumber}`;
967
- }
968
- logger.debug("Generating branch name with Claude", { issueNumber, issueTitle });
969
- const prompt = `<Task>
970
- Generate a git branch name for the following issue:
971
- <Issue>
972
- <IssueNumber>${issueNumber}</IssueNumber>
973
- <IssueTitle>${issueTitle}</IssueTitle>
974
- </Issue>
975
-
976
- <Requirements>
977
- <IssueNumber>Must use this exact issue number: ${issueNumber}</IssueNumber>
978
- <Format>Format must be: {prefix}/issue-${issueNumber}-{description}</Format>
979
- <Prefix>Prefix must be one of: feat, fix, docs, refactor, test, chore</Prefix>
980
- <MaxLength>Maximum 50 characters total</MaxLength>
981
- <Characters>Only lowercase letters, numbers, and hyphens allowed</Characters>
982
- <Output>Reply with ONLY the branch name, nothing else</Output>
983
- </Requirements>
984
- </Task>`;
985
- logger.debug("Sending prompt to Claude", { prompt });
986
- const result = await launchClaude(prompt, {
987
- model,
988
- headless: true
989
- });
990
- const branchName = result.trim();
991
- logger.debug("Claude returned branch name", { branchName, issueNumber });
992
- if (!branchName || !isValidBranchName(branchName, issueNumber)) {
993
- logger.warn("Invalid branch name from Claude, using fallback", { branchName });
994
- return `feat/issue-${issueNumber}`;
995
- }
996
- return branchName;
997
- } catch (error) {
998
- logger.warn("Failed to generate branch name with Claude", { error });
999
- return `feat/issue-${issueNumber}`;
1000
- }
1001
- }
1002
- function isValidBranchName(name, issueNumber) {
1003
- const pattern = new RegExp(`^(feat|fix|docs|refactor|test|chore)/issue-${issueNumber}-[a-z0-9-]+$`);
1004
- return pattern.test(name) && name.length <= 50;
1005
- }
1006
- var init_claude = __esm({
1007
- "src/utils/claude.ts"() {
1008
- "use strict";
1009
- init_logger();
1010
- }
1011
- });
1012
-
1013
- // src/lib/WorkspaceManager.ts
1014
- var WorkspaceManager = class {
1015
- // TODO: Implement in Issue #6
1016
- };
1017
-
1018
- // src/lib/GitWorktreeManager.ts
1019
- import path3 from "path";
1020
- import fs from "fs-extra";
1021
-
1022
- // src/utils/git.ts
1023
- init_logger();
1024
- import path2 from "path";
1025
- import { execa } from "execa";
1026
- async function executeGitCommand(args, options) {
1027
- try {
1028
- const result = await execa("git", args, {
1029
- cwd: (options == null ? void 0 : options.cwd) ?? process.cwd(),
1030
- timeout: (options == null ? void 0 : options.timeout) ?? 3e4,
1031
- encoding: "utf8",
1032
- stdio: (options == null ? void 0 : options.stdio) ?? "pipe",
1033
- verbose: logger.isDebugEnabled()
1034
- });
1035
- return result.stdout;
1036
- } catch (error) {
1037
- const execaError = error;
1038
- const stderr = execaError.stderr ?? execaError.message ?? "Unknown Git error";
1039
- throw new Error(`Git command failed: ${stderr}`);
1040
- }
1041
- }
1042
- function parseWorktreeList(output, defaultBranch) {
1043
- var _a, _b;
1044
- const worktrees = [];
1045
- const lines = output.trim().split("\n");
1046
- let i = 0;
1047
- while (i < lines.length) {
1048
- const pathLine = lines[i];
1049
- if (!(pathLine == null ? void 0 : pathLine.startsWith("worktree "))) {
1050
- i++;
1051
- continue;
1052
- }
1053
- const pathMatch = pathLine.match(/^worktree (.+)$/);
1054
- if (!pathMatch) {
1055
- i++;
1056
- continue;
1057
- }
1058
- let branch = "";
1059
- let commit = "";
1060
- let detached = false;
1061
- let bare = false;
1062
- let locked = false;
1063
- let lockReason;
1064
- i++;
1065
- while (i < lines.length && !((_a = lines[i]) == null ? void 0 : _a.startsWith("worktree "))) {
1066
- const line = (_b = lines[i]) == null ? void 0 : _b.trim();
1067
- if (!line) {
1068
- i++;
1069
- continue;
1070
- }
1071
- if (line === "bare") {
1072
- bare = true;
1073
- branch = defaultBranch ?? "main";
1074
- } else if (line === "detached") {
1075
- detached = true;
1076
- branch = "HEAD";
1077
- } else if (line.startsWith("locked")) {
1078
- locked = true;
1079
- const lockMatch = line.match(/^locked (.+)$/);
1080
- lockReason = lockMatch == null ? void 0 : lockMatch[1];
1081
- branch = branch || "unknown";
1082
- } else if (line.startsWith("HEAD ")) {
1083
- const commitMatch = line.match(/^HEAD ([a-f0-9]+)/);
1084
- if (commitMatch) {
1085
- commit = commitMatch[1] ?? "";
1086
- }
1087
- } else if (line.startsWith("branch ")) {
1088
- const branchMatch = line.match(/^branch refs\/heads\/(.+)$/);
1089
- branch = (branchMatch == null ? void 0 : branchMatch[1]) ?? line.replace("branch ", "");
1090
- }
1091
- i++;
1092
- }
1093
- const worktree = {
1094
- path: pathMatch[1] ?? "",
1095
- branch,
1096
- commit,
1097
- bare,
1098
- detached,
1099
- locked
1100
- };
1101
- if (lockReason !== void 0) {
1102
- worktree.lockReason = lockReason;
1103
- }
1104
- worktrees.push(worktree);
1035
+ worktrees.push(worktree);
1105
1036
  }
1106
1037
  return worktrees;
1107
1038
  }
@@ -1144,6 +1075,28 @@ function extractPRNumber(branchName) {
1144
1075
  }
1145
1076
  return null;
1146
1077
  }
1078
+ function extractIssueNumber(branchName) {
1079
+ const newFormatPattern = /issue-([^_]+)__/i;
1080
+ const newMatch = branchName.match(newFormatPattern);
1081
+ if (newMatch == null ? void 0 : newMatch[1]) return newMatch[1];
1082
+ const oldFormatPattern = /issue-(\d+)(?:-|$)/i;
1083
+ const oldMatch = branchName.match(oldFormatPattern);
1084
+ if (oldMatch == null ? void 0 : oldMatch[1]) return oldMatch[1];
1085
+ const alphanumericEndPattern = /issue-([^_\s/]+)$/i;
1086
+ const alphanumericMatch = branchName.match(alphanumericEndPattern);
1087
+ if (alphanumericMatch == null ? void 0 : alphanumericMatch[1]) return alphanumericMatch[1];
1088
+ const legacyPatterns = [
1089
+ /issue_(\d+)/i,
1090
+ // issue_42
1091
+ /^(\d+)-/
1092
+ // 42-feature-name
1093
+ ];
1094
+ for (const pattern of legacyPatterns) {
1095
+ const match = branchName.match(pattern);
1096
+ if (match == null ? void 0 : match[1]) return match[1];
1097
+ }
1098
+ return null;
1099
+ }
1147
1100
  function isWorktreePath(path5) {
1148
1101
  const worktreePatterns = [
1149
1102
  /\/worktrees?\//i,
@@ -1737,7 +1690,7 @@ var GitWorktreeManager = class {
1737
1690
  */
1738
1691
  async findWorktreeForIssue(issueNumber) {
1739
1692
  const worktrees = await this.listWorktrees({ porcelain: true });
1740
- const pattern = new RegExp(`(?:^|[/_-])issue-${issueNumber}(?:-|$)`);
1693
+ const pattern = new RegExp(`(?:^|[/_-])issue-${issueNumber}(?:-|__|$)`);
1741
1694
  return worktrees.find((wt) => pattern.test(wt.branch)) ?? null;
1742
1695
  }
1743
1696
  /**
@@ -1811,9 +1764,9 @@ var GitHubError = class extends Error {
1811
1764
 
1812
1765
  // src/utils/github.ts
1813
1766
  init_logger();
1814
- import { execa as execa4 } from "execa";
1767
+ import { execa as execa2 } from "execa";
1815
1768
  async function executeGhCommand(args, options) {
1816
- const result = await execa4("gh", args, {
1769
+ const result = await execa2("gh", args, {
1817
1770
  cwd: (options == null ? void 0 : options.cwd) ?? process.cwd(),
1818
1771
  timeout: (options == null ? void 0 : options.timeout) ?? 3e4,
1819
1772
  encoding: "utf8"
@@ -1955,21 +1908,6 @@ async function updateProjectItemField(itemId, projectId, fieldId, optionId) {
1955
1908
  "json"
1956
1909
  ]);
1957
1910
  }
1958
- var SimpleBranchNameStrategy = class {
1959
- async generate(issueNumber, title) {
1960
- const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").substring(0, 20);
1961
- return `feat/issue-${issueNumber}-${slug}`;
1962
- }
1963
- };
1964
- var ClaudeBranchNameStrategy = class {
1965
- constructor(claudeModel = "haiku") {
1966
- this.claudeModel = claudeModel;
1967
- }
1968
- async generate(issueNumber, title) {
1969
- const { generateBranchName: generateBranchName2 } = await Promise.resolve().then(() => (init_claude(), claude_exports));
1970
- return generateBranchName2(title, issueNumber, this.claudeModel);
1971
- }
1972
- };
1973
1911
  async function createIssue(title, body, options) {
1974
1912
  const { repo, labels } = options ?? {};
1975
1913
  logger.debug("Creating GitHub issue", { title, repo, labels });
@@ -1994,7 +1932,7 @@ async function createIssue(title, body, options) {
1994
1932
  if (!repo) {
1995
1933
  execaOptions.cwd = process.cwd();
1996
1934
  }
1997
- const result = await execa4("gh", args, execaOptions);
1935
+ const result = await execa2("gh", args, execaOptions);
1998
1936
  const urlMatch = result.stdout.trim().match(/https:\/\/github\.com\/[^/]+\/[^/]+\/issues\/(\d+)/);
1999
1937
  if (!(urlMatch == null ? void 0 : urlMatch[1])) {
2000
1938
  throw new Error(`Failed to parse issue URL from gh output: ${result.stdout}`);
@@ -2048,35 +1986,29 @@ async function promptConfirmation(message, defaultValue = false) {
2048
1986
  // src/lib/GitHubService.ts
2049
1987
  var GitHubService = class {
2050
1988
  constructor(options) {
2051
- if (options == null ? void 0 : options.branchNameStrategy) {
2052
- this.defaultBranchNameStrategy = options.branchNameStrategy;
2053
- } else if ((options == null ? void 0 : options.useClaude) !== false) {
2054
- this.defaultBranchNameStrategy = new ClaudeBranchNameStrategy(
2055
- options == null ? void 0 : options.claudeModel
2056
- );
2057
- } else {
2058
- this.defaultBranchNameStrategy = new SimpleBranchNameStrategy();
2059
- }
1989
+ // IssueTracker interface implementation
1990
+ this.providerName = "github";
1991
+ this.supportsPullRequests = true;
2060
1992
  this.prompter = (options == null ? void 0 : options.prompter) ?? promptConfirmation;
2061
1993
  }
2062
- // Input detection
1994
+ // Input detection - IssueTracker interface implementation
2063
1995
  async detectInputType(input, repo) {
2064
1996
  const numberMatch = input.match(/^#?(\d+)$/);
2065
1997
  if (!(numberMatch == null ? void 0 : numberMatch[1])) {
2066
- return { type: "unknown", number: null, rawInput: input };
1998
+ return { type: "unknown", identifier: null, rawInput: input };
2067
1999
  }
2068
2000
  const number = parseInt(numberMatch[1], 10);
2069
2001
  logger.debug("Checking if input is a PR", { number });
2070
2002
  const pr = await this.isValidPR(number, repo);
2071
2003
  if (pr) {
2072
- return { type: "pr", number, rawInput: input };
2004
+ return { type: "pr", identifier: number.toString(), rawInput: input };
2073
2005
  }
2074
2006
  logger.debug("Checking if input is an issue", { number });
2075
2007
  const issue = await this.isValidIssue(number, repo);
2076
2008
  if (issue) {
2077
- return { type: "issue", number, rawInput: input };
2009
+ return { type: "issue", identifier: number.toString(), rawInput: input };
2078
2010
  }
2079
- return { type: "unknown", number: null, rawInput: input };
2011
+ return { type: "unknown", identifier: null, rawInput: input };
2080
2012
  }
2081
2013
  // Issue fetching with validation
2082
2014
  async fetchIssue(issueNumber, repo) {
@@ -2170,17 +2102,6 @@ var GitHubService = class {
2170
2102
  }
2171
2103
  }
2172
2104
  }
2173
- // Branch name generation using strategy pattern
2174
- async generateBranchName(options) {
2175
- const { issueNumber, title, strategy } = options;
2176
- const nameStrategy = strategy ?? this.defaultBranchNameStrategy;
2177
- logger.debug("Generating branch name", {
2178
- issueNumber,
2179
- title,
2180
- strategy: nameStrategy.constructor.name
2181
- });
2182
- return nameStrategy.generate(issueNumber, title);
2183
- }
2184
2105
  // Issue creation
2185
2106
  async createIssue(title, body, repository, labels) {
2186
2107
  return createIssue(title, body, { repo: repository, labels });
@@ -2315,33 +2236,392 @@ State: ${entity.state}`;
2315
2236
  async promptUserConfirmation(message) {
2316
2237
  return this.prompter(message);
2317
2238
  }
2318
- // Allow setting strategy at runtime for specific operations
2319
- setDefaultBranchNameStrategy(strategy) {
2320
- this.defaultBranchNameStrategy = strategy;
2321
- }
2322
- // Get current strategy for testing
2323
- getBranchNameStrategy() {
2324
- return this.defaultBranchNameStrategy;
2325
- }
2326
2239
  };
2327
2240
 
2328
- // src/lib/EnvironmentManager.ts
2329
- init_logger();
2330
- import fs2 from "fs-extra";
2241
+ // src/types/linear.ts
2242
+ var LinearServiceError = class _LinearServiceError extends Error {
2243
+ constructor(code, message, details) {
2244
+ super(message);
2245
+ this.code = code;
2246
+ this.details = details;
2247
+ this.name = "LinearServiceError";
2248
+ if (Error.captureStackTrace) {
2249
+ Error.captureStackTrace(this, _LinearServiceError);
2250
+ }
2251
+ }
2252
+ };
2331
2253
 
2332
- // src/utils/env.ts
2254
+ // src/utils/linear.ts
2255
+ import { LinearClient } from "@linear/sdk";
2333
2256
  init_logger();
2334
- import dotenvFlow from "dotenv-flow";
2335
- function parseEnvFile(content) {
2336
- const envMap = /* @__PURE__ */ new Map();
2337
- const lines = content.split("\n");
2338
- for (const line of lines) {
2339
- const trimmedLine = line.trim();
2340
- if (!trimmedLine || trimmedLine.startsWith("#")) {
2341
- continue;
2257
+ function slugifyTitle(title, maxLength = 50) {
2258
+ const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
2259
+ if (slug.length <= maxLength) {
2260
+ return slug;
2261
+ }
2262
+ const parts = slug.split("-");
2263
+ let result = "";
2264
+ for (const part of parts) {
2265
+ const candidate = result ? `${result}-${part}` : part;
2266
+ if (candidate.length > maxLength) {
2267
+ break;
2342
2268
  }
2343
- const cleanLine = trimmedLine.startsWith("export ") ? trimmedLine.substring(7) : trimmedLine;
2344
- const equalsIndex = cleanLine.indexOf("=");
2269
+ result = candidate;
2270
+ }
2271
+ return result || slug.slice(0, maxLength);
2272
+ }
2273
+ function buildLinearIssueUrl(identifier, title) {
2274
+ const base = `https://linear.app/issue/${identifier}`;
2275
+ if (title) {
2276
+ const slug = slugifyTitle(title);
2277
+ return slug ? `${base}/${slug}` : base;
2278
+ }
2279
+ return base;
2280
+ }
2281
+ function getLinearApiToken() {
2282
+ const token = process.env.LINEAR_API_TOKEN;
2283
+ if (!token) {
2284
+ throw new LinearServiceError(
2285
+ "UNAUTHORIZED",
2286
+ "LINEAR_API_TOKEN not set. Configure in settings.local.json or set environment variable."
2287
+ );
2288
+ }
2289
+ return token;
2290
+ }
2291
+ function createLinearClient() {
2292
+ return new LinearClient({ apiKey: getLinearApiToken() });
2293
+ }
2294
+ function handleLinearError(error, context) {
2295
+ logger.debug(`${context}: Handling error`, { error });
2296
+ const errorMessage = error instanceof Error ? error.message : String(error);
2297
+ if (errorMessage.includes("not found") || errorMessage.includes("Not found")) {
2298
+ throw new LinearServiceError("NOT_FOUND", "Linear issue or resource not found", { error });
2299
+ }
2300
+ if (errorMessage.includes("unauthorized") || errorMessage.includes("Unauthorized") || errorMessage.includes("Invalid API key")) {
2301
+ throw new LinearServiceError(
2302
+ "UNAUTHORIZED",
2303
+ "Linear authentication failed. Check LINEAR_API_TOKEN.",
2304
+ { error }
2305
+ );
2306
+ }
2307
+ if (errorMessage.includes("rate limit")) {
2308
+ throw new LinearServiceError("RATE_LIMITED", "Linear API rate limit exceeded", { error });
2309
+ }
2310
+ throw new LinearServiceError("CLI_ERROR", `Linear SDK error: ${errorMessage}`, { error });
2311
+ }
2312
+ async function fetchLinearIssue(identifier) {
2313
+ try {
2314
+ logger.debug(`Fetching Linear issue: ${identifier}`);
2315
+ const client = createLinearClient();
2316
+ const issue = await client.issue(identifier);
2317
+ if (!issue) {
2318
+ throw new LinearServiceError("NOT_FOUND", `Linear issue ${identifier} not found`);
2319
+ }
2320
+ const result = {
2321
+ id: issue.id,
2322
+ identifier: issue.identifier,
2323
+ title: issue.title,
2324
+ url: issue.url,
2325
+ createdAt: issue.createdAt.toISOString(),
2326
+ updatedAt: issue.updatedAt.toISOString()
2327
+ };
2328
+ if (issue.description) {
2329
+ result.description = issue.description;
2330
+ }
2331
+ if (issue.state) {
2332
+ const state = await issue.state;
2333
+ if (state == null ? void 0 : state.name) {
2334
+ result.state = state.name;
2335
+ }
2336
+ }
2337
+ return result;
2338
+ } catch (error) {
2339
+ if (error instanceof LinearServiceError) {
2340
+ throw error;
2341
+ }
2342
+ handleLinearError(error, "fetchLinearIssue");
2343
+ }
2344
+ }
2345
+ async function createLinearIssue(title, body, teamKey, _labels) {
2346
+ try {
2347
+ logger.debug(`Creating Linear issue in team ${teamKey}: ${title}`);
2348
+ const client = createLinearClient();
2349
+ const teams = await client.teams();
2350
+ const team = teams.nodes.find((t) => t.key === teamKey);
2351
+ if (!team) {
2352
+ throw new LinearServiceError("NOT_FOUND", `Linear team ${teamKey} not found`);
2353
+ }
2354
+ const issueInput = {
2355
+ teamId: team.id,
2356
+ title
2357
+ };
2358
+ if (body) {
2359
+ issueInput.description = body;
2360
+ }
2361
+ const payload = await client.createIssue(issueInput);
2362
+ const issue = await payload.issue;
2363
+ if (!issue) {
2364
+ throw new LinearServiceError("CLI_ERROR", "Failed to create Linear issue");
2365
+ }
2366
+ const url = issue.url ?? buildLinearIssueUrl(issue.identifier, title);
2367
+ return {
2368
+ identifier: issue.identifier,
2369
+ url
2370
+ };
2371
+ } catch (error) {
2372
+ if (error instanceof LinearServiceError) {
2373
+ throw error;
2374
+ }
2375
+ handleLinearError(error, "createLinearIssue");
2376
+ }
2377
+ }
2378
+ async function updateLinearIssueState(identifier, stateName) {
2379
+ try {
2380
+ logger.debug(`Updating Linear issue ${identifier} state to: ${stateName}`);
2381
+ const client = createLinearClient();
2382
+ const issue = await client.issue(identifier);
2383
+ if (!issue) {
2384
+ throw new LinearServiceError("NOT_FOUND", `Linear issue ${identifier} not found`);
2385
+ }
2386
+ const team = await issue.team;
2387
+ if (!team) {
2388
+ throw new LinearServiceError("CLI_ERROR", "Issue has no team");
2389
+ }
2390
+ const states = await team.states();
2391
+ const state = states.nodes.find((s) => s.name === stateName);
2392
+ if (!state) {
2393
+ throw new LinearServiceError(
2394
+ "NOT_FOUND",
2395
+ `State "${stateName}" not found in team ${team.key}`
2396
+ );
2397
+ }
2398
+ await client.updateIssue(issue.id, {
2399
+ stateId: state.id
2400
+ });
2401
+ } catch (error) {
2402
+ if (error instanceof LinearServiceError) {
2403
+ throw error;
2404
+ }
2405
+ handleLinearError(error, "updateLinearIssueState");
2406
+ }
2407
+ }
2408
+
2409
+ // src/lib/LinearService.ts
2410
+ init_logger();
2411
+ var LinearService = class {
2412
+ constructor(config, options) {
2413
+ // IssueTracker interface implementation
2414
+ this.providerName = "linear";
2415
+ this.supportsPullRequests = false;
2416
+ this.config = config ?? {};
2417
+ this.prompter = (options == null ? void 0 : options.prompter) ?? promptConfirmation;
2418
+ }
2419
+ /**
2420
+ * Detect if input matches Linear identifier format (TEAM-NUMBER)
2421
+ * @param input - User input string
2422
+ * @param _repo - Repository (unused for Linear)
2423
+ * @returns Detection result with type and identifier
2424
+ */
2425
+ async detectInputType(input, _repo) {
2426
+ logger.debug(`LinearService.detectInputType called with input: "${input}"`);
2427
+ const linearPattern = /^([A-Z]{2,}-\d+)$/i;
2428
+ const match = input.match(linearPattern);
2429
+ if (!(match == null ? void 0 : match[1])) {
2430
+ logger.debug(`LinearService: Input "${input}" does not match Linear pattern`);
2431
+ return { type: "unknown", identifier: null, rawInput: input };
2432
+ }
2433
+ const identifier = match[1].toUpperCase();
2434
+ logger.debug(`LinearService: Matched Linear identifier: ${identifier}`);
2435
+ logger.debug(`LinearService: Checking if ${identifier} is a valid Linear issue via SDK`);
2436
+ const issue = await this.isValidIssue(identifier);
2437
+ if (issue) {
2438
+ logger.debug(`LinearService: Issue ${identifier} found: "${issue.title}"`);
2439
+ return { type: "issue", identifier, rawInput: input };
2440
+ }
2441
+ logger.debug(`LinearService: Issue ${identifier} NOT found by SDK`);
2442
+ return { type: "unknown", identifier: null, rawInput: input };
2443
+ }
2444
+ /**
2445
+ * Fetch a Linear issue by identifier
2446
+ * @param identifier - Linear issue identifier (string or number)
2447
+ * @param _repo - Repository (unused for Linear)
2448
+ * @returns Generic Issue type
2449
+ * @throws LinearServiceError if issue not found
2450
+ */
2451
+ async fetchIssue(identifier, _repo) {
2452
+ const linearIssue = await fetchLinearIssue(String(identifier));
2453
+ return this.mapLinearIssueToIssue(linearIssue);
2454
+ }
2455
+ /**
2456
+ * Check if an issue identifier is valid (silent validation)
2457
+ * @param identifier - Linear issue identifier
2458
+ * @param _repo - Repository (unused for Linear)
2459
+ * @returns Issue if valid, false if not found
2460
+ */
2461
+ async isValidIssue(identifier, _repo) {
2462
+ try {
2463
+ return await this.fetchIssue(identifier);
2464
+ } catch (error) {
2465
+ if (error instanceof LinearServiceError && error.code === "NOT_FOUND") {
2466
+ return false;
2467
+ }
2468
+ throw error;
2469
+ }
2470
+ }
2471
+ /**
2472
+ * Validate issue state and prompt user if closed
2473
+ * @param issue - Issue to validate
2474
+ * @throws LinearServiceError if user cancels due to closed issue
2475
+ */
2476
+ async validateIssueState(issue) {
2477
+ if (issue.state === "closed") {
2478
+ const shouldContinue = await this.prompter(
2479
+ `Issue ${issue.number} is closed. Continue anyway?`
2480
+ );
2481
+ if (!shouldContinue) {
2482
+ throw new LinearServiceError("INVALID_STATE", "User cancelled due to closed issue");
2483
+ }
2484
+ }
2485
+ }
2486
+ /**
2487
+ * Create a new Linear issue
2488
+ * @param title - Issue title
2489
+ * @param body - Issue description (markdown)
2490
+ * @param _repository - Repository (unused for Linear)
2491
+ * @param labels - Optional label names
2492
+ * @returns Created issue identifier and URL
2493
+ * @throws LinearServiceError if teamId not configured or creation fails
2494
+ */
2495
+ async createIssue(title, body, _repository, labels) {
2496
+ if (!this.config.teamId) {
2497
+ throw new LinearServiceError(
2498
+ "INVALID_STATE",
2499
+ "Linear teamId not configured. Run `il init` to configure Linear settings."
2500
+ );
2501
+ }
2502
+ logger.info(`Creating Linear issue in team ${this.config.teamId}: ${title}`);
2503
+ const result = await createLinearIssue(title, body, this.config.teamId, labels);
2504
+ return {
2505
+ number: result.identifier,
2506
+ url: result.url
2507
+ };
2508
+ }
2509
+ /**
2510
+ * Get the web URL for a Linear issue
2511
+ * @param identifier - Linear issue identifier
2512
+ * @param _repo - Repository (unused for Linear)
2513
+ * @returns Issue URL
2514
+ */
2515
+ async getIssueUrl(identifier, _repo) {
2516
+ const issue = await this.fetchIssue(identifier);
2517
+ return issue.url;
2518
+ }
2519
+ /**
2520
+ * Move a Linear issue to "In Progress" state
2521
+ * @param identifier - Linear issue identifier
2522
+ * @throws LinearServiceError if state update fails
2523
+ */
2524
+ async moveIssueToInProgress(identifier) {
2525
+ logger.info(`Moving Linear issue ${identifier} to In Progress`);
2526
+ await updateLinearIssueState(String(identifier), "In Progress");
2527
+ }
2528
+ /**
2529
+ * Extract issue context for AI prompts
2530
+ * @param entity - Issue (Linear doesn't have PRs)
2531
+ * @returns Formatted context string
2532
+ */
2533
+ extractContext(entity) {
2534
+ const issue = entity;
2535
+ return `Linear Issue ${issue.number}: ${issue.title}
2536
+ State: ${issue.state}
2537
+
2538
+ ${issue.body}`;
2539
+ }
2540
+ /**
2541
+ * Map Linear API issue to generic Issue type
2542
+ * @param linear - Linear issue from SDK
2543
+ * @returns Generic Issue type
2544
+ */
2545
+ mapLinearIssueToIssue(linear) {
2546
+ return {
2547
+ number: linear.identifier,
2548
+ // Keep as string (e.g., "ENG-123")
2549
+ title: linear.title,
2550
+ body: linear.description ?? "",
2551
+ state: linear.state ? linear.state.toLowerCase().includes("done") || linear.state.toLowerCase().includes("completed") || linear.state.toLowerCase().includes("canceled") ? "closed" : "open" : "open",
2552
+ labels: [],
2553
+ assignees: [],
2554
+ url: linear.url
2555
+ };
2556
+ }
2557
+ };
2558
+
2559
+ // src/lib/IssueTrackerFactory.ts
2560
+ init_logger();
2561
+ var IssueTrackerFactory = class {
2562
+ /**
2563
+ * Create an IssueTracker instance based on settings configuration
2564
+ * Defaults to GitHub if no provider specified
2565
+ *
2566
+ * @param settings - iloom settings containing issueManagement.provider
2567
+ * @returns IssueTracker instance configured for the specified provider
2568
+ * @throws Error if provider type is not supported
2569
+ */
2570
+ static create(settings) {
2571
+ var _a, _b;
2572
+ const provider = ((_a = settings.issueManagement) == null ? void 0 : _a.provider) ?? "github";
2573
+ logger.debug(`IssueTrackerFactory: Creating tracker for provider "${provider}"`);
2574
+ logger.debug(`IssueTrackerFactory: issueManagement settings:`, JSON.stringify(settings.issueManagement, null, 2));
2575
+ switch (provider) {
2576
+ case "github":
2577
+ logger.debug("IssueTrackerFactory: Creating GitHubService");
2578
+ return new GitHubService();
2579
+ case "linear": {
2580
+ const linearSettings = (_b = settings.issueManagement) == null ? void 0 : _b.linear;
2581
+ const linearConfig = {};
2582
+ if (linearSettings == null ? void 0 : linearSettings.teamId) {
2583
+ linearConfig.teamId = linearSettings.teamId;
2584
+ }
2585
+ if (linearSettings == null ? void 0 : linearSettings.branchFormat) {
2586
+ linearConfig.branchFormat = linearSettings.branchFormat;
2587
+ }
2588
+ logger.debug(`IssueTrackerFactory: Creating LinearService with config:`, JSON.stringify(linearConfig, null, 2));
2589
+ return new LinearService(linearConfig);
2590
+ }
2591
+ default:
2592
+ throw new Error(`Unsupported issue tracker provider: ${provider}`);
2593
+ }
2594
+ }
2595
+ /**
2596
+ * Get the configured provider name from settings
2597
+ * Defaults to 'github' if not configured
2598
+ *
2599
+ * @param settings - iloom settings
2600
+ * @returns Provider type string
2601
+ */
2602
+ static getProviderName(settings) {
2603
+ var _a;
2604
+ return ((_a = settings.issueManagement) == null ? void 0 : _a.provider) ?? "github";
2605
+ }
2606
+ };
2607
+
2608
+ // src/lib/EnvironmentManager.ts
2609
+ init_logger();
2610
+ import fs2 from "fs-extra";
2611
+
2612
+ // src/utils/env.ts
2613
+ init_logger();
2614
+ import dotenvFlow from "dotenv-flow";
2615
+ function parseEnvFile(content) {
2616
+ const envMap = /* @__PURE__ */ new Map();
2617
+ const lines = content.split("\n");
2618
+ for (const line of lines) {
2619
+ const trimmedLine = line.trim();
2620
+ if (!trimmedLine || trimmedLine.startsWith("#")) {
2621
+ continue;
2622
+ }
2623
+ const cleanLine = trimmedLine.startsWith("export ") ? trimmedLine.substring(7) : trimmedLine;
2624
+ const equalsIndex = cleanLine.indexOf("=");
2345
2625
  if (equalsIndex === -1) {
2346
2626
  continue;
2347
2627
  }
@@ -2386,12 +2666,12 @@ function isValidEnvKey(key) {
2386
2666
  }
2387
2667
 
2388
2668
  // src/utils/port.ts
2389
- import { createHash as createHash2 } from "crypto";
2669
+ import { createHash } from "crypto";
2390
2670
  function generatePortOffsetFromBranchName(branchName) {
2391
2671
  if (!branchName || branchName.trim().length === 0) {
2392
2672
  throw new Error("Branch name cannot be empty");
2393
2673
  }
2394
- const hash = createHash2("sha256").update(branchName).digest("hex");
2674
+ const hash = createHash("sha256").update(branchName).digest("hex");
2395
2675
  const hashPrefix = hash.slice(0, 8);
2396
2676
  const hashAsInt = parseInt(hashPrefix, 16);
2397
2677
  const portOffset = hashAsInt % 999 + 1;
@@ -2486,6 +2766,14 @@ var EnvironmentManager = class {
2486
2766
  return /* @__PURE__ */ new Map();
2487
2767
  }
2488
2768
  }
2769
+ /**
2770
+ * Get a specific environment variable from a .env file
2771
+ * Returns null if file doesn't exist or variable is not found
2772
+ */
2773
+ async getEnvVariable(filePath, variableName) {
2774
+ const envVars = await this.readEnvFile(filePath);
2775
+ return envVars.get(variableName) ?? null;
2776
+ }
2489
2777
  /**
2490
2778
  * Generic file copy helper that only copies if source exists
2491
2779
  * Does not throw if source file doesn't exist - just logs and returns
@@ -2509,13 +2797,17 @@ var EnvironmentManager = class {
2509
2797
  calculatePort(options) {
2510
2798
  const basePort = options.basePort ?? 3e3;
2511
2799
  if (options.issueNumber !== void 0) {
2512
- const port = basePort + options.issueNumber;
2513
- if (port > 65535) {
2514
- throw new Error(
2515
- `Calculated port ${port} exceeds maximum (65535). Use a lower base port or issue number.`
2516
- );
2800
+ const numericIssue = typeof options.issueNumber === "number" ? options.issueNumber : parseInt(String(options.issueNumber), 10);
2801
+ if (!isNaN(numericIssue) && String(numericIssue) === String(options.issueNumber)) {
2802
+ const port = basePort + numericIssue;
2803
+ if (port > 65535) {
2804
+ throw new Error(
2805
+ `Calculated port ${port} exceeds maximum (65535). Use a lower base port or issue number.`
2806
+ );
2807
+ }
2808
+ return port;
2517
2809
  }
2518
- return port;
2810
+ return calculatePortForBranch(String(options.issueNumber), basePort);
2519
2811
  }
2520
2812
  if (options.prNumber !== void 0) {
2521
2813
  const port = basePort + options.prNumber;
@@ -2635,8 +2927,9 @@ var DatabaseManager = class {
2635
2927
  * @param branchName - Name of the branch to create
2636
2928
  * @param envFilePath - Path to .env file for configuration checks
2637
2929
  * @param cwd - Optional working directory to run commands from
2930
+ * @param fromBranch - Optional parent branch to create from (for child looms)
2638
2931
  */
2639
- async createBranchIfConfigured(branchName, envFilePath, cwd) {
2932
+ async createBranchIfConfigured(branchName, envFilePath, cwd, fromBranch) {
2640
2933
  if (!await this.shouldUseDatabaseBranching(envFilePath)) {
2641
2934
  return null;
2642
2935
  }
@@ -2658,7 +2951,7 @@ var DatabaseManager = class {
2658
2951
  throw error;
2659
2952
  }
2660
2953
  try {
2661
- const connectionString = await this.provider.createBranch(branchName, void 0, cwd);
2954
+ const connectionString = await this.provider.createBranch(branchName, fromBranch, cwd);
2662
2955
  logger3.success(`Database branch ready: ${this.provider.sanitizeBranchName(branchName)}`);
2663
2956
  return connectionString;
2664
2957
  } catch (error) {
@@ -2745,6 +3038,24 @@ var DatabaseManager = class {
2745
3038
  };
2746
3039
  }
2747
3040
  }
3041
+ /**
3042
+ * Get database branch name from connection string (reverse lookup)
3043
+ * Returns branch name if provider supports reverse lookup, null otherwise
3044
+ *
3045
+ * @param connectionString - Database connection string
3046
+ * @param cwd - Optional working directory to run commands from
3047
+ */
3048
+ async getBranchNameFromConnectionString(connectionString, cwd) {
3049
+ if (!this.provider.isConfigured()) {
3050
+ logger3.debug("Provider not configured, skipping reverse lookup");
3051
+ return null;
3052
+ }
3053
+ if ("getBranchNameFromConnectionString" in this.provider && typeof this.provider.getBranchNameFromConnectionString === "function") {
3054
+ return this.provider.getBranchNameFromConnectionString(connectionString, cwd);
3055
+ }
3056
+ logger3.debug("Provider does not support reverse lookup");
3057
+ return null;
3058
+ }
2748
3059
  /**
2749
3060
  * Check if .env has the configured database URL variable
2750
3061
  * CRITICAL: If user explicitly configured a custom variable name (not default),
@@ -2788,8 +3099,187 @@ var DatabaseManager = class {
2788
3099
  }
2789
3100
  };
2790
3101
 
2791
- // src/lib/ClaudeService.ts
2792
- init_claude();
3102
+ // src/utils/claude.ts
3103
+ init_logger();
3104
+ import { execa as execa4 } from "execa";
3105
+ import { existsSync as existsSync2 } from "fs";
3106
+ import { join } from "path";
3107
+ async function detectClaudeCli() {
3108
+ try {
3109
+ await execa4("command", ["-v", "claude"], {
3110
+ shell: true,
3111
+ timeout: 5e3
3112
+ });
3113
+ return true;
3114
+ } catch (error) {
3115
+ logger.debug("Claude CLI not available", { error });
3116
+ return false;
3117
+ }
3118
+ }
3119
+ function parseJsonStreamOutput(output) {
3120
+ try {
3121
+ const lines = output.split("\n").filter((line) => line.trim());
3122
+ let lastResult = "";
3123
+ for (const line of lines) {
3124
+ try {
3125
+ const jsonObj = JSON.parse(line);
3126
+ if (jsonObj && typeof jsonObj === "object" && jsonObj.type === "result" && "result" in jsonObj) {
3127
+ lastResult = jsonObj.result;
3128
+ }
3129
+ } catch {
3130
+ continue;
3131
+ }
3132
+ }
3133
+ return lastResult || output;
3134
+ } catch {
3135
+ return output;
3136
+ }
3137
+ }
3138
+ async function launchClaude(prompt, options = {}) {
3139
+ const { model, permissionMode, addDir, headless = false, appendSystemPrompt, mcpConfig, allowedTools, disallowedTools, agents } = options;
3140
+ const args = [];
3141
+ if (headless) {
3142
+ args.push("-p");
3143
+ args.push("--output-format", "stream-json");
3144
+ args.push("--verbose");
3145
+ }
3146
+ if (model) {
3147
+ args.push("--model", model);
3148
+ }
3149
+ if (permissionMode && permissionMode !== "default") {
3150
+ args.push("--permission-mode", permissionMode);
3151
+ }
3152
+ if (addDir) {
3153
+ args.push("--add-dir", addDir);
3154
+ }
3155
+ args.push("--add-dir", "/tmp");
3156
+ if (appendSystemPrompt) {
3157
+ args.push("--append-system-prompt", appendSystemPrompt);
3158
+ }
3159
+ if (mcpConfig && mcpConfig.length > 0) {
3160
+ for (const config of mcpConfig) {
3161
+ args.push("--mcp-config", JSON.stringify(config));
3162
+ }
3163
+ }
3164
+ if (allowedTools && allowedTools.length > 0) {
3165
+ args.push("--allowed-tools", ...allowedTools);
3166
+ }
3167
+ if (disallowedTools && disallowedTools.length > 0) {
3168
+ args.push("--disallowed-tools", ...disallowedTools);
3169
+ }
3170
+ if (agents) {
3171
+ args.push("--agents", JSON.stringify(agents));
3172
+ }
3173
+ try {
3174
+ if (headless) {
3175
+ const isDebugMode = logger.isDebugEnabled();
3176
+ const execaOptions = {
3177
+ input: prompt,
3178
+ timeout: 0,
3179
+ // Disable timeout for long responses
3180
+ ...addDir && { cwd: addDir },
3181
+ // Run Claude in the worktree directory
3182
+ verbose: isDebugMode,
3183
+ ...isDebugMode && { stdio: ["pipe", "pipe", "pipe"] }
3184
+ // Enable streaming in debug mode
3185
+ };
3186
+ const subprocess = execa4("claude", args, execaOptions);
3187
+ const isJsonStreamFormat = args.includes("--output-format") && args.includes("stream-json");
3188
+ let outputBuffer = "";
3189
+ let isStreaming = false;
3190
+ let isFirstProgress = true;
3191
+ if (subprocess.stdout && typeof subprocess.stdout.on === "function") {
3192
+ isStreaming = true;
3193
+ subprocess.stdout.on("data", (chunk) => {
3194
+ const text = chunk.toString();
3195
+ outputBuffer += text;
3196
+ if (isDebugMode) {
3197
+ process.stdout.write(text);
3198
+ } else {
3199
+ if (isFirstProgress) {
3200
+ process.stdout.write("\u{1F916} .");
3201
+ isFirstProgress = false;
3202
+ } else {
3203
+ process.stdout.write(".");
3204
+ }
3205
+ }
3206
+ });
3207
+ }
3208
+ const result = await subprocess;
3209
+ if (isStreaming) {
3210
+ const rawOutput = outputBuffer.trim();
3211
+ if (!isDebugMode) {
3212
+ process.stdout.write("\n");
3213
+ }
3214
+ return isJsonStreamFormat ? parseJsonStreamOutput(rawOutput) : rawOutput;
3215
+ } else {
3216
+ if (isDebugMode) {
3217
+ process.stdout.write(result.stdout);
3218
+ if (result.stdout && !result.stdout.endsWith("\n")) {
3219
+ process.stdout.write("\n");
3220
+ }
3221
+ } else {
3222
+ process.stdout.write("\u{1F916} .");
3223
+ process.stdout.write("\n");
3224
+ }
3225
+ const rawOutput = result.stdout.trim();
3226
+ return isJsonStreamFormat ? parseJsonStreamOutput(rawOutput) : rawOutput;
3227
+ }
3228
+ } else {
3229
+ await execa4("claude", [...args, "--", prompt], {
3230
+ ...addDir && { cwd: addDir },
3231
+ stdio: "inherit",
3232
+ // Let user interact directly in current terminal
3233
+ timeout: 0,
3234
+ // Disable timeout
3235
+ verbose: logger.isDebugEnabled()
3236
+ });
3237
+ return;
3238
+ }
3239
+ } catch (error) {
3240
+ const execaError = error;
3241
+ const errorMessage = execaError.stderr ?? execaError.message ?? "Unknown Claude CLI error";
3242
+ throw new Error(`Claude CLI error: ${errorMessage}`);
3243
+ }
3244
+ }
3245
+ async function launchClaudeInNewTerminalWindow(_prompt, options) {
3246
+ const { workspacePath, branchName, oneShot = "default", port, setArguments, executablePath } = options;
3247
+ if (!workspacePath) {
3248
+ throw new Error("workspacePath is required for terminal window launch");
3249
+ }
3250
+ const { openTerminalWindow: openTerminalWindow2 } = await Promise.resolve().then(() => (init_terminal(), terminal_exports));
3251
+ const executable = executablePath ?? "iloom";
3252
+ let launchCommand = `${executable} spin`;
3253
+ if (oneShot !== "default") {
3254
+ launchCommand += ` --one-shot=${oneShot}`;
3255
+ }
3256
+ if (setArguments && setArguments.length > 0) {
3257
+ for (const setArg of setArguments) {
3258
+ launchCommand += ` --set ${setArg}`;
3259
+ }
3260
+ }
3261
+ let backgroundColor;
3262
+ if (branchName) {
3263
+ try {
3264
+ const { generateColorFromBranchName: generateColorFromBranchName2 } = await Promise.resolve().then(() => (init_color(), color_exports));
3265
+ const colorData = generateColorFromBranchName2(branchName);
3266
+ backgroundColor = colorData.rgb;
3267
+ } catch (error) {
3268
+ logger.warn(
3269
+ `Failed to generate terminal color: ${error instanceof Error ? error.message : "Unknown error"}`
3270
+ );
3271
+ }
3272
+ }
3273
+ const hasEnvFile = existsSync2(join(workspacePath, ".env"));
3274
+ await openTerminalWindow2({
3275
+ workspacePath,
3276
+ command: launchCommand,
3277
+ ...backgroundColor && { backgroundColor },
3278
+ includeEnvSetup: hasEnvFile,
3279
+ // source .env only if it exists
3280
+ ...port !== void 0 && { port, includePortExport: true }
3281
+ });
3282
+ }
2793
3283
 
2794
3284
  // src/lib/PromptTemplateManager.ts
2795
3285
  init_logger();
@@ -2869,6 +3359,9 @@ var PromptTemplateManager = class {
2869
3359
  if (variables.SETTINGS_SCHEMA !== void 0) {
2870
3360
  result = result.replace(/SETTINGS_SCHEMA/g, variables.SETTINGS_SCHEMA);
2871
3361
  }
3362
+ if (variables.SETTINGS_GLOBAL_JSON !== void 0) {
3363
+ result = result.replace(/SETTINGS_GLOBAL_JSON/g, variables.SETTINGS_GLOBAL_JSON);
3364
+ }
2872
3365
  if (variables.SETTINGS_JSON !== void 0) {
2873
3366
  result = result.replace(/SETTINGS_JSON/g, variables.SETTINGS_JSON);
2874
3367
  }
@@ -2930,6 +3423,12 @@ var PromptTemplateManager = class {
2930
3423
  } else {
2931
3424
  result = result.replace(settingsJsonRegex, "");
2932
3425
  }
3426
+ const settingsGlobalJsonRegex = /\{\{#IF SETTINGS_GLOBAL_JSON\}\}(.*?)\{\{\/IF SETTINGS_GLOBAL_JSON\}\}/gs;
3427
+ if (variables.SETTINGS_GLOBAL_JSON !== void 0 && variables.SETTINGS_GLOBAL_JSON !== "") {
3428
+ result = result.replace(settingsGlobalJsonRegex, "$1");
3429
+ } else {
3430
+ result = result.replace(settingsGlobalJsonRegex, "");
3431
+ }
2933
3432
  const settingsLocalJsonRegex = /\{\{#IF SETTINGS_LOCAL_JSON\}\}(.*?)\{\{\/IF SETTINGS_LOCAL_JSON\}\}/gs;
2934
3433
  if (variables.SETTINGS_LOCAL_JSON !== void 0 && variables.SETTINGS_LOCAL_JSON !== "") {
2935
3434
  result = result.replace(settingsLocalJsonRegex, "$1");
@@ -3090,17 +3589,6 @@ var ClaudeService = class {
3090
3589
  throw error;
3091
3590
  }
3092
3591
  }
3093
- /**
3094
- * Generate branch name with Claude, with fallback on failure
3095
- */
3096
- async generateBranchNameWithFallback(issueTitle, issueNumber) {
3097
- try {
3098
- return await generateBranchName(issueTitle, issueNumber);
3099
- } catch (error) {
3100
- logger.warn("Claude branch name generation failed, using fallback", { error });
3101
- return `feat/issue-${issueNumber}`;
3102
- }
3103
- }
3104
3592
  };
3105
3593
 
3106
3594
  // src/lib/ClaudeContextManager.ts
@@ -3160,17 +3648,322 @@ var ClaudeContextManager = class {
3160
3648
 
3161
3649
  // src/utils/index.ts
3162
3650
  init_logger();
3651
+
3652
+ // src/utils/linear-markup-converter.ts
3653
+ import { appendFileSync } from "fs";
3654
+ import { join as join2, dirname, basename, extname } from "path";
3655
+ var LinearMarkupConverter = class {
3656
+ /**
3657
+ * Convert HTML details/summary blocks to Linear's collapsible format
3658
+ * Handles nested details blocks recursively
3659
+ *
3660
+ * @param text - Text containing HTML details/summary blocks
3661
+ * @returns Text with details/summary converted to Linear format
3662
+ */
3663
+ static convertDetailsToLinear(text) {
3664
+ if (!text) {
3665
+ return text;
3666
+ }
3667
+ let previousText = "";
3668
+ let currentText = text;
3669
+ while (previousText !== currentText) {
3670
+ previousText = currentText;
3671
+ currentText = this.convertSinglePass(currentText);
3672
+ }
3673
+ return currentText;
3674
+ }
3675
+ /**
3676
+ * Perform a single pass of details block conversion
3677
+ * Converts the innermost details blocks first
3678
+ */
3679
+ static convertSinglePass(text) {
3680
+ const detailsRegex = /<details[^>]*>\s*<summary[^>]*>(.*?)<\/summary>\s*(.*?)\s*<\/details>/gis;
3681
+ return text.replace(detailsRegex, (_match, summary, content) => {
3682
+ const cleanSummary = this.cleanText(summary);
3683
+ const cleanContent = this.cleanContent(content);
3684
+ if (cleanContent) {
3685
+ return `+++ ${cleanSummary}
3686
+
3687
+ ${cleanContent}
3688
+
3689
+ +++`;
3690
+ } else {
3691
+ return `+++ ${cleanSummary}
3692
+
3693
+ +++`;
3694
+ }
3695
+ });
3696
+ }
3697
+ /**
3698
+ * Clean text by trimming whitespace and decoding common HTML entities
3699
+ */
3700
+ static cleanText(text) {
3701
+ return text.trim().replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
3702
+ }
3703
+ /**
3704
+ * Clean content while preserving internal structure
3705
+ * - Removes leading/trailing whitespace
3706
+ * - Normalizes internal blank lines (max 2 consecutive newlines)
3707
+ * - Preserves code blocks and other formatting
3708
+ */
3709
+ static cleanContent(content) {
3710
+ if (!content) {
3711
+ return "";
3712
+ }
3713
+ let cleaned = content.trim();
3714
+ cleaned = cleaned.replace(/\n{3,}/g, "\n\n");
3715
+ return cleaned;
3716
+ }
3717
+ /**
3718
+ * Check if text contains HTML details/summary blocks
3719
+ * Useful for conditional conversion
3720
+ */
3721
+ static hasDetailsBlocks(text) {
3722
+ if (!text) {
3723
+ return false;
3724
+ }
3725
+ const detailsRegex = /<details[^>]*>.*?<summary[^>]*>.*?<\/summary>.*?<\/details>/is;
3726
+ return detailsRegex.test(text);
3727
+ }
3728
+ /**
3729
+ * Remove wrapper tags from code sample details blocks
3730
+ * Identifies details blocks where summary contains "X lines" pattern
3731
+ * and removes the details/summary tags while preserving the content
3732
+ *
3733
+ * @param text - Text containing potential code sample details blocks
3734
+ * @returns Text with code sample wrappers removed
3735
+ */
3736
+ static removeCodeSampleWrappers(text) {
3737
+ if (!text) {
3738
+ return text;
3739
+ }
3740
+ const codeSampleRegex = /<details[^>]*>\s*<summary[^>]*>([^<]*\d+\s+lines[^<]*)<\/summary>\s*([\s\S]*?)<\/details>/gi;
3741
+ return text.replace(codeSampleRegex, (_match, _summary, content) => {
3742
+ return content.trim();
3743
+ });
3744
+ }
3745
+ /**
3746
+ * Convert text for Linear - applies all necessary conversions
3747
+ * Currently only converts details/summary blocks, but can be extended
3748
+ * for other HTML to Linear markdown conversions
3749
+ */
3750
+ static convertToLinear(text) {
3751
+ if (!text) {
3752
+ return text;
3753
+ }
3754
+ this.logConversion("INPUT", text);
3755
+ let converted = text;
3756
+ converted = this.removeCodeSampleWrappers(converted);
3757
+ converted = this.convertDetailsToLinear(converted);
3758
+ this.logConversion("OUTPUT", converted);
3759
+ return converted;
3760
+ }
3761
+ /**
3762
+ * Log conversion input/output if LINEAR_MARKDOWN_LOG_FILE is set
3763
+ */
3764
+ static logConversion(label, content) {
3765
+ const logFilePath = process.env.LINEAR_MARKDOWN_LOG_FILE;
3766
+ if (!logFilePath) {
3767
+ return;
3768
+ }
3769
+ try {
3770
+ const timestampedPath = this.getTimestampedLogPath(logFilePath);
3771
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
3772
+ const separator = "================================";
3773
+ const logEntry = `${separator}
3774
+ [${timestamp}] CONVERSION ${label}
3775
+ ${separator}
3776
+ ${label}:
3777
+ ${content}
3778
+
3779
+ `;
3780
+ appendFileSync(timestampedPath, logEntry, "utf-8");
3781
+ } catch {
3782
+ }
3783
+ }
3784
+ /**
3785
+ * Generate timestamped log file path
3786
+ * Example: debug.log -> debug-20231202-161234.log
3787
+ */
3788
+ static getTimestampedLogPath(logFilePath) {
3789
+ const dir = dirname(logFilePath);
3790
+ const ext = extname(logFilePath);
3791
+ const base = basename(logFilePath, ext);
3792
+ const now = /* @__PURE__ */ new Date();
3793
+ const timestamp = [
3794
+ now.getFullYear(),
3795
+ String(now.getMonth() + 1).padStart(2, "0"),
3796
+ String(now.getDate()).padStart(2, "0")
3797
+ ].join("") + "-" + [
3798
+ String(now.getHours()).padStart(2, "0"),
3799
+ String(now.getMinutes()).padStart(2, "0"),
3800
+ String(now.getSeconds()).padStart(2, "0")
3801
+ ].join("");
3802
+ return join2(dir, `${base}-${timestamp}${ext}`);
3803
+ }
3804
+ };
3805
+
3806
+ // src/utils/table-formatter.ts
3807
+ var TableFormatter = class {
3808
+ /**
3809
+ * Pad table headers to achieve equal column widths
3810
+ * @param headers Array of header text
3811
+ * @param options Formatting options
3812
+ * @returns Array of padded headers
3813
+ */
3814
+ static padHeaders(headers, options = {}) {
3815
+ if (!headers || headers.length === 0) {
3816
+ throw new Error("Headers array cannot be empty");
3817
+ }
3818
+ const {
3819
+ targetTotalWidth = this.DEFAULT_TARGET_WIDTH,
3820
+ paddingChar = this.DEFAULT_PADDING_CHAR,
3821
+ maxPadding = this.DEFAULT_MAX_PADDING
3822
+ } = options;
3823
+ const columnCount = headers.length;
3824
+ const targetWidthPerColumn = Math.floor(targetTotalWidth / columnCount);
3825
+ return headers.map((header) => {
3826
+ const currentLength = header.length;
3827
+ const paddingNeeded = Math.max(0, targetWidthPerColumn - currentLength);
3828
+ const cappedPadding = Math.min(paddingNeeded, maxPadding);
3829
+ const padding = paddingChar.repeat(cappedPadding);
3830
+ return header + padding;
3831
+ });
3832
+ }
3833
+ /**
3834
+ * Generate a complete markdown table with padded headers
3835
+ * @param options Table generation options
3836
+ * @returns Complete markdown table string
3837
+ */
3838
+ static generateTable(options) {
3839
+ var _a;
3840
+ const { headers, rows, ...paddingOptions } = options;
3841
+ if (!rows || rows.length === 0) {
3842
+ throw new Error("Table must have at least one row");
3843
+ }
3844
+ const invalidRows = rows.filter((row) => row.length !== headers.length);
3845
+ if (invalidRows.length > 0) {
3846
+ throw new Error(
3847
+ `All rows must have ${headers.length} columns. Found rows with ${(_a = invalidRows[0]) == null ? void 0 : _a.length} columns.`
3848
+ );
3849
+ }
3850
+ const paddedHeaders = this.padHeaders(headers, paddingOptions);
3851
+ const headerRow = `| ${paddedHeaders.join(" | ")} |`;
3852
+ const separators = headers.map(() => "---");
3853
+ const separatorRow = `| ${separators.join(" | ")} |`;
3854
+ const dataRows = rows.map((row) => `| ${row.join(" | ")} |`).join("\n");
3855
+ return [headerRow, separatorRow, dataRows].join("\n");
3856
+ }
3857
+ /**
3858
+ * Calculate the optimal width distribution for given headers
3859
+ * @param headers Array of header text
3860
+ * @param targetTotalWidth Target total width
3861
+ * @param maxPadding Maximum padding entities per column
3862
+ * @returns Width distribution information
3863
+ */
3864
+ static calculateWidthDistribution(headers, targetTotalWidth = this.DEFAULT_TARGET_WIDTH, maxPadding = this.DEFAULT_MAX_PADDING) {
3865
+ if (!headers || headers.length === 0) {
3866
+ throw new Error("Headers array cannot be empty");
3867
+ }
3868
+ const columnCount = headers.length;
3869
+ const widthPerColumn = Math.floor(targetTotalWidth / columnCount);
3870
+ const headerInfo = headers.map((header) => {
3871
+ const paddingNeeded = Math.max(0, widthPerColumn - header.length);
3872
+ const paddingUsed = Math.min(paddingNeeded, maxPadding);
3873
+ return {
3874
+ text: header,
3875
+ currentLength: header.length,
3876
+ targetLength: widthPerColumn,
3877
+ paddingNeeded,
3878
+ paddingUsed
3879
+ };
3880
+ });
3881
+ return {
3882
+ totalWidth: targetTotalWidth,
3883
+ widthPerColumn,
3884
+ headers: headerInfo
3885
+ };
3886
+ }
3887
+ /**
3888
+ * Create a simple two-column assessment table (common pattern)
3889
+ * @param assessmentData Array of [question, answer] pairs
3890
+ * @param options Formatting options
3891
+ * @returns Formatted markdown table
3892
+ */
3893
+ static createAssessmentTable(assessmentData, options = {}) {
3894
+ const headers = ["Question", "Answer"];
3895
+ const rows = assessmentData;
3896
+ return this.generateTable({
3897
+ headers,
3898
+ rows,
3899
+ ...options
3900
+ });
3901
+ }
3902
+ /**
3903
+ * Create a three-column status table (common pattern)
3904
+ * @param statusData Array of [task, status, assignee] tuples
3905
+ * @param options Formatting options
3906
+ * @returns Formatted markdown table
3907
+ */
3908
+ static createStatusTable(statusData, options = {}) {
3909
+ const headers = ["Task", "Status", "Assignee"];
3910
+ const rows = statusData;
3911
+ return this.generateTable({
3912
+ headers,
3913
+ rows,
3914
+ ...options
3915
+ });
3916
+ }
3917
+ /**
3918
+ * Preview table formatting without generating full markdown
3919
+ * Useful for debugging and development
3920
+ * @param headers Array of header text
3921
+ * @param options Formatting options
3922
+ * @returns Human-readable formatting preview
3923
+ */
3924
+ static previewFormatting(headers, options = {}) {
3925
+ const { maxPadding = this.DEFAULT_MAX_PADDING } = options;
3926
+ const distribution = this.calculateWidthDistribution(
3927
+ headers,
3928
+ options.targetTotalWidth,
3929
+ maxPadding
3930
+ );
3931
+ const lines = [
3932
+ `Table Formatting Preview`,
3933
+ `========================`,
3934
+ `Target total width: ${distribution.totalWidth} characters`,
3935
+ `Columns: ${headers.length}`,
3936
+ `Width per column: ${distribution.widthPerColumn} characters`,
3937
+ `Max padding per column: ${maxPadding} entities`,
3938
+ ``
3939
+ ];
3940
+ distribution.headers.forEach((header, index) => {
3941
+ const cappedIndicator = header.paddingUsed < header.paddingNeeded ? " (capped)" : "";
3942
+ lines.push(
3943
+ `Column ${index + 1}: "${header.text}" (${header.currentLength} chars) + ${header.paddingUsed} padding${cappedIndicator} (target: ${header.paddingNeeded}) = ${header.currentLength + header.paddingUsed * 6} total string length`
3944
+ );
3945
+ });
3946
+ return lines.join("\n");
3947
+ }
3948
+ };
3949
+ TableFormatter.DEFAULT_TARGET_WIDTH = 140;
3950
+ TableFormatter.DEFAULT_PADDING_CHAR = "&nbsp;";
3951
+ TableFormatter.DEFAULT_MAX_PADDING = 16;
3163
3952
  export {
3164
3953
  ClaudeContextManager,
3165
3954
  DatabaseManager,
3166
3955
  EnvironmentManager,
3167
3956
  GitHubService,
3168
3957
  GitWorktreeManager,
3958
+ IssueTrackerFactory,
3959
+ LinearMarkupConverter,
3960
+ TableFormatter,
3169
3961
  WorkspaceManager,
3170
3962
  branchExists,
3171
3963
  createLogger,
3172
3964
  ensureRepositoryHasCommits,
3173
3965
  executeGitCommand,
3966
+ extractIssueNumber,
3174
3967
  extractPRNumber,
3175
3968
  findAllBranchesForIssue,
3176
3969
  findMainWorktreePath,