@link-assistant/hive-mind 1.12.0 → 1.13.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.13.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 03adcb6: Add --auto-merge and --auto-restart-until-mergable options for autonomous PR management
8
+
9
+ New CLI options:
10
+ - `--auto-merge`: Automatically merge the pull request when CI passes and PR is mergeable. Implies --auto-restart-until-mergable.
11
+ - `--auto-restart-until-mergable`: Auto-restart the AI agent until PR becomes mergeable (no iteration limit). Restarts on new comments from non-bot users, CI failures, merge conflicts, or uncommitted changes. Does NOT auto-merge.
12
+
13
+ Features:
14
+ - Non-bot comment detection with configurable bot patterns
15
+ - Automatic detection of CI/CD status and merge readiness
16
+ - Continuous monitoring loop with configurable check intervals
17
+ - Progress and status reporting throughout the process
18
+ - Graceful handling of API errors with exponential backoff
19
+ - Session data tracking for accurate pricing across iterations
20
+
3
21
  ## 1.12.0
4
22
 
5
23
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.12.0",
3
+ "version": "1.13.0",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -239,6 +239,16 @@ export const createYargsConfig = yargsInstance => {
239
239
  alias: 'w',
240
240
  default: false,
241
241
  })
242
+ .option('auto-merge', {
243
+ type: 'boolean',
244
+ description: 'Automatically merge the pull request when the working session is finished and all CI/CD statuses pass and PR is mergeable. Implies --auto-restart-until-mergable.',
245
+ default: false,
246
+ })
247
+ .option('auto-restart-until-mergable', {
248
+ type: 'boolean',
249
+ description: 'Auto-restart until PR becomes mergeable (no iteration limit). Restarts on new comments from non-bot users, CI failures, merge conflicts, or other issues. Does NOT auto-merge.',
250
+ default: false,
251
+ })
242
252
  .option('issue-order', {
243
253
  type: 'string',
244
254
  description: 'Order issues by publication date: "asc" (oldest first) or "desc" (newest first)',
package/src/hive.mjs CHANGED
@@ -771,6 +771,8 @@ if (isDirectExecution) {
771
771
  if (argv.promptCaseStudies) args.push('--prompt-case-studies');
772
772
  if (argv.promptPlaywrightMcp !== undefined) args.push(argv.promptPlaywrightMcp ? '--prompt-playwright-mcp' : '--no-prompt-playwright-mcp');
773
773
  if (argv.executeToolWithBun) args.push('--execute-tool-with-bun');
774
+ if (argv.autoMerge) args.push('--auto-merge');
775
+ if (argv.autoRestartUntilMergable) args.push('--auto-restart-until-mergable');
774
776
  // Log the actual command being executed so users can investigate/reproduce
775
777
  await log(` 📋 Command: ${solveCommand} ${args.join(' ')}`);
776
778
 
@@ -0,0 +1,598 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Auto-merge and auto-restart-until-mergable module for solve.mjs
5
+ * Handles automatic merging of PRs and continuous restart until PR becomes mergeable
6
+ *
7
+ * Uses shared utilities from solve.restart-shared.lib.mjs for common functions.
8
+ *
9
+ * @see https://github.com/link-assistant/hive-mind/issues/1190
10
+ */
11
+
12
+ // Check if use is already defined globally (when imported from solve.mjs)
13
+ // If not, fetch it (when running standalone)
14
+ if (typeof globalThis.use === 'undefined') {
15
+ globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
16
+ }
17
+ const use = globalThis.use;
18
+
19
+ // Use command-stream for consistent $ behavior across runtimes
20
+ const { $ } = await use('command-stream');
21
+
22
+ // Import shared library functions
23
+ const lib = await import('./lib.mjs');
24
+ const { log, cleanErrorMessage, formatAligned, getLogFile } = lib;
25
+
26
+ // Note: We don't use detectAndCountFeedback from solve.feedback.lib.mjs
27
+ // because we have our own non-bot comment detection logic that's more
28
+ // appropriate for auto-restart-until-mergable mode
29
+
30
+ // Import Sentry integration
31
+ const sentryLib = await import('./sentry.lib.mjs');
32
+ const { reportError } = sentryLib;
33
+
34
+ // Import GitHub merge functions
35
+ const githubMergeLib = await import('./github-merge.lib.mjs');
36
+ const { checkPRCIStatus, checkPRMergeable, mergePullRequest, waitForCI } = githubMergeLib;
37
+
38
+ // Import GitHub functions for log attachment
39
+ const githubLib = await import('./github.lib.mjs');
40
+ const { sanitizeLogContent, attachLogToGitHub } = githubLib;
41
+
42
+ // Import shared utilities from the restart-shared module
43
+ const restartShared = await import('./solve.restart-shared.lib.mjs');
44
+ const { checkPRMerged, checkPRClosed, checkForUncommittedChanges, getUncommittedChangesDetails, executeToolIteration, buildAutoRestartInstructions, isApiError } = restartShared;
45
+
46
+ /**
47
+ * Check for new comments from non-bot users since last commit
48
+ * @returns {Promise<{hasNewComments: boolean, comments: Array}>}
49
+ */
50
+ const checkForNonBotComments = async (owner, repo, prNumber, issueNumber, lastCheckTime, verbose = false) => {
51
+ try {
52
+ // Get current GitHub user to identify which comments are from the bot/hive-mind
53
+ let currentUser = null;
54
+ try {
55
+ const userResult = await $`gh api user --jq .login`;
56
+ if (userResult.code === 0) {
57
+ currentUser = userResult.stdout.toString().trim();
58
+ }
59
+ } catch {
60
+ // If we can't get the current user, continue without filtering
61
+ }
62
+
63
+ // Common bot usernames and patterns to filter out
64
+ // Note: Patterns use word boundaries or end-of-string to avoid false positives
65
+ // (e.g., "claudeuser" should NOT match as a bot)
66
+ const botPatterns = [
67
+ /\[bot\]$/i, // Any username ending with [bot]
68
+ /^github-actions$/i, // GitHub Actions
69
+ /^dependabot$/i, // Dependabot
70
+ /^renovate$/i, // Renovate
71
+ /^codecov$/i, // Codecov
72
+ /^netlify$/i, // Netlify
73
+ /^vercel$/i, // Vercel
74
+ /^hive-?mind$/i, // Hive Mind (with or without hyphen)
75
+ /^claude$/i, // Claude (exact match only)
76
+ /^copilot$/i, // GitHub Copilot
77
+ ];
78
+
79
+ const isBot = login => {
80
+ if (!login) return false;
81
+ // Check if it's the current user (the bot running hive-mind)
82
+ if (currentUser && login === currentUser) return true;
83
+ // Check against known bot patterns
84
+ return botPatterns.some(pattern => pattern.test(login));
85
+ };
86
+
87
+ // Fetch PR conversation comments
88
+ const prCommentsResult = await $`gh api repos/${owner}/${repo}/issues/${prNumber}/comments --paginate`;
89
+ let prComments = [];
90
+ if (prCommentsResult.code === 0 && prCommentsResult.stdout) {
91
+ prComments = JSON.parse(prCommentsResult.stdout.toString() || '[]');
92
+ }
93
+
94
+ // Fetch PR review comments (inline code comments)
95
+ const prReviewCommentsResult = await $`gh api repos/${owner}/${repo}/pulls/${prNumber}/comments --paginate`;
96
+ let prReviewComments = [];
97
+ if (prReviewCommentsResult.code === 0 && prReviewCommentsResult.stdout) {
98
+ prReviewComments = JSON.parse(prReviewCommentsResult.stdout.toString() || '[]');
99
+ }
100
+
101
+ // Fetch issue comments if we have an issue number
102
+ let issueComments = [];
103
+ if (issueNumber && issueNumber !== prNumber) {
104
+ const issueCommentsResult = await $`gh api repos/${owner}/${repo}/issues/${issueNumber}/comments --paginate`;
105
+ if (issueCommentsResult.code === 0 && issueCommentsResult.stdout) {
106
+ issueComments = JSON.parse(issueCommentsResult.stdout.toString() || '[]');
107
+ }
108
+ }
109
+
110
+ // Combine all comments
111
+ const allComments = [...prComments, ...prReviewComments, ...issueComments];
112
+
113
+ // Filter for new comments from non-bot users
114
+ const newNonBotComments = allComments.filter(comment => {
115
+ const commentTime = new Date(comment.created_at);
116
+ const isAfterLastCheck = commentTime > lastCheckTime;
117
+ const isFromNonBot = !isBot(comment.user?.login);
118
+
119
+ if (verbose && isAfterLastCheck && isFromNonBot) {
120
+ console.log(`[VERBOSE] New non-bot comment from ${comment.user?.login} at ${comment.created_at}`);
121
+ }
122
+
123
+ return isAfterLastCheck && isFromNonBot;
124
+ });
125
+
126
+ return {
127
+ hasNewComments: newNonBotComments.length > 0,
128
+ comments: newNonBotComments,
129
+ };
130
+ } catch (error) {
131
+ reportError(error, {
132
+ context: 'check_non_bot_comments',
133
+ owner,
134
+ repo,
135
+ prNumber,
136
+ operation: 'fetch_comments',
137
+ });
138
+ return { hasNewComments: false, comments: [] };
139
+ }
140
+ };
141
+
142
+ /**
143
+ * Get the reasons why PR is not mergeable
144
+ */
145
+ const getMergeBlockers = async (owner, repo, prNumber, verbose = false) => {
146
+ const blockers = [];
147
+
148
+ // Check CI status
149
+ const ciStatus = await checkPRCIStatus(owner, repo, prNumber, verbose);
150
+ if (ciStatus.status === 'failure') {
151
+ blockers.push({
152
+ type: 'ci_failure',
153
+ message: 'CI/CD checks are failing',
154
+ details: ciStatus.checks.filter(c => c.conclusion === 'failure').map(c => c.name),
155
+ });
156
+ } else if (ciStatus.status === 'pending') {
157
+ blockers.push({
158
+ type: 'ci_pending',
159
+ message: 'CI/CD checks are still running',
160
+ details: ciStatus.checks.filter(c => c.status !== 'completed').map(c => c.name),
161
+ });
162
+ }
163
+
164
+ // Check mergeability
165
+ const mergeStatus = await checkPRMergeable(owner, repo, prNumber, verbose);
166
+ if (!mergeStatus.mergeable) {
167
+ blockers.push({
168
+ type: 'not_mergeable',
169
+ message: mergeStatus.reason || 'PR is not mergeable',
170
+ details: [],
171
+ });
172
+ }
173
+
174
+ return blockers;
175
+ };
176
+
177
+ /**
178
+ * Main function: Watch and restart until PR becomes mergeable
179
+ * This implements --auto-restart-until-mergable functionality
180
+ */
181
+ export const watchUntilMergable = async params => {
182
+ const { issueUrl, owner, repo, issueNumber, prNumber, prBranch, branchName, tempDir, argv } = params;
183
+
184
+ const watchInterval = argv.watchInterval || 60; // seconds
185
+ const isAutoMerge = argv.autoMerge || false;
186
+
187
+ // Track latest session data across all iterations for accurate pricing
188
+ let latestSessionId = null;
189
+ let latestAnthropicCost = null;
190
+
191
+ // Track consecutive API errors for retry limit
192
+ const MAX_API_ERROR_RETRIES = 3;
193
+ let consecutiveApiErrors = 0;
194
+ let currentBackoffSeconds = watchInterval;
195
+
196
+ await log('');
197
+ await log(formatAligned('🔄', 'AUTO-RESTART-UNTIL-MERGABLE MODE ACTIVE', ''));
198
+ await log(formatAligned('', 'Monitoring PR:', `#${prNumber}`, 2));
199
+ await log(formatAligned('', 'Mode:', isAutoMerge ? 'Auto-merge (will merge when ready)' : 'Auto-restart-until-mergable (will NOT auto-merge)', 2));
200
+ await log(formatAligned('', 'Checking interval:', `${watchInterval} seconds`, 2));
201
+ await log(formatAligned('', 'Stop conditions:', 'PR merged, PR closed, or becomes mergeable', 2));
202
+ await log(formatAligned('', 'Restart triggers:', 'New non-bot comments, CI failures, merge conflicts', 2));
203
+ await log('');
204
+ await log('Press Ctrl+C to stop watching manually');
205
+ await log('');
206
+
207
+ let iteration = 0;
208
+ let lastCheckTime = new Date();
209
+
210
+ while (true) {
211
+ iteration++;
212
+ const currentTime = new Date();
213
+
214
+ // Check if PR is merged
215
+ const isMerged = await checkPRMerged(owner, repo, prNumber);
216
+ if (isMerged) {
217
+ await log('');
218
+ await log(formatAligned('🎉', 'PR MERGED!', 'Stopping auto-restart-until-mergable mode'));
219
+ await log(formatAligned('', 'Pull request:', `#${prNumber} has been merged`, 2));
220
+ await log('');
221
+ return { success: true, reason: 'merged', latestSessionId, latestAnthropicCost };
222
+ }
223
+
224
+ // Check if PR is closed (not merged)
225
+ const isClosed = await checkPRClosed(owner, repo, prNumber);
226
+ if (isClosed) {
227
+ await log('');
228
+ await log(formatAligned('🚫', 'PR CLOSED!', 'Stopping auto-restart-until-mergable mode'));
229
+ await log(formatAligned('', 'Pull request:', `#${prNumber} has been closed without merging`, 2));
230
+ await log('');
231
+ return { success: false, reason: 'closed', latestSessionId, latestAnthropicCost };
232
+ }
233
+
234
+ await log(formatAligned('🔍', `Check #${iteration}:`, currentTime.toLocaleTimeString()));
235
+
236
+ try {
237
+ // Get merge blockers
238
+ const blockers = await getMergeBlockers(owner, repo, prNumber, argv.verbose);
239
+
240
+ // Check for new comments from non-bot users
241
+ const { hasNewComments, comments } = await checkForNonBotComments(owner, repo, prNumber, issueNumber, lastCheckTime, argv.verbose);
242
+
243
+ // Check for uncommitted changes using shared utility
244
+ const hasUncommittedChanges = await checkForUncommittedChanges(tempDir, argv);
245
+
246
+ // If PR is mergeable, no blockers, no new comments, and no uncommitted changes
247
+ if (blockers.length === 0 && !hasNewComments && !hasUncommittedChanges) {
248
+ await log(formatAligned('✅', 'PR IS MERGEABLE!', ''));
249
+
250
+ if (isAutoMerge) {
251
+ // Attempt to merge the PR
252
+ await log(formatAligned('🔀', 'Auto-merging PR...', ''));
253
+ const mergeResult = await mergePullRequest(owner, repo, prNumber, { squash: argv.squash || false, deleteAfter: argv.deleteBranchAfterMerge || false }, argv.verbose);
254
+
255
+ if (mergeResult.success) {
256
+ await log(formatAligned('🎉', 'PR MERGED SUCCESSFULLY!', ''));
257
+ await log(formatAligned('', 'Pull request:', `#${prNumber} has been auto-merged`, 2));
258
+
259
+ // Post success comment
260
+ try {
261
+ const commentBody = `## 🎉 Auto-merged\n\nThis pull request has been automatically merged by hive-mind after all CI checks passed and the PR became mergeable.\n\n---\n*Auto-merged by hive-mind with --auto-merge flag*`;
262
+ await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${commentBody}`;
263
+ } catch {
264
+ // Don't fail if comment posting fails
265
+ }
266
+
267
+ return { success: true, reason: 'auto-merged', latestSessionId, latestAnthropicCost };
268
+ } else {
269
+ await log(formatAligned('⚠️', 'Auto-merge failed:', mergeResult.error || 'Unknown error', 2));
270
+ await log(formatAligned('', 'Will continue monitoring...', '', 2));
271
+ }
272
+ } else {
273
+ // Just report that PR is mergeable and exit
274
+ await log(formatAligned('', 'PR is ready to be merged manually', '', 2));
275
+ await log(formatAligned('', 'Exiting auto-restart-until-mergable mode', '', 2));
276
+
277
+ // Post success comment
278
+ try {
279
+ const commentBody = `## ✅ Ready to merge\n\nThis pull request is now ready to be merged:\n- All CI checks have passed\n- No merge conflicts\n- No pending changes\n\n---\n*Monitored by hive-mind with --auto-restart-until-mergable flag*`;
280
+ await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${commentBody}`;
281
+ } catch {
282
+ // Don't fail if comment posting fails
283
+ }
284
+
285
+ return { success: true, reason: 'mergeable', latestSessionId, latestAnthropicCost };
286
+ }
287
+ }
288
+
289
+ // Determine if we need to restart
290
+ let shouldRestart = false;
291
+ let restartReason = '';
292
+ let feedbackLines = [];
293
+
294
+ // Reason 1: New comments from non-bot users
295
+ if (hasNewComments) {
296
+ shouldRestart = true;
297
+ restartReason = `New comment(s) from non-bot user(s): ${comments.map(c => c.user?.login).join(', ')}`;
298
+ feedbackLines.push('📬 New comments detected from non-bot users:');
299
+ for (const comment of comments) {
300
+ feedbackLines.push(` - ${comment.user?.login}: "${comment.body?.substring(0, 100)}${comment.body?.length > 100 ? '...' : ''}"`);
301
+ }
302
+ feedbackLines.push('');
303
+ feedbackLines.push('Please review and address the feedback from these comments.');
304
+ }
305
+
306
+ // Reason 2: CI failures
307
+ const ciBlocker = blockers.find(b => b.type === 'ci_failure');
308
+ if (ciBlocker) {
309
+ shouldRestart = true;
310
+ restartReason = restartReason ? `${restartReason}; CI failures` : 'CI failures detected';
311
+ feedbackLines.push('❌ CI/CD checks are failing:');
312
+ for (const check of ciBlocker.details) {
313
+ feedbackLines.push(` - ${check}`);
314
+ }
315
+ feedbackLines.push('');
316
+ feedbackLines.push('Please fix the failing CI checks.');
317
+ }
318
+
319
+ // Reason 3: Merge conflicts or other merge issues
320
+ const mergeBlocker = blockers.find(b => b.type === 'not_mergeable');
321
+ if (mergeBlocker && mergeBlocker.message.includes('conflicts')) {
322
+ shouldRestart = true;
323
+ restartReason = restartReason ? `${restartReason}; Merge conflicts` : 'Merge conflicts detected';
324
+ feedbackLines.push('⚠️ Merge conflicts detected:');
325
+ feedbackLines.push(` ${mergeBlocker.message}`);
326
+ feedbackLines.push('');
327
+ feedbackLines.push('Please resolve the merge conflicts.');
328
+ }
329
+
330
+ // Reason 4: Uncommitted changes
331
+ if (hasUncommittedChanges) {
332
+ shouldRestart = true;
333
+ restartReason = restartReason ? `${restartReason}; Uncommitted changes` : 'Uncommitted changes detected';
334
+
335
+ // Get uncommitted changes for display using shared utility
336
+ const changes = await getUncommittedChangesDetails(tempDir);
337
+ feedbackLines.push('📝 Uncommitted changes detected:');
338
+ for (const line of changes) {
339
+ feedbackLines.push(` ${line}`);
340
+ }
341
+ feedbackLines.push('');
342
+ feedbackLines.push('IMPORTANT: You MUST handle these uncommitted changes by either:');
343
+ feedbackLines.push('1. COMMITTING them if they are part of the solution (git add + git commit + git push)');
344
+ feedbackLines.push('2. REVERTING them if they are not needed (git checkout -- <file> or git clean -fd)');
345
+ }
346
+
347
+ if (shouldRestart) {
348
+ // Add standard instructions for auto-restart-until-mergable mode using shared utility
349
+ feedbackLines.push(...buildAutoRestartInstructions());
350
+
351
+ await log(formatAligned('🔄', 'RESTART TRIGGERED:', restartReason));
352
+ await log('');
353
+
354
+ // Post a comment to PR about the restart
355
+ try {
356
+ const commentBody = `## 🔄 Auto-restart triggered\n\n**Reason:** ${restartReason}\n\nStarting new session to address the issues.\n\n---\n*Auto-restart-until-mergable mode is active. Will continue until PR becomes mergeable.*`;
357
+ await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${commentBody}`;
358
+ await log(formatAligned('', '💬 Posted auto-restart notification to PR', '', 2));
359
+ } catch (commentError) {
360
+ reportError(commentError, {
361
+ context: 'post_auto_restart_comment',
362
+ owner,
363
+ repo,
364
+ prNumber,
365
+ operation: 'comment_on_pr',
366
+ });
367
+ await log(formatAligned('', '⚠️ Could not post comment to PR', '', 2));
368
+ }
369
+
370
+ // Get PR merge state status
371
+ const prStateResult = await $`gh api repos/${owner}/${repo}/pulls/${prNumber} --jq '.mergeStateStatus'`;
372
+ const mergeStateStatus = prStateResult.code === 0 ? prStateResult.stdout.toString().trim() : null;
373
+
374
+ // Execute the AI tool using shared utility
375
+ await log(formatAligned('🔄', 'Restarting:', `Running ${argv.tool.toUpperCase()} to address issues...`));
376
+
377
+ const toolResult = await executeToolIteration({
378
+ issueUrl,
379
+ owner,
380
+ repo,
381
+ issueNumber,
382
+ prNumber,
383
+ branchName: prBranch || branchName,
384
+ tempDir,
385
+ mergeStateStatus,
386
+ feedbackLines,
387
+ argv,
388
+ });
389
+
390
+ if (!toolResult.success) {
391
+ // Check if this is an API error using shared utility
392
+ if (isApiError(toolResult)) {
393
+ consecutiveApiErrors++;
394
+ await log(formatAligned('⚠️', `${argv.tool.toUpperCase()} execution failed`, `API error detected (${consecutiveApiErrors}/${MAX_API_ERROR_RETRIES})`, 2));
395
+
396
+ if (consecutiveApiErrors >= MAX_API_ERROR_RETRIES) {
397
+ await log('');
398
+ await log(formatAligned('❌', 'MAXIMUM API ERROR RETRIES REACHED', ''));
399
+ await log(formatAligned('', 'Error details:', toolResult.result || 'Unknown API error', 2));
400
+ await log(formatAligned('', 'Action:', 'Exiting to prevent infinite loop', 2));
401
+ return { success: false, reason: 'api_error', latestSessionId, latestAnthropicCost };
402
+ }
403
+
404
+ // Apply exponential backoff
405
+ currentBackoffSeconds = Math.min(currentBackoffSeconds * 2, 300);
406
+ await log(formatAligned('', 'Backing off:', `Will retry after ${currentBackoffSeconds} seconds`, 2));
407
+ } else {
408
+ consecutiveApiErrors = 0;
409
+ currentBackoffSeconds = watchInterval;
410
+ await log(formatAligned('⚠️', `${argv.tool.toUpperCase()} execution failed`, 'Will retry in next check', 2));
411
+ }
412
+ } else {
413
+ // Success - reset error counters
414
+ consecutiveApiErrors = 0;
415
+ currentBackoffSeconds = watchInterval;
416
+
417
+ // Capture latest session data
418
+ if (toolResult.sessionId) {
419
+ latestSessionId = toolResult.sessionId;
420
+ latestAnthropicCost = toolResult.anthropicTotalCostUSD;
421
+ }
422
+
423
+ // Attach log if enabled
424
+ const shouldAttachLogs = argv.attachLogs || argv['attach-logs'];
425
+ if (prNumber && shouldAttachLogs) {
426
+ await log('');
427
+ await log(formatAligned('📎', 'Uploading session log...', ''));
428
+ try {
429
+ const logFile = getLogFile();
430
+ if (logFile) {
431
+ const customTitle = `🔄 Auto-restart-until-mergable Log (iteration ${iteration})`;
432
+ await attachLogToGitHub({
433
+ logFile,
434
+ targetType: 'pr',
435
+ targetNumber: prNumber,
436
+ owner,
437
+ repo,
438
+ $,
439
+ log,
440
+ sanitizeLogContent,
441
+ verbose: argv.verbose,
442
+ customTitle,
443
+ sessionId: latestSessionId,
444
+ tempDir,
445
+ anthropicTotalCostUSD: latestAnthropicCost,
446
+ publicPricingEstimate: toolResult.publicPricingEstimate,
447
+ pricingInfo: toolResult.pricingInfo,
448
+ });
449
+ await log(formatAligned('', '✅ Session log uploaded to PR', '', 2));
450
+ }
451
+ } catch (logUploadError) {
452
+ reportError(logUploadError, {
453
+ context: 'attach_auto_restart_log',
454
+ prNumber,
455
+ owner,
456
+ repo,
457
+ iteration,
458
+ operation: 'upload_session_log',
459
+ });
460
+ await log(formatAligned('', `⚠️ Log upload error: ${cleanErrorMessage(logUploadError)}`, '', 2));
461
+ }
462
+ }
463
+
464
+ await log('');
465
+ await log(formatAligned('✅', `${argv.tool.toUpperCase()} execution completed:`, 'Checking if PR is now mergeable...'));
466
+ }
467
+
468
+ // Update last check time after restart
469
+ lastCheckTime = new Date();
470
+ } else if (blockers.length > 0) {
471
+ // There are blockers but none that warrant a restart (e.g., CI pending)
472
+ await log(formatAligned('⏳', 'Waiting for:', blockers.map(b => b.message).join(', '), 2));
473
+ } else {
474
+ await log(formatAligned('', 'No action needed', 'Continuing to monitor...', 2));
475
+ }
476
+
477
+ // Update last check time
478
+ lastCheckTime = currentTime;
479
+ } catch (error) {
480
+ reportError(error, {
481
+ context: 'watch_until_mergable',
482
+ prNumber,
483
+ owner,
484
+ repo,
485
+ operation: 'check_and_restart',
486
+ });
487
+ await log(formatAligned('⚠️', 'Check failed:', cleanErrorMessage(error), 2));
488
+ await log(formatAligned('', 'Will retry in:', `${watchInterval} seconds`, 2));
489
+ }
490
+
491
+ // Wait for next interval
492
+ const actualWaitSeconds = consecutiveApiErrors > 0 ? currentBackoffSeconds : watchInterval;
493
+ await log(formatAligned('⏱️', 'Next check in:', `${actualWaitSeconds} seconds...`, 2));
494
+ await log('');
495
+ await new Promise(resolve => setTimeout(resolve, actualWaitSeconds * 1000));
496
+ }
497
+ };
498
+
499
+ /**
500
+ * Attempt to auto-merge PR after session ends
501
+ * This implements the --auto-merge functionality for one-shot merge attempts
502
+ */
503
+ export const attemptAutoMerge = async params => {
504
+ const { owner, repo, prNumber, argv } = params;
505
+
506
+ await log('');
507
+ await log(formatAligned('🔀', 'AUTO-MERGE:', 'Checking if PR can be merged...'));
508
+
509
+ // Wait for CI to complete (with timeout)
510
+ const ciWaitResult = await waitForCI(
511
+ owner,
512
+ repo,
513
+ prNumber,
514
+ {
515
+ timeout: argv.autoMergeCiTimeout || 30 * 60 * 1000, // 30 minutes default
516
+ pollInterval: argv.autoMergeCiPollInterval || 30 * 1000, // 30 seconds default
517
+ onStatusUpdate: async status => {
518
+ if (argv.verbose) {
519
+ await log(` CI status: ${status.status}`, { verbose: true });
520
+ }
521
+ },
522
+ },
523
+ argv.verbose
524
+ );
525
+
526
+ if (!ciWaitResult.success) {
527
+ await log(formatAligned('⚠️', 'CI check failed or timed out:', ciWaitResult.error || ciWaitResult.status, 2));
528
+ return { success: false, reason: ciWaitResult.status, error: ciWaitResult.error };
529
+ }
530
+
531
+ await log(formatAligned('✅', 'CI checks passed:', 'Checking mergeability...', 2));
532
+
533
+ // Check if PR is mergeable
534
+ const mergeStatus = await checkPRMergeable(owner, repo, prNumber, argv.verbose);
535
+ if (!mergeStatus.mergeable) {
536
+ await log(formatAligned('⚠️', 'PR not mergeable:', mergeStatus.reason || 'Unknown reason', 2));
537
+ return { success: false, reason: 'not_mergeable', error: mergeStatus.reason };
538
+ }
539
+
540
+ await log(formatAligned('✅', 'PR is mergeable:', 'Attempting to merge...', 2));
541
+
542
+ // Attempt to merge
543
+ const mergeResult = await mergePullRequest(owner, repo, prNumber, { squash: argv.squash || false, deleteAfter: argv.deleteBranchAfterMerge || false }, argv.verbose);
544
+
545
+ if (mergeResult.success) {
546
+ await log(formatAligned('🎉', 'PR MERGED SUCCESSFULLY!', ''));
547
+
548
+ // Post success comment
549
+ try {
550
+ const commentBody = `## 🎉 Auto-merged\n\nThis pull request has been automatically merged by hive-mind after all CI checks passed and the PR became mergeable.\n\n---\n*Auto-merged by hive-mind with --auto-merge flag*`;
551
+ await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${commentBody}`;
552
+ } catch {
553
+ // Don't fail if comment posting fails
554
+ }
555
+
556
+ return { success: true, reason: 'merged' };
557
+ } else {
558
+ await log(formatAligned('⚠️', 'Merge failed:', mergeResult.error || 'Unknown error', 2));
559
+ return { success: false, reason: 'merge_failed', error: mergeResult.error };
560
+ }
561
+ };
562
+
563
+ /**
564
+ * Start auto-restart-until-mergable mode
565
+ */
566
+ export const startAutoRestartUntilMergable = async params => {
567
+ const { argv } = params;
568
+
569
+ // Determine the mode
570
+ const isAutoMerge = argv.autoMerge || false;
571
+ const isAutoRestartUntilMergable = argv.autoRestartUntilMergable || false;
572
+
573
+ if (!isAutoMerge && !isAutoRestartUntilMergable) {
574
+ return null; // Neither mode enabled
575
+ }
576
+
577
+ if (!params.prNumber) {
578
+ await log('');
579
+ await log(formatAligned('⚠️', 'Auto-restart-until-mergable:', 'Requires a pull request'));
580
+ await log(formatAligned('', 'Note:', 'This mode only works with existing PRs', 2));
581
+ return null;
582
+ }
583
+
584
+ // If --auto-merge implies --auto-restart-until-mergable
585
+ if (isAutoMerge) {
586
+ argv.autoRestartUntilMergable = true;
587
+ }
588
+
589
+ // Start the watch loop
590
+ return await watchUntilMergable(params);
591
+ };
592
+
593
+ export default {
594
+ watchUntilMergable,
595
+ attemptAutoMerge,
596
+ startAutoRestartUntilMergable,
597
+ checkForNonBotComments,
598
+ };
@@ -200,6 +200,16 @@ export const createYargsConfig = yargsInstance => {
200
200
  description: 'Maximum number of auto-restart iterations when uncommitted changes are detected (default: 3)',
201
201
  default: 3,
202
202
  })
203
+ .option('auto-merge', {
204
+ type: 'boolean',
205
+ description: 'Automatically merge the pull request when the working session is finished and all CI/CD statuses pass and PR is mergeable. Implies --auto-restart-until-mergable.',
206
+ default: false,
207
+ })
208
+ .option('auto-restart-until-mergable', {
209
+ type: 'boolean',
210
+ description: 'Auto-restart until PR becomes mergeable (no iteration limit). Restarts on new comments from non-bot users, CI failures, merge conflicts, or other issues. Does NOT auto-merge.',
211
+ default: false,
212
+ })
203
213
  .option('continue-only-on-feedback', {
204
214
  type: 'boolean',
205
215
  description: 'Only continue if feedback is detected (works only with pull request link or issue link with --auto-continue)',