@deep-assistant/hive-mind
Version:
AI-powered issue solver and hive mind for collaborative problem solving
403 lines (351 loc) โข 18.3 kB
JavaScript
// Repository management module for solve command
// Extracted from solve.mjs to keep files under 1500 lines
// Use use-m to dynamically import modules for cross-runtime compatibility
// Check if use is already defined globally (when imported from solve.mjs)
// If not, fetch it (when running standalone)
if (typeof globalThis.use === 'undefined') {
globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
}
const use = globalThis.use;
// Use command-stream for consistent $ behavior across runtimes
const { $ } = await use('command-stream');
const os = (await use('os')).default;
const path = (await use('path')).default;
const fs = (await use('fs')).promises;
// Import shared library functions
const lib = await import('./lib.mjs');
// Import Sentry integration
const sentryLib = await import('./sentry.lib.mjs');
const { reportError } = sentryLib;
const {
log,
formatAligned
} = lib;
// Import exit handler
import { safeExit } from './exit-handler.lib.mjs';
// Create or find temporary directory for cloning the repository
export const setupTempDirectory = async (argv) => {
let tempDir;
let isResuming = argv.resume;
if (isResuming) {
// When resuming, try to find existing directory or create a new one
const scriptDir = path.dirname(process.argv[1]);
const sessionLogPattern = path.join(scriptDir, `${argv.resume}.log`);
try {
// Check if session log exists to verify session is valid
await fs.access(sessionLogPattern);
await log(`๐ Resuming session ${argv.resume} (session log found)`);
// For resumed sessions, create new temp directory since old one may be cleaned up
tempDir = path.join(os.tmpdir(), `gh-issue-solver-resume-${argv.resume}-${Date.now()}`);
await fs.mkdir(tempDir, { recursive: true });
await log(`Creating new temporary directory for resumed session: ${tempDir}`);
} catch (err) {
reportError(err, {
context: 'resume_session_lookup',
sessionId: argv.resume,
operation: 'find_session_log'
});
await log(`Warning: Session log for ${argv.resume} not found, but continuing with resume attempt`);
tempDir = path.join(os.tmpdir(), `gh-issue-solver-resume-${argv.resume}-${Date.now()}`);
await fs.mkdir(tempDir, { recursive: true });
await log(`Creating temporary directory for resumed session: ${tempDir}`);
}
} else {
tempDir = path.join(os.tmpdir(), `gh-issue-solver-${Date.now()}`);
await fs.mkdir(tempDir, { recursive: true });
await log(`\nCreating temporary directory: ${tempDir}`);
}
return { tempDir, isResuming };
};
// Handle fork creation and repository setup
export const setupRepository = async (argv, owner, repo) => {
let repoToClone = `${owner}/${repo}`;
let forkedRepo = null;
let upstreamRemote = null;
if (argv.fork) {
await log(`\n${formatAligned('๐ด', 'Fork mode:', 'ENABLED')}`);
await log(`${formatAligned('', 'Checking fork status...', '')}\n`);
// Get current user
const userResult = await $`gh api user --jq .login`;
if (userResult.code !== 0) {
await log(`${formatAligned('โ', 'Error:', 'Failed to get current user')}`);
await safeExit(1, 'Repository setup failed');
}
const currentUser = userResult.stdout.toString().trim();
// Check if fork already exists
const forkCheckResult = await $`gh repo view ${currentUser}/${repo} --json name 2>/dev/null`;
if (forkCheckResult.code === 0) {
// Fork exists
await log(`${formatAligned('โ
', 'Fork exists:', `${currentUser}/${repo}`)}`);
repoToClone = `${currentUser}/${repo}`;
forkedRepo = `${currentUser}/${repo}`;
upstreamRemote = `${owner}/${repo}`;
} else {
// Need to create fork with retry logic for concurrent scenarios
await log(`${formatAligned('๐', 'Creating fork...', '')}`);
const maxForkRetries = 5;
const baseDelay = 2000; // Start with 2 seconds
let forkCreated = false;
let forkExists = false;
for (let attempt = 1; attempt <= maxForkRetries; attempt++) {
// Try to create fork
const forkResult = await $`gh repo fork ${owner}/${repo} --clone=false 2>&1`;
if (forkResult.code === 0) {
// Fork successfully created
await log(`${formatAligned('โ
', 'Fork created:', `${currentUser}/${repo}`)}`);
forkCreated = true;
forkExists = true;
break;
} else {
// Fork creation failed - check if it's because fork already exists
const forkOutput = (forkResult.stderr ? forkResult.stderr.toString() : '') +
(forkResult.stdout ? forkResult.stdout.toString() : '');
if (forkOutput.includes('already exists') ||
forkOutput.includes('Name already exists') ||
forkOutput.includes('fork of') ||
forkOutput.includes('HTTP 422')) {
// Fork already exists (likely created by another concurrent worker)
await log(`${formatAligned('โน๏ธ', 'Fork exists:', 'Already created (likely by another worker)')}`);
forkExists = true;
break;
}
// Check if fork was created by another worker even if error message doesn't explicitly say so
await log(`${formatAligned('๐', 'Checking:', 'If fork exists after failed creation attempt...')}`);
const checkResult = await $`gh repo view ${currentUser}/${repo} --json name 2>/dev/null`;
if (checkResult.code === 0) {
// Fork exists now (created by another worker during our attempt)
await log(`${formatAligned('โ
', 'Fork found:', 'Created by another concurrent worker')}`);
forkExists = true;
break;
}
// Fork still doesn't exist and creation failed
if (attempt < maxForkRetries) {
const delay = baseDelay * Math.pow(2, attempt - 1); // Exponential backoff
await log(`${formatAligned('โณ', 'Retry:', `Attempt ${attempt}/${maxForkRetries} failed, waiting ${delay/1000}s before retry...`)}`);
await log(` Error: ${forkOutput.split('\n')[0]}`); // Show first line of error
await new Promise(resolve => setTimeout(resolve, delay));
} else {
// All retries exhausted
await log(`${formatAligned('โ', 'Error:', 'Failed to create fork after all retries')}`);
await log(forkOutput);
await safeExit(1, 'Repository setup failed');
}
}
}
// If fork exists (either created or already existed), verify it's accessible
if (forkExists) {
await log(`${formatAligned('๐', 'Verifying fork:', 'Checking accessibility...')}`);
// Verify fork with retries (GitHub may need time to propagate)
const maxVerifyRetries = 5;
let forkVerified = false;
for (let attempt = 1; attempt <= maxVerifyRetries; attempt++) {
const delay = baseDelay * Math.pow(2, attempt - 1);
if (attempt > 1) {
await log(`${formatAligned('โณ', 'Verifying fork:', `Attempt ${attempt}/${maxVerifyRetries} (waiting ${delay/1000}s)...`)}`);
await new Promise(resolve => setTimeout(resolve, delay));
}
const verifyResult = await $`gh repo view ${currentUser}/${repo} --json name 2>/dev/null`;
if (verifyResult.code === 0) {
forkVerified = true;
await log(`${formatAligned('โ
', 'Fork verified:', `${currentUser}/${repo} is accessible`)}`);
break;
}
}
if (!forkVerified) {
await log(`${formatAligned('โ', 'Error:', 'Fork exists but not accessible after multiple retries')}`);
await log(`${formatAligned('', 'Suggestion:', 'GitHub may be experiencing delays - try running the command again in a few minutes')}`);
await safeExit(1, 'Repository setup failed');
}
// Wait a moment for fork to be fully ready
if (forkCreated) {
await log(`${formatAligned('โณ', 'Waiting:', 'For fork to be fully ready...')}`);
await new Promise(resolve => setTimeout(resolve, 3000));
}
}
repoToClone = `${currentUser}/${repo}`;
forkedRepo = `${currentUser}/${repo}`;
upstreamRemote = `${owner}/${repo}`;
}
}
return { repoToClone, forkedRepo, upstreamRemote };
};
// Clone repository and set up remotes
export const cloneRepository = async (repoToClone, tempDir, argv, owner, repo) => {
// Clone the repository (or fork) using gh tool with authentication
await log(`\n${formatAligned('๐ฅ', 'Cloning repository:', repoToClone)}`);
// Use 2>&1 to capture all output and filter "Cloning into" message
const cloneResult = await $`gh repo clone ${repoToClone} ${tempDir} 2>&1`;
// Verify clone was successful
if (cloneResult.code !== 0) {
const errorOutput = (cloneResult.stderr || cloneResult.stdout || 'Unknown error').toString().trim();
await log('');
await log(`${formatAligned('โ', 'CLONE FAILED', '')}`, { level: 'error' });
await log('');
await log(' ๐ What happened:');
await log(` Failed to clone repository ${repoToClone}`);
await log('');
await log(' ๐ฆ Error details:');
for (const line of errorOutput.split('\n')) {
if (line.trim()) await log(` ${line}`);
}
await log('');
await log(' ๐ก Common causes:');
await log(' โข Repository doesn\'t exist or is private');
await log(' โข No GitHub authentication');
await log(' โข Network connectivity issues');
if (argv.fork) {
await log(' โข Fork not ready yet (try again in a moment)');
}
await log('');
await log(' ๐ง How to fix:');
await log(' 1. Check authentication: gh auth status');
await log(' 2. Login if needed: gh auth login');
await log(` 3. Verify access: gh repo view ${owner}/${repo}`);
if (argv.fork) {
await log(` 4. Check fork: gh repo view ${repoToClone}`);
}
await log('');
await safeExit(1, 'Repository setup failed');
}
await log(`${formatAligned('โ
', 'Cloned to:', tempDir)}`);
// Verify and fix remote configuration
const remoteCheckResult = await $({ cwd: tempDir })`git remote -v 2>&1`;
if (!remoteCheckResult.stdout || !remoteCheckResult.stdout.toString().includes('origin')) {
await log(' Setting up git remote...', { verbose: true });
// Add origin remote manually
await $({ cwd: tempDir })`git remote add origin https://github.com/${repoToClone}.git 2>&1`;
}
};
// Set up upstream remote and sync fork
export const setupUpstreamAndSync = async (tempDir, forkedRepo, upstreamRemote, owner, repo) => {
if (!forkedRepo || !upstreamRemote) return;
await log(`${formatAligned('๐', 'Setting upstream:', upstreamRemote)}`);
// Check if upstream remote already exists
const checkUpstreamResult = await $({ cwd: tempDir })`git remote get-url upstream 2>/dev/null`;
let upstreamExists = checkUpstreamResult.code === 0;
if (upstreamExists) {
await log(`${formatAligned('โน๏ธ', 'Upstream exists:', 'Using existing upstream remote')}`);
} else {
// Add upstream remote since it doesn't exist
const upstreamResult = await $({ cwd: tempDir })`git remote add upstream https://github.com/${upstreamRemote}.git`;
if (upstreamResult.code === 0) {
await log(`${formatAligned('โ
', 'Upstream set:', upstreamRemote)}`);
upstreamExists = true;
} else {
await log(`${formatAligned('โ ๏ธ', 'Warning:', 'Failed to add upstream remote')}`);
if (upstreamResult.stderr) {
await log(`${formatAligned('', 'Error details:', upstreamResult.stderr.toString().trim())}`);
}
}
}
// Proceed with fork sync if upstream remote is available
if (upstreamExists) {
// Fetch upstream
await log(`${formatAligned('๐', 'Fetching upstream...', '')}`);
const fetchResult = await $({ cwd: tempDir })`git fetch upstream`;
if (fetchResult.code === 0) {
await log(`${formatAligned('โ
', 'Upstream fetched:', 'Successfully')}`);
// Sync the default branch with upstream to avoid merge conflicts
await log(`${formatAligned('๐', 'Syncing default branch...', '')}`);
// Get current branch so we can return to it after sync
const currentBranchResult = await $({ cwd: tempDir })`git branch --show-current`;
if (currentBranchResult.code === 0) {
const currentBranch = currentBranchResult.stdout.toString().trim();
// Get the default branch name from the original repository using GitHub API
const repoInfoResult = await $`gh api repos/${owner}/${repo} --jq .default_branch`;
if (repoInfoResult.code === 0) {
const upstreamDefaultBranch = repoInfoResult.stdout.toString().trim();
await log(`${formatAligned('โน๏ธ', 'Default branch:', upstreamDefaultBranch)}`);
// Always sync the default branch, regardless of current branch
// This ensures fork is up-to-date even if we're working on a different branch
// Step 1: Switch to default branch if not already on it
let syncSuccessful = true;
if (currentBranch !== upstreamDefaultBranch) {
await log(`${formatAligned('๐', 'Switching to:', `${upstreamDefaultBranch} branch`)}`);
const checkoutResult = await $({ cwd: tempDir })`git checkout ${upstreamDefaultBranch}`;
if (checkoutResult.code !== 0) {
await log(`${formatAligned('โ ๏ธ', 'Warning:', `Failed to checkout ${upstreamDefaultBranch}`)}`);
syncSuccessful = false; // Cannot proceed with sync
}
}
// Step 2: Sync default branch with upstream (only if checkout was successful)
if (syncSuccessful) {
const syncResult = await $({ cwd: tempDir })`git reset --hard upstream/${upstreamDefaultBranch}`;
if (syncResult.code === 0) {
await log(`${formatAligned('โ
', 'Default branch synced:', `with upstream/${upstreamDefaultBranch}`)}`);
// Step 3: Push the updated default branch to fork to keep it in sync
await log(`${formatAligned('๐', 'Pushing to fork:', `${upstreamDefaultBranch} branch`)}`);
const pushResult = await $({ cwd: tempDir })`git push origin ${upstreamDefaultBranch}`;
if (pushResult.code === 0) {
await log(`${formatAligned('โ
', 'Fork updated:', 'Default branch pushed to fork')}`);
} else {
// Fork sync failed - exit immediately as per maintainer feedback
await log(`${formatAligned('โ', 'FATAL ERROR:', 'Failed to push updated default branch to fork')}`);
if (pushResult.stderr) {
const errorMsg = pushResult.stderr.toString().trim();
await log(`${formatAligned('', 'Push error:', errorMsg)}`);
}
await log(`${formatAligned('', 'Reason:', 'Fork must be updated or process must stop')}`);
await log(`${formatAligned('', 'Solution draft:', 'Fork sync is required for proper workflow')}`);
await log(`${formatAligned('', 'Next steps:', '1. Check GitHub permissions for the fork')}`);
await log(`${formatAligned('', '', '2. Ensure fork is not protected')}`);
await log(`${formatAligned('', '', '3. Try again after resolving fork issues')}`);
await safeExit(1, 'Repository setup failed');
}
// Step 4: Return to the original branch if it was different
if (currentBranch !== upstreamDefaultBranch) {
await log(`${formatAligned('๐', 'Returning to:', `${currentBranch} branch`)}`);
const returnResult = await $({ cwd: tempDir })`git checkout ${currentBranch}`;
if (returnResult.code === 0) {
await log(`${formatAligned('โ
', 'Branch restored:', `Back on ${currentBranch}`)}`);
} else {
await log(`${formatAligned('โ ๏ธ', 'Warning:', `Failed to return to ${currentBranch}`)}`);
// This is not fatal, continue with sync on default branch
}
}
} else {
await log(`${formatAligned('โ ๏ธ', 'Warning:', `Failed to sync ${upstreamDefaultBranch} with upstream`)}`);
if (syncResult.stderr) {
await log(`${formatAligned('', 'Sync error:', syncResult.stderr.toString().trim())}`);
}
}
}
} else {
await log(`${formatAligned('โ ๏ธ', 'Warning:', 'Failed to get default branch name')}`);
}
} else {
await log(`${formatAligned('โ ๏ธ', 'Warning:', 'Failed to get current branch')}`);
}
} else {
await log(`${formatAligned('โ ๏ธ', 'Warning:', 'Failed to fetch upstream')}`);
if (fetchResult.stderr) {
await log(`${formatAligned('', 'Fetch error:', fetchResult.stderr.toString().trim())}`);
}
}
}
};
// Cleanup temporary directory
export const cleanupTempDirectory = async (tempDir, argv, limitReached) => {
// Clean up temporary directory (but not when resuming, when limit reached, or when auto-continue is active)
if (!argv.resume && !limitReached && !(argv.autoContinueLimit && global.limitResetTime)) {
try {
process.stdout.write('\n๐งน Cleaning up...');
await fs.rm(tempDir, { recursive: true, force: true });
await log(' โ
');
} catch (cleanupError) {
reportError(cleanupError, {
context: 'cleanup_temp_directory',
tempDir,
operation: 'remove_temp_dir'
});
await log(' โ ๏ธ (failed)');
}
} else if (argv.resume) {
await log(`\n๐ Keeping directory for resumed session: ${tempDir}`);
} else if (limitReached && argv.autoContinueLimit) {
await log(`\n๐ Keeping directory for auto-continue: ${tempDir}`);
} else if (limitReached) {
await log(`\n๐ Keeping directory for future resume: ${tempDir}`);
}
};