@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/CHANGELOG.md +73 -0
- package/package.json +1 -1
- package/src/agent.lib.mjs +3 -0
- package/src/agent.prompts.lib.mjs +6 -1
- package/src/claude.lib.mjs +4 -1
- package/src/claude.prompts.lib.mjs +6 -1
- package/src/codex.lib.mjs +2 -0
- package/src/codex.prompts.lib.mjs +6 -1
- package/src/exit-handler.lib.mjs +16 -1
- package/src/github-merge-ready-sync.lib.mjs +251 -0
- package/src/github-merge.lib.mjs +15 -185
- package/src/opencode.lib.mjs +2 -0
- package/src/opencode.prompts.lib.mjs +6 -1
- package/src/option-suggestions.lib.mjs +3 -0
- package/src/solve.auto-ensure.lib.mjs +120 -0
- package/src/solve.auto-merge.lib.mjs +61 -5
- package/src/solve.config.lib.mjs +26 -0
- package/src/solve.error-handlers.lib.mjs +39 -0
- package/src/solve.interrupt.lib.mjs +70 -0
- package/src/solve.mjs +39 -61
- package/src/telegram-merge-command.lib.mjs +23 -1
- package/src/telegram-merge-queue.lib.mjs +16 -0
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
|
|
77
|
-
const {
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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} `;
|