@link-assistant/hive-mind 2.12.0 → 2.12.2

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.
package/src/hive.mjs CHANGED
@@ -133,7 +133,6 @@ if (isRunningDirectly) {
133
133
  }
134
134
  await log(' šŸ“‹ Fetching repository list (using --paginate for unlimited pagination)...', { verbose: true });
135
135
  await log(` šŸ”Ž Command: ${repoListCmd}`, { verbose: true });
136
-
137
136
  // Add delay for rate limiting
138
137
  await new Promise(resolve => setTimeout(resolve, 2000));
139
138
  // #1756: route through execGhWithRetry for transient 5xx + rate-limit
@@ -148,7 +147,6 @@ if (isRunningDirectly) {
148
147
  .filter(line => line.trim());
149
148
  const allRepositories = repoLines.map(line => JSON.parse(line));
150
149
  await log(` šŸ“Š Found ${allRepositories.length} repositories`);
151
-
152
150
  // Filter repositories to only include those owned by the target user/org
153
151
  const ownedRepositories = allRepositories.filter(repo => {
154
152
  const repoOwner = repo.owner?.login || repo.owner;
@@ -165,7 +163,6 @@ if (isRunningDirectly) {
165
163
  await log(` ā­ļø Skipping ${archivedCount} archived repository(ies)`);
166
164
  }
167
165
  await log(` āœ… Processing ${repositories.length} non-archived repositories owned by ${owner}`);
168
-
169
166
  let collectedIssues = [];
170
167
  let processedRepos = 0;
171
168
  // Process repositories in batches to avoid overwhelming the API
@@ -174,7 +171,6 @@ if (isRunningDirectly) {
174
171
  const repoName = repo.name;
175
172
  const ownerName = repo.owner?.login || owner;
176
173
  await log(` šŸ” Fetching issues from ${ownerName}/${repoName}...`, { verbose: true });
177
-
178
174
  // Build the appropriate issue list command
179
175
  let issueCmd;
180
176
  if (fetchAllIssues) {
@@ -210,7 +206,6 @@ if (isRunningDirectly) {
210
206
  // Continue with other repositories
211
207
  }
212
208
  }
213
-
214
209
  await log(` āœ… Repository fallback complete: ${collectedIssues.length} issues from ${processedRepos}/${repositories.length} repositories`);
215
210
  return collectedIssues;
216
211
  } catch (error) {
@@ -224,14 +219,12 @@ if (isRunningDirectly) {
224
219
  return [];
225
220
  }
226
221
  }
227
-
228
222
  // Configure command line arguments - GitHub URL as positional argument
229
223
  const rawArgs = normalizeCliArgs(hideBin(process.argv));
230
224
  // Use .parse() instead of .argv to ensure .strict() mode works correctly
231
225
  // When you use .argv, strict mode doesn't trigger properly
232
226
  // See: https://github.com/yargs/yargs/issues - .strict() only works with .parse()
233
227
  let argv;
234
-
235
228
  // Temporarily suppress stderr to prevent yargs from printing error messages
236
229
  // We'll handle error reporting ourselves
237
230
  const originalStderrWrite = process.stderr.write;
@@ -246,7 +239,6 @@ if (isRunningDirectly) {
246
239
  }
247
240
  return true;
248
241
  };
249
-
250
242
  try {
251
243
  argv = parseCliArgumentsWithLino({
252
244
  argv: ['node', 'hive', ...rawArgs],
@@ -259,14 +251,12 @@ if (isRunningDirectly) {
259
251
  } catch (error) {
260
252
  // Restore stderr before handling the error
261
253
  process.stderr.write = originalStderrWrite;
262
-
263
254
  // If .strict() mode catches an unknown argument, yargs will throw an error
264
255
  // We should fail fast for truly invalid arguments
265
256
  if (error.message && error.message.includes('Unknown argument')) {
266
257
  console.error('Error:', error.message);
267
258
  process.exit(1);
268
259
  }
269
-
270
260
  // Yargs sometimes throws "Not enough arguments" errors even when arguments are present
271
261
  // This is a quirk with optional positional arguments [github-url]
272
262
  // The error.argv object still contains the parsed arguments, so we can safely continue
@@ -279,27 +269,20 @@ if (isRunningDirectly) {
279
269
  }
280
270
  throw error;
281
271
  }
282
-
283
272
  // Normalize deprecated flags to new names
284
273
  if (argv && (argv.skipToolCheck || argv.skipClaudeCheck)) argv.skipToolConnectionCheck = true;
285
274
  if (argv && argv.toolCheck === false) argv.toolConnectionCheck = false;
286
275
  }
287
-
288
276
  let githubUrl = argv['github-url'];
289
-
290
277
  // Set global verbose mode
291
278
  global.verboseMode = argv.verbose;
292
-
293
279
  const { initI18n } = await import('./i18n.lib.mjs');
294
280
  await initI18n({ language: argv.language, uiLanguage: argv.uiLanguage, workLanguage: argv.workLanguage });
295
-
296
281
  setupVerboseLogInterceptor(); // Issue #1466: capture [VERBOSE] output in log files
297
282
  setupStdioLogInterceptor(); // Issue #1549: capture ALL terminal output in log file
298
-
299
283
  // Use the universal GitHub URL parser
300
284
  if (githubUrl) {
301
285
  const parsedUrl = parseGitHubUrl(githubUrl);
302
-
303
286
  if (!parsedUrl.valid) {
304
287
  console.error('Error: Invalid GitHub URL format');
305
288
  if (parsedUrl.error) console.error(` ${parsedUrl.error}`);
@@ -314,7 +297,6 @@ if (isRunningDirectly) {
314
297
  console.error(' - owner/repo (will be converted to https://github.com/owner/repo)');
315
298
  await safeExit(1, 'Error occurred');
316
299
  }
317
-
318
300
  // Check if it's a valid type for hive (user or repo)
319
301
  if (parsedUrl.type !== 'user' && parsedUrl.type !== 'repo') {
320
302
  console.error('Error: Invalid GitHub URL for monitoring');
@@ -322,18 +304,14 @@ if (isRunningDirectly) {
322
304
  console.error('Expected: https://github.com/owner or https://github.com/owner/repo');
323
305
  await safeExit(1, 'Error occurred');
324
306
  }
325
-
326
307
  // Use the normalized URL
327
308
  githubUrl = parsedUrl.normalized;
328
309
  }
329
-
330
310
  // Validate GitHub URL format ONCE AND FOR ALL at the beginning
331
311
  // Parse URL format: https://github.com/owner or https://github.com/owner/repo
332
312
  let urlMatch = null;
333
-
334
313
  // Only validate if we have a URL
335
314
  const needsUrlValidation = githubUrl;
336
-
337
315
  if (needsUrlValidation) {
338
316
  // Do the regex matching ONCE - this result will be used everywhere
339
317
  urlMatch = githubUrl.match(/^https:\/\/github\.com\/([^/]+)(\/([^/]+))?$/);
@@ -350,11 +328,9 @@ if (isRunningDirectly) {
350
328
  await safeExit(1, 'Error occurred');
351
329
  }
352
330
  }
353
-
354
331
  // Create log file with timestamp
355
332
  // Use log-dir option if provided, otherwise use current working directory
356
333
  let targetDir = argv.logDir || process.cwd();
357
-
358
334
  // Verify the directory exists, create if necessary
359
335
  try {
360
336
  await fs.access(targetDir);
@@ -379,20 +355,16 @@ if (isRunningDirectly) {
379
355
  targetDir = process.cwd();
380
356
  }
381
357
  }
382
-
383
358
  const timestamp = formatTimestamp();
384
359
  const logFile = path.join(targetDir, `hive-${timestamp}.log`);
385
-
386
360
  // Set the log file for the lib.mjs logging system
387
361
  setLogFile(logFile);
388
-
389
362
  // Create the log file immediately
390
363
  await fs.writeFile(logFile, `# Hive.mjs Log - ${new Date().toISOString()}\n\n`);
391
364
  // Always use absolute path for log file display
392
365
  const absoluteLogPath = path.resolve(logFile);
393
366
  await log(`šŸ“ Log file: ${absoluteLogPath}`);
394
367
  await log(' (All output will be logged here)');
395
-
396
368
  // Initialize Sentry integration (unless disabled)
397
369
  if (argv.sentry) {
398
370
  await initializeSentry({
@@ -400,7 +372,6 @@ if (isRunningDirectly) {
400
372
  debug: argv.verbose,
401
373
  version: process.env.npm_package_version || '0.12.0',
402
374
  });
403
-
404
375
  // Add breadcrumb for monitoring configuration
405
376
  addBreadcrumb({
406
377
  category: 'hive',
@@ -413,11 +384,9 @@ if (isRunningDirectly) {
413
384
  },
414
385
  });
415
386
  }
416
-
417
387
  // Initialize the exit handler with getAbsoluteLogPath function and Sentry cleanup
418
388
  initializeExitHandler(getAbsoluteLogPath, log);
419
389
  installGlobalExitHandlers();
420
-
421
390
  // Validate GitHub URL requirement
422
391
  if (!githubUrl) {
423
392
  await log('āŒ GitHub URL is required', { level: 'error' });
@@ -425,7 +394,6 @@ if (isRunningDirectly) {
425
394
  await log(` šŸ“ Full log file: ${absoluteLogPath}`, { level: 'error' });
426
395
  await safeExit(1, 'Error occurred');
427
396
  }
428
-
429
397
  // Validate project mode arguments
430
398
  if (argv.projectMode) {
431
399
  if (!argv.projectNumber) {
@@ -435,7 +403,6 @@ if (isRunningDirectly) {
435
403
  });
436
404
  await safeExit(1, 'Error occurred');
437
405
  }
438
-
439
406
  if (!argv.projectOwner) {
440
407
  await log('āŒ Project mode requires --project-owner', { level: 'error' });
441
408
  await log(' Usage: hive <github-url> --project-mode --project-number NUMBER --project-owner OWNER', {
@@ -443,24 +410,20 @@ if (isRunningDirectly) {
443
410
  });
444
411
  await safeExit(1, 'Error occurred');
445
412
  }
446
-
447
413
  if (typeof argv.projectNumber !== 'number' || argv.projectNumber <= 0) {
448
414
  await log('āŒ Project number must be a positive integer', { level: 'error' });
449
415
  await safeExit(1, 'Error occurred');
450
416
  }
451
417
  }
452
-
453
418
  // --plan flag expansion: shortcut for --plan-model opus --worker-model sonnet (Issue #1223)
454
419
  if (argv.plan) {
455
420
  if (!rawArgs.includes('--plan-model')) argv.planModel = 'opus';
456
421
  if (!rawArgs.includes('--model') && !rawArgs.includes('-m') && !rawArgs.includes('--worker-model')) argv.model = 'sonnet';
457
422
  }
458
-
459
423
  const modelExplicitlyProvided = rawArgs.includes('--model') || rawArgs.includes('-m') || rawArgs.includes('--worker-model');
460
424
  if (argv.tool && !modelExplicitlyProvided && defaultModels[argv.tool]) {
461
425
  argv.model = await resolveRuntimeDefaultModel(argv.tool);
462
426
  }
463
-
464
427
  // Validate model names EARLY (simple string check, always runs)
465
428
  const tool = argv.tool || 'claude';
466
429
  await validateAndExitOnInvalidModel(argv.model, tool, safeExit);
@@ -477,11 +440,9 @@ if (isRunningDirectly) {
477
440
  if (argv.subAgentModel) {
478
441
  await validateAndExitOnInvalidClaudeSubAgentModel(argv.subAgentModel, tool, safeExit);
479
442
  }
480
-
481
443
  // Handle -s (--skip-issues-with-prs) and --auto-continue interaction
482
444
  const hasExplicitAutoContinue = rawArgs.includes('--auto-continue');
483
445
  const hasExplicitNoAutoContinue = rawArgs.includes('--no-auto-continue');
484
-
485
446
  if (argv.skipIssuesWithPrs) {
486
447
  if (hasExplicitAutoContinue) {
487
448
  await log('āŒ Conflicting options: --skip-issues-with-prs and --auto-continue cannot be used together', {
@@ -492,15 +453,12 @@ if (isRunningDirectly) {
492
453
  await log(` šŸ“ Full log file: ${absoluteLogPath}`, { level: 'error' });
493
454
  await safeExit(1, 'Error occurred');
494
455
  }
495
-
496
456
  // -s implies disabling auto-continue unless explicitly set
497
457
  if (!hasExplicitNoAutoContinue) {
498
458
  argv.autoContinue = false;
499
459
  }
500
460
  }
501
-
502
461
  // Helper function to check GitHub permissions - moved to github.lib.mjs
503
-
504
462
  // Check GitHub permissions early in the process (skip in dry-run mode or when explicitly requested)
505
463
  if (argv.dryRun || argv.skipToolConnectionCheck || argv.toolConnectionCheck === false) {
506
464
  await log('ā© Skipping GitHub permissions check (dry-run mode or skip-tool-connection-check enabled)', {
@@ -513,13 +471,11 @@ if (isRunningDirectly) {
513
471
  await safeExit(1, 'Error occurred');
514
472
  }
515
473
  }
516
-
517
474
  // YouTrack configuration and validation
518
475
  let youTrackConfig = null;
519
476
  if (argv.youtrackMode) {
520
477
  // Create YouTrack config from environment variables and CLI overrides
521
478
  youTrackConfig = createYouTrackConfigFromEnv();
522
-
523
479
  if (!youTrackConfig) {
524
480
  await log('āŒ YouTrack mode requires environment variables to be set', { level: 'error' });
525
481
  await log(' Required: YOUTRACK_URL, YOUTRACK_API_KEY, YOUTRACK_PROJECT_CODE, YOUTRACK_STAGE', {
@@ -528,7 +484,6 @@ if (isRunningDirectly) {
528
484
  await log(' Example: YOUTRACK_URL=https://mycompany.youtrack.cloud', { level: 'error' });
529
485
  process.exit(1);
530
486
  }
531
-
532
487
  // Apply CLI overrides
533
488
  if (argv.youtrackStage) {
534
489
  youTrackConfig.stage = argv.youtrackStage;
@@ -536,7 +491,6 @@ if (isRunningDirectly) {
536
491
  if (argv.youtrackProject) {
537
492
  youTrackConfig.projectCode = argv.youtrackProject;
538
493
  }
539
-
540
494
  // Validate configuration
541
495
  try {
542
496
  validateYouTrackConfig(youTrackConfig);
@@ -544,7 +498,6 @@ if (isRunningDirectly) {
544
498
  await log(`āŒ YouTrack configuration error: ${error.message}`, { level: 'error' });
545
499
  process.exit(1);
546
500
  }
547
-
548
501
  // Test YouTrack connection
549
502
  const youTrackConnected = await testYouTrackConnection(youTrackConfig);
550
503
  if (!youTrackConnected) {
@@ -552,12 +505,10 @@ if (isRunningDirectly) {
552
505
  process.exit(1);
553
506
  }
554
507
  }
555
-
556
508
  // Parse GitHub URL to determine organization, repository, or user
557
509
  let scope = 'repository';
558
510
  let owner = null;
559
511
  let repo = null;
560
-
561
512
  // NO DUPLICATE VALIDATION! URL was already validated at the beginning.
562
513
  // If we have a URL but no validation results, that's a logic error.
563
514
  if (githubUrl && urlMatch === null) {
@@ -566,12 +517,10 @@ if (isRunningDirectly) {
566
517
  await log('This is a bug in the script logic', { level: 'error' });
567
518
  await safeExit(1, 'Error occurred');
568
519
  }
569
-
570
520
  if (urlMatch) {
571
521
  owner = urlMatch[1];
572
522
  repo = urlMatch[3] || null;
573
523
  }
574
-
575
524
  // Determine scope
576
525
  if (!repo) {
577
526
  // Check if it's an organization or user (skip in dry-run mode to avoid hanging)
@@ -597,7 +546,6 @@ if (isRunningDirectly) {
597
546
  } else {
598
547
  scope = 'repository';
599
548
  }
600
-
601
549
  await log('šŸŽÆ Monitoring Configuration:');
602
550
  if (argv.youtrackMode) {
603
551
  await log(` šŸ“ Source: YouTrack - ${youTrackConfig.url}`);
@@ -647,7 +595,6 @@ if (isRunningDirectly) {
647
595
  if (argv.autoCleanup) await log(' 🧹 Auto-cleanup: ENABLED (will clean /tmp/* /var/tmp/* on success)');
648
596
  if (argv.interactiveMode) await log(' šŸ”Œ Interactive Mode: ENABLED');
649
597
  await log('');
650
-
651
598
  // Producer/Consumer Queue implementation
652
599
  class IssueQueue {
653
600
  constructor() {
@@ -658,7 +605,6 @@ if (isRunningDirectly) {
658
605
  this.workers = [];
659
606
  this.isRunning = true;
660
607
  }
661
-
662
608
  // Add issue to queue if not already processed or in queue
663
609
  enqueue(issueUrl) {
664
610
  if (this.completed.has(issueUrl) || this.processing.has(issueUrl) || this.queue.includes(issueUrl)) {
@@ -667,7 +613,6 @@ if (isRunningDirectly) {
667
613
  this.queue.push(issueUrl);
668
614
  return true;
669
615
  }
670
-
671
616
  // Get next issue from queue
672
617
  dequeue() {
673
618
  if (this.queue.length === 0) {
@@ -677,19 +622,16 @@ if (isRunningDirectly) {
677
622
  this.processing.add(issue);
678
623
  return issue;
679
624
  }
680
-
681
625
  // Mark issue as completed
682
626
  markCompleted(issueUrl) {
683
627
  this.processing.delete(issueUrl);
684
628
  this.completed.add(issueUrl);
685
629
  }
686
-
687
630
  // Mark issue as failed
688
631
  markFailed(issueUrl) {
689
632
  this.processing.delete(issueUrl);
690
633
  this.failed.add(issueUrl);
691
634
  }
692
-
693
635
  // Get queue statistics
694
636
  getStats() {
695
637
  return {
@@ -700,36 +642,28 @@ if (isRunningDirectly) {
700
642
  processingIssues: Array.from(this.processing),
701
643
  };
702
644
  }
703
-
704
645
  // Stop all workers
705
646
  stop() {
706
647
  this.isRunning = false;
707
648
  }
708
649
  }
709
-
710
650
  // Create global queue instance
711
651
  const issueQueue = new IssueQueue();
712
-
713
652
  // Issue #1823: Track in-flight solve child processes. A *first* interrupt forwards a
714
653
  // controlled SIGTERM to each (they run in their own detached process group, so the
715
654
  // terminal's SIGINT never reaches them); a *second* interrupt force-kills the groups.
716
655
  const activeSolveChildren = new Set();
717
-
718
656
  // Worker function to process issues from queue
719
657
  async function worker(workerId) {
720
658
  await log(`šŸ”§ Worker ${workerId} started`, { verbose: true });
721
-
722
659
  while (issueQueue.isRunning) {
723
660
  const issueUrl = issueQueue.dequeue();
724
-
725
661
  if (!issueUrl) {
726
662
  // No work available, wait a bit
727
663
  await new Promise(resolve => setTimeout(resolve, 5000));
728
664
  continue;
729
665
  }
730
-
731
666
  await log(`\nšŸ‘· Worker ${workerId} processing: ${issueUrl}`);
732
-
733
667
  // Recheck conditions before processing to avoid wasted work
734
668
  const recheckResult = await recheckIssueConditions(issueUrl, argv);
735
669
  if (!recheckResult.shouldProcess) {
@@ -739,18 +673,15 @@ if (isRunningDirectly) {
739
673
  await log(` šŸ“Š Queue: ${stats.queued} waiting, ${stats.processing} processing, ${stats.completed} completed, ${stats.failed} failed`);
740
674
  continue;
741
675
  }
742
-
743
676
  // Track if this issue failed
744
677
  let issueFailed = false;
745
678
  // Issue #1823: Track a graceful shutdown stop so it is neither failed nor completed.
746
679
  let gracefulStop = false;
747
-
748
680
  // Process the issue multiple times if needed
749
681
  for (let prNum = 1; prNum <= argv.pullRequestsPerIssue; prNum++) {
750
682
  if (argv.pullRequestsPerIssue > 1) {
751
683
  await log(` šŸ“ Creating PR ${prNum}/${argv.pullRequestsPerIssue} for issue`);
752
684
  }
753
-
754
685
  try {
755
686
  // Execute solve command using spawn to enable real-time streaming while avoiding command-stream quoting issues
756
687
  if (argv.dryRun) {
@@ -758,7 +689,6 @@ if (isRunningDirectly) {
758
689
  } else {
759
690
  await log(` šŸš€ Executing ${solveCommand} for ${issueUrl}...`);
760
691
  }
761
-
762
692
  const startTime = Date.now();
763
693
  // Use spawn to get real-time streaming output while avoiding command-stream's automatic quote addition
764
694
  const { spawn } = await import('child_process');
@@ -784,7 +714,6 @@ if (isRunningDirectly) {
784
714
  if (argv.dryRun) args.push('--dry-run');
785
715
  if (argv.autoCleanup) args.push('--auto-cleanup');
786
716
  const SKIP_AUTO_FORWARD = new Set(['model', 'worker-model', 'base-branch', 'skip-tool-connection-check', 'tool-connection-check', 'skip-tool-check', 'skip-claude-check', 'tool-check', 'dry-run', 'auto-cleanup']);
787
-
788
717
  for (const optionName of getSolvePassthroughOptionNames()) {
789
718
  if (SKIP_AUTO_FORWARD.has(optionName)) continue;
790
719
  const camelName = kebabToCamel(optionName);
@@ -809,7 +738,6 @@ if (isRunningDirectly) {
809
738
  }
810
739
  // Log the actual command being executed so users can investigate/reproduce
811
740
  await log(` šŸ“‹ Command: ${solveCommand} ${args.join(' ')}`);
812
-
813
741
  let exitCode = 0;
814
742
  // Create promise to handle async spawn process
815
743
  await new Promise(resolve => {
@@ -822,11 +750,9 @@ if (isRunningDirectly) {
822
750
  // must NOT unref() — hive keeps waiting. See docs/case-studies/issue-1823.
823
751
  detached: true,
824
752
  });
825
-
826
753
  // Issue #1823: register the in-flight child for optional force-kill on a 2nd signal
827
754
  activeSolveChildren.add(child);
828
755
  log(` šŸ§’ Spawned ${solveCommand} worker-${workerId} (pid ${child.pid}, detached process group)`, { verbose: true }).catch(() => {});
829
-
830
756
  // Handle stdout data - stream output in real-time
831
757
  child.stdout.on('data', data => {
832
758
  const lines = data.toString().split('\n');
@@ -842,7 +768,6 @@ if (isRunningDirectly) {
842
768
  }
843
769
  }
844
770
  });
845
-
846
771
  // Handle stderr data - stream output in real-time.
847
772
  // Issue #1823: Do NOT blanket-tag stderr as ERROR — solve relays non-error
848
773
  // diagnostics there (codex DEBUG/INFO traces, git branch messages, etc.), which
@@ -862,7 +787,6 @@ if (isRunningDirectly) {
862
787
  }
863
788
  }
864
789
  });
865
-
866
790
  // Handle process completion and spawn failure. Issue #2135: a signalled
867
791
  // child reports `code === null`, which `code || 0` read as success.
868
792
  attachChildExitHandlers({
@@ -879,9 +803,7 @@ if (isRunningDirectly) {
879
803
  },
880
804
  });
881
805
  });
882
-
883
806
  const duration = Math.round((Date.now() - startTime) / 1000);
884
-
885
807
  if (exitCode === 0) {
886
808
  await log(` āœ… Worker ${workerId} completed ${issueUrl} (${duration}s)`);
887
809
  } else if (!issueQueue.isRunning && (exitCode === 130 || exitCode === 143)) {
@@ -894,7 +816,6 @@ if (isRunningDirectly) {
894
816
  } else {
895
817
  throw new Error(`${solveCommand} exited with code ${exitCode}`);
896
818
  }
897
-
898
819
  // Small delay between multiple PRs for same issue
899
820
  if (prNum < argv.pullRequestsPerIssue) {
900
821
  await new Promise(resolve => setTimeout(resolve, 10000));
@@ -914,19 +835,16 @@ if (isRunningDirectly) {
914
835
  break; // Stop trying more PRs for this issue
915
836
  }
916
837
  }
917
-
918
838
  // Only mark as completed if it didn't fail and wasn't gracefully stopped mid-shutdown.
919
839
  // Issue #1823: a graceful stop is neither a success nor a failure — leave it in
920
840
  // "processing" so it is not miscounted as completed (which would also trigger cleanup).
921
841
  if (!issueFailed && !gracefulStop) {
922
842
  issueQueue.markCompleted(issueUrl);
923
843
  }
924
-
925
844
  // Show queue stats
926
845
  const stats = issueQueue.getStats();
927
846
  await log(` šŸ“Š Queue: ${stats.queued} waiting, ${stats.processing} processing, ${stats.completed} completed, ${stats.failed} failed`);
928
847
  await log(` šŸ“ Hive log file: ${absoluteLogPath}`);
929
-
930
848
  // Show which issues are currently being processed
931
849
  if (stats.processingIssues && stats.processingIssues.length > 0) {
932
850
  await log(' šŸ”§ Currently processing solve commands:');
@@ -935,14 +853,11 @@ if (isRunningDirectly) {
935
853
  }
936
854
  }
937
855
  }
938
-
939
856
  await log(`šŸ”§ Worker ${workerId} stopped`, { verbose: true });
940
857
  }
941
-
942
858
  // Function to check if an issue has open pull requests
943
859
  // Note: hasOpenPullRequests function has been replaced by batchCheckPullRequestsForIssues
944
860
  // in github.lib.mjs for better performance and reduced API calls
945
-
946
861
  // Function to fetch issues from GitHub
947
862
  async function fetchIssues() {
948
863
  if (argv.youtrackMode) {
@@ -954,24 +869,19 @@ if (isRunningDirectly) {
954
869
  } else {
955
870
  await log(`\nšŸ” Fetching issues with label "${argv.monitorTag}"...`);
956
871
  }
957
-
958
872
  // In dry-run mode, skip actual API calls and return empty list immediately
959
873
  if (argv.dryRun) {
960
874
  await log(' 🧪 Dry-run mode: Skipping actual issue fetching');
961
875
  return [];
962
876
  }
963
-
964
877
  try {
965
878
  let issues = [];
966
-
967
879
  if (argv.youtrackMode) {
968
880
  // Sync YouTrack issues to GitHub
969
881
  if (!owner || !repo) {
970
882
  throw new Error('YouTrack mode requires a specific repository URL (not organization/user)');
971
883
  }
972
-
973
884
  const githubIssues = await syncYouTrackToGitHub(youTrackConfig, owner, repo, $, log);
974
-
975
885
  // Convert to format expected by hive
976
886
  issues = formatIssuesForHive(githubIssues).map(issue => ({
977
887
  url: issue.html_url,
@@ -983,7 +893,6 @@ if (isRunningDirectly) {
983
893
  if (!argv.projectNumber || !argv.projectOwner) {
984
894
  throw new Error('Project mode requires --project-number and --project-owner');
985
895
  }
986
-
987
896
  issues = await fetchProjectIssues(argv.projectNumber, argv.projectOwner, argv.projectStatus);
988
897
  } else if (argv.allIssues) {
989
898
  // Fetch all open issues without label filter using pagination
@@ -996,10 +905,8 @@ if (isRunningDirectly) {
996
905
  // User scope
997
906
  searchCmd = `gh search issues user:${owner} is:open --json url,title,number,createdAt,repository`;
998
907
  }
999
-
1000
908
  await log(' šŸ”Ž Fetching all issues with pagination and rate limiting...');
1001
909
  await log(` šŸ”Ž Command: ${searchCmd}`, { verbose: true });
1002
-
1003
910
  try {
1004
911
  issues = await fetchAllIssuesWithPagination(searchCmd);
1005
912
  } catch (searchError) {
@@ -1010,7 +917,6 @@ if (isRunningDirectly) {
1010
917
  operation: 'search_all_issues',
1011
918
  });
1012
919
  await log(` āš ļø Search failed: ${cleanErrorMessage(searchError)}`, { verbose: true });
1013
-
1014
920
  // Check if the error is due to rate limiting or search API limit and we're not in repository scope
1015
921
  const errorMsg = searchError.message || searchError.toString();
1016
922
  const isSearchLimitError = errorMsg.includes('Hit search API limit') || errorMsg.includes('repository-by-repository fallback');
@@ -1035,13 +941,11 @@ if (isRunningDirectly) {
1035
941
  } else {
1036
942
  // Use label filter
1037
943
  // execSync is used within fetchAllIssuesWithPagination
1038
-
1039
944
  // For repositories, use gh issue list which works better with new repos
1040
945
  if (scope === 'repository') {
1041
946
  const listCmd = `gh issue list --repo ${owner}/${repo} --state open --label "${argv.monitorTag}" --json url,title,number,createdAt`;
1042
947
  await log(' šŸ”Ž Fetching labeled issues with pagination and rate limiting...');
1043
948
  await log(` šŸ”Ž Command: ${listCmd}`, { verbose: true });
1044
-
1045
949
  try {
1046
950
  issues = await fetchAllIssuesWithPagination(listCmd);
1047
951
  } catch (listError) {
@@ -1063,11 +967,9 @@ if (isRunningDirectly) {
1063
967
  } else {
1064
968
  baseQuery = `user:${owner} is:issue is:open`;
1065
969
  }
1066
-
1067
970
  // Handle label with potential spaces
1068
971
  let searchQuery;
1069
972
  let searchCmd;
1070
-
1071
973
  if (argv.monitorTag.includes(' ')) {
1072
974
  searchQuery = `${baseQuery} label:"${argv.monitorTag}"`;
1073
975
  searchCmd = `gh search issues '${searchQuery}' --json url,title,number,createdAt,repository`;
@@ -1075,11 +977,9 @@ if (isRunningDirectly) {
1075
977
  searchQuery = `${baseQuery} label:${argv.monitorTag}`;
1076
978
  searchCmd = `gh search issues '${searchQuery}' --json url,title,number,createdAt,repository`;
1077
979
  }
1078
-
1079
980
  await log(' šŸ”Ž Fetching labeled issues with pagination and rate limiting...');
1080
981
  await log(` šŸ”Ž Search query: ${searchQuery}`, { verbose: true });
1081
982
  await log(` šŸ”Ž Command: ${searchCmd}`, { verbose: true });
1082
-
1083
983
  try {
1084
984
  issues = await fetchAllIssuesWithPagination(searchCmd);
1085
985
  } catch (searchError) {
@@ -1091,7 +991,6 @@ if (isRunningDirectly) {
1091
991
  operation: 'search_labeled_issues',
1092
992
  });
1093
993
  await log(` āš ļø Search failed: ${cleanErrorMessage(searchError)}`, { verbose: true });
1094
-
1095
994
  // Check if the error is due to rate limiting or search API limit
1096
995
  const errorMsg = searchError.message || searchError.toString();
1097
996
  const isSearchLimitError = errorMsg.includes('Hit search API limit') || errorMsg.includes('repository-by-repository fallback');
@@ -1116,7 +1015,6 @@ if (isRunningDirectly) {
1116
1015
  }
1117
1016
  }
1118
1017
  }
1119
-
1120
1018
  if (issues.length === 0) {
1121
1019
  if (argv.youtrackMode) {
1122
1020
  await log(` ā„¹ļø No issues found in YouTrack with stage "${youTrackConfig.stage}"`);
@@ -1129,7 +1027,6 @@ if (isRunningDirectly) {
1129
1027
  }
1130
1028
  return [];
1131
1029
  }
1132
-
1133
1030
  if (argv.youtrackMode) {
1134
1031
  await log(` šŸ“‹ Found ${issues.length} YouTrack issue(s) with stage "${youTrackConfig.stage}"`);
1135
1032
  } else if (argv.projectMode) {
@@ -1139,7 +1036,6 @@ if (isRunningDirectly) {
1139
1036
  } else {
1140
1037
  await log(` šŸ“‹ Found ${issues.length} issue(s) with label "${argv.monitorTag}"`);
1141
1038
  }
1142
-
1143
1039
  // Sort issues by publication date (createdAt) based on issue-order option
1144
1040
  if (issues.length > 0 && issues[0].createdAt) {
1145
1041
  await log(` šŸ”„ Sorting issues by publication date (${argv.issueOrder === 'asc' ? 'oldest first' : 'newest first'})...`);
@@ -1150,16 +1046,13 @@ if (isRunningDirectly) {
1150
1046
  });
1151
1047
  await log(' āœ… Issues sorted by publication date');
1152
1048
  }
1153
-
1154
1049
  // Filter out issues from archived repositories
1155
1050
  // This is critical because we cannot do write operations on archived repositories
1156
1051
  let issuesToProcess = issues;
1157
-
1158
1052
  // Helper function to extract repository info from issue (API response or URL)
1159
1053
  const getRepoInfo = issue => {
1160
1054
  let repoName = issue.repository?.name;
1161
1055
  let repoOwner = issue.repository?.owner?.login || issue.repository?.nameWithOwner?.split('/')[0];
1162
-
1163
1056
  // If repository info is not available, extract it from the issue URL
1164
1057
  if (!repoName || !repoOwner) {
1165
1058
  const urlMatch = issue.url?.match(/github\.com\/([^/]+)\/([^/]+)\/issues\/\d+/);
@@ -1168,15 +1061,12 @@ if (isRunningDirectly) {
1168
1061
  repoName = urlMatch[2];
1169
1062
  }
1170
1063
  }
1171
-
1172
1064
  return { repoOwner, repoName };
1173
1065
  };
1174
-
1175
1066
  // Only filter for organization/user scopes
1176
1067
  // For repository scope, we're already working on a specific repo
1177
1068
  if (scope !== 'repository' && issues.length > 0) {
1178
1069
  await log(' šŸ” Checking for archived repositories...');
1179
-
1180
1070
  // Extract unique repositories from issues
1181
1071
  const uniqueRepos = new Map();
1182
1072
  for (const issue of issues) {
@@ -1188,20 +1078,15 @@ if (isRunningDirectly) {
1188
1078
  }
1189
1079
  }
1190
1080
  }
1191
-
1192
1081
  // Batch check archived status for all repositories
1193
1082
  const archivedStatusMap = await batchCheckArchivedRepositories(Array.from(uniqueRepos.values()));
1194
-
1195
1083
  // Filter out issues from archived repositories
1196
1084
  const filteredIssues = [];
1197
1085
  let archivedIssuesCount = 0;
1198
-
1199
1086
  for (const issue of issues) {
1200
1087
  const { repoOwner, repoName } = getRepoInfo(issue);
1201
-
1202
1088
  if (repoOwner && repoName) {
1203
1089
  const repoKey = `${repoOwner}/${repoName}`;
1204
-
1205
1090
  if (archivedStatusMap[repoKey] === true) {
1206
1091
  await log(` ā­ļø Skipping (archived repository): ${issue.title || 'Untitled'} (${issue.url})`, {
1207
1092
  verbose: true,
@@ -1216,18 +1101,14 @@ if (isRunningDirectly) {
1216
1101
  filteredIssues.push(issue);
1217
1102
  }
1218
1103
  }
1219
-
1220
1104
  if (archivedIssuesCount > 0) {
1221
1105
  await log(` ā­ļø Skipped ${archivedIssuesCount} issue(s) from archived repositories`);
1222
1106
  }
1223
-
1224
1107
  issuesToProcess = filteredIssues;
1225
1108
  }
1226
-
1227
1109
  // Filter out issues with open PRs if option is enabled
1228
1110
  if (argv.skipIssuesWithPrs) {
1229
1111
  await log(' šŸ” Checking for existing pull requests using batch GraphQL query...');
1230
-
1231
1112
  // Extract issue numbers and repository info from URLs
1232
1113
  const issuesByRepo = {};
1233
1114
  for (const issue of issuesToProcess) {
@@ -1235,7 +1116,6 @@ if (isRunningDirectly) {
1235
1116
  if (urlMatch) {
1236
1117
  const [, issueOwner, issueRepo, issueNumber] = urlMatch;
1237
1118
  const repoKey = `${issueOwner}/${issueRepo}`;
1238
-
1239
1119
  if (!issuesByRepo[repoKey]) {
1240
1120
  issuesByRepo[repoKey] = {
1241
1121
  owner: issueOwner,
@@ -1243,22 +1123,18 @@ if (isRunningDirectly) {
1243
1123
  issues: [],
1244
1124
  };
1245
1125
  }
1246
-
1247
1126
  issuesByRepo[repoKey].issues.push({
1248
1127
  number: parseInt(issueNumber),
1249
1128
  issue: issue,
1250
1129
  });
1251
1130
  }
1252
1131
  }
1253
-
1254
1132
  // Batch check PRs for each repository
1255
1133
  const filteredIssues = [];
1256
1134
  let totalSkipped = 0;
1257
-
1258
1135
  for (const repoData of Object.values(issuesByRepo)) {
1259
1136
  const issueNumbers = repoData.issues.map(i => i.number);
1260
1137
  const prResults = await batchCheckPullRequestsForIssues(repoData.owner, repoData.repo, issueNumbers);
1261
-
1262
1138
  // Process results
1263
1139
  for (const issueData of repoData.issues) {
1264
1140
  const prInfo = prResults[issueData.number];
@@ -1270,19 +1146,16 @@ if (isRunningDirectly) {
1270
1146
  }
1271
1147
  }
1272
1148
  }
1273
-
1274
1149
  if (totalSkipped > 0) {
1275
1150
  await log(` ā­ļø Skipped ${totalSkipped} issue(s) with existing pull requests`);
1276
1151
  }
1277
1152
  issuesToProcess = filteredIssues;
1278
1153
  }
1279
-
1280
1154
  // Apply max issues limit if set (after filtering to exclude skipped issues from count)
1281
1155
  if (argv.maxIssues > 0 && issuesToProcess.length > argv.maxIssues) {
1282
1156
  issuesToProcess = issuesToProcess.slice(0, argv.maxIssues);
1283
1157
  await log(` šŸ”¢ Limiting to first ${argv.maxIssues} issues (after filtering)`);
1284
1158
  }
1285
-
1286
1159
  // In dry-run mode, show the issues that would be processed
1287
1160
  if (argv.dryRun && issuesToProcess.length > 0) {
1288
1161
  await log('\n šŸ“ Issues that would be processed:');
@@ -1290,7 +1163,6 @@ if (isRunningDirectly) {
1290
1163
  await log(` - ${issue.title || 'Untitled'} (${issue.url})`);
1291
1164
  }
1292
1165
  }
1293
-
1294
1166
  return issuesToProcess.map(issue => issue.url);
1295
1167
  } catch (error) {
1296
1168
  reportError(error, {
@@ -1304,26 +1176,21 @@ if (isRunningDirectly) {
1304
1176
  return [];
1305
1177
  }
1306
1178
  }
1307
-
1308
1179
  // Main monitoring loop
1309
1180
  async function monitor() {
1310
1181
  await log('\nšŸš€ Starting Hive Mind monitoring system...');
1311
-
1312
1182
  // Start workers
1313
1183
  await log(`\nšŸ‘· Starting ${argv.concurrency} workers...`);
1314
1184
  for (let i = 1; i <= argv.concurrency; i++) {
1315
1185
  issueQueue.workers.push(worker(i));
1316
1186
  }
1317
-
1318
1187
  // Main monitoring loop
1319
1188
  let iteration = 0;
1320
1189
  while (true) {
1321
1190
  iteration++;
1322
1191
  await log(`\nšŸ”„ Monitoring iteration ${iteration} at ${new Date().toISOString()}`);
1323
-
1324
1192
  // Fetch issues
1325
1193
  const issueUrls = await fetchIssues();
1326
-
1327
1194
  // Add new issues to queue
1328
1195
  let newIssues = 0;
1329
1196
  for (const url of issueUrls) {
@@ -1332,13 +1199,11 @@ if (isRunningDirectly) {
1332
1199
  await log(` āž• Added to queue: ${url}`);
1333
1200
  }
1334
1201
  }
1335
-
1336
1202
  if (newIssues > 0) {
1337
1203
  await log(` šŸ“„ Added ${newIssues} new issue(s) to queue`);
1338
1204
  } else {
1339
1205
  await log(' ā„¹ļø No new issues to add (all already processed or in queue)');
1340
1206
  }
1341
-
1342
1207
  // Show current stats
1343
1208
  const stats = issueQueue.getStats();
1344
1209
  await log('\nšŸ“Š Current Status:');
@@ -1347,7 +1212,6 @@ if (isRunningDirectly) {
1347
1212
  await log(` āœ… Completed: ${stats.completed}`);
1348
1213
  await log(` āŒ Failed: ${stats.failed}`);
1349
1214
  await log(` šŸ“ Hive log file: ${absoluteLogPath}`);
1350
-
1351
1215
  // Show which issues are currently being processed
1352
1216
  if (stats.processingIssues && stats.processingIssues.length > 0) {
1353
1217
  await log(' šŸ”§ Currently processing solve commands:');
@@ -1355,11 +1219,9 @@ if (isRunningDirectly) {
1355
1219
  await log(` - ${issueUrl}`);
1356
1220
  }
1357
1221
  }
1358
-
1359
1222
  // If running once, wait for queue to empty then exit
1360
1223
  if (argv.once) {
1361
1224
  await log('\nšŸ Single run mode - waiting for queue to empty...');
1362
-
1363
1225
  while (stats.queued > 0 || stats.processing > 0) {
1364
1226
  await new Promise(resolve => setTimeout(resolve, 5000));
1365
1227
  const currentStats = issueQueue.getStats();
@@ -1376,36 +1238,29 @@ if (isRunningDirectly) {
1376
1238
  await log(` Completed: ${stats.completed}`);
1377
1239
  await log(` Failed: ${stats.failed}`);
1378
1240
  await log(` šŸ“ Full log file: ${absoluteLogPath}`);
1379
-
1380
1241
  // Perform cleanup if enabled and there were successful completions
1381
1242
  if (stats.completed > 0) {
1382
1243
  await cleanupTempDirectories(argv);
1383
1244
  }
1384
-
1385
1245
  // Stop workers before breaking to avoid hanging
1386
1246
  issueQueue.stop();
1387
1247
  break;
1388
1248
  }
1389
-
1390
1249
  // Wait for next iteration
1391
1250
  await log(`\nā° Next check in ${argv.interval} seconds...`);
1392
1251
  await new Promise(resolve => setTimeout(resolve, argv.interval * 1000));
1393
1252
  }
1394
-
1395
1253
  // Stop workers
1396
1254
  issueQueue.stop();
1397
1255
  await Promise.all(issueQueue.workers);
1398
-
1399
1256
  // Perform cleanup if enabled and there were successful completions
1400
1257
  const finalStats = issueQueue.getStats();
1401
1258
  if (finalStats.completed > 0) {
1402
1259
  await cleanupTempDirectories();
1403
1260
  }
1404
-
1405
1261
  await log('\nšŸ‘‹ Hive Mind monitoring stopped');
1406
1262
  await log(` šŸ“ Full log file: ${absoluteLogPath}`);
1407
1263
  }
1408
-
1409
1264
  // Issue #1823: Graceful-shutdown + force-kill logic lives in hive.shutdown.lib.mjs.
1410
1265
  // gracefulShutdown waits (uncapped) for in-flight solve workers to finish on the first
1411
1266
  // interrupt; on a second interrupt it force-kills their detached process groups.
@@ -1420,7 +1275,6 @@ if (isRunningDirectly) {
1420
1275
  absoluteLogPath,
1421
1276
  activeSolveChildren,
1422
1277
  });
1423
-
1424
1278
  // Handle graceful shutdown.
1425
1279
  // Issue #1823: Tell the global exit handler (installed earlier via installGlobalExitHandlers)
1426
1280
  // to stand down on SIGINT/SIGTERM so it does not call process.exit() and race us. From here
@@ -1429,7 +1283,6 @@ if (isRunningDirectly) {
1429
1283
  delegateSignalHandling(true);
1430
1284
  process.on('SIGINT', () => gracefulShutdown('interrupt'));
1431
1285
  process.on('SIGTERM', () => gracefulShutdown('termination'));
1432
-
1433
1286
  // Check system resources (disk space and RAM) before starting monitoring (skip in dry-run mode)
1434
1287
  if (argv.dryRun || argv.skipToolConnectionCheck || argv.toolConnectionCheck === false) {
1435
1288
  await log('ā© Skipping system resource check (dry-run mode or skip-tool-connection-check enabled)', {
@@ -1447,11 +1300,9 @@ if (isRunningDirectly) {
1447
1300
  },
1448
1301
  { log }
1449
1302
  );
1450
-
1451
1303
  if (!systemCheck.success) {
1452
1304
  await safeExit(1, 'Error occurred');
1453
1305
  }
1454
-
1455
1306
  // Validate the selected AI tool connection before starting monitoring with the same model that will be used
1456
1307
  const isToolConnected = await validateToolConnection({ tool: argv.tool, model: argv.model, verbose: argv.verbose, validateClaudeConnection });
1457
1308
  if (!isToolConnected) {
@@ -1459,10 +1310,8 @@ if (isRunningDirectly) {
1459
1310
  await safeExit(1, 'Error occurred');
1460
1311
  }
1461
1312
  }
1462
-
1463
1313
  // Wrap monitor function with Sentry error tracking
1464
1314
  const monitorWithSentry = !argv.sentry ? monitor : withSentry(monitor, 'hive.monitor', 'command');
1465
-
1466
1315
  // Start monitoring
1467
1316
  try {
1468
1317
  await monitorWithSentry();
@@ -1475,7 +1324,6 @@ if (isRunningDirectly) {
1475
1324
  await log(` šŸ“ Full log file: ${absoluteLogPath}`, { level: 'error' });
1476
1325
  await safeExit(1, 'Error occurred');
1477
1326
  }
1478
-
1479
1327
  const finalStats = issueQueue.getStats(); // Issue #1718: surface worker failures via exit code
1480
1328
  if (finalStats.failed > 0) await safeExit(1, `${finalStats.failed} task(s) failed (completed: ${finalStats.completed})`);
1481
1329
  } catch (fatalError) {