@iloom/cli 0.2.0 → 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 (169) hide show
  1. package/README.md +274 -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-CTSWJL35.js → LoomLauncher-FLEMBCSQ.js} +63 -32
  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 +274 -30
  11. package/dist/{SettingsManager-XOYCLH3D.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-SWCRXDZC.js → chunk-3RUPPQRG.js} +1 -18
  23. package/dist/chunk-3RUPPQRG.js.map +1 -0
  24. package/dist/{chunk-RF2YI2XJ.js → chunk-47KSHUCR.js} +3 -3
  25. package/dist/chunk-47KSHUCR.js.map +1 -0
  26. package/dist/{chunk-VETG35MF.js → chunk-4HHRTA7Q.js} +3 -3
  27. package/dist/{chunk-VETG35MF.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-4IV6W4U5.js → chunk-AWOFAD5O.js} +12 -12
  31. package/dist/chunk-AWOFAD5O.js.map +1 -0
  32. package/dist/{chunk-2PLUQT6J.js → chunk-C5QCTEQK.js} +2 -2
  33. package/dist/{chunk-CWR2SANQ.js → chunk-EBISESAP.js} +1 -1
  34. package/dist/{chunk-LHP6ROUM.js → chunk-FIAT22G7.js} +4 -16
  35. package/dist/chunk-FIAT22G7.js.map +1 -0
  36. package/dist/{chunk-TS6DL67T.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-HBVFXN7R.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-DJUGYNQE.js → chunk-PA6Q6AWM.js} +16 -3
  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-6LEQW46Y.js → chunk-VAYCCUXW.js} +72 -2
  55. package/dist/{chunk-6LEQW46Y.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-MFU53H6J.js → chunk-XXV3UFZL.js} +3 -3
  61. package/dist/{chunk-MFU53H6J.js.map → chunk-XXV3UFZL.js.map} +1 -1
  62. package/dist/{chunk-GZP4UGGM.js → chunk-ZM3CFL5L.js} +2 -2
  63. package/dist/{chunk-BLCTGFZN.js → chunk-ZT3YZB4K.js} +3 -4
  64. package/dist/chunk-ZT3YZB4K.js.map +1 -0
  65. package/dist/{claude-ZIWDG4XG.js → claude-GOP6PFC7.js} +2 -2
  66. package/dist/{cleanup-FEIVZSIV.js → cleanup-7RWLBSLE.js} +86 -25
  67. package/dist/cleanup-7RWLBSLE.js.map +1 -0
  68. package/dist/cli.js +2511 -62
  69. package/dist/cli.js.map +1 -1
  70. package/dist/{contribute-EMZKCAC6.js → contribute-BS2L4FZR.js} +6 -6
  71. package/dist/{feedback-LFNMQBAZ.js → feedback-N4ECWIPF.js} +15 -14
  72. package/dist/{feedback-LFNMQBAZ.js.map → feedback-N4ECWIPF.js.map} +1 -1
  73. package/dist/{git-WC6HZLOT.js → git-TDXKRTXM.js} +4 -2
  74. package/dist/{ignite-MQWVJEAB.js → ignite-VM64QO3J.js} +32 -27
  75. package/dist/ignite-VM64QO3J.js.map +1 -0
  76. package/dist/index.d.ts +359 -45
  77. package/dist/index.js +1266 -502
  78. package/dist/index.js.map +1 -1
  79. package/dist/{init-GJDYN2IK.js → init-G3T64SC4.js} +104 -40
  80. package/dist/init-G3T64SC4.js.map +1 -0
  81. package/dist/mcp/issue-management-server.js +934 -0
  82. package/dist/mcp/issue-management-server.js.map +1 -0
  83. package/dist/{neon-helpers-ZVIRPKCI.js → neon-helpers-WPUACUVC.js} +3 -3
  84. package/dist/neon-helpers-WPUACUVC.js.map +1 -0
  85. package/dist/{open-NXSN7XOC.js → open-KXDXEKRZ.js} +39 -36
  86. package/dist/open-KXDXEKRZ.js.map +1 -0
  87. package/dist/{prompt-ANTQWHUF.js → prompt-7INJ7YRU.js} +4 -2
  88. package/dist/prompt-7INJ7YRU.js.map +1 -0
  89. package/dist/prompts/init-prompt.txt +538 -95
  90. package/dist/prompts/issue-prompt.txt +27 -27
  91. package/dist/{rebase-DUNFOJVS.js → rebase-Q7GMM7EI.js} +6 -6
  92. package/dist/{remote-ZCXJVVNW.js → remote-VUNCQZ6J.js} +3 -2
  93. package/dist/remote-VUNCQZ6J.js.map +1 -0
  94. package/dist/{run-O7ZK7CKA.js → run-PAWJJCSX.js} +39 -36
  95. package/dist/run-PAWJJCSX.js.map +1 -0
  96. package/dist/schema/settings.schema.json +56 -0
  97. package/dist/{test-git-T76HOTIA.js → test-git-3WDLNQCA.js} +3 -3
  98. package/dist/{test-prefix-6HJUVQMH.js → test-prefix-EVGAWAJW.js} +3 -3
  99. package/dist/{test-webserver-M2I3EV4J.js → test-webserver-DAHONWCS.js} +4 -4
  100. package/dist/test-webserver-DAHONWCS.js.map +1 -0
  101. package/package.json +2 -1
  102. package/dist/ClaudeContextManager-LVCYRM6Q.js +0 -13
  103. package/dist/ClaudeService-WVTWB3DK.js +0 -12
  104. package/dist/GitHubService-7E2S5NNZ.js +0 -11
  105. package/dist/LoomLauncher-CTSWJL35.js.map +0 -1
  106. package/dist/add-issue-OBI325W7.js +0 -69
  107. package/dist/add-issue-OBI325W7.js.map +0 -1
  108. package/dist/chunk-4IV6W4U5.js.map +0 -1
  109. package/dist/chunk-BLCTGFZN.js.map +0 -1
  110. package/dist/chunk-CVLAZRNB.js +0 -54
  111. package/dist/chunk-CVLAZRNB.js.map +0 -1
  112. package/dist/chunk-DJUGYNQE.js.map +0 -1
  113. package/dist/chunk-H4E4THUZ.js +0 -55
  114. package/dist/chunk-H4E4THUZ.js.map +0 -1
  115. package/dist/chunk-H5LDRGVK.js +0 -642
  116. package/dist/chunk-H5LDRGVK.js.map +0 -1
  117. package/dist/chunk-HBVFXN7R.js.map +0 -1
  118. package/dist/chunk-LHP6ROUM.js.map +0 -1
  119. package/dist/chunk-PVAVNJKS.js.map +0 -1
  120. package/dist/chunk-RF2YI2XJ.js.map +0 -1
  121. package/dist/chunk-SPYPLHMK.js.map +0 -1
  122. package/dist/chunk-SWCRXDZC.js.map +0 -1
  123. package/dist/chunk-SYOSCMIT.js +0 -545
  124. package/dist/chunk-SYOSCMIT.js.map +0 -1
  125. package/dist/chunk-T3KEIB4D.js +0 -243
  126. package/dist/chunk-T3KEIB4D.js.map +0 -1
  127. package/dist/chunk-TS6DL67T.js.map +0 -1
  128. package/dist/chunk-ZMNQBJUI.js.map +0 -1
  129. package/dist/cleanup-FEIVZSIV.js.map +0 -1
  130. package/dist/enhance-MNA4ZGXW.js +0 -176
  131. package/dist/enhance-MNA4ZGXW.js.map +0 -1
  132. package/dist/finish-TX5CJICB.js +0 -1749
  133. package/dist/finish-TX5CJICB.js.map +0 -1
  134. package/dist/ignite-MQWVJEAB.js.map +0 -1
  135. package/dist/init-GJDYN2IK.js.map +0 -1
  136. package/dist/mcp/chunk-6SDFJ42P.js +0 -62
  137. package/dist/mcp/chunk-6SDFJ42P.js.map +0 -1
  138. package/dist/mcp/claude-NDFOCQQQ.js +0 -249
  139. package/dist/mcp/claude-NDFOCQQQ.js.map +0 -1
  140. package/dist/mcp/color-QS5BFCNN.js +0 -168
  141. package/dist/mcp/color-QS5BFCNN.js.map +0 -1
  142. package/dist/mcp/github-comment-server.js +0 -168
  143. package/dist/mcp/github-comment-server.js.map +0 -1
  144. package/dist/mcp/terminal-OMNRFWB3.js +0 -227
  145. package/dist/mcp/terminal-OMNRFWB3.js.map +0 -1
  146. package/dist/open-NXSN7XOC.js.map +0 -1
  147. package/dist/run-O7ZK7CKA.js.map +0 -1
  148. package/dist/start-73I5W7WW.js +0 -983
  149. package/dist/start-73I5W7WW.js.map +0 -1
  150. package/dist/test-webserver-M2I3EV4J.js.map +0 -1
  151. /package/dist/{ClaudeContextManager-LVCYRM6Q.js.map → BranchNamingService-3OQPRSWT.js.map} +0 -0
  152. /package/dist/{ClaudeService-WVTWB3DK.js.map → ClaudeContextManager-MUQSDY2E.js.map} +0 -0
  153. /package/dist/{GitHubService-7E2S5NNZ.js.map → ClaudeService-HG4VQ7AW.js.map} +0 -0
  154. /package/dist/{PromptTemplateManager-WII75TKH.js.map → GitHubService-EBOETDIW.js.map} +0 -0
  155. /package/dist/{SettingsManager-XOYCLH3D.js.map → ProjectCapabilityDetector-34LU7JJ4.js.map} +0 -0
  156. /package/dist/{claude-ZIWDG4XG.js.map → PromptTemplateManager-A52RUAMS.js.map} +0 -0
  157. /package/dist/{git-WC6HZLOT.js.map → SettingsManager-WHHFGSL7.js.map} +0 -0
  158. /package/dist/{neon-helpers-ZVIRPKCI.js.map → SettingsMigrationManager-AGIIIPDQ.js.map} +0 -0
  159. /package/dist/{chunk-2PLUQT6J.js.map → chunk-C5QCTEQK.js.map} +0 -0
  160. /package/dist/{chunk-CWR2SANQ.js.map → chunk-EBISESAP.js.map} +0 -0
  161. /package/dist/{SettingsMigrationManager-MTQIMI54.js.map → chunk-KLBYVHPK.js.map} +0 -0
  162. /package/dist/{chunk-USVVV3FP.js.map → chunk-MKWYLDFK.js.map} +0 -0
  163. /package/dist/{chunk-GZP4UGGM.js.map → chunk-ZM3CFL5L.js.map} +0 -0
  164. /package/dist/{prompt-ANTQWHUF.js.map → claude-GOP6PFC7.js.map} +0 -0
  165. /package/dist/{contribute-EMZKCAC6.js.map → contribute-BS2L4FZR.js.map} +0 -0
  166. /package/dist/{remote-ZCXJVVNW.js.map → git-TDXKRTXM.js.map} +0 -0
  167. /package/dist/{rebase-DUNFOJVS.js.map → rebase-Q7GMM7EI.js.map} +0 -0
  168. /package/dist/{test-git-T76HOTIA.js.map → test-git-3WDLNQCA.js.map} +0 -0
  169. /package/dist/{test-prefix-6HJUVQMH.js.map → test-prefix-EVGAWAJW.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,32 +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)"),
231
261
  github: z.object({
232
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.")
233
268
  }).optional()
234
269
  }).optional().describe("Issue management configuration"),
235
270
  mergeBehavior: z.object({
236
271
  mode: z.enum(["local", "github-pr"]).default("local"),
237
272
  remote: z.string().optional()
238
- }).optional().describe("Merge behavior configuration: local (merge locally) or github-pr (create PR)")
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)"),
317
+ github: z.object({
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.")
324
+ }).optional()
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
+ )
239
337
  });
240
338
  SettingsManager = class {
241
339
  /**
242
- * Load settings from <PROJECT_ROOT>/.iloom/settings.json and settings.local.json
243
- * Merges settings.local.json over settings.json with priority
244
- * CLI overrides have highest priority if provided
245
- * 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)
246
347
  */
247
348
  async loadSettings(projectRoot, cliOverrides) {
248
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));
249
353
  const baseSettings = await this.loadSettingsFile(root, "settings.json");
250
354
  const baseSettingsPath = path.join(root, ".iloom", "settings.json");
251
355
  logger.debug(`\u{1F4C4} Base settings from ${baseSettingsPath}:`, JSON.stringify(baseSettings, null, 2));
252
356
  const localSettings = await this.loadSettingsFile(root, "settings.local.json");
253
357
  const localSettingsPath = path.join(root, ".iloom", "settings.local.json");
254
358
  logger.debug(`\u{1F4C4} Local settings from ${localSettingsPath}:`, JSON.stringify(localSettings, null, 2));
255
- let merged = this.mergeSettings(baseSettings, localSettings);
256
- 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));
257
361
  if (cliOverrides && Object.keys(cliOverrides).length > 0) {
258
362
  logger.debug("\u2699\uFE0F CLI overrides to apply:", JSON.stringify(cliOverrides, null, 2));
259
363
  merged = this.mergeSettings(merged, cliOverrides);
@@ -285,6 +389,7 @@ Note: CLI overrides were applied. Check your --set arguments.`);
285
389
  /**
286
390
  * Load and parse a single settings file
287
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
288
393
  */
289
394
  async loadSettingsFile(projectRoot, filename) {
290
395
  const settingsPath = path.join(projectRoot, ".iloom", filename);
@@ -298,16 +403,13 @@ Note: CLI overrides were applied. Check your --set arguments.`);
298
403
  `Failed to parse settings file at ${settingsPath}: ${error instanceof Error ? error.message : "Invalid JSON"}`
299
404
  );
300
405
  }
301
- try {
302
- const validated = IloomSettingsSchema.strict().parse(parsed);
303
- return validated;
304
- } catch (error) {
305
- if (error instanceof z.ZodError) {
306
- const errorMsg = this.formatAllZodErrors(error, filename);
307
- throw errorMsg;
308
- }
309
- 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
+ );
310
411
  }
412
+ return parsed;
311
413
  } catch (error) {
312
414
  if (error.code === "ENOENT") {
313
415
  logger.debug(`No settings file found at ${settingsPath}, using defaults`);
@@ -361,6 +463,57 @@ ${errorMessages.join("\n")}`
361
463
  getProjectRoot(projectRoot) {
362
464
  return projectRoot ?? process.cwd();
363
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
+ }
364
517
  /**
365
518
  * Get effective protected branches list with mainBranch always included
366
519
  *
@@ -396,7 +549,7 @@ __export(terminal_exports, {
396
549
  openMultipleTerminalWindows: () => openMultipleTerminalWindows,
397
550
  openTerminalWindow: () => openTerminalWindow
398
551
  });
399
- import { execa as execa2 } from "execa";
552
+ import { execa as execa3 } from "execa";
400
553
  import { existsSync } from "fs";
401
554
  function detectPlatform() {
402
555
  const platform = process.platform;
@@ -420,9 +573,9 @@ async function openTerminalWindow(options) {
420
573
  const hasITerm2 = await detectITerm2();
421
574
  const applescript = hasITerm2 ? buildITerm2SingleTabScript(options) : buildAppleScript(options);
422
575
  try {
423
- await execa2("osascript", ["-e", applescript]);
576
+ await execa3("osascript", ["-e", applescript]);
424
577
  if (!hasITerm2) {
425
- await execa2("osascript", ["-e", 'tell application "Terminal" to activate']);
578
+ await execa3("osascript", ["-e", 'tell application "Terminal" to activate']);
426
579
  }
427
580
  } catch (error) {
428
581
  throw new Error(
@@ -590,7 +743,7 @@ async function openMultipleTerminalWindows(optionsArray) {
590
743
  if (hasITerm2) {
591
744
  const applescript = buildITerm2MultiTabScript(optionsArray);
592
745
  try {
593
- await execa2("osascript", ["-e", applescript]);
746
+ await execa3("osascript", ["-e", applescript]);
594
747
  } catch (error) {
595
748
  throw new Error(
596
749
  `Failed to open iTerm2 window: ${error instanceof Error ? error.message : "Unknown error"}`
@@ -629,7 +782,7 @@ __export(color_exports, {
629
782
  rgbToHex: () => rgbToHex,
630
783
  saturateColor: () => saturateColor
631
784
  });
632
- import { createHash } from "crypto";
785
+ import { createHash as createHash2 } from "crypto";
633
786
  function getColorPalette() {
634
787
  return [
635
788
  // First 10 colors preserved for backward compatibility
@@ -736,7 +889,7 @@ function hexToRgb(hex) {
736
889
  return { r, g, b };
737
890
  }
738
891
  function generateColorFromBranchName(branchName) {
739
- const hash = createHash("sha256").update(branchName).digest("hex");
892
+ const hash = createHash2("sha256").update(branchName).digest("hex");
740
893
  const hashPrefix = hash.slice(0, 8);
741
894
  const palette = getColorPalette();
742
895
  const hashAsInt = parseInt(hashPrefix, 16);
@@ -788,388 +941,159 @@ var init_color = __esm({
788
941
  }
789
942
  });
790
943
 
791
- // src/utils/claude.ts
792
- var claude_exports = {};
793
- __export(claude_exports, {
794
- detectClaudeCli: () => detectClaudeCli,
795
- generateBranchName: () => generateBranchName,
796
- getClaudeVersion: () => getClaudeVersion,
797
- launchClaude: () => launchClaude,
798
- launchClaudeInNewTerminalWindow: () => launchClaudeInNewTerminalWindow
799
- });
800
- import { execa as execa3 } from "execa";
801
- import { existsSync as existsSync2 } from "fs";
802
- import { join } from "path";
803
- async function detectClaudeCli() {
804
- try {
805
- await execa3("command", ["-v", "claude"], {
806
- shell: true,
807
- timeout: 5e3
808
- });
809
- return true;
810
- } catch (error) {
811
- logger.debug("Claude CLI not available", { error });
812
- return false;
813
- }
814
- }
815
- 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) {
816
958
  try {
817
- const result = await execa3("claude", ["--version"], {
818
- 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()
819
965
  });
820
- return result.stdout.trim();
966
+ return result.stdout;
821
967
  } catch (error) {
822
- logger.warn("Failed to get Claude version", { error });
823
- return null;
968
+ const execaError = error;
969
+ const stderr = execaError.stderr ?? execaError.message ?? "Unknown Git error";
970
+ throw new Error(`Git command failed: ${stderr}`);
824
971
  }
825
972
  }
826
- function parseJsonStreamOutput(output) {
827
- try {
828
- const lines = output.split("\n").filter((line) => line.trim());
829
- let lastResult = "";
830
- for (const line of lines) {
831
- try {
832
- const jsonObj = JSON.parse(line);
833
- if (jsonObj && typeof jsonObj === "object" && jsonObj.type === "result" && "result" in jsonObj) {
834
- lastResult = jsonObj.result;
835
- }
836
- } 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++;
837
1000
  continue;
838
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++;
839
1023
  }
840
- return lastResult || output;
841
- } catch {
842
- return output;
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;
1034
+ }
1035
+ worktrees.push(worktree);
843
1036
  }
1037
+ return worktrees;
844
1038
  }
845
- async function launchClaude(prompt, options = {}) {
846
- const { model, permissionMode, addDir, headless = false, appendSystemPrompt, mcpConfig, allowedTools, disallowedTools, agents } = options;
847
- const args = [];
848
- if (headless) {
849
- args.push("-p");
850
- args.push("--output-format", "stream-json");
851
- args.push("--verbose");
1039
+ function isPRBranch(branchName) {
1040
+ const prPatterns = [
1041
+ /^pr\/\d+/i,
1042
+ // pr/123, pr/123-feature-name
1043
+ /^pull\/\d+/i,
1044
+ // pull/123
1045
+ /^\d+[-_]/,
1046
+ // 123-feature-name, 123_feature_name
1047
+ /^feature\/pr[-_]?\d+/i,
1048
+ // feature/pr123, feature/pr-123
1049
+ /^hotfix\/pr[-_]?\d+/i
1050
+ // hotfix/pr123
1051
+ ];
1052
+ return prPatterns.some((pattern) => pattern.test(branchName));
1053
+ }
1054
+ function extractPRNumber(branchName) {
1055
+ const patterns = [
1056
+ /^pr\/(\d+)/i,
1057
+ // pr/123
1058
+ /^pull\/(\d+)/i,
1059
+ // pull/123
1060
+ /^(\d+)[-_]/,
1061
+ // 123-feature-name
1062
+ /^feature\/pr[-_]?(\d+)/i,
1063
+ // feature/pr123
1064
+ /^hotfix\/pr[-_]?(\d+)/i,
1065
+ // hotfix/pr123
1066
+ /pr[-_]?(\d+)/i
1067
+ // anywhere with pr123 or pr-123
1068
+ ];
1069
+ for (const pattern of patterns) {
1070
+ const match = branchName.match(pattern);
1071
+ if (match == null ? void 0 : match[1]) {
1072
+ const num = parseInt(match[1], 10);
1073
+ if (!isNaN(num)) return num;
1074
+ }
852
1075
  }
853
- if (model) {
854
- args.push("--model", model);
855
- }
856
- if (permissionMode && permissionMode !== "default") {
857
- args.push("--permission-mode", permissionMode);
858
- }
859
- if (addDir) {
860
- args.push("--add-dir", addDir);
861
- }
862
- args.push("--add-dir", "/tmp");
863
- if (appendSystemPrompt) {
864
- args.push("--append-system-prompt", appendSystemPrompt);
865
- }
866
- if (mcpConfig && mcpConfig.length > 0) {
867
- for (const config of mcpConfig) {
868
- args.push("--mcp-config", JSON.stringify(config));
869
- }
870
- }
871
- if (allowedTools && allowedTools.length > 0) {
872
- args.push("--allowed-tools", ...allowedTools);
873
- }
874
- if (disallowedTools && disallowedTools.length > 0) {
875
- args.push("--disallowed-tools", ...disallowedTools);
876
- }
877
- if (agents) {
878
- args.push("--agents", JSON.stringify(agents));
879
- }
880
- try {
881
- if (headless) {
882
- const isDebugMode = logger.isDebugEnabled();
883
- const execaOptions = {
884
- input: prompt,
885
- timeout: 0,
886
- // Disable timeout for long responses
887
- ...addDir && { cwd: addDir },
888
- // Run Claude in the worktree directory
889
- verbose: isDebugMode,
890
- ...isDebugMode && { stdio: ["pipe", "pipe", "pipe"] }
891
- // Enable streaming in debug mode
892
- };
893
- const subprocess = execa3("claude", args, execaOptions);
894
- const isJsonStreamFormat = args.includes("--output-format") && args.includes("stream-json");
895
- let outputBuffer = "";
896
- let isStreaming = false;
897
- let isFirstProgress = true;
898
- if (subprocess.stdout && typeof subprocess.stdout.on === "function") {
899
- isStreaming = true;
900
- subprocess.stdout.on("data", (chunk) => {
901
- const text = chunk.toString();
902
- outputBuffer += text;
903
- if (isDebugMode) {
904
- process.stdout.write(text);
905
- } else {
906
- if (isFirstProgress) {
907
- process.stdout.write("\u{1F916} .");
908
- isFirstProgress = false;
909
- } else {
910
- process.stdout.write(".");
911
- }
912
- }
913
- });
914
- }
915
- const result = await subprocess;
916
- if (isStreaming) {
917
- const rawOutput = outputBuffer.trim();
918
- if (!isDebugMode) {
919
- process.stdout.write("\n");
920
- }
921
- return isJsonStreamFormat ? parseJsonStreamOutput(rawOutput) : rawOutput;
922
- } else {
923
- if (isDebugMode) {
924
- process.stdout.write(result.stdout);
925
- if (result.stdout && !result.stdout.endsWith("\n")) {
926
- process.stdout.write("\n");
927
- }
928
- } else {
929
- process.stdout.write("\u{1F916} .");
930
- process.stdout.write("\n");
931
- }
932
- const rawOutput = result.stdout.trim();
933
- return isJsonStreamFormat ? parseJsonStreamOutput(rawOutput) : rawOutput;
934
- }
935
- } else {
936
- await execa3("claude", [...args, "--", prompt], {
937
- ...addDir && { cwd: addDir },
938
- stdio: "inherit",
939
- // Let user interact directly in current terminal
940
- timeout: 0,
941
- // Disable timeout
942
- verbose: logger.isDebugEnabled()
943
- });
944
- return;
945
- }
946
- } catch (error) {
947
- const execaError = error;
948
- const errorMessage = execaError.stderr ?? execaError.message ?? "Unknown Claude CLI error";
949
- throw new Error(`Claude CLI error: ${errorMessage}`);
950
- }
951
- }
952
- async function launchClaudeInNewTerminalWindow(_prompt, options) {
953
- const { workspacePath, branchName, oneShot = "default", port, setArguments, executablePath } = options;
954
- if (!workspacePath) {
955
- throw new Error("workspacePath is required for terminal window launch");
956
- }
957
- const { openTerminalWindow: openTerminalWindow2 } = await Promise.resolve().then(() => (init_terminal(), terminal_exports));
958
- const executable = executablePath ?? "iloom";
959
- let launchCommand = `${executable} spin`;
960
- if (oneShot !== "default") {
961
- launchCommand += ` --one-shot=${oneShot}`;
962
- }
963
- if (setArguments && setArguments.length > 0) {
964
- for (const setArg of setArguments) {
965
- launchCommand += ` --set ${setArg}`;
966
- }
967
- }
968
- let backgroundColor;
969
- if (branchName) {
970
- try {
971
- const { generateColorFromBranchName: generateColorFromBranchName2 } = await Promise.resolve().then(() => (init_color(), color_exports));
972
- const colorData = generateColorFromBranchName2(branchName);
973
- backgroundColor = colorData.rgb;
974
- } catch (error) {
975
- logger.warn(
976
- `Failed to generate terminal color: ${error instanceof Error ? error.message : "Unknown error"}`
977
- );
978
- }
979
- }
980
- const hasEnvFile = existsSync2(join(workspacePath, ".env"));
981
- await openTerminalWindow2({
982
- workspacePath,
983
- command: launchCommand,
984
- ...backgroundColor && { backgroundColor },
985
- includeEnvSetup: hasEnvFile,
986
- // source .env only if it exists
987
- ...port !== void 0 && { port, includePortExport: true }
988
- });
989
- }
990
- async function generateBranchName(issueTitle, issueNumber, model = "haiku") {
991
- try {
992
- const isAvailable = await detectClaudeCli();
993
- if (!isAvailable) {
994
- logger.warn("Claude CLI not available, using fallback branch name");
995
- return `feat/issue-${issueNumber}`;
996
- }
997
- logger.debug("Generating branch name with Claude", { issueNumber, issueTitle });
998
- const prompt = `<Task>
999
- Generate a git branch name for the following issue:
1000
- <Issue>
1001
- <IssueNumber>${issueNumber}</IssueNumber>
1002
- <IssueTitle>${issueTitle}</IssueTitle>
1003
- </Issue>
1004
-
1005
- <Requirements>
1006
- <IssueNumber>Must use this exact issue number: ${issueNumber}</IssueNumber>
1007
- <Format>Format must be: {prefix}/issue-${issueNumber}-{description}</Format>
1008
- <Prefix>Prefix must be one of: feat, fix, docs, refactor, test, chore</Prefix>
1009
- <MaxLength>Maximum 50 characters total</MaxLength>
1010
- <Characters>Only lowercase letters, numbers, and hyphens allowed</Characters>
1011
- <Output>Reply with ONLY the branch name, nothing else</Output>
1012
- </Requirements>
1013
- </Task>`;
1014
- logger.debug("Sending prompt to Claude", { prompt });
1015
- const result = await launchClaude(prompt, {
1016
- model,
1017
- headless: true
1018
- });
1019
- const branchName = result.trim();
1020
- logger.debug("Claude returned branch name", { branchName, issueNumber });
1021
- if (!branchName || !isValidBranchName(branchName, issueNumber)) {
1022
- logger.warn("Invalid branch name from Claude, using fallback", { branchName });
1023
- return `feat/issue-${issueNumber}`;
1024
- }
1025
- return branchName;
1026
- } catch (error) {
1027
- logger.warn("Failed to generate branch name with Claude", { error });
1028
- return `feat/issue-${issueNumber}`;
1029
- }
1030
- }
1031
- function isValidBranchName(name, issueNumber) {
1032
- const pattern = new RegExp(`^(feat|fix|docs|refactor|test|chore)/issue-${issueNumber}-[a-z0-9-]+$`);
1033
- return pattern.test(name) && name.length <= 50;
1034
- }
1035
- var init_claude = __esm({
1036
- "src/utils/claude.ts"() {
1037
- "use strict";
1038
- init_logger();
1039
- }
1040
- });
1041
-
1042
- // src/lib/WorkspaceManager.ts
1043
- var WorkspaceManager = class {
1044
- // TODO: Implement in Issue #6
1045
- };
1046
-
1047
- // src/lib/GitWorktreeManager.ts
1048
- import path3 from "path";
1049
- import fs from "fs-extra";
1050
-
1051
- // src/utils/git.ts
1052
- init_logger();
1053
- import path2 from "path";
1054
- import { execa } from "execa";
1055
- async function executeGitCommand(args, options) {
1056
- try {
1057
- const result = await execa("git", args, {
1058
- cwd: (options == null ? void 0 : options.cwd) ?? process.cwd(),
1059
- timeout: (options == null ? void 0 : options.timeout) ?? 3e4,
1060
- encoding: "utf8",
1061
- stdio: (options == null ? void 0 : options.stdio) ?? "pipe",
1062
- verbose: logger.isDebugEnabled()
1063
- });
1064
- return result.stdout;
1065
- } catch (error) {
1066
- const execaError = error;
1067
- const stderr = execaError.stderr ?? execaError.message ?? "Unknown Git error";
1068
- throw new Error(`Git command failed: ${stderr}`);
1069
- }
1070
- }
1071
- function parseWorktreeList(output, defaultBranch) {
1072
- var _a, _b;
1073
- const worktrees = [];
1074
- const lines = output.trim().split("\n");
1075
- let i = 0;
1076
- while (i < lines.length) {
1077
- const pathLine = lines[i];
1078
- if (!(pathLine == null ? void 0 : pathLine.startsWith("worktree "))) {
1079
- i++;
1080
- continue;
1081
- }
1082
- const pathMatch = pathLine.match(/^worktree (.+)$/);
1083
- if (!pathMatch) {
1084
- i++;
1085
- continue;
1086
- }
1087
- let branch = "";
1088
- let commit = "";
1089
- let detached = false;
1090
- let bare = false;
1091
- let locked = false;
1092
- let lockReason;
1093
- i++;
1094
- while (i < lines.length && !((_a = lines[i]) == null ? void 0 : _a.startsWith("worktree "))) {
1095
- const line = (_b = lines[i]) == null ? void 0 : _b.trim();
1096
- if (!line) {
1097
- i++;
1098
- continue;
1099
- }
1100
- if (line === "bare") {
1101
- bare = true;
1102
- branch = defaultBranch ?? "main";
1103
- } else if (line === "detached") {
1104
- detached = true;
1105
- branch = "HEAD";
1106
- } else if (line.startsWith("locked")) {
1107
- locked = true;
1108
- const lockMatch = line.match(/^locked (.+)$/);
1109
- lockReason = lockMatch == null ? void 0 : lockMatch[1];
1110
- branch = branch || "unknown";
1111
- } else if (line.startsWith("HEAD ")) {
1112
- const commitMatch = line.match(/^HEAD ([a-f0-9]+)/);
1113
- if (commitMatch) {
1114
- commit = commitMatch[1] ?? "";
1115
- }
1116
- } else if (line.startsWith("branch ")) {
1117
- const branchMatch = line.match(/^branch refs\/heads\/(.+)$/);
1118
- branch = (branchMatch == null ? void 0 : branchMatch[1]) ?? line.replace("branch ", "");
1119
- }
1120
- i++;
1121
- }
1122
- const worktree = {
1123
- path: pathMatch[1] ?? "",
1124
- branch,
1125
- commit,
1126
- bare,
1127
- detached,
1128
- locked
1129
- };
1130
- if (lockReason !== void 0) {
1131
- worktree.lockReason = lockReason;
1132
- }
1133
- worktrees.push(worktree);
1134
- }
1135
- return worktrees;
1136
- }
1137
- function isPRBranch(branchName) {
1138
- const prPatterns = [
1139
- /^pr\/\d+/i,
1140
- // pr/123, pr/123-feature-name
1141
- /^pull\/\d+/i,
1142
- // pull/123
1143
- /^\d+[-_]/,
1144
- // 123-feature-name, 123_feature_name
1145
- /^feature\/pr[-_]?\d+/i,
1146
- // feature/pr123, feature/pr-123
1147
- /^hotfix\/pr[-_]?\d+/i
1148
- // hotfix/pr123
1149
- ];
1150
- return prPatterns.some((pattern) => pattern.test(branchName));
1151
- }
1152
- function extractPRNumber(branchName) {
1153
- const patterns = [
1154
- /^pr\/(\d+)/i,
1155
- // pr/123
1156
- /^pull\/(\d+)/i,
1157
- // pull/123
1158
- /^(\d+)[-_]/,
1159
- // 123-feature-name
1160
- /^feature\/pr[-_]?(\d+)/i,
1161
- // feature/pr123
1162
- /^hotfix\/pr[-_]?(\d+)/i,
1163
- // hotfix/pr123
1164
- /pr[-_]?(\d+)/i
1165
- // anywhere with pr123 or pr-123
1076
+ return null;
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
1166
1093
  ];
1167
- for (const pattern of patterns) {
1094
+ for (const pattern of legacyPatterns) {
1168
1095
  const match = branchName.match(pattern);
1169
- if (match == null ? void 0 : match[1]) {
1170
- const num = parseInt(match[1], 10);
1171
- if (!isNaN(num)) return num;
1172
- }
1096
+ if (match == null ? void 0 : match[1]) return match[1];
1173
1097
  }
1174
1098
  return null;
1175
1099
  }
@@ -1766,7 +1690,7 @@ var GitWorktreeManager = class {
1766
1690
  */
1767
1691
  async findWorktreeForIssue(issueNumber) {
1768
1692
  const worktrees = await this.listWorktrees({ porcelain: true });
1769
- const pattern = new RegExp(`(?:^|[/_-])issue-${issueNumber}(?:-|$)`);
1693
+ const pattern = new RegExp(`(?:^|[/_-])issue-${issueNumber}(?:-|__|$)`);
1770
1694
  return worktrees.find((wt) => pattern.test(wt.branch)) ?? null;
1771
1695
  }
1772
1696
  /**
@@ -1840,9 +1764,9 @@ var GitHubError = class extends Error {
1840
1764
 
1841
1765
  // src/utils/github.ts
1842
1766
  init_logger();
1843
- import { execa as execa4 } from "execa";
1767
+ import { execa as execa2 } from "execa";
1844
1768
  async function executeGhCommand(args, options) {
1845
- const result = await execa4("gh", args, {
1769
+ const result = await execa2("gh", args, {
1846
1770
  cwd: (options == null ? void 0 : options.cwd) ?? process.cwd(),
1847
1771
  timeout: (options == null ? void 0 : options.timeout) ?? 3e4,
1848
1772
  encoding: "utf8"
@@ -1984,21 +1908,6 @@ async function updateProjectItemField(itemId, projectId, fieldId, optionId) {
1984
1908
  "json"
1985
1909
  ]);
1986
1910
  }
1987
- var SimpleBranchNameStrategy = class {
1988
- async generate(issueNumber, title) {
1989
- const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").substring(0, 20);
1990
- return `feat/issue-${issueNumber}-${slug}`;
1991
- }
1992
- };
1993
- var ClaudeBranchNameStrategy = class {
1994
- constructor(claudeModel = "haiku") {
1995
- this.claudeModel = claudeModel;
1996
- }
1997
- async generate(issueNumber, title) {
1998
- const { generateBranchName: generateBranchName2 } = await Promise.resolve().then(() => (init_claude(), claude_exports));
1999
- return generateBranchName2(title, issueNumber, this.claudeModel);
2000
- }
2001
- };
2002
1911
  async function createIssue(title, body, options) {
2003
1912
  const { repo, labels } = options ?? {};
2004
1913
  logger.debug("Creating GitHub issue", { title, repo, labels });
@@ -2023,7 +1932,7 @@ async function createIssue(title, body, options) {
2023
1932
  if (!repo) {
2024
1933
  execaOptions.cwd = process.cwd();
2025
1934
  }
2026
- const result = await execa4("gh", args, execaOptions);
1935
+ const result = await execa2("gh", args, execaOptions);
2027
1936
  const urlMatch = result.stdout.trim().match(/https:\/\/github\.com\/[^/]+\/[^/]+\/issues\/(\d+)/);
2028
1937
  if (!(urlMatch == null ? void 0 : urlMatch[1])) {
2029
1938
  throw new Error(`Failed to parse issue URL from gh output: ${result.stdout}`);
@@ -2077,35 +1986,29 @@ async function promptConfirmation(message, defaultValue = false) {
2077
1986
  // src/lib/GitHubService.ts
2078
1987
  var GitHubService = class {
2079
1988
  constructor(options) {
2080
- if (options == null ? void 0 : options.branchNameStrategy) {
2081
- this.defaultBranchNameStrategy = options.branchNameStrategy;
2082
- } else if ((options == null ? void 0 : options.useClaude) !== false) {
2083
- this.defaultBranchNameStrategy = new ClaudeBranchNameStrategy(
2084
- options == null ? void 0 : options.claudeModel
2085
- );
2086
- } else {
2087
- this.defaultBranchNameStrategy = new SimpleBranchNameStrategy();
2088
- }
1989
+ // IssueTracker interface implementation
1990
+ this.providerName = "github";
1991
+ this.supportsPullRequests = true;
2089
1992
  this.prompter = (options == null ? void 0 : options.prompter) ?? promptConfirmation;
2090
1993
  }
2091
- // Input detection
1994
+ // Input detection - IssueTracker interface implementation
2092
1995
  async detectInputType(input, repo) {
2093
1996
  const numberMatch = input.match(/^#?(\d+)$/);
2094
1997
  if (!(numberMatch == null ? void 0 : numberMatch[1])) {
2095
- return { type: "unknown", number: null, rawInput: input };
1998
+ return { type: "unknown", identifier: null, rawInput: input };
2096
1999
  }
2097
2000
  const number = parseInt(numberMatch[1], 10);
2098
2001
  logger.debug("Checking if input is a PR", { number });
2099
2002
  const pr = await this.isValidPR(number, repo);
2100
2003
  if (pr) {
2101
- return { type: "pr", number, rawInput: input };
2004
+ return { type: "pr", identifier: number.toString(), rawInput: input };
2102
2005
  }
2103
2006
  logger.debug("Checking if input is an issue", { number });
2104
2007
  const issue = await this.isValidIssue(number, repo);
2105
2008
  if (issue) {
2106
- return { type: "issue", number, rawInput: input };
2009
+ return { type: "issue", identifier: number.toString(), rawInput: input };
2107
2010
  }
2108
- return { type: "unknown", number: null, rawInput: input };
2011
+ return { type: "unknown", identifier: null, rawInput: input };
2109
2012
  }
2110
2013
  // Issue fetching with validation
2111
2014
  async fetchIssue(issueNumber, repo) {
@@ -2199,17 +2102,6 @@ var GitHubService = class {
2199
2102
  }
2200
2103
  }
2201
2104
  }
2202
- // Branch name generation using strategy pattern
2203
- async generateBranchName(options) {
2204
- const { issueNumber, title, strategy } = options;
2205
- const nameStrategy = strategy ?? this.defaultBranchNameStrategy;
2206
- logger.debug("Generating branch name", {
2207
- issueNumber,
2208
- title,
2209
- strategy: nameStrategy.constructor.name
2210
- });
2211
- return nameStrategy.generate(issueNumber, title);
2212
- }
2213
2105
  // Issue creation
2214
2106
  async createIssue(title, body, repository, labels) {
2215
2107
  return createIssue(title, body, { repo: repository, labels });
@@ -2344,13 +2236,372 @@ State: ${entity.state}`;
2344
2236
  async promptUserConfirmation(message) {
2345
2237
  return this.prompter(message);
2346
2238
  }
2347
- // Allow setting strategy at runtime for specific operations
2348
- setDefaultBranchNameStrategy(strategy) {
2349
- this.defaultBranchNameStrategy = strategy;
2239
+ };
2240
+
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
+ };
2253
+
2254
+ // src/utils/linear.ts
2255
+ import { LinearClient } from "@linear/sdk";
2256
+ init_logger();
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;
2268
+ }
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
+ }
2350
2594
  }
2351
- // Get current strategy for testing
2352
- getBranchNameStrategy() {
2353
- return this.defaultBranchNameStrategy;
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";
2354
2605
  }
2355
2606
  };
2356
2607
 
@@ -2415,12 +2666,12 @@ function isValidEnvKey(key) {
2415
2666
  }
2416
2667
 
2417
2668
  // src/utils/port.ts
2418
- import { createHash as createHash2 } from "crypto";
2669
+ import { createHash } from "crypto";
2419
2670
  function generatePortOffsetFromBranchName(branchName) {
2420
2671
  if (!branchName || branchName.trim().length === 0) {
2421
2672
  throw new Error("Branch name cannot be empty");
2422
2673
  }
2423
- const hash = createHash2("sha256").update(branchName).digest("hex");
2674
+ const hash = createHash("sha256").update(branchName).digest("hex");
2424
2675
  const hashPrefix = hash.slice(0, 8);
2425
2676
  const hashAsInt = parseInt(hashPrefix, 16);
2426
2677
  const portOffset = hashAsInt % 999 + 1;
@@ -2515,6 +2766,14 @@ var EnvironmentManager = class {
2515
2766
  return /* @__PURE__ */ new Map();
2516
2767
  }
2517
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
+ }
2518
2777
  /**
2519
2778
  * Generic file copy helper that only copies if source exists
2520
2779
  * Does not throw if source file doesn't exist - just logs and returns
@@ -2538,13 +2797,17 @@ var EnvironmentManager = class {
2538
2797
  calculatePort(options) {
2539
2798
  const basePort = options.basePort ?? 3e3;
2540
2799
  if (options.issueNumber !== void 0) {
2541
- const port = basePort + options.issueNumber;
2542
- if (port > 65535) {
2543
- throw new Error(
2544
- `Calculated port ${port} exceeds maximum (65535). Use a lower base port or issue number.`
2545
- );
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;
2546
2809
  }
2547
- return port;
2810
+ return calculatePortForBranch(String(options.issueNumber), basePort);
2548
2811
  }
2549
2812
  if (options.prNumber !== void 0) {
2550
2813
  const port = basePort + options.prNumber;
@@ -2664,8 +2927,9 @@ var DatabaseManager = class {
2664
2927
  * @param branchName - Name of the branch to create
2665
2928
  * @param envFilePath - Path to .env file for configuration checks
2666
2929
  * @param cwd - Optional working directory to run commands from
2930
+ * @param fromBranch - Optional parent branch to create from (for child looms)
2667
2931
  */
2668
- async createBranchIfConfigured(branchName, envFilePath, cwd) {
2932
+ async createBranchIfConfigured(branchName, envFilePath, cwd, fromBranch) {
2669
2933
  if (!await this.shouldUseDatabaseBranching(envFilePath)) {
2670
2934
  return null;
2671
2935
  }
@@ -2687,7 +2951,7 @@ var DatabaseManager = class {
2687
2951
  throw error;
2688
2952
  }
2689
2953
  try {
2690
- const connectionString = await this.provider.createBranch(branchName, void 0, cwd);
2954
+ const connectionString = await this.provider.createBranch(branchName, fromBranch, cwd);
2691
2955
  logger3.success(`Database branch ready: ${this.provider.sanitizeBranchName(branchName)}`);
2692
2956
  return connectionString;
2693
2957
  } catch (error) {
@@ -2774,51 +3038,248 @@ var DatabaseManager = class {
2774
3038
  };
2775
3039
  }
2776
3040
  }
2777
- /**
2778
- * Check if .env has the configured database URL variable
2779
- * CRITICAL: If user explicitly configured a custom variable name (not default),
2780
- * throw an error if it's missing from .env
2781
- */
2782
- async hasDatabaseUrlInEnv(envFilePath) {
2783
- try {
2784
- const envMap = await this.environment.readEnvFile(envFilePath);
2785
- if (this.databaseUrlEnvVarName !== "DATABASE_URL") {
2786
- logger3.debug(`Looking for custom database URL variable: ${this.databaseUrlEnvVarName}`);
2787
- } else {
2788
- logger3.debug("Looking for default database URL variable: DATABASE_URL");
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
+ }
3059
+ /**
3060
+ * Check if .env has the configured database URL variable
3061
+ * CRITICAL: If user explicitly configured a custom variable name (not default),
3062
+ * throw an error if it's missing from .env
3063
+ */
3064
+ async hasDatabaseUrlInEnv(envFilePath) {
3065
+ try {
3066
+ const envMap = await this.environment.readEnvFile(envFilePath);
3067
+ if (this.databaseUrlEnvVarName !== "DATABASE_URL") {
3068
+ logger3.debug(`Looking for custom database URL variable: ${this.databaseUrlEnvVarName}`);
3069
+ } else {
3070
+ logger3.debug("Looking for default database URL variable: DATABASE_URL");
3071
+ }
3072
+ if (envMap.has(this.databaseUrlEnvVarName)) {
3073
+ if (this.databaseUrlEnvVarName !== "DATABASE_URL") {
3074
+ logger3.debug(`\u2705 Found custom database URL variable: ${this.databaseUrlEnvVarName}`);
3075
+ } else {
3076
+ logger3.debug(`\u2705 Found default database URL variable: DATABASE_URL`);
3077
+ }
3078
+ return true;
3079
+ }
3080
+ if (this.databaseUrlEnvVarName !== "DATABASE_URL") {
3081
+ logger3.debug(`\u274C Custom database URL variable '${this.databaseUrlEnvVarName}' not found in .env file`);
3082
+ throw new Error(
3083
+ `Configured database URL environment variable '${this.databaseUrlEnvVarName}' not found in .env file. Please add it to your .env file or update your iloom configuration.`
3084
+ );
3085
+ }
3086
+ const hasDefaultVar = envMap.has("DATABASE_URL");
3087
+ if (hasDefaultVar) {
3088
+ logger3.debug("\u2705 Found fallback DATABASE_URL variable");
3089
+ } else {
3090
+ logger3.debug("\u274C No DATABASE_URL variable found in .env file");
3091
+ }
3092
+ return hasDefaultVar;
3093
+ } catch (error) {
3094
+ if (error instanceof Error && error.message.includes("not found in .env")) {
3095
+ throw error;
3096
+ }
3097
+ return false;
3098
+ }
3099
+ }
3100
+ };
3101
+
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
+ });
2789
3207
  }
2790
- if (envMap.has(this.databaseUrlEnvVarName)) {
2791
- if (this.databaseUrlEnvVarName !== "DATABASE_URL") {
2792
- logger3.debug(`\u2705 Found custom database URL variable: ${this.databaseUrlEnvVarName}`);
2793
- } else {
2794
- logger3.debug(`\u2705 Found default database URL variable: DATABASE_URL`);
3208
+ const result = await subprocess;
3209
+ if (isStreaming) {
3210
+ const rawOutput = outputBuffer.trim();
3211
+ if (!isDebugMode) {
3212
+ process.stdout.write("\n");
2795
3213
  }
2796
- return true;
2797
- }
2798
- if (this.databaseUrlEnvVarName !== "DATABASE_URL") {
2799
- logger3.debug(`\u274C Custom database URL variable '${this.databaseUrlEnvVarName}' not found in .env file`);
2800
- throw new Error(
2801
- `Configured database URL environment variable '${this.databaseUrlEnvVarName}' not found in .env file. Please add it to your .env file or update your iloom configuration.`
2802
- );
2803
- }
2804
- const hasDefaultVar = envMap.has("DATABASE_URL");
2805
- if (hasDefaultVar) {
2806
- logger3.debug("\u2705 Found fallback DATABASE_URL variable");
3214
+ return isJsonStreamFormat ? parseJsonStreamOutput(rawOutput) : rawOutput;
2807
3215
  } else {
2808
- logger3.debug("\u274C No DATABASE_URL variable found in .env file");
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;
2809
3227
  }
2810
- return hasDefaultVar;
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;
2811
3267
  } catch (error) {
2812
- if (error instanceof Error && error.message.includes("not found in .env")) {
2813
- throw error;
2814
- }
2815
- return false;
3268
+ logger.warn(
3269
+ `Failed to generate terminal color: ${error instanceof Error ? error.message : "Unknown error"}`
3270
+ );
2816
3271
  }
2817
3272
  }
2818
- };
2819
-
2820
- // src/lib/ClaudeService.ts
2821
- init_claude();
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
+ }
2822
3283
 
2823
3284
  // src/lib/PromptTemplateManager.ts
2824
3285
  init_logger();
@@ -2898,6 +3359,9 @@ var PromptTemplateManager = class {
2898
3359
  if (variables.SETTINGS_SCHEMA !== void 0) {
2899
3360
  result = result.replace(/SETTINGS_SCHEMA/g, variables.SETTINGS_SCHEMA);
2900
3361
  }
3362
+ if (variables.SETTINGS_GLOBAL_JSON !== void 0) {
3363
+ result = result.replace(/SETTINGS_GLOBAL_JSON/g, variables.SETTINGS_GLOBAL_JSON);
3364
+ }
2901
3365
  if (variables.SETTINGS_JSON !== void 0) {
2902
3366
  result = result.replace(/SETTINGS_JSON/g, variables.SETTINGS_JSON);
2903
3367
  }
@@ -2959,6 +3423,12 @@ var PromptTemplateManager = class {
2959
3423
  } else {
2960
3424
  result = result.replace(settingsJsonRegex, "");
2961
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
+ }
2962
3432
  const settingsLocalJsonRegex = /\{\{#IF SETTINGS_LOCAL_JSON\}\}(.*?)\{\{\/IF SETTINGS_LOCAL_JSON\}\}/gs;
2963
3433
  if (variables.SETTINGS_LOCAL_JSON !== void 0 && variables.SETTINGS_LOCAL_JSON !== "") {
2964
3434
  result = result.replace(settingsLocalJsonRegex, "$1");
@@ -3119,17 +3589,6 @@ var ClaudeService = class {
3119
3589
  throw error;
3120
3590
  }
3121
3591
  }
3122
- /**
3123
- * Generate branch name with Claude, with fallback on failure
3124
- */
3125
- async generateBranchNameWithFallback(issueTitle, issueNumber) {
3126
- try {
3127
- return await generateBranchName(issueTitle, issueNumber);
3128
- } catch (error) {
3129
- logger.warn("Claude branch name generation failed, using fallback", { error });
3130
- return `feat/issue-${issueNumber}`;
3131
- }
3132
- }
3133
3592
  };
3134
3593
 
3135
3594
  // src/lib/ClaudeContextManager.ts
@@ -3189,17 +3648,322 @@ var ClaudeContextManager = class {
3189
3648
 
3190
3649
  // src/utils/index.ts
3191
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;
3192
3952
  export {
3193
3953
  ClaudeContextManager,
3194
3954
  DatabaseManager,
3195
3955
  EnvironmentManager,
3196
3956
  GitHubService,
3197
3957
  GitWorktreeManager,
3958
+ IssueTrackerFactory,
3959
+ LinearMarkupConverter,
3960
+ TableFormatter,
3198
3961
  WorkspaceManager,
3199
3962
  branchExists,
3200
3963
  createLogger,
3201
3964
  ensureRepositoryHasCommits,
3202
3965
  executeGitCommand,
3966
+ extractIssueNumber,
3203
3967
  extractPRNumber,
3204
3968
  findAllBranchesForIssue,
3205
3969
  findMainWorktreePath,