@link-assistant/hive-mind 0.39.0

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.
Files changed (63) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/LICENSE +24 -0
  3. package/README.md +769 -0
  4. package/package.json +58 -0
  5. package/src/agent.lib.mjs +705 -0
  6. package/src/agent.prompts.lib.mjs +196 -0
  7. package/src/buildUserMention.lib.mjs +71 -0
  8. package/src/claude-limits.lib.mjs +389 -0
  9. package/src/claude.lib.mjs +1445 -0
  10. package/src/claude.prompts.lib.mjs +203 -0
  11. package/src/codex.lib.mjs +552 -0
  12. package/src/codex.prompts.lib.mjs +194 -0
  13. package/src/config.lib.mjs +207 -0
  14. package/src/contributing-guidelines.lib.mjs +268 -0
  15. package/src/exit-handler.lib.mjs +205 -0
  16. package/src/git.lib.mjs +145 -0
  17. package/src/github-issue-creator.lib.mjs +246 -0
  18. package/src/github-linking.lib.mjs +152 -0
  19. package/src/github.batch.lib.mjs +272 -0
  20. package/src/github.graphql.lib.mjs +258 -0
  21. package/src/github.lib.mjs +1479 -0
  22. package/src/hive.config.lib.mjs +254 -0
  23. package/src/hive.mjs +1500 -0
  24. package/src/instrument.mjs +191 -0
  25. package/src/interactive-mode.lib.mjs +1000 -0
  26. package/src/lenv-reader.lib.mjs +206 -0
  27. package/src/lib.mjs +490 -0
  28. package/src/lino.lib.mjs +176 -0
  29. package/src/local-ci-checks.lib.mjs +324 -0
  30. package/src/memory-check.mjs +419 -0
  31. package/src/model-mapping.lib.mjs +145 -0
  32. package/src/model-validation.lib.mjs +278 -0
  33. package/src/opencode.lib.mjs +479 -0
  34. package/src/opencode.prompts.lib.mjs +194 -0
  35. package/src/protect-branch.mjs +159 -0
  36. package/src/review.mjs +433 -0
  37. package/src/reviewers-hive.mjs +643 -0
  38. package/src/sentry.lib.mjs +284 -0
  39. package/src/solve.auto-continue.lib.mjs +568 -0
  40. package/src/solve.auto-pr.lib.mjs +1374 -0
  41. package/src/solve.branch-errors.lib.mjs +341 -0
  42. package/src/solve.branch.lib.mjs +230 -0
  43. package/src/solve.config.lib.mjs +342 -0
  44. package/src/solve.error-handlers.lib.mjs +256 -0
  45. package/src/solve.execution.lib.mjs +291 -0
  46. package/src/solve.feedback.lib.mjs +436 -0
  47. package/src/solve.mjs +1128 -0
  48. package/src/solve.preparation.lib.mjs +210 -0
  49. package/src/solve.repo-setup.lib.mjs +114 -0
  50. package/src/solve.repository.lib.mjs +961 -0
  51. package/src/solve.results.lib.mjs +558 -0
  52. package/src/solve.session.lib.mjs +135 -0
  53. package/src/solve.validation.lib.mjs +325 -0
  54. package/src/solve.watch.lib.mjs +572 -0
  55. package/src/start-screen.mjs +324 -0
  56. package/src/task.mjs +308 -0
  57. package/src/telegram-bot.mjs +1481 -0
  58. package/src/telegram-markdown.lib.mjs +64 -0
  59. package/src/usage-limit.lib.mjs +218 -0
  60. package/src/version.lib.mjs +41 -0
  61. package/src/youtrack/solve.youtrack.lib.mjs +116 -0
  62. package/src/youtrack/youtrack-sync.mjs +219 -0
  63. package/src/youtrack/youtrack.lib.mjs +425 -0
@@ -0,0 +1,1374 @@
1
+ /**
2
+ * Auto PR creation functionality for solve.mjs
3
+ * Handles automatic creation of draft pull requests with initial commits
4
+ */
5
+
6
+ export async function handleAutoPrCreation({
7
+ argv,
8
+ tempDir,
9
+ branchName,
10
+ issueNumber,
11
+ owner,
12
+ repo,
13
+ defaultBranch,
14
+ forkedRepo,
15
+ isContinueMode,
16
+ prNumber,
17
+ log,
18
+ formatAligned,
19
+ $,
20
+ reportError,
21
+ path,
22
+ fs
23
+ }) {
24
+ // Skip auto-PR creation if:
25
+ // 1. Auto-PR creation is disabled AND we're not in continue mode with no PR
26
+ // 2. Continue mode is active AND we already have a PR
27
+ if (!argv.autoPullRequestCreation && !(isContinueMode && !prNumber)) {
28
+ return null;
29
+ }
30
+
31
+ if (isContinueMode && prNumber) {
32
+ // Continue mode with existing PR - skip PR creation
33
+ return null;
34
+ }
35
+
36
+ await log(`\n${formatAligned('🚀', 'Auto PR creation:', 'ENABLED')}`);
37
+ await log(' Creating: Initial commit and draft PR...');
38
+ await log('');
39
+
40
+ let prUrl = null;
41
+ let localPrNumber = null;
42
+ let claudeCommitHash = null;
43
+
44
+ // Extract issue URL at the top level so it's available in error handlers
45
+ // Use argv['issue-url'] (named positional) with fallback to argv._[0] (raw positional)
46
+ // This handles both yargs command mode (argv['issue-url']) and direct positional mode (argv._[0])
47
+ const issueUrl = argv['issue-url'] || argv._[0];
48
+
49
+ try {
50
+ // Create CLAUDE.md file with the task details
51
+ await log(formatAligned('📝', 'Creating:', 'CLAUDE.md with task details'));
52
+
53
+ // Check if CLAUDE.md already exists and read its content
54
+ const claudeMdPath = path.join(tempDir, 'CLAUDE.md');
55
+ let existingContent = null;
56
+ let fileExisted = false;
57
+ try {
58
+ existingContent = await fs.readFile(claudeMdPath, 'utf8');
59
+ fileExisted = true;
60
+ } catch (err) {
61
+ // File doesn't exist, which is fine
62
+ if (err.code !== 'ENOENT') {
63
+ throw err;
64
+ }
65
+ }
66
+
67
+ // Build task info section
68
+
69
+ // Verbose logging to help debug issue URL parsing issues (issue #651)
70
+ if (argv.verbose) {
71
+ await log(` Issue URL from argv['issue-url']: ${argv['issue-url'] || 'undefined'}`, { verbose: true });
72
+ await log(` Issue URL from argv._[0]: ${argv._[0] || 'undefined'}`, { verbose: true });
73
+ await log(` Final issue URL: ${issueUrl}`, { verbose: true });
74
+ }
75
+
76
+ // Add timestamp to ensure unique content on each run when appending
77
+ // This is critical for --auto-continue mode when reusing an existing branch
78
+ // Without this, appending the same task info produces no git changes,
79
+ // leading to "No commits between branches" error during PR creation
80
+ const timestamp = new Date().toISOString();
81
+ const taskInfo = `Issue to solve: ${issueUrl}
82
+ Your prepared branch: ${branchName}
83
+ Your prepared working directory: ${tempDir}${argv.fork && forkedRepo ? `
84
+ Your forked repository: ${forkedRepo}
85
+ Original repository (upstream): ${owner}/${repo}` : ''}
86
+
87
+ Proceed.`;
88
+
89
+ // If CLAUDE.md already exists, append the task info with separator and timestamp
90
+ // Otherwise, create new file with just the task info (no timestamp needed for new files)
91
+ let finalContent;
92
+ if (fileExisted && existingContent) {
93
+ await log(' CLAUDE.md already exists, appending task info...', { verbose: true });
94
+ // Remove any trailing whitespace and add separator
95
+ const trimmedExisting = existingContent.trimEnd();
96
+ // Add timestamp to ensure uniqueness when appending
97
+ finalContent = `${trimmedExisting}\n\n---\n\n${taskInfo}\n\nRun timestamp: ${timestamp}`;
98
+ } else {
99
+ finalContent = taskInfo;
100
+ }
101
+
102
+ await fs.writeFile(claudeMdPath, finalContent);
103
+ await log(formatAligned('✅', 'File created:', 'CLAUDE.md'));
104
+
105
+ // Add and commit the file
106
+ await log(formatAligned('📦', 'Adding file:', 'To git staging'));
107
+
108
+ // Use explicit cwd option for better reliability
109
+ const addResult = await $({ cwd: tempDir })`git add CLAUDE.md`;
110
+
111
+ if (addResult.code !== 0) {
112
+ await log('❌ Failed to add CLAUDE.md', { level: 'error' });
113
+ await log(` Error: ${addResult.stderr ? addResult.stderr.toString() : 'Unknown error'}`, { level: 'error' });
114
+ throw new Error('Failed to add CLAUDE.md');
115
+ }
116
+
117
+ // Verify the file was actually staged
118
+ let statusResult = await $({ cwd: tempDir })`git status --short`;
119
+ let gitStatus = statusResult.stdout ? statusResult.stdout.toString().trim() : '';
120
+
121
+ if (argv.verbose) {
122
+ await log(` Git status after add: ${gitStatus || 'empty'}`);
123
+ }
124
+
125
+ // Track which file we're using for the commit
126
+ let commitFileName = 'CLAUDE.md';
127
+
128
+ // Check if anything was actually staged
129
+ if (!gitStatus || gitStatus.length === 0) {
130
+ await log('');
131
+ await log(formatAligned('⚠️', 'CLAUDE.md not staged:', 'Checking if file is ignored'), { level: 'warning' });
132
+
133
+ // Check if CLAUDE.md is in .gitignore
134
+ const checkIgnoreResult = await $({ cwd: tempDir })`git check-ignore CLAUDE.md`;
135
+ const isIgnored = checkIgnoreResult.code === 0;
136
+
137
+ if (isIgnored) {
138
+ await log(formatAligned('ℹ️', 'CLAUDE.md is ignored:', 'Using .gitkeep fallback'));
139
+ await log('');
140
+ await log(' 📝 Fallback strategy:');
141
+ await log(' CLAUDE.md is in .gitignore, using .gitkeep instead.');
142
+ await log(' This allows auto-PR creation to proceed without modifying .gitignore.');
143
+ await log('');
144
+
145
+ // Create a .gitkeep file as fallback
146
+ const gitkeepPath = path.join(tempDir, '.gitkeep');
147
+ const gitkeepContent = `# Auto-generated file for PR creation
148
+ # Issue: ${issueUrl}
149
+ # Branch: ${branchName}
150
+ # This file was created because CLAUDE.md is in .gitignore
151
+ # It will be removed when the task is complete`;
152
+
153
+ await fs.writeFile(gitkeepPath, gitkeepContent);
154
+ await log(formatAligned('✅', 'Created:', '.gitkeep file'));
155
+
156
+ // Try to add .gitkeep
157
+ const gitkeepAddResult = await $({ cwd: tempDir })`git add .gitkeep`;
158
+
159
+ if (gitkeepAddResult.code !== 0) {
160
+ await log('❌ Failed to add .gitkeep', { level: 'error' });
161
+ await log(` Error: ${gitkeepAddResult.stderr ? gitkeepAddResult.stderr.toString() : 'Unknown error'}`, { level: 'error' });
162
+ throw new Error('Failed to add .gitkeep');
163
+ }
164
+
165
+ // Verify .gitkeep was staged
166
+ statusResult = await $({ cwd: tempDir })`git status --short`;
167
+ gitStatus = statusResult.stdout ? statusResult.stdout.toString().trim() : '';
168
+
169
+ if (!gitStatus || gitStatus.length === 0) {
170
+ await log('');
171
+ await log(formatAligned('❌', 'GIT ADD FAILED:', 'Neither CLAUDE.md nor .gitkeep could be staged'), { level: 'error' });
172
+ await log('');
173
+ await log(' 🔍 What happened:');
174
+ await log(' Both CLAUDE.md and .gitkeep failed to stage.');
175
+ await log('');
176
+ await log(' 🔧 Troubleshooting steps:');
177
+ await log(` 1. Check git status: cd "${tempDir}" && git status`);
178
+ await log(` 2. Check .gitignore: cat "${tempDir}/.gitignore"`);
179
+ await log(` 3. Try force add: cd "${tempDir}" && git add -f .gitkeep`);
180
+ await log('');
181
+ throw new Error('Git add staged nothing - both files failed');
182
+ }
183
+
184
+ commitFileName = '.gitkeep';
185
+ await log(formatAligned('✅', 'File staged:', '.gitkeep'));
186
+ } else {
187
+ await log('');
188
+ await log(formatAligned('❌', 'GIT ADD FAILED:', 'Nothing was staged'), { level: 'error' });
189
+ await log('');
190
+ await log(' 🔍 What happened:');
191
+ await log(' CLAUDE.md was created but git did not stage any changes.');
192
+ await log('');
193
+ await log(' 💡 Possible causes:');
194
+ await log(' • CLAUDE.md already exists with identical content');
195
+ await log(' • File system sync issue');
196
+ await log('');
197
+ await log(' 🔧 Troubleshooting steps:');
198
+ await log(` 1. Check file exists: ls -la "${tempDir}/CLAUDE.md"`);
199
+ await log(` 2. Check git status: cd "${tempDir}" && git status`);
200
+ await log(` 3. Force add: cd "${tempDir}" && git add -f CLAUDE.md`);
201
+ await log('');
202
+ await log(' 📂 Debug information:');
203
+ await log(` Working directory: ${tempDir}`);
204
+ await log(` Branch: ${branchName}`);
205
+ if (existingContent) {
206
+ await log(' Note: CLAUDE.md already existed (attempted to update with timestamp)');
207
+ }
208
+ await log('');
209
+ throw new Error('Git add staged nothing - CLAUDE.md may be unchanged');
210
+ }
211
+ }
212
+
213
+ await log(formatAligned('📝', 'Creating commit:', `With ${commitFileName} file`));
214
+ const commitMessage = commitFileName === 'CLAUDE.md'
215
+ ? `Initial commit with task details
216
+
217
+ Adding CLAUDE.md with task information for AI processing.
218
+ This file will be removed when the task is complete.
219
+
220
+ Issue: ${issueUrl}`
221
+ : `Initial commit with task details
222
+
223
+ Adding .gitkeep for PR creation (CLAUDE.md is in .gitignore).
224
+ This file will be removed when the task is complete.
225
+
226
+ Issue: ${issueUrl}`;
227
+
228
+ // Use explicit cwd option for better reliability
229
+ const commitResult = await $({ cwd: tempDir })`git commit -m ${commitMessage}`;
230
+
231
+ if (commitResult.code !== 0) {
232
+ const commitStderr = commitResult.stderr ? commitResult.stderr.toString() : '';
233
+ const commitStdout = commitResult.stdout ? commitResult.stdout.toString() : '';
234
+
235
+ await log('');
236
+ await log(formatAligned('❌', 'COMMIT FAILED:', 'Could not create initial commit'), { level: 'error' });
237
+ await log('');
238
+ await log(' 🔍 What happened:');
239
+ await log(' Git commit command failed after staging CLAUDE.md.');
240
+ await log('');
241
+
242
+ // Check for specific error patterns
243
+ if (commitStdout.includes('nothing to commit') || commitStdout.includes('working tree clean')) {
244
+ await log(' 💡 Root cause:');
245
+ await log(' Git reports "nothing to commit, working tree clean".');
246
+ await log(' This means no changes were staged, despite running git add.');
247
+ await log('');
248
+ await log(' 🔎 Why this happens:');
249
+ await log(' • CLAUDE.md already exists with identical content');
250
+ await log(' • File content did not actually change');
251
+ await log(' • Previous run may have left CLAUDE.md in the repository');
252
+ await log('');
253
+ await log(' 🔧 How to fix:');
254
+ await log(' Option 1: Remove CLAUDE.md and try again');
255
+ await log(` cd "${tempDir}" && git rm CLAUDE.md && git commit -m "Remove CLAUDE.md"`);
256
+ await log('');
257
+ await log(' Option 2: Skip auto-PR creation');
258
+ await log(' Run solve.mjs without --auto-pull-request-creation flag');
259
+ await log('');
260
+ } else {
261
+ await log(' 📦 Error output:');
262
+ if (commitStderr) await log(` stderr: ${commitStderr}`);
263
+ if (commitStdout) await log(` stdout: ${commitStdout}`);
264
+ await log('');
265
+ }
266
+
267
+ await log(' 📂 Debug information:');
268
+ await log(` Working directory: ${tempDir}`);
269
+ await log(` Branch: ${branchName}`);
270
+ await log(` Git status: ${gitStatus || '(empty)'}`);
271
+ await log('');
272
+
273
+ throw new Error('Failed to create initial commit');
274
+ } else {
275
+ await log(formatAligned('✅', 'Commit created:', `Successfully with ${commitFileName}`));
276
+ if (argv.verbose) {
277
+ await log(` Commit output: ${commitResult.stdout.toString().trim()}`, { verbose: true });
278
+ }
279
+
280
+ // Get the commit hash of the CLAUDE.md commit we just created
281
+ const commitHashResult = await $({ cwd: tempDir })`git log --format=%H -1 2>&1`;
282
+ if (commitHashResult.code === 0) {
283
+ claudeCommitHash = commitHashResult.stdout.toString().trim();
284
+ await log(` Commit hash: ${claudeCommitHash.substring(0, 7)}...`, { verbose: true });
285
+ }
286
+
287
+ // Verify commit was created before pushing
288
+ const verifyCommitResult = await $({ cwd: tempDir })`git log --format="%h %s" -1 2>&1`;
289
+ if (verifyCommitResult.code === 0) {
290
+ const latestCommit = verifyCommitResult.stdout ? verifyCommitResult.stdout.toString().trim() : '';
291
+ if (argv.verbose) {
292
+ await log(` Latest commit: ${latestCommit || '(empty - this is a problem!)'}`);
293
+
294
+ // Show git status
295
+ const statusResult = await $({ cwd: tempDir })`git status --short 2>&1`;
296
+ await log(` Git status: ${statusResult.stdout ? statusResult.stdout.toString().trim() || 'clean' : 'clean'}`);
297
+
298
+ // Show remote info
299
+ const remoteResult = await $({ cwd: tempDir })`git remote -v 2>&1`;
300
+ const remoteOutput = remoteResult.stdout ? remoteResult.stdout.toString().trim() : 'none';
301
+ await log(` Remotes: ${remoteOutput ? remoteOutput.split('\n')[0] : 'none configured'}`);
302
+
303
+ // Show branch info
304
+ const branchResult = await $({ cwd: tempDir })`git branch -vv 2>&1`;
305
+ await log(` Branch info: ${branchResult.stdout ? branchResult.stdout.toString().trim() : 'none'}`);
306
+ }
307
+ }
308
+
309
+ // Push the branch
310
+ await log(formatAligned('📤', 'Pushing branch:', 'To remote repository...'));
311
+
312
+ // Always use regular push - never force push, rebase, or reset
313
+ // History must be preserved at all times
314
+ if (argv.verbose) {
315
+ await log(` Push command: git push -u origin ${branchName}`);
316
+ }
317
+
318
+ const pushResult = await $({ cwd: tempDir })`git push -u origin ${branchName} 2>&1`;
319
+
320
+ if (argv.verbose) {
321
+ await log(` Push exit code: ${pushResult.code}`);
322
+ if (pushResult.stdout) {
323
+ await log(` Push output: ${pushResult.stdout.toString().trim()}`);
324
+ }
325
+ if (pushResult.stderr) {
326
+ await log(` Push stderr: ${pushResult.stderr.toString().trim()}`);
327
+ }
328
+ }
329
+
330
+ if (pushResult.code !== 0) {
331
+ const errorOutput = pushResult.stderr ? pushResult.stderr.toString() : pushResult.stdout ? pushResult.stdout.toString() : 'Unknown error';
332
+
333
+ // Check for archived repository error
334
+ if (errorOutput.includes('archived') && errorOutput.includes('read-only')) {
335
+ await log(`\n${formatAligned('❌', 'REPOSITORY ARCHIVED:', 'Cannot push to archived repository')}`, { level: 'error' });
336
+ await log('');
337
+ await log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
338
+ await log('');
339
+ await log(` 📦 Repository ${owner}/${repo} has been archived`);
340
+ await log('');
341
+ await log(' Archived repositories are read-only and cannot accept new commits.');
342
+ await log('');
343
+ await log(' 📋 WHAT THIS MEANS:');
344
+ await log('');
345
+ await log(' This repository has been archived by its owner, which means:');
346
+ await log(' • No new commits can be pushed');
347
+ await log(' • No new pull requests can be created');
348
+ await log(' • The repository is in read-only mode');
349
+ await log(' • Issues cannot be worked on');
350
+ await log('');
351
+ await log(' 🔧 POSSIBLE ACTIONS:');
352
+ await log('');
353
+ await log(' Option 1: Contact the repository owner');
354
+ await log(' ──────────────────────────────────────');
355
+ await log(' Ask the owner to unarchive the repository at:');
356
+ await log(` https://github.com/${owner}/${repo}/settings`);
357
+ await log('');
358
+ await log(' Option 2: Close the issue');
359
+ await log(' ──────────────────────────────────────');
360
+ await log(' If the repository is intentionally archived, close the issue:');
361
+ await log(` gh issue close ${issueNumber} --repo ${owner}/${repo} \\`);
362
+ await log(' --comment "Closing as repository is archived"');
363
+ await log('');
364
+ await log(' Option 3: Fork and work independently');
365
+ await log(' ──────────────────────────────────────');
366
+ await log(' You can fork the archived repository and make changes there,');
367
+ await log(' but note that you cannot create a PR back to the archived repo.');
368
+ await log('');
369
+ await log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
370
+ await log('');
371
+ throw new Error('Repository is archived and read-only');
372
+ }
373
+
374
+ // Check for permission denied error
375
+ if (errorOutput.includes('Permission to') && errorOutput.includes('denied')) {
376
+ // Check if user already has a fork
377
+ let userHasFork = false;
378
+ let currentUser = null;
379
+ // Determine fork name based on --prefix-fork-name-with-owner-name option
380
+ const forkRepoName = argv.prefixForkNameWithOwnerName ? `${owner}-${repo}` : repo;
381
+ try {
382
+ const userResult = await $`gh api user --jq .login`;
383
+ if (userResult.code === 0) {
384
+ currentUser = userResult.stdout.toString().trim();
385
+ const userForkName = `${currentUser}/${forkRepoName}`;
386
+ const forkCheckResult = await $`gh repo view ${userForkName} --json parent 2>/dev/null`;
387
+ if (forkCheckResult.code === 0) {
388
+ const forkData = JSON.parse(forkCheckResult.stdout.toString());
389
+ if (forkData.parent && forkData.parent.owner && forkData.parent.owner.login === owner) {
390
+ userHasFork = true;
391
+ }
392
+ }
393
+ }
394
+ } catch (e) {
395
+ reportError(e, {
396
+ context: 'fork_check',
397
+ owner,
398
+ repo,
399
+ operation: 'check_user_fork'
400
+ });
401
+ // Ignore error - fork check is optional
402
+ }
403
+
404
+ await log(`\n${formatAligned('❌', 'PERMISSION DENIED:', 'Cannot push to repository')}`, { level: 'error' });
405
+ await log('');
406
+ await log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
407
+ await log('');
408
+ await log(` 🔒 You don't have write access to ${owner}/${repo}`);
409
+ await log('');
410
+ await log(' This typically happens when:');
411
+ await log(' • You\'re not a collaborator on the repository');
412
+ await log(' • The repository belongs to another user/organization');
413
+ await log('');
414
+ await log(' 📋 HOW TO FIX THIS:');
415
+ await log('');
416
+ await log(' ┌──────────────────────────────────────────────────────────┐');
417
+ await log(' │ RECOMMENDED: Use the --fork option │');
418
+ await log(' └──────────────────────────────────────────────────────────┘');
419
+ await log('');
420
+ await log(' Run the command again with --fork:');
421
+ await log('');
422
+ await log(` ./solve.mjs "${issueUrl}" --fork`);
423
+ await log('');
424
+ await log(' This will automatically:');
425
+ if (userHasFork) {
426
+ await log(` ✓ Use your existing fork (${currentUser}/${forkRepoName})`);
427
+ await log(' ✓ Sync your fork with the latest changes');
428
+ } else {
429
+ await log(' ✓ Fork the repository to your account');
430
+ }
431
+ await log(' ✓ Push changes to your fork');
432
+ await log(' ✓ Create a PR from your fork to the original repo');
433
+ await log(' ✓ Handle all the remote setup automatically');
434
+ await log('');
435
+ await log(' ─────────────────────────────────────────────────────────');
436
+ await log('');
437
+ await log(' Alternative options:');
438
+ await log('');
439
+ await log(' Option 2: Request collaborator access');
440
+ await log(` ${'-'.repeat(40)}`);
441
+ await log(' Ask the repository owner to add you as a collaborator:');
442
+ await log(` → Go to: https://github.com/${owner}/${repo}/settings/access`);
443
+ await log('');
444
+ await log(' Option 3: Manual fork and clone');
445
+ await log(` ${'-'.repeat(40)}`);
446
+ await log(` 1. Fork the repo: https://github.com/${owner}/${repo}/fork`);
447
+ await log(' 2. Clone your fork and work there');
448
+ await log(' 3. Create a PR from your fork');
449
+ await log('');
450
+ await log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
451
+ await log('');
452
+ await log('💡 Tip: The --fork option automates the entire fork workflow!');
453
+ if (userHasFork) {
454
+ await log(` Note: We detected you already have a fork at ${currentUser}/${forkRepoName}`);
455
+ }
456
+ await log('');
457
+ throw new Error('Permission denied - need fork or collaborator access');
458
+ } else if (errorOutput.includes('non-fast-forward') || errorOutput.includes('rejected') || errorOutput.includes('! [rejected]')) {
459
+ // Push rejected due to conflicts or diverged history
460
+ await log('');
461
+ await log(formatAligned('❌', 'PUSH REJECTED:', 'Branch has diverged from remote'), { level: 'error' });
462
+ await log('');
463
+ await log(' 🔍 What happened:');
464
+ await log(' The remote branch has changes that conflict with your local changes.');
465
+ await log(' This typically means someone else has pushed to this branch.');
466
+ await log('');
467
+ await log(' 💡 Why we cannot fix this automatically:');
468
+ await log(' • We never use force push to preserve history');
469
+ await log(' • We never use rebase or reset to avoid altering git history');
470
+ await log(' • Manual conflict resolution is required');
471
+ await log('');
472
+ await log(' 🔧 How to fix:');
473
+ await log(' 1. Clone the repository and checkout the branch:');
474
+ await log(` git clone https://github.com/${owner}/${repo}.git`);
475
+ await log(` cd ${repo}`);
476
+ await log(` git checkout ${branchName}`);
477
+ await log('');
478
+ await log(' 2. Pull and merge the remote changes:');
479
+ await log(` git pull origin ${branchName}`);
480
+ await log('');
481
+ await log(' 3. Resolve any conflicts manually, then:');
482
+ await log(` git push origin ${branchName}`);
483
+ await log('');
484
+ await log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
485
+ await log('');
486
+ throw new Error('Push rejected - branch has diverged, manual resolution required');
487
+ } else {
488
+ // Other push errors
489
+ await log(`${formatAligned('❌', 'Failed to push:', 'See error below')}`, { level: 'error' });
490
+ await log(` Error: ${errorOutput}`, { level: 'error' });
491
+ throw new Error('Failed to push branch');
492
+ }
493
+ } else {
494
+ await log(`${formatAligned('✅', 'Branch pushed:', 'Successfully to remote')}`);
495
+ if (argv.verbose) {
496
+ await log(` Push output: ${pushResult.stdout.toString().trim()}`, { verbose: true });
497
+ }
498
+
499
+ // CRITICAL: Wait for GitHub to process the push before creating PR
500
+ // This prevents "No commits between branches" error
501
+ await log(' Waiting for GitHub to sync...');
502
+
503
+ // Use exponential backoff to wait for GitHub's compare API to see the commits
504
+ // This is essential because GitHub has multiple backend systems:
505
+ // - Git receive: Accepts push immediately
506
+ // - Branch API: Returns quickly from cache
507
+ // - Compare/PR API: May take longer to index commits
508
+ let compareReady = false;
509
+ let compareAttempts = 0;
510
+ const maxCompareAttempts = 5;
511
+ const targetBranchForCompare = argv.baseBranch || defaultBranch;
512
+ let compareResult; // Declare outside loop so it's accessible for error checking
513
+
514
+ while (!compareReady && compareAttempts < maxCompareAttempts) {
515
+ compareAttempts++;
516
+ const waitTime = Math.min(2000 * compareAttempts, 10000); // 2s, 4s, 6s, 8s, 10s
517
+
518
+ if (compareAttempts > 1) {
519
+ await log(` Retry ${compareAttempts}/${maxCompareAttempts}: Waiting ${waitTime}ms for GitHub to index commits...`);
520
+ }
521
+
522
+ await new Promise(resolve => setTimeout(resolve, waitTime));
523
+
524
+ // Check if GitHub's compare API can see commits between base and head
525
+ // This is the SAME API that gh pr create uses internally, so if this works,
526
+ // PR creation should work too
527
+ // For fork mode, we need to use forkUser:branchName format for the head
528
+ let headRef;
529
+ if (argv.fork && forkedRepo) {
530
+ const forkUser = forkedRepo.split('/')[0];
531
+ headRef = `${forkUser}:${branchName}`;
532
+ } else {
533
+ headRef = branchName;
534
+ }
535
+ compareResult = await $({ silent: true })`gh api repos/${owner}/${repo}/compare/${targetBranchForCompare}...${headRef} --jq '.ahead_by' 2>&1`;
536
+
537
+ if (compareResult.code === 0) {
538
+ const aheadBy = parseInt(compareResult.stdout.toString().trim(), 10);
539
+ if (argv.verbose) {
540
+ await log(` Compare API check: ${aheadBy} commit(s) ahead of ${targetBranchForCompare}`);
541
+ }
542
+
543
+ if (aheadBy > 0) {
544
+ compareReady = true;
545
+ await log(` GitHub compare API ready: ${aheadBy} commit(s) found`);
546
+ } else {
547
+ await log(` ⚠️ GitHub compare API shows 0 commits ahead (attempt ${compareAttempts}/${maxCompareAttempts})`, { level: 'warning' });
548
+ }
549
+ } else {
550
+ if (argv.verbose) {
551
+ await log(` Compare API error (attempt ${compareAttempts}/${maxCompareAttempts}): ${compareResult.stdout || compareResult.stderr || 'unknown'}`, { verbose: true });
552
+ }
553
+ }
554
+ }
555
+
556
+ if (!compareReady) {
557
+ // Check if this is a repository mismatch error (HTTP 404 from compare API)
558
+ let isRepositoryMismatch = false;
559
+ if (argv.fork && forkedRepo) {
560
+ // For fork mode, check the last compare API call result for 404
561
+ const lastCompareOutput = compareResult.stdout || compareResult.stderr || '';
562
+ if (lastCompareOutput.includes('HTTP 404') || lastCompareOutput.includes('Not Found')) {
563
+ isRepositoryMismatch = true;
564
+ }
565
+ }
566
+
567
+ if (isRepositoryMismatch) {
568
+ // BEFORE showing any error, verify if the repository is actually a GitHub fork
569
+ await log('');
570
+ await log(formatAligned('🔍', 'Investigating:', 'Checking fork relationship...'));
571
+
572
+ const forkInfoResult = await $({ silent: true })`gh api repos/${forkedRepo} --jq '{fork: .fork, parent: .parent.full_name, source: .source.full_name}' 2>&1`;
573
+
574
+ let isFork = false;
575
+ let parentRepo = null;
576
+ let sourceRepo = null;
577
+
578
+ if (forkInfoResult.code === 0) {
579
+ try {
580
+ const forkInfo = JSON.parse(forkInfoResult.stdout.toString().trim());
581
+ isFork = forkInfo.fork === true;
582
+ parentRepo = forkInfo.parent || null;
583
+ sourceRepo = forkInfo.source || null;
584
+ } catch {
585
+ // Failed to parse fork info
586
+ }
587
+ }
588
+
589
+ if (!isFork) {
590
+ // Repository is NOT a fork at all
591
+ await log('');
592
+ await log(formatAligned('❌', 'NOT A GITHUB FORK:', 'Repository is not a fork'), { level: 'error' });
593
+ await log('');
594
+ await log(' 🔍 What happened:');
595
+ await log(` The repository ${forkedRepo} is NOT a GitHub fork.`);
596
+ await log(' GitHub API reports: fork=false, parent=null');
597
+ await log('');
598
+ await log(' 💡 Why this happens:');
599
+ await log(' This repository was likely created by cloning and pushing (git clone + git push)');
600
+ await log(' instead of using GitHub\'s Fork button or API.');
601
+ await log('');
602
+ await log(' When a repository is created this way:');
603
+ await log(' • GitHub does not track it as a fork');
604
+ await log(' • It has no parent relationship with the original repository');
605
+ await log(' • Pull requests cannot be created to the original repository');
606
+ await log(' • Compare API returns 404 when comparing with unrelated repositories');
607
+ await log('');
608
+ await log(' 📦 Repository details:');
609
+ await log(' • Target repository: ' + `${owner}/${repo}`);
610
+ await log(' • Your repository: ' + forkedRepo);
611
+ await log(' • Fork status: false (NOT A FORK)');
612
+ await log('');
613
+ await log(' 🔧 How to fix:');
614
+ await log(' Option 1: Delete the non-fork repository and create a proper fork');
615
+ await log(` gh repo delete ${forkedRepo}`);
616
+ await log(` Then run this command again to create a proper GitHub fork of ${owner}/${repo}`);
617
+ await log('');
618
+ await log(' Option 2: Use --prefix-fork-name-with-owner-name to avoid name conflicts');
619
+ await log(` ./solve.mjs "https://github.com/${owner}/${repo}/issues/${issueNumber}" --prefix-fork-name-with-owner-name`);
620
+ await log(' This creates forks with names like "owner-repo" instead of just "repo"');
621
+ await log('');
622
+ await log(' Option 3: Work directly on the repository (if you have write access)');
623
+ await log(` ./solve.mjs "https://github.com/${owner}/${repo}/issues/${issueNumber}" --no-fork`);
624
+ await log('');
625
+
626
+ throw new Error('Repository is not a GitHub fork - cannot create PR to unrelated repository');
627
+ } else if (parentRepo !== `${owner}/${repo}` && sourceRepo !== `${owner}/${repo}`) {
628
+ // Repository IS a fork, but of a different repository
629
+ await log('');
630
+ await log(formatAligned('❌', 'WRONG FORK PARENT:', 'Fork is from different repository'), { level: 'error' });
631
+ await log('');
632
+ await log(' 🔍 What happened:');
633
+ await log(` The repository ${forkedRepo} IS a GitHub fork,`);
634
+ await log(` but it's a fork of a DIFFERENT repository than ${owner}/${repo}.`);
635
+ await log('');
636
+ await log(' 📦 Fork relationship:');
637
+ await log(' • Your fork: ' + forkedRepo);
638
+ await log(' • Fork parent: ' + (parentRepo || 'unknown'));
639
+ await log(' • Fork source: ' + (sourceRepo || 'unknown'));
640
+ await log(' • Target repository: ' + `${owner}/${repo}`);
641
+ await log('');
642
+ await log(' 💡 Why this happens:');
643
+ await log(' You have an existing fork from a different repository');
644
+ await log(' that shares the same name but is from a different source.');
645
+ await log(' GitHub treats forks hierarchically - each fork tracks its root repository.');
646
+ await log('');
647
+ await log(' 🔧 How to fix:');
648
+ await log(' Option 1: Delete the conflicting fork and create a new one');
649
+ await log(` gh repo delete ${forkedRepo}`);
650
+ await log(` Then run this command again to create a proper fork of ${owner}/${repo}`);
651
+ await log('');
652
+ await log(' Option 2: Use --prefix-fork-name-with-owner-name to avoid conflicts');
653
+ await log(` ./solve.mjs "https://github.com/${owner}/${repo}/issues/${issueNumber}" --prefix-fork-name-with-owner-name`);
654
+ await log(' This creates forks with names like "owner-repo" instead of just "repo"');
655
+ await log('');
656
+ await log(' Option 3: Work directly on the repository (if you have write access)');
657
+ await log(` ./solve.mjs "https://github.com/${owner}/${repo}/issues/${issueNumber}" --no-fork`);
658
+ await log('');
659
+
660
+ throw new Error('Fork parent mismatch - fork is from different repository tree');
661
+ } else {
662
+ // Repository is a fork of the correct parent, but compare API still failed
663
+ // This is unexpected - show detailed error
664
+ await log('');
665
+ await log(formatAligned('❌', 'COMPARE API ERROR:', 'Unexpected failure'), { level: 'error' });
666
+ await log('');
667
+ await log(' 🔍 What happened:');
668
+ await log(` The repository ${forkedRepo} is a valid fork of ${owner}/${repo},`);
669
+ await log(' but GitHub\'s compare API still returned an error.');
670
+ await log('');
671
+ await log(' 📦 Fork verification:');
672
+ await log(' • Your fork: ' + forkedRepo);
673
+ await log(' • Fork status: true (VALID FORK)');
674
+ await log(' • Fork parent: ' + (parentRepo || 'unknown'));
675
+ await log(' • Target repository: ' + `${owner}/${repo}`);
676
+ await log('');
677
+ await log(' 💡 This is unexpected:');
678
+ await log(' The fork relationship is correct, but the compare API failed.');
679
+ await log(' This might be a temporary GitHub API issue.');
680
+ await log('');
681
+ await log(' 🔧 How to fix:');
682
+ await log(' 1. Wait a minute and try creating the PR manually:');
683
+ if (argv.fork && forkedRepo) {
684
+ const forkUser = forkedRepo.split('/')[0];
685
+ await log(` gh pr create --draft --repo ${owner}/${repo} --base ${targetBranchForCompare} --head ${forkUser}:${branchName}`);
686
+ }
687
+ await log(' 2. Check if the issue persists - it might be a GitHub API outage');
688
+ await log('');
689
+
690
+ throw new Error('Compare API failed unexpectedly despite valid fork relationship');
691
+ }
692
+ } else {
693
+ // Original timeout error for other cases
694
+ await log('');
695
+ await log(formatAligned('❌', 'GITHUB SYNC TIMEOUT:', 'Compare API not ready after retries'), { level: 'error' });
696
+ await log('');
697
+ await log(' 🔍 What happened:');
698
+ await log(` After ${maxCompareAttempts} attempts, GitHub's compare API still shows no commits`);
699
+ await log(` between ${targetBranchForCompare} and ${branchName}.`);
700
+ await log('');
701
+ await log(' 💡 This usually means:');
702
+ await log(' • GitHub\'s backend systems haven\'t finished indexing the push');
703
+ await log(' • There\'s a temporary issue with GitHub\'s API');
704
+ await log(' • The commits may not have been pushed correctly');
705
+ await log('');
706
+ await log(' 🔧 How to fix:');
707
+ await log(' 1. Wait a minute and try creating the PR manually:');
708
+ // For fork mode, use the correct head reference format
709
+ if (argv.fork && forkedRepo) {
710
+ const forkUser = forkedRepo.split('/')[0];
711
+ await log(` gh pr create --draft --repo ${owner}/${repo} --base ${targetBranchForCompare} --head ${forkUser}:${branchName}`);
712
+ } else {
713
+ await log(` gh pr create --draft --repo ${owner}/${repo} --base ${targetBranchForCompare} --head ${branchName}`);
714
+ }
715
+ await log(' 2. Check if the branch exists on GitHub:');
716
+ // Show the correct repository where the branch was pushed
717
+ const branchRepo = (argv.fork && forkedRepo) ? forkedRepo : `${owner}/${repo}`;
718
+ await log(` https://github.com/${branchRepo}/tree/${branchName}`);
719
+ await log(' 3. Check the commit is on GitHub:');
720
+ // Use the correct head reference for the compare API check
721
+ if (argv.fork && forkedRepo) {
722
+ const forkUser = forkedRepo.split('/')[0];
723
+ await log(` gh api repos/${owner}/${repo}/compare/${targetBranchForCompare}...${forkUser}:${branchName}`);
724
+ } else {
725
+ await log(` gh api repos/${owner}/${repo}/compare/${targetBranchForCompare}...${branchName}`);
726
+ }
727
+ await log('');
728
+
729
+ throw new Error('GitHub compare API not ready - cannot create PR safely');
730
+ }
731
+ }
732
+
733
+ // Verify the push actually worked by checking GitHub API
734
+ // When using fork mode, check the fork repository; otherwise check the original repository
735
+ const repoToCheck = (argv.fork && forkedRepo) ? forkedRepo : `${owner}/${repo}`;
736
+ const branchCheckResult = await $({ silent: true })`gh api repos/${repoToCheck}/branches/${branchName} --jq .name 2>&1`;
737
+ if (branchCheckResult.code === 0 && branchCheckResult.stdout.toString().trim() === branchName) {
738
+ await log(` Branch verified on GitHub: ${branchName}`);
739
+
740
+ // Get the commit SHA from GitHub
741
+ const shaCheckResult = await $({ silent: true })`gh api repos/${repoToCheck}/branches/${branchName} --jq .commit.sha 2>&1`;
742
+ if (shaCheckResult.code === 0) {
743
+ const remoteSha = shaCheckResult.stdout.toString().trim();
744
+ await log(` Remote commit SHA: ${remoteSha.substring(0, 7)}...`);
745
+ }
746
+ } else {
747
+ await log(' Warning: Branch not found on GitHub!');
748
+ await log(' This will cause PR creation to fail.');
749
+
750
+ if (argv.verbose) {
751
+ await log(` Branch check result: ${branchCheckResult.stdout || branchCheckResult.stderr || 'empty'}`);
752
+
753
+ // Show all branches on GitHub
754
+ const allBranchesResult = await $({ silent: true })`gh api repos/${repoToCheck}/branches --jq '.[].name' 2>&1`;
755
+ if (allBranchesResult.code === 0) {
756
+ await log(` All GitHub branches: ${allBranchesResult.stdout.toString().split('\n').slice(0, 5).join(', ')}...`);
757
+ }
758
+ }
759
+
760
+ // Try one more push with explicit ref (without force)
761
+ await log(' Attempting explicit push...');
762
+ const explicitPushCmd = `git push origin HEAD:refs/heads/${branchName}`;
763
+ if (argv.verbose) {
764
+ await log(` Command: ${explicitPushCmd}`);
765
+ }
766
+ const explicitPushResult = await $`cd ${tempDir} && ${explicitPushCmd} 2>&1`;
767
+ if (explicitPushResult.code === 0) {
768
+ await log(' Explicit push completed');
769
+ if (argv.verbose && explicitPushResult.stdout) {
770
+ await log(` Output: ${explicitPushResult.stdout.toString().trim()}`);
771
+ }
772
+ // Wait a bit more for GitHub to process
773
+ await new Promise(resolve => setTimeout(resolve, 3000));
774
+ } else {
775
+ await log(' ERROR: Cannot push to GitHub!');
776
+ await log(` Error: ${explicitPushResult.stderr || explicitPushResult.stdout || 'Unknown'}`);
777
+ await log(' Force push is not allowed to preserve history');
778
+ }
779
+ }
780
+
781
+ // Get issue title for PR title
782
+ await log(formatAligned('📋', 'Getting issue:', 'Title from GitHub...'), { verbose: true });
783
+ const issueTitleResult = await $({ silent: true })`gh api repos/${owner}/${repo}/issues/${issueNumber} --jq .title 2>&1`;
784
+ let issueTitle = `Fix issue #${issueNumber}`;
785
+ if (issueTitleResult.code === 0) {
786
+ issueTitle = issueTitleResult.stdout.toString().trim();
787
+ await log(` Issue title: "${issueTitle}"`, { verbose: true });
788
+ } else {
789
+ await log(' Warning: Could not get issue title, using default', { verbose: true });
790
+ }
791
+
792
+ // Get current GitHub user to set as assignee (but validate it's a collaborator)
793
+ await log(formatAligned('👤', 'Getting user:', 'Current GitHub account...'), { verbose: true });
794
+ const currentUserResult = await $({ silent: true })`gh api user --jq .login 2>&1`;
795
+ let currentUser = null;
796
+ let canAssign = false;
797
+
798
+ if (currentUserResult.code === 0) {
799
+ currentUser = currentUserResult.stdout.toString().trim();
800
+ await log(` Current user: ${currentUser}`, { verbose: true });
801
+
802
+ // Check if user has push access (is a collaborator or owner)
803
+ // IMPORTANT: We need to completely suppress the JSON error output
804
+ // Using async exec to have full control over stderr
805
+ try {
806
+ const { exec } = await import('child_process');
807
+ const { promisify } = await import('util');
808
+ const execAsync = promisify(exec);
809
+ // This will throw if user doesn't have access, but won't print anything
810
+ await execAsync(`gh api repos/${owner}/${repo}/collaborators/${currentUser} 2>/dev/null`,
811
+ { encoding: 'utf8', env: process.env });
812
+ canAssign = true;
813
+ await log(' User has collaborator access', { verbose: true });
814
+ } catch (e) {
815
+ reportError(e, {
816
+ context: 'collaborator_check',
817
+ owner,
818
+ repo,
819
+ currentUser,
820
+ operation: 'check_collaborator_access'
821
+ });
822
+ // User doesn't have permission, but that's okay - we just won't assign
823
+ canAssign = false;
824
+ await log(' User is not a collaborator (will skip assignment)', { verbose: true });
825
+ }
826
+
827
+ // Set permCheckResult for backward compatibility
828
+ const permCheckResult = { code: canAssign ? 0 : 1 };
829
+ if (permCheckResult.code === 0) {
830
+ canAssign = true;
831
+ await log(' User has collaborator access', { verbose: true });
832
+ } else {
833
+ // User doesn't have permission, but that's okay - we just won't assign
834
+ await log(' User is not a collaborator (will skip assignment)', { verbose: true });
835
+ }
836
+ } else {
837
+ await log(' Warning: Could not get current user', { verbose: true });
838
+ }
839
+
840
+ // Fetch latest state of target branch to ensure accurate comparison
841
+ const targetBranch = argv.baseBranch || defaultBranch;
842
+ await log(formatAligned('🔄', 'Fetching:', `Latest ${targetBranch} branch...`));
843
+ const fetchBaseResult = await $({ cwd: tempDir, silent: true })`git fetch origin ${targetBranch}:refs/remotes/origin/${targetBranch} 2>&1`;
844
+
845
+ if (fetchBaseResult.code !== 0) {
846
+ await log(`⚠️ Warning: Could not fetch latest ${targetBranch}`, { level: 'warning' });
847
+ if (argv.verbose) {
848
+ await log(` Fetch output: ${fetchBaseResult.stdout || fetchBaseResult.stderr || 'none'}`, { verbose: true });
849
+ }
850
+ } else {
851
+ await log(formatAligned('✅', 'Base updated:', `Fetched latest ${targetBranch}`));
852
+ }
853
+
854
+ // Verify there are commits between base and head before attempting PR creation
855
+ await log(formatAligned('🔍', 'Checking:', 'Commits between branches...'));
856
+ const commitCheckResult = await $({ cwd: tempDir, silent: true })`git rev-list --count origin/${targetBranch}..HEAD 2>&1`;
857
+
858
+ if (commitCheckResult.code === 0) {
859
+ const commitCount = parseInt(commitCheckResult.stdout.toString().trim(), 10);
860
+ if (argv.verbose) {
861
+ await log(` Commits ahead of origin/${targetBranch}: ${commitCount}`, { verbose: true });
862
+ }
863
+
864
+ if (commitCount === 0) {
865
+ // Check if the branch was already merged
866
+ const mergedCheckResult = await $({ cwd: tempDir, silent: true })`git branch -r --merged origin/${targetBranch} | grep -q "origin/${branchName}" 2>&1`;
867
+ const wasAlreadyMerged = mergedCheckResult.code === 0;
868
+
869
+ // No commits to create PR - branch is up to date with base or behind it
870
+ await log('');
871
+ await log(formatAligned('❌', 'NO COMMITS TO CREATE PR', ''), { level: 'error' });
872
+ await log('');
873
+ await log(' 🔍 What happened:');
874
+ await log(` The branch ${branchName} has no new commits compared to ${targetBranch}.`);
875
+
876
+ if (wasAlreadyMerged) {
877
+ await log(` ✅ This branch was already merged into ${targetBranch}.`);
878
+ await log('');
879
+ await log(' 📋 Branch Status: ALREADY MERGED');
880
+ await log('');
881
+ await log(' 💡 This means:');
882
+ await log(' • The work on this branch has been completed and integrated');
883
+ await log(' • A new branch should be created for any additional work');
884
+ await log(' • The issue may already be resolved');
885
+ } else {
886
+ await log(` This means all commits in this branch are already in ${targetBranch}.`);
887
+ }
888
+
889
+ await log('');
890
+ await log(' 💡 Possible causes:');
891
+ if (wasAlreadyMerged) {
892
+ await log(' • ✅ The branch was already merged (confirmed)');
893
+ } else {
894
+ await log(' • The branch was already merged');
895
+ }
896
+ await log(' • The branch is outdated and needs to be rebased');
897
+ await log(` • Local ${targetBranch} is outdated (though we just fetched it)`);
898
+ await log('');
899
+ await log(' 🔧 How to fix:');
900
+ await log('');
901
+
902
+ if (wasAlreadyMerged) {
903
+ await log(' Option 1: Check the merged PR and close the issue');
904
+ await log(` gh pr list --repo ${owner}/${repo} --head ${branchName} --state merged`);
905
+ await log(' If the issue is resolved, close it. Otherwise, create a new branch.');
906
+ } else {
907
+ await log(' Option 1: Check if branch was already merged');
908
+ await log(` gh pr list --repo ${owner}/${repo} --head ${branchName} --state merged`);
909
+ await log(' If merged, you may want to close the related issue or create a new branch');
910
+ }
911
+
912
+ await log('');
913
+ await log(' Option 2: Verify branch state');
914
+ await log(` cd ${tempDir}`);
915
+ await log(` git log ${targetBranch}..${branchName} --oneline`);
916
+ await log(` git log origin/${targetBranch}..${branchName} --oneline`);
917
+ await log('');
918
+ await log(' Option 3: Create new commits on this branch');
919
+ await log(' The branch exists but has no new work to contribute');
920
+ await log('');
921
+
922
+ if (wasAlreadyMerged) {
923
+ throw new Error('Branch was already merged into base - cannot create PR');
924
+ } else {
925
+ throw new Error('No commits between base and head - cannot create PR');
926
+ }
927
+ } else {
928
+ await log(formatAligned('✅', 'Commits found:', `${commitCount} commit(s) ahead`));
929
+ }
930
+ } else {
931
+ await log('⚠️ Warning: Could not verify commit count', { level: 'warning' });
932
+ if (argv.verbose) {
933
+ await log(` Check output: ${commitCheckResult.stdout || commitCheckResult.stderr || 'none'}`, { verbose: true });
934
+ }
935
+ }
936
+
937
+ // Create draft pull request
938
+ await log(formatAligned('🔀', 'Creating PR:', 'Draft pull request...'));
939
+ if (argv.baseBranch) {
940
+ await log(formatAligned('🎯', 'Target branch:', `${targetBranch} (custom)`));
941
+ } else {
942
+ await log(formatAligned('🎯', 'Target branch:', `${targetBranch} (default)`));
943
+ }
944
+
945
+ // Use full repository reference for cross-repo PRs (forks)
946
+ const issueRef = argv.fork ? `${owner}/${repo}#${issueNumber}` : `#${issueNumber}`;
947
+
948
+ const prBody = `## 🤖 AI-Powered Solution Draft
949
+
950
+ This pull request is being automatically generated to solve issue ${issueRef}.
951
+
952
+ ### 📋 Issue Reference
953
+ Fixes ${issueRef}
954
+
955
+ ### 🚧 Status
956
+ **Work in Progress** - The AI assistant is currently analyzing and implementing the solution draft.
957
+
958
+ ### 📝 Implementation Details
959
+ _Details will be added as the solution draft is developed..._
960
+
961
+ ---
962
+ *This PR was created automatically by the AI issue solver*`;
963
+
964
+ if (argv.verbose) {
965
+ await log(` PR Title: [WIP] ${issueTitle}`, { verbose: true });
966
+ await log(` Base branch: ${defaultBranch}`, { verbose: true });
967
+ await log(` Head branch: ${branchName}`, { verbose: true });
968
+ if (currentUser) {
969
+ await log(` Assignee: ${currentUser}`, { verbose: true });
970
+ }
971
+ await log(` PR Body:
972
+ ${prBody}`, { verbose: true });
973
+ }
974
+
975
+ // Use async exec for gh pr create to avoid command-stream output issues
976
+ // Similar to how create-test-repo.mjs handles it
977
+ try {
978
+ const { exec } = await import('child_process');
979
+ const { promisify } = await import('util');
980
+ const execAsync = promisify(exec);
981
+
982
+ // Write PR body to temp file to avoid shell escaping issues
983
+ const prBodyFile = `/tmp/pr-body-${Date.now()}.md`;
984
+ await fs.writeFile(prBodyFile, prBody);
985
+
986
+ // Write PR title to temp file to avoid shell escaping issues with quotes/apostrophes
987
+ // This solves the issue where titles containing apostrophes (e.g., "don't") would cause
988
+ // "Unterminated quoted string" errors
989
+ const prTitle = `[WIP] ${issueTitle}`;
990
+ const prTitleFile = `/tmp/pr-title-${Date.now()}.txt`;
991
+ await fs.writeFile(prTitleFile, prTitle);
992
+
993
+ // Build command with optional assignee and handle forks
994
+ // Note: targetBranch is already defined above
995
+ // IMPORTANT: Use --title-file instead of --title to avoid shell parsing issues with special characters
996
+ let command;
997
+ if (argv.fork && forkedRepo) {
998
+ // For forks, specify the full head reference
999
+ const forkUser = forkedRepo.split('/')[0];
1000
+ command = `cd "${tempDir}" && gh pr create --draft --title "$(cat '${prTitleFile}')" --body-file "${prBodyFile}" --base ${targetBranch} --head ${forkUser}:${branchName} --repo ${owner}/${repo}`;
1001
+ } else {
1002
+ command = `cd "${tempDir}" && gh pr create --draft --title "$(cat '${prTitleFile}')" --body-file "${prBodyFile}" --base ${targetBranch} --head ${branchName}`;
1003
+ }
1004
+ // Only add assignee if user has permissions
1005
+ if (currentUser && canAssign) {
1006
+ command += ` --assignee ${currentUser}`;
1007
+ }
1008
+
1009
+ if (argv.verbose) {
1010
+ await log(` Command: ${command}`, { verbose: true });
1011
+ }
1012
+
1013
+ let output;
1014
+ let assigneeFailed = false;
1015
+
1016
+ // Try to create PR with assignee first (if specified)
1017
+ try {
1018
+ const result = await execAsync(command, { encoding: 'utf8', cwd: tempDir, env: process.env });
1019
+ output = result.stdout;
1020
+ } catch (firstError) {
1021
+ // Check if the error is specifically about assignee validation
1022
+ const errorMsg = firstError.message || '';
1023
+ if ((errorMsg.includes('could not assign user') || errorMsg.includes('not found')) && currentUser && canAssign) {
1024
+ // Assignee validation failed - retry without assignee
1025
+ assigneeFailed = true;
1026
+ await log('');
1027
+ await log(formatAligned('⚠️', 'Warning:', `User assignment failed for '${currentUser}'`), { level: 'warning' });
1028
+ await log(' Retrying PR creation without assignee...');
1029
+
1030
+ // Rebuild command without --assignee flag
1031
+ if (argv.fork && forkedRepo) {
1032
+ const forkUser = forkedRepo.split('/')[0];
1033
+ command = `cd "${tempDir}" && gh pr create --draft --title "$(cat '${prTitleFile}')" --body-file "${prBodyFile}" --base ${targetBranch} --head ${forkUser}:${branchName} --repo ${owner}/${repo}`;
1034
+ } else {
1035
+ command = `cd "${tempDir}" && gh pr create --draft --title "$(cat '${prTitleFile}')" --body-file "${prBodyFile}" --base ${targetBranch} --head ${branchName}`;
1036
+ }
1037
+
1038
+ if (argv.verbose) {
1039
+ await log(` Retry command (without assignee): ${command}`, { verbose: true });
1040
+ }
1041
+
1042
+ // Retry without assignee - if this fails, let the error propagate to outer catch
1043
+ const retryResult = await execAsync(command, { encoding: 'utf8', cwd: tempDir, env: process.env });
1044
+ output = retryResult.stdout;
1045
+ } else {
1046
+ // Not an assignee error, re-throw the original error
1047
+ throw firstError;
1048
+ }
1049
+ }
1050
+
1051
+ // Clean up temp files
1052
+ await fs.unlink(prBodyFile).catch((unlinkError) => {
1053
+ reportError(unlinkError, {
1054
+ context: 'pr_body_file_cleanup',
1055
+ prBodyFile,
1056
+ operation: 'delete_temp_file'
1057
+ });
1058
+ });
1059
+ await fs.unlink(prTitleFile).catch((unlinkError) => {
1060
+ reportError(unlinkError, {
1061
+ context: 'pr_title_file_cleanup',
1062
+ prTitleFile,
1063
+ operation: 'delete_temp_file'
1064
+ });
1065
+ });
1066
+
1067
+ // Extract PR URL from output - gh pr create outputs the URL to stdout
1068
+ prUrl = output.trim();
1069
+
1070
+ if (!prUrl) {
1071
+ await log('⚠️ Warning: PR created but no URL returned', { level: 'warning' });
1072
+ await log(` Output: ${output}`, { verbose: true });
1073
+
1074
+ // Try to get the PR URL using gh pr list
1075
+ await log(' Attempting to find PR using gh pr list...', { verbose: true });
1076
+ const prListResult = await $`cd ${tempDir} && gh pr list --head ${branchName} --json url --jq '.[0].url'`;
1077
+ if (prListResult.code === 0 && prListResult.stdout.toString().trim()) {
1078
+ prUrl = prListResult.stdout.toString().trim();
1079
+ await log(` Found PR URL: ${prUrl}`, { verbose: true });
1080
+ }
1081
+ }
1082
+
1083
+ // Extract PR number from URL
1084
+ if (prUrl) {
1085
+ const prMatch = prUrl.match(/\/pull\/(\d+)/);
1086
+ if (prMatch) {
1087
+ localPrNumber = prMatch[1];
1088
+
1089
+ // CRITICAL: Verify the PR was actually created by querying GitHub API
1090
+ // This is essential because gh pr create can return a URL but PR creation might have failed
1091
+ await log(formatAligned('🔍', 'Verifying:', 'PR creation...'), { verbose: true });
1092
+ const verifyResult = await $({ silent: true })`gh pr view ${localPrNumber} --repo ${owner}/${repo} --json number,url,state 2>&1`;
1093
+
1094
+ if (verifyResult.code === 0) {
1095
+ try {
1096
+ const prData = JSON.parse(verifyResult.stdout.toString().trim());
1097
+ if (prData.number && prData.url) {
1098
+ await log(formatAligned('✅', 'Verification:', 'PR exists on GitHub'), { verbose: true });
1099
+ // Update prUrl and localPrNumber from verified data
1100
+ prUrl = prData.url;
1101
+ localPrNumber = String(prData.number);
1102
+ } else {
1103
+ throw new Error('PR data incomplete');
1104
+ }
1105
+ } catch {
1106
+ await log('❌ PR verification failed: Could not parse PR data', { level: 'error' });
1107
+ throw new Error('PR creation verification failed - invalid response');
1108
+ }
1109
+ } else {
1110
+ // PR does not exist - gh pr create must have failed silently
1111
+ await log('');
1112
+ await log(formatAligned('❌', 'FATAL ERROR:', 'PR creation failed'), { level: 'error' });
1113
+ await log('');
1114
+ await log(' 🔍 What happened:');
1115
+ await log(' The gh pr create command returned a URL, but the PR does not exist on GitHub.');
1116
+ await log('');
1117
+ await log(' 🔧 How to fix:');
1118
+ await log(' 1. Check if PR exists manually:');
1119
+ await log(` gh pr list --repo ${owner}/${repo} --head ${branchName}`);
1120
+ await log(' 2. Try creating PR manually:');
1121
+ await log(` cd ${tempDir}`);
1122
+ await log(` gh pr create --draft --title "Fix issue #${issueNumber}" --body "Fixes #${issueNumber}"`);
1123
+ await log(' 3. Check GitHub authentication:');
1124
+ await log(' gh auth status');
1125
+ await log('');
1126
+ throw new Error('PR creation failed - PR does not exist on GitHub');
1127
+ }
1128
+ // Store PR info globally for error handlers
1129
+ global.createdPR = { number: localPrNumber, url: prUrl };
1130
+ await log(formatAligned('✅', 'PR created:', `#${localPrNumber}`));
1131
+ await log(formatAligned('📍', 'PR URL:', prUrl));
1132
+ if (assigneeFailed) {
1133
+ // Show detailed information about why assignee failed and how to fix it
1134
+ await log('');
1135
+ await log(formatAligned('⚠️', 'Assignee Note:', 'PR created without assignee'));
1136
+ await log('');
1137
+ await log(` The PR was created successfully, but user '${currentUser}' could not be assigned.`);
1138
+ await log('');
1139
+ await log(' 📋 Why this happened:');
1140
+ await log(` • User '${currentUser}' may not have collaborator access to ${owner}/${repo}`);
1141
+ await log(' • GitHub requires users to be repository collaborators to be assigned');
1142
+ await log(' • The GitHub CLI enforces strict assignee validation');
1143
+ await log('');
1144
+ await log(' 🔧 How to fix:');
1145
+ await log('');
1146
+ await log(' Option 1: Assign manually in the PR page');
1147
+ await log(' ─────────────────────────────────────────');
1148
+ await log(` 1. Visit the PR: ${prUrl}`);
1149
+ await log(' 2. Click "Assignees" in the right sidebar');
1150
+ await log(' 3. Add yourself to the PR');
1151
+ await log('');
1152
+ await log(' Option 2: Request collaborator access');
1153
+ await log(' ─────────────────────────────────────────');
1154
+ await log(' Ask the repository owner to add you as a collaborator:');
1155
+ await log(` → Go to: https://github.com/${owner}/${repo}/settings/access`);
1156
+ await log(` → Add user: ${currentUser}`);
1157
+ await log('');
1158
+ await log(' ℹ️ Note: This does not affect the PR itself - it was created successfully.');
1159
+ await log('');
1160
+ } else if (currentUser && canAssign) {
1161
+ await log(formatAligned('👤', 'Assigned to:', currentUser));
1162
+ } else if (currentUser && !canAssign) {
1163
+ await log(formatAligned('ℹ️', 'Note:', 'Could not assign (no permission)'));
1164
+ }
1165
+
1166
+ // CLAUDE.md will be removed after Claude command completes
1167
+
1168
+ // Link the issue to the PR in GitHub's Development section using GraphQL API
1169
+ await log(formatAligned('🔗', 'Linking:', `Issue #${issueNumber} to PR #${localPrNumber}...`));
1170
+ try {
1171
+ // First, get the node IDs for both the issue and the PR
1172
+ const issueNodeResult = await $`gh api graphql -f query='query { repository(owner: "${owner}", name: "${repo}") { issue(number: ${issueNumber}) { id } } }' --jq .data.repository.issue.id`;
1173
+
1174
+ if (issueNodeResult.code !== 0) {
1175
+ throw new Error(`Failed to get issue node ID: ${issueNodeResult.stderr}`);
1176
+ }
1177
+
1178
+ const issueNodeId = issueNodeResult.stdout.toString().trim();
1179
+ await log(` Issue node ID: ${issueNodeId}`, { verbose: true });
1180
+
1181
+ const prNodeResult = await $`gh api graphql -f query='query { repository(owner: "${owner}", name: "${repo}") { pullRequest(number: ${localPrNumber}) { id } } }' --jq .data.repository.pullRequest.id`;
1182
+
1183
+ if (prNodeResult.code !== 0) {
1184
+ throw new Error(`Failed to get PR node ID: ${prNodeResult.stderr}`);
1185
+ }
1186
+
1187
+ const prNodeId = prNodeResult.stdout.toString().trim();
1188
+ await log(` PR node ID: ${prNodeId}`, { verbose: true });
1189
+
1190
+ // Now link them using the GraphQL mutation
1191
+ // GitHub automatically creates the link when we use "Fixes #" or "Fixes owner/repo#"
1192
+ // The Development section link is created automatically by GitHub when:
1193
+ // 1. The PR body contains "Fixes #N", "Closes #N", or "Resolves #N"
1194
+ // 2. For cross-repo (fork) PRs, we need "Fixes owner/repo#N"
1195
+
1196
+ // Let's verify the link was created
1197
+ const linkCheckResult = await $`gh api graphql -f query='query { repository(owner: "${owner}", name: "${repo}") { pullRequest(number: ${localPrNumber}) { closingIssuesReferences(first: 10) { nodes { number } } } } }' --jq '.data.repository.pullRequest.closingIssuesReferences.nodes[].number'`;
1198
+
1199
+ if (linkCheckResult.code === 0) {
1200
+ const linkedIssues = linkCheckResult.stdout.toString().trim().split('\n').filter(n => n);
1201
+ if (linkedIssues.includes(issueNumber)) {
1202
+ await log(formatAligned('✅', 'Link verified:', `Issue #${issueNumber} → PR #${localPrNumber}`));
1203
+ } else {
1204
+ // This is a problem - the link wasn't created
1205
+ await log('');
1206
+ await log(formatAligned('⚠️', 'ISSUE LINK MISSING:', 'PR not linked to issue'), { level: 'warning' });
1207
+ await log('');
1208
+
1209
+ if (argv.fork) {
1210
+ await log(' The PR was created from a fork but wasn\'t linked to the issue.', { level: 'warning' });
1211
+ await log(` Expected: "Fixes ${owner}/${repo}#${issueNumber}" in PR body`, { level: 'warning' });
1212
+ await log('');
1213
+ await log(' To fix manually:', { level: 'warning' });
1214
+ await log(` 1. Edit the PR description at: ${prUrl}`, { level: 'warning' });
1215
+ await log(` 2. Add this line: Fixes ${owner}/${repo}#${issueNumber}`, { level: 'warning' });
1216
+ } else {
1217
+ await log(` The PR wasn't linked to issue #${issueNumber}`, { level: 'warning' });
1218
+ await log(` Expected: "Fixes #${issueNumber}" in PR body`, { level: 'warning' });
1219
+ await log('');
1220
+ await log(' To fix manually:', { level: 'warning' });
1221
+ await log(` 1. Edit the PR description at: ${prUrl}`, { level: 'warning' });
1222
+ await log(` 2. Ensure it contains: Fixes #${issueNumber}`, { level: 'warning' });
1223
+ }
1224
+ await log('');
1225
+ }
1226
+ } else {
1227
+ // Could not verify but show what should have been used
1228
+ const expectedRef = argv.fork ? `${owner}/${repo}#${issueNumber}` : `#${issueNumber}`;
1229
+ await log('⚠️ Could not verify issue link (API error)', { level: 'warning' });
1230
+ await log(` PR body should contain: "Fixes ${expectedRef}"`, { level: 'warning' });
1231
+ await log(` Please verify manually at: ${prUrl}`, { level: 'warning' });
1232
+ }
1233
+ } catch (linkError) {
1234
+ reportError(linkError, {
1235
+ context: 'pr_issue_link_verification',
1236
+ prUrl,
1237
+ issueNumber,
1238
+ operation: 'verify_issue_link'
1239
+ });
1240
+ const expectedRef = argv.fork ? `${owner}/${repo}#${issueNumber}` : `#${issueNumber}`;
1241
+ await log(`⚠️ Could not verify issue linking: ${linkError.message}`, { level: 'warning' });
1242
+ await log(` PR body should contain: "Fixes ${expectedRef}"`, { level: 'warning' });
1243
+ await log(` Please check manually at: ${prUrl}`, { level: 'warning' });
1244
+ }
1245
+ } else {
1246
+ await log(formatAligned('✅', 'PR created:', 'Successfully'));
1247
+ await log(formatAligned('📍', 'PR URL:', prUrl));
1248
+ }
1249
+
1250
+ // CLAUDE.md will be removed after Claude command completes
1251
+ } else {
1252
+ await log('⚠️ Draft pull request created but URL could not be determined', { level: 'warning' });
1253
+ }
1254
+ } catch (prCreateError) {
1255
+ reportError(prCreateError, {
1256
+ context: 'pr_creation',
1257
+ issueNumber,
1258
+ branchName,
1259
+ operation: 'create_pull_request'
1260
+ });
1261
+ const errorMsg = prCreateError.message || '';
1262
+
1263
+ // Clean up the error message - extract the meaningful part
1264
+ let cleanError = errorMsg;
1265
+ if (errorMsg.includes('pull request create failed:')) {
1266
+ cleanError = errorMsg.split('pull request create failed:')[1].trim();
1267
+ } else if (errorMsg.includes('Command failed:')) {
1268
+ // Extract just the error part, not the full command
1269
+ const lines = errorMsg.split('\n');
1270
+ cleanError = lines[lines.length - 1] || errorMsg;
1271
+ }
1272
+
1273
+ // Check for specific error types
1274
+ // Note: Assignee errors are now handled by automatic retry in the try block above
1275
+ // This catch block only handles other types of PR creation failures
1276
+ if (errorMsg.includes('No commits between') || errorMsg.includes('Head sha can\'t be blank')) {
1277
+ // Empty PR error
1278
+ await log('');
1279
+ await log(formatAligned('❌', 'PR CREATION FAILED', ''), { level: 'error' });
1280
+ await log('');
1281
+ await log(' 🔍 What happened:');
1282
+ await log(' Cannot create PR - no commits between branches.');
1283
+ await log('');
1284
+ await log(' 📦 Error details:');
1285
+ for (const line of cleanError.split('\n')) {
1286
+ if (line.trim()) await log(` ${line.trim()}`);
1287
+ }
1288
+ await log('');
1289
+ await log(' 💡 Possible causes:');
1290
+ await log(' • The branch wasn\'t pushed properly');
1291
+ await log(' • The commit wasn\'t created');
1292
+ await log(' • GitHub sync issue');
1293
+ await log('');
1294
+ await log(' 🔧 How to fix:');
1295
+ await log(' 1. Verify commit exists:');
1296
+ await log(` cd ${tempDir} && git log --format="%h %s" -5`);
1297
+ await log(' 2. Push again with tracking:');
1298
+ await log(` cd ${tempDir} && git push -u origin ${branchName}`);
1299
+ await log(' 3. Create PR manually:');
1300
+ await log(` cd ${tempDir} && gh pr create --draft`);
1301
+ await log('');
1302
+ await log(` 📂 Working directory: ${tempDir}`);
1303
+ await log(` 🌿 Current branch: ${branchName}`);
1304
+ await log('');
1305
+ throw new Error('PR creation failed - no commits between branches');
1306
+ } else {
1307
+ // Generic PR creation error
1308
+ await log('');
1309
+ await log(formatAligned('❌', 'PR CREATION FAILED', ''), { level: 'error' });
1310
+ await log('');
1311
+ await log(' 🔍 What happened:');
1312
+ await log(' Failed to create pull request.');
1313
+ await log('');
1314
+ await log(' 📦 Error details:');
1315
+ for (const line of cleanError.split('\n')) {
1316
+ if (line.trim()) await log(` ${line.trim()}`);
1317
+ }
1318
+ await log('');
1319
+ await log(' 🔧 How to fix:');
1320
+ await log(' 1. Try creating PR manually:');
1321
+ await log(` cd ${tempDir} && gh pr create --draft`);
1322
+ await log(' 2. Check branch status:');
1323
+ await log(` cd ${tempDir} && git status`);
1324
+ await log(' 3. Verify GitHub authentication:');
1325
+ await log(' gh auth status');
1326
+ await log('');
1327
+ throw new Error('PR creation failed');
1328
+ }
1329
+ }
1330
+ }
1331
+ }
1332
+ } catch (prError) {
1333
+ reportError(prError, {
1334
+ context: 'auto_pr_creation',
1335
+ issueNumber,
1336
+ operation: 'handle_auto_pr'
1337
+ });
1338
+
1339
+ // CRITICAL: PR creation failure should stop the entire process
1340
+ // We cannot continue without a PR when auto-PR creation is enabled
1341
+ await log('');
1342
+ await log(formatAligned('❌', 'FATAL ERROR:', 'PR creation failed'), { level: 'error' });
1343
+ await log('');
1344
+ await log(' 🔍 What this means:');
1345
+ await log(' The solve command cannot continue without a pull request.');
1346
+ await log(' Auto-PR creation is enabled but failed to create the PR.');
1347
+ await log('');
1348
+ await log(' 📦 Error details:');
1349
+ await log(` ${prError.message}`);
1350
+ await log('');
1351
+ await log(' 🔧 How to fix:');
1352
+ await log('');
1353
+ await log(' Option 1: Retry without auto-PR creation');
1354
+ await log(` ./solve.mjs "${issueUrl}" --no-auto-pull-request-creation`);
1355
+ await log(' (Claude will create the PR during the session)');
1356
+ await log('');
1357
+ await log(' Option 2: Create PR manually first');
1358
+ await log(` cd ${tempDir}`);
1359
+ await log(` gh pr create --draft --title "Fix issue #${issueNumber}" --body "Fixes #${issueNumber}"`);
1360
+ await log(` Then use: ./solve.mjs "${issueUrl}" --continue`);
1361
+ await log('');
1362
+ await log(' Option 3: Debug the issue');
1363
+ await log(` cd ${tempDir}`);
1364
+ await log(' git status');
1365
+ await log(' git log --oneline -5');
1366
+ await log(' gh pr create --draft # Try manually to see detailed error');
1367
+ await log('');
1368
+
1369
+ // Re-throw the error to stop execution
1370
+ throw new Error(`PR creation failed: ${prError.message}`);
1371
+ }
1372
+
1373
+ return { prUrl, prNumber: localPrNumber, claudeCommitHash };
1374
+ }