@link-assistant/hive-mind 1.46.1 → 1.46.2

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,19 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.46.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 37daeb7: Auto-recover from non-fork repositories during fork validation (Issue #1518)
8
+ - When a repository exists but is NOT a proper GitHub fork (or has wrong parent), safely auto-recover by comparing commits against upstream first — only delete and re-fork if no additional commits would be lost
9
+ - Add verbose logging of fork commands for debugging non-fork creation scenarios
10
+ - Add post-creation fork validation to detect non-fork repos immediately after `gh repo fork`
11
+ - Report non-fork creation to Sentry for monitoring
12
+ - Add `--allow-force-non-fork-repository-deletion` flag to force deletion even when additional commits would be lost
13
+ - Add case study documenting the root cause analysis of konard/MixaByk1996-elements-app
14
+ - Document all previously undocumented solve options in CONFIGURATION.md (12 options including --allow-force-non-fork-repository-deletion)
15
+ - Add CI/CD test to verify documentation stays in sync with code options (prevents drift)
16
+
3
17
  ## 1.46.1
4
18
 
5
19
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.46.1",
3
+ "version": "1.46.2",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -199,6 +199,7 @@ const KNOWN_OPTION_NAMES = [
199
199
  'only-prepare-command',
200
200
  'auto-merge-default-branch-to-pull-request-branch',
201
201
  'allow-fork-divergence-resolution-using-force-push-with-lease',
202
+ 'allow-force-non-fork-repository-deletion',
202
203
  'allow-to-push-to-contributors-pull-requests-as-maintainer',
203
204
  'prefix-fork-name-with-owner-name',
204
205
  'auto-restart-max-iterations',
@@ -273,6 +273,11 @@ export const SOLVE_OPTION_DEFINITIONS = {
273
273
  description: 'Allow automatic force-push (--force-with-lease) when fork diverges from upstream (DANGEROUS: can overwrite fork history)',
274
274
  default: false,
275
275
  },
276
+ 'allow-force-non-fork-repository-deletion': {
277
+ type: 'boolean',
278
+ description: 'Allow deletion of non-fork repositories even when they contain additional commits that would be lost (DANGEROUS: data loss possible)',
279
+ default: false,
280
+ },
276
281
  'allow-to-push-to-contributors-pull-requests-as-maintainer': {
277
282
  type: 'boolean',
278
283
  description: 'When continuing a fork PR as a maintainer, attempt to push directly to the contributor\'s fork if "Allow edits by maintainers" is enabled. Requires --auto-fork to be enabled.',
@@ -490,78 +490,77 @@ export const setupRepository = async (argv, owner, repo, forkOwner = null, issue
490
490
 
491
491
  const forkValidation = await validateForkParent(existingForkName, `${owner}/${repo}`);
492
492
 
493
- if (!forkValidation.isValid) {
493
+ if (forkValidation.isValid) {
494
+ // Fork is valid — use it
495
+ await log(`${formatAligned('✅', 'Fork parent validated:', `${forkValidation.parent}`)}`);
496
+ repoToClone = existingForkName;
497
+ forkedRepo = existingForkName;
498
+ upstreamRemote = `${owner}/${repo}`;
499
+ } else if (forkValidation.isNetworkError) {
494
500
  // Issue #1311: Handle network errors separately from fork mismatch errors
495
- if (forkValidation.isNetworkError) {
496
- await log('');
497
- await log(`${formatAligned('❌', 'NETWORK ERROR DURING FORK VALIDATION', '')}`, { level: 'error' });
498
- await log('');
499
- await log(' 🔍 What happened:');
500
- await log(` Failed to connect to GitHub API while validating fork.`);
501
- await log(` Error: ${forkValidation.error}`);
502
- await log('');
503
- await log(' 💡 This is likely a temporary network issue. You can:');
504
- await log(' 1. Wait a moment and try again');
505
- await log(' 2. Check your internet connection');
506
- await log(' 3. Check GitHub status: https://www.githubstatus.com/');
507
- await log('');
508
- await log(' Or use --no-fork to skip fork validation if you have write access.');
509
- await log('');
510
- await safeExit(1, 'Network error during fork validation - please retry');
511
- }
512
-
513
- // Fork parent mismatch detected - this prevents issue #967
514
501
  await log('');
515
- await log(`${formatAligned('❌', 'FORK PARENT MISMATCH DETECTED', '')}`, { level: 'error' });
502
+ await log(`${formatAligned('❌', 'NETWORK ERROR DURING FORK VALIDATION', '')}`, { level: 'error' });
516
503
  await log('');
517
504
  await log(' 🔍 What happened:');
518
- if (!forkValidation.isFork) {
519
- await log(` The repository ${existingForkName} is NOT a GitHub fork.`);
520
- await log(' It may have been created by cloning and pushing instead of forking.');
521
- } else {
522
- await log(` Your fork ${existingForkName} was created from an intermediate fork,`);
523
- await log(` not directly from the target repository ${owner}/${repo}.`);
524
- }
525
- await log('');
526
- await log(' 📦 Fork relationship:');
527
- await log(` • Your fork: ${existingForkName}`);
528
- await log(` • Fork parent: ${forkValidation.parent || 'N/A (not a fork)'}`);
529
- await log(` • Fork source (root): ${forkValidation.source || 'N/A'}`);
530
- await log(` • Expected parent: ${owner}/${repo}`);
531
- await log('');
532
- await log(' ⚠️ Why this is a problem:');
533
- await log(' When a fork is created from an intermediate fork (a "fork of a fork"),');
534
- await log(' any commits that exist in the intermediate fork but not in the target');
535
- await log(' repository will be included in your pull requests. This can result in');
536
- await log(' pull requests with hundreds or thousands of unexpected commits.');
505
+ await log(` Failed to connect to GitHub API while validating fork.`);
506
+ await log(` Error: ${forkValidation.error}`);
537
507
  await log('');
538
- await log(' 📖 Case study: See issue #967');
539
- await log(' A fork created from veb86/zcadvelecAI (which had 1,678 extra commits)');
540
- await log(' instead of zamtmn/zcad resulted in a PR with 1,681 commits');
541
- await log(' instead of the expected 3 commits.');
508
+ await log(' 💡 This is likely a temporary network issue. You can:');
509
+ await log(' 1. Wait a moment and try again');
510
+ await log(' 2. Check your internet connection');
511
+ await log(' 3. Check GitHub status: https://www.githubstatus.com/');
542
512
  await log('');
543
- await log(' 💡 How to fix:');
513
+ await log(' Or use --no-fork to skip fork validation if you have write access.');
544
514
  await log('');
545
- await log(' Option 1: Delete the problematic fork and create a fresh one');
546
- await log(` gh repo delete ${existingForkName}`);
547
- await log(` Then run this command again to create a proper fork of ${owner}/${repo}`);
548
- await log('');
549
- await log(' Option 2: Use --prefix-fork-name-with-owner-name to create a new fork');
550
- await log(` This creates a fork named ${currentUser}/${owner}-${repo} instead`);
551
- await log(` ./solve.mjs "${issueUrl || `https://github.com/${owner}/${repo}/issues/<number>`}" --prefix-fork-name-with-owner-name --fork`);
552
- await log('');
553
- await log(' Option 3: Work directly on the repository (if you have write access)');
554
- await log(` ./solve.mjs "${issueUrl || `https://github.com/${owner}/${repo}/issues/<number>`}" --no-fork`);
515
+ await safeExit(1, 'Network error during fork validation - please retry');
516
+ } else {
517
+ // Issue #1518: Auto-recovery delete non-fork/mismatched repo and re-fork, but only if no commits would be lost
555
518
  await log('');
556
-
557
- await safeExit(1, 'Fork parent mismatch - fork was created from intermediate fork');
519
+ await log(`${formatAligned('⚠️', 'FORK PARENT MISMATCH DETECTED', '')}`, { level: 'warning' });
520
+ const detail = !forkValidation.isFork ? `Repository ${existingForkName} is NOT a GitHub fork (see issue #1518)` : `Fork ${existingForkName} was created from ${forkValidation.parent} instead of ${owner}/${repo} (see issue #967)`;
521
+ await log(`${formatAligned('', '', detail)}`);
522
+ await log(`${formatAligned('', '', `Fork parent: ${forkValidation.parent || 'N/A (not a fork)'}, source: ${forkValidation.source || 'N/A'}, expected: ${owner}/${repo}`)}`);
523
+ // Safety check: compare commits before deleting to avoid data loss
524
+ await log(`${formatAligned('🔍', 'Safety check:', 'Comparing commits against upstream...')}`);
525
+ let safeToDelete = false;
526
+ try {
527
+ const cmp = await $`gh api repos/${owner}/${repo}/compare/${owner}:HEAD...${existingForkName.split('/')[0]}:HEAD --jq '.ahead_by' 2>&1`;
528
+ if (cmp.code === 0 && parseInt(cmp.stdout.toString().trim(), 10) === 0) {
529
+ await log(`${formatAligned('✅', 'Safe to delete:', 'No additional commits in non-fork repository')}`);
530
+ safeToDelete = true;
531
+ } else if (cmp.code === 0) {
532
+ await log(`${formatAligned('⚠️', 'UNSAFE:', `Repository has ${cmp.stdout.toString().trim()} commit(s) ahead of upstream that would be lost`)}`, { level: 'warning' });
533
+ } else {
534
+ await log(`${formatAligned('⚠️', 'Compare failed:', ((cmp.stderr?.toString() || '') + (cmp.stdout?.toString() || '')).split('\n')[0])}`, { level: 'warning' });
535
+ }
536
+ } catch (e) {
537
+ await log(`${formatAligned('⚠️', 'Compare error:', e.message)}`, { level: 'warning' });
538
+ }
539
+ if (!safeToDelete) {
540
+ if (argv.allowForceNonForkRepositoryDeletion) {
541
+ await log(`${formatAligned('⚠️', 'Force deletion ENABLED:', '--allow-force-non-fork-repository-deletion — proceeding despite potential data loss')}`, { level: 'warning' });
542
+ safeToDelete = true;
543
+ } else {
544
+ await log(` 💡 Manual fix required: back up work, then: gh repo delete ${existingForkName} --yes`);
545
+ await log(` Then run this command again to create a proper fork of ${owner}/${repo}`);
546
+ await log(` 🔧 Or force deletion (DANGEROUS): solve ${argv.url || argv['issue-url'] || argv._[0] || '<issue-url>'} --allow-force-non-fork-repository-deletion`);
547
+ await safeExit(1, 'Auto-recovery skipped - repository may contain commits that would be lost');
548
+ }
549
+ }
550
+ await log(`${formatAligned('🔄', 'Auto-recovery:', 'Deleting non-fork repository and creating fresh fork...')}`);
551
+ const deleteResult = await $`gh repo delete ${existingForkName} --yes 2>&1`;
552
+ if (deleteResult.code !== 0) {
553
+ const delOut = (deleteResult.stderr?.toString() || '') + (deleteResult.stdout?.toString() || '');
554
+ await log(`${formatAligned('❌', 'Delete failed:', delOut.split('\n')[0])}`, { level: 'error' });
555
+ await log(` 💡 Manual fix: gh repo delete ${existingForkName} --yes, then re-run`);
556
+ await safeExit(1, 'Auto-recovery failed - could not delete problematic repository');
557
+ }
558
+ await log(`${formatAligned('✅', 'Deleted:', existingForkName)}`);
559
+ existingForkName = null; // Fall through to fork creation below
558
560
  }
561
+ }
559
562
 
560
- await log(`${formatAligned('✅', 'Fork parent validated:', `${forkValidation.parent}`)}`);
561
- repoToClone = existingForkName;
562
- forkedRepo = existingForkName;
563
- upstreamRemote = `${owner}/${repo}`;
564
- } else {
563
+ if (!existingForkName) {
565
564
  // Need to create fork with retry logic for concurrent scenarios
566
565
  await log(`${formatAligned('🔄', 'Creating fork...', '')}`);
567
566
 
@@ -575,19 +574,18 @@ export const setupRepository = async (argv, owner, repo, forkOwner = null, issue
575
574
  let actualForkName = `${currentUser}/${defaultForkName}`;
576
575
 
577
576
  for (let attempt = 1; attempt <= maxForkRetries; attempt++) {
578
- // Try to create fork with optional custom name
579
577
  let forkResult;
578
+ // Issue #1518: Log the exact fork command for debugging non-fork creation scenarios
579
+ if (argv.verbose) await log(`${formatAligned('🔧', 'Fork command:', argv.prefixForkNameWithOwnerName ? `gh repo fork ${owner}/${repo} --fork-name ${owner}-${repo} --clone=false` : `gh repo fork ${owner}/${repo} --clone=false`)}`);
580
580
  if (argv.prefixForkNameWithOwnerName) {
581
- // Use --fork-name flag to create fork with owner prefix
582
581
  forkResult = await $`gh repo fork ${owner}/${repo} --fork-name ${owner}-${repo} --clone=false 2>&1`;
583
582
  } else {
584
- // Standard fork creation (no custom name)
585
583
  forkResult = await $`gh repo fork ${owner}/${repo} --clone=false 2>&1`;
586
584
  }
587
585
 
588
586
  // Always capture output to parse actual fork name
589
587
  const forkOutput = (forkResult.stderr ? forkResult.stderr.toString() : '') + (forkResult.stdout ? forkResult.stdout.toString() : '');
590
-
588
+ if (argv.verbose) await log(`${formatAligned('🔧', 'Fork output:', forkOutput.split('\n')[0] || '(empty)')}`); // Issue #1518
591
589
  // Parse actual fork name from output (e.g., "konard/netkeep80-jsonRVM already exists")
592
590
  // GitHub may create forks with modified names to avoid conflicts
593
591
  // Use regex that won't match domain names like "github.com/user" -> "com/user"
@@ -792,11 +790,20 @@ Thank you!`;
792
790
  await safeExit(1, 'Repository setup failed');
793
791
  }
794
792
 
795
- // Wait a moment for fork to be fully ready
796
793
  if (forkCreated) {
794
+ // Wait a moment for fork to be fully ready
797
795
  await log(`${formatAligned('⏳', 'Waiting:', 'For fork to be fully ready...')}`);
798
796
  await new Promise(resolve => setTimeout(resolve, 3000));
799
797
  }
798
+ // Issue #1518: Validate fork parent after creation/discovery to detect non-fork repos early (covers concurrent worker scenarios too)
799
+ await log(`${formatAligned('🔍', 'Validating fork parent...', '')}`);
800
+ const pcv = await validateForkParent(actualForkName, `${owner}/${repo}`);
801
+ if (pcv.isValid) {
802
+ await log(`${formatAligned('✅', 'Fork parent validated:', `${pcv.parent}`)}`);
803
+ } else if (!pcv.isNetworkError) {
804
+ await log(`${formatAligned('⚠️', 'WARNING:', `Fork failed validation (possible gh CLI bug, see issue #1518): ${pcv.error}`)}`, { level: 'warning' });
805
+ reportError(new Error(`Fork created as non-fork: ${pcv.error}`), { context: 'fork_creation_validation', forkRepo: actualForkName, expectedUpstream: `${owner}/${repo}`, isFork: pcv.isFork, parent: pcv.parent, source: pcv.source });
806
+ }
800
807
  }
801
808
 
802
809
  repoToClone = actualForkName;