@deep-assistant/hive-mind
Version:
AI-powered issue solver and hive mind for collaborative problem solving
1,069 lines (937 loc) âĸ 68.1 kB
JavaScript
#!/usr/bin/env node
// Import Sentry instrumentation first (must be before other imports)
import './instrument.mjs';
// Early exit paths - handle these before loading all modules to speed up testing
const earlyArgs = process.argv.slice(2);
if (earlyArgs.includes('--version')) {
// Quick version output without loading modules - get version from package.json or use dev version format
const { execSync } = await import('child_process');
const { readFileSync } = await import('fs');
const { dirname, join } = await import('path');
const { fileURLToPath } = await import('url');
const { getGitVersion } = await import('./git.lib.mjs');
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const packagePath = join(__dirname, '..', 'package.json');
try {
const packageJson = JSON.parse(readFileSync(packagePath, 'utf8'));
const currentVersion = packageJson.version;
const version = await getGitVersion(execSync, currentVersion);
console.log(version);
} catch (versionError) {
reportError(versionError, {
context: 'version_detection',
operation: 'get_package_version'
});
// Fallback to hardcoded version if all else fails
console.log('0.10.4');
}
process.exit(0);
}
if (earlyArgs.includes('--help') || earlyArgs.includes('-h')) {
// Load minimal modules needed for help
const { use } = eval(await (await fetch('https://unpkg.com/use-m/use.js')).text());
globalThis.use = use;
const config = await import('./solve.config.lib.mjs');
const { initializeConfig, createYargsConfig } = config;
const { yargs, hideBin } = await initializeConfig(use);
const rawArgs = hideBin(process.argv);
// Filter out help flags to avoid duplicate display
const argsWithoutHelp = rawArgs.filter(arg => arg !== '--help' && arg !== '-h');
createYargsConfig(yargs(argsWithoutHelp)).showHelp();
process.exit(0);
}
if (earlyArgs.length === 0) {
console.error('Usage: solve.mjs <issue-url> [options]');
console.error('\nError: Missing required github issue or pull request URL');
console.error('\nRun "solve.mjs --help" for more information');
process.exit(1);
}
// Now load all modules for normal operation
const { use } = eval(await (await fetch('https://unpkg.com/use-m/use.js')).text());
globalThis.use = use;
const { $ } = await use('command-stream');
const config = await import('./solve.config.lib.mjs');
const { initializeConfig, parseArguments } = config;
// Import Sentry integration
const sentryLib = await import('./sentry.lib.mjs');
const { initializeSentry, addBreadcrumb, reportError } = sentryLib;
const { yargs, hideBin } = await initializeConfig(use);
// const os = (await use('os')).default; // Not currently used
const path = (await use('path')).default;
const fs = (await use('fs')).promises;
const crypto = (await use('crypto')).default;
const memoryCheck = await import('./memory-check.mjs');
const lib = await import('./lib.mjs');
const { log, setLogFile, getLogFile, getAbsoluteLogPath, cleanErrorMessage, formatAligned, getVersionInfo } = lib;
const githubLib = await import('./github.lib.mjs');
const { sanitizeLogContent, attachLogToGitHub } = githubLib;
// Claude lib import removed - not currently used
const validation = await import('./solve.validation.lib.mjs');
const { validateGitHubUrl, showAttachLogsWarning, initializeLogFile, validateUrlRequirement, validateContinueOnlyOnFeedback, performSystemChecks, parseUrlComponents } = validation;
const autoContinue = await import('./solve.auto-continue.lib.mjs');
const { processAutoContinueForIssue } = autoContinue;
const repository = await import('./solve.repository.lib.mjs');
const { setupTempDirectory, setupRepository, cloneRepository, setupUpstreamAndSync, cleanupTempDirectory } = repository;
const results = await import('./solve.results.lib.mjs');
const { cleanupClaudeFile, showSessionSummary, verifyResults } = results;
const claudeLib = await import('./claude.lib.mjs');
const { executeClaude, checkForUncommittedChanges } = claudeLib;
const feedback = await import('./solve.feedback.lib.mjs');
const { detectAndCountFeedback } = feedback;
const errorHandlers = await import('./solve.error-handlers.lib.mjs');
const { createUncaughtExceptionHandler, createUnhandledRejectionHandler, handleMainExecutionError } = errorHandlers;
const branchErrors = await import('./solve.branch-errors.lib.mjs');
const { handleBranchCheckoutError, handleBranchCreationError, handleBranchVerificationError } = branchErrors;
const watchLib = await import('./solve.watch.lib.mjs');
const { startWatchMode } = watchLib;
const exitHandler = await import('./exit-handler.lib.mjs');
const { initializeExitHandler, installGlobalExitHandlers, safeExit } = exitHandler;
const getResourceSnapshot = memoryCheck.getResourceSnapshot;
const argv = await parseArguments(yargs, hideBin);
global.verboseMode = argv.verbose;
if (argv.verbose) {
await log(`Debug: argv.attachLogs = ${argv.attachLogs}`, { verbose: true });
await log(`Debug: argv["attach-logs"] = ${argv['attach-logs']}`, { verbose: true });
}
const shouldAttachLogs = argv.attachLogs || argv['attach-logs'];
await showAttachLogsWarning(shouldAttachLogs);
const logFile = await initializeLogFile(argv.logDir);
const absoluteLogPath = path.resolve(logFile);
// Initialize Sentry integration (unless disabled)
if (!argv.noSentry) {
await initializeSentry({
noSentry: argv.noSentry,
debug: argv.verbose,
version: process.env.npm_package_version || '0.12.0'
});
// Add breadcrumb for solve operation
addBreadcrumb({
category: 'solve',
message: 'Started solving issue',
level: 'info',
data: {
model: argv.model,
issueUrl: argv._?.[0] || 'not-set-yet'
}
});
}
// Initialize the exit handler with getAbsoluteLogPath function and Sentry cleanup
initializeExitHandler(getAbsoluteLogPath, log);
installGlobalExitHandlers();
// Log version and raw command at the start
const versionInfo = await getVersionInfo();
await log('');
await log(`đ solve v${versionInfo}`);
await log('');
// Log the raw command that was executed (for better bug reporting)
const rawCommand = process.argv.join(' ');
await log('đ§ Raw command executed:');
await log(` ${rawCommand}`);
await log('');
// Now handle argument validation that was moved from early checks
let issueUrl = argv._[0];
if (!issueUrl) {
await log('Usage: solve.mjs <issue-url> [options]', { level: 'error' });
await log('');
await log('Error: Missing required github issue or pull request URL', { level: 'error' });
await log('');
await log('Run "solve.mjs --help" for more information', { level: 'error' });
await safeExit(1, 'Missing required GitHub URL');
}
// Validate GitHub URL using validation module (more thorough check)
const urlValidation = validateGitHubUrl(issueUrl);
if (!urlValidation.isValid) {
await safeExit(1, 'Invalid GitHub URL');
}
const { isIssueUrl, isPrUrl, normalizedUrl } = urlValidation;
// Use the normalized URL for all subsequent operations
issueUrl = normalizedUrl || issueUrl;
// Setup unhandled error handlers to ensure log path is always shown
const errorHandlerOptions = {
log,
cleanErrorMessage,
absoluteLogPath,
shouldAttachLogs,
argv,
global,
owner: null, // Will be set later when parsed
repo: null, // Will be set later when parsed
getLogFile,
attachLogToGitHub,
sanitizeLogContent,
$
};
process.on('uncaughtException', createUncaughtExceptionHandler(errorHandlerOptions));
process.on('unhandledRejection', createUnhandledRejectionHandler(errorHandlerOptions));
// Graceful shutdown handlers are now handled by exit-handler.lib.mjs
// Validate GitHub URL requirement and options using validation module
if (!(await validateUrlRequirement(issueUrl))) {
await safeExit(1, 'URL requirement validation failed');
}
if (!(await validateContinueOnlyOnFeedback(argv, isPrUrl, isIssueUrl))) {
await safeExit(1, 'Feedback validation failed');
}
// Perform all system checks using validation module
// Skip Claude validation in dry-run mode or when --skip-claude-check is enabled
const skipClaudeCheck = argv.dryRun || argv.skipClaudeCheck;
if (!(await performSystemChecks(argv.minDiskSpace || 500, skipClaudeCheck, argv.model))) {
await safeExit(1, 'System checks failed');
}
// URL validation debug logging
if (argv.verbose) {
await log('đ URL validation:', { verbose: true });
await log(` Input URL: ${issueUrl}`, { verbose: true });
await log(` Is Issue URL: ${!!isIssueUrl}`, { verbose: true });
await log(` Is PR URL: ${!!isPrUrl}`, { verbose: true });
}
const claudePath = process.env.CLAUDE_PATH || 'claude';
// Parse URL components using validation module
const { owner, repo, urlNumber } = parseUrlComponents(issueUrl);
// Store owner and repo globally for error handlers
global.owner = owner;
global.repo = repo;
// Determine mode and get issue details
let issueNumber;
let prNumber;
let prBranch;
let mergeStateStatus;
// let isForkPR = false; // Not currently used
let isContinueMode = false;
// Auto-continue logic: check for existing PRs if --auto-continue is enabled
const autoContinueResult = await processAutoContinueForIssue(argv, isIssueUrl, urlNumber, owner, repo);
if (autoContinueResult.isContinueMode) {
isContinueMode = true;
prNumber = autoContinueResult.prNumber;
prBranch = autoContinueResult.prBranch;
issueNumber = autoContinueResult.issueNumber;
// Store PR info globally for error handlers
global.createdPR = { number: prNumber };
} else if (isIssueUrl) {
issueNumber = autoContinueResult.issueNumber || urlNumber;
}
if (isPrUrl) {
isContinueMode = true;
prNumber = urlNumber;
// Store PR info globally for error handlers
global.createdPR = { number: prNumber, url: issueUrl };
await log(`đ Continue mode: Working with PR #${prNumber}`);
if (argv.verbose) {
await log(' Continue mode activated: PR URL provided directly', { verbose: true });
await log(` PR Number set to: ${prNumber}`, { verbose: true });
await log(' Will fetch PR details and linked issue', { verbose: true });
}
// Get PR details to find the linked issue and branch
try {
const prResult = await $`gh pr view ${prNumber} --repo ${owner}/${repo} --json headRefName,body,number,mergeStateStatus,headRepositoryOwner`;
if (prResult.code !== 0) {
await log('Error: Failed to get PR details', { level: 'error' });
await log(`Error: ${prResult.stderr ? prResult.stderr.toString() : 'Unknown error'}`, { level: 'error' });
await safeExit(1, 'Failed to get PR details');
}
const prData = JSON.parse(prResult.stdout.toString());
prBranch = prData.headRefName;
mergeStateStatus = prData.mergeStateStatus;
// Check if this is a fork PR
// Check if this is a fork PR (not currently used)
// isForkPR = prData.headRepositoryOwner && prData.headRepositoryOwner.login !== owner;
await log(`đ PR branch: ${prBranch}`);
// Extract issue number from PR body (look for "fixes #123", "closes #123", etc.)
const prBody = prData.body || '';
const issueMatch = prBody.match(/(?:fixes|closes|resolves)\s+(?:.*?[/#])?(\d+)/i);
if (issueMatch) {
issueNumber = issueMatch[1];
await log(`đ Found linked issue #${issueNumber}`);
} else {
// If no linked issue found, we can still continue but warn
await log('â ī¸ Warning: No linked issue found in PR body', { level: 'warning' });
await log(' The PR should contain "Fixes #123" or similar to link an issue', { level: 'warning' });
// Set issueNumber to PR number as fallback
issueNumber = prNumber;
}
} catch (error) {
reportError(error, {
context: 'pr_processing',
prNumber,
operation: 'process_pull_request'
});
await log(`Error: Failed to process PR: ${cleanErrorMessage(error)}`, { level: 'error' });
await safeExit(1, 'Failed to process PR');
}
} else {
// Traditional issue mode
issueNumber = urlNumber;
await log(`đ Issue mode: Working with issue #${issueNumber}`);
}
// Create or find temporary directory for cloning the repository
const { tempDir } = await setupTempDirectory(argv);
// Initialize limitReached variable outside try block for finally clause
let limitReached = false;
try {
// Set up repository and handle forking
const { repoToClone, forkedRepo, upstreamRemote } = await setupRepository(argv, owner, repo);
// Clone repository and set up remotes
await cloneRepository(repoToClone, tempDir, argv, owner, repo);
// Set up upstream remote and sync fork if needed
await setupUpstreamAndSync(tempDir, forkedRepo, upstreamRemote, owner, repo);
// Set up git authentication using gh
const authSetupResult = await $({ cwd: tempDir })`gh auth setup-git 2>&1`;
if (authSetupResult.code !== 0) {
await log('Note: gh auth setup-git had issues, continuing anyway\n');
}
// Verify we're on the default branch and get its name
const defaultBranchResult = await $({ cwd: tempDir })`git branch --show-current`;
if (defaultBranchResult.code !== 0) {
await log('Error: Failed to get current branch');
await log(defaultBranchResult.stderr ? defaultBranchResult.stderr.toString() : 'Unknown error');
await safeExit(1, 'Failed to get current branch');
}
const defaultBranch = defaultBranchResult.stdout.toString().trim();
if (!defaultBranch) {
await log('');
await log(`${formatAligned('â', 'DEFAULT BRANCH DETECTION FAILED', '')}`, { level: 'error' });
await log('');
await log(' đ What happened:');
await log(' Unable to determine the repository\'s default branch.');
await log('');
await log(' đĄ This might mean:');
await log(' âĸ Repository is empty (no commits)');
await log(' âĸ Unusual repository configuration');
await log(' âĸ Git command issues');
await log('');
await log(' đ§ How to fix:');
await log(` 1. Check repository: gh repo view ${owner}/${repo}`);
await log(` 2. Verify locally: cd ${tempDir} && git branch`);
await log(` 3. Check remote: cd ${tempDir} && git branch -r`);
await log('');
await safeExit(1, 'Default branch detection failed');
}
await log(`\n${formatAligned('đ', 'Default branch:', defaultBranch)}`);
// Ensure we're on a clean default branch
const statusResult = await $({ cwd: tempDir })`git status --porcelain`;
if (statusResult.code !== 0) {
await log('Error: Failed to check git status');
await log(statusResult.stderr ? statusResult.stderr.toString() : 'Unknown error');
await safeExit(1, 'Failed to check git status');
}
// Note: Empty output means clean working directory
const statusOutput = statusResult.stdout.toString().trim();
if (statusOutput) {
await log('Error: Repository has uncommitted changes after clone');
await log(`Status output: ${statusOutput}`);
await safeExit(1, 'Repository has uncommitted changes after clone');
}
// Create a branch for the issue or checkout existing PR branch
let branchName;
let checkoutResult;
if (isContinueMode && prBranch) {
// Continue mode: checkout existing PR branch
branchName = prBranch;
await log(`\n${formatAligned('đ', 'Checking out PR branch:', branchName)}`);
// First fetch all branches from remote
await log(`${formatAligned('đĨ', 'Fetching branches:', 'From remote...')}`);
const fetchResult = await $({ cwd: tempDir })`git fetch origin`;
if (fetchResult.code !== 0) {
await log('Warning: Failed to fetch branches from remote', { level: 'warning' });
}
// Checkout the PR branch (it might exist locally or remotely)
const localBranchResult = await $({ cwd: tempDir })`git show-ref --verify --quiet refs/heads/${branchName}`;
if (localBranchResult.code === 0) {
// Branch exists locally
checkoutResult = await $({ cwd: tempDir })`git checkout ${branchName}`;
} else {
// Branch doesn't exist locally, try to checkout from remote
checkoutResult = await $({ cwd: tempDir })`git checkout -b ${branchName} origin/${branchName}`;
}
} else {
// Traditional mode: create new branch for issue
const randomHex = crypto.randomBytes(4).toString('hex');
branchName = `issue-${issueNumber}-${randomHex}`;
await log(`\n${formatAligned('đŋ', 'Creating branch:', `${branchName} from ${defaultBranch}`)}`);
// IMPORTANT: Don't use 2>&1 here as it can interfere with exit codes
// Git checkout -b outputs to stderr but that's normal
checkoutResult = await $({ cwd: tempDir })`git checkout -b ${branchName}`;
}
if (checkoutResult.code !== 0) {
const errorOutput = (checkoutResult.stderr || checkoutResult.stdout || 'Unknown error').toString().trim();
await log('');
if (isContinueMode) {
await handleBranchCheckoutError({
branchName,
prNumber,
errorOutput,
issueUrl,
owner,
repo,
tempDir,
argv,
formatAligned,
log,
$
});
} else {
await handleBranchCreationError({
branchName,
errorOutput,
tempDir,
owner,
repo,
formatAligned,
log
});
}
await log('');
await log(` đ Working directory: ${tempDir}`);
await safeExit(1, 'Branch operation failed');
}
// CRITICAL: Verify the branch was checked out and we switched to it
await log(`${formatAligned('đ', 'Verifying:', isContinueMode ? 'Branch checkout...' : 'Branch creation...')}`);
const verifyResult = await $({ cwd: tempDir })`git branch --show-current`;
if (verifyResult.code !== 0 || !verifyResult.stdout) {
await log('');
await log(`${formatAligned('â', 'BRANCH VERIFICATION FAILED', '')}`, { level: 'error' });
await log('');
await log(' đ What happened:');
await log(` Unable to verify branch after ${isContinueMode ? 'checkout' : 'creation'} attempt.`);
await log('');
await log(' đ§ Debug commands to try:');
await log(` cd ${tempDir} && git branch -a`);
await log(` cd ${tempDir} && git status`);
await log('');
await safeExit(1, 'Branch verification failed');
}
const actualBranch = verifyResult.stdout.toString().trim();
if (actualBranch !== branchName) {
// Branch wasn't actually created/checked out or we didn't switch to it
await handleBranchVerificationError({
isContinueMode,
branchName,
actualBranch,
prNumber,
owner,
repo,
tempDir,
formatAligned,
log,
$
});
await safeExit(1, 'Branch verification mismatch');
}
if (isContinueMode) {
await log(`${formatAligned('â
', 'Branch checked out:', branchName)}`);
await log(`${formatAligned('â
', 'Current branch:', actualBranch)}`);
if (argv.verbose) {
await log(' Branch operation: Checkout existing PR branch', { verbose: true });
await log(` Branch verification: ${actualBranch === branchName ? 'Matches expected' : 'MISMATCH!'}`, { verbose: true });
}
} else {
await log(`${formatAligned('â
', 'Branch created:', branchName)}`);
await log(`${formatAligned('â
', 'Current branch:', actualBranch)}`);
if (argv.verbose) {
await log(' Branch operation: Create new branch', { verbose: true });
await log(` Branch verification: ${actualBranch === branchName ? 'Matches expected' : 'MISMATCH!'}`, { verbose: true });
}
}
// Initialize PR variables early
let prUrl = null;
// In continue mode, we already have the PR details
if (isContinueMode) {
prUrl = issueUrl; // The input URL is the PR URL
// prNumber is already set from earlier when we parsed the PR
}
// Don't build the prompt yet - we'll build it after we have all the information
// This includes PR URL (if created) and comment info (if in continue mode)
if (argv.autoPullRequestCreation && !isContinueMode) {
await log(`\n${formatAligned('đ', 'Auto PR creation:', 'ENABLED')}`);
await log(' Creating: Initial commit and draft PR...');
await log('');
try {
// Create CLAUDE.md file with the task details
await log(formatAligned('đ', 'Creating:', 'CLAUDE.md with task details'));
// Write initial task info to CLAUDE.md
const initialTaskInfo = `Issue to solve: ${issueUrl}
Your prepared branch: ${branchName}
Your prepared working directory: ${tempDir}${argv.fork && forkedRepo ? `
Your forked repository: ${forkedRepo}
Original repository (upstream): ${owner}/${repo}` : ''}
Proceed.`;
await fs.writeFile(path.join(tempDir, 'CLAUDE.md'), initialTaskInfo);
await log(formatAligned('â
', 'File created:', 'CLAUDE.md'));
// Add and commit the file
await log(formatAligned('đĻ', 'Adding file:', 'To git staging'));
// Use explicit cwd option for better reliability
const addResult = await $({ cwd: tempDir })`git add CLAUDE.md`;
if (addResult.code !== 0) {
await log('â Failed to add CLAUDE.md', { level: 'error' });
await log(` Error: ${addResult.stderr ? addResult.stderr.toString() : 'Unknown error'}`, { level: 'error' });
await safeExit(1, 'Failed to add CLAUDE.md');
}
// Verify the file was actually staged
if (argv.verbose) {
const statusResult = await $({ cwd: tempDir })`git status --short`;
await log(` Git status after add: ${statusResult.stdout ? statusResult.stdout.toString().trim() : 'empty'}`);
}
await log(formatAligned('đ', 'Creating commit:', 'With CLAUDE.md file'));
const commitMessage = `Initial commit with task details for issue #${issueNumber}
Adding CLAUDE.md with task information for AI processing.
This file will be removed when the task is complete.
Issue: ${issueUrl}`;
// Use explicit cwd option for better reliability
const commitResult = await $({ cwd: tempDir })`git commit -m ${commitMessage}`;
if (commitResult.code !== 0) {
await log('â Failed to create initial commit', { level: 'error' });
await log(` Error: ${commitResult.stderr ? commitResult.stderr.toString() : 'Unknown error'}`, { level: 'error' });
await log(` stdout: ${commitResult.stdout ? commitResult.stdout.toString() : 'none'}`, { verbose: true });
await safeExit(1, 'Failed to create initial commit');
} else {
await log(formatAligned('â
', 'Commit created:', 'Successfully with CLAUDE.md'));
if (argv.verbose) {
await log(` Commit output: ${commitResult.stdout.toString().trim()}`, { verbose: true });
}
// Verify commit was created before pushing
const verifyCommitResult = await $({ cwd: tempDir })`git log --format="%h %s" -1 2>&1`;
if (verifyCommitResult.code === 0) {
const latestCommit = verifyCommitResult.stdout ? verifyCommitResult.stdout.toString().trim() : '';
if (argv.verbose) {
await log(` Latest commit: ${latestCommit || '(empty - this is a problem!)'}`);
// Show git status
const statusResult = await $({ cwd: tempDir })`git status --short 2>&1`;
await log(` Git status: ${statusResult.stdout ? statusResult.stdout.toString().trim() || 'clean' : 'clean'}`);
// Show remote info
const remoteResult = await $({ cwd: tempDir })`git remote -v 2>&1`;
const remoteOutput = remoteResult.stdout ? remoteResult.stdout.toString().trim() : 'none';
await log(` Remotes: ${remoteOutput ? remoteOutput.split('\n')[0] : 'none configured'}`);
// Show branch info
const branchResult = await $({ cwd: tempDir })`git branch -vv 2>&1`;
await log(` Branch info: ${branchResult.stdout ? branchResult.stdout.toString().trim() : 'none'}`);
}
}
// Push the branch
await log(formatAligned('đ¤', 'Pushing branch:', 'To remote repository...'));
if (argv.verbose) {
await log(` Command: git push -u origin ${branchName}`, { verbose: true });
}
// Push the branch with the CLAUDE.md commit
if (argv.verbose) {
await log(` Push command: git push -f -u origin ${branchName}`);
}
// Always use force push to ensure our commit gets to GitHub
// (The branch is new with random name, so force is safe)
const pushResult = await $({ cwd: tempDir })`git push -f -u origin ${branchName} 2>&1`;
if (argv.verbose) {
await log(` Push exit code: ${pushResult.code}`);
if (pushResult.stdout) {
await log(` Push output: ${pushResult.stdout.toString().trim()}`);
}
if (pushResult.stderr) {
await log(` Push stderr: ${pushResult.stderr.toString().trim()}`);
}
}
if (pushResult.code !== 0) {
const errorOutput = pushResult.stderr ? pushResult.stderr.toString() : pushResult.stdout ? pushResult.stdout.toString() : 'Unknown error';
// Check for permission denied error
if (errorOutput.includes('Permission to') && errorOutput.includes('denied')) {
// Check if user already has a fork
let userHasFork = false;
let currentUser = null;
try {
const userResult = await $`gh api user --jq .login`;
if (userResult.code === 0) {
currentUser = userResult.stdout.toString().trim();
const forkCheckResult = await $`gh repo view ${currentUser}/${repo} --json parent 2>/dev/null`;
if (forkCheckResult.code === 0) {
const forkData = JSON.parse(forkCheckResult.stdout.toString());
if (forkData.parent && forkData.parent.owner && forkData.parent.owner.login === owner) {
userHasFork = true;
}
}
}
} catch (e) {
reportError(e, {
context: 'fork_check',
owner,
repo,
operation: 'check_user_fork'
});
// Ignore error - fork check is optional
}
await log(`\n${formatAligned('â', 'PERMISSION DENIED:', 'Cannot push to repository')}`, { level: 'error' });
await log('');
await log('âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ');
await log('');
await log(` đ You don't have write access to ${owner}/${repo}`);
await log('');
await log(' This typically happens when:');
await log(' âĸ You\'re not a collaborator on the repository');
await log(' âĸ The repository belongs to another user/organization');
await log('');
await log(' đ HOW TO FIX THIS:');
await log('');
await log(' ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ');
await log(' â RECOMMENDED: Use the --fork option â');
await log(' ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ');
await log('');
await log(' Run the command again with --fork:');
await log('');
await log(` ./solve.mjs "${issueUrl}" --fork`);
await log('');
await log(' This will automatically:');
if (userHasFork) {
await log(` â Use your existing fork (${currentUser}/${repo})`);
await log(' â Sync your fork with the latest changes');
} else {
await log(' â Fork the repository to your account');
}
await log(' â Push changes to your fork');
await log(' â Create a PR from your fork to the original repo');
await log(' â Handle all the remote setup automatically');
await log('');
await log(' âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ');
await log('');
await log(' Alternative options:');
await log('');
await log(' Option 2: Request collaborator access');
await log(` ${'-'.repeat(40)}`);
await log(' Ask the repository owner to add you as a collaborator:');
await log(` â Go to: https://github.com/${owner}/${repo}/settings/access`);
await log('');
await log(' Option 3: Manual fork and clone');
await log(` ${'-'.repeat(40)}`);
await log(` 1. Fork the repo: https://github.com/${owner}/${repo}/fork`);
await log(' 2. Clone your fork and work there');
await log(' 3. Create a PR from your fork');
await log('');
await log('âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ');
await log('');
await log('đĄ Tip: The --fork option automates the entire fork workflow!');
if (userHasFork) {
await log(` Note: We detected you already have a fork at ${currentUser}/${repo}`);
}
await log('');
await safeExit(1, 'Permission denied - need fork or collaborator access');
} else {
// Other push errors
await log(`${formatAligned('â', 'Failed to push:', 'See error below')}`, { level: 'error' });
await log(` Error: ${errorOutput}`, { level: 'error' });
await safeExit(1, 'Failed to push branch');
}
} else {
await log(`${formatAligned('â
', 'Branch pushed:', 'Successfully to remote')}`);
if (argv.verbose) {
await log(` Push output: ${pushResult.stdout.toString().trim()}`, { verbose: true });
}
// CRITICAL: Wait for GitHub to process the push before creating PR
// This prevents "No commits between branches" error
await log(' Waiting for GitHub to sync...');
await new Promise(resolve => setTimeout(resolve, 8000)); // Longer wait for GitHub to process
// Verify the push actually worked by checking GitHub API
const branchCheckResult = await $({ silent: true })`gh api repos/${owner}/${repo}/branches/${branchName} --jq .name 2>&1`;
if (branchCheckResult.code === 0 && branchCheckResult.stdout.toString().trim() === branchName) {
await log(` Branch verified on GitHub: ${branchName}`);
// Get the commit SHA from GitHub
const shaCheckResult = await $({ silent: true })`gh api repos/${owner}/${repo}/branches/${branchName} --jq .commit.sha 2>&1`;
if (shaCheckResult.code === 0) {
const remoteSha = shaCheckResult.stdout.toString().trim();
await log(` Remote commit SHA: ${remoteSha.substring(0, 7)}...`);
}
} else {
await log(' Warning: Branch not found on GitHub!');
await log(' This will cause PR creation to fail.');
if (argv.verbose) {
await log(` Branch check result: ${branchCheckResult.stdout || branchCheckResult.stderr || 'empty'}`);
// Show all branches on GitHub
const allBranchesResult = await $({ silent: true })`gh api repos/${owner}/${repo}/branches --jq '.[].name' 2>&1`;
if (allBranchesResult.code === 0) {
await log(` All GitHub branches: ${allBranchesResult.stdout.toString().split('\n').slice(0, 5).join(', ')}...`);
}
}
// Try one more force push with explicit ref
await log(' Attempting explicit push...');
const explicitPushCmd = `git push origin HEAD:refs/heads/${branchName} -f`;
if (argv.verbose) {
await log(` Command: ${explicitPushCmd}`);
}
const explicitPushResult = await $`cd ${tempDir} && ${explicitPushCmd} 2>&1`;
if (explicitPushResult.code === 0) {
await log(' Explicit push completed');
if (argv.verbose && explicitPushResult.stdout) {
await log(` Output: ${explicitPushResult.stdout.toString().trim()}`);
}
// Wait a bit more for GitHub to process
await new Promise(resolve => setTimeout(resolve, 3000));
} else {
await log(' ERROR: Cannot push to GitHub!');
await log(` Error: ${explicitPushResult.stderr || explicitPushResult.stdout || 'Unknown'}`);
}
}
// Get issue title for PR title
await log(formatAligned('đ', 'Getting issue:', 'Title from GitHub...'), { verbose: true });
const issueTitleResult = await $({ silent: true })`gh api repos/${owner}/${repo}/issues/${issueNumber} --jq .title 2>&1`;
let issueTitle = `Fix issue #${issueNumber}`;
if (issueTitleResult.code === 0) {
issueTitle = issueTitleResult.stdout.toString().trim();
await log(` Issue title: "${issueTitle}"`, { verbose: true });
} else {
await log(' Warning: Could not get issue title, using default', { verbose: true });
}
// Get current GitHub user to set as assignee (but validate it's a collaborator)
await log(formatAligned('đ¤', 'Getting user:', 'Current GitHub account...'), { verbose: true });
const currentUserResult = await $({ silent: true })`gh api user --jq .login 2>&1`;
let currentUser = null;
let canAssign = false;
if (currentUserResult.code === 0) {
currentUser = currentUserResult.stdout.toString().trim();
await log(` Current user: ${currentUser}`, { verbose: true });
// Check if user has push access (is a collaborator or owner)
// IMPORTANT: We need to completely suppress the JSON error output
// Using execSync to have full control over stderr
try {
const { execSync } = await import('child_process');
// This will throw if user doesn't have access, but won't print anything
execSync(`gh api repos/${owner}/${repo}/collaborators/${currentUser} 2>/dev/null`,
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] });
canAssign = true;
await log(' User has collaborator access', { verbose: true });
} catch (e) {
reportError(e, {
context: 'collaborator_check',
owner,
repo,
currentUser,
operation: 'check_collaborator_access'
});
// User doesn't have access, which is fine - we just won't assign
canAssign = false;
await log(' User is not a collaborator (will skip assignment)', { verbose: true });
}
// Set permCheckResult for backward compatibility
const permCheckResult = { code: canAssign ? 0 : 1 };
if (permCheckResult.code === 0) {
canAssign = true;
await log(' User has collaborator access', { verbose: true });
} else {
// User doesn't have permission, but that's okay - we just won't assign
await log(' User is not a collaborator (will skip assignment)', { verbose: true });
}
} else {
await log(' Warning: Could not get current user', { verbose: true });
}
// Create draft pull request
await log(formatAligned('đ', 'Creating PR:', 'Draft pull request...'));
// Use full repository reference for cross-repo PRs (forks)
const issueRef = argv.fork ? `${owner}/${repo}#${issueNumber}` : `#${issueNumber}`;
const prBody = `## đ¤ AI-Powered Solution Draft
This pull request is being automatically generated to solve issue ${issueRef}.
### đ Issue Reference
Fixes ${issueRef}
### đ§ Status
**Work in Progress** - The AI assistant is currently analyzing and implementing the solution draft.
### đ Implementation Details
_Details will be added as the solution draft is developed..._
---
*This PR was created automatically by the AI issue solver*`;
if (argv.verbose) {
await log(` PR Title: [WIP] ${issueTitle}`, { verbose: true });
await log(` Base branch: ${defaultBranch}`, { verbose: true });
await log(` Head branch: ${branchName}`, { verbose: true });
if (currentUser) {
await log(` Assignee: ${currentUser}`, { verbose: true });
}
await log(` PR Body:
${prBody}`, { verbose: true });
}
// Use execSync for gh pr create to avoid command-stream output issues
// Similar to how create-test-repo.mjs handles it
try {
const { execSync } = await import('child_process');
// Write PR body to temp file to avoid shell escaping issues
const prBodyFile = `/tmp/pr-body-${Date.now()}.md`;
await fs.writeFile(prBodyFile, prBody);
// Build command with optional assignee and handle forks
let command;
if (argv.fork && forkedRepo) {
// For forks, specify the full head reference
const forkUser = forkedRepo.split('/')[0];
command = `cd "${tempDir}" && gh pr create --draft --title "[WIP] ${issueTitle}" --body-file "${prBodyFile}" --base ${defaultBranch} --head ${forkUser}:${branchName} --repo ${owner}/${repo}`;
} else {
command = `cd "${tempDir}" && gh pr create --draft --title "[WIP] ${issueTitle}" --body-file "${prBodyFile}" --base ${defaultBranch} --head ${branchName}`;
}
// Only add assignee if user has permissions
if (currentUser && canAssign) {
command += ` --assignee ${currentUser}`;
}
if (argv.verbose) {
await log(` Command: ${command}`, { verbose: true });
}
const output = execSync(command, { encoding: 'utf8', cwd: tempDir });
// Clean up temp file
await fs.unlink(prBodyFile).catch((unlinkError) => {
reportError(unlinkError, {
context: 'pr_body_file_cleanup',
prBodyFile,
operation: 'delete_temp_file'
});
});
// Extract PR URL from output - gh pr create outputs the URL to stdout
prUrl = output.trim();
if (!prUrl) {
await log('â ī¸ Warning: PR created but no URL returned', { level: 'warning' });
await log(` Output: ${output}`, { verbose: true });
// Try to get the PR URL using gh pr list
await log(' Attempting to find PR using gh pr list...', { verbose: true });
const prListResult = await $`cd ${tempDir} && gh pr list --head ${branchName} --json url --jq '.[0].url'`;
if (prListResult.code === 0 && prListResult.stdout.toString().trim()) {
prUrl = prListResult.stdout.toString().trim();
await log(` Found PR URL: ${prUrl}`, { verbose: true });
}
}
// Extract PR number from URL
if (prUrl) {
const prMatch = prUrl.match(/\/pull\/(\d+)/);
if (prMatch) {
prNumber = prMatch[1];
// Store PR info globally for error handlers
global.createdPR = { number: prNumber, url: prUrl };
await log(formatAligned('â
', 'PR created:', `#${prNumber}`));
await log(formatAligned('đ', 'PR URL:', prUrl));
if (currentUser && canAssign) {
await log(formatAligned('đ¤', 'Assigned to:', currentUser));
} else if (currentUser && !canAssign) {
await log(formatAligned('âšī¸', 'Note:', 'Could not assign (no permission)'));
}
// CLAUDE.md will be removed after Claude command completes
// Link the issue to the PR in GitHub's Development section using GraphQL API
await log(formatAligned('đ', 'Linking:', `Issue #${issueNumber} to PR #${prNumber}...`));
try {
// First, get the node IDs for both the issue and the PR
const issueNodeResult = await $`gh api graphql -f query='query { repository(owner: "${owner}", name: "${repo}") { issue(number: ${issueNumber}) { id } } }' --jq .data.repository.issue.id`;
if (issueNodeResult.code !== 0) {
throw new Error(`Failed to get issue node ID: ${issueNodeResult.stderr}`);
}
const issueNodeId = issueNodeResult.stdout.toString().trim();
await log(` Issue node ID: ${issueNodeId}`, { verbose: true });
const prNodeResult = await $`gh api graphql -f query='query { repository(owner: "${owner}", name: "${repo}") { pullRequest(number: ${prNumber}) { id } } }' --jq .data.repository.pullRequest.id`;
if (prNodeResult.code !== 0) {
throw new Error(`Failed to get PR node ID: ${prNodeResult.stderr}`);
}
const prNodeId = prNodeResult.stdout.toString().trim();
await log(` PR node ID: ${prNodeId}`, { verbose: true });
// Now link them using the GraphQL mutation
// GitHub automatically creates the link when we use "Fixes #" or "Fixes owner/repo#"
// The Development section link is created automatically by GitHub when:
// 1. The PR body contains "Fixes #N", "Closes #N", or "Resolves #N"
// 2. For cross-repo (fork) PRs, we need "Fixes owner/repo#N"
// Let's verify the link was created
const linkCheckResult = await $`gh api graphql -f query='query { repository(owner: "${owner}", name: "${repo}") { pullRequest(number: ${prNumber}) { closingIssuesReferences(first: 10) { nodes { number } } } } }' --jq '.data.repository.pullRequest.closingIssuesReferences.nodes[].number'`;
if (linkCheckResult.code === 0) {
const linkedIssues = linkCheckResult.stdout.toString().trim().split('\n').filter(n => n);
if (linkedIssues.includes(issueNumber)) {
await log(formatAligned('â
', 'Link verified:', `Issue #${issueNumber} â PR #${prNumber}`));
} else {
// This is a problem - the link wasn't created
await log('');
await log(formatAligned('â ī¸', 'ISSUE LINK MISSING:', 'PR not linked to issue'), { level: 'warning' });
await log('');
if (argv.fork) {
await log(' The PR was created from a fork but wasn\'t linked to the issue.', { level: 'warning' });
await log(` Expected: "Fixes ${owner}/${repo}#${issueNumber}" in PR body`, { level: 'warning' });
await log('');
await log(' To fix manually:', { level: 'warning' });
await log(` 1. Edit the PR description at: ${prUrl}`, { level: 'warning' });
await log(` 2. Add this line: Fixes ${owner}/${repo}#${issueNumber}`, { level: 'warning' });
} else {
await log(` The PR wasn't linked to issue #${issueNumber}`, { level: 'warning' });
await log(` Expected: "Fixes #${issueNumber}" in PR body`, { level: 'warning' });
await log('');
await log(' To fix manually:', { level: 'warning' });
await log(` 1. Edit the PR description at: ${prUrl}`, { level: 'warning' });
await log(` 2. Ensure it contains: Fixes #${issueNumber}`, { level: 'warning' });
}
await log('');
}
} else {
// Could not verify but show what should have been used
const expectedRef = argv.fork ? `${owner}/${repo}#${issueNumber}` : `#${issueNumber}`;
await log('â ī¸ Could not verify issue link (API error)', { level: 'warning' });
await log(` PR body should contain: "Fixes ${expectedRef}"`, { level: 'warning' });
await log(` Please verify manually at: ${prUrl}`, { level: 'warning' });
}
} catch (linkError) {
reportError(linkError, {
context: 'pr_issue_link_verification',
prUrl,
issueNumber,
operation: 'verify_issue_link'
});
const expectedRef = argv.fork ? `${owner}/${repo}#${issueNumber}` : `#${issueNumber}`;
await log(`â ī¸ Could not verify issue linking: ${linkError.message}`, { level: 'warning' });
await log(` PR body should contain: "Fixes ${expectedRef}"`, { level: 'warning' });
await log(` Please check manually at: ${prUrl}`, { level: 'warning' });
}
} else {
await log(formatAligned('â
', 'PR created:', 'Successfully'));
await log(formatAligned('đ', 'PR URL:', prUrl));
}
// CLAUDE.md will be removed after Claude command completes
} else {
await log('â ī¸ Draft pull request created but URL could not be determined', { level: 'warning' });
}
} catch (prCreateError) {
reportError(prCreateError, {
context: 'pr_creation',
issueNumber,
branchName,
operation: 'create_pull_request'
});
const errorMsg = prCreateError.message || '';
// Clean up the error message - extract the meaningful part
let cleanError = errorMsg;
if (errorMsg.includes('pull request create failed:')) {
cleanError = errorMsg.split('pull request create failed:')[1].trim();
} else if (errorMsg.includes('Command failed:')) {
// Extract just the error part, not the full command
const lines = errorMsg.split('\n');
cleanError = lines[lines.length - 1] || errorMsg;
}
// Check for specific error types
if (errorMsg.includes('could not assign user') || errorMsg.includes('not found')) {
// Assignment failed but PR might have been created
await log(formatAligned('â ī¸', 'Warning:', 'Could not assign user'), { level: 'warning' });
// Try to get the PR that was just created (use silent mode)
const prListResult = await $({ silent: true })`cd ${tempDir} && gh pr list --head ${branchName} --json url,number --jq '.[0]' 2>&1`;
if (prListResult.code === 0 && prListResult.stdout.toString().trim()) {
try {
const prData = JSON.parse(prListResult.stdout.toString().trim());
prUrl = prData.url;
prNumber = prData.number;
// Store PR info globally for error handlers
global.createdPR = { number: prNumber, url: prUrl };
await log(formatAligned('â
', 'PR created:', `#${prNumber} (without assignee)`));
await log(formatAligned('đ', 'PR URL:', prUrl));
} catch (parseErr) {
reportError(parseErr, {
context: 'pr_output_parsing',
operation: 'parse_pr_creation_output'
});
// If we can't parse, continue without PR info
await log(formatAligned('â ī¸', 'PR status:', 'Unknown (check GitHub)'));
}
} else {
// PR creation actually failed
await log('');
await log(formatAligned('â', 'PR CREATION FAILED', ''), { level: 'error' });
await log('');
await log(' đ What happened:');
await log(' Failed to create pull request after pushing branch.');
await log('');
await log(' đĻ Error details:');
for (const line of cleanError.split('\n')) {
if (line.trim()) await log(` ${line.trim()}`);
}
await log('');
await log(' đ§ How to fix:');
await log(' 1. Check GitHub to see if PR was partially created');
await log(' 2. Try creating PR manually: gh pr create');
await log(` 3. Verify branch was pushed: git push -u origin ${branchName}`);
await log('');
await safeExit(1, 'PR creation failed');