@link-assistant/hive-mind 2.11.13 → 2.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/package.json +4 -1
- package/src/agent-command.lib.mjs +74 -0
- package/src/agent.lib.mjs +59 -34
- package/src/agentic-cli-updater.lib.mjs +241 -0
- package/src/claude.connection.lib.mjs +209 -0
- package/src/claude.lib.mjs +6 -202
- package/src/codex.lib.mjs +0 -128
- package/src/formal-ai-isolation.lib.mjs +62 -0
- package/src/formal-ai-maintenance.lib.mjs +106 -0
- package/src/formal-ai-model.lib.mjs +25 -0
- package/src/formal-ai-runtime.lib.mjs +10 -0
- package/src/formal-ai-sidecar.lib.mjs +565 -0
- package/src/formal-ai-updater.lib.mjs +294 -0
- package/src/formal-ai-version.lib.mjs +100 -0
- package/src/formal-ai.lib.mjs +11 -16
- package/src/github-rate-limit.lib.mjs +3 -0
- package/src/github-url-parser.lib.mjs +255 -0
- package/src/github.lib.mjs +22 -343
- package/src/hive.mjs +0 -152
- package/src/interactive-mode.lib.mjs +0 -43
- package/src/isolation-runner.lib.mjs +44 -173
- package/src/limits.lib.mjs +0 -89
- package/src/model-args.lib.mjs +32 -0
- package/src/models/index.mjs +5 -19
- package/src/session-monitor.lib.mjs +14 -172
- package/src/solve.auto-merge.lib.mjs +70 -164
- package/src/solve.mjs +31 -193
- package/src/solve.repository.lib.mjs +0 -83
- package/src/solve.results.lib.mjs +2 -92
- package/src/solve.session.lib.mjs +52 -19
- package/src/solve.tool-uncommitted.lib.mjs +22 -0
- package/src/state-lock.lib.mjs +82 -0
- package/src/telegram-bot.mjs +17 -65
- package/src/telegram-fix-command.lib.mjs +1 -8
- package/src/telegram-merge-queue.lib.mjs +3 -155
- package/src/telegram-solve-queue.lib.mjs +9 -168
- package/src/telegram-task-command.lib.mjs +1 -8
- package/src/use-m-bootstrap.lib.mjs +6 -5
- package/src/use-with-retry.lib.mjs +128 -2
- package/src/working-session-summary.lib.mjs +47 -1
|
@@ -3,7 +3,6 @@ import { ensureUseM } from './use-m-bootstrap.lib.mjs';
|
|
|
3
3
|
|
|
4
4
|
// Repository management module for solve command
|
|
5
5
|
// Extracted from solve.mjs to keep files under 1500 lines
|
|
6
|
-
|
|
7
6
|
// Use use-m to dynamically import modules for cross-runtime compatibility
|
|
8
7
|
// Check if use is already defined globally (when imported from solve.mjs)
|
|
9
8
|
// If not, fetch it (when running standalone)
|
|
@@ -19,7 +18,6 @@ const $ = wrapDollarWithGhRetry((await use('command-stream')).$);
|
|
|
19
18
|
const os = (await use('os')).default;
|
|
20
19
|
const path = (await use('path')).default;
|
|
21
20
|
const fs = (await use('fs')).promises;
|
|
22
|
-
|
|
23
21
|
// Import shared library functions
|
|
24
22
|
const lib = await import('./lib.mjs');
|
|
25
23
|
// Import Sentry integration
|
|
@@ -27,7 +25,6 @@ const sentryLib = await import('./sentry.lib.mjs');
|
|
|
27
25
|
const { reportError } = sentryLib;
|
|
28
26
|
|
|
29
27
|
const { log, formatAligned } = lib;
|
|
30
|
-
|
|
31
28
|
// Import exit handler
|
|
32
29
|
import { safeExit } from './exit-handler.lib.mjs';
|
|
33
30
|
import { ensureAiToolScratchIgnored } from './ai-tool-scratch.lib.mjs';
|
|
@@ -40,7 +37,6 @@ const githubLib = await import('./github.lib.mjs');
|
|
|
40
37
|
const { checkRepositoryWritePermission } = githubLib;
|
|
41
38
|
// Issue #1625: centralized markers + tracked posting.
|
|
42
39
|
const { REPOSITORY_INITIALIZATION_REQUIRED_MARKER, postTrackedComment } = await import('./tool-comments.lib.mjs');
|
|
43
|
-
|
|
44
40
|
// Get root repository (fork source or self), or null if inaccessible
|
|
45
41
|
export const getRootRepository = async (owner, repo) => {
|
|
46
42
|
try {
|
|
@@ -54,7 +50,6 @@ export const getRootRepository = async (owner, repo) => {
|
|
|
54
50
|
return null;
|
|
55
51
|
}
|
|
56
52
|
};
|
|
57
|
-
|
|
58
53
|
// Check if current user has a fork of the given root repository
|
|
59
54
|
export const checkExistingForkOfRoot = async rootRepo => {
|
|
60
55
|
try {
|
|
@@ -80,7 +75,6 @@ export const checkExistingForkOfRoot = async rootRepo => {
|
|
|
80
75
|
return null;
|
|
81
76
|
}
|
|
82
77
|
};
|
|
83
|
-
|
|
84
78
|
/**
|
|
85
79
|
* Validate that a fork's parent matches the expected upstream repository.
|
|
86
80
|
* This prevents issues where a fork was created from an intermediate fork (fork of a fork)
|
|
@@ -101,7 +95,6 @@ export const validateForkParent = async (forkRepo, expectedUpstream) => {
|
|
|
101
95
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
102
96
|
try {
|
|
103
97
|
const forkInfoResult = await $`gh api repos/${forkRepo} --jq '{fork: .fork, parent: .parent.full_name, source: .source.full_name}'`;
|
|
104
|
-
|
|
105
98
|
// Check for network errors in non-zero exit code
|
|
106
99
|
if (forkInfoResult.code !== 0) {
|
|
107
100
|
const errorOutput = (forkInfoResult.stderr?.toString() || '') + (forkInfoResult.stdout?.toString() || '');
|
|
@@ -122,7 +115,6 @@ export const validateForkParent = async (forkRepo, expectedUpstream) => {
|
|
|
122
115
|
const isFork = forkInfo.fork === true;
|
|
123
116
|
const parent = forkInfo.parent || null;
|
|
124
117
|
const source = forkInfo.source || null;
|
|
125
|
-
|
|
126
118
|
// If not a fork at all, it's invalid for our purposes
|
|
127
119
|
if (!isFork) {
|
|
128
120
|
return { isValid: false, isFork: false, parent: null, source: null, error: `Repository ${forkRepo} is not a GitHub fork` };
|
|
@@ -132,7 +124,6 @@ export const validateForkParent = async (forkRepo, expectedUpstream) => {
|
|
|
132
124
|
// The SOURCE (ultimate root) is also acceptable as it indicates the fork is part of the correct hierarchy
|
|
133
125
|
const parentMatches = parent === expectedUpstream;
|
|
134
126
|
const sourceMatches = source === expectedUpstream;
|
|
135
|
-
|
|
136
127
|
if (parentMatches) {
|
|
137
128
|
return { isValid: true, isFork: true, parent, source, error: null };
|
|
138
129
|
}
|
|
@@ -142,7 +133,6 @@ export const validateForkParent = async (forkRepo, expectedUpstream) => {
|
|
|
142
133
|
if (sourceMatches && !parentMatches) {
|
|
143
134
|
return { isValid: false, isFork: true, parent, source, error: `Fork ${forkRepo} was created from ${parent} (intermediate fork), not directly from ${expectedUpstream}. This can cause pull requests to include unexpected commits from the intermediate fork.` };
|
|
144
135
|
}
|
|
145
|
-
|
|
146
136
|
// Neither parent nor source matches - completely different repository tree
|
|
147
137
|
return { isValid: false, isFork: true, parent, source, error: `Fork ${forkRepo} is from a different repository tree (parent: ${parent}, source: ${source}) and cannot be used with ${expectedUpstream}` };
|
|
148
138
|
} catch (error) {
|
|
@@ -163,7 +153,6 @@ export const validateForkParent = async (forkRepo, expectedUpstream) => {
|
|
|
163
153
|
}
|
|
164
154
|
return networkErr(`Failed to validate fork after ${maxAttempts} attempts`);
|
|
165
155
|
};
|
|
166
|
-
|
|
167
156
|
/**
|
|
168
157
|
* Build workspace directory name according to the specification:
|
|
169
158
|
* /tmp/hive-mind-solve-gh-{owner}/{repo}-issue-{issueNumber}-workspace-{timestamp}
|
|
@@ -193,7 +182,6 @@ export const setupTempDirectory = async (argv, workspaceInfo = null) => {
|
|
|
193
182
|
// needsClone indicates if the repository needs to be cloned into the directory
|
|
194
183
|
// This is true when: new directory is created, or existing directory is empty
|
|
195
184
|
let needsClone = true;
|
|
196
|
-
|
|
197
185
|
// Check if workspace mode should be enabled (works with all tools)
|
|
198
186
|
const useWorkspaces = argv.enableWorkspaces;
|
|
199
187
|
|
|
@@ -202,7 +190,6 @@ export const setupTempDirectory = async (argv, workspaceInfo = null) => {
|
|
|
202
190
|
// because Claude Code stores sessions by working directory path, not session ID alone
|
|
203
191
|
if (argv.workingDirectory) {
|
|
204
192
|
tempDir = path.resolve(argv.workingDirectory);
|
|
205
|
-
|
|
206
193
|
// Check if directory exists
|
|
207
194
|
try {
|
|
208
195
|
const stat = await fs.stat(tempDir);
|
|
@@ -232,7 +219,6 @@ export const setupTempDirectory = async (argv, workspaceInfo = null) => {
|
|
|
232
219
|
|
|
233
220
|
return { tempDir, workspaceTmpDir, isResuming, needsClone };
|
|
234
221
|
}
|
|
235
|
-
|
|
236
222
|
if (isResuming) {
|
|
237
223
|
// When resuming without --working-directory, create a new temp directory
|
|
238
224
|
// WARNING: This will NOT work correctly with Claude Code because the session
|
|
@@ -244,7 +230,6 @@ export const setupTempDirectory = async (argv, workspaceInfo = null) => {
|
|
|
244
230
|
// Check if session log exists to verify session is valid
|
|
245
231
|
await fs.access(sessionLogPattern);
|
|
246
232
|
await log(`🔄 Resuming session ${argv.resume} (session log found)`);
|
|
247
|
-
|
|
248
233
|
// For resumed sessions, create new temp directory since old one may be cleaned up
|
|
249
234
|
tempDir = path.join(os.tmpdir(), `gh-issue-solver-resume-${argv.resume}-${Date.now()}`);
|
|
250
235
|
await fs.mkdir(tempDir, { recursive: true });
|
|
@@ -273,12 +258,10 @@ export const setupTempDirectory = async (argv, workspaceInfo = null) => {
|
|
|
273
258
|
// {workspace}/tmp - for temp files, logs, command outputs
|
|
274
259
|
const repoDir = path.join(workspaceDir, 'repository');
|
|
275
260
|
workspaceTmpDir = path.join(workspaceDir, 'tmp');
|
|
276
|
-
|
|
277
261
|
await fs.mkdir(repoDir, { recursive: true });
|
|
278
262
|
await fs.mkdir(workspaceTmpDir, { recursive: true });
|
|
279
263
|
|
|
280
264
|
tempDir = repoDir;
|
|
281
|
-
|
|
282
265
|
await log(`\n${formatAligned('📂', 'Workspace mode:', 'ENABLED')}`);
|
|
283
266
|
await log(formatAligned('', 'Workspace root:', workspaceDir, 2));
|
|
284
267
|
await log(formatAligned('', 'Repository dir:', repoDir, 2));
|
|
@@ -291,7 +274,6 @@ export const setupTempDirectory = async (argv, workspaceInfo = null) => {
|
|
|
291
274
|
|
|
292
275
|
return { tempDir, workspaceTmpDir, isResuming, needsClone };
|
|
293
276
|
};
|
|
294
|
-
|
|
295
277
|
// Try to initialize an empty repository by creating a simple README.md
|
|
296
278
|
// This makes the repository forkable and allows branch creation
|
|
297
279
|
// Exported for use in solve.repo-setup.lib.mjs (direct access path for empty repos)
|
|
@@ -302,7 +284,6 @@ export const tryInitializeEmptyRepository = async (owner, repo) => {
|
|
|
302
284
|
// Check write access before attempting to create files
|
|
303
285
|
await log(`${formatAligned('', '', 'Checking repository write access...')}`);
|
|
304
286
|
const hasWriteAccess = await checkRepositoryWritePermission(owner, repo, { useFork: false });
|
|
305
|
-
|
|
306
287
|
if (!hasWriteAccess) {
|
|
307
288
|
await log(`${formatAligned('❌', 'No access:', 'You do not have write access to this repository')}`);
|
|
308
289
|
await log(`${formatAligned('', '', 'Cannot initialize empty repository without write access')}`);
|
|
@@ -310,7 +291,6 @@ export const tryInitializeEmptyRepository = async (owner, repo) => {
|
|
|
310
291
|
}
|
|
311
292
|
|
|
312
293
|
await log(`${formatAligned('', '', 'Creating a simple README.md to make repository forkable')}`);
|
|
313
|
-
|
|
314
294
|
// Get repository description to include in README
|
|
315
295
|
const repoInfoResult = await $`gh api repos/${owner}/${repo} --jq '{description: .description}'`;
|
|
316
296
|
let description = '';
|
|
@@ -329,7 +309,6 @@ export const tryInitializeEmptyRepository = async (owner, repo) => {
|
|
|
329
309
|
readmeContent += `\n${description}\n`;
|
|
330
310
|
}
|
|
331
311
|
const base64Content = Buffer.from(readmeContent).toString('base64');
|
|
332
|
-
|
|
333
312
|
// Try to create README.md using GitHub API
|
|
334
313
|
// Issue #2119: `--field content="${base64Content}"` would leak literal quotes
|
|
335
314
|
// into the field value as soon as command-stream decides the value needs
|
|
@@ -338,7 +317,6 @@ export const tryInitializeEmptyRepository = async (owner, repo) => {
|
|
|
338
317
|
const createResult = await $`gh api repos/${owner}/${repo}/contents/README.md --method PUT --silent \
|
|
339
318
|
--field message=${'Initialize repository with README'} \
|
|
340
319
|
--field ${contentField} 2>&1`;
|
|
341
|
-
|
|
342
320
|
if (createResult.code === 0) {
|
|
343
321
|
await log(`${formatAligned('✅', 'Success:', 'README.md created successfully')}`);
|
|
344
322
|
await log(`${formatAligned('', '', 'Repository is now forkable, retrying fork creation...')}`);
|
|
@@ -372,7 +350,6 @@ export const setupRepository = async (argv, owner, repo, forkOwner = null, issue
|
|
|
372
350
|
let repoToClone = `${owner}/${repo}`;
|
|
373
351
|
let forkedRepo = null;
|
|
374
352
|
let upstreamRemote = null;
|
|
375
|
-
|
|
376
353
|
// Priority 1: Check --fork flag first (user explicitly wants to use their own fork)
|
|
377
354
|
// This takes precedence over forkOwner to avoid trying to access someone else's fork
|
|
378
355
|
if (argv.fork) {
|
|
@@ -386,7 +363,6 @@ export const setupRepository = async (argv, owner, repo, forkOwner = null, issue
|
|
|
386
363
|
await safeExit(1, 'Repository setup failed');
|
|
387
364
|
}
|
|
388
365
|
const currentUser = userResult.stdout.toString().trim();
|
|
389
|
-
|
|
390
366
|
// Check if user owns the repository (Issue #1206)
|
|
391
367
|
// GitHub doesn't allow forking your own repositories and returns HTTP 403
|
|
392
368
|
// When --fork is explicitly used, fail with a clear error and suggest --auto-fork
|
|
@@ -416,13 +392,11 @@ export const setupRepository = async (argv, owner, repo, forkOwner = null, issue
|
|
|
416
392
|
// as an existing fork we already have
|
|
417
393
|
await log(`${formatAligned('🔍', 'Detecting fork conflicts...', '')}`);
|
|
418
394
|
const rootRepo = await getRootRepository(owner, repo);
|
|
419
|
-
|
|
420
395
|
if (rootRepo) {
|
|
421
396
|
const existingFork = await checkExistingForkOfRoot(rootRepo);
|
|
422
397
|
|
|
423
398
|
if (existingFork) {
|
|
424
399
|
const existingForkOwner = existingFork.split('/')[0];
|
|
425
|
-
|
|
426
400
|
if (existingForkOwner === currentUser) {
|
|
427
401
|
const targetRepo = `${owner}/${repo}`;
|
|
428
402
|
const targetIsRoot = targetRepo === rootRepo;
|
|
@@ -459,7 +433,6 @@ export const setupRepository = async (argv, owner, repo, forkOwner = null, issue
|
|
|
459
433
|
}
|
|
460
434
|
}
|
|
461
435
|
}
|
|
462
|
-
|
|
463
436
|
await log(`${formatAligned('✅', 'No fork conflict:', 'Safe to proceed')}`);
|
|
464
437
|
} else {
|
|
465
438
|
await log(`${formatAligned('⚠️', 'Warning:', 'Could not determine root repository')}`);
|
|
@@ -472,7 +445,6 @@ export const setupRepository = async (argv, owner, repo, forkOwner = null, issue
|
|
|
472
445
|
let existingForkName = null;
|
|
473
446
|
const standardForkName = `${currentUser}/${repo}`;
|
|
474
447
|
const prefixedForkName = `${currentUser}/${owner}-${repo}`;
|
|
475
|
-
|
|
476
448
|
// Determine expected fork name based on --prefix-fork-name-with-owner-name option
|
|
477
449
|
const expectedForkName = argv.prefixForkNameWithOwnerName ? prefixedForkName : standardForkName;
|
|
478
450
|
const alternateForkName = argv.prefixForkNameWithOwnerName ? standardForkName : prefixedForkName;
|
|
@@ -498,14 +470,12 @@ export const setupRepository = async (argv, owner, repo, forkOwner = null, issue
|
|
|
498
470
|
await log(` Creating prefixed fork ${expectedForkName} instead (--prefix-fork-name-with-owner-name enabled)`);
|
|
499
471
|
}
|
|
500
472
|
}
|
|
501
|
-
|
|
502
473
|
if (existingForkName) {
|
|
503
474
|
// Fork exists - validate that its parent matches the expected upstream
|
|
504
475
|
await log(`${formatAligned('✅', 'Fork exists:', existingForkName)}`);
|
|
505
476
|
await log(`${formatAligned('🔍', 'Validating fork parent...', '')}`);
|
|
506
477
|
|
|
507
478
|
const forkValidation = await validateForkParent(existingForkName, `${owner}/${repo}`);
|
|
508
|
-
|
|
509
479
|
if (forkValidation.isValid) {
|
|
510
480
|
// Fork is valid — use it
|
|
511
481
|
await log(`${formatAligned('✅', 'Fork parent validated:', `${forkValidation.parent}`)}`);
|
|
@@ -581,7 +551,6 @@ export const setupRepository = async (argv, owner, repo, forkOwner = null, issue
|
|
|
581
551
|
});
|
|
582
552
|
safetyCheckDescription = branchSafety.safetyCheckDescription;
|
|
583
553
|
likelyDetachedFork = Boolean(branchSafety.likelyDetachedFork);
|
|
584
|
-
|
|
585
554
|
if (likelyDetachedFork) {
|
|
586
555
|
await log(`${formatAligned('🔗', 'Detached fork:', `${existingForkName} shares history with ${owner}/${repo} but is not a GitHub fork — this matches a fork detached by a private/public visibility change`)}`);
|
|
587
556
|
}
|
|
@@ -634,11 +603,9 @@ export const setupRepository = async (argv, owner, repo, forkOwner = null, issue
|
|
|
634
603
|
existingForkName = null; // Fall through to fork creation below
|
|
635
604
|
}
|
|
636
605
|
}
|
|
637
|
-
|
|
638
606
|
if (!existingForkName) {
|
|
639
607
|
// Need to create fork with retry logic for concurrent scenarios
|
|
640
608
|
await log(`${formatAligned('🔄', 'Creating fork...', '')}`);
|
|
641
|
-
|
|
642
609
|
const maxForkRetries = 5;
|
|
643
610
|
const baseDelay = 2000; // Start with 2 seconds
|
|
644
611
|
let forkCreated = false;
|
|
@@ -647,7 +614,6 @@ export const setupRepository = async (argv, owner, repo, forkOwner = null, issue
|
|
|
647
614
|
// Determine the expected fork name based on --prefix-fork-name-with-owner-name option
|
|
648
615
|
const defaultForkName = argv.prefixForkNameWithOwnerName ? `${owner}-${repo}` : repo;
|
|
649
616
|
let actualForkName = `${currentUser}/${defaultForkName}`;
|
|
650
|
-
|
|
651
617
|
for (let attempt = 1; attempt <= maxForkRetries; attempt++) {
|
|
652
618
|
let forkResult;
|
|
653
619
|
// Issue #1518: Log the exact fork command for debugging non-fork creation scenarios
|
|
@@ -667,7 +633,6 @@ export const setupRepository = async (argv, owner, repo, forkOwner = null, issue
|
|
|
667
633
|
if (parsedForkName) {
|
|
668
634
|
actualForkName = parsedForkName;
|
|
669
635
|
}
|
|
670
|
-
|
|
671
636
|
if (forkResult.code === 0) {
|
|
672
637
|
// Fork successfully created or already exists
|
|
673
638
|
if (forkOutput.includes('already exists')) {
|
|
@@ -724,7 +689,6 @@ export const setupRepository = async (argv, owner, repo, forkOwner = null, issue
|
|
|
724
689
|
await log('');
|
|
725
690
|
await safeExit(1, 'Repository setup failed - repository not accessible (HTTP 404)');
|
|
726
691
|
}
|
|
727
|
-
|
|
728
692
|
// Check if it's an empty repository (HTTP 403) - try to auto-fix
|
|
729
693
|
if (forkOutput.includes('HTTP 403') && (forkOutput.includes('Empty repositories cannot be forked') || forkOutput.includes('contains no Git content'))) {
|
|
730
694
|
// Empty repository detected - try to initialize it
|
|
@@ -735,7 +699,6 @@ export const setupRepository = async (argv, owner, repo, forkOwner = null, issue
|
|
|
735
699
|
|
|
736
700
|
// Try to initialize the repository by creating a README.md
|
|
737
701
|
const initialized = await tryInitializeEmptyRepository(owner, repo);
|
|
738
|
-
|
|
739
702
|
if (initialized) {
|
|
740
703
|
// Success! Repository is now initialized, retry fork creation
|
|
741
704
|
await log('');
|
|
@@ -772,7 +735,6 @@ export const setupRepository = async (argv, owner, repo, forkOwner = null, issue
|
|
|
772
735
|
if (issueMatch) {
|
|
773
736
|
const issueNumber = issueMatch[1];
|
|
774
737
|
await log(`${formatAligned('💬', 'Creating comment:', 'Requesting maintainer to initialize repository...')}`);
|
|
775
|
-
|
|
776
738
|
const commentBody = `## ⚠️ ${REPOSITORY_INITIALIZATION_REQUIRED_MARKER}
|
|
777
739
|
|
|
778
740
|
Hello! I attempted to work on this issue, but encountered a problem:
|
|
@@ -791,7 +753,6 @@ Please add initial content to the repository. Even a simple README.md (even if i
|
|
|
791
753
|
Once the repository contains at least one commit with any file, I'll be able to fork it and proceed with solving this issue.
|
|
792
754
|
|
|
793
755
|
Thank you!`;
|
|
794
|
-
|
|
795
756
|
const posted = await postTrackedComment({ $, owner, repo, targetNumber: issueNumber, body: commentBody });
|
|
796
757
|
if (posted.ok) {
|
|
797
758
|
await log(`${formatAligned('✅', 'Comment created:', `Posted to issue #${issueNumber}${posted.commentId ? ` (id=${posted.commentId})` : ''}`)}`);
|
|
@@ -804,7 +765,6 @@ Thank you!`;
|
|
|
804
765
|
await log(`${formatAligned('⚠️', 'Note:', 'Could not post comment to issue (this is not critical)')}`);
|
|
805
766
|
}
|
|
806
767
|
}
|
|
807
|
-
|
|
808
768
|
await safeExit(1, 'Repository setup failed - empty repository');
|
|
809
769
|
}
|
|
810
770
|
}
|
|
@@ -812,7 +772,6 @@ Thank you!`;
|
|
|
812
772
|
// Check if fork was created by another worker even if error message doesn't explicitly say so
|
|
813
773
|
await log(`${formatAligned('🔍', 'Checking:', 'If fork exists after failed creation attempt...')}`);
|
|
814
774
|
const checkResult = await $`gh repo view ${actualForkName} --json name 2>/dev/null`;
|
|
815
|
-
|
|
816
775
|
if (checkResult.code === 0) {
|
|
817
776
|
// Fork exists now (created by another worker during our attempt)
|
|
818
777
|
await log(`${formatAligned('✅', 'Fork found:', 'Created by another concurrent worker')}`);
|
|
@@ -834,7 +793,6 @@ Thank you!`;
|
|
|
834
793
|
}
|
|
835
794
|
}
|
|
836
795
|
}
|
|
837
|
-
|
|
838
796
|
// If fork exists (either created or already existed), verify it's accessible
|
|
839
797
|
if (forkExists) {
|
|
840
798
|
await log(`${formatAligned('🔍', 'Verifying fork:', 'Checking accessibility...')}`);
|
|
@@ -842,7 +800,6 @@ Thank you!`;
|
|
|
842
800
|
// Verify fork with retries (GitHub may need time to propagate)
|
|
843
801
|
const maxVerifyRetries = 5;
|
|
844
802
|
let forkVerified = false;
|
|
845
|
-
|
|
846
803
|
for (let attempt = 1; attempt <= maxVerifyRetries; attempt++) {
|
|
847
804
|
const delay = baseDelay * Math.pow(2, attempt - 1);
|
|
848
805
|
if (attempt > 1) {
|
|
@@ -857,7 +814,6 @@ Thank you!`;
|
|
|
857
814
|
break;
|
|
858
815
|
}
|
|
859
816
|
}
|
|
860
|
-
|
|
861
817
|
if (!forkVerified) {
|
|
862
818
|
await log(`${formatAligned('❌', 'Error:', 'Fork exists but not accessible after multiple retries')}`);
|
|
863
819
|
await log(`${formatAligned('', 'Suggestion:', 'GitHub may be experiencing delays - try running the command again in a few minutes')}`);
|
|
@@ -879,7 +835,6 @@ Thank you!`;
|
|
|
879
835
|
reportError(new Error(`Fork created as non-fork: ${pcv.error}`), { context: 'fork_creation_validation', forkRepo: actualForkName, expectedUpstream: `${owner}/${repo}`, isFork: pcv.isFork, parent: pcv.parent, source: pcv.source });
|
|
880
836
|
}
|
|
881
837
|
}
|
|
882
|
-
|
|
883
838
|
repoToClone = actualForkName;
|
|
884
839
|
forkedRepo = actualForkName;
|
|
885
840
|
upstreamRemote = `${owner}/${repo}`;
|
|
@@ -896,14 +851,12 @@ Thank you!`;
|
|
|
896
851
|
const prefixedForkName = `${forkOwner}/${owner}-${headRepoName}`;
|
|
897
852
|
const expectedForkName = forkRepoName ? `${forkOwner}/${forkRepoName}` : argv.prefixForkNameWithOwnerName ? prefixedForkName : standardForkName;
|
|
898
853
|
const alternateForkName = forkRepoName ? null : argv.prefixForkNameWithOwnerName ? standardForkName : prefixedForkName;
|
|
899
|
-
|
|
900
854
|
await log(`${formatAligned('✅', 'Using fork:', expectedForkName)}\n`);
|
|
901
855
|
|
|
902
856
|
// Verify the fork exists and is accessible - try expected name first, then alternate
|
|
903
857
|
await log(`${formatAligned('🔍', 'Verifying fork:', 'Checking accessibility...')}`);
|
|
904
858
|
let forkCheckResult = await $`gh repo view ${expectedForkName} --json name 2>/dev/null`;
|
|
905
859
|
let actualForkName = expectedForkName;
|
|
906
|
-
|
|
907
860
|
if (forkCheckResult.code !== 0 && alternateForkName && !argv.prefixForkNameWithOwnerName) {
|
|
908
861
|
// Only try alternate name if --prefix-fork-name-with-owner-name is off AND we're guessing
|
|
909
862
|
// (forkRepoName authoritative → alternateForkName is null and no fallback is attempted).
|
|
@@ -915,7 +868,6 @@ Thank you!`;
|
|
|
915
868
|
|
|
916
869
|
if (forkCheckResult.code === 0) {
|
|
917
870
|
await log(`${formatAligned('✅', 'Fork verified:', `${actualForkName} is accessible`)}`);
|
|
918
|
-
|
|
919
871
|
// Validate fork parent before using it (prevents issue #967)
|
|
920
872
|
await log(`${formatAligned('🔍', 'Validating fork parent...', '')}`);
|
|
921
873
|
const forkValidation = await validateForkParent(actualForkName, `${owner}/${repo}`);
|
|
@@ -962,7 +914,6 @@ Thank you!`;
|
|
|
962
914
|
} else {
|
|
963
915
|
await log(`${formatAligned('✅', 'Fork parent validated:', `${forkValidation.parent}`)}`);
|
|
964
916
|
}
|
|
965
|
-
|
|
966
917
|
repoToClone = actualForkName;
|
|
967
918
|
forkedRepo = actualForkName;
|
|
968
919
|
upstreamRemote = `${owner}/${repo}`;
|
|
@@ -977,11 +928,9 @@ Thank you!`;
|
|
|
977
928
|
|
|
978
929
|
return { repoToClone, forkedRepo, upstreamRemote, prForkOwner: forkOwner };
|
|
979
930
|
};
|
|
980
|
-
|
|
981
931
|
// Classify git clone errors to determine if they are retryable
|
|
982
932
|
export const classifyCloneError = errorOutput => {
|
|
983
933
|
const output = errorOutput.toLowerCase();
|
|
984
|
-
|
|
985
934
|
// Issue #1211: ENOSPC (disk full) errors - NOT retryable, requires user action
|
|
986
935
|
if (lib.isENOSPC(errorOutput) || output.includes('no space left on device') || (output.includes('unable to write file') && output.includes('error')) || output.includes('errno -28')) {
|
|
987
936
|
return { type: 'ENOSPC', retryable: false, description: 'No space left on device' };
|
|
@@ -991,7 +940,6 @@ export const classifyCloneError = errorOutput => {
|
|
|
991
940
|
if (output.includes('error: 500') || output.includes('internal server error') || output.includes('error: 502') || output.includes('error: 503') || output.includes('error: 504')) {
|
|
992
941
|
return { type: 'TRANSIENT', retryable: true, description: 'GitHub server error' };
|
|
993
942
|
}
|
|
994
|
-
|
|
995
943
|
// Network-related errors - typically retryable
|
|
996
944
|
// Issue #1957: git fetch-pack/sideband disconnects (e.g.
|
|
997
945
|
// "fetch-pack: unexpected disconnect while reading sideband packet",
|
|
@@ -1005,7 +953,6 @@ export const classifyCloneError = errorOutput => {
|
|
|
1005
953
|
if (output.includes('error: 401') || output.includes('error: 403') || output.includes('authentication failed') || output.includes('permission denied')) {
|
|
1006
954
|
return { type: 'PERMISSION', retryable: false, description: 'Authentication or permission error' };
|
|
1007
955
|
}
|
|
1008
|
-
|
|
1009
956
|
// Repository not found - not retryable
|
|
1010
957
|
if (output.includes('error: 404') || output.includes('not found') || output.includes('repository not found')) {
|
|
1011
958
|
return { type: 'NOT_FOUND', retryable: false, description: 'Repository not found' };
|
|
@@ -1015,7 +962,6 @@ export const classifyCloneError = errorOutput => {
|
|
|
1015
962
|
if (output.includes('rate limit') || output.includes('too many requests') || output.includes('api rate limit exceeded')) {
|
|
1016
963
|
return { type: 'RATE_LIMIT', retryable: true, description: 'Rate limit exceeded' };
|
|
1017
964
|
}
|
|
1018
|
-
|
|
1019
965
|
// Default to retryable for unknown errors
|
|
1020
966
|
return { type: 'UNKNOWN', retryable: true, description: 'Unknown error' };
|
|
1021
967
|
};
|
|
@@ -1037,14 +983,12 @@ export const cleanPartialClone = async tempDir => {
|
|
|
1037
983
|
}
|
|
1038
984
|
}
|
|
1039
985
|
};
|
|
1040
|
-
|
|
1041
986
|
// Clone repository and set up remotes with retry mechanism
|
|
1042
987
|
export const cloneRepository = async (repoToClone, tempDir, argv, owner, repo) => {
|
|
1043
988
|
const maxRetries = 3;
|
|
1044
989
|
const baseDelay = 2000; // Start with 2 seconds
|
|
1045
990
|
|
|
1046
991
|
await log(`\n${formatAligned('📥', 'Cloning repository:', repoToClone)}`);
|
|
1047
|
-
|
|
1048
992
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
1049
993
|
if (attempt > 1) {
|
|
1050
994
|
await log(`${formatAligned('⏳', 'Clone attempt:', `${attempt}/${maxRetries} (with retry logic)`)}`);
|
|
@@ -1053,7 +997,6 @@ export const cloneRepository = async (repoToClone, tempDir, argv, owner, repo) =
|
|
|
1053
997
|
// Use 2>&1 to capture all output and filter "Cloning into" message
|
|
1054
998
|
const cloneResult = await $`gh repo clone ${repoToClone} ${tempDir} 2>&1`;
|
|
1055
999
|
const cloneOutput = (cloneResult.stdout || cloneResult.stderr || '').toString().trim();
|
|
1056
|
-
|
|
1057
1000
|
// Issue #1957: `gh repo clone` (and the `git clone` it wraps) can exit 0 even when
|
|
1058
1001
|
// the underlying transfer was interrupted — e.g. "fetch-pack: unexpected disconnect
|
|
1059
1002
|
// while reading sideband packet" — leaving an incomplete or completely missing
|
|
@@ -1072,7 +1015,6 @@ export const cloneRepository = async (repoToClone, tempDir, argv, owner, repo) =
|
|
|
1072
1015
|
// Verify clone was successful (exit code 0 AND a valid working tree exists)
|
|
1073
1016
|
if (cloneResult.code === 0 && repoIsValid) {
|
|
1074
1017
|
await log(`${formatAligned('✅', 'Cloned to:', tempDir)}`);
|
|
1075
|
-
|
|
1076
1018
|
// Issue #2119: AI tools drop scratch state (`.formal-ai/`, `.playwright-mcp/`)
|
|
1077
1019
|
// into the workspace. Exclude it here, once, so every later `git status` and
|
|
1078
1020
|
// `git add -A` agrees instead of reading it as the AI's uncommitted work.
|
|
@@ -1087,7 +1029,6 @@ export const cloneRepository = async (repoToClone, tempDir, argv, owner, repo) =
|
|
|
1087
1029
|
}
|
|
1088
1030
|
return; // Success - exit function
|
|
1089
1031
|
}
|
|
1090
|
-
|
|
1091
1032
|
// Clone failed - analyze error and determine if retry is appropriate
|
|
1092
1033
|
// Issue #1957: when the wrapper exited 0 but left no valid repo, surface the
|
|
1093
1034
|
// interrupted-transfer output (which carries the real "unexpected disconnect"
|
|
@@ -1098,7 +1039,6 @@ export const cloneRepository = async (repoToClone, tempDir, argv, owner, repo) =
|
|
|
1098
1039
|
// `gh repo clone <dir>` fail with "directory exists and is not empty". Clean the
|
|
1099
1040
|
// target directory before classifying/retrying so the next attempt starts fresh.
|
|
1100
1041
|
await cleanPartialClone(tempDir);
|
|
1101
|
-
|
|
1102
1042
|
const errorClassification = classifyCloneError(errorOutput);
|
|
1103
1043
|
|
|
1104
1044
|
if (!errorClassification.retryable || attempt === maxRetries) {
|
|
@@ -1108,7 +1048,6 @@ export const cloneRepository = async (repoToClone, tempDir, argv, owner, repo) =
|
|
|
1108
1048
|
await log('');
|
|
1109
1049
|
await log(' 🔍 What happened:');
|
|
1110
1050
|
await log(` Failed to clone repository ${repoToClone}`);
|
|
1111
|
-
|
|
1112
1051
|
if (!errorClassification.retryable) {
|
|
1113
1052
|
await log(` Error type: ${errorClassification.description} (not retryable)`);
|
|
1114
1053
|
} else {
|
|
@@ -1120,7 +1059,6 @@ export const cloneRepository = async (repoToClone, tempDir, argv, owner, repo) =
|
|
|
1120
1059
|
if (line.trim()) await log(` ${line}`);
|
|
1121
1060
|
}
|
|
1122
1061
|
await log('');
|
|
1123
|
-
|
|
1124
1062
|
// Issue #1211: ENOSPC-specific guidance
|
|
1125
1063
|
if (errorClassification.type === 'ENOSPC') {
|
|
1126
1064
|
await log(' 💡 Cause: Disk is full — not enough space to clone the repository');
|
|
@@ -1163,14 +1101,12 @@ export const cloneRepository = async (repoToClone, tempDir, argv, owner, repo) =
|
|
|
1163
1101
|
const delay = baseDelay * Math.pow(2, attempt - 1); // Exponential backoff
|
|
1164
1102
|
await log(`${formatAligned('⚠️', 'Clone failed:', errorClassification.description)}`);
|
|
1165
1103
|
await log(`${formatAligned('⏳', 'Retrying:', `Waiting ${delay / 1000}s before attempt ${attempt + 1}/${maxRetries}...`)}`);
|
|
1166
|
-
|
|
1167
1104
|
if (errorClassification.type === 'RATE_LIMIT') {
|
|
1168
1105
|
await log(' 💡 Tip: Rate limiting detected - using longer delay');
|
|
1169
1106
|
}
|
|
1170
1107
|
|
|
1171
1108
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
1172
1109
|
}
|
|
1173
|
-
|
|
1174
1110
|
// This should never be reached due to the loop logic above
|
|
1175
1111
|
await log(`${formatAligned('❌', 'UNEXPECTED ERROR:', 'Clone logic failed')}`);
|
|
1176
1112
|
await safeExit(1, 'Repository setup failed');
|
|
@@ -1180,7 +1116,6 @@ export const cloneRepository = async (repoToClone, tempDir, argv, owner, repo) =
|
|
|
1180
1116
|
// Extracted into solve.fork-sync.lib.mjs (#1893) to keep this file under the
|
|
1181
1117
|
// 1500-line limit; re-exported here so existing importers keep working.
|
|
1182
1118
|
export { setupUpstreamAndSync } from './solve.fork-sync.lib.mjs';
|
|
1183
|
-
|
|
1184
1119
|
// Set up pr-fork remote for continuing someone else's fork PR with --fork flag
|
|
1185
1120
|
export const setupPrForkRemote = async (tempDir, argv, prForkOwner, repo, isContinueMode, owner = null) => {
|
|
1186
1121
|
// Only set up pr-fork remote if:
|
|
@@ -1198,7 +1133,6 @@ export const setupPrForkRemote = async (tempDir, argv, prForkOwner, repo, isCont
|
|
|
1198
1133
|
await log(`${formatAligned('⚠️', 'Warning:', 'Failed to get current user, cannot set up pr-fork remote')}`);
|
|
1199
1134
|
return null;
|
|
1200
1135
|
}
|
|
1201
|
-
|
|
1202
1136
|
const currentUser = userResult.stdout.toString().trim();
|
|
1203
1137
|
|
|
1204
1138
|
// If PR is from current user's fork, no need for pr-fork remote
|
|
@@ -1206,7 +1140,6 @@ export const setupPrForkRemote = async (tempDir, argv, prForkOwner, repo, isCont
|
|
|
1206
1140
|
await log(`${formatAligned('ℹ️', 'PR fork owner:', 'Same as current user, using origin remote')}`);
|
|
1207
1141
|
return null;
|
|
1208
1142
|
}
|
|
1209
|
-
|
|
1210
1143
|
// This is someone else's fork - add it as pr-fork remote
|
|
1211
1144
|
// IMPORTANT: The fork owner's repository name is independent of our naming preferences
|
|
1212
1145
|
// We need to discover the actual fork name, not assume it matches our convention
|
|
@@ -1215,7 +1148,6 @@ export const setupPrForkRemote = async (tempDir, argv, prForkOwner, repo, isCont
|
|
|
1215
1148
|
await log(`${formatAligned('🔗', 'Setting up pr-fork:', "Branch exists in another user's fork")}`);
|
|
1216
1149
|
await log(`${formatAligned('', 'PR fork owner:', prForkOwner)}`);
|
|
1217
1150
|
await log(`${formatAligned('', 'Current user:', currentUser)}`);
|
|
1218
|
-
|
|
1219
1151
|
// Discover the actual fork repository name by querying GitHub API
|
|
1220
1152
|
// The fork could have any name (standard, prefixed, or custom renamed)
|
|
1221
1153
|
let prForkRepoName = null;
|
|
@@ -1237,7 +1169,6 @@ export const setupPrForkRemote = async (tempDir, argv, prForkOwner, repo, isCont
|
|
|
1237
1169
|
}
|
|
1238
1170
|
}
|
|
1239
1171
|
}
|
|
1240
|
-
|
|
1241
1172
|
// Strategy 2: If not found in forks list, try common naming patterns
|
|
1242
1173
|
if (!prForkRepoName) {
|
|
1243
1174
|
const possibleNames = [
|
|
@@ -1246,7 +1177,6 @@ export const setupPrForkRemote = async (tempDir, argv, prForkOwner, repo, isCont
|
|
|
1246
1177
|
].filter(Boolean);
|
|
1247
1178
|
|
|
1248
1179
|
await log(`${formatAligned('🔍', 'Trying common names:', possibleNames.join(', '))}`);
|
|
1249
|
-
|
|
1250
1180
|
for (const candidateName of possibleNames) {
|
|
1251
1181
|
const checkResult = await $`gh repo view ${prForkOwner}/${candidateName} --json name 2>/dev/null`;
|
|
1252
1182
|
if (checkResult.code === 0) {
|
|
@@ -1265,7 +1195,6 @@ export const setupPrForkRemote = async (tempDir, argv, prForkOwner, repo, isCont
|
|
|
1265
1195
|
await log(`${formatAligned('', 'Workaround:', 'Remove --fork flag to continue work in the original fork')}`);
|
|
1266
1196
|
return null;
|
|
1267
1197
|
}
|
|
1268
|
-
|
|
1269
1198
|
await log(`${formatAligned('', 'Action:', `Adding ${prForkOwner}/${prForkRepoName} as pr-fork remote`)}`);
|
|
1270
1199
|
|
|
1271
1200
|
const addRemoteResult = await $({
|
|
@@ -1280,9 +1209,7 @@ export const setupPrForkRemote = async (tempDir, argv, prForkOwner, repo, isCont
|
|
|
1280
1209
|
await log(`${formatAligned('', 'Workaround:', 'Remove --fork flag to continue work in the original fork')}`);
|
|
1281
1210
|
return null;
|
|
1282
1211
|
}
|
|
1283
|
-
|
|
1284
1212
|
await log(`${formatAligned('✅', 'Remote added:', 'pr-fork')}`);
|
|
1285
|
-
|
|
1286
1213
|
// Fetch from pr-fork to get the branch
|
|
1287
1214
|
await log(`${formatAligned('📥', 'Fetching branches:', 'From pr-fork remote...')}`);
|
|
1288
1215
|
const fetchPrForkResult = await $({ cwd: tempDir })`git fetch pr-fork`;
|
|
@@ -1299,7 +1226,6 @@ export const setupPrForkRemote = async (tempDir, argv, prForkOwner, repo, isCont
|
|
|
1299
1226
|
await log(`${formatAligned('ℹ️', 'Next step:', 'Will checkout branch from pr-fork remote')}`);
|
|
1300
1227
|
return 'pr-fork';
|
|
1301
1228
|
};
|
|
1302
|
-
|
|
1303
1229
|
// Checkout branch for continue mode (PR branch from remote)
|
|
1304
1230
|
// prNumber is optional - when provided, enables PR refs fallback (refs/pull/{number}/head)
|
|
1305
1231
|
export const checkoutPrBranch = async (tempDir, branchName, prForkRemote, prForkOwner, prNumber = null) => {
|
|
@@ -1307,7 +1233,6 @@ export const checkoutPrBranch = async (tempDir, branchName, prForkRemote, prFork
|
|
|
1307
1233
|
|
|
1308
1234
|
// Determine which remote to use for branch checkout
|
|
1309
1235
|
const remoteName = prForkRemote || 'origin';
|
|
1310
|
-
|
|
1311
1236
|
// First fetch all branches from remote (if not already fetched from pr-fork)
|
|
1312
1237
|
if (!prForkRemote) {
|
|
1313
1238
|
await log(`${formatAligned('📥', 'Fetching branches:', 'From remote...')}`);
|
|
@@ -1319,7 +1244,6 @@ export const checkoutPrBranch = async (tempDir, branchName, prForkRemote, prFork
|
|
|
1319
1244
|
} else {
|
|
1320
1245
|
await log(`${formatAligned('ℹ️', 'Using pr-fork remote:', `Branch exists in ${prForkOwner}'s fork`)}`);
|
|
1321
1246
|
}
|
|
1322
|
-
|
|
1323
1247
|
// Checkout the PR branch (it might exist locally or remotely)
|
|
1324
1248
|
const localBranchResult = await $({ cwd: tempDir })`git show-ref --verify --quiet refs/heads/${branchName}`;
|
|
1325
1249
|
|
|
@@ -1330,7 +1254,6 @@ export const checkoutPrBranch = async (tempDir, branchName, prForkRemote, prFork
|
|
|
1330
1254
|
} else {
|
|
1331
1255
|
// Branch doesn't exist locally, try to checkout from remote
|
|
1332
1256
|
checkoutResult = await $({ cwd: tempDir })`git checkout -b ${branchName} ${remoteName}/${branchName}`;
|
|
1333
|
-
|
|
1334
1257
|
// If checkout from origin failed, try upstream remote as fallback
|
|
1335
1258
|
// This handles the case where we're in fork mode but the PR branch exists in upstream
|
|
1336
1259
|
// (e.g., a bot created PR in the upstream repo, not a fork PR)
|
|
@@ -1343,7 +1266,6 @@ export const checkoutPrBranch = async (tempDir, branchName, prForkRemote, prFork
|
|
|
1343
1266
|
// Fetch from upstream to ensure we have the latest branches
|
|
1344
1267
|
await log(`${formatAligned('📥', 'Fetching from upstream:', 'Looking for PR branch...')}`);
|
|
1345
1268
|
const fetchUpstreamResult = await $({ cwd: tempDir })`git fetch upstream`;
|
|
1346
|
-
|
|
1347
1269
|
if (fetchUpstreamResult.code === 0) {
|
|
1348
1270
|
// Check if branch exists in upstream
|
|
1349
1271
|
const upstreamBranchCheckResult = await $({ cwd: tempDir })`git show-ref --verify --quiet refs/remotes/upstream/${branchName}`;
|
|
@@ -1352,7 +1274,6 @@ export const checkoutPrBranch = async (tempDir, branchName, prForkRemote, prFork
|
|
|
1352
1274
|
await log(`${formatAligned('✅', 'Found branch in upstream:', `upstream/${branchName}`)}`);
|
|
1353
1275
|
// Try to checkout from upstream instead
|
|
1354
1276
|
checkoutResult = await $({ cwd: tempDir })`git checkout -b ${branchName} upstream/${branchName}`;
|
|
1355
|
-
|
|
1356
1277
|
if (checkoutResult.code === 0) {
|
|
1357
1278
|
await log(`${formatAligned('ℹ️', 'Note:', 'PR branch was in upstream repository, not your fork')}`);
|
|
1358
1279
|
await log(`${formatAligned('', '', 'This can happen when a bot creates a PR directly in the main repository')}`);
|
|
@@ -1372,14 +1293,12 @@ export const checkoutPrBranch = async (tempDir, branchName, prForkRemote, prFork
|
|
|
1372
1293
|
// See: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/checking-out-pull-requests-locally
|
|
1373
1294
|
if (checkoutResult.code !== 0 && prNumber) {
|
|
1374
1295
|
await log(`${formatAligned('🔄', 'Trying PR refs fallback:', `Fetching refs/pull/${prNumber}/head...`)}`);
|
|
1375
|
-
|
|
1376
1296
|
// Fetch the PR head using GitHub's special refs
|
|
1377
1297
|
const prRefFetchResult = await $({ cwd: tempDir })`git fetch origin pull/${prNumber}/head:${branchName}`;
|
|
1378
1298
|
|
|
1379
1299
|
if (prRefFetchResult.code === 0) {
|
|
1380
1300
|
await log(`${formatAligned('✅', 'Fetched PR ref:', `refs/pull/${prNumber}/head`)}`);
|
|
1381
1301
|
checkoutResult = await $({ cwd: tempDir })`git checkout ${branchName}`;
|
|
1382
|
-
|
|
1383
1302
|
if (checkoutResult.code === 0) {
|
|
1384
1303
|
await log(`${formatAligned('ℹ️', 'Note:', 'Checked out using GitHub PR refs (fork access not required)')}`);
|
|
1385
1304
|
await log(`${formatAligned('', '', 'This is a read-only checkout - you may need to push to a different branch')}`);
|
|
@@ -1395,12 +1314,10 @@ export const checkoutPrBranch = async (tempDir, branchName, prForkRemote, prFork
|
|
|
1395
1314
|
|
|
1396
1315
|
return checkoutResult;
|
|
1397
1316
|
};
|
|
1398
|
-
|
|
1399
1317
|
// Cleanup temporary directory
|
|
1400
1318
|
export const cleanupTempDirectory = async (tempDir, argv, limitReached) => {
|
|
1401
1319
|
// Determine if we should skip cleanup
|
|
1402
1320
|
const shouldKeepDirectory = !argv.autoCleanup || argv.resume || limitReached || (argv.autoResumeOnLimitReset && global.limitResetTime);
|
|
1403
|
-
|
|
1404
1321
|
if (!shouldKeepDirectory) {
|
|
1405
1322
|
try {
|
|
1406
1323
|
process.stdout.write('\n🧹 Cleaning up...');
|