@link-assistant/hive-mind 1.22.4 → 1.22.5

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,16 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.22.5
4
+
5
+ ### Patch Changes
6
+
7
+ - fdd8eaa: Fix auto-merge failure in fork mode with permission pre-check (Issue #1226)
8
+ - Add fork-mode guard in `startAutoRestartUntilMergable()` to detect when `--auto-merge` cannot work
9
+ - Add `checkMergePermissions()` function to verify write/push/admin/maintain access before merge attempts
10
+ - Add permission pre-check in `attemptAutoMerge()` to fail fast when user lacks write access
11
+ - Post "Ready to merge" comment to PR when auto-merge cannot be performed due to permissions
12
+ - Prevent silent failures and infinite restart loops in fork mode scenarios
13
+
3
14
  ## 1.22.4
4
15
 
5
16
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.22.4",
3
+ "version": "1.22.5",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -433,6 +433,37 @@ export async function checkPRMergeable(owner, repo, prNumber, verbose = false) {
433
433
  }
434
434
  }
435
435
 
436
+ /**
437
+ * Check if the authenticated user has write/merge permissions on the repository
438
+ * @param {string} owner - Repository owner
439
+ * @param {string} repo - Repository name
440
+ * @param {boolean} verbose - Whether to log verbose output
441
+ * @returns {Promise<{canMerge: boolean, permission: string|null}>}
442
+ */
443
+ export async function checkMergePermissions(owner, repo, verbose = false) {
444
+ try {
445
+ const { stdout } = await exec(`gh api repos/${owner}/${repo} --jq '.permissions'`);
446
+ const permissions = JSON.parse(stdout.trim());
447
+
448
+ const canMerge = permissions.admin === true || permissions.maintain === true || permissions.push === true;
449
+
450
+ if (verbose) {
451
+ console.log(`[VERBOSE] /merge: Merge permissions for ${owner}/${repo}: push=${permissions.push}, admin=${permissions.admin}, maintain=${permissions.maintain}`);
452
+ console.log(`[VERBOSE] /merge: Can merge: ${canMerge}`);
453
+ }
454
+
455
+ return {
456
+ canMerge,
457
+ permission: permissions.admin ? 'admin' : permissions.maintain ? 'maintain' : permissions.push ? 'push' : 'read',
458
+ };
459
+ } catch (error) {
460
+ if (verbose) {
461
+ console.log(`[VERBOSE] /merge: Error checking merge permissions: ${error.message}`);
462
+ }
463
+ return { canMerge: false, permission: null };
464
+ }
465
+ }
466
+
436
467
  /**
437
468
  * Merge a pull request
438
469
  * @param {string} owner - Repository owner
@@ -600,6 +631,7 @@ export default {
600
631
  getAllReadyPRs,
601
632
  checkPRCIStatus,
602
633
  checkPRMergeable,
634
+ checkMergePermissions,
603
635
  mergePullRequest,
604
636
  waitForCI,
605
637
  parseRepositoryUrl,
@@ -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 { checkPRCIStatus, checkPRMergeable, mergePullRequest, waitForCI } = githubMergeLib;
36
+ const { checkPRCIStatus, checkPRMergeable, checkMergePermissions, mergePullRequest, waitForCI } = githubMergeLib;
37
37
 
38
38
  // Import GitHub functions for log attachment
39
39
  const githubLib = await import('./github.lib.mjs');
@@ -506,6 +506,13 @@ export const attemptAutoMerge = async params => {
506
506
  await log('');
507
507
  await log(formatAligned('🔀', 'AUTO-MERGE:', 'Checking if PR can be merged...'));
508
508
 
509
+ // Issue #1226: Check merge permissions before attempting
510
+ const { canMerge, permission } = await checkMergePermissions(owner, repo, argv.verbose);
511
+ if (!canMerge) {
512
+ await log(formatAligned('⚠️', 'Cannot merge:', `Insufficient permissions (${permission || 'unknown'})`, 2));
513
+ return { success: false, reason: 'insufficient_permissions', error: `User has ${permission || 'unknown'} access, needs push/maintain/admin` };
514
+ }
515
+
509
516
  // Wait for CI to complete (with timeout)
510
517
  const ciWaitResult = await waitForCI(
511
518
  owner,
@@ -564,7 +571,7 @@ export const attemptAutoMerge = async params => {
564
571
  * Start auto-restart-until-mergable mode
565
572
  */
566
573
  export const startAutoRestartUntilMergable = async params => {
567
- const { argv } = params;
574
+ const { argv, owner, repo, prNumber } = params;
568
575
 
569
576
  // Determine the mode
570
577
  const isAutoMerge = argv.autoMerge || false;
@@ -574,13 +581,57 @@ export const startAutoRestartUntilMergable = async params => {
574
581
  return null; // Neither mode enabled
575
582
  }
576
583
 
577
- if (!params.prNumber) {
584
+ if (!prNumber) {
578
585
  await log('');
579
586
  await log(formatAligned('⚠️', 'Auto-restart-until-mergable:', 'Requires a pull request'));
580
587
  await log(formatAligned('', 'Note:', 'This mode only works with existing PRs', 2));
581
588
  return null;
582
589
  }
583
590
 
591
+ // Issue #1226: Check if running in fork mode — auto-merge cannot work without write access
592
+ if (argv.fork && isAutoMerge) {
593
+ await log('');
594
+ await log(formatAligned('⚠️', 'Auto-merge:', 'Cannot auto-merge fork PRs'));
595
+ await log(formatAligned('', 'Reason:', 'Fork contributors do not have write access to merge PRs to upstream repositories', 2));
596
+ await log(formatAligned('', 'Action:', 'PR is ready for manual merge by a repository maintainer', 2));
597
+ await log('');
598
+
599
+ // Post a comment to the PR notifying the maintainer
600
+ try {
601
+ const commentBody = `## ✅ Ready to merge\n\nThis pull request is ready to be merged. Auto-merge was requested (\`--auto-merge\`) but cannot be performed because this PR was created from a fork (no write access to the target repository).\n\nPlease merge manually.\n\n---\n*hive-mind with --auto-merge flag (fork mode)*`;
602
+ await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${commentBody}`;
603
+ await log(formatAligned('', '💬 Posted merge readiness notification to PR', '', 2));
604
+ } catch {
605
+ // Don't fail if comment posting fails
606
+ }
607
+
608
+ return { success: false, reason: 'fork_no_write_access' };
609
+ }
610
+
611
+ // Issue #1226: Verify merge permissions before entering the auto-merge/restart loop
612
+ if (isAutoMerge && owner && repo) {
613
+ const { canMerge, permission } = await checkMergePermissions(owner, repo, argv.verbose);
614
+ if (!canMerge) {
615
+ await log('');
616
+ await log(formatAligned('⚠️', 'Auto-merge:', 'Insufficient permissions to merge'));
617
+ await log(formatAligned('', 'Permission level:', permission || 'unknown', 2));
618
+ await log(formatAligned('', 'Required:', 'push, maintain, or admin access', 2));
619
+ await log(formatAligned('', 'Action:', 'PR is ready for manual merge by a repository maintainer', 2));
620
+ await log('');
621
+
622
+ // Post a comment to the PR notifying the maintainer
623
+ try {
624
+ const commentBody = `## ✅ Ready to merge\n\nThis pull request is ready to be merged. Auto-merge was requested (\`--auto-merge\`) but cannot be performed because the authenticated user lacks write access to \`${owner}/${repo}\` (current permission: \`${permission || 'unknown'}\`).\n\nPlease merge manually.\n\n---\n*hive-mind with --auto-merge flag*`;
625
+ await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${commentBody}`;
626
+ await log(formatAligned('', '💬 Posted merge readiness notification to PR', '', 2));
627
+ } catch {
628
+ // Don't fail if comment posting fails
629
+ }
630
+
631
+ return { success: false, reason: 'insufficient_permissions' };
632
+ }
633
+ }
634
+
584
635
  // If --auto-merge implies --auto-restart-until-mergable
585
636
  if (isAutoMerge) {
586
637
  argv.autoRestartUntilMergable = true;