@link-assistant/hive-mind 1.30.5 → 1.31.1

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/solve.mjs CHANGED
@@ -69,14 +69,16 @@ const usageLimitLib = await import('./usage-limit.lib.mjs');
69
69
  const { formatResetTimeWithRelative } = usageLimitLib;
70
70
 
71
71
  const errorHandlers = await import('./solve.error-handlers.lib.mjs');
72
- const { createUncaughtExceptionHandler, createUnhandledRejectionHandler, handleMainExecutionError } = errorHandlers;
72
+ const { createUncaughtExceptionHandler, createUnhandledRejectionHandler, handleMainExecutionError, handleNoPrAvailableError } = errorHandlers;
73
73
 
74
74
  const watchLib = await import('./solve.watch.lib.mjs');
75
75
  const { startWatchMode } = watchLib;
76
- const autoMergeLib = await import('./solve.auto-merge.lib.mjs');
77
- const { startAutoRestartUntilMergeable } = autoMergeLib;
76
+ const { startAutoRestartUntilMergeable } = await import('./solve.auto-merge.lib.mjs');
77
+ const { runAutoEnsureRequirements } = await import('./solve.auto-ensure.lib.mjs');
78
78
  const exitHandler = await import('./exit-handler.lib.mjs');
79
79
  const { initializeExitHandler, installGlobalExitHandlers, safeExit } = exitHandler;
80
+ const interruptLib = await import('./solve.interrupt.lib.mjs');
81
+ const { createInterruptWrapper } = interruptLib;
80
82
  const getResourceSnapshot = memoryCheck.getResourceSnapshot;
81
83
 
82
84
  // Import new modular components
@@ -162,22 +164,17 @@ if (argv.sentry) {
162
164
  },
163
165
  });
164
166
  }
165
- // Create a cleanup wrapper that will be populated with context later
166
- let cleanupContext = { tempDir: null, argv: null, limitReached: false };
167
+ // Create cleanup/interrupt wrappers populated with context as solve progresses
168
+ let cleanupContext = { tempDir: null, argv: null, limitReached: false, branchName: null, prNumber: null, owner: null, repo: null };
167
169
  const cleanupWrapper = async () => {
168
170
  if (cleanupContext.tempDir && cleanupContext.argv) {
169
171
  await cleanupTempDirectory(cleanupContext.tempDir, cleanupContext.argv, cleanupContext.limitReached);
170
172
  }
171
173
  };
172
- // Initialize the exit handler with getAbsoluteLogPath function and cleanup wrapper
173
- initializeExitHandler(getAbsoluteLogPath, log, cleanupWrapper);
174
+ const interruptWrapper = createInterruptWrapper({ cleanupContext, checkForUncommittedChanges, shouldAttachLogs, attachLogToGitHub, getLogFile, sanitizeLogContent, $, log });
175
+ initializeExitHandler(getAbsoluteLogPath, log, cleanupWrapper, interruptWrapper);
174
176
  installGlobalExitHandlers();
175
177
 
176
- // Note: Version and raw command are logged BEFORE parseArguments() (see above)
177
- // This ensures they appear even if strict validation fails
178
- // Strict options validation is now handled by yargs .strict() mode in solve.config.lib.mjs
179
- // This prevents unrecognized options from being silently ignored (issue #453, #482)
180
-
181
178
  // Now handle argument validation that was moved from early checks
182
179
  let issueUrl = argv['issue-url'] || argv._[0];
183
180
  if (!issueUrl) {
@@ -193,9 +190,11 @@ if (!urlValidation.isValid) {
193
190
  }
194
191
  const { isIssueUrl, isPrUrl, normalizedUrl, owner, repo, number: urlNumber } = urlValidation;
195
192
  issueUrl = normalizedUrl || issueUrl;
196
- // Store owner and repo globally for error handlers early
193
+ // Store owner and repo globally for error handlers and interrupt context
197
194
  global.owner = owner;
198
195
  global.repo = repo;
196
+ cleanupContext.owner = owner;
197
+ cleanupContext.repo = repo;
199
198
  // Setup unhandled error handlers to ensure log path is always shown
200
199
  const errorHandlerOptions = {
201
200
  log,
@@ -492,8 +491,6 @@ if (isPrUrl) {
492
491
  }
493
492
  }
494
493
  await log(`šŸ“ PR branch: ${prBranch}`);
495
- // Extract issue number from PR body using GitHub linking detection library
496
- // This ensures we only detect actual GitHub-recognized linking keywords
497
494
  const prBody = prData.body || '';
498
495
  const extractedIssueNumber = extractLinkedIssueNumber(prBody);
499
496
  if (extractedIssueNumber) {
@@ -524,9 +521,12 @@ if (isPrUrl) {
524
521
  // Pass workspace info for --enable-workspaces mode (works with all tools)
525
522
  const workspaceInfo = argv.enableWorkspaces ? { owner, repo, issueNumber } : null;
526
523
  const { tempDir, workspaceTmpDir, needsClone } = await setupTempDirectory(argv, workspaceInfo);
527
- // Populate cleanup context for signal handlers
524
+ // Populate cleanup context for signal handlers (owner/repo updated again here for redundancy)
528
525
  cleanupContext.tempDir = tempDir;
529
526
  cleanupContext.argv = argv;
527
+ cleanupContext.owner = owner;
528
+ cleanupContext.repo = repo;
529
+ if (prNumber) cleanupContext.prNumber = prNumber;
530
530
  // Initialize limitReached variable outside try block for finally clause
531
531
  let limitReached = false;
532
532
  try {
@@ -570,6 +570,7 @@ try {
570
570
  repo,
571
571
  prNumber,
572
572
  });
573
+ cleanupContext.branchName = branchName;
573
574
 
574
575
  // Auto-merge default branch to pull request branch if enabled
575
576
  let autoMergeFeedbackLines = [];
@@ -614,9 +615,6 @@ try {
614
615
  // prNumber is already set from earlier when we parsed the PR
615
616
  }
616
617
 
617
- // Don't build the prompt yet - we'll build it after we have all the information
618
- // This includes PR URL (if created) and comment info (if in continue mode)
619
-
620
618
  // Handle auto PR creation using the new module
621
619
  const autoPrResult = await handleAutoPrCreation({
622
620
  argv,
@@ -647,43 +645,12 @@ try {
647
645
  claudeCommitHash = autoPrResult.claudeCommitHash;
648
646
  }
649
647
  }
648
+ if (prNumber) cleanupContext.prNumber = prNumber;
650
649
 
651
650
  // CRITICAL: Validate that we have a PR number when required
652
651
  // This prevents continuing without a PR when one was supposed to be created
653
652
  if ((isContinueMode || argv.autoPullRequestCreation) && !prNumber) {
654
- await log('');
655
- await log(formatAligned('āŒ', 'FATAL ERROR:', 'No pull request available'), { level: 'error' });
656
- await log('');
657
- await log(' šŸ” What happened:');
658
- if (isContinueMode) {
659
- await log(' Continue mode is active but no PR number is available.');
660
- await log(' This usually means PR creation failed or was skipped incorrectly.');
661
- } else {
662
- await log(' Auto-PR creation is enabled but no PR was created.');
663
- await log(' PR creation may have failed without throwing an error.');
664
- }
665
- await log('');
666
- await log(' šŸ’” Why this is critical:');
667
- await log(' The solve command requires a PR for:');
668
- await log(' • Tracking work progress');
669
- await log(' • Receiving and processing feedback');
670
- await log(' • Managing code changes');
671
- await log(' • Auto-merging when complete');
672
- await log('');
673
- await log(' šŸ”§ How to fix:');
674
- await log('');
675
- await log(' Option 1: Create PR manually and use --continue');
676
- await log(` cd ${tempDir}`);
677
- await log(` gh pr create --draft --title "Fix issue #${issueNumber}" --body "Fixes #${issueNumber}"`);
678
- await log(' # Then use the PR URL with solve.mjs');
679
- await log('');
680
- await log(' Option 2: Start fresh without continue mode');
681
- await log(` ./solve.mjs "${issueUrl}" --auto-pull-request-creation`);
682
- await log('');
683
- await log(' Option 3: Disable auto-PR creation (Claude will create it)');
684
- await log(` ./solve.mjs "${issueUrl}" --no-auto-pull-request-creation`);
685
- await log('');
686
- await safeExit(1, 'No PR available');
653
+ await handleNoPrAvailableError({ isContinueMode, tempDir, issueNumber, issueUrl, log, formatAligned });
687
654
  }
688
655
 
689
656
  if (isContinueMode) {
@@ -695,9 +662,6 @@ try {
695
662
  await log(formatAligned('', 'Workflow:', 'AI will create the PR', 2));
696
663
  }
697
664
 
698
- // Don't build the prompt yet - we'll build it after we have all the information
699
- // This includes PR URL (if created) and comment info (if in continue mode)
700
-
701
665
  // Start work session using the new module
702
666
  // Determine session type based on command line flags
703
667
  // See: https://github.com/link-assistant/hive-mind/issues/1152
@@ -1186,7 +1150,6 @@ try {
1186
1150
  await log('ā„¹ļø Playwright MCP auto-cleanup disabled via --no-playwright-mcp-auto-cleanup', { verbose: true });
1187
1151
  }
1188
1152
 
1189
- // Check for uncommitted changes
1190
1153
  // When limit is reached, force auto-commit of any uncommitted changes to preserve work
1191
1154
  const shouldAutoCommit = argv['auto-commit-uncommitted-changes'] || limitReached;
1192
1155
  const autoRestartEnabled = argv['autoRestartOnUncommittedChanges'] !== false;
@@ -1234,11 +1197,6 @@ try {
1234
1197
  }
1235
1198
 
1236
1199
  // Search for newly created pull requests and comments
1237
- // Pass shouldRestart to prevent early exit when auto-restart is needed
1238
- // Include agent tool pricing data when available (publicPricingEstimate, pricingInfo)
1239
- // Issue #1088: Pass errorDuringExecution for "Finished with errors" state
1240
- // Issue #1152: Pass sessionType for differentiated log comments
1241
- // Issue #1154: Track if logs were already uploaded to prevent duplicates
1242
1200
  const verifyResult = await verifyResults(owner, repo, branchName, issueNumber, prNumber, prUrl, referenceTime, argv, shouldAttachLogs, shouldRestart, sessionId, tempDir, anthropicTotalCostUSD, publicPricingEstimate, pricingInfo, errorDuringExecution, sessionType);
1243
1201
  const logsAlreadyUploaded = verifyResult?.logUploadSuccess || false;
1244
1202
 
@@ -1293,6 +1251,15 @@ try {
1293
1251
  }
1294
1252
  }
1295
1253
 
1254
+ // Issue #1383: --finalize
1255
+ const autoEnsureResult = await runAutoEnsureRequirements({ issueUrl, owner, repo, issueNumber, prNumber, branchName, tempDir, argv, cleanupClaudeFile });
1256
+ if (autoEnsureResult) {
1257
+ if (autoEnsureResult.sessionId) sessionId = autoEnsureResult.sessionId;
1258
+ if (autoEnsureResult.anthropicTotalCostUSD) anthropicTotalCostUSD = autoEnsureResult.anthropicTotalCostUSD;
1259
+ if (autoEnsureResult.publicPricingEstimate) publicPricingEstimate = autoEnsureResult.publicPricingEstimate;
1260
+ if (autoEnsureResult.pricingInfo) pricingInfo = autoEnsureResult.pricingInfo;
1261
+ }
1262
+
1296
1263
  // Start watch mode if enabled OR if we need to handle uncommitted changes
1297
1264
  if (argv.verbose) {
1298
1265
  await log('');
@@ -1496,4 +1463,15 @@ try {
1496
1463
  // Issue #1346: Flush Sentry events before exit.
1497
1464
  // closeSentry() uses a hard Promise.race deadline so it cannot block indefinitely.
1498
1465
  await closeSentry();
1466
+
1467
+ // Issue #1335: Log active handles at exit to diagnose future process hang.
1468
+ if (argv.verbose) {
1469
+ const handles = process._getActiveHandles();
1470
+ const requests = process._getActiveRequests();
1471
+ if (handles.length > 0 || requests.length > 0) {
1472
+ await log(`\nšŸ” Active Node.js handles at exit (${handles.length} handles, ${requests.length} requests):`, { verbose: true });
1473
+ for (const h of handles) await log(` Handle: ${h.constructor?.name || typeof h}`, { verbose: true });
1474
+ for (const r of requests) await log(` Request: ${r.constructor?.name || typeof r}`, { verbose: true });
1475
+ }
1476
+ }
1499
1477
  }
@@ -366,7 +366,29 @@ export function registerMergeCommand(bot, options) {
366
366
 
367
367
  // Cancel the operation
368
368
  operation.processor.cancel();
369
- await ctx.answerCbQuery('Merge operation cancellation requested. The current PR will finish processing.');
369
+ // Issue #1407: Acknowledge the cancel with a short toast message
370
+ await ctx.answerCbQuery('Cancellation requested.');
371
+
372
+ // Issue #1407: Immediately hide the cancel button and update the message to show
373
+ // that the queue is being cancelled. Without this, the button stays visible until
374
+ // the current PR finishes processing (which can take hours if waiting for CI).
375
+ try {
376
+ const cancellingMessage = operation.processor.formatProgressMessage();
377
+ await ctx.editMessageText(cancellingMessage, {
378
+ parse_mode: 'MarkdownV2',
379
+ // No reply_markup = cancel button is removed immediately
380
+ });
381
+ } catch (err) {
382
+ // If the full message edit fails, fall back to just removing the button
383
+ if (!err.message?.includes('message is not modified')) {
384
+ VERBOSE && console.log(`[VERBOSE] /merge: Error updating message on cancel: ${err.message}`);
385
+ }
386
+ try {
387
+ await ctx.editMessageReplyMarkup({ inline_keyboard: [] });
388
+ } catch {
389
+ // Ignore errors - the button will be removed when the operation completes
390
+ }
391
+ }
370
392
 
371
393
  VERBOSE && console.log(`[VERBOSE] /merge: Cancelled operation for ${repoKey}`);
372
394
  });
@@ -408,11 +408,22 @@ export class MergeQueueProcessor {
408
408
  await this.onProgress(this.getProgressUpdate());
409
409
  }
410
410
  },
411
+ // Issue #1407: Pass cancellation check so CI wait can abort early
412
+ isCancelled: () => this.isCancelled,
411
413
  },
412
414
  this.verbose
413
415
  );
414
416
 
415
417
  if (!waitResult.success) {
418
+ // Issue #1407: If cancelled during CI wait, mark as skipped (not failed)
419
+ // so the queue can cleanly stop without misleading failure statistics
420
+ if (waitResult.status === 'cancelled') {
421
+ item.status = MergeItemStatus.SKIPPED;
422
+ item.error = 'Cancelled';
423
+ this.stats.skipped++;
424
+ this.log(`Skipped PR #${item.pr.number}: cancelled during CI wait`);
425
+ return;
426
+ }
416
427
  item.status = MergeItemStatus.FAILED;
417
428
  item.error = waitResult.error;
418
429
  this.stats.failed++;
@@ -686,6 +697,11 @@ export class MergeQueueProcessor {
686
697
  message += `${update.progress.processed}/${update.progress.total} PRs processed\n`;
687
698
  message += '```\n\n';
688
699
 
700
+ // Issue #1407: Show cancelling indicator when cancellation requested but queue still running
701
+ if (this.isCancelled) {
702
+ message += `šŸ›‘ *Cancelling\\.\\.\\.*\n\n`;
703
+ }
704
+
689
705
  // Status summary with emojis
690
706
  message += `āœ… Merged: ${update.stats.merged} `;
691
707
  message += `āŒ Failed: ${update.stats.failed} `;