@link-assistant/hive-mind 1.50.2 → 1.50.3

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.
@@ -33,7 +33,7 @@ const { reportError } = sentryLib;
33
33
 
34
34
  // Import GitHub merge functions
35
35
  const githubMergeLib = await import('./github-merge.lib.mjs');
36
- const { checkPRMergeable, checkMergePermissions, mergePullRequest, waitForCI, checkForBillingLimitError, getRepoVisibility, BILLING_LIMIT_ERROR_PATTERN, getDetailedCIStatus, rerunWorkflowRun, getWorkflowRunsForSha, getActiveRepoWorkflows, getCommitDate, checkWorkflowsHavePRTriggers, checkPreviousPRCommitsHadCI, getAllActiveRepoRuns, checkCIConsensus } = githubMergeLib;
36
+ const { checkPRMergeable, checkMergePermissions, mergePullRequest, waitForCI, getRepoVisibility, BILLING_LIMIT_ERROR_PATTERN, getDetailedCIStatus, rerunWorkflowRun, getWorkflowRunsForSha, getAllActiveRepoRuns, checkCIConsensus } = githubMergeLib;
37
37
 
38
38
  // Import GitHub functions for log attachment
39
39
  const githubLib = await import('./github.lib.mjs');
@@ -50,457 +50,9 @@ const { calculateWaitTime } = validation;
50
50
  // Import configuration (used for limit reset buffer and jitter)
51
51
  import { limitReset } from './config.lib.mjs';
52
52
 
53
- /**
54
- * Issue #1323: Check if a comment with specific content already exists on the PR
55
- * This prevents duplicate status comments when multiple processes or restarts occur
56
- * @param {string} owner - Repository owner
57
- * @param {string} repo - Repository name
58
- * @param {number} prNumber - Pull request number
59
- * @param {string} commentSignature - Unique signature to search for in comment body (e.g., "✅ Ready to merge")
60
- * @param {boolean} verbose - Enable verbose logging
61
- * @returns {Promise<boolean>} - True if a matching comment already exists
62
- */
63
- const checkForExistingComment = async (owner, repo, prNumber, commentSignature, verbose = false) => {
64
- try {
65
- // Fetch recent PR comments (last 20 to avoid fetching entire history)
66
- const result = await $`gh api repos/${owner}/${repo}/issues/${prNumber}/comments --jq '.[].body' 2>/dev/null`;
67
- if (result.code === 0 && result.stdout) {
68
- const bodies = result.stdout.toString();
69
- const hasMatch = bodies.includes(commentSignature);
70
- if (verbose && hasMatch) {
71
- console.log(`[VERBOSE] Found existing comment with signature: "${commentSignature}"`);
72
- }
73
- return hasMatch;
74
- }
75
- } catch (error) {
76
- // If check fails, allow posting to avoid silent failures
77
- if (verbose) {
78
- console.log(`[VERBOSE] Failed to check for existing comment: ${error.message}`);
79
- }
80
- }
81
- return false;
82
- };
83
-
84
- /**
85
- * Check for new comments from non-bot users since last commit
86
- * @returns {Promise<{hasNewComments: boolean, comments: Array}>}
87
- */
88
- const checkForNonBotComments = async (owner, repo, prNumber, issueNumber, lastCheckTime, verbose = false) => {
89
- try {
90
- // Get current GitHub user to identify which comments are from the bot/hive-mind
91
- let currentUser = null;
92
- try {
93
- const userResult = await $`gh api user --jq .login`;
94
- if (userResult.code === 0) {
95
- currentUser = userResult.stdout.toString().trim();
96
- }
97
- } catch {
98
- // If we can't get the current user, continue without filtering
99
- }
100
-
101
- // Common bot usernames and patterns to filter out
102
- // Note: Patterns use word boundaries or end-of-string to avoid false positives
103
- // (e.g., "claudeuser" should NOT match as a bot)
104
- const botPatterns = [
105
- /\[bot\]$/i, // Any username ending with [bot]
106
- /^github-actions$/i, // GitHub Actions
107
- /^dependabot$/i, // Dependabot
108
- /^renovate$/i, // Renovate
109
- /^codecov$/i, // Codecov
110
- /^netlify$/i, // Netlify
111
- /^vercel$/i, // Vercel
112
- /^hive-?mind$/i, // Hive Mind (with or without hyphen)
113
- /^claude$/i, // Claude (exact match only)
114
- /^copilot$/i, // GitHub Copilot
115
- ];
116
-
117
- const isBot = login => {
118
- if (!login) return false;
119
- // Check if it's the current user (the bot running hive-mind)
120
- if (currentUser && login === currentUser) return true;
121
- // Check against known bot patterns
122
- return botPatterns.some(pattern => pattern.test(login));
123
- };
124
-
125
- // Fetch PR conversation comments
126
- const prCommentsResult = await $`gh api repos/${owner}/${repo}/issues/${prNumber}/comments --paginate`;
127
- let prComments = [];
128
- if (prCommentsResult.code === 0 && prCommentsResult.stdout) {
129
- prComments = JSON.parse(prCommentsResult.stdout.toString() || '[]');
130
- }
131
-
132
- // Fetch PR review comments (inline code comments)
133
- const prReviewCommentsResult = await $`gh api repos/${owner}/${repo}/pulls/${prNumber}/comments --paginate`;
134
- let prReviewComments = [];
135
- if (prReviewCommentsResult.code === 0 && prReviewCommentsResult.stdout) {
136
- prReviewComments = JSON.parse(prReviewCommentsResult.stdout.toString() || '[]');
137
- }
138
-
139
- // Fetch issue comments if we have an issue number
140
- let issueComments = [];
141
- if (issueNumber && issueNumber !== prNumber) {
142
- const issueCommentsResult = await $`gh api repos/${owner}/${repo}/issues/${issueNumber}/comments --paginate`;
143
- if (issueCommentsResult.code === 0 && issueCommentsResult.stdout) {
144
- issueComments = JSON.parse(issueCommentsResult.stdout.toString() || '[]');
145
- }
146
- }
147
-
148
- // Combine all comments
149
- const allComments = [...prComments, ...prReviewComments, ...issueComments];
150
-
151
- // Filter for new comments from non-bot users
152
- const newNonBotComments = allComments.filter(comment => {
153
- const commentTime = new Date(comment.created_at);
154
- const isAfterLastCheck = commentTime > lastCheckTime;
155
- const isFromNonBot = !isBot(comment.user?.login);
156
-
157
- if (verbose && isAfterLastCheck && isFromNonBot) {
158
- console.log(`[VERBOSE] New non-bot comment from ${comment.user?.login} at ${comment.created_at}`);
159
- }
160
-
161
- return isAfterLastCheck && isFromNonBot;
162
- });
163
-
164
- return {
165
- hasNewComments: newNonBotComments.length > 0,
166
- comments: newNonBotComments,
167
- };
168
- } catch (error) {
169
- reportError(error, {
170
- context: 'check_non_bot_comments',
171
- owner,
172
- repo,
173
- prNumber,
174
- operation: 'fetch_comments',
175
- });
176
- return { hasNewComments: false, comments: [] };
177
- }
178
- };
179
-
180
- /**
181
- * Get the reasons why PR is not mergeable
182
- * Issue #1314: Comprehensive CI/CD status handling covering all possible states:
183
- * - success: All CI passed → no blocker
184
- * - failure: Genuine code failures → restart AI
185
- * - cancelled: Manually cancelled or workflow cancelled → re-trigger, don't restart AI
186
- * - pending/queued: Still running or waiting for runner → wait, don't restart AI
187
- * - billing_limit: Billing/spending limit reached → stop (private) or wait (public)
188
- * - no_checks: No CI checks yet (race condition) → wait
189
- */
190
- const getMergeBlockers = async (owner, repo, prNumber, verbose = false, checkCount = 1, prBranchRef = null) => {
191
- const blockers = [];
192
-
193
- // Use detailed CI status to distinguish between all possible states
194
- const ciStatus = await getDetailedCIStatus(owner, repo, prNumber, verbose);
195
-
196
- if (ciStatus.status === 'no_checks') {
197
- // No CI checks exist yet - this could be:
198
- // 1. A race condition after push (checks haven't started yet) - wait
199
- // 2. A repository with no CI/CD configured at all - should be mergeable immediately
200
- // 3. CI workflows exist but were not triggered for this commit (fork PR, paths-ignore, etc.)
201
- //
202
- // Issue #1345: Distinguish by checking the PR's mergeability status.
203
- // If GitHub says the PR is MERGEABLE (mergeStateStatus === 'CLEAN'),
204
- // then no CI is required and we should not block indefinitely.
205
- // Otherwise (e.g. mergeStateStatus === 'BLOCKED'), treat as pending race condition.
206
- const earlyMergeStatus = await checkPRMergeable(owner, repo, prNumber, verbose);
207
- if (earlyMergeStatus.mergeable) {
208
- // Issue #1363: Before concluding "no CI configured", verify the repo actually
209
- // has no active GitHub Actions workflows. If workflows exist but no checks have
210
- // started yet, this is a race condition (GitHub takes ~10-30s to register checks
211
- // after a push), NOT a "no CI configured" situation.
212
- //
213
- // This fixes a false positive where a repo with CI workflows but WITHOUT branch
214
- // protection (required status checks) would be declared "no CI configured" because:
215
- // - mergeStateStatus=CLEAN (no required checks to block it)
216
- // - check_runs=[] (CI hasn't started yet — race condition)
217
- const repoWorkflows = await getActiveRepoWorkflows(owner, repo, verbose);
218
- if (repoWorkflows.hasWorkflows) {
219
- // Repo HAS workflows — but were they triggered for this commit?
220
- // Issue #1442: Use the GitHub Actions workflow runs API to definitively check
221
- // if any workflow runs were triggered for this PR's HEAD SHA. This avoids
222
- // the need for timeout-based detection:
223
- // - workflow_runs.length > 0 → genuine race condition (CI started, check-runs not yet registered)
224
- // - workflow_runs.length === 0 → CI was NOT triggered (fork PR, paths-ignore, etc.)
225
- const workflowRuns = await getWorkflowRunsForSha(owner, repo, ciStatus.sha, verbose);
226
- if (workflowRuns.length > 0) {
227
- // Issue #1466: Check if ALL workflow runs are completed without producing check-runs.
228
- // This happens when workflows require manual approval (first-time fork contributors,
229
- // deployment approvals) — they complete with conclusion=action_required but never
230
- // create check-runs. Waiting for check-runs in this case is an infinite loop.
231
- //
232
- // Also covers other non-executing conclusions: cancelled, stale workflows that
233
- // completed without producing check-runs won't produce them in the future either.
234
- const allRunsCompleted = workflowRuns.every(r => r.status === 'completed');
235
- const allRunsNonExecuting = allRunsCompleted && workflowRuns.every(r => r.conclusion === 'action_required' || r.conclusion === 'cancelled' || r.conclusion === 'stale' || r.conclusion === 'skipped');
236
-
237
- if (allRunsNonExecuting) {
238
- // All workflow runs completed without executing jobs — check-runs will never appear.
239
- // Treat the same as "CI not triggered" to avoid infinite waiting.
240
- const conclusions = [...new Set(workflowRuns.map(r => r.conclusion))].join(', ');
241
- if (verbose) {
242
- await log(`[VERBOSE] /merge: PR #${prNumber} has ${workflowRuns.length} workflow run(s) for SHA ${ciStatus.sha.substring(0, 7)}, but all completed without executing (conclusions: ${conclusions}) — check-runs will never appear`);
243
- }
244
- await log(formatAligned('ℹ️', 'CI workflows completed without executing:', `${conclusions} (${workflowRuns.map(r => r.name).join(', ')})`, 2));
245
- return { blockers, ciStatus, noCiConfigured: false, noCiTriggered: true, workflowRunConclusions: conclusions };
246
- }
247
-
248
- // Some workflow runs are still in progress or produced results — genuine race condition
249
- if (verbose) {
250
- await log(`[VERBOSE] /merge: PR #${prNumber} has no CI check-runs yet, but ${workflowRuns.length} workflow run(s) were triggered for SHA ${ciStatus.sha.substring(0, 7)} - genuine race condition (waiting for check-runs to appear)`);
251
- }
252
- blockers.push({
253
- type: 'ci_pending',
254
- message: `CI/CD checks have not started yet (${workflowRuns.length} workflow run(s) triggered, waiting for check-runs to appear)`,
255
- details: workflowRuns.map(r => r.name),
256
- });
257
- } else {
258
- // No workflow runs for this SHA — but this could be a race condition!
259
- // Issue #1480: GitHub Actions workflow runs take 30-120 seconds to appear in the
260
- // API after a push. The previous fix (issue #1442) assumed 0 workflow runs meant
261
- // "CI definitively NOT triggered", but this caused false positive "Ready to merge"
262
- // when checked too soon after a push.
263
- //
264
- // Multi-layer defense (Issue #1480 enhanced):
265
- // Layer 1: Grace period — check commit age
266
- // Layer 2: Workflow file parsing — check .github/workflows for PR triggers
267
- // Layer 3: Previous commit CI history — check if earlier PR commits had CI runs
268
- const WORKFLOW_RUN_GRACE_PERIOD_SECONDS = 120; // 2 minutes — generous to cover slow GitHub API registration
269
- const commitInfo = await getCommitDate(owner, repo, ciStatus.sha, verbose);
270
-
271
- // Issue #1480: Parse workflow files for PR triggers (used in both grace period and post-grace checks)
272
- const prTriggers = await checkWorkflowsHavePRTriggers(owner, repo, verbose, prBranchRef);
273
-
274
- // Issue #1480: If .github/workflows folder doesn't exist or has no workflow files,
275
- // that's a definitive signal — no CI/CD will execute, skip grace period entirely
276
- if (!prTriggers.hasWorkflowFiles) {
277
- if (verbose) {
278
- await log(`[VERBOSE] /merge: PR #${prNumber} repo has no workflow files in .github/workflows/ — CI definitively not configured at file level`);
279
- }
280
- return { blockers, ciStatus, noCiConfigured: false, noCiTriggered: true };
281
- }
282
-
283
- if (prTriggers.hasPRTriggers) {
284
- // Issue #1480 (enhanced): Workflows have PR/push triggers but no runs yet.
285
- // This is almost certainly a race condition — GitHub takes 30-120s to register
286
- // workflow runs after a push. We MUST wait regardless of commit age, because
287
- // commit date reflects authoring time, NOT push time.
288
- //
289
- // The commit may have been authored hours ago but pushed just now (rebased branches,
290
- // amended commits, cherry-picks). Using commit age as a proxy for push age caused
291
- // false positives in Case 1 of Issue #1480.
292
- //
293
- // Safety valve: after MAX_NO_RUNS_CHECKS consecutive checks (typically 5 × 60s = 5 min),
294
- // conclude CI was not triggered. This handles cases like paths-ignore excluding all
295
- // changed files, conditional workflows that don't match, etc.
296
- const MAX_NO_RUNS_CHECKS = 5;
297
- if (checkCount >= MAX_NO_RUNS_CHECKS) {
298
- // Issue #1503 (enhanced): Before concluding CI was not triggered, check if
299
- // previous commits in this PR had CI runs. If they did, CI should be expected
300
- // for the current commit too — extend waiting with a higher threshold.
301
- const MAX_NO_RUNS_CHECKS_WITH_CI_HISTORY = 10;
302
- if (checkCount < MAX_NO_RUNS_CHECKS_WITH_CI_HISTORY) {
303
- const previousCI = await checkPreviousPRCommitsHadCI(owner, repo, prNumber, ciStatus.sha, verbose);
304
- if (previousCI.hadPreviousCI) {
305
- // Previous commits had CI — this commit should too, keep waiting
306
- await log(formatAligned('⚠️', 'CI history signal:', `${previousCI.previousCommitsWithCI} previous commit(s) had CI runs — extending wait (check ${checkCount}/${MAX_NO_RUNS_CHECKS_WITH_CI_HISTORY})`, 2));
307
- blockers.push({
308
- type: 'ci_pending',
309
- message: `CI/CD workflow runs have not appeared yet — previous commits had CI runs, extending wait (check ${checkCount}/${MAX_NO_RUNS_CHECKS_WITH_CI_HISTORY})`,
310
- details: prTriggers.workflows.map(w => w.name),
311
- });
312
- return { blockers, ciStatus, noCiConfigured: false, noCiTriggered: false };
313
- }
314
- }
315
- // We've waited long enough (and no CI history signal) — CI was genuinely not triggered
316
- if (verbose) {
317
- await log(formatAligned('ℹ️', 'CI not triggered:', `No workflow runs after ${checkCount} consecutive checks — concluding CI was not triggered`, 2));
318
- }
319
- return { blockers, ciStatus, noCiConfigured: false, noCiTriggered: true };
320
- }
321
-
322
- if (verbose) {
323
- await log(formatAligned('⏳', 'Waiting for CI:', `No workflow runs for SHA ${ciStatus.sha.substring(0, 7)}, but workflows have PR/push triggers (${prTriggers.workflows.map(w => w.name).join(', ')}) — check ${checkCount}/${MAX_NO_RUNS_CHECKS}, commit age: ${commitInfo.ageSeconds ?? 'unknown'}s`, 2));
324
- }
325
- blockers.push({
326
- type: 'ci_pending',
327
- message: `CI/CD workflow runs have not appeared yet — workflows have PR/push triggers (${prTriggers.workflows.map(w => w.name).join(', ')}), waiting for GitHub to register workflow runs (check ${checkCount}/${MAX_NO_RUNS_CHECKS})`,
328
- details: prTriggers.workflows.map(w => w.name),
329
- });
330
- } else if (commitInfo.ageSeconds !== null && commitInfo.ageSeconds < WORKFLOW_RUN_GRACE_PERIOD_SECONDS) {
331
- // No PR triggers found in workflow files, but commit is still recent — be safe and wait
332
- if (verbose) {
333
- await log(`[VERBOSE] /merge: No PR/push triggers found in workflow files, but commit is only ${commitInfo.ageSeconds}s old — waiting to be safe`);
334
- }
335
- blockers.push({
336
- type: 'ci_pending',
337
- message: `CI/CD workflow runs have not appeared yet — commit is ${commitInfo.ageSeconds}s old, waiting for GitHub to register workflow runs (grace period: ${WORKFLOW_RUN_GRACE_PERIOD_SECONDS}s)`,
338
- details: [],
339
- });
340
- } else {
341
- // No PR triggers AND commit is old enough — CI was definitively NOT triggered
342
- // Issue #1442: Fork PRs needing maintainer approval, paths-ignore filtering,
343
- // workflow conditions not matching, etc. all result in zero workflow runs.
344
- if (verbose) {
345
- await log(`[VERBOSE] /merge: PR #${prNumber} has no CI checks and no workflow runs for SHA ${ciStatus.sha.substring(0, 7)} (commit age: ${commitInfo.ageSeconds ?? 'unknown'}s, no PR/push triggers in workflow files) — CI was not triggered`);
346
- }
347
- return { blockers, ciStatus, noCiConfigured: false, noCiTriggered: true };
348
- }
349
- }
350
- } else {
351
- // Repo has NO workflows — this is truly "no CI configured"
352
- // PR is already mergeable with no CI checks configured.
353
- // Do NOT add a ci_pending blocker. The mergeability check below will also
354
- // confirm this is mergeable, so blockers will be empty → PR IS MERGEABLE path.
355
- if (verbose) {
356
- await log(`[VERBOSE] /merge: PR #${prNumber} has no CI checks and repo has no active workflows - no CI/CD configured`);
357
- }
358
- // Return early with no CI blocker, mergeability already confirmed
359
- return { blockers, ciStatus, noCiConfigured: true };
360
- }
361
- } else {
362
- // PR is not yet mergeable despite no checks - treat as pending race condition
363
- blockers.push({
364
- type: 'ci_pending',
365
- message: 'CI/CD checks have not started yet (waiting for checks to appear)',
366
- details: [],
367
- });
368
- }
369
- } else if (ciStatus.status === 'success') {
370
- // Issue #1480: Cross-validate "success" with workflow runs API.
371
- // A fast external check (e.g., CodeFactor) can register and pass before the main CI
372
- // pipeline starts, causing getDetailedCIStatus to return 'success' prematurely.
373
- // We must verify that all expected workflow runs have actually completed.
374
- const workflowRuns = await getWorkflowRunsForSha(owner, repo, ciStatus.sha, verbose);
375
-
376
- if (workflowRuns.length > 0) {
377
- // Workflow runs exist — check if any are still running
378
- const incompleteRuns = workflowRuns.filter(r => r.status !== 'completed');
379
- if (incompleteRuns.length > 0) {
380
- // Some workflow runs are still in progress — more check-runs may appear
381
- if (verbose) {
382
- await log(`[VERBOSE] /merge: PR #${prNumber} CI status is 'success' (${ciStatus.passedChecks.length} checks passed), but ${incompleteRuns.length} workflow run(s) still in progress — waiting for completion`);
383
- }
384
- blockers.push({
385
- type: 'ci_pending',
386
- message: `CI checks show success (${ciStatus.passedChecks.length} passed) but ${incompleteRuns.length} workflow run(s) still in progress — waiting for all to complete`,
387
- details: incompleteRuns.map(r => r.name),
388
- });
389
- }
390
- // All workflow runs completed — the check-runs we see are the final set, trust the 'success' status
391
- } else {
392
- // No workflow runs for this SHA — the passed checks are from external services only
393
- // (e.g., CodeFactor, Codecov). Check if the repo has workflows that should produce runs.
394
- const repoWorkflows = await getActiveRepoWorkflows(owner, repo, verbose);
395
- if (repoWorkflows.hasWorkflows) {
396
- const prTriggers = await checkWorkflowsHavePRTriggers(owner, repo, verbose, prBranchRef);
397
- if (prTriggers.hasPRTriggers) {
398
- // Repo has workflows with PR triggers but no runs yet — CI hasn't started
399
- // This is the exact scenario from Case 2 of Issue #1480
400
- //
401
- // Safety valve: after MAX_NO_RUNS_CHECKS consecutive checks, trust the external checks
402
- const MAX_NO_RUNS_CHECKS = 5;
403
- if (checkCount >= MAX_NO_RUNS_CHECKS) {
404
- if (verbose) {
405
- await log(`[VERBOSE] /merge: PR #${prNumber} CI 'success' with ${ciStatus.passedChecks.length} external checks, no workflow runs after ${checkCount} checks — trusting external checks`);
406
- }
407
- // Fall through — trust the success status from external checks
408
- } else {
409
- if (verbose) {
410
- await log(`[VERBOSE] /merge: PR #${prNumber} CI status is 'success' (${ciStatus.passedChecks.length} external checks), but repo has PR-triggered workflows with 0 workflow runs — likely race condition (check ${checkCount}/${MAX_NO_RUNS_CHECKS})`);
411
- }
412
- // Wait for GitHub Actions to register workflow runs
413
- blockers.push({
414
- type: 'ci_pending',
415
- message: `CI shows ${ciStatus.passedChecks.length} passed check(s) from external services, but repo has PR-triggered workflows that haven't started yet — waiting for GitHub Actions to register (check ${checkCount}/${MAX_NO_RUNS_CHECKS})`,
416
- details: prTriggers.workflows.map(w => w.name),
417
- });
418
- }
419
- }
420
- }
421
- // No repo workflows → external checks are the only CI, trust the 'success' status
422
- }
423
- } else if (ciStatus.status === 'pending') {
424
- // CI is still running or queued - wait for completion
425
- const pendingNames = [...ciStatus.pendingChecks, ...ciStatus.queuedChecks].map(c => c.name);
426
- blockers.push({
427
- type: 'ci_pending',
428
- message: 'CI/CD checks are still running or queued',
429
- details: pendingNames,
430
- });
431
- } else if (ciStatus.status === 'cancelled') {
432
- // All non-passed checks are cancelled or stale (no genuine failures)
433
- // First check if this is actually a billing limit issue (billing-limited jobs may appear as cancelled)
434
- const billingCheck = await checkForBillingLimitError(owner, repo, prNumber, verbose);
435
- if (billingCheck.isBillingLimitError) {
436
- blockers.push({
437
- type: 'billing_limit',
438
- message: 'GitHub Actions billing/spending limit reached',
439
- details: billingCheck.affectedJobs,
440
- allJobsAffected: billingCheck.allJobsAffected,
441
- billingMessage: billingCheck.message,
442
- });
443
- } else {
444
- // These need to be re-triggered, NOT treated as AI-fixable failures
445
- const cancelledOrStaleChecks = [...ciStatus.cancelledChecks, ...(ciStatus.staleChecks || [])];
446
- blockers.push({
447
- type: 'ci_cancelled',
448
- message: 'CI/CD checks were cancelled or became stale',
449
- details: cancelledOrStaleChecks.map(c => c.name),
450
- sha: ciStatus.sha,
451
- });
452
- }
453
- } else if (ciStatus.status === 'failure') {
454
- // Some checks genuinely failed - check if it's billing limits first
455
- const billingCheck = await checkForBillingLimitError(owner, repo, prNumber, verbose);
456
-
457
- if (billingCheck.isBillingLimitError) {
458
- blockers.push({
459
- type: 'billing_limit',
460
- message: 'GitHub Actions billing/spending limit reached',
461
- details: billingCheck.affectedJobs,
462
- allJobsAffected: billingCheck.allJobsAffected,
463
- billingMessage: billingCheck.message,
464
- });
465
- } else {
466
- // Check if there are also cancelled/stale checks alongside failures
467
- const cancelledOrStaleChecks = [...(ciStatus.hasCancelled ? ciStatus.cancelledChecks : []), ...((ciStatus.hasStale && ciStatus.staleChecks) || [])];
468
- if (cancelledOrStaleChecks.length > 0) {
469
- blockers.push({
470
- type: 'ci_cancelled',
471
- message: 'Some CI/CD checks were cancelled or became stale (will be re-triggered)',
472
- details: cancelledOrStaleChecks.map(c => c.name),
473
- sha: ciStatus.sha,
474
- });
475
- }
476
- blockers.push({
477
- type: 'ci_failure',
478
- message: 'CI/CD checks are failing',
479
- details: ciStatus.failedChecks.map(c => c.name),
480
- });
481
- }
482
- } else if (ciStatus.status === 'unknown') {
483
- // Unable to determine CI status - treat as pending to be safe
484
- // Do NOT treat as mergeable (which would be incorrect)
485
- blockers.push({
486
- type: 'ci_pending',
487
- message: 'CI/CD status could not be determined (will retry)',
488
- details: [],
489
- });
490
- }
491
-
492
- // Check mergeability
493
- const mergeStatus = await checkPRMergeable(owner, repo, prNumber, verbose);
494
- if (!mergeStatus.mergeable) {
495
- blockers.push({
496
- type: 'not_mergeable',
497
- message: mergeStatus.reason || 'PR is not mergeable',
498
- details: [],
499
- });
500
- }
501
-
502
- return { blockers, ciStatus, noCiConfigured: false, noCiTriggered: false };
503
- };
53
+ // Import helper functions extracted for file size management (Issue #1593)
54
+ const autoMergeHelpers = await import('./solve.auto-merge-helpers.lib.mjs');
55
+ const { checkForExistingComment, checkForNonBotComments, getMergeBlockers } = autoMergeHelpers;
504
56
 
505
57
  /**
506
58
  * Main function: Watch and restart until PR becomes mergeable
@@ -599,6 +599,10 @@ async function executeAndUpdateMessage(ctx, startingMessage, commandName, args,
599
599
  result = await executeStartScreen(commandName, args);
600
600
  const match = result.success && (result.output.match(/session:\s*(\S+)/i) || result.output.match(/screen -R\s+(\S+)/));
601
601
  session = match ? match[1] : 'unknown';
602
+ // Issue #1586: Track non-isolation sessions with timeout-based expiry.
603
+ // These sessions cannot reliably detect completion (screen stays alive via
604
+ // `exec bash`), so hasActiveSessionForUrl() auto-expires them after 10 min.
605
+ // This prevents accidental duplicate commands within the timeout window.
602
606
  if (result.success && session !== 'unknown') trackSession(session, { chatId: ctx.chat.id, messageId: msgId, startTime: new Date(), url: args[0], command: commandName }, VERBOSE);
603
607
  }
604
608
  if (result.warning) return safeEdit(`⚠️ ${result.warning}`);