@link-assistant/hive-mind 1.21.1 → 1.21.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,13 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.21.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 586b84d: Add retry mechanism for GitHub 500 errors during repository clone
8
+
9
+ This change adds intelligent retry logic with exponential backoff to handle transient GitHub server errors during repository cloning operations.
10
+
3
11
  ## 1.21.1
4
12
 
5
13
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.21.1",
3
+ "version": "1.21.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",
@@ -884,56 +884,140 @@ Thank you!`;
884
884
  return { repoToClone, forkedRepo, upstreamRemote, prForkOwner: forkOwner };
885
885
  };
886
886
 
887
- // Clone repository and set up remotes
887
+ // Classify git clone errors to determine if they are retryable
888
+ export const classifyCloneError = errorOutput => {
889
+ const output = errorOutput.toLowerCase();
890
+
891
+ // Transient server errors (5xx) - typically retryable
892
+ if (output.includes('error: 500') || output.includes('internal server error') || output.includes('error: 502') || output.includes('error: 503') || output.includes('error: 504')) {
893
+ return { type: 'TRANSIENT', retryable: true, description: 'GitHub server error' };
894
+ }
895
+
896
+ // Network-related errors - typically retryable
897
+ if (output.includes('connection refused') || output.includes('connection timed out') || output.includes('connection reset') || output.includes('unable to connect') || output.includes('network is unreachable') || output.includes('ssl error')) {
898
+ return { type: 'NETWORK', retryable: true, description: 'Network connectivity issue' };
899
+ }
900
+
901
+ // Authentication/permission errors - not retryable
902
+ if (output.includes('error: 401') || output.includes('error: 403') || output.includes('authentication failed') || output.includes('permission denied')) {
903
+ return { type: 'PERMISSION', retryable: false, description: 'Authentication or permission error' };
904
+ }
905
+
906
+ // Repository not found - not retryable
907
+ if (output.includes('error: 404') || output.includes('not found') || output.includes('repository not found')) {
908
+ return { type: 'NOT_FOUND', retryable: false, description: 'Repository not found' };
909
+ }
910
+
911
+ // Rate limiting - retryable with backoff
912
+ if (output.includes('rate limit') || output.includes('too many requests') || output.includes('api rate limit exceeded')) {
913
+ return { type: 'RATE_LIMIT', retryable: true, description: 'Rate limit exceeded' };
914
+ }
915
+
916
+ // Default to retryable for unknown errors
917
+ return { type: 'UNKNOWN', retryable: true, description: 'Unknown error' };
918
+ };
919
+
920
+ // Clone repository and set up remotes with retry mechanism
888
921
  export const cloneRepository = async (repoToClone, tempDir, argv, owner, repo) => {
889
- // Clone the repository (or fork) using gh tool with authentication
890
- await log(`\n${formatAligned('📥', 'Cloning repository:', repoToClone)}`);
922
+ const maxRetries = 3;
923
+ const baseDelay = 2000; // Start with 2 seconds
891
924
 
892
- // Use 2>&1 to capture all output and filter "Cloning into" message
893
- const cloneResult = await $`gh repo clone ${repoToClone} ${tempDir} 2>&1`;
925
+ await log(`\n${formatAligned('📥', 'Cloning repository:', repoToClone)}`);
894
926
 
895
- // Verify clone was successful
896
- if (cloneResult.code !== 0) {
897
- const errorOutput = (cloneResult.stderr || cloneResult.stdout || 'Unknown error').toString().trim();
898
- await log('');
899
- await log(`${formatAligned('❌', 'CLONE FAILED', '')}`, { level: 'error' });
900
- await log('');
901
- await log(' 🔍 What happened:');
902
- await log(` Failed to clone repository ${repoToClone}`);
903
- await log('');
904
- await log(' 📦 Error details:');
905
- for (const line of errorOutput.split('\n')) {
906
- if (line.trim()) await log(` ${line}`);
927
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
928
+ if (attempt > 1) {
929
+ await log(`${formatAligned('⏳', 'Clone attempt:', `${attempt}/${maxRetries} (with retry logic)`)}`);
907
930
  }
908
- await log('');
909
- await log(' 💡 Common causes:');
910
- await log(" • Repository doesn't exist or is private");
911
- await log(' • No GitHub authentication');
912
- await log(' • Network connectivity issues');
913
- if (argv.fork) {
914
- await log(' Fork not ready yet (try again in a moment)');
931
+
932
+ // Use 2>&1 to capture all output and filter "Cloning into" message
933
+ const cloneResult = await $`gh repo clone ${repoToClone} ${tempDir} 2>&1`;
934
+
935
+ // Verify clone was successful
936
+ if (cloneResult.code === 0) {
937
+ await log(`${formatAligned('✅', 'Cloned to:', tempDir)}`);
938
+
939
+ // Verify and fix remote configuration
940
+ const remoteCheckResult = await $({ cwd: tempDir })`git remote -v 2>&1`;
941
+ if (!remoteCheckResult.stdout || !remoteCheckResult.stdout.toString().includes('origin')) {
942
+ await log(' Setting up git remote...', { verbose: true });
943
+ // Add origin remote manually
944
+ await $({ cwd: tempDir })`git remote add origin https://github.com/${repoToClone}.git 2>&1`;
945
+ }
946
+ return; // Success - exit function
915
947
  }
916
- await log('');
917
- await log(' 🔧 How to fix:');
918
- await log(' 1. Check authentication: gh auth status');
919
- await log(' 2. Login if needed: gh auth login');
920
- await log(` 3. Verify access: gh repo view ${owner}/${repo}`);
921
- if (argv.fork) {
922
- await log(` 4. Check fork: gh repo view ${repoToClone}`);
948
+
949
+ // Clone failed - analyze error and determine if retry is appropriate
950
+ const errorOutput = (cloneResult.stderr || cloneResult.stdout || 'Unknown error').toString().trim();
951
+
952
+ const errorClassification = classifyCloneError(errorOutput);
953
+
954
+ if (!errorClassification.retryable || attempt === maxRetries) {
955
+ // Non-retryable error or max retries reached - fail with detailed error
956
+ await log('');
957
+ await log(`${formatAligned('❌', 'CLONE FAILED', '')}`, { level: 'error' });
958
+ await log('');
959
+ await log(' 🔍 What happened:');
960
+ await log(` Failed to clone repository ${repoToClone}`);
961
+
962
+ if (!errorClassification.retryable) {
963
+ await log(` Error type: ${errorClassification.description} (not retryable)`);
964
+ } else {
965
+ await log(` Error type: ${errorClassification.description} (max retries exceeded)`);
966
+ }
967
+ await log('');
968
+ await log(' 📦 Error details:');
969
+ for (const line of errorOutput.split('\n')) {
970
+ if (line.trim()) await log(` ${line}`);
971
+ }
972
+ await log('');
973
+ await log(' 💡 Common causes:');
974
+ await log(" • Repository doesn't exist or is private");
975
+ await log(' • No GitHub authentication');
976
+ await log(' • Network connectivity issues');
977
+ if (errorClassification.type === 'TRANSIENT') {
978
+ await log(' • GitHub server issues (temporary)');
979
+ }
980
+ if (errorClassification.type === 'RATE_LIMIT') {
981
+ await log(' • API rate limiting exceeded');
982
+ }
983
+ if (argv.fork) {
984
+ await log(' • Fork not ready yet (try again in a moment)');
985
+ }
986
+ await log('');
987
+ await log(' 🔧 How to fix:');
988
+ await log(' 1. Check authentication: gh auth status');
989
+ await log(' 2. Login if needed: gh auth login');
990
+ await log(` 3. Verify access: gh repo view ${owner}/${repo}`);
991
+ if (argv.fork) {
992
+ await log(` 4. Check fork: gh repo view ${repoToClone}`);
993
+ }
994
+ if (errorClassification.type === 'TRANSIENT') {
995
+ await log(' 5. Wait a few minutes and retry (GitHub server issue)');
996
+ await log(' 6. Check GitHub status: https://www.githubstatus.com');
997
+ }
998
+ if (errorClassification.type === 'RATE_LIMIT') {
999
+ await log(' 5. Wait for rate limit to reset (check your quota)');
1000
+ await log(' 6. Use --token flag with different token if available');
1001
+ }
1002
+ await log('');
1003
+ await safeExit(1, 'Repository setup failed');
923
1004
  }
924
- await log('');
925
- await safeExit(1, 'Repository setup failed');
926
- }
927
1005
 
928
- await log(`${formatAligned('✅', 'Cloned to:', tempDir)}`);
1006
+ // Retryable error and we have attempts left
1007
+ const delay = baseDelay * Math.pow(2, attempt - 1); // Exponential backoff
1008
+ await log(`${formatAligned('⚠️', 'Clone failed:', errorClassification.description)}`);
1009
+ await log(`${formatAligned('⏳', 'Retrying:', `Waiting ${delay / 1000}s before attempt ${attempt + 1}/${maxRetries}...`)}`);
1010
+
1011
+ if (errorClassification.type === 'RATE_LIMIT') {
1012
+ await log(' 💡 Tip: Rate limiting detected - using longer delay');
1013
+ }
929
1014
 
930
- // Verify and fix remote configuration
931
- const remoteCheckResult = await $({ cwd: tempDir })`git remote -v 2>&1`;
932
- if (!remoteCheckResult.stdout || !remoteCheckResult.stdout.toString().includes('origin')) {
933
- await log(' Setting up git remote...', { verbose: true });
934
- // Add origin remote manually
935
- await $({ cwd: tempDir })`git remote add origin https://github.com/${repoToClone}.git 2>&1`;
1015
+ await new Promise(resolve => setTimeout(resolve, delay));
936
1016
  }
1017
+
1018
+ // This should never be reached due to the loop logic above
1019
+ await log(`${formatAligned('❌', 'UNEXPECTED ERROR:', 'Clone logic failed')}`);
1020
+ await safeExit(1, 'Repository setup failed');
937
1021
  };
938
1022
 
939
1023
  // Set up upstream remote and sync fork