@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,568 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Auto-continue module for solve command
|
|
4
|
+
// Extracted from solve.mjs to keep files under 1500 lines
|
|
5
|
+
|
|
6
|
+
// Use use-m to dynamically import modules for cross-runtime compatibility
|
|
7
|
+
// Check if use is already defined globally (when imported from solve.mjs)
|
|
8
|
+
// If not, fetch it (when running standalone)
|
|
9
|
+
if (typeof globalThis.use === 'undefined') {
|
|
10
|
+
globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
|
|
11
|
+
}
|
|
12
|
+
const use = globalThis.use;
|
|
13
|
+
|
|
14
|
+
// Use command-stream for consistent $ behavior across runtimes
|
|
15
|
+
const { $ } = await use('command-stream');
|
|
16
|
+
|
|
17
|
+
// Import shared library functions
|
|
18
|
+
const lib = await import('./lib.mjs');
|
|
19
|
+
const {
|
|
20
|
+
log,
|
|
21
|
+
cleanErrorMessage
|
|
22
|
+
} = lib;
|
|
23
|
+
|
|
24
|
+
// Import exit handler
|
|
25
|
+
import { safeExit } from './exit-handler.lib.mjs';
|
|
26
|
+
|
|
27
|
+
// Import branch name validation functions
|
|
28
|
+
const branchLib = await import('./solve.branch.lib.mjs');
|
|
29
|
+
const {
|
|
30
|
+
getIssueBranchPrefix,
|
|
31
|
+
matchesIssuePattern
|
|
32
|
+
} = branchLib;
|
|
33
|
+
|
|
34
|
+
// Import GitHub-related functions
|
|
35
|
+
const githubLib = await import('./github.lib.mjs');
|
|
36
|
+
const {
|
|
37
|
+
checkFileInBranch
|
|
38
|
+
} = githubLib;
|
|
39
|
+
|
|
40
|
+
// Import validation functions for time parsing
|
|
41
|
+
const validation = await import('./solve.validation.lib.mjs');
|
|
42
|
+
|
|
43
|
+
// Import Sentry integration
|
|
44
|
+
const sentryLib = await import('./sentry.lib.mjs');
|
|
45
|
+
const { reportError } = sentryLib;
|
|
46
|
+
|
|
47
|
+
// Import GitHub linking detection library
|
|
48
|
+
const githubLinking = await import('./github-linking.lib.mjs');
|
|
49
|
+
const { extractLinkedIssueNumber } = githubLinking;
|
|
50
|
+
|
|
51
|
+
// Import configuration
|
|
52
|
+
import { autoContinue } from './config.lib.mjs';
|
|
53
|
+
|
|
54
|
+
const {
|
|
55
|
+
calculateWaitTime
|
|
56
|
+
} = validation;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Format time duration in days:hours:minutes:seconds
|
|
60
|
+
* @param {number} ms - Milliseconds
|
|
61
|
+
* @returns {string} - Formatted time string (e.g., "0:02:15:30")
|
|
62
|
+
*/
|
|
63
|
+
const formatWaitTime = (ms) => {
|
|
64
|
+
const seconds = Math.floor(ms / 1000);
|
|
65
|
+
const minutes = Math.floor(seconds / 60);
|
|
66
|
+
const hours = Math.floor(minutes / 60);
|
|
67
|
+
const days = Math.floor(hours / 24);
|
|
68
|
+
|
|
69
|
+
const s = seconds % 60;
|
|
70
|
+
const m = minutes % 60;
|
|
71
|
+
const h = hours % 24;
|
|
72
|
+
|
|
73
|
+
return `${days}:${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Auto-continue function that waits until limit resets
|
|
77
|
+
export const autoContinueWhenLimitResets = async (issueUrl, sessionId, argv, shouldAttachLogs) => {
|
|
78
|
+
try {
|
|
79
|
+
const resetTime = global.limitResetTime;
|
|
80
|
+
const waitMs = calculateWaitTime(resetTime);
|
|
81
|
+
|
|
82
|
+
await log(`\n⏰ Waiting until ${resetTime} for limit to reset...`);
|
|
83
|
+
await log(` Wait time: ${formatWaitTime(waitMs)}`);
|
|
84
|
+
await log(` Current time: ${new Date().toLocaleTimeString()}`);
|
|
85
|
+
|
|
86
|
+
// Show countdown every 30 minutes for long waits, every minute for short waits
|
|
87
|
+
const countdownInterval = waitMs > 30 * 60 * 1000 ? 30 * 60 * 1000 : 60 * 1000;
|
|
88
|
+
let remainingMs = waitMs;
|
|
89
|
+
|
|
90
|
+
const countdownTimer = setInterval(async () => {
|
|
91
|
+
remainingMs -= countdownInterval;
|
|
92
|
+
if (remainingMs > 0) {
|
|
93
|
+
await log(`⏳ Time remaining: ${formatWaitTime(remainingMs)} until ${resetTime}`);
|
|
94
|
+
}
|
|
95
|
+
}, countdownInterval);
|
|
96
|
+
|
|
97
|
+
// Wait until reset time
|
|
98
|
+
await new Promise(resolve => setTimeout(resolve, waitMs));
|
|
99
|
+
clearInterval(countdownTimer);
|
|
100
|
+
|
|
101
|
+
await log('\n✅ Limit reset time reached! Resuming session...');
|
|
102
|
+
await log(` Current time: ${new Date().toLocaleTimeString()}`);
|
|
103
|
+
|
|
104
|
+
// Recursively call the solve script with --resume
|
|
105
|
+
// We need to reconstruct the command with appropriate flags
|
|
106
|
+
const childProcess = await import('child_process');
|
|
107
|
+
|
|
108
|
+
// Build the resume command
|
|
109
|
+
const resumeArgs = [
|
|
110
|
+
process.argv[1], // solve.mjs path
|
|
111
|
+
issueUrl,
|
|
112
|
+
'--resume', sessionId
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
// Preserve auto-continue flag
|
|
116
|
+
if (argv.autoContinueOnLimitReset) {
|
|
117
|
+
resumeArgs.push('--auto-continue-on-limit-reset');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Preserve other flags from original invocation
|
|
121
|
+
if (argv.model !== 'sonnet') resumeArgs.push('--model', argv.model);
|
|
122
|
+
if (argv.verbose) resumeArgs.push('--verbose');
|
|
123
|
+
if (argv.fork) resumeArgs.push('--fork');
|
|
124
|
+
if (shouldAttachLogs) resumeArgs.push('--attach-logs');
|
|
125
|
+
|
|
126
|
+
await log(`\n🔄 Executing: ${resumeArgs.join(' ')}`);
|
|
127
|
+
|
|
128
|
+
// Execute the resume command
|
|
129
|
+
const child = childProcess.spawn('node', resumeArgs, {
|
|
130
|
+
stdio: 'inherit',
|
|
131
|
+
cwd: process.cwd(),
|
|
132
|
+
env: process.env
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
child.on('close', (code) => {
|
|
136
|
+
process.exit(code);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
} catch (error) {
|
|
140
|
+
reportError(error, {
|
|
141
|
+
context: 'auto_continue_with_command',
|
|
142
|
+
issueUrl,
|
|
143
|
+
sessionId,
|
|
144
|
+
operation: 'auto_continue_execution'
|
|
145
|
+
});
|
|
146
|
+
await log(`\n❌ Auto-continue failed: ${cleanErrorMessage(error)}`, { level: 'error' });
|
|
147
|
+
await log('\n🔄 Manual resume command:');
|
|
148
|
+
await log(`./solve.mjs "${issueUrl}" --resume ${sessionId}`);
|
|
149
|
+
await safeExit(1, 'Auto-continue failed');
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// Auto-continue logic: check for existing PRs if --auto-continue is enabled
|
|
154
|
+
export const checkExistingPRsForAutoContinue = async (argv, isIssueUrl, owner, repo, urlNumber) => {
|
|
155
|
+
let isContinueMode = false;
|
|
156
|
+
let prNumber = null;
|
|
157
|
+
let prBranch = null;
|
|
158
|
+
let issueNumber = null;
|
|
159
|
+
|
|
160
|
+
if (argv.autoContinue && isIssueUrl) {
|
|
161
|
+
issueNumber = urlNumber;
|
|
162
|
+
await log(`🔍 Auto-continue enabled: Checking for existing PRs for issue #${issueNumber}...`);
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
// Get all PRs linked to this issue
|
|
166
|
+
const prListResult = await $`gh pr list --repo ${owner}/${repo} --search "linked:issue-${issueNumber}" --json number,createdAt,headRefName,isDraft,state --limit 10`;
|
|
167
|
+
|
|
168
|
+
if (prListResult.code === 0) {
|
|
169
|
+
const prs = JSON.parse(prListResult.stdout.toString().trim() || '[]');
|
|
170
|
+
|
|
171
|
+
if (prs.length > 0) {
|
|
172
|
+
await log(`📋 Found ${prs.length} existing PR(s) linked to issue #${issueNumber}`);
|
|
173
|
+
|
|
174
|
+
// Find PRs that are older than 24 hours
|
|
175
|
+
const now = new Date();
|
|
176
|
+
const twentyFourHoursAgo = new Date(now.getTime() - autoContinue.ageThresholdHours * 60 * 60 * 1000);
|
|
177
|
+
|
|
178
|
+
for (const pr of prs) {
|
|
179
|
+
const createdAt = new Date(pr.createdAt);
|
|
180
|
+
const ageHours = Math.floor((now - createdAt) / (1000 * 60 * 60));
|
|
181
|
+
|
|
182
|
+
await log(` PR #${pr.number}: created ${ageHours}h ago (${pr.state}, ${pr.isDraft ? 'draft' : 'ready'})`);
|
|
183
|
+
|
|
184
|
+
// Check if PR is open (not closed)
|
|
185
|
+
if (pr.state === 'OPEN') {
|
|
186
|
+
// CRITICAL: Validate that branch name matches the expected pattern for this issue
|
|
187
|
+
// Branch naming convention: issue-{issueNumber}-{randomHash} (supports both 8-char legacy and 12-char new formats)
|
|
188
|
+
if (!matchesIssuePattern(pr.headRefName, issueNumber)) {
|
|
189
|
+
const expectedBranchPrefix = getIssueBranchPrefix(issueNumber);
|
|
190
|
+
await log(` PR #${pr.number}: Branch '${pr.headRefName}' doesn't match expected pattern '${expectedBranchPrefix}*' - skipping`);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Check if CLAUDE.md exists in this PR branch
|
|
195
|
+
const claudeMdExists = await checkFileInBranch(owner, repo, 'CLAUDE.md', pr.headRefName);
|
|
196
|
+
|
|
197
|
+
if (!claudeMdExists) {
|
|
198
|
+
await log(`✅ Auto-continue: Using PR #${pr.number} (CLAUDE.md missing - work completed, branch: ${pr.headRefName})`);
|
|
199
|
+
|
|
200
|
+
// Switch to continue mode immediately (don't wait 24 hours if CLAUDE.md is missing)
|
|
201
|
+
isContinueMode = true;
|
|
202
|
+
prNumber = pr.number;
|
|
203
|
+
prBranch = pr.headRefName;
|
|
204
|
+
if (argv.verbose) {
|
|
205
|
+
await log(' Continue mode activated: Auto-continue (CLAUDE.md missing)', { verbose: true });
|
|
206
|
+
await log(` PR Number: ${prNumber}`, { verbose: true });
|
|
207
|
+
await log(` PR Branch: ${prBranch}`, { verbose: true });
|
|
208
|
+
}
|
|
209
|
+
break;
|
|
210
|
+
} else if (createdAt < twentyFourHoursAgo) {
|
|
211
|
+
await log(`✅ Auto-continue: Using PR #${pr.number} (created ${ageHours}h ago, branch: ${pr.headRefName})`);
|
|
212
|
+
|
|
213
|
+
// Switch to continue mode
|
|
214
|
+
isContinueMode = true;
|
|
215
|
+
prNumber = pr.number;
|
|
216
|
+
prBranch = pr.headRefName;
|
|
217
|
+
if (argv.verbose) {
|
|
218
|
+
await log(' Continue mode activated: Auto-continue (24h+ old PR)', { verbose: true });
|
|
219
|
+
await log(` PR Number: ${prNumber}`, { verbose: true });
|
|
220
|
+
await log(` PR Branch: ${prBranch}`, { verbose: true });
|
|
221
|
+
await log(` PR Age: ${ageHours} hours`, { verbose: true });
|
|
222
|
+
}
|
|
223
|
+
break;
|
|
224
|
+
} else {
|
|
225
|
+
await log(` PR #${pr.number}: CLAUDE.md exists, age ${ageHours}h < 24h - skipping`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (!isContinueMode) {
|
|
231
|
+
await log('⏭️ No suitable PRs found (missing CLAUDE.md or older than 24h) - creating new PR as usual');
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
await log(`📝 No existing PRs found for issue #${issueNumber} - creating new PR`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
} catch (prSearchError) {
|
|
238
|
+
reportError(prSearchError, {
|
|
239
|
+
context: 'check_existing_pr_for_issue',
|
|
240
|
+
owner,
|
|
241
|
+
repo,
|
|
242
|
+
issueNumber,
|
|
243
|
+
operation: 'search_issue_prs'
|
|
244
|
+
});
|
|
245
|
+
await log(`⚠️ Warning: Could not search for existing PRs: ${prSearchError.message}`, { level: 'warning' });
|
|
246
|
+
await log(' Continuing with normal flow...');
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return { isContinueMode, prNumber, prBranch, issueNumber };
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// Process PR URL mode and extract issue information
|
|
254
|
+
export const processPRMode = async (isPrUrl, urlNumber, owner, repo, argv) => {
|
|
255
|
+
let isContinueMode = false;
|
|
256
|
+
let prNumber = null;
|
|
257
|
+
let prBranch = null;
|
|
258
|
+
let issueNumber = null;
|
|
259
|
+
let mergeStateStatus = null;
|
|
260
|
+
let isForkPR = false;
|
|
261
|
+
|
|
262
|
+
if (isPrUrl) {
|
|
263
|
+
isContinueMode = true;
|
|
264
|
+
prNumber = urlNumber;
|
|
265
|
+
|
|
266
|
+
await log(`🔄 Continue mode: Working with PR #${prNumber}`);
|
|
267
|
+
if (argv.verbose) {
|
|
268
|
+
await log(' Continue mode activated: PR URL provided directly', { verbose: true });
|
|
269
|
+
await log(` PR Number set to: ${prNumber}`, { verbose: true });
|
|
270
|
+
await log(' Will fetch PR details and linked issue', { verbose: true });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Get PR details to find the linked issue and branch
|
|
274
|
+
try {
|
|
275
|
+
const prResult = await githubLib.ghPrView({
|
|
276
|
+
prNumber,
|
|
277
|
+
owner,
|
|
278
|
+
repo,
|
|
279
|
+
jsonFields: 'headRefName,body,number,mergeStateStatus,headRepositoryOwner'
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
if (prResult.code !== 0 || !prResult.data) {
|
|
283
|
+
await log('Error: Failed to get PR details', { level: 'error' });
|
|
284
|
+
|
|
285
|
+
if (prResult.output.includes('Could not resolve to a PullRequest')) {
|
|
286
|
+
await githubLib.handlePRNotFoundError({ prNumber, owner, repo, argv, shouldAttachLogs: argv.attachLogs || argv['attach-logs'] });
|
|
287
|
+
} else {
|
|
288
|
+
await log(`Error: ${prResult.stderr || 'Unknown error'}`, { level: 'error' });
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
await safeExit(1, 'Auto-continue failed');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const prData = prResult.data;
|
|
295
|
+
prBranch = prData.headRefName;
|
|
296
|
+
mergeStateStatus = prData.mergeStateStatus;
|
|
297
|
+
|
|
298
|
+
// Check if this is a fork PR
|
|
299
|
+
isForkPR = prData.headRepositoryOwner && prData.headRepositoryOwner.login !== owner;
|
|
300
|
+
|
|
301
|
+
await log(`📝 PR branch: ${prBranch}`);
|
|
302
|
+
|
|
303
|
+
// Extract issue number from PR body using GitHub linking detection library
|
|
304
|
+
// This ensures we only detect actual GitHub-recognized linking keywords
|
|
305
|
+
const prBody = prData.body || '';
|
|
306
|
+
const extractedIssueNumber = extractLinkedIssueNumber(prBody);
|
|
307
|
+
|
|
308
|
+
if (extractedIssueNumber) {
|
|
309
|
+
issueNumber = extractedIssueNumber;
|
|
310
|
+
await log(`🔗 Found linked issue #${issueNumber}`);
|
|
311
|
+
} else {
|
|
312
|
+
// If no linked issue found, we can still continue but warn
|
|
313
|
+
await log('⚠️ Warning: No linked issue found in PR body', { level: 'warning' });
|
|
314
|
+
await log(' The PR should contain "Fixes #123" or similar to link an issue', { level: 'warning' });
|
|
315
|
+
// Set issueNumber to PR number as fallback
|
|
316
|
+
issueNumber = prNumber;
|
|
317
|
+
}
|
|
318
|
+
} catch (error) {
|
|
319
|
+
reportError(error, {
|
|
320
|
+
context: 'process_pr_in_auto_continue',
|
|
321
|
+
prNumber,
|
|
322
|
+
operation: 'process_pr_for_continuation'
|
|
323
|
+
});
|
|
324
|
+
await log(`Error: Failed to process PR: ${cleanErrorMessage(error)}`, { level: 'error' });
|
|
325
|
+
await safeExit(1, 'Auto-continue failed');
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return { isContinueMode, prNumber, prBranch, issueNumber, mergeStateStatus, isForkPR };
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
// Process auto-continue logic for issue URLs
|
|
333
|
+
export const processAutoContinueForIssue = async (argv, isIssueUrl, urlNumber, owner, repo) => {
|
|
334
|
+
if (!argv.autoContinue || !isIssueUrl) {
|
|
335
|
+
return { isContinueMode: false };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const issueNumber = urlNumber;
|
|
339
|
+
await log(`🔍 Auto-continue enabled: Checking for existing PRs for issue #${issueNumber}...`);
|
|
340
|
+
|
|
341
|
+
// Check for existing branches in the repository (main repo or fork)
|
|
342
|
+
let existingBranches = [];
|
|
343
|
+
|
|
344
|
+
if (argv.fork) {
|
|
345
|
+
// When in fork mode, check for existing branches in the fork
|
|
346
|
+
try {
|
|
347
|
+
// Get current user to determine fork name
|
|
348
|
+
const userResult = await $`gh api user --jq .login`;
|
|
349
|
+
if (userResult.code === 0) {
|
|
350
|
+
const currentUser = userResult.stdout.toString().trim();
|
|
351
|
+
// Determine fork name based on --prefix-fork-name-with-owner-name option
|
|
352
|
+
const forkRepoName = argv.prefixForkNameWithOwnerName ? `${owner}-${repo}` : repo;
|
|
353
|
+
const forkRepo = `${currentUser}/${forkRepoName}`;
|
|
354
|
+
|
|
355
|
+
// Check if fork exists
|
|
356
|
+
const forkCheckResult = await $`gh repo view ${forkRepo} --json name 2>/dev/null`;
|
|
357
|
+
if (forkCheckResult.code === 0) {
|
|
358
|
+
await log(`🔍 Fork mode: Checking for existing branches in ${forkRepo}...`);
|
|
359
|
+
|
|
360
|
+
// List all branches in the fork that match the pattern issue-{issueNumber}-* (supports both 8-char and 12-char formats)
|
|
361
|
+
const branchPattern = getIssueBranchPrefix(issueNumber);
|
|
362
|
+
const branchListResult = await $`gh api --paginate repos/${forkRepo}/branches --jq '.[].name'`;
|
|
363
|
+
|
|
364
|
+
if (branchListResult.code === 0) {
|
|
365
|
+
const allBranches = branchListResult.stdout.toString().trim().split('\n').filter(b => b);
|
|
366
|
+
existingBranches = allBranches.filter(branch => matchesIssuePattern(branch, issueNumber));
|
|
367
|
+
|
|
368
|
+
if (existingBranches.length > 0) {
|
|
369
|
+
await log(`📋 Found ${existingBranches.length} existing branch(es) in fork matching pattern '${branchPattern}*':`);
|
|
370
|
+
for (const branch of existingBranches) {
|
|
371
|
+
await log(` • ${branch}`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
} catch (forkBranchError) {
|
|
378
|
+
reportError(forkBranchError, {
|
|
379
|
+
context: 'check_fork_branches',
|
|
380
|
+
owner,
|
|
381
|
+
repo,
|
|
382
|
+
issueNumber,
|
|
383
|
+
operation: 'search_fork_branches'
|
|
384
|
+
});
|
|
385
|
+
await log(`⚠️ Warning: Could not check for existing branches in fork: ${forkBranchError.message}`, { level: 'warning' });
|
|
386
|
+
}
|
|
387
|
+
} else {
|
|
388
|
+
// NOT in fork mode - check for existing branches in the main repository
|
|
389
|
+
try {
|
|
390
|
+
await log(`🔍 Checking for existing branches in ${owner}/${repo}...`);
|
|
391
|
+
|
|
392
|
+
// List all branches in the main repo that match the pattern issue-{issueNumber}-* (supports both 8-char and 12-char formats)
|
|
393
|
+
const branchPattern = getIssueBranchPrefix(issueNumber);
|
|
394
|
+
const branchListResult = await $`gh api --paginate repos/${owner}/${repo}/branches --jq '.[].name'`;
|
|
395
|
+
|
|
396
|
+
if (branchListResult.code === 0) {
|
|
397
|
+
const allBranches = branchListResult.stdout.toString().trim().split('\n').filter(b => b);
|
|
398
|
+
existingBranches = allBranches.filter(branch => matchesIssuePattern(branch, issueNumber));
|
|
399
|
+
|
|
400
|
+
if (existingBranches.length > 0) {
|
|
401
|
+
await log(`📋 Found ${existingBranches.length} existing branch(es) in main repo matching pattern '${branchPattern}*':`);
|
|
402
|
+
for (const branch of existingBranches) {
|
|
403
|
+
await log(` • ${branch}`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
} catch (mainBranchError) {
|
|
408
|
+
reportError(mainBranchError, {
|
|
409
|
+
context: 'check_main_repo_branches',
|
|
410
|
+
owner,
|
|
411
|
+
repo,
|
|
412
|
+
issueNumber,
|
|
413
|
+
operation: 'search_main_repo_branches'
|
|
414
|
+
});
|
|
415
|
+
await log(`⚠️ Warning: Could not check for existing branches in main repo: ${mainBranchError.message}`, { level: 'warning' });
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
try {
|
|
420
|
+
// Get all PRs linked to this issue
|
|
421
|
+
const prListResult = await $`gh pr list --repo ${owner}/${repo} --search "linked:issue-${issueNumber}" --json number,createdAt,headRefName,isDraft,state --limit 10`;
|
|
422
|
+
|
|
423
|
+
if (prListResult.code === 0) {
|
|
424
|
+
const prs = JSON.parse(prListResult.stdout.toString().trim() || '[]');
|
|
425
|
+
|
|
426
|
+
if (prs.length > 0) {
|
|
427
|
+
await log(`📋 Found ${prs.length} existing PR(s) linked to issue #${issueNumber}`);
|
|
428
|
+
|
|
429
|
+
// Find PRs that are older than 24 hours
|
|
430
|
+
const now = new Date();
|
|
431
|
+
const twentyFourHoursAgo = new Date(now.getTime() - autoContinue.ageThresholdHours * 60 * 60 * 1000);
|
|
432
|
+
|
|
433
|
+
for (const pr of prs) {
|
|
434
|
+
const createdAt = new Date(pr.createdAt);
|
|
435
|
+
const ageHours = Math.floor((now - createdAt) / (1000 * 60 * 60));
|
|
436
|
+
|
|
437
|
+
await log(` PR #${pr.number}: created ${ageHours}h ago (${pr.state}, ${pr.isDraft ? 'draft' : 'ready'})`);
|
|
438
|
+
|
|
439
|
+
// Check if PR is open (not closed)
|
|
440
|
+
if (pr.state === 'OPEN') {
|
|
441
|
+
// CRITICAL: Validate that branch name matches the expected pattern for this issue
|
|
442
|
+
// Branch naming convention: issue-{issueNumber}-{randomHash} (supports both 8-char legacy and 12-char new formats)
|
|
443
|
+
if (!matchesIssuePattern(pr.headRefName, issueNumber)) {
|
|
444
|
+
const expectedBranchPrefix = getIssueBranchPrefix(issueNumber);
|
|
445
|
+
await log(` PR #${pr.number}: Branch '${pr.headRefName}' doesn't match expected pattern '${expectedBranchPrefix}*' - skipping`);
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Check if CLAUDE.md exists in this PR branch
|
|
450
|
+
const claudeMdExists = await checkFileInBranch(owner, repo, 'CLAUDE.md', pr.headRefName);
|
|
451
|
+
|
|
452
|
+
if (!claudeMdExists) {
|
|
453
|
+
await log(`✅ Auto-continue: Using PR #${pr.number} (CLAUDE.md missing - work completed, branch: ${pr.headRefName})`);
|
|
454
|
+
|
|
455
|
+
// Switch to continue mode immediately (don't wait 24 hours if CLAUDE.md is missing)
|
|
456
|
+
if (argv.verbose) {
|
|
457
|
+
await log(' Continue mode activated: Auto-continue (CLAUDE.md missing)', { verbose: true });
|
|
458
|
+
await log(` PR Number: ${pr.number}`, { verbose: true });
|
|
459
|
+
await log(` PR Branch: ${pr.headRefName}`, { verbose: true });
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return {
|
|
463
|
+
isContinueMode: true,
|
|
464
|
+
prNumber: pr.number,
|
|
465
|
+
prBranch: pr.headRefName,
|
|
466
|
+
issueNumber
|
|
467
|
+
};
|
|
468
|
+
} else if (createdAt < twentyFourHoursAgo) {
|
|
469
|
+
await log(`✅ Auto-continue: Using PR #${pr.number} (created ${ageHours}h ago, branch: ${pr.headRefName})`);
|
|
470
|
+
|
|
471
|
+
if (argv.verbose) {
|
|
472
|
+
await log(' Continue mode activated: Auto-continue (24h+ old PR)', { verbose: true });
|
|
473
|
+
await log(` PR Number: ${pr.number}`, { verbose: true });
|
|
474
|
+
await log(` PR Branch: ${pr.headRefName}`, { verbose: true });
|
|
475
|
+
await log(` PR Age: ${ageHours} hours`, { verbose: true });
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
isContinueMode: true,
|
|
480
|
+
prNumber: pr.number,
|
|
481
|
+
prBranch: pr.headRefName,
|
|
482
|
+
issueNumber
|
|
483
|
+
};
|
|
484
|
+
} else {
|
|
485
|
+
await log(` PR #${pr.number}: CLAUDE.md exists, age ${ageHours}h < 24h - skipping`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
await log('⏭️ No suitable PRs found (missing CLAUDE.md or older than 24h) - creating new PR as usual');
|
|
491
|
+
} else {
|
|
492
|
+
await log(`📝 No existing PRs found for issue #${issueNumber} - creating new PR`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
} catch (prSearchError) {
|
|
496
|
+
reportError(prSearchError, {
|
|
497
|
+
context: 'check_existing_pr_with_claude',
|
|
498
|
+
owner,
|
|
499
|
+
repo,
|
|
500
|
+
issueNumber,
|
|
501
|
+
operation: 'search_pr_with_claude_md'
|
|
502
|
+
});
|
|
503
|
+
await log(`⚠️ Warning: Could not search for existing PRs: ${prSearchError.message}`, { level: 'warning' });
|
|
504
|
+
await log(' Continuing with normal flow...');
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// If no suitable PR was found but we have existing branches, use the first one
|
|
508
|
+
if (existingBranches.length > 0) {
|
|
509
|
+
// Sort branches by name (newest hash suffix last) and use the most recent one
|
|
510
|
+
const sortedBranches = existingBranches.sort();
|
|
511
|
+
const selectedBranch = sortedBranches[sortedBranches.length - 1];
|
|
512
|
+
|
|
513
|
+
const repoType = argv.fork ? 'fork' : 'main repo';
|
|
514
|
+
await log(`✅ Using existing branch from ${repoType}: ${selectedBranch}`);
|
|
515
|
+
await log(` Found ${existingBranches.length} matching branch(es), selected most recent`);
|
|
516
|
+
|
|
517
|
+
// Check if there's a PR for this branch (including merged/closed PRs)
|
|
518
|
+
try {
|
|
519
|
+
const prForBranchResult = await $`gh pr list --repo ${owner}/${repo} --head ${selectedBranch} --state all --json number,state --limit 10`;
|
|
520
|
+
if (prForBranchResult.code === 0) {
|
|
521
|
+
const prsForBranch = JSON.parse(prForBranchResult.stdout.toString().trim() || '[]');
|
|
522
|
+
if (prsForBranch.length > 0) {
|
|
523
|
+
// Check if any PR is MERGED or CLOSED
|
|
524
|
+
const mergedOrClosedPr = prsForBranch.find(pr => pr.state === 'MERGED' || pr.state === 'CLOSED');
|
|
525
|
+
if (mergedOrClosedPr) {
|
|
526
|
+
await log(` Branch ${selectedBranch} has a ${mergedOrClosedPr.state} PR #${mergedOrClosedPr.number} - cannot reuse`);
|
|
527
|
+
await log(` Will create a new branch for issue #${issueNumber}`);
|
|
528
|
+
return { isContinueMode: false, issueNumber };
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// All PRs are OPEN - find the first open PR
|
|
532
|
+
const openPr = prsForBranch.find(pr => pr.state === 'OPEN');
|
|
533
|
+
if (openPr) {
|
|
534
|
+
await log(` Existing open PR found: #${openPr.number}`);
|
|
535
|
+
return {
|
|
536
|
+
isContinueMode: true,
|
|
537
|
+
prNumber: openPr.number,
|
|
538
|
+
prBranch: selectedBranch,
|
|
539
|
+
issueNumber
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
} catch (prCheckError) {
|
|
545
|
+
reportError(prCheckError, {
|
|
546
|
+
context: 'check_pr_for_existing_branch',
|
|
547
|
+
owner,
|
|
548
|
+
repo,
|
|
549
|
+
selectedBranch,
|
|
550
|
+
operation: 'search_pr_for_branch'
|
|
551
|
+
});
|
|
552
|
+
// If we can't check for PR, still continue with the branch
|
|
553
|
+
await log(`⚠️ Warning: Could not check for existing PR for branch: ${prCheckError.message}`, { level: 'warning' });
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// No PR exists yet for this branch, but we can still use the branch
|
|
557
|
+
await log(' No existing PR for this branch - will create PR from existing branch');
|
|
558
|
+
|
|
559
|
+
return {
|
|
560
|
+
isContinueMode: true,
|
|
561
|
+
prNumber: null, // No PR yet
|
|
562
|
+
prBranch: selectedBranch,
|
|
563
|
+
issueNumber
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
return { isContinueMode: false, issueNumber };
|
|
568
|
+
};
|