@link-assistant/hive-mind 0.46.1 → 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 +10 -15
  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
@@ -16,18 +16,18 @@ let logFile = null;
16
16
  // Helper function to log to both console and file
17
17
  const log = async (message, options = {}) => {
18
18
  const { level = 'info', verbose = false } = options;
19
-
19
+
20
20
  // Skip verbose logs unless --verbose is enabled
21
21
  if (verbose && !global.verboseMode) {
22
22
  return;
23
23
  }
24
-
24
+
25
25
  // Write to file if log file is set
26
26
  if (logFile) {
27
27
  const logMessage = `[${new Date().toISOString()}] [${level.toUpperCase()}] ${message}`;
28
28
  await fs.appendFile(logFile, logMessage + '\n').catch(() => {});
29
29
  }
30
-
30
+
31
31
  // Write to console based on level
32
32
  switch (level) {
33
33
  case 'error':
@@ -49,92 +49,91 @@ const argv = yargs(process.argv.slice(2))
49
49
  .usage('Usage: $0 <github-url> [options]')
50
50
  .positional('github-url', {
51
51
  type: 'string',
52
- description: 'GitHub organization, repository, or user URL to monitor for pull requests'
52
+ description: 'GitHub organization, repository, or user URL to monitor for pull requests',
53
53
  })
54
54
  .option('review-label', {
55
55
  type: 'string',
56
56
  description: 'GitHub label to identify PRs needing review',
57
57
  default: 'needs-review',
58
- alias: 'l'
58
+ alias: 'l',
59
59
  })
60
60
  .option('all-prs', {
61
61
  type: 'boolean',
62
62
  description: 'Review all open pull requests regardless of labels',
63
63
  default: false,
64
- alias: 'a'
64
+ alias: 'a',
65
65
  })
66
66
  .option('skip-draft', {
67
67
  type: 'boolean',
68
68
  description: 'Skip draft pull requests',
69
69
  default: true,
70
- alias: 'd'
70
+ alias: 'd',
71
71
  })
72
72
  .option('skip-approved', {
73
73
  type: 'boolean',
74
74
  description: 'Skip pull requests that already have approvals',
75
- default: true
75
+ default: true,
76
76
  })
77
77
  .option('concurrency', {
78
78
  type: 'number',
79
79
  description: 'Number of concurrent review.mjs instances',
80
80
  default: 2,
81
- alias: 'c'
81
+ alias: 'c',
82
82
  })
83
83
  .option('reviews-per-pr', {
84
84
  type: 'number',
85
85
  description: 'Number of reviews to generate per PR (for diverse perspectives)',
86
86
  default: 1,
87
- alias: 'r'
87
+ alias: 'r',
88
88
  })
89
89
  .option('model', {
90
90
  type: 'string',
91
91
  description: 'Model to use for review.mjs (opus or sonnet)',
92
92
  alias: 'm',
93
93
  default: 'opus',
94
- choices: ['opus', 'sonnet']
94
+ choices: ['opus', 'sonnet'],
95
95
  })
96
96
  .option('focus', {
97
97
  type: 'string',
98
98
  description: 'Focus areas for reviews (security, performance, logic, style, tests, all)',
99
99
  default: 'all',
100
- alias: 'f'
100
+ alias: 'f',
101
101
  })
102
102
  .option('auto-approve', {
103
103
  type: 'boolean',
104
104
  description: 'Auto-approve PRs that pass review criteria',
105
- default: false
105
+ default: false,
106
106
  })
107
107
  .option('interval', {
108
108
  type: 'number',
109
109
  description: 'Polling interval in seconds',
110
110
  default: 300, // 5 minutes
111
- alias: 'i'
111
+ alias: 'i',
112
112
  })
113
113
  .option('max-prs', {
114
114
  type: 'number',
115
115
  description: 'Maximum number of PRs to process (0 = unlimited)',
116
- default: 0
116
+ default: 0,
117
117
  })
118
118
  .option('dry-run', {
119
119
  type: 'boolean',
120
120
  description: 'List PRs that would be reviewed without actually reviewing them',
121
- default: false
121
+ default: false,
122
122
  })
123
123
  .option('verbose', {
124
124
  type: 'boolean',
125
125
  description: 'Enable verbose logging',
126
126
  alias: 'v',
127
- default: false
127
+ default: false,
128
128
  })
129
129
  .option('once', {
130
130
  type: 'boolean',
131
131
  description: 'Run once and exit instead of continuous monitoring',
132
- default: false
132
+ default: false,
133
133
  })
134
134
  .demandCommand(1, 'GitHub URL is required')
135
135
  .help('h')
136
- .alias('h', 'help')
137
- .argv;
136
+ .alias('h', 'help').argv;
138
137
 
139
138
  const githubUrl = argv['github-url'] || argv._[0];
140
139
 
@@ -227,9 +226,7 @@ class PRQueue {
227
226
 
228
227
  // Add PR to queue if not already processed or in queue
229
228
  enqueue(prUrl) {
230
- if (this.completed.has(prUrl) ||
231
- this.processing.has(prUrl) ||
232
- this.queue.includes(prUrl)) {
229
+ if (this.completed.has(prUrl) || this.processing.has(prUrl) || this.queue.includes(prUrl)) {
233
230
  return false;
234
231
  }
235
232
  this.queue.push(prUrl);
@@ -264,7 +261,7 @@ class PRQueue {
264
261
  queued: this.queue.length,
265
262
  processing: this.processing.size,
266
263
  completed: this.completed.size,
267
- failed: this.failed.size
264
+ failed: this.failed.size,
268
265
  };
269
266
  }
270
267
 
@@ -280,10 +277,10 @@ const prQueue = new PRQueue();
280
277
  // Worker function to review PRs from queue
281
278
  async function reviewer(reviewerId) {
282
279
  await log(`šŸ” Reviewer ${reviewerId} started`, { verbose: true });
283
-
280
+
284
281
  while (prQueue.isRunning) {
285
282
  const prUrl = prQueue.dequeue();
286
-
283
+
287
284
  if (!prUrl) {
288
285
  // No work available, wait a bit
289
286
  await new Promise(resolve => setTimeout(resolve, 5000));
@@ -291,13 +288,13 @@ async function reviewer(reviewerId) {
291
288
  }
292
289
 
293
290
  await log(`\nšŸ‘€ Reviewer ${reviewerId} reviewing: ${prUrl}`);
294
-
291
+
295
292
  // Review the PR multiple times if needed (for diverse perspectives)
296
293
  for (let reviewNum = 1; reviewNum <= argv.reviewsPerPr; reviewNum++) {
297
294
  if (argv.reviewsPerPr > 1) {
298
295
  await log(` šŸ“ Creating review ${reviewNum}/${argv.reviewsPerPr} for PR`);
299
296
  }
300
-
297
+
301
298
  try {
302
299
  if (argv.dryRun) {
303
300
  await log(` 🧪 [DRY RUN] Would execute: ./review.mjs "${prUrl}" --model ${argv.model} --focus ${argv.focus}${argv.autoApprove ? ' --approve' : ''}`);
@@ -305,14 +302,14 @@ async function reviewer(reviewerId) {
305
302
  } else {
306
303
  // Execute review.mjs using command-stream
307
304
  await log(` šŸš€ Executing review.mjs for ${prUrl}...`);
308
-
305
+
309
306
  const startTime = Date.now();
310
307
  let reviewCommand = $`./review.mjs "${prUrl}" --model ${argv.model} --focus ${argv.focus}`;
311
-
308
+
312
309
  if (argv.autoApprove) {
313
310
  reviewCommand = $`./review.mjs "${prUrl}" --model ${argv.model} --focus ${argv.focus} --approve`;
314
311
  }
315
-
312
+
316
313
  // Stream output and capture result
317
314
  let exitCode = 0;
318
315
  for await (const chunk of reviewCommand.stream()) {
@@ -330,16 +327,16 @@ async function reviewer(reviewerId) {
330
327
  exitCode = chunk.code;
331
328
  }
332
329
  }
333
-
330
+
334
331
  const duration = Math.round((Date.now() - startTime) / 1000);
335
-
332
+
336
333
  if (exitCode === 0) {
337
334
  await log(` āœ… Reviewer ${reviewerId} completed ${prUrl} (${duration}s)`);
338
335
  } else {
339
336
  throw new Error(`review.mjs exited with code ${exitCode}`);
340
337
  }
341
338
  }
342
-
339
+
343
340
  // Small delay between multiple reviews for same PR
344
341
  if (reviewNum < argv.reviewsPerPr) {
345
342
  await new Promise(resolve => setTimeout(resolve, 10000));
@@ -350,14 +347,14 @@ async function reviewer(reviewerId) {
350
347
  break; // Stop trying more reviews for this PR
351
348
  }
352
349
  }
353
-
350
+
354
351
  prQueue.markCompleted(prUrl);
355
-
352
+
356
353
  // Show queue stats
357
354
  const stats = prQueue.getStats();
358
355
  await log(` šŸ“Š Queue: ${stats.queued} waiting, ${stats.processing} reviewing, ${stats.completed} completed, ${stats.failed} failed`);
359
356
  }
360
-
357
+
361
358
  await log(`šŸ” Reviewer ${reviewerId} stopped`, { verbose: true });
362
359
  }
363
360
 
@@ -379,12 +376,12 @@ async function hasApprovals(prUrl) {
379
376
 
380
377
  const { stdout } = await execAsync(cmd, { encoding: 'utf8', env: process.env });
381
378
  const approvalCount = parseInt(stdout.trim()) || 0;
382
-
379
+
383
380
  if (approvalCount > 0) {
384
381
  await log(` ↳ Skipping (has ${approvalCount} approval${approvalCount > 1 ? 's' : ''})`, { verbose: true });
385
382
  return true;
386
383
  }
387
-
384
+
388
385
  return false;
389
386
  } catch (error) {
390
387
  // If we can't check, assume no approvals
@@ -400,10 +397,10 @@ async function fetchPullRequests() {
400
397
  } else {
401
398
  await log(`\nšŸ” Fetching pull requests with label "${argv.reviewLabel}"...`);
402
399
  }
403
-
400
+
404
401
  try {
405
402
  let prs = [];
406
-
403
+
407
404
  if (argv.allPrs) {
408
405
  // Fetch all open PRs without label filter
409
406
  let searchCmd;
@@ -415,7 +412,7 @@ async function fetchPullRequests() {
415
412
  // User scope
416
413
  searchCmd = `gh search prs user:${owner} is:open --limit 100 --json url,title,number,repository,isDraft`;
417
414
  }
418
-
415
+
419
416
  await log(` šŸ”Ž Command: ${searchCmd}`, { verbose: true });
420
417
 
421
418
  // Use async exec to avoid escaping issues
@@ -424,7 +421,6 @@ async function fetchPullRequests() {
424
421
  const execAsync = promisify(exec);
425
422
  const { stdout } = await execAsync(searchCmd, { encoding: 'utf8', env: process.env });
426
423
  prs = JSON.parse(stdout || '[]');
427
-
428
424
  } else {
429
425
  // Use label filter
430
426
  const { exec } = await import('child_process');
@@ -451,11 +447,11 @@ async function fetchPullRequests() {
451
447
  } else {
452
448
  baseQuery = `user:${owner} is:pr is:open`;
453
449
  }
454
-
450
+
455
451
  // Handle label with potential spaces
456
452
  let searchQuery;
457
453
  let searchCmd;
458
-
454
+
459
455
  if (argv.reviewLabel.includes(' ')) {
460
456
  searchQuery = `${baseQuery} label:"${argv.reviewLabel}"`;
461
457
  searchCmd = `gh search prs '${searchQuery}' --limit 100 --json url,title,number,repository,isDraft`;
@@ -463,7 +459,7 @@ async function fetchPullRequests() {
463
459
  searchQuery = `${baseQuery} label:${argv.reviewLabel}`;
464
460
  searchCmd = `gh search prs '${searchQuery}' --limit 100 --json url,title,number,repository,isDraft`;
465
461
  }
466
-
462
+
467
463
  await log(` šŸ”Ž Search query: ${searchQuery}`, { verbose: true });
468
464
  await log(` šŸ”Ž Command: ${searchCmd}`, { verbose: true });
469
465
 
@@ -476,7 +472,7 @@ async function fetchPullRequests() {
476
472
  }
477
473
  }
478
474
  }
479
-
475
+
480
476
  if (prs.length === 0) {
481
477
  if (argv.allPrs) {
482
478
  await log(' ā„¹ļø No open pull requests found');
@@ -485,13 +481,13 @@ async function fetchPullRequests() {
485
481
  }
486
482
  return [];
487
483
  }
488
-
484
+
489
485
  if (argv.allPrs) {
490
486
  await log(` šŸ“‹ Found ${prs.length} open pull request(s)`);
491
487
  } else {
492
488
  await log(` šŸ“‹ Found ${prs.length} pull request(s) with label "${argv.reviewLabel}"`);
493
489
  }
494
-
490
+
495
491
  // Filter out draft PRs if option is enabled
496
492
  if (argv.skipDraft) {
497
493
  const nonDraftPrs = prs.filter(pr => !pr.isDraft);
@@ -501,19 +497,19 @@ async function fetchPullRequests() {
501
497
  }
502
498
  prs = nonDraftPrs;
503
499
  }
504
-
500
+
505
501
  // Apply max PRs limit if set
506
502
  let prsToProcess = prs;
507
503
  if (argv.maxPrs > 0 && prs.length > argv.maxPrs) {
508
504
  prsToProcess = prs.slice(0, argv.maxPrs);
509
505
  await log(` šŸ”¢ Limiting to first ${argv.maxPrs} PRs`);
510
506
  }
511
-
507
+
512
508
  // Filter out PRs with approvals if option is enabled
513
509
  if (argv.skipApproved) {
514
510
  await log(' šŸ” Checking for existing approvals...');
515
511
  const filteredPrs = [];
516
-
512
+
517
513
  for (const pr of prsToProcess) {
518
514
  const hasApproval = await hasApprovals(pr.url);
519
515
  if (hasApproval) {
@@ -522,14 +518,14 @@ async function fetchPullRequests() {
522
518
  filteredPrs.push(pr);
523
519
  }
524
520
  }
525
-
521
+
526
522
  const skippedCount = prsToProcess.length - filteredPrs.length;
527
523
  if (skippedCount > 0) {
528
524
  await log(` ā­ļø Skipped ${skippedCount} PR(s) with existing approvals`);
529
525
  }
530
526
  prsToProcess = filteredPrs;
531
527
  }
532
-
528
+
533
529
  // In dry-run mode, show the PRs that would be reviewed
534
530
  if (argv.dryRun && prsToProcess.length > 0) {
535
531
  await log('\n šŸ“ PRs that would be reviewed:');
@@ -537,9 +533,8 @@ async function fetchPullRequests() {
537
533
  await log(` - ${pr.title || 'Untitled'} (${pr.url})`);
538
534
  }
539
535
  }
540
-
536
+
541
537
  return prsToProcess.map(pr => pr.url);
542
-
543
538
  } catch (error) {
544
539
  await log(` āŒ Error fetching pull requests: ${error.message}`, { level: 'error' });
545
540
  return [];
@@ -549,22 +544,22 @@ async function fetchPullRequests() {
549
544
  // Main monitoring loop
550
545
  async function monitor() {
551
546
  await log('\nšŸš€ Starting Reviewers Hive Mind monitoring system...');
552
-
547
+
553
548
  // Start reviewers
554
549
  await log(`\nšŸ‘€ Starting ${argv.concurrency} reviewers...`);
555
550
  for (let i = 1; i <= argv.concurrency; i++) {
556
551
  prQueue.workers.push(reviewer(i));
557
552
  }
558
-
553
+
559
554
  // Main monitoring loop
560
555
  let iteration = 0;
561
556
  while (true) {
562
557
  iteration++;
563
558
  await log(`\nšŸ”„ Monitoring iteration ${iteration} at ${new Date().toISOString()}`);
564
-
559
+
565
560
  // Fetch PRs
566
561
  const prUrls = await fetchPullRequests();
567
-
562
+
568
563
  // Add new PRs to queue
569
564
  let newPrs = 0;
570
565
  for (const url of prUrls) {
@@ -573,13 +568,13 @@ async function monitor() {
573
568
  await log(` āž• Added to review queue: ${url}`);
574
569
  }
575
570
  }
576
-
571
+
577
572
  if (newPrs > 0) {
578
573
  await log(` šŸ“„ Added ${newPrs} new PR(s) to review queue`);
579
574
  } else {
580
575
  await log(' ā„¹ļø No new PRs to add (all already reviewed or in queue)');
581
576
  }
582
-
577
+
583
578
  // Show current stats
584
579
  const stats = prQueue.getStats();
585
580
  await log('\nšŸ“Š Current Status:');
@@ -587,11 +582,11 @@ async function monitor() {
587
582
  await log(` āš™ļø Reviewing: ${stats.processing}`);
588
583
  await log(` āœ… Completed: ${stats.completed}`);
589
584
  await log(` āŒ Failed: ${stats.failed}`);
590
-
585
+
591
586
  // If running once, wait for queue to empty then exit
592
587
  if (argv.once) {
593
588
  await log('\nšŸ Single run mode - waiting for review queue to empty...');
594
-
589
+
595
590
  while (stats.queued > 0 || stats.processing > 0) {
596
591
  await new Promise(resolve => setTimeout(resolve, 5000));
597
592
  const currentStats = prQueue.getStats();
@@ -600,22 +595,22 @@ async function monitor() {
600
595
  }
601
596
  Object.assign(stats, currentStats);
602
597
  }
603
-
598
+
604
599
  await log('\nāœ… All PRs reviewed!');
605
600
  await log(` Completed: ${stats.completed}`);
606
601
  await log(` Failed: ${stats.failed}`);
607
602
  break;
608
603
  }
609
-
604
+
610
605
  // Wait for next iteration
611
606
  await log(`\nā° Next check in ${argv.interval} seconds...`);
612
607
  await new Promise(resolve => setTimeout(resolve, argv.interval * 1000));
613
608
  }
614
-
609
+
615
610
  // Stop reviewers
616
611
  prQueue.stop();
617
612
  await Promise.all(prQueue.workers);
618
-
613
+
619
614
  await log('\nšŸ‘‹ Reviewers Hive Mind monitoring stopped');
620
615
  }
621
616
 
@@ -640,4 +635,4 @@ try {
640
635
  } catch (error) {
641
636
  await log(`\nāŒ Fatal error: ${error.message}`, { level: 'error' });
642
637
  process.exit(1);
643
- }
638
+ }
@@ -117,12 +117,15 @@ export const withSpan = async (name, callback) => {
117
117
  return callback();
118
118
  }
119
119
 
120
- return sentry.startSpan({
121
- name,
122
- op: 'function',
123
- }, async () => {
124
- return callback();
125
- });
120
+ return sentry.startSpan(
121
+ {
122
+ name,
123
+ op: 'function',
124
+ },
125
+ async () => {
126
+ return callback();
127
+ }
128
+ );
126
129
  };
127
130
 
128
131
  /**
@@ -173,7 +176,7 @@ export const reportWarning = (warning, context = {}) => {
173
176
  * Add breadcrumb for better error context
174
177
  * @param {Object} breadcrumb - Breadcrumb data
175
178
  */
176
- export const addBreadcrumb = async (breadcrumb) => {
179
+ export const addBreadcrumb = async breadcrumb => {
177
180
  if (!isSentryEnabled() || sentryDisabled) {
178
181
  return;
179
182
  }
@@ -188,7 +191,7 @@ export const addBreadcrumb = async (breadcrumb) => {
188
191
  * Set user context for Sentry
189
192
  * @param {Object} user - User data
190
193
  */
191
- export const setUserContext = async (user) => {
194
+ export const setUserContext = async user => {
192
195
  if (!isSentryEnabled() || sentryDisabled) {
193
196
  return;
194
197
  }
@@ -219,7 +222,7 @@ export const setExtraContext = async (key, value) => {
219
222
  * Set tags for Sentry
220
223
  * @param {Object} tags - Tags to set
221
224
  */
222
- export const setTags = async (tags) => {
225
+ export const setTags = async tags => {
223
226
  if (!isSentryEnabled() || sentryDisabled) {
224
227
  return;
225
228
  }
@@ -281,4 +284,4 @@ export const closeSentry = async (timeout = 2000) => {
281
284
  };
282
285
 
283
286
  // Export the Sentry check function
284
- export { isSentryEnabled };
287
+ export { isSentryEnabled };