@link-assistant/hive-mind 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/LICENSE +24 -0
  3. package/README.md +769 -0
  4. package/package.json +58 -0
  5. package/src/agent.lib.mjs +705 -0
  6. package/src/agent.prompts.lib.mjs +196 -0
  7. package/src/buildUserMention.lib.mjs +71 -0
  8. package/src/claude-limits.lib.mjs +389 -0
  9. package/src/claude.lib.mjs +1445 -0
  10. package/src/claude.prompts.lib.mjs +203 -0
  11. package/src/codex.lib.mjs +552 -0
  12. package/src/codex.prompts.lib.mjs +194 -0
  13. package/src/config.lib.mjs +207 -0
  14. package/src/contributing-guidelines.lib.mjs +268 -0
  15. package/src/exit-handler.lib.mjs +205 -0
  16. package/src/git.lib.mjs +145 -0
  17. package/src/github-issue-creator.lib.mjs +246 -0
  18. package/src/github-linking.lib.mjs +152 -0
  19. package/src/github.batch.lib.mjs +272 -0
  20. package/src/github.graphql.lib.mjs +258 -0
  21. package/src/github.lib.mjs +1479 -0
  22. package/src/hive.config.lib.mjs +254 -0
  23. package/src/hive.mjs +1500 -0
  24. package/src/instrument.mjs +191 -0
  25. package/src/interactive-mode.lib.mjs +1000 -0
  26. package/src/lenv-reader.lib.mjs +206 -0
  27. package/src/lib.mjs +490 -0
  28. package/src/lino.lib.mjs +176 -0
  29. package/src/local-ci-checks.lib.mjs +324 -0
  30. package/src/memory-check.mjs +419 -0
  31. package/src/model-mapping.lib.mjs +145 -0
  32. package/src/model-validation.lib.mjs +278 -0
  33. package/src/opencode.lib.mjs +479 -0
  34. package/src/opencode.prompts.lib.mjs +194 -0
  35. package/src/protect-branch.mjs +159 -0
  36. package/src/review.mjs +433 -0
  37. package/src/reviewers-hive.mjs +643 -0
  38. package/src/sentry.lib.mjs +284 -0
  39. package/src/solve.auto-continue.lib.mjs +568 -0
  40. package/src/solve.auto-pr.lib.mjs +1374 -0
  41. package/src/solve.branch-errors.lib.mjs +341 -0
  42. package/src/solve.branch.lib.mjs +230 -0
  43. package/src/solve.config.lib.mjs +342 -0
  44. package/src/solve.error-handlers.lib.mjs +256 -0
  45. package/src/solve.execution.lib.mjs +291 -0
  46. package/src/solve.feedback.lib.mjs +436 -0
  47. package/src/solve.mjs +1128 -0
  48. package/src/solve.preparation.lib.mjs +210 -0
  49. package/src/solve.repo-setup.lib.mjs +114 -0
  50. package/src/solve.repository.lib.mjs +961 -0
  51. package/src/solve.results.lib.mjs +558 -0
  52. package/src/solve.session.lib.mjs +135 -0
  53. package/src/solve.validation.lib.mjs +325 -0
  54. package/src/solve.watch.lib.mjs +572 -0
  55. package/src/start-screen.mjs +324 -0
  56. package/src/task.mjs +308 -0
  57. package/src/telegram-bot.mjs +1481 -0
  58. package/src/telegram-markdown.lib.mjs +64 -0
  59. package/src/usage-limit.lib.mjs +218 -0
  60. package/src/version.lib.mjs +41 -0
  61. package/src/youtrack/solve.youtrack.lib.mjs +116 -0
  62. package/src/youtrack/youtrack-sync.mjs +219 -0
  63. package/src/youtrack/youtrack.lib.mjs +425 -0
@@ -0,0 +1,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
+ };