@link-assistant/hive-mind 1.24.3 → 1.24.4

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,15 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.24.4
4
+
5
+ ### Patch Changes
6
+
7
+ - 40282f3: fix: escape '...' ellipsis in MarkdownV2 and retry on UNKNOWN merge state (Issue #1339)
8
+
9
+ Two root causes fixed:
10
+ 1. **MarkdownV2 escaping**: In `formatProgressMessage()`, literal '...' was appended in PR titles, error messages, and overflow lines. Telegram's MarkdownV2 requires '.' to be escaped as '\.' - unescaped periods caused 400 Bad Request errors on every message update during CI wait.
11
+ 2. **UNKNOWN merge state**: GitHub computes PR mergeability asynchronously, so initial queries may return `mergeStateStatus: 'UNKNOWN'`. The old code immediately skipped PRs in this state. Fixed by adding retry logic to `checkPRMergeable()` that retries up to 3 times with 5-second delays before giving up.
12
+
3
13
  ## 1.24.3
4
14
 
5
15
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.24.3",
3
+ "version": "1.24.4",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -400,6 +400,11 @@ export async function checkPRCIStatus(owner, repo, prNumber, verbose = false) {
400
400
 
401
401
  /**
402
402
  * Check if PR is mergeable
403
+ *
404
+ * Issue #1339: GitHub computes mergeability asynchronously. The first request may return
405
+ * mergeable: null and mergeStateStatus: 'UNKNOWN' while the computation is in progress.
406
+ * We retry up to 3 times with a 5-second delay between attempts to handle this case.
407
+ *
403
408
  * @param {string} owner - Repository owner
404
409
  * @param {string} repo - Repository name
405
410
  * @param {number} prNumber - Pull request number
@@ -407,46 +412,73 @@ export async function checkPRCIStatus(owner, repo, prNumber, verbose = false) {
407
412
  * @returns {Promise<{mergeable: boolean, reason: string|null}>}
408
413
  */
409
414
  export async function checkPRMergeable(owner, repo, prNumber, verbose = false) {
410
- try {
411
- const { stdout } = await exec(`gh pr view ${prNumber} --repo ${owner}/${repo} --json mergeable,mergeStateStatus`);
412
- const pr = JSON.parse(stdout.trim());
413
-
414
- const mergeable = pr.mergeable === 'MERGEABLE';
415
- let reason = null;
416
-
417
- if (!mergeable) {
418
- switch (pr.mergeStateStatus) {
419
- case 'BLOCKED':
420
- reason = 'PR is blocked (possibly by branch protection rules)';
421
- break;
422
- case 'BEHIND':
423
- reason = 'PR branch is behind the base branch';
424
- break;
425
- case 'DIRTY':
426
- reason = 'PR has merge conflicts';
427
- break;
428
- case 'UNSTABLE':
429
- reason = 'PR has failing required status checks';
430
- break;
431
- case 'DRAFT':
432
- reason = 'PR is a draft';
433
- break;
434
- default:
435
- reason = `Merge state: ${pr.mergeStateStatus || 'unknown'}`;
415
+ // Issue #1339: GitHub computes mergeability asynchronously. When mergeStateStatus is
416
+ // 'UNKNOWN', it means GitHub hasn't calculated the merge state yet. Retry a few times.
417
+ const MAX_UNKNOWN_RETRIES = 3;
418
+ const UNKNOWN_RETRY_DELAY_MS = 5000;
419
+
420
+ for (let attempt = 0; attempt < MAX_UNKNOWN_RETRIES; attempt++) {
421
+ try {
422
+ const { stdout } = await exec(`gh pr view ${prNumber} --repo ${owner}/${repo} --json mergeable,mergeStateStatus`);
423
+ const pr = JSON.parse(stdout.trim());
424
+
425
+ // Issue #1339: If mergeStateStatus is 'UNKNOWN', GitHub is still computing.
426
+ // Wait and retry instead of immediately skipping the PR.
427
+ if (pr.mergeStateStatus === 'UNKNOWN' || pr.mergeable === null) {
428
+ if (attempt < MAX_UNKNOWN_RETRIES - 1) {
429
+ if (verbose) {
430
+ console.log(`[VERBOSE] /merge: PR #${prNumber} mergeability is UNKNOWN (attempt ${attempt + 1}/${MAX_UNKNOWN_RETRIES}), retrying in ${UNKNOWN_RETRY_DELAY_MS / 1000}s...`);
431
+ }
432
+ await new Promise(resolve => setTimeout(resolve, UNKNOWN_RETRY_DELAY_MS));
433
+ continue;
434
+ }
435
+ // All retries exhausted, still UNKNOWN - treat as not mergeable
436
+ if (verbose) {
437
+ console.log(`[VERBOSE] /merge: PR #${prNumber} mergeability still UNKNOWN after ${MAX_UNKNOWN_RETRIES} attempts`);
438
+ }
439
+ return { mergeable: false, reason: `Merge state: UNKNOWN (GitHub could not compute mergeability after ${MAX_UNKNOWN_RETRIES} attempts)` };
436
440
  }
437
- }
438
441
 
439
- if (verbose) {
440
- console.log(`[VERBOSE] /merge: PR #${prNumber} mergeable: ${mergeable}, state: ${pr.mergeStateStatus}`);
441
- }
442
+ const mergeable = pr.mergeable === 'MERGEABLE';
443
+ let reason = null;
444
+
445
+ if (!mergeable) {
446
+ switch (pr.mergeStateStatus) {
447
+ case 'BLOCKED':
448
+ reason = 'PR is blocked (possibly by branch protection rules)';
449
+ break;
450
+ case 'BEHIND':
451
+ reason = 'PR branch is behind the base branch';
452
+ break;
453
+ case 'DIRTY':
454
+ reason = 'PR has merge conflicts';
455
+ break;
456
+ case 'UNSTABLE':
457
+ reason = 'PR has failing required status checks';
458
+ break;
459
+ case 'DRAFT':
460
+ reason = 'PR is a draft';
461
+ break;
462
+ default:
463
+ reason = `Merge state: ${pr.mergeStateStatus || 'unknown'}`;
464
+ }
465
+ }
442
466
 
443
- return { mergeable, reason };
444
- } catch (error) {
445
- if (verbose) {
446
- console.log(`[VERBOSE] /merge: Error checking mergeability: ${error.message}`);
467
+ if (verbose) {
468
+ console.log(`[VERBOSE] /merge: PR #${prNumber} mergeable: ${mergeable}, state: ${pr.mergeStateStatus}`);
469
+ }
470
+
471
+ return { mergeable, reason };
472
+ } catch (error) {
473
+ if (verbose) {
474
+ console.log(`[VERBOSE] /merge: Error checking mergeability: ${error.message}`);
475
+ }
476
+ return { mergeable: false, reason: error.message };
447
477
  }
448
- return { mergeable: false, reason: error.message };
449
478
  }
479
+
480
+ // Should not reach here, but return safe default
481
+ return { mergeable: false, reason: 'Merge state: UNKNOWN' };
450
482
  }
451
483
 
452
484
  /**
@@ -520,7 +520,7 @@ export class MergeQueueProcessor {
520
520
  const elapsedSec = Math.round(this.targetBranchCIStatus.elapsedMs / 1000);
521
521
  const elapsedMin = Math.floor(elapsedSec / 60);
522
522
  const elapsedSecRemainder = elapsedSec % 60;
523
- message += `⏱️ Waiting for ${this.targetBranchCIStatus.count} CI run\\(s\\) on target branch to complete \\(${elapsedMin}m ${elapsedSecRemainder}s\\)\\.\\.\\.\\n\\n`;
523
+ message += `⏱️ Waiting for ${this.targetBranchCIStatus.count} CI run\\(s\\) on target branch to complete \\(${elapsedMin}m ${elapsedSecRemainder}s\\)\\.\\.\\.\n\n`;
524
524
  } else if (this.waitingForTargetBranchCI) {
525
525
  message += `⏱️ Checking for active CI runs on target branch\\.\\.\\.\n\n`;
526
526
  }
@@ -528,7 +528,8 @@ export class MergeQueueProcessor {
528
528
  // Current item being processed
529
529
  if (update.current && !this.waitingForTargetBranchCI) {
530
530
  const statusEmoji = update.currentStatus === MergeItemStatus.WAITING_CI ? '⏱️' : '🔄';
531
- message += `${statusEmoji} ${update.current}\n\n`;
531
+ // Issue #1339: escape the current item description for MarkdownV2
532
+ message += `${statusEmoji} ${this.escapeMarkdown(update.current)}\n\n`;
532
533
  }
533
534
 
534
535
  // Show errors/failures/skips inline so user gets immediate feedback (Issue #1269, #1294)
@@ -538,10 +539,12 @@ export class MergeQueueProcessor {
538
539
  message += `⚠️ *Issues:*\n`;
539
540
  for (const item of problemItems.slice(0, 5)) {
540
541
  const statusEmoji = item.status === MergeItemStatus.FAILED ? '❌' : '⏭️';
541
- message += ` ${statusEmoji} \\#${item.prNumber}: ${this.escapeMarkdown(item.error.substring(0, 50))}${item.error.length > 50 ? '...' : ''}\n`;
542
+ // Issue #1339: escape the ellipsis '...' for MarkdownV2 (periods are reserved)
543
+ message += ` ${statusEmoji} \\#${item.prNumber}: ${this.escapeMarkdown(item.error.substring(0, 50))}${item.error.length > 50 ? '\\.\\.\\.' : ''}\n`;
542
544
  }
543
545
  if (problemItems.length > 5) {
544
- message += ` _...and ${problemItems.length - 5} more issues_\n`;
546
+ // Issue #1339: escape the ellipsis '...' for MarkdownV2
547
+ message += ` _\\.\\.\\.and ${problemItems.length - 5} more issues_\n`;
545
548
  }
546
549
  message += '\n';
547
550
  }
@@ -549,11 +552,13 @@ export class MergeQueueProcessor {
549
552
  // PRs list with emojis
550
553
  message += `*Queue:*\n`;
551
554
  for (const item of update.items.slice(0, 10)) {
552
- message += `${item.emoji} \\#${item.prNumber}: ${this.escapeMarkdown(item.title.substring(0, 35))}${item.title.length > 35 ? '...' : ''}\n`;
555
+ // Issue #1339: escape the ellipsis '...' for MarkdownV2 (periods are reserved)
556
+ message += `${item.emoji} \\#${item.prNumber}: ${this.escapeMarkdown(item.title.substring(0, 35))}${item.title.length > 35 ? '\\.\\.\\.' : ''}\n`;
553
557
  }
554
558
 
555
559
  if (update.items.length > 10) {
556
- message += `_...and ${update.items.length - 10} more_\n`;
560
+ // Issue #1339: escape the ellipsis '...' for MarkdownV2
561
+ message += `_\\.\\.\\.and ${update.items.length - 10} more_\n`;
557
562
  }
558
563
 
559
564
  return message;