@link-assistant/hive-mind 0.46.0 → 0.47.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 (63) hide show
  1. package/CHANGELOG.md +26 -13
  2. package/README.md +42 -8
  3. package/package.json +16 -3
  4. package/src/agent.lib.mjs +49 -70
  5. package/src/agent.prompts.lib.mjs +6 -20
  6. package/src/buildUserMention.lib.mjs +4 -17
  7. package/src/claude-limits.lib.mjs +15 -15
  8. package/src/claude.lib.mjs +617 -626
  9. package/src/claude.prompts.lib.mjs +7 -22
  10. package/src/codex.lib.mjs +39 -71
  11. package/src/codex.prompts.lib.mjs +6 -20
  12. package/src/config.lib.mjs +3 -16
  13. package/src/contributing-guidelines.lib.mjs +5 -18
  14. package/src/exit-handler.lib.mjs +4 -4
  15. package/src/git.lib.mjs +7 -7
  16. package/src/github-issue-creator.lib.mjs +17 -17
  17. package/src/github-linking.lib.mjs +8 -33
  18. package/src/github.batch.lib.mjs +20 -16
  19. package/src/github.graphql.lib.mjs +18 -18
  20. package/src/github.lib.mjs +89 -91
  21. package/src/hive.config.lib.mjs +50 -50
  22. package/src/hive.mjs +1293 -1296
  23. package/src/instrument.mjs +7 -11
  24. package/src/interactive-mode.lib.mjs +112 -138
  25. package/src/lenv-reader.lib.mjs +1 -6
  26. package/src/lib.mjs +36 -45
  27. package/src/lino.lib.mjs +2 -2
  28. package/src/local-ci-checks.lib.mjs +15 -14
  29. package/src/memory-check.mjs +52 -60
  30. package/src/model-mapping.lib.mjs +25 -32
  31. package/src/model-validation.lib.mjs +31 -31
  32. package/src/opencode.lib.mjs +37 -62
  33. package/src/opencode.prompts.lib.mjs +7 -21
  34. package/src/protect-branch.mjs +14 -15
  35. package/src/review.mjs +28 -27
  36. package/src/reviewers-hive.mjs +64 -69
  37. package/src/sentry.lib.mjs +13 -10
  38. package/src/solve.auto-continue.lib.mjs +48 -38
  39. package/src/solve.auto-pr.lib.mjs +111 -69
  40. package/src/solve.branch-errors.lib.mjs +17 -46
  41. package/src/solve.branch.lib.mjs +16 -23
  42. package/src/solve.config.lib.mjs +263 -261
  43. package/src/solve.error-handlers.lib.mjs +21 -79
  44. package/src/solve.execution.lib.mjs +10 -18
  45. package/src/solve.feedback.lib.mjs +25 -46
  46. package/src/solve.mjs +59 -60
  47. package/src/solve.preparation.lib.mjs +10 -36
  48. package/src/solve.repo-setup.lib.mjs +4 -19
  49. package/src/solve.repository.lib.mjs +37 -37
  50. package/src/solve.results.lib.mjs +32 -46
  51. package/src/solve.session.lib.mjs +7 -22
  52. package/src/solve.validation.lib.mjs +19 -17
  53. package/src/solve.watch.lib.mjs +20 -33
  54. package/src/start-screen.mjs +24 -24
  55. package/src/task.mjs +38 -44
  56. package/src/telegram-bot.mjs +125 -121
  57. package/src/telegram-top-command.lib.mjs +32 -48
  58. package/src/usage-limit.lib.mjs +9 -13
  59. package/src/version-info.lib.mjs +1 -1
  60. package/src/version.lib.mjs +1 -1
  61. package/src/youtrack/solve.youtrack.lib.mjs +3 -8
  62. package/src/youtrack/youtrack-sync.mjs +8 -14
  63. package/src/youtrack/youtrack.lib.mjs +26 -28
@@ -53,18 +53,12 @@ async function captureTopOutput(chatId) {
53
53
  * @param {Function} options.isChatAuthorized - Function to check if chat is authorized
54
54
  */
55
55
  export function registerTopCommand(bot, options) {
56
- const {
57
- VERBOSE = false,
58
- isOldMessage,
59
- isForwardedOrReply,
60
- isGroupChat,
61
- isChatAuthorized
62
- } = options;
56
+ const { VERBOSE = false, isOldMessage, isForwardedOrReply, isGroupChat, isChatAuthorized } = options;
63
57
 
64
58
  // /top command - show system top output in an auto-updating message (EXPERIMENTAL)
65
59
  // Only accessible by chat owner
66
60
  // Not documented in /help as requested in issue #500
67
- bot.command('top', async (ctx) => {
61
+ bot.command('top', async ctx => {
68
62
  if (VERBOSE) {
69
63
  console.log('[VERBOSE] /top command received');
70
64
  }
@@ -89,7 +83,9 @@ export function registerTopCommand(bot, options) {
89
83
  if (VERBOSE) {
90
84
  console.log('[VERBOSE] /top ignored: not a group chat');
91
85
  }
92
- await ctx.reply('❌ The /top command only works in group chats.', { reply_to_message_id: ctx.message.message_id });
86
+ await ctx.reply('❌ The /top command only works in group chats.', {
87
+ reply_to_message_id: ctx.message.message_id,
88
+ });
93
89
  return;
94
90
  }
95
91
 
@@ -98,7 +94,9 @@ export function registerTopCommand(bot, options) {
98
94
  if (VERBOSE) {
99
95
  console.log('[VERBOSE] /top ignored: chat not authorized');
100
96
  }
101
- await ctx.reply(`❌ This chat (ID: ${chatId}) is not authorized to use this bot.`, { reply_to_message_id: ctx.message.message_id });
97
+ await ctx.reply(`❌ This chat (ID: ${chatId}) is not authorized to use this bot.`, {
98
+ reply_to_message_id: ctx.message.message_id,
99
+ });
102
100
  return;
103
101
  }
104
102
 
@@ -109,7 +107,9 @@ export function registerTopCommand(bot, options) {
109
107
  if (VERBOSE) {
110
108
  console.log('[VERBOSE] /top ignored: user is not chat owner');
111
109
  }
112
- await ctx.reply('❌ This command is only available to the chat owner.', { reply_to_message_id: ctx.message.message_id });
110
+ await ctx.reply('❌ This command is only available to the chat owner.', {
111
+ reply_to_message_id: ctx.message.message_id,
112
+ });
113
113
  return;
114
114
  }
115
115
  } catch (error) {
@@ -125,12 +125,14 @@ export function registerTopCommand(bot, options) {
125
125
  // Show experimental feature warning
126
126
  await ctx.reply('🧪 *EXPERIMENTAL FEATURE*\n\nThis command is experimental and may have issues. Use with caution.', {
127
127
  parse_mode: 'Markdown',
128
- reply_to_message_id: ctx.message.message_id
128
+ reply_to_message_id: ctx.message.message_id,
129
129
  });
130
130
 
131
131
  // Check if there's already an active top session for this chat
132
132
  if (activeTopSessions.has(chatId)) {
133
- await ctx.reply('❌ A top session is already running for this chat. Stop it first using the button.', { reply_to_message_id: ctx.message.message_id });
133
+ await ctx.reply('❌ A top session is already running for this chat. Stop it first using the button.', {
134
+ reply_to_message_id: ctx.message.message_id,
135
+ });
134
136
  return;
135
137
  }
136
138
 
@@ -173,30 +175,20 @@ export function registerTopCommand(bot, options) {
173
175
  const initialMessage = await ctx.reply('🧪 📊 Loading system monitor... (EXPERIMENTAL)', {
174
176
  reply_to_message_id: ctx.message.message_id,
175
177
  reply_markup: {
176
- inline_keyboard: [[
177
- { text: '🛑 Stop', callback_data: `stop_top_${chatId}` }
178
- ]]
179
- }
178
+ inline_keyboard: [[{ text: '🛑 Stop', callback_data: `stop_top_${chatId}` }]],
179
+ },
180
180
  });
181
181
 
182
182
  // Capture and display first output
183
183
  const firstOutput = await captureTopOutput(chatId);
184
184
  if (firstOutput) {
185
185
  try {
186
- await ctx.telegram.editMessageText(
187
- chatId,
188
- initialMessage.message_id,
189
- undefined,
190
- `\`\`\`\n${firstOutput}\n\`\`\``,
191
- {
192
- parse_mode: 'Markdown',
193
- reply_markup: {
194
- inline_keyboard: [[
195
- { text: '🛑 Stop', callback_data: `stop_top_${chatId}` }
196
- ]]
197
- }
198
- }
199
- );
186
+ await ctx.telegram.editMessageText(chatId, initialMessage.message_id, undefined, `\`\`\`\n${firstOutput}\n\`\`\``, {
187
+ parse_mode: 'Markdown',
188
+ reply_markup: {
189
+ inline_keyboard: [[{ text: '🛑 Stop', callback_data: `stop_top_${chatId}` }]],
190
+ },
191
+ });
200
192
  } catch (error) {
201
193
  console.error('[ERROR] Failed to update message:', error);
202
194
  }
@@ -207,20 +199,12 @@ export function registerTopCommand(bot, options) {
207
199
  const output = await captureTopOutput(chatId);
208
200
  if (output) {
209
201
  try {
210
- await ctx.telegram.editMessageText(
211
- chatId,
212
- initialMessage.message_id,
213
- undefined,
214
- `\`\`\`\n${output}\n\`\`\``,
215
- {
216
- parse_mode: 'Markdown',
217
- reply_markup: {
218
- inline_keyboard: [[
219
- { text: '🛑 Stop', callback_data: `stop_top_${chatId}` }
220
- ]]
221
- }
222
- }
223
- );
202
+ await ctx.telegram.editMessageText(chatId, initialMessage.message_id, undefined, `\`\`\`\n${output}\n\`\`\``, {
203
+ parse_mode: 'Markdown',
204
+ reply_markup: {
205
+ inline_keyboard: [[{ text: '🛑 Stop', callback_data: `stop_top_${chatId}` }]],
206
+ },
207
+ });
224
208
  } catch (error) {
225
209
  // Ignore "message is not modified" errors
226
210
  if (!error.message?.includes('message is not modified')) {
@@ -234,7 +218,7 @@ export function registerTopCommand(bot, options) {
234
218
  activeTopSessions.set(chatId, {
235
219
  messageId: initialMessage.message_id,
236
220
  screenName,
237
- intervalId
221
+ intervalId,
238
222
  });
239
223
 
240
224
  if (VERBOSE) {
@@ -243,7 +227,7 @@ export function registerTopCommand(bot, options) {
243
227
  });
244
228
 
245
229
  // Handle stop button callback
246
- bot.action(/^stop_top_(.+)$/, async (ctx) => {
230
+ bot.action(/^stop_top_(.+)$/, async ctx => {
247
231
  const chatId = parseInt(ctx.match[1]);
248
232
 
249
233
  if (VERBOSE) {
@@ -302,7 +286,7 @@ export function registerTopCommand(bot, options) {
302
286
  // Update the message to show it's stopped
303
287
  try {
304
288
  await ctx.editMessageText('🛑 Top session stopped.', {
305
- parse_mode: 'Markdown'
289
+ parse_mode: 'Markdown',
306
290
  });
307
291
  } catch (error) {
308
292
  console.error('[ERROR] Failed to edit message:', error);
@@ -34,13 +34,13 @@ export function isUsageLimitError(message) {
34
34
  'limit has been reached',
35
35
  // Provider-specific phrasings we’ve seen in the wild
36
36
  'session limit reached', // Claude
37
- 'weekly limit reached', // Claude
37
+ 'weekly limit reached', // Claude
38
38
  'daily limit reached',
39
39
  'monthly limit reached',
40
40
  'billing hard limit',
41
- 'please try again at', // Codex/OpenCode style
41
+ 'please try again at', // Codex/OpenCode style
42
42
  'available again at',
43
- 'resets' // Claude shows: “∙ resets 5am”
43
+ 'resets', // Claude shows: “∙ resets 5am”
44
44
  ];
45
45
 
46
46
  return patterns.some(pattern => lowerMessage.includes(pattern));
@@ -100,7 +100,8 @@ export function extractResetTime(message) {
100
100
  let hour = parseInt(resets24h[1], 10);
101
101
  const minute = resets24h[2];
102
102
  const ampm = hour >= 12 ? 'PM' : 'AM';
103
- if (hour === 0) hour = 12; // 0 -> 12 AM
103
+ if (hour === 0)
104
+ hour = 12; // 0 -> 12 AM
104
105
  else if (hour > 12) hour -= 12; // 13-23 -> 1-11 PM
105
106
  return `${hour}:${minute} ${ampm}`;
106
107
  }
@@ -137,7 +138,7 @@ export function detectUsageLimit(message) {
137
138
 
138
139
  return {
139
140
  isUsageLimit,
140
- resetTime
141
+ resetTime,
141
142
  };
142
143
  }
143
144
 
@@ -152,12 +153,7 @@ export function detectUsageLimit(message) {
152
153
  * @returns {string[]} - Array of formatted message lines
153
154
  */
154
155
  export function formatUsageLimitMessage({ tool, resetTime, sessionId, resumeCommand }) {
155
- const lines = [
156
- '',
157
- '⏳ Usage Limit Reached!',
158
- '',
159
- `Your ${tool || 'AI tool'} usage limit has been reached.`
160
- ];
156
+ const lines = ['', '⏳ Usage Limit Reached!', '', `Your ${tool || 'AI tool'} usage limit has been reached.`];
161
157
 
162
158
  if (resetTime) {
163
159
  lines.push(`The limit will reset at: ${resetTime}`);
@@ -195,7 +191,7 @@ export function parseUsageLimitJson(line) {
195
191
  return {
196
192
  type: 'error',
197
193
  message: data.message,
198
- limitInfo: detectUsageLimit(data.message)
194
+ limitInfo: detectUsageLimit(data.message),
199
195
  };
200
196
  }
201
197
  }
@@ -206,7 +202,7 @@ export function parseUsageLimitJson(line) {
206
202
  return {
207
203
  type: 'turn.failed',
208
204
  message: data.error.message,
209
- limitInfo: detectUsageLimit(data.error.message)
205
+ limitInfo: detectUsageLimit(data.error.message),
210
206
  };
211
207
  }
212
208
  }
@@ -61,7 +61,7 @@ export async function getVersionInfo(verbose = false) {
61
61
  }
62
62
 
63
63
  // Playwright MCP (check if installed via npm)
64
- const playwrightMcpVersion = execCommand('npm list -g @playwright/mcp --depth=0 2>&1 | grep @playwright/mcp | awk \'{print $2}\'', verbose);
64
+ const playwrightMcpVersion = execCommand("npm list -g @playwright/mcp --depth=0 2>&1 | grep @playwright/mcp | awk '{print $2}'", verbose);
65
65
  if (verbose && playwrightMcpVersion) {
66
66
  console.log(`[VERBOSE] Playwright MCP version: ${playwrightMcpVersion}`);
67
67
  }
@@ -38,4 +38,4 @@ export async function getVersion() {
38
38
  }
39
39
  }
40
40
 
41
- export default { getVersion };
41
+ export default { getVersion };
@@ -7,12 +7,7 @@
7
7
 
8
8
  // Import YouTrack-related functions
9
9
  const youTrackLib = await import('./youtrack.lib.mjs');
10
- const {
11
- parseYouTrackIssueId,
12
- updateYouTrackIssueStage,
13
- addYouTrackComment,
14
- createYouTrackConfigFromEnv
15
- } = youTrackLib;
10
+ const { parseYouTrackIssueId, updateYouTrackIssueStage, addYouTrackComment, createYouTrackConfigFromEnv } = youTrackLib;
16
11
 
17
12
  /**
18
13
  * Validates YouTrack URLs and extracts issue information
@@ -60,7 +55,7 @@ export async function validateYouTrackUrl(issueUrl) {
60
55
  return {
61
56
  isYouTrackUrl: !!isYouTrackUrl,
62
57
  youTrackIssueId,
63
- youTrackConfig
58
+ youTrackConfig,
64
59
  };
65
60
  }
66
61
 
@@ -113,4 +108,4 @@ export function isYouTrackFormat(url) {
113
108
  if (url.match(/^[A-Z0-9]+-\d+$/i)) return true;
114
109
 
115
110
  return false;
116
- }
111
+ }
@@ -11,9 +11,7 @@
11
11
 
12
12
  // Import YouTrack functions
13
13
  const youTrackLib = await import('./youtrack.lib.mjs');
14
- const {
15
- fetchYouTrackIssues
16
- } = youTrackLib;
14
+ const { fetchYouTrackIssues } = youTrackLib;
17
15
 
18
16
  /**
19
17
  * Find existing GitHub issue for a YouTrack issue
@@ -37,16 +35,12 @@ export async function findGitHubIssueForYouTrack(youTrackId, owner, repo, $) {
37
35
 
38
36
  // Find exact match (YouTrack ID should be in brackets or at start)
39
37
  // Return the first matching issue (prefer open issues)
40
- const openIssue = issues.find(issue =>
41
- issue.state === 'open' && (issue.title.includes(`[${youTrackId}]`) || issue.title.startsWith(`${youTrackId}:`))
42
- );
38
+ const openIssue = issues.find(issue => issue.state === 'open' && (issue.title.includes(`[${youTrackId}]`) || issue.title.startsWith(`${youTrackId}:`)));
43
39
 
44
40
  if (openIssue) return openIssue;
45
41
 
46
42
  // If no open issue, check for closed issues to prevent duplicates
47
- const closedIssue = issues.find(issue =>
48
- issue.state === 'closed' && (issue.title.includes(`[${youTrackId}]`) || issue.title.startsWith(`${youTrackId}:`))
49
- );
43
+ const closedIssue = issues.find(issue => issue.state === 'closed' && (issue.title.includes(`[${youTrackId}]`) || issue.title.startsWith(`${youTrackId}:`)));
50
44
 
51
45
  return closedIssue || null;
52
46
  } catch {
@@ -123,7 +117,7 @@ ${youTrackIssue.description || 'No description provided.'}
123
117
  await log(` 🏷️ Added 'help wanted' label to #${existingIssue.number}`);
124
118
  } catch {
125
119
  // Silently skip if label doesn't exist
126
- await log(' ⚠️ Could not add \'help wanted\' label (may not exist in repo)', { verbose: true });
120
+ await log(" ⚠️ Could not add 'help wanted' label (may not exist in repo)", { verbose: true });
127
121
  }
128
122
  }
129
123
 
@@ -145,7 +139,7 @@ ${youTrackIssue.description || 'No description provided.'}
145
139
  number: issueNumber,
146
140
  title: ghTitle,
147
141
  body: ghBody,
148
- html_url: issueUrl
142
+ html_url: issueUrl,
149
143
  };
150
144
  } else {
151
145
  await log(` ❌ Failed to create issue for ${youTrackId}`, { level: 'error' });
@@ -192,7 +186,7 @@ export async function syncYouTrackToGitHub(youTrackConfig, owner, repo, $, log)
192
186
  githubIssues.push({
193
187
  ...ghIssue,
194
188
  youtrackId: ytIssue.id,
195
- youtrackUrl: `${youTrackConfig.url}/issue/${ytIssue.idReadable}`
189
+ youtrackUrl: `${youTrackConfig.url}/issue/${ytIssue.idReadable}`,
196
190
  });
197
191
  }
198
192
  }
@@ -214,6 +208,6 @@ export function formatIssuesForHive(githubIssues) {
214
208
  html_url: issue.html_url,
215
209
  labels: issue.labels || [{ name: 'help-wanted' }],
216
210
  youtrackId: issue.youtrackId,
217
- youtrackUrl: issue.youtrackUrl
211
+ youtrackUrl: issue.youtrackUrl,
218
212
  }));
219
- }
213
+ }
@@ -94,16 +94,16 @@ async function makeYouTrackRequest(endpoint, config, options = {}) {
94
94
 
95
95
  // Prepare headers
96
96
  const requestHeaders = {
97
- 'Authorization': `Bearer ${config.apiKey}`,
98
- 'Accept': 'application/json',
97
+ Authorization: `Bearer ${config.apiKey}`,
98
+ Accept: 'application/json',
99
99
  'Content-Type': 'application/json',
100
- ...headers
100
+ ...headers,
101
101
  };
102
102
 
103
103
  // Prepare request options
104
104
  const requestOptions = {
105
105
  method,
106
- headers: requestHeaders
106
+ headers: requestHeaders,
107
107
  };
108
108
 
109
109
  if (body && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
@@ -187,15 +187,15 @@ export async function fetchYouTrackIssues(config) {
187
187
 
188
188
  // Transform YouTrack issues to our standard format
189
189
  const issues = response.map(issue => ({
190
- id: issue.idReadable || issue.id, // Use readable ID (PAG-45) if available
190
+ id: issue.idReadable || issue.id, // Use readable ID (PAG-45) if available
191
191
  summary: issue.summary || 'No title',
192
192
  description: issue.description || '',
193
193
  stage: config.stage, // Current stage (what we filtered by)
194
194
  url: `${config.url}/issue/${issue.idReadable || issue.id}`,
195
- reporter: issue.reporter ? (issue.reporter.fullName || issue.reporter.login) : 'Unknown',
196
- assignee: issue.assignee ? (issue.assignee.fullName || issue.assignee.login) : null,
195
+ reporter: issue.reporter ? issue.reporter.fullName || issue.reporter.login : 'Unknown',
196
+ assignee: issue.assignee ? issue.assignee.fullName || issue.assignee.login : null,
197
197
  created: issue.created ? new Date(issue.created) : new Date(),
198
- updated: issue.updated ? new Date(issue.updated) : new Date()
198
+ updated: issue.updated ? new Date(issue.updated) : new Date(),
199
199
  }));
200
200
 
201
201
  await log(`📋 Found ${issues.length} YouTrack issue(s) in stage "${config.stage}"`);
@@ -239,9 +239,7 @@ export async function getYouTrackIssue(issueId, config) {
239
239
  // Find the State/Stage custom field (check both possible names)
240
240
  let currentStage = 'Unknown';
241
241
  if (issue.customFields && Array.isArray(issue.customFields)) {
242
- const stateField = issue.customFields.find(field =>
243
- field.name === 'State' || field.name === 'Stage'
244
- );
242
+ const stateField = issue.customFields.find(field => field.name === 'State' || field.name === 'Stage');
245
243
  if (stateField && stateField.value && stateField.value.name) {
246
244
  currentStage = stateField.value.name;
247
245
  }
@@ -249,16 +247,16 @@ export async function getYouTrackIssue(issueId, config) {
249
247
 
250
248
  // Transform to our standard format
251
249
  const transformedIssue = {
252
- id: issue.idReadable || issue.id, // Use readable ID (PAG-45) as primary ID
250
+ id: issue.idReadable || issue.id, // Use readable ID (PAG-45) as primary ID
253
251
  idReadable: issue.idReadable || issue.id, // User-friendly ID like PAG-55
254
252
  summary: issue.summary || 'No title',
255
253
  description: issue.description || '',
256
254
  stage: currentStage,
257
255
  url: `${config.url}/issue/${issue.idReadable || issue.id}`,
258
- reporter: issue.reporter ? (issue.reporter.fullName || issue.reporter.login) : 'Unknown',
259
- assignee: issue.assignee ? (issue.assignee.fullName || issue.assignee.login) : null,
256
+ reporter: issue.reporter ? issue.reporter.fullName || issue.reporter.login : 'Unknown',
257
+ assignee: issue.assignee ? issue.assignee.fullName || issue.assignee.login : null,
260
258
  created: issue.created ? new Date(issue.created) : new Date(),
261
- updated: issue.updated ? new Date(issue.updated) : new Date()
259
+ updated: issue.updated ? new Date(issue.updated) : new Date(),
262
260
  };
263
261
 
264
262
  await log(`✅ Retrieved YouTrack issue: ${transformedIssue.id} - ${transformedIssue.summary}`);
@@ -292,15 +290,15 @@ export async function updateYouTrackIssueStage(issueId, newStage, config) {
292
290
  name: 'Stage',
293
291
  value: {
294
292
  $type: 'StateBundleElement',
295
- name: newStage
296
- }
297
- }
298
- ]
293
+ name: newStage,
294
+ },
295
+ },
296
+ ],
299
297
  };
300
298
 
301
299
  await makeYouTrackRequest(endpoint, config, {
302
300
  method: 'POST',
303
- body: updateData
301
+ body: updateData,
304
302
  });
305
303
 
306
304
  await log(`✅ Updated YouTrack issue ${issueId} stage to "${newStage}"`);
@@ -328,12 +326,12 @@ export async function addYouTrackComment(issueId, comment, config) {
328
326
  const endpoint = `/issues/${issueId}/comments`;
329
327
  const commentData = {
330
328
  text: comment,
331
- usesMarkdown: true
329
+ usesMarkdown: true,
332
330
  };
333
331
 
334
332
  await makeYouTrackRequest(endpoint, config, {
335
333
  method: 'POST',
336
- body: commentData
334
+ body: commentData,
337
335
  });
338
336
 
339
337
  await log(`✅ Added comment to YouTrack issue ${issueId}`);
@@ -354,7 +352,7 @@ export function createYouTrackConfigFromEnv() {
354
352
  apiKey: process.env.YOUTRACK_API_KEY,
355
353
  projectCode: process.env.YOUTRACK_PROJECT_CODE,
356
354
  stage: process.env.YOUTRACK_STAGE,
357
- nextStage: process.env.YOUTRACK_NEXT_STAGE
355
+ nextStage: process.env.YOUTRACK_NEXT_STAGE,
358
356
  };
359
357
 
360
358
  // Check if basic configuration is available
@@ -383,7 +381,7 @@ export function parseYouTrackIssueId(input) {
383
381
  // URL format: https://company.youtrack.cloud/issue/PROJECT-123
384
382
  /\/issue\/([A-Z0-9][A-Z0-9]*-\d+)/i,
385
383
  // Text containing issue ID
386
- /\b([A-Z0-9][A-Z0-9]*-\d+)\b/i
384
+ /\b([A-Z0-9][A-Z0-9]*-\d+)\b/i,
387
385
  ];
388
386
 
389
387
  for (const pattern of patterns) {
@@ -415,11 +413,11 @@ export function convertYouTrackIssueForGitHub(youTrackIssue, githubRepoUrl) {
415
413
  url: youTrackIssue.url,
416
414
  stage: youTrackIssue.stage,
417
415
  reporter: youTrackIssue.reporter,
418
- assignee: youTrackIssue.assignee
416
+ assignee: youTrackIssue.assignee,
419
417
  },
420
418
  // Store GitHub repo info for PR creation
421
419
  github: {
422
- repoUrl: githubRepoUrl
423
- }
420
+ repoUrl: githubRepoUrl,
421
+ },
424
422
  };
425
- }
423
+ }