@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.
- package/CHANGELOG.md +20 -0
- package/LICENSE +24 -0
- package/README.md +769 -0
- package/package.json +58 -0
- package/src/agent.lib.mjs +705 -0
- package/src/agent.prompts.lib.mjs +196 -0
- package/src/buildUserMention.lib.mjs +71 -0
- package/src/claude-limits.lib.mjs +389 -0
- package/src/claude.lib.mjs +1445 -0
- package/src/claude.prompts.lib.mjs +203 -0
- package/src/codex.lib.mjs +552 -0
- package/src/codex.prompts.lib.mjs +194 -0
- package/src/config.lib.mjs +207 -0
- package/src/contributing-guidelines.lib.mjs +268 -0
- package/src/exit-handler.lib.mjs +205 -0
- package/src/git.lib.mjs +145 -0
- package/src/github-issue-creator.lib.mjs +246 -0
- package/src/github-linking.lib.mjs +152 -0
- package/src/github.batch.lib.mjs +272 -0
- package/src/github.graphql.lib.mjs +258 -0
- package/src/github.lib.mjs +1479 -0
- package/src/hive.config.lib.mjs +254 -0
- package/src/hive.mjs +1500 -0
- package/src/instrument.mjs +191 -0
- package/src/interactive-mode.lib.mjs +1000 -0
- package/src/lenv-reader.lib.mjs +206 -0
- package/src/lib.mjs +490 -0
- package/src/lino.lib.mjs +176 -0
- package/src/local-ci-checks.lib.mjs +324 -0
- package/src/memory-check.mjs +419 -0
- package/src/model-mapping.lib.mjs +145 -0
- package/src/model-validation.lib.mjs +278 -0
- package/src/opencode.lib.mjs +479 -0
- package/src/opencode.prompts.lib.mjs +194 -0
- package/src/protect-branch.mjs +159 -0
- package/src/review.mjs +433 -0
- package/src/reviewers-hive.mjs +643 -0
- package/src/sentry.lib.mjs +284 -0
- package/src/solve.auto-continue.lib.mjs +568 -0
- package/src/solve.auto-pr.lib.mjs +1374 -0
- package/src/solve.branch-errors.lib.mjs +341 -0
- package/src/solve.branch.lib.mjs +230 -0
- package/src/solve.config.lib.mjs +342 -0
- package/src/solve.error-handlers.lib.mjs +256 -0
- package/src/solve.execution.lib.mjs +291 -0
- package/src/solve.feedback.lib.mjs +436 -0
- package/src/solve.mjs +1128 -0
- package/src/solve.preparation.lib.mjs +210 -0
- package/src/solve.repo-setup.lib.mjs +114 -0
- package/src/solve.repository.lib.mjs +961 -0
- package/src/solve.results.lib.mjs +558 -0
- package/src/solve.session.lib.mjs +135 -0
- package/src/solve.validation.lib.mjs +325 -0
- package/src/solve.watch.lib.mjs +572 -0
- package/src/start-screen.mjs +324 -0
- package/src/task.mjs +308 -0
- package/src/telegram-bot.mjs +1481 -0
- package/src/telegram-markdown.lib.mjs +64 -0
- package/src/usage-limit.lib.mjs +218 -0
- package/src/version.lib.mjs +41 -0
- package/src/youtrack/solve.youtrack.lib.mjs +116 -0
- package/src/youtrack/youtrack-sync.mjs +219 -0
- 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
|
+
}
|