@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,961 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Repository management module for solve command
4
+ // Extracted from solve.mjs to keep files under 1500 lines
5
+
6
+ // Use use-m to dynamically import modules for cross-runtime compatibility
7
+ // Check if use is already defined globally (when imported from solve.mjs)
8
+ // If not, fetch it (when running standalone)
9
+ if (typeof globalThis.use === 'undefined') {
10
+ globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
11
+ }
12
+ const use = globalThis.use;
13
+
14
+ // Use command-stream for consistent $ behavior across runtimes
15
+ const { $ } = await use('command-stream');
16
+
17
+ const os = (await use('os')).default;
18
+ const path = (await use('path')).default;
19
+ const fs = (await use('fs')).promises;
20
+
21
+ // Import shared library functions
22
+ const lib = await import('./lib.mjs');
23
+ // Import Sentry integration
24
+ const sentryLib = await import('./sentry.lib.mjs');
25
+ const { reportError } = sentryLib;
26
+
27
+ const {
28
+ log,
29
+ formatAligned
30
+ } = lib;
31
+
32
+ // Import exit handler
33
+ import { safeExit } from './exit-handler.lib.mjs';
34
+
35
+ // Import GitHub utilities for permission checks
36
+ const githubLib = await import('./github.lib.mjs');
37
+ const { checkRepositoryWritePermission } = githubLib;
38
+
39
+ // Get the root repository of any repository
40
+ // Returns the source (root) repository if the repo is a fork, otherwise returns the repo itself
41
+ export const getRootRepository = async (owner, repo) => {
42
+ try {
43
+ const result = await $`gh api repos/${owner}/${repo} --jq '{fork: .fork, source: .source.full_name}'`;
44
+
45
+ if (result.code !== 0) {
46
+ return null;
47
+ }
48
+
49
+ const repoInfo = JSON.parse(result.stdout.toString().trim());
50
+
51
+ if (repoInfo.fork && repoInfo.source) {
52
+ return repoInfo.source;
53
+ } else {
54
+ return `${owner}/${repo}`;
55
+ }
56
+ } catch (error) {
57
+ reportError(error, {
58
+ context: 'get_root_repository',
59
+ owner,
60
+ repo,
61
+ operation: 'determine_fork_root'
62
+ });
63
+ return null;
64
+ }
65
+ };
66
+
67
+ // Check if current user has a fork of the given root repository
68
+ export const checkExistingForkOfRoot = async (rootRepo) => {
69
+ try {
70
+ const userResult = await $`gh api user --jq .login`;
71
+ if (userResult.code !== 0) {
72
+ return null;
73
+ }
74
+ const currentUser = userResult.stdout.toString().trim();
75
+
76
+ const forksResult = await $`gh api repos/${rootRepo}/forks --paginate --jq '.[] | select(.owner.login == "${currentUser}") | .full_name'`;
77
+
78
+ if (forksResult.code !== 0) {
79
+ return null;
80
+ }
81
+
82
+ const forks = forksResult.stdout.toString().trim().split('\n').filter(f => f);
83
+
84
+ if (forks.length > 0) {
85
+ return forks[0];
86
+ } else {
87
+ return null;
88
+ }
89
+ } catch (error) {
90
+ reportError(error, {
91
+ context: 'check_existing_fork_of_root',
92
+ rootRepo,
93
+ operation: 'search_user_forks'
94
+ });
95
+ return null;
96
+ }
97
+ };
98
+
99
+ // Create or find temporary directory for cloning the repository
100
+ export const setupTempDirectory = async (argv) => {
101
+ let tempDir;
102
+ let isResuming = argv.resume;
103
+
104
+ if (isResuming) {
105
+ // When resuming, try to find existing directory or create a new one
106
+ const scriptDir = path.dirname(process.argv[1]);
107
+ const sessionLogPattern = path.join(scriptDir, `${argv.resume}.log`);
108
+
109
+ try {
110
+ // Check if session log exists to verify session is valid
111
+ await fs.access(sessionLogPattern);
112
+ await log(`🔄 Resuming session ${argv.resume} (session log found)`);
113
+
114
+ // For resumed sessions, create new temp directory since old one may be cleaned up
115
+ tempDir = path.join(os.tmpdir(), `gh-issue-solver-resume-${argv.resume}-${Date.now()}`);
116
+ await fs.mkdir(tempDir, { recursive: true });
117
+ await log(`Creating new temporary directory for resumed session: ${tempDir}`);
118
+ } catch (err) {
119
+ reportError(err, {
120
+ context: 'resume_session_lookup',
121
+ sessionId: argv.resume,
122
+ operation: 'find_session_log'
123
+ });
124
+ await log(`Warning: Session log for ${argv.resume} not found, but continuing with resume attempt`);
125
+ tempDir = path.join(os.tmpdir(), `gh-issue-solver-resume-${argv.resume}-${Date.now()}`);
126
+ await fs.mkdir(tempDir, { recursive: true });
127
+ await log(`Creating temporary directory for resumed session: ${tempDir}`);
128
+ }
129
+ } else {
130
+ tempDir = path.join(os.tmpdir(), `gh-issue-solver-${Date.now()}`);
131
+ await fs.mkdir(tempDir, { recursive: true });
132
+ await log(`\nCreating temporary directory: ${tempDir}`);
133
+ }
134
+
135
+ return { tempDir, isResuming };
136
+ };
137
+
138
+ // Try to initialize an empty repository by creating a simple README.md
139
+ // This makes the repository forkable
140
+ const tryInitializeEmptyRepository = async (owner, repo) => {
141
+ try {
142
+ await log(`${formatAligned('🔧', 'Auto-fix:', 'Attempting to initialize empty repository...')}`);
143
+
144
+ // Check write access before attempting to create files
145
+ await log(`${formatAligned('', '', 'Checking repository write access...')}`);
146
+ const hasWriteAccess = await checkRepositoryWritePermission(owner, repo, { useFork: false });
147
+
148
+ if (!hasWriteAccess) {
149
+ await log(`${formatAligned('❌', 'No access:', 'You do not have write access to this repository')}`);
150
+ await log(`${formatAligned('', '', 'Cannot initialize empty repository without write access')}`);
151
+ return false;
152
+ }
153
+
154
+ await log(`${formatAligned('', '', 'Creating a simple README.md to make repository forkable')}`);
155
+
156
+ // Create simple README content with just the repository name
157
+ const readmeContent = `# ${repo}\n`;
158
+ const base64Content = Buffer.from(readmeContent).toString('base64');
159
+
160
+ // Try to create README.md using GitHub API
161
+ const createResult = await $`gh api repos/${owner}/${repo}/contents/README.md --method PUT --silent \
162
+ --field message="Initialize repository with README" \
163
+ --field content="${base64Content}" 2>&1`;
164
+
165
+ if (createResult.code === 0) {
166
+ await log(`${formatAligned('✅', 'Success:', 'README.md created successfully')}`);
167
+ await log(`${formatAligned('', '', 'Repository is now forkable, retrying fork creation...')}`);
168
+ return true;
169
+ } else {
170
+ const errorOutput = createResult.stdout.toString() + createResult.stderr.toString();
171
+ // Check if it's a permission error
172
+ if (errorOutput.includes('403') || errorOutput.includes('Forbidden') ||
173
+ errorOutput.includes('not have permission') || errorOutput.includes('Resource not accessible')) {
174
+ await log(`${formatAligned('❌', 'No access:', 'You do not have write access to this repository')}`);
175
+ return false;
176
+ } else {
177
+ await log(`${formatAligned('❌', 'Failed:', 'Could not create README.md')}`);
178
+ await log(` Error: ${errorOutput.split('\n')[0]}`);
179
+ return false;
180
+ }
181
+ }
182
+ } catch (error) {
183
+ reportError(error, {
184
+ context: 'initialize_empty_repository',
185
+ owner,
186
+ repo,
187
+ operation: 'create_readme'
188
+ });
189
+ await log(`${formatAligned('❌', 'Error:', 'Failed to initialize repository')}`);
190
+ return false;
191
+ }
192
+ };
193
+
194
+ // Handle fork creation and repository setup
195
+ export const setupRepository = async (argv, owner, repo, forkOwner = null, issueUrl = null) => {
196
+ let repoToClone = `${owner}/${repo}`;
197
+ let forkedRepo = null;
198
+ let upstreamRemote = null;
199
+
200
+ // Priority 1: Check --fork flag first (user explicitly wants to use their own fork)
201
+ // This takes precedence over forkOwner to avoid trying to access someone else's fork
202
+ if (argv.fork) {
203
+ await log(`\n${formatAligned('🍴', 'Fork mode:', 'ENABLED')}`);
204
+ await log(`${formatAligned('', 'Checking fork status...', '')}\n`);
205
+
206
+ // Get current user
207
+ const userResult = await $`gh api user --jq .login`;
208
+ if (userResult.code !== 0) {
209
+ await log(`${formatAligned('❌', 'Error:', 'Failed to get current user')}`);
210
+ await safeExit(1, 'Repository setup failed');
211
+ }
212
+ const currentUser = userResult.stdout.toString().trim();
213
+
214
+ // Check for fork conflicts (Issue #344)
215
+ // Detect if we're trying to fork a repository that shares the same root
216
+ // as an existing fork we already have
217
+ await log(`${formatAligned('🔍', 'Detecting fork conflicts...', '')}`);
218
+ const rootRepo = await getRootRepository(owner, repo);
219
+
220
+ if (rootRepo) {
221
+ const existingFork = await checkExistingForkOfRoot(rootRepo);
222
+
223
+ if (existingFork) {
224
+ const existingForkOwner = existingFork.split('/')[0];
225
+
226
+ if (existingForkOwner === currentUser) {
227
+ const targetRepo = `${owner}/${repo}`;
228
+ const targetIsRoot = (targetRepo === rootRepo);
229
+
230
+ if (!targetIsRoot) {
231
+ await log('');
232
+ await log(`${formatAligned('❌', 'FORK CONFLICT DETECTED', '')}`, { level: 'error' });
233
+ await log('');
234
+ await log(' 🔍 What happened:');
235
+ await log(` You are trying to fork ${targetRepo}`);
236
+ await log(` But you already have a fork of ${rootRepo}: ${existingFork}`);
237
+ await log(' GitHub doesn\'t allow multiple forks of the same root repository');
238
+ await log('');
239
+ await log(' 📦 Root repository analysis:');
240
+ await log(` • Target repository: ${targetRepo}`);
241
+ await log(` • Root repository: ${rootRepo}`);
242
+ await log(` • Your existing fork: ${existingFork}`);
243
+ await log('');
244
+ await log(' ⚠️ Why this is a problem:');
245
+ await log(' GitHub treats forks hierarchically. When you fork a repository,');
246
+ await log(' GitHub tracks the original source repository. If you try to fork');
247
+ await log(' a different fork of the same source, GitHub will silently use your');
248
+ await log(' existing fork instead, causing PRs to be created in the wrong place.');
249
+ await log('');
250
+ await log(' 💡 How to fix:');
251
+ await log(` 1. Delete your existing fork: gh repo delete ${existingFork}`);
252
+ await log(` 2. Then run this command again to fork ${targetRepo}`);
253
+ await log('');
254
+ await log(' ℹ️ Alternative:');
255
+ await log(` If you want to work on ${targetRepo}, you can work directly`);
256
+ await log(' on that repository without forking (if you have write access).');
257
+ await log('');
258
+ await safeExit(1, 'Repository setup failed due to fork conflict');
259
+ }
260
+ }
261
+ }
262
+
263
+ await log(`${formatAligned('✅', 'No fork conflict:', 'Safe to proceed')}`);
264
+ } else {
265
+ await log(`${formatAligned('⚠️', 'Warning:', 'Could not determine root repository')}`);
266
+ }
267
+
268
+ // Check if fork already exists
269
+ // GitHub may create forks with different names to avoid conflicts
270
+ // Try standard name first: currentUser/repo
271
+ // If --prefix-fork-name-with-owner-name is enabled, prefer owner-repo format
272
+ let existingForkName = null;
273
+ const standardForkName = `${currentUser}/${repo}`;
274
+ const prefixedForkName = `${currentUser}/${owner}-${repo}`;
275
+
276
+ // Determine expected fork name based on --prefix-fork-name-with-owner-name option
277
+ const expectedForkName = argv.prefixForkNameWithOwnerName ? prefixedForkName : standardForkName;
278
+ const alternateForkName = argv.prefixForkNameWithOwnerName ? standardForkName : prefixedForkName;
279
+
280
+ let forkCheckResult = await $`gh repo view ${expectedForkName} --json name 2>/dev/null`;
281
+ if (forkCheckResult.code === 0) {
282
+ existingForkName = expectedForkName;
283
+ } else if (!argv.prefixForkNameWithOwnerName) {
284
+ // Only check alternate name if NOT using --prefix-fork-name-with-owner-name
285
+ // When the option is enabled, we ONLY want to use/create the prefixed fork
286
+ // This prevents falling back to an existing standard fork which would cause
287
+ // Compare API 404 errors since branches are in different fork repositories
288
+ forkCheckResult = await $`gh repo view ${alternateForkName} --json name 2>/dev/null`;
289
+ if (forkCheckResult.code === 0) {
290
+ existingForkName = alternateForkName;
291
+ }
292
+ } else {
293
+ // Check if alternate (standard) fork exists when prefix option is enabled
294
+ // If it does, warn user since we won't be using it
295
+ const standardForkCheck = await $`gh repo view ${alternateForkName} --json name 2>/dev/null`;
296
+ if (standardForkCheck.code === 0) {
297
+ await log(`${formatAligned('ℹ️', 'Note:', `Standard fork ${alternateForkName} exists but won't be used`)}`);
298
+ await log(` Creating prefixed fork ${expectedForkName} instead (--prefix-fork-name-with-owner-name enabled)`);
299
+ }
300
+ }
301
+
302
+ if (existingForkName) {
303
+ // Fork exists
304
+ await log(`${formatAligned('✅', 'Fork exists:', existingForkName)}`);
305
+ repoToClone = existingForkName;
306
+ forkedRepo = existingForkName;
307
+ upstreamRemote = `${owner}/${repo}`;
308
+ } else {
309
+ // Need to create fork with retry logic for concurrent scenarios
310
+ await log(`${formatAligned('🔄', 'Creating fork...', '')}`);
311
+
312
+ const maxForkRetries = 5;
313
+ const baseDelay = 2000; // Start with 2 seconds
314
+ let forkCreated = false;
315
+ let forkExists = false;
316
+
317
+ // Determine the expected fork name based on --prefix-fork-name-with-owner-name option
318
+ const defaultForkName = argv.prefixForkNameWithOwnerName ? `${owner}-${repo}` : repo;
319
+ let actualForkName = `${currentUser}/${defaultForkName}`;
320
+
321
+ for (let attempt = 1; attempt <= maxForkRetries; attempt++) {
322
+ // Try to create fork with optional custom name
323
+ let forkResult;
324
+ if (argv.prefixForkNameWithOwnerName) {
325
+ // Use --fork-name flag to create fork with owner prefix
326
+ forkResult = await $`gh repo fork ${owner}/${repo} --fork-name ${owner}-${repo} --clone=false 2>&1`;
327
+ } else {
328
+ // Standard fork creation (no custom name)
329
+ forkResult = await $`gh repo fork ${owner}/${repo} --clone=false 2>&1`;
330
+ }
331
+
332
+ // Always capture output to parse actual fork name
333
+ const forkOutput = (forkResult.stderr ? forkResult.stderr.toString() : '') +
334
+ (forkResult.stdout ? forkResult.stdout.toString() : '');
335
+
336
+ // Parse actual fork name from output (e.g., "konard/netkeep80-jsonRVM already exists")
337
+ // GitHub may create forks with modified names to avoid conflicts
338
+ // Use regex that won't match domain names like "github.com/user" -> "com/user"
339
+ const forkNameMatch = forkOutput.match(/(?:github\.com\/|^|\s)([a-zA-Z0-9_-]+\/[a-zA-Z0-9_-]+)/);
340
+ if (forkNameMatch) {
341
+ actualForkName = forkNameMatch[1];
342
+ }
343
+
344
+ if (forkResult.code === 0) {
345
+ // Fork successfully created or already exists
346
+ if (forkOutput.includes('already exists')) {
347
+ await log(`${formatAligned('ℹ️', 'Fork exists:', actualForkName)}`);
348
+ forkExists = true;
349
+ } else {
350
+ await log(`${formatAligned('✅', 'Fork created:', actualForkName)}`);
351
+ forkCreated = true;
352
+ forkExists = true;
353
+ }
354
+ break;
355
+ } else {
356
+ // Fork creation failed - check if it's because fork already exists
357
+ if (forkOutput.includes('already exists') ||
358
+ forkOutput.includes('Name already exists') ||
359
+ forkOutput.includes('fork of') ||
360
+ forkOutput.includes('HTTP 422')) {
361
+ // Fork already exists (likely created by another concurrent worker)
362
+ await log(`${formatAligned('ℹ️', 'Fork exists:', actualForkName)}`);
363
+ forkExists = true;
364
+ break;
365
+ }
366
+
367
+ // Check if it's an empty repository (HTTP 403) - try to auto-fix
368
+ if (forkOutput.includes('HTTP 403') &&
369
+ (forkOutput.includes('Empty repositories cannot be forked') ||
370
+ forkOutput.includes('contains no Git content'))) {
371
+ // Empty repository detected - try to initialize it
372
+ await log('');
373
+ await log(`${formatAligned('⚠️', 'EMPTY REPOSITORY', 'detected')}`, { level: 'warn' });
374
+ await log(`${formatAligned('', '', `Repository ${owner}/${repo} contains no content`)}`);
375
+ await log('');
376
+
377
+ // Try to initialize the repository by creating a README.md
378
+ const initialized = await tryInitializeEmptyRepository(owner, repo);
379
+
380
+ if (initialized) {
381
+ // Success! Repository is now initialized, retry fork creation
382
+ await log('');
383
+ await log(`${formatAligned('🔄', 'Retrying:', 'Fork creation after repository initialization...')}`);
384
+ // Wait a moment for GitHub to process the new file
385
+ await new Promise(resolve => setTimeout(resolve, 2000));
386
+ // Continue to next iteration (retry fork creation)
387
+ continue;
388
+ } else {
389
+ // Failed to initialize - provide helpful suggestions
390
+ await log('');
391
+ await log(`${formatAligned('❌', 'Cannot proceed:', 'Unable to initialize empty repository')}`, { level: 'error' });
392
+ await log('');
393
+ await log(' 🔍 What happened:');
394
+ await log(` The repository ${owner}/${repo} is empty and cannot be forked.`);
395
+ await log(' GitHub doesn\'t allow forking repositories with no content.');
396
+ await log(' Auto-fix failed: You need write access to initialize the repository.');
397
+ await log('');
398
+ await log(' 💡 How to fix:');
399
+ await log(' Option 1: Ask repository owner to add initial content');
400
+ await log(' Even a simple README.md file would make the repository forkable');
401
+ await log('');
402
+ await log(' Option 2: Work directly on the original repository (if you get write access)');
403
+ await log(` Run: solve ${issueUrl || '<issue-url>'} --no-fork`);
404
+ await log('');
405
+
406
+ // Try to create a comment on the issue asking the maintainer to initialize the repository
407
+ if (issueUrl) {
408
+ try {
409
+ // Extract issue number from URL (e.g., https://github.com/owner/repo/issues/123)
410
+ const issueMatch = issueUrl.match(/\/issues\/(\d+)/);
411
+ if (issueMatch) {
412
+ const issueNumber = issueMatch[1];
413
+ await log(`${formatAligned('💬', 'Creating comment:', 'Requesting maintainer to initialize repository...')}`);
414
+
415
+ const commentBody = `## ⚠️ Repository Initialization Required
416
+
417
+ Hello! I attempted to work on this issue, but encountered a problem:
418
+
419
+ **Issue**: The repository is empty and cannot be forked.
420
+ **Reason**: GitHub doesn't allow forking repositories with no content.
421
+
422
+ ### 🔧 How to resolve:
423
+
424
+ **Option 1: Grant write access for me to initialize the repository**
425
+ You could grant write access to allow me to initialize the repository directly.
426
+
427
+ **Option 2: Initialize the repository yourself**
428
+ Please add initial content to the repository. Even a simple README.md (even if it is empty or contains just the title) file would make it possible to fork and work on this issue.
429
+
430
+ Once the repository contains at least one commit with any file, I'll be able to fork it and proceed with solving this issue.
431
+
432
+ Thank you!`;
433
+
434
+ const commentResult = await $`gh issue comment ${issueNumber} --repo ${owner}/${repo} --body ${commentBody}`;
435
+ if (commentResult.code === 0) {
436
+ await log(`${formatAligned('✅', 'Comment created:', `Posted to issue #${issueNumber}`)}`);
437
+ } else {
438
+ await log(`${formatAligned('⚠️', 'Note:', 'Could not post comment to issue (this is not critical)')}`);
439
+ }
440
+ }
441
+ } catch {
442
+ // Silently ignore comment creation errors - not critical to the process
443
+ await log(`${formatAligned('⚠️', 'Note:', 'Could not post comment to issue (this is not critical)')}`);
444
+ }
445
+ }
446
+
447
+ await safeExit(1, 'Repository setup failed - empty repository');
448
+ }
449
+ }
450
+
451
+ // Check if fork was created by another worker even if error message doesn't explicitly say so
452
+ await log(`${formatAligned('🔍', 'Checking:', 'If fork exists after failed creation attempt...')}`);
453
+ const checkResult = await $`gh repo view ${actualForkName} --json name 2>/dev/null`;
454
+
455
+ if (checkResult.code === 0) {
456
+ // Fork exists now (created by another worker during our attempt)
457
+ await log(`${formatAligned('✅', 'Fork found:', 'Created by another concurrent worker')}`);
458
+ forkExists = true;
459
+ break;
460
+ }
461
+
462
+ // Fork still doesn't exist and creation failed
463
+ if (attempt < maxForkRetries) {
464
+ const delay = baseDelay * Math.pow(2, attempt - 1); // Exponential backoff
465
+ await log(`${formatAligned('⏳', 'Retry:', `Attempt ${attempt}/${maxForkRetries} failed, waiting ${delay/1000}s before retry...`)}`);
466
+ await log(` Error: ${forkOutput.split('\n')[0]}`); // Show first line of error
467
+ await new Promise(resolve => setTimeout(resolve, delay));
468
+ } else {
469
+ // All retries exhausted
470
+ await log(`${formatAligned('❌', 'Error:', 'Failed to create fork after all retries')}`);
471
+ await log(forkOutput);
472
+ await safeExit(1, 'Repository setup failed');
473
+ }
474
+ }
475
+ }
476
+
477
+ // If fork exists (either created or already existed), verify it's accessible
478
+ if (forkExists) {
479
+ await log(`${formatAligned('🔍', 'Verifying fork:', 'Checking accessibility...')}`);
480
+
481
+ // Verify fork with retries (GitHub may need time to propagate)
482
+ const maxVerifyRetries = 5;
483
+ let forkVerified = false;
484
+
485
+ for (let attempt = 1; attempt <= maxVerifyRetries; attempt++) {
486
+ const delay = baseDelay * Math.pow(2, attempt - 1);
487
+ if (attempt > 1) {
488
+ await log(`${formatAligned('⏳', 'Verifying fork:', `Attempt ${attempt}/${maxVerifyRetries} (waiting ${delay/1000}s)...`)}`);
489
+ await new Promise(resolve => setTimeout(resolve, delay));
490
+ }
491
+
492
+ const verifyResult = await $`gh repo view ${actualForkName} --json name 2>/dev/null`;
493
+ if (verifyResult.code === 0) {
494
+ forkVerified = true;
495
+ await log(`${formatAligned('✅', 'Fork verified:', `${actualForkName} is accessible`)}`);
496
+ break;
497
+ }
498
+ }
499
+
500
+ if (!forkVerified) {
501
+ await log(`${formatAligned('❌', 'Error:', 'Fork exists but not accessible after multiple retries')}`);
502
+ await log(`${formatAligned('', 'Suggestion:', 'GitHub may be experiencing delays - try running the command again in a few minutes')}`);
503
+ await safeExit(1, 'Repository setup failed');
504
+ }
505
+
506
+ // Wait a moment for fork to be fully ready
507
+ if (forkCreated) {
508
+ await log(`${formatAligned('⏳', 'Waiting:', 'For fork to be fully ready...')}`);
509
+ await new Promise(resolve => setTimeout(resolve, 3000));
510
+ }
511
+ }
512
+
513
+ repoToClone = actualForkName;
514
+ forkedRepo = actualForkName;
515
+ upstreamRemote = `${owner}/${repo}`;
516
+ }
517
+ } else if (forkOwner) {
518
+ // Priority 2: If forkOwner is provided (from auto-continue/PR mode) and --fork was not used,
519
+ // try to use that fork directly (only works if it's accessible)
520
+ await log(`\n${formatAligned('🍴', 'Fork mode:', 'DETECTED from PR')}`);
521
+ await log(`${formatAligned('', 'Fork owner:', forkOwner)}`);
522
+
523
+ // Determine fork name - try prefixed name first if option is enabled, otherwise try standard name
524
+ const standardForkName = `${forkOwner}/${repo}`;
525
+ const prefixedForkName = `${forkOwner}/${owner}-${repo}`;
526
+ const expectedForkName = argv.prefixForkNameWithOwnerName ? prefixedForkName : standardForkName;
527
+ const alternateForkName = argv.prefixForkNameWithOwnerName ? standardForkName : prefixedForkName;
528
+
529
+ await log(`${formatAligned('✅', 'Using fork:', expectedForkName)}\n`);
530
+
531
+ // Verify the fork exists and is accessible - try expected name first, then alternate
532
+ await log(`${formatAligned('🔍', 'Verifying fork:', 'Checking accessibility...')}`);
533
+ let forkCheckResult = await $`gh repo view ${expectedForkName} --json name 2>/dev/null`;
534
+ let actualForkName = expectedForkName;
535
+
536
+ if (forkCheckResult.code !== 0 && !argv.prefixForkNameWithOwnerName) {
537
+ // Only try alternate name if NOT using --prefix-fork-name-with-owner-name
538
+ // When the option is enabled, we should only use the prefixed fork name
539
+ forkCheckResult = await $`gh repo view ${alternateForkName} --json name 2>/dev/null`;
540
+ if (forkCheckResult.code === 0) {
541
+ actualForkName = alternateForkName;
542
+ }
543
+ }
544
+
545
+ if (forkCheckResult.code === 0) {
546
+ await log(`${formatAligned('✅', 'Fork verified:', `${actualForkName} is accessible`)}`);
547
+ repoToClone = actualForkName;
548
+ forkedRepo = actualForkName;
549
+ upstreamRemote = `${owner}/${repo}`;
550
+ } else {
551
+ await log(`${formatAligned('❌', 'Error:', 'Fork not accessible')}`);
552
+ await log(`${formatAligned('', 'Fork:', expectedForkName)}`);
553
+ await log(`${formatAligned('', 'Suggestion:', 'The PR may be from a fork you no longer have access to')}`);
554
+ await log(`${formatAligned('', 'Hint:', 'Try running with --fork flag to use your own fork instead')}`);
555
+ await safeExit(1, 'Repository setup failed');
556
+ }
557
+ }
558
+
559
+ return { repoToClone, forkedRepo, upstreamRemote, prForkOwner: forkOwner };
560
+ };
561
+
562
+ // Clone repository and set up remotes
563
+ export const cloneRepository = async (repoToClone, tempDir, argv, owner, repo) => {
564
+ // Clone the repository (or fork) using gh tool with authentication
565
+ await log(`\n${formatAligned('📥', 'Cloning repository:', repoToClone)}`);
566
+
567
+ // Use 2>&1 to capture all output and filter "Cloning into" message
568
+ const cloneResult = await $`gh repo clone ${repoToClone} ${tempDir} 2>&1`;
569
+
570
+ // Verify clone was successful
571
+ if (cloneResult.code !== 0) {
572
+ const errorOutput = (cloneResult.stderr || cloneResult.stdout || 'Unknown error').toString().trim();
573
+ await log('');
574
+ await log(`${formatAligned('❌', 'CLONE FAILED', '')}`, { level: 'error' });
575
+ await log('');
576
+ await log(' 🔍 What happened:');
577
+ await log(` Failed to clone repository ${repoToClone}`);
578
+ await log('');
579
+ await log(' 📦 Error details:');
580
+ for (const line of errorOutput.split('\n')) {
581
+ if (line.trim()) await log(` ${line}`);
582
+ }
583
+ await log('');
584
+ await log(' 💡 Common causes:');
585
+ await log(' • Repository doesn\'t exist or is private');
586
+ await log(' • No GitHub authentication');
587
+ await log(' • Network connectivity issues');
588
+ if (argv.fork) {
589
+ await log(' • Fork not ready yet (try again in a moment)');
590
+ }
591
+ await log('');
592
+ await log(' 🔧 How to fix:');
593
+ await log(' 1. Check authentication: gh auth status');
594
+ await log(' 2. Login if needed: gh auth login');
595
+ await log(` 3. Verify access: gh repo view ${owner}/${repo}`);
596
+ if (argv.fork) {
597
+ await log(` 4. Check fork: gh repo view ${repoToClone}`);
598
+ }
599
+ await log('');
600
+ await safeExit(1, 'Repository setup failed');
601
+ }
602
+
603
+ await log(`${formatAligned('✅', 'Cloned to:', tempDir)}`);
604
+
605
+ // Verify and fix remote configuration
606
+ const remoteCheckResult = await $({ cwd: tempDir })`git remote -v 2>&1`;
607
+ if (!remoteCheckResult.stdout || !remoteCheckResult.stdout.toString().includes('origin')) {
608
+ await log(' Setting up git remote...', { verbose: true });
609
+ // Add origin remote manually
610
+ await $({ cwd: tempDir })`git remote add origin https://github.com/${repoToClone}.git 2>&1`;
611
+ }
612
+ };
613
+
614
+ // Set up upstream remote and sync fork
615
+ export const setupUpstreamAndSync = async (tempDir, forkedRepo, upstreamRemote, owner, repo, argv) => {
616
+ if (!forkedRepo || !upstreamRemote) return;
617
+
618
+ await log(`${formatAligned('🔗', 'Setting upstream:', upstreamRemote)}`);
619
+
620
+ // Check if upstream remote already exists
621
+ const checkUpstreamResult = await $({ cwd: tempDir })`git remote get-url upstream 2>/dev/null`;
622
+ let upstreamExists = checkUpstreamResult.code === 0;
623
+
624
+ if (upstreamExists) {
625
+ await log(`${formatAligned('ℹ️', 'Upstream exists:', 'Using existing upstream remote')}`);
626
+ } else {
627
+ // Add upstream remote since it doesn't exist
628
+ const upstreamResult = await $({ cwd: tempDir })`git remote add upstream https://github.com/${upstreamRemote}.git`;
629
+
630
+ if (upstreamResult.code === 0) {
631
+ await log(`${formatAligned('✅', 'Upstream set:', upstreamRemote)}`);
632
+ upstreamExists = true;
633
+ } else {
634
+ await log(`${formatAligned('⚠️', 'Warning:', 'Failed to add upstream remote')}`);
635
+ if (upstreamResult.stderr) {
636
+ await log(`${formatAligned('', 'Error details:', upstreamResult.stderr.toString().trim())}`);
637
+ }
638
+ }
639
+ }
640
+
641
+ // Proceed with fork sync if upstream remote is available
642
+ if (upstreamExists) {
643
+ // Fetch upstream
644
+ await log(`${formatAligned('🔄', 'Fetching upstream...', '')}`);
645
+ const fetchResult = await $({ cwd: tempDir })`git fetch upstream`;
646
+ if (fetchResult.code === 0) {
647
+ await log(`${formatAligned('✅', 'Upstream fetched:', 'Successfully')}`);
648
+
649
+ // Sync the default branch with upstream to avoid merge conflicts
650
+ await log(`${formatAligned('🔄', 'Syncing default branch...', '')}`);
651
+
652
+ // Get current branch so we can return to it after sync
653
+ const currentBranchResult = await $({ cwd: tempDir })`git branch --show-current`;
654
+ if (currentBranchResult.code === 0) {
655
+ const currentBranch = currentBranchResult.stdout.toString().trim();
656
+
657
+ // Get the default branch name from the original repository using GitHub API
658
+ const repoInfoResult = await $`gh api repos/${owner}/${repo} --jq .default_branch`;
659
+ if (repoInfoResult.code === 0) {
660
+ const upstreamDefaultBranch = repoInfoResult.stdout.toString().trim();
661
+ await log(`${formatAligned('ℹ️', 'Default branch:', upstreamDefaultBranch)}`);
662
+
663
+ // Always sync the default branch, regardless of current branch
664
+ // This ensures fork is up-to-date even if we're working on a different branch
665
+
666
+ // Step 1: Switch to default branch if not already on it
667
+ let syncSuccessful = true;
668
+ if (currentBranch !== upstreamDefaultBranch) {
669
+ await log(`${formatAligned('🔄', 'Switching to:', `${upstreamDefaultBranch} branch`)}`);
670
+ const checkoutResult = await $({ cwd: tempDir })`git checkout ${upstreamDefaultBranch}`;
671
+ if (checkoutResult.code !== 0) {
672
+ await log(`${formatAligned('⚠️', 'Warning:', `Failed to checkout ${upstreamDefaultBranch}`)}`);
673
+ syncSuccessful = false; // Cannot proceed with sync
674
+ }
675
+ }
676
+
677
+ // Step 2: Sync default branch with upstream (only if checkout was successful)
678
+ if (syncSuccessful) {
679
+ const syncResult = await $({ cwd: tempDir })`git reset --hard upstream/${upstreamDefaultBranch}`;
680
+ if (syncResult.code === 0) {
681
+ await log(`${formatAligned('✅', 'Default branch synced:', `with upstream/${upstreamDefaultBranch}`)}`);
682
+
683
+ // Step 3: Push the updated default branch to fork to keep it in sync
684
+ await log(`${formatAligned('🔄', 'Pushing to fork:', `${upstreamDefaultBranch} branch`)}`);
685
+ const pushResult = await $({ cwd: tempDir })`git push origin ${upstreamDefaultBranch}`;
686
+ if (pushResult.code === 0) {
687
+ await log(`${formatAligned('✅', 'Fork updated:', 'Default branch pushed to fork')}`);
688
+ } else {
689
+ // Check if it's a non-fast-forward error (fork has diverged from upstream)
690
+ const errorMsg = pushResult.stderr ? pushResult.stderr.toString().trim() : '';
691
+ const isNonFastForward = errorMsg.includes('non-fast-forward') ||
692
+ errorMsg.includes('rejected') ||
693
+ errorMsg.includes('tip of your current branch is behind');
694
+
695
+ if (isNonFastForward) {
696
+ // Fork has diverged from upstream
697
+ await log('');
698
+ await log(`${formatAligned('⚠️', 'FORK DIVERGENCE DETECTED', '')}`, { level: 'warn' });
699
+ await log('');
700
+ await log(' 🔍 What happened:');
701
+ await log(` Your fork's ${upstreamDefaultBranch} branch has different commits than upstream`);
702
+ await log(' This typically occurs when upstream had a force push (e.g., git reset --hard)');
703
+ await log('');
704
+ await log(' 📦 Current state:');
705
+ await log(` • Fork: ${forkedRepo}`);
706
+ await log(` • Upstream: ${owner}/${repo}`);
707
+ await log(` • Branch: ${upstreamDefaultBranch}`);
708
+ await log('');
709
+
710
+ // Check if user has enabled automatic force push
711
+ if (argv.allowForkDivergenceResolutionUsingForcePushWithLease) {
712
+ await log(' 🔄 Auto-resolution ENABLED (--allow-fork-divergence-resolution-using-force-push-with-lease):');
713
+ await log(' Attempting to force-push with --force-with-lease...');
714
+ await log('');
715
+
716
+ // Use --force-with-lease for safer force push
717
+ // This will only force push if the remote hasn't changed since our last fetch
718
+ await log(`${formatAligned('🔄', 'Force pushing:', 'Syncing fork with upstream (--force-with-lease)')}`);
719
+ const forcePushResult = await $({ cwd: tempDir })`git push --force-with-lease origin ${upstreamDefaultBranch}`;
720
+
721
+ if (forcePushResult.code === 0) {
722
+ await log(`${formatAligned('✅', 'Fork synced:', 'Successfully force-pushed to align with upstream')}`);
723
+ await log('');
724
+ } else {
725
+ // Force push also failed - this is a more serious issue
726
+ await log('');
727
+ await log(`${formatAligned('❌', 'FATAL ERROR:', 'Failed to sync fork with upstream')}`, { level: 'error' });
728
+ await log('');
729
+ await log(' 🔍 What happened:');
730
+ await log(` Fork branch ${upstreamDefaultBranch} has diverged from upstream`);
731
+ await log(' Both normal push and force-with-lease push failed');
732
+ await log('');
733
+ await log(' 📦 Error details:');
734
+ const forceErrorMsg = forcePushResult.stderr ? forcePushResult.stderr.toString().trim() : '';
735
+ for (const line of forceErrorMsg.split('\n')) {
736
+ if (line.trim()) await log(` ${line}`);
737
+ }
738
+ await log('');
739
+ await log(' 💡 Possible causes:');
740
+ await log(' • Fork branch is protected (branch protection rules prevent force push)');
741
+ await log(' • Someone else pushed to fork after our fetch');
742
+ await log(' • Insufficient permissions to force push');
743
+ await log('');
744
+ await log(' 🔧 Manual resolution:');
745
+ await log(` 1. Visit your fork: https://github.com/${forkedRepo}`);
746
+ await log(' 2. Check branch protection settings');
747
+ await log(' 3. Manually sync fork with upstream:');
748
+ await log(' git fetch upstream');
749
+ await log(` git reset --hard upstream/${upstreamDefaultBranch}`);
750
+ await log(` git push --force origin ${upstreamDefaultBranch}`);
751
+ await log('');
752
+ await safeExit(1, 'Repository setup failed - fork sync failed');
753
+ }
754
+ } else {
755
+ // Flag is not enabled - provide guidance
756
+ await log(' ⚠️ RISKS of force-pushing:');
757
+ await log(' • Overwrites fork history - any unique commits in your fork will be LOST');
758
+ await log(' • Other collaborators working on your fork may face conflicts');
759
+ await log(' • Cannot be undone - use with extreme caution');
760
+ await log('');
761
+ await log(' 💡 Your options:');
762
+ await log('');
763
+ await log(' Option 1: Enable automatic force-push (DANGEROUS)');
764
+ await log(' Add --allow-fork-divergence-resolution-using-force-push-with-lease flag to your command');
765
+ await log(' This will automatically sync your fork with upstream using force-with-lease');
766
+ await log('');
767
+ await log(' Option 2: Manually resolve the divergence');
768
+ await log(' 1. Decide if you need any commits unique to your fork');
769
+ await log(' 2. If yes, cherry-pick them after syncing');
770
+ await log(' 3. If no, manually force-push:');
771
+ await log(' git fetch upstream');
772
+ await log(` git reset --hard upstream/${upstreamDefaultBranch}`);
773
+ await log(` git push --force origin ${upstreamDefaultBranch}`);
774
+ await log('');
775
+ await log(' Option 3: Work without syncing fork (NOT RECOMMENDED)');
776
+ await log(' Your fork will remain out-of-sync with upstream');
777
+ await log(' May cause merge conflicts in pull requests');
778
+ await log('');
779
+ await log(' 🔧 To proceed with auto-resolution, restart with:');
780
+ await log(` solve ${argv.url || argv['issue-url'] || argv._[0] || '<issue-url>'} --allow-fork-divergence-resolution-using-force-push-with-lease`);
781
+ await log('');
782
+ await safeExit(1, 'Repository setup halted - fork divergence requires user decision');
783
+ }
784
+ } else {
785
+ // Some other push error (not divergence-related)
786
+ await log(`${formatAligned('❌', 'FATAL ERROR:', 'Failed to push updated default branch to fork')}`);
787
+ await log(`${formatAligned('', 'Push error:', errorMsg)}`);
788
+ await log(`${formatAligned('', 'Reason:', 'Fork must be updated or process must stop')}`);
789
+ await log(`${formatAligned('', 'Solution draft:', 'Fork sync is required for proper workflow')}`);
790
+ await log(`${formatAligned('', 'Next steps:', '1. Check GitHub permissions for the fork')}`);
791
+ await log(`${formatAligned('', '', '2. Ensure fork is not protected')}`);
792
+ await log(`${formatAligned('', '', '3. Try again after resolving fork issues')}`);
793
+ await safeExit(1, 'Repository setup failed');
794
+ }
795
+ }
796
+
797
+ // Step 4: Return to the original branch if it was different
798
+ if (currentBranch !== upstreamDefaultBranch) {
799
+ await log(`${formatAligned('🔄', 'Returning to:', `${currentBranch} branch`)}`);
800
+ const returnResult = await $({ cwd: tempDir })`git checkout ${currentBranch}`;
801
+ if (returnResult.code === 0) {
802
+ await log(`${formatAligned('✅', 'Branch restored:', `Back on ${currentBranch}`)}`);
803
+ } else {
804
+ await log(`${formatAligned('⚠️', 'Warning:', `Failed to return to ${currentBranch}`)}`);
805
+ // This is not fatal, continue with sync on default branch
806
+ }
807
+ }
808
+ } else {
809
+ await log(`${formatAligned('⚠️', 'Warning:', `Failed to sync ${upstreamDefaultBranch} with upstream`)}`);
810
+ if (syncResult.stderr) {
811
+ await log(`${formatAligned('', 'Sync error:', syncResult.stderr.toString().trim())}`);
812
+ }
813
+ }
814
+ }
815
+ } else {
816
+ await log(`${formatAligned('⚠️', 'Warning:', 'Failed to get default branch name')}`);
817
+ }
818
+ } else {
819
+ await log(`${formatAligned('⚠️', 'Warning:', 'Failed to get current branch')}`);
820
+ }
821
+ } else {
822
+ await log(`${formatAligned('⚠️', 'Warning:', 'Failed to fetch upstream')}`);
823
+ if (fetchResult.stderr) {
824
+ await log(`${formatAligned('', 'Fetch error:', fetchResult.stderr.toString().trim())}`);
825
+ }
826
+ }
827
+ }
828
+ };
829
+
830
+ // Set up pr-fork remote for continuing someone else's fork PR with --fork flag
831
+ export const setupPrForkRemote = async (tempDir, argv, prForkOwner, repo, isContinueMode, owner = null) => {
832
+ // Only set up pr-fork remote if:
833
+ // 1. --fork flag is used (user wants to use their own fork)
834
+ // 2. prForkOwner is provided (continuing an existing PR from a fork)
835
+ // 3. In continue mode (auto-continue or continuing existing PR)
836
+ if (!argv.fork || !prForkOwner || !isContinueMode) {
837
+ return null;
838
+ }
839
+
840
+ // Get current user to check if it's someone else's fork
841
+ await log(`\n${formatAligned('🔍', 'Checking PR fork:', 'Determining if branch is in another fork...')}`);
842
+ const userResult = await $`gh api user --jq .login`;
843
+ if (userResult.code !== 0) {
844
+ await log(`${formatAligned('⚠️', 'Warning:', 'Failed to get current user, cannot set up pr-fork remote')}`);
845
+ return null;
846
+ }
847
+
848
+ const currentUser = userResult.stdout.toString().trim();
849
+
850
+ // If PR is from current user's fork, no need for pr-fork remote
851
+ if (prForkOwner === currentUser) {
852
+ await log(`${formatAligned('ℹ️', 'PR fork owner:', 'Same as current user, using origin remote')}`);
853
+ return null;
854
+ }
855
+
856
+ // This is someone else's fork - add it as pr-fork remote
857
+ // Determine the fork repository name (might be prefixed if --prefix-fork-name-with-owner-name was used)
858
+ // Try both standard and prefixed names
859
+ let prForkRepoName = repo;
860
+ if (owner && argv.prefixForkNameWithOwnerName) {
861
+ // When prefix option is enabled, try prefixed name first
862
+ prForkRepoName = `${owner}-${repo}`;
863
+ }
864
+
865
+ await log(`${formatAligned('🔗', 'Setting up pr-fork:', 'Branch exists in another user\'s fork')}`);
866
+ await log(`${formatAligned('', 'PR fork owner:', prForkOwner)}`);
867
+ await log(`${formatAligned('', 'Current user:', currentUser)}`);
868
+ await log(`${formatAligned('', 'Action:', `Adding ${prForkOwner}/${prForkRepoName} as pr-fork remote`)}`);
869
+
870
+ const addRemoteResult = await $({ cwd: tempDir })`git remote add pr-fork https://github.com/${prForkOwner}/${prForkRepoName}.git`;
871
+ if (addRemoteResult.code !== 0) {
872
+ await log(`${formatAligned('❌', 'Error:', 'Failed to add pr-fork remote')}`);
873
+ if (addRemoteResult.stderr) {
874
+ await log(`${formatAligned('', 'Details:', addRemoteResult.stderr.toString().trim())}`);
875
+ }
876
+ await log(`${formatAligned('', 'Suggestion:', 'The PR branch may not be accessible')}`);
877
+ await log(`${formatAligned('', 'Workaround:', 'Remove --fork flag to continue work in the original fork')}`);
878
+ return null;
879
+ }
880
+
881
+ await log(`${formatAligned('✅', 'Remote added:', 'pr-fork')}`);
882
+
883
+ // Fetch from pr-fork to get the branch
884
+ await log(`${formatAligned('📥', 'Fetching branches:', 'From pr-fork remote...')}`);
885
+ const fetchPrForkResult = await $({ cwd: tempDir })`git fetch pr-fork`;
886
+ if (fetchPrForkResult.code !== 0) {
887
+ await log(`${formatAligned('❌', 'Error:', 'Failed to fetch from pr-fork')}`);
888
+ if (fetchPrForkResult.stderr) {
889
+ await log(`${formatAligned('', 'Details:', fetchPrForkResult.stderr.toString().trim())}`);
890
+ }
891
+ await log(`${formatAligned('', 'Suggestion:', 'Check if you have access to the fork')}`);
892
+ return null;
893
+ }
894
+
895
+ await log(`${formatAligned('✅', 'Fetched:', 'pr-fork branches')}`);
896
+ await log(`${formatAligned('ℹ️', 'Next step:', 'Will checkout branch from pr-fork remote')}`);
897
+ return 'pr-fork';
898
+ };
899
+
900
+ // Checkout branch for continue mode (PR branch from remote)
901
+ export const checkoutPrBranch = async (tempDir, branchName, prForkRemote, prForkOwner) => {
902
+ await log(`\n${formatAligned('🔄', 'Checking out PR branch:', branchName)}`);
903
+
904
+ // Determine which remote to use for branch checkout
905
+ const remoteName = prForkRemote || 'origin';
906
+
907
+ // First fetch all branches from remote (if not already fetched from pr-fork)
908
+ if (!prForkRemote) {
909
+ await log(`${formatAligned('📥', 'Fetching branches:', 'From remote...')}`);
910
+ const fetchResult = await $({ cwd: tempDir })`git fetch origin`;
911
+
912
+ if (fetchResult.code !== 0) {
913
+ await log('Warning: Failed to fetch branches from remote', { level: 'warning' });
914
+ }
915
+ } else {
916
+ await log(`${formatAligned('ℹ️', 'Using pr-fork remote:', `Branch exists in ${prForkOwner}'s fork`)}`);
917
+ }
918
+
919
+ // Checkout the PR branch (it might exist locally or remotely)
920
+ const localBranchResult = await $({ cwd: tempDir })`git show-ref --verify --quiet refs/heads/${branchName}`;
921
+
922
+ let checkoutResult;
923
+ if (localBranchResult.code === 0) {
924
+ // Branch exists locally
925
+ checkoutResult = await $({ cwd: tempDir })`git checkout ${branchName}`;
926
+ } else {
927
+ // Branch doesn't exist locally, try to checkout from remote
928
+ checkoutResult = await $({ cwd: tempDir })`git checkout -b ${branchName} ${remoteName}/${branchName}`;
929
+ }
930
+
931
+ return checkoutResult;
932
+ };
933
+
934
+ // Cleanup temporary directory
935
+ export const cleanupTempDirectory = async (tempDir, argv, limitReached) => {
936
+ // Determine if we should skip cleanup
937
+ const shouldKeepDirectory = !argv.autoCleanup || argv.resume || limitReached || (argv.autoContinueOnLimitReset && global.limitResetTime);
938
+
939
+ if (!shouldKeepDirectory) {
940
+ try {
941
+ process.stdout.write('\n🧹 Cleaning up...');
942
+ await fs.rm(tempDir, { recursive: true, force: true });
943
+ await log(' ✅');
944
+ } catch (cleanupError) {
945
+ reportError(cleanupError, {
946
+ context: 'cleanup_temp_directory',
947
+ tempDir,
948
+ operation: 'remove_temp_dir'
949
+ });
950
+ await log(' ⚠️ (failed)');
951
+ }
952
+ } else if (argv.resume) {
953
+ await log(`\n📁 Keeping directory for resumed session: ${tempDir}`);
954
+ } else if (limitReached && argv.autoContinueLimit) {
955
+ await log(`\n📁 Keeping directory for auto-continue: ${tempDir}`);
956
+ } else if (limitReached) {
957
+ await log(`\n📁 Keeping directory for future resume: ${tempDir}`);
958
+ } else if (!argv.autoCleanup) {
959
+ await log(`\n📁 Keeping directory (--no-auto-cleanup): ${tempDir}`);
960
+ }
961
+ };