@link-assistant/hive-mind 0.39.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 +20 -0
  2. package/LICENSE +24 -0
  3. package/README.md +769 -0
  4. package/package.json +58 -0
  5. package/src/agent.lib.mjs +705 -0
  6. package/src/agent.prompts.lib.mjs +196 -0
  7. package/src/buildUserMention.lib.mjs +71 -0
  8. package/src/claude-limits.lib.mjs +389 -0
  9. package/src/claude.lib.mjs +1445 -0
  10. package/src/claude.prompts.lib.mjs +203 -0
  11. package/src/codex.lib.mjs +552 -0
  12. package/src/codex.prompts.lib.mjs +194 -0
  13. package/src/config.lib.mjs +207 -0
  14. package/src/contributing-guidelines.lib.mjs +268 -0
  15. package/src/exit-handler.lib.mjs +205 -0
  16. package/src/git.lib.mjs +145 -0
  17. package/src/github-issue-creator.lib.mjs +246 -0
  18. package/src/github-linking.lib.mjs +152 -0
  19. package/src/github.batch.lib.mjs +272 -0
  20. package/src/github.graphql.lib.mjs +258 -0
  21. package/src/github.lib.mjs +1479 -0
  22. package/src/hive.config.lib.mjs +254 -0
  23. package/src/hive.mjs +1500 -0
  24. package/src/instrument.mjs +191 -0
  25. package/src/interactive-mode.lib.mjs +1000 -0
  26. package/src/lenv-reader.lib.mjs +206 -0
  27. package/src/lib.mjs +490 -0
  28. package/src/lino.lib.mjs +176 -0
  29. package/src/local-ci-checks.lib.mjs +324 -0
  30. package/src/memory-check.mjs +419 -0
  31. package/src/model-mapping.lib.mjs +145 -0
  32. package/src/model-validation.lib.mjs +278 -0
  33. package/src/opencode.lib.mjs +479 -0
  34. package/src/opencode.prompts.lib.mjs +194 -0
  35. package/src/protect-branch.mjs +159 -0
  36. package/src/review.mjs +433 -0
  37. package/src/reviewers-hive.mjs +643 -0
  38. package/src/sentry.lib.mjs +284 -0
  39. package/src/solve.auto-continue.lib.mjs +568 -0
  40. package/src/solve.auto-pr.lib.mjs +1374 -0
  41. package/src/solve.branch-errors.lib.mjs +341 -0
  42. package/src/solve.branch.lib.mjs +230 -0
  43. package/src/solve.config.lib.mjs +342 -0
  44. package/src/solve.error-handlers.lib.mjs +256 -0
  45. package/src/solve.execution.lib.mjs +291 -0
  46. package/src/solve.feedback.lib.mjs +436 -0
  47. package/src/solve.mjs +1128 -0
  48. package/src/solve.preparation.lib.mjs +210 -0
  49. package/src/solve.repo-setup.lib.mjs +114 -0
  50. package/src/solve.repository.lib.mjs +961 -0
  51. package/src/solve.results.lib.mjs +558 -0
  52. package/src/solve.session.lib.mjs +135 -0
  53. package/src/solve.validation.lib.mjs +325 -0
  54. package/src/solve.watch.lib.mjs +572 -0
  55. package/src/start-screen.mjs +324 -0
  56. package/src/task.mjs +308 -0
  57. package/src/telegram-bot.mjs +1481 -0
  58. package/src/telegram-markdown.lib.mjs +64 -0
  59. package/src/usage-limit.lib.mjs +218 -0
  60. package/src/version.lib.mjs +41 -0
  61. package/src/youtrack/solve.youtrack.lib.mjs +116 -0
  62. package/src/youtrack/youtrack-sync.mjs +219 -0
  63. package/src/youtrack/youtrack.lib.mjs +425 -0
@@ -0,0 +1,643 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Use use-m to dynamically import modules for cross-runtime compatibility
4
+ const { use } = eval(await (await fetch('https://unpkg.com/use-m/use.js')).text());
5
+
6
+ // Use command-stream for consistent $ behavior across runtimes
7
+ const { $ } = await use('command-stream');
8
+
9
+ const yargs = (await use('yargs@latest')).default;
10
+ const path = (await use('path')).default;
11
+ const fs = (await use('fs')).promises;
12
+
13
+ // Global log file reference
14
+ let logFile = null;
15
+
16
+ // Helper function to log to both console and file
17
+ const log = async (message, options = {}) => {
18
+ const { level = 'info', verbose = false } = options;
19
+
20
+ // Skip verbose logs unless --verbose is enabled
21
+ if (verbose && !global.verboseMode) {
22
+ return;
23
+ }
24
+
25
+ // Write to file if log file is set
26
+ if (logFile) {
27
+ const logMessage = `[${new Date().toISOString()}] [${level.toUpperCase()}] ${message}`;
28
+ await fs.appendFile(logFile, logMessage + '\n').catch(() => {});
29
+ }
30
+
31
+ // Write to console based on level
32
+ switch (level) {
33
+ case 'error':
34
+ console.error(message);
35
+ break;
36
+ case 'warning':
37
+ case 'warn':
38
+ console.warn(message);
39
+ break;
40
+ case 'info':
41
+ default:
42
+ console.log(message);
43
+ break;
44
+ }
45
+ };
46
+
47
+ // Configure command line arguments
48
+ const argv = yargs(process.argv.slice(2))
49
+ .usage('Usage: $0 <github-url> [options]')
50
+ .positional('github-url', {
51
+ type: 'string',
52
+ description: 'GitHub organization, repository, or user URL to monitor for pull requests'
53
+ })
54
+ .option('review-label', {
55
+ type: 'string',
56
+ description: 'GitHub label to identify PRs needing review',
57
+ default: 'needs-review',
58
+ alias: 'l'
59
+ })
60
+ .option('all-prs', {
61
+ type: 'boolean',
62
+ description: 'Review all open pull requests regardless of labels',
63
+ default: false,
64
+ alias: 'a'
65
+ })
66
+ .option('skip-draft', {
67
+ type: 'boolean',
68
+ description: 'Skip draft pull requests',
69
+ default: true,
70
+ alias: 'd'
71
+ })
72
+ .option('skip-approved', {
73
+ type: 'boolean',
74
+ description: 'Skip pull requests that already have approvals',
75
+ default: true
76
+ })
77
+ .option('concurrency', {
78
+ type: 'number',
79
+ description: 'Number of concurrent review.mjs instances',
80
+ default: 2,
81
+ alias: 'c'
82
+ })
83
+ .option('reviews-per-pr', {
84
+ type: 'number',
85
+ description: 'Number of reviews to generate per PR (for diverse perspectives)',
86
+ default: 1,
87
+ alias: 'r'
88
+ })
89
+ .option('model', {
90
+ type: 'string',
91
+ description: 'Model to use for review.mjs (opus or sonnet)',
92
+ alias: 'm',
93
+ default: 'opus',
94
+ choices: ['opus', 'sonnet']
95
+ })
96
+ .option('focus', {
97
+ type: 'string',
98
+ description: 'Focus areas for reviews (security, performance, logic, style, tests, all)',
99
+ default: 'all',
100
+ alias: 'f'
101
+ })
102
+ .option('auto-approve', {
103
+ type: 'boolean',
104
+ description: 'Auto-approve PRs that pass review criteria',
105
+ default: false
106
+ })
107
+ .option('interval', {
108
+ type: 'number',
109
+ description: 'Polling interval in seconds',
110
+ default: 300, // 5 minutes
111
+ alias: 'i'
112
+ })
113
+ .option('max-prs', {
114
+ type: 'number',
115
+ description: 'Maximum number of PRs to process (0 = unlimited)',
116
+ default: 0
117
+ })
118
+ .option('dry-run', {
119
+ type: 'boolean',
120
+ description: 'List PRs that would be reviewed without actually reviewing them',
121
+ default: false
122
+ })
123
+ .option('verbose', {
124
+ type: 'boolean',
125
+ description: 'Enable verbose logging',
126
+ alias: 'v',
127
+ default: false
128
+ })
129
+ .option('once', {
130
+ type: 'boolean',
131
+ description: 'Run once and exit instead of continuous monitoring',
132
+ default: false
133
+ })
134
+ .demandCommand(1, 'GitHub URL is required')
135
+ .help('h')
136
+ .alias('h', 'help')
137
+ .argv;
138
+
139
+ const githubUrl = argv['github-url'] || argv._[0];
140
+
141
+ // Set global verbose mode
142
+ global.verboseMode = argv.verbose;
143
+
144
+ // Create log file with timestamp
145
+ const scriptDir = path.dirname(process.argv[1]);
146
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
147
+ logFile = path.join(scriptDir, `reviewers-hive-${timestamp}.log`);
148
+
149
+ // Create the log file immediately
150
+ await fs.writeFile(logFile, `# Reviewers-Hive.mjs Log - ${new Date().toISOString()}\n\n`);
151
+ await log(`šŸ“ Log file: ${logFile}`);
152
+ await log(' (All output will be logged here)\n');
153
+
154
+ // Parse GitHub URL to determine organization, repository, or user
155
+ let scope = 'repository';
156
+ let owner = null;
157
+ let repo = null;
158
+
159
+ // Parse URL format: https://github.com/owner or https://github.com/owner/repo
160
+ const urlMatch = githubUrl.match(/^https:\/\/github\.com\/([^/]+)(\/([^/]+))?$/);
161
+ if (!urlMatch) {
162
+ await log('Error: Invalid GitHub URL format', { level: 'error' });
163
+ await log('Expected: https://github.com/owner or https://github.com/owner/repo', { level: 'error' });
164
+ process.exit(1);
165
+ }
166
+
167
+ owner = urlMatch[1];
168
+ repo = urlMatch[3] || null;
169
+
170
+ // Determine scope
171
+ if (!repo) {
172
+ // Check if it's an organization or user
173
+ try {
174
+ const typeResult = await $`gh api users/${owner} --jq .type`;
175
+ const accountType = typeResult.stdout.toString().trim();
176
+ scope = accountType === 'Organization' ? 'organization' : 'user';
177
+ } catch {
178
+ // Default to user if API call fails
179
+ scope = 'user';
180
+ }
181
+ } else {
182
+ scope = 'repository';
183
+ }
184
+
185
+ await log('šŸŽÆ PR Review Monitoring Configuration:');
186
+ await log(` šŸ“ Target: ${scope.charAt(0).toUpperCase() + scope.slice(1)} - ${owner}${repo ? `/${repo}` : ''}`);
187
+ if (argv.allPrs) {
188
+ await log(' šŸ·ļø Mode: ALL PULL REQUESTS (no label filter)');
189
+ } else {
190
+ await log(` šŸ·ļø Label: "${argv.reviewLabel}"`);
191
+ }
192
+ if (argv.skipDraft) {
193
+ await log(' 🚫 Skipping: Draft PRs');
194
+ }
195
+ if (argv.skipApproved) {
196
+ await log(' 🚫 Skipping: Already approved PRs');
197
+ }
198
+ await log(` šŸ”„ Concurrency: ${argv.concurrency} parallel reviewers`);
199
+ await log(` šŸ“Š Reviews per PR: ${argv.reviewsPerPr}`);
200
+ await log(` šŸ¤– Model: ${argv.model}`);
201
+ await log(` šŸŽÆ Focus: ${argv.focus}`);
202
+ if (argv.autoApprove) {
203
+ await log(' āœ… Auto-approve: Enabled');
204
+ }
205
+ if (!argv.once) {
206
+ await log(` ā±ļø Polling Interval: ${argv.interval} seconds`);
207
+ }
208
+ await log(` ${argv.once ? 'šŸš€ Mode: Single run' : 'ā™¾ļø Mode: Continuous monitoring'}`);
209
+ if (argv.maxPrs > 0) {
210
+ await log(` šŸ”¢ Max PRs: ${argv.maxPrs}`);
211
+ }
212
+ if (argv.dryRun) {
213
+ await log(' 🧪 DRY RUN MODE - No actual reviewing');
214
+ }
215
+ await log('');
216
+
217
+ // Producer/Consumer Queue implementation for PRs
218
+ class PRQueue {
219
+ constructor() {
220
+ this.queue = [];
221
+ this.processing = new Set();
222
+ this.completed = new Set();
223
+ this.failed = new Set();
224
+ this.workers = [];
225
+ this.isRunning = true;
226
+ }
227
+
228
+ // Add PR to queue if not already processed or in queue
229
+ enqueue(prUrl) {
230
+ if (this.completed.has(prUrl) ||
231
+ this.processing.has(prUrl) ||
232
+ this.queue.includes(prUrl)) {
233
+ return false;
234
+ }
235
+ this.queue.push(prUrl);
236
+ return true;
237
+ }
238
+
239
+ // Get next PR from queue
240
+ dequeue() {
241
+ if (this.queue.length === 0) {
242
+ return null;
243
+ }
244
+ const pr = this.queue.shift();
245
+ this.processing.add(pr);
246
+ return pr;
247
+ }
248
+
249
+ // Mark PR as completed
250
+ markCompleted(prUrl) {
251
+ this.processing.delete(prUrl);
252
+ this.completed.add(prUrl);
253
+ }
254
+
255
+ // Mark PR as failed
256
+ markFailed(prUrl) {
257
+ this.processing.delete(prUrl);
258
+ this.failed.add(prUrl);
259
+ }
260
+
261
+ // Get queue statistics
262
+ getStats() {
263
+ return {
264
+ queued: this.queue.length,
265
+ processing: this.processing.size,
266
+ completed: this.completed.size,
267
+ failed: this.failed.size
268
+ };
269
+ }
270
+
271
+ // Stop all workers
272
+ stop() {
273
+ this.isRunning = false;
274
+ }
275
+ }
276
+
277
+ // Create global queue instance
278
+ const prQueue = new PRQueue();
279
+
280
+ // Worker function to review PRs from queue
281
+ async function reviewer(reviewerId) {
282
+ await log(`šŸ” Reviewer ${reviewerId} started`, { verbose: true });
283
+
284
+ while (prQueue.isRunning) {
285
+ const prUrl = prQueue.dequeue();
286
+
287
+ if (!prUrl) {
288
+ // No work available, wait a bit
289
+ await new Promise(resolve => setTimeout(resolve, 5000));
290
+ continue;
291
+ }
292
+
293
+ await log(`\nšŸ‘€ Reviewer ${reviewerId} reviewing: ${prUrl}`);
294
+
295
+ // Review the PR multiple times if needed (for diverse perspectives)
296
+ for (let reviewNum = 1; reviewNum <= argv.reviewsPerPr; reviewNum++) {
297
+ if (argv.reviewsPerPr > 1) {
298
+ await log(` šŸ“ Creating review ${reviewNum}/${argv.reviewsPerPr} for PR`);
299
+ }
300
+
301
+ try {
302
+ if (argv.dryRun) {
303
+ await log(` 🧪 [DRY RUN] Would execute: ./review.mjs "${prUrl}" --model ${argv.model} --focus ${argv.focus}${argv.autoApprove ? ' --approve' : ''}`);
304
+ await new Promise(resolve => setTimeout(resolve, 2000)); // Simulate work
305
+ } else {
306
+ // Execute review.mjs using command-stream
307
+ await log(` šŸš€ Executing review.mjs for ${prUrl}...`);
308
+
309
+ const startTime = Date.now();
310
+ let reviewCommand = $`./review.mjs "${prUrl}" --model ${argv.model} --focus ${argv.focus}`;
311
+
312
+ if (argv.autoApprove) {
313
+ reviewCommand = $`./review.mjs "${prUrl}" --model ${argv.model} --focus ${argv.focus} --approve`;
314
+ }
315
+
316
+ // Stream output and capture result
317
+ let exitCode = 0;
318
+ for await (const chunk of reviewCommand.stream()) {
319
+ if (chunk.type === 'stdout') {
320
+ const output = chunk.data.toString().trim();
321
+ if (output) {
322
+ await log(` [review.mjs] ${output}`, { verbose: true });
323
+ }
324
+ } else if (chunk.type === 'stderr') {
325
+ const error = chunk.data.toString().trim();
326
+ if (error) {
327
+ await log(` [review.mjs ERROR] ${error}`, { level: 'error', verbose: true });
328
+ }
329
+ } else if (chunk.type === 'exit') {
330
+ exitCode = chunk.code;
331
+ }
332
+ }
333
+
334
+ const duration = Math.round((Date.now() - startTime) / 1000);
335
+
336
+ if (exitCode === 0) {
337
+ await log(` āœ… Reviewer ${reviewerId} completed ${prUrl} (${duration}s)`);
338
+ } else {
339
+ throw new Error(`review.mjs exited with code ${exitCode}`);
340
+ }
341
+ }
342
+
343
+ // Small delay between multiple reviews for same PR
344
+ if (reviewNum < argv.reviewsPerPr) {
345
+ await new Promise(resolve => setTimeout(resolve, 10000));
346
+ }
347
+ } catch (error) {
348
+ await log(` āŒ Reviewer ${reviewerId} failed on ${prUrl}: ${error.message}`, { level: 'error' });
349
+ prQueue.markFailed(prUrl);
350
+ break; // Stop trying more reviews for this PR
351
+ }
352
+ }
353
+
354
+ prQueue.markCompleted(prUrl);
355
+
356
+ // Show queue stats
357
+ const stats = prQueue.getStats();
358
+ await log(` šŸ“Š Queue: ${stats.queued} waiting, ${stats.processing} reviewing, ${stats.completed} completed, ${stats.failed} failed`);
359
+ }
360
+
361
+ await log(`šŸ” Reviewer ${reviewerId} stopped`, { verbose: true });
362
+ }
363
+
364
+ // Function to check if a PR already has approvals
365
+ async function hasApprovals(prUrl) {
366
+ try {
367
+ const { exec } = await import('child_process');
368
+ const { promisify } = await import('util');
369
+ const execAsync = promisify(exec);
370
+
371
+ // Extract owner, repo, and PR number from URL
372
+ const urlMatch = prUrl.match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
373
+ if (!urlMatch) return false;
374
+
375
+ const [, prOwner, prRepo, prNumber] = urlMatch;
376
+
377
+ // Check for reviews using GitHub API
378
+ const cmd = `gh api repos/${prOwner}/${prRepo}/pulls/${prNumber}/reviews --jq '[.[] | select(.state == "APPROVED")] | length'`;
379
+
380
+ const { stdout } = await execAsync(cmd, { encoding: 'utf8', env: process.env });
381
+ const approvalCount = parseInt(stdout.trim()) || 0;
382
+
383
+ if (approvalCount > 0) {
384
+ await log(` ↳ Skipping (has ${approvalCount} approval${approvalCount > 1 ? 's' : ''})`, { verbose: true });
385
+ return true;
386
+ }
387
+
388
+ return false;
389
+ } catch (error) {
390
+ // If we can't check, assume no approvals
391
+ await log(` ↳ Could not check for approvals: ${error.message.split('\n')[0]}`, { verbose: true });
392
+ return false;
393
+ }
394
+ }
395
+
396
+ // Function to fetch pull requests from GitHub
397
+ async function fetchPullRequests() {
398
+ if (argv.allPrs) {
399
+ await log('\nšŸ” Fetching ALL open pull requests...');
400
+ } else {
401
+ await log(`\nšŸ” Fetching pull requests with label "${argv.reviewLabel}"...`);
402
+ }
403
+
404
+ try {
405
+ let prs = [];
406
+
407
+ if (argv.allPrs) {
408
+ // Fetch all open PRs without label filter
409
+ let searchCmd;
410
+ if (scope === 'repository') {
411
+ searchCmd = `gh pr list --repo ${owner}/${repo} --state open --limit 100 --json url,title,number,isDraft`;
412
+ } else if (scope === 'organization') {
413
+ searchCmd = `gh search prs org:${owner} is:open --limit 100 --json url,title,number,repository,isDraft`;
414
+ } else {
415
+ // User scope
416
+ searchCmd = `gh search prs user:${owner} is:open --limit 100 --json url,title,number,repository,isDraft`;
417
+ }
418
+
419
+ await log(` šŸ”Ž Command: ${searchCmd}`, { verbose: true });
420
+
421
+ // Use async exec to avoid escaping issues
422
+ const { exec } = await import('child_process');
423
+ const { promisify } = await import('util');
424
+ const execAsync = promisify(exec);
425
+ const { stdout } = await execAsync(searchCmd, { encoding: 'utf8', env: process.env });
426
+ prs = JSON.parse(stdout || '[]');
427
+
428
+ } else {
429
+ // Use label filter
430
+ const { exec } = await import('child_process');
431
+ const { promisify } = await import('util');
432
+ const execAsync = promisify(exec);
433
+
434
+ // For repositories, use gh pr list which works better
435
+ if (scope === 'repository') {
436
+ const listCmd = `gh pr list --repo ${owner}/${repo} --state open --label "${argv.reviewLabel}" --limit 100 --json url,title,number,isDraft`;
437
+ await log(` šŸ”Ž Command: ${listCmd}`, { verbose: true });
438
+
439
+ try {
440
+ const { stdout } = await execAsync(listCmd, { encoding: 'utf8', env: process.env });
441
+ prs = JSON.parse(stdout || '[]');
442
+ } catch (listError) {
443
+ await log(` āš ļø List failed: ${listError.message.split('\n')[0]}`, { verbose: true });
444
+ prs = [];
445
+ }
446
+ } else {
447
+ // For organizations and users, use search
448
+ let baseQuery;
449
+ if (scope === 'organization') {
450
+ baseQuery = `org:${owner} is:pr is:open`;
451
+ } else {
452
+ baseQuery = `user:${owner} is:pr is:open`;
453
+ }
454
+
455
+ // Handle label with potential spaces
456
+ let searchQuery;
457
+ let searchCmd;
458
+
459
+ if (argv.reviewLabel.includes(' ')) {
460
+ searchQuery = `${baseQuery} label:"${argv.reviewLabel}"`;
461
+ searchCmd = `gh search prs '${searchQuery}' --limit 100 --json url,title,number,repository,isDraft`;
462
+ } else {
463
+ searchQuery = `${baseQuery} label:${argv.reviewLabel}`;
464
+ searchCmd = `gh search prs '${searchQuery}' --limit 100 --json url,title,number,repository,isDraft`;
465
+ }
466
+
467
+ await log(` šŸ”Ž Search query: ${searchQuery}`, { verbose: true });
468
+ await log(` šŸ”Ž Command: ${searchCmd}`, { verbose: true });
469
+
470
+ try {
471
+ const { stdout } = await execAsync(searchCmd, { encoding: 'utf8', env: process.env });
472
+ prs = JSON.parse(stdout || '[]');
473
+ } catch (searchError) {
474
+ await log(` āš ļø Search failed: ${searchError.message.split('\n')[0]}`, { verbose: true });
475
+ prs = [];
476
+ }
477
+ }
478
+ }
479
+
480
+ if (prs.length === 0) {
481
+ if (argv.allPrs) {
482
+ await log(' ā„¹ļø No open pull requests found');
483
+ } else {
484
+ await log(` ā„¹ļø No pull requests found with label "${argv.reviewLabel}"`);
485
+ }
486
+ return [];
487
+ }
488
+
489
+ if (argv.allPrs) {
490
+ await log(` šŸ“‹ Found ${prs.length} open pull request(s)`);
491
+ } else {
492
+ await log(` šŸ“‹ Found ${prs.length} pull request(s) with label "${argv.reviewLabel}"`);
493
+ }
494
+
495
+ // Filter out draft PRs if option is enabled
496
+ if (argv.skipDraft) {
497
+ const nonDraftPrs = prs.filter(pr => !pr.isDraft);
498
+ const draftCount = prs.length - nonDraftPrs.length;
499
+ if (draftCount > 0) {
500
+ await log(` ā­ļø Filtered out ${draftCount} draft PR(s)`);
501
+ }
502
+ prs = nonDraftPrs;
503
+ }
504
+
505
+ // Apply max PRs limit if set
506
+ let prsToProcess = prs;
507
+ if (argv.maxPrs > 0 && prs.length > argv.maxPrs) {
508
+ prsToProcess = prs.slice(0, argv.maxPrs);
509
+ await log(` šŸ”¢ Limiting to first ${argv.maxPrs} PRs`);
510
+ }
511
+
512
+ // Filter out PRs with approvals if option is enabled
513
+ if (argv.skipApproved) {
514
+ await log(' šŸ” Checking for existing approvals...');
515
+ const filteredPrs = [];
516
+
517
+ for (const pr of prsToProcess) {
518
+ const hasApproval = await hasApprovals(pr.url);
519
+ if (hasApproval) {
520
+ await log(` ā­ļø Skipping (approved): ${pr.title || 'Untitled'} (${pr.url})`, { verbose: true });
521
+ } else {
522
+ filteredPrs.push(pr);
523
+ }
524
+ }
525
+
526
+ const skippedCount = prsToProcess.length - filteredPrs.length;
527
+ if (skippedCount > 0) {
528
+ await log(` ā­ļø Skipped ${skippedCount} PR(s) with existing approvals`);
529
+ }
530
+ prsToProcess = filteredPrs;
531
+ }
532
+
533
+ // In dry-run mode, show the PRs that would be reviewed
534
+ if (argv.dryRun && prsToProcess.length > 0) {
535
+ await log('\n šŸ“ PRs that would be reviewed:');
536
+ for (const pr of prsToProcess) {
537
+ await log(` - ${pr.title || 'Untitled'} (${pr.url})`);
538
+ }
539
+ }
540
+
541
+ return prsToProcess.map(pr => pr.url);
542
+
543
+ } catch (error) {
544
+ await log(` āŒ Error fetching pull requests: ${error.message}`, { level: 'error' });
545
+ return [];
546
+ }
547
+ }
548
+
549
+ // Main monitoring loop
550
+ async function monitor() {
551
+ await log('\nšŸš€ Starting Reviewers Hive Mind monitoring system...');
552
+
553
+ // Start reviewers
554
+ await log(`\nšŸ‘€ Starting ${argv.concurrency} reviewers...`);
555
+ for (let i = 1; i <= argv.concurrency; i++) {
556
+ prQueue.workers.push(reviewer(i));
557
+ }
558
+
559
+ // Main monitoring loop
560
+ let iteration = 0;
561
+ while (true) {
562
+ iteration++;
563
+ await log(`\nšŸ”„ Monitoring iteration ${iteration} at ${new Date().toISOString()}`);
564
+
565
+ // Fetch PRs
566
+ const prUrls = await fetchPullRequests();
567
+
568
+ // Add new PRs to queue
569
+ let newPrs = 0;
570
+ for (const url of prUrls) {
571
+ if (prQueue.enqueue(url)) {
572
+ newPrs++;
573
+ await log(` āž• Added to review queue: ${url}`);
574
+ }
575
+ }
576
+
577
+ if (newPrs > 0) {
578
+ await log(` šŸ“„ Added ${newPrs} new PR(s) to review queue`);
579
+ } else {
580
+ await log(' ā„¹ļø No new PRs to add (all already reviewed or in queue)');
581
+ }
582
+
583
+ // Show current stats
584
+ const stats = prQueue.getStats();
585
+ await log('\nšŸ“Š Current Status:');
586
+ await log(` šŸ“‹ Queued: ${stats.queued}`);
587
+ await log(` āš™ļø Reviewing: ${stats.processing}`);
588
+ await log(` āœ… Completed: ${stats.completed}`);
589
+ await log(` āŒ Failed: ${stats.failed}`);
590
+
591
+ // If running once, wait for queue to empty then exit
592
+ if (argv.once) {
593
+ await log('\nšŸ Single run mode - waiting for review queue to empty...');
594
+
595
+ while (stats.queued > 0 || stats.processing > 0) {
596
+ await new Promise(resolve => setTimeout(resolve, 5000));
597
+ const currentStats = prQueue.getStats();
598
+ if (currentStats.queued !== stats.queued || currentStats.processing !== stats.processing) {
599
+ await log(` ā³ Waiting... Queue: ${currentStats.queued}, Reviewing: ${currentStats.processing}`);
600
+ }
601
+ Object.assign(stats, currentStats);
602
+ }
603
+
604
+ await log('\nāœ… All PRs reviewed!');
605
+ await log(` Completed: ${stats.completed}`);
606
+ await log(` Failed: ${stats.failed}`);
607
+ break;
608
+ }
609
+
610
+ // Wait for next iteration
611
+ await log(`\nā° Next check in ${argv.interval} seconds...`);
612
+ await new Promise(resolve => setTimeout(resolve, argv.interval * 1000));
613
+ }
614
+
615
+ // Stop reviewers
616
+ prQueue.stop();
617
+ await Promise.all(prQueue.workers);
618
+
619
+ await log('\nšŸ‘‹ Reviewers Hive Mind monitoring stopped');
620
+ }
621
+
622
+ // Handle graceful shutdown
623
+ process.on('SIGINT', async () => {
624
+ await log('\n\nšŸ›‘ Received interrupt signal, shutting down gracefully...');
625
+ prQueue.stop();
626
+ await Promise.all(prQueue.workers);
627
+ process.exit(0);
628
+ });
629
+
630
+ process.on('SIGTERM', async () => {
631
+ await log('\n\nšŸ›‘ Received termination signal, shutting down gracefully...');
632
+ prQueue.stop();
633
+ await Promise.all(prQueue.workers);
634
+ process.exit(0);
635
+ });
636
+
637
+ // Start monitoring
638
+ try {
639
+ await monitor();
640
+ } catch (error) {
641
+ await log(`\nāŒ Fatal error: ${error.message}`, { level: 'error' });
642
+ process.exit(1);
643
+ }