@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,558 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Results processing module for solve command
4
+ // Extracted from solve.mjs to keep files under 1500 lines
5
+
6
+ // Use use-m to dynamically import modules for cross-runtime compatibility
7
+ // Check if use is already defined globally (when imported from solve.mjs)
8
+ // If not, fetch it (when running standalone)
9
+ if (typeof globalThis.use === 'undefined') {
10
+ globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
11
+ }
12
+ const use = globalThis.use;
13
+
14
+ // Use command-stream for consistent $ behavior across runtimes
15
+ const { $ } = await use('command-stream');
16
+
17
+ const path = (await use('path')).default;
18
+
19
+ // Import shared library functions
20
+ const lib = await import('./lib.mjs');
21
+ const {
22
+ log,
23
+ getLogFile,
24
+ formatAligned
25
+ } = lib;
26
+
27
+ // Import exit handler
28
+ import { safeExit } from './exit-handler.lib.mjs';
29
+
30
+ // Import GitHub-related functions
31
+ const githubLib = await import('./github.lib.mjs');
32
+ const {
33
+ sanitizeLogContent,
34
+ attachLogToGitHub
35
+ } = githubLib;
36
+
37
+ // Import auto-continue functions
38
+ const autoContinue = await import('./solve.auto-continue.lib.mjs');
39
+ const {
40
+ autoContinueWhenLimitResets
41
+ } = autoContinue;
42
+
43
+ // Import error handling functions
44
+ // const errorHandlers = await import('./solve.error-handlers.lib.mjs'); // Not currently used
45
+ // Import Sentry integration
46
+ const sentryLib = await import('./sentry.lib.mjs');
47
+ const { reportError } = sentryLib;
48
+
49
+ // Import GitHub linking detection library
50
+ const githubLinking = await import('./github-linking.lib.mjs');
51
+ const { hasGitHubLinkingKeyword } = githubLinking;
52
+
53
+ // Revert the CLAUDE.md commit to restore original state
54
+ export const cleanupClaudeFile = async (tempDir, branchName, claudeCommitHash = null) => {
55
+ try {
56
+ // Only revert if we have the commit hash from this session
57
+ // This prevents reverting the wrong commit in continue mode
58
+ if (!claudeCommitHash) {
59
+ await log(' No CLAUDE.md commit to revert (not created in this session)', { verbose: true });
60
+ return;
61
+ }
62
+
63
+ await log(formatAligned('šŸ”„', 'Cleanup:', 'Reverting CLAUDE.md commit'));
64
+ await log(` Using saved commit hash: ${claudeCommitHash.substring(0, 7)}...`, { verbose: true });
65
+
66
+ const commitToRevert = claudeCommitHash;
67
+
68
+ // APPROACH 3: Check for modifications before reverting (proactive detection)
69
+ // This is the main strategy - detect if CLAUDE.md was modified after initial commit
70
+ await log(' Checking if CLAUDE.md was modified since initial commit...', { verbose: true });
71
+ const diffResult = await $({ cwd: tempDir })`git diff ${commitToRevert} HEAD -- CLAUDE.md 2>&1`;
72
+
73
+ if (diffResult.stdout && diffResult.stdout.trim()) {
74
+ // CLAUDE.md was modified after initial commit - use manual approach to avoid conflicts
75
+ await log(' CLAUDE.md was modified after initial commit, using manual cleanup...', { verbose: true });
76
+
77
+ // Get the state of CLAUDE.md from before the initial commit (parent of the commit we're reverting)
78
+ const parentCommit = `${commitToRevert}~1`;
79
+ const parentFileExists = await $({ cwd: tempDir })`git cat-file -e ${parentCommit}:CLAUDE.md 2>&1`;
80
+
81
+ if (parentFileExists.code === 0) {
82
+ // CLAUDE.md existed before the initial commit - restore it to that state
83
+ await log(' CLAUDE.md existed before session, restoring to previous state...', { verbose: true });
84
+ await $({ cwd: tempDir })`git checkout ${parentCommit} -- CLAUDE.md`;
85
+ } else {
86
+ // CLAUDE.md didn't exist before the initial commit - delete it
87
+ await log(' CLAUDE.md was created in session, removing it...', { verbose: true });
88
+ await $({ cwd: tempDir })`git rm -f CLAUDE.md 2>&1`;
89
+ }
90
+
91
+ // Create a manual revert commit
92
+ const commitResult = await $({ cwd: tempDir })`git commit -m "Revert: Remove CLAUDE.md changes from initial commit" 2>&1`;
93
+
94
+ if (commitResult.code === 0) {
95
+ await log(formatAligned('šŸ“¦', 'Committed:', 'CLAUDE.md revert (manual)'));
96
+
97
+ // Push the revert
98
+ const pushRevertResult = await $({ cwd: tempDir })`git push origin ${branchName} 2>&1`;
99
+ if (pushRevertResult.code === 0) {
100
+ await log(formatAligned('šŸ“¤', 'Pushed:', 'CLAUDE.md revert to GitHub'));
101
+ } else {
102
+ await log(' Warning: Could not push CLAUDE.md revert', { verbose: true });
103
+ }
104
+ } else {
105
+ await log(' Warning: Could not create manual revert commit', { verbose: true });
106
+ await log(` Commit output: ${commitResult.stderr || commitResult.stdout}`, { verbose: true });
107
+ }
108
+ } else {
109
+ // No modifications detected - safe to use git revert (standard approach)
110
+ await log(' No modifications detected, using standard git revert...', { verbose: true });
111
+
112
+ // FALLBACK 1: Standard git revert
113
+ const revertResult = await $({ cwd: tempDir })`git revert ${commitToRevert} --no-edit 2>&1`;
114
+ if (revertResult.code === 0) {
115
+ await log(formatAligned('šŸ“¦', 'Committed:', 'CLAUDE.md revert'));
116
+
117
+ // Push the revert
118
+ const pushRevertResult = await $({ cwd: tempDir })`git push origin ${branchName} 2>&1`;
119
+ if (pushRevertResult.code === 0) {
120
+ await log(formatAligned('šŸ“¤', 'Pushed:', 'CLAUDE.md revert to GitHub'));
121
+ } else {
122
+ await log(' Warning: Could not push CLAUDE.md revert', { verbose: true });
123
+ }
124
+ } else {
125
+ // FALLBACK 2: Handle unexpected conflicts (three-way merge with automatic resolution)
126
+ const revertOutput = revertResult.stderr || revertResult.stdout || '';
127
+ const hasConflict = revertOutput.includes('CONFLICT') || revertOutput.includes('conflict');
128
+
129
+ if (hasConflict) {
130
+ await log(' Unexpected conflict detected, attempting automatic resolution...', { verbose: true });
131
+
132
+ // Check git status to see what files are in conflict
133
+ const statusResult = await $({ cwd: tempDir })`git status --short 2>&1`;
134
+ const statusOutput = statusResult.stdout || '';
135
+
136
+ // Check if CLAUDE.md is in the conflict
137
+ if (statusOutput.includes('CLAUDE.md')) {
138
+ await log(' Resolving CLAUDE.md conflict by restoring pre-session state...', { verbose: true });
139
+
140
+ // Get the state of CLAUDE.md from before the initial commit (parent of the commit we're reverting)
141
+ const parentCommit = `${commitToRevert}~1`;
142
+ const parentFileExists = await $({ cwd: tempDir })`git cat-file -e ${parentCommit}:CLAUDE.md 2>&1`;
143
+
144
+ if (parentFileExists.code === 0) {
145
+ // CLAUDE.md existed before the initial commit - restore it to that state
146
+ await log(' CLAUDE.md existed before session, restoring to previous state...', { verbose: true });
147
+ await $({ cwd: tempDir })`git checkout ${parentCommit} -- CLAUDE.md`;
148
+ // Stage the resolved CLAUDE.md
149
+ await $({ cwd: tempDir })`git add CLAUDE.md 2>&1`;
150
+ } else {
151
+ // CLAUDE.md didn't exist before the initial commit - delete it
152
+ await log(' CLAUDE.md was created in session, removing it...', { verbose: true });
153
+ await $({ cwd: tempDir })`git rm -f CLAUDE.md 2>&1`;
154
+ // No need to git add since git rm stages the deletion
155
+ }
156
+
157
+ // Complete the revert with the resolved conflict
158
+ const continueResult = await $({ cwd: tempDir })`git revert --continue --no-edit 2>&1`;
159
+
160
+ if (continueResult.code === 0) {
161
+ await log(formatAligned('šŸ“¦', 'Committed:', 'CLAUDE.md revert (conflict resolved)'));
162
+
163
+ // Push the revert
164
+ const pushRevertResult = await $({ cwd: tempDir })`git push origin ${branchName} 2>&1`;
165
+ if (pushRevertResult.code === 0) {
166
+ await log(formatAligned('šŸ“¤', 'Pushed:', 'CLAUDE.md revert to GitHub'));
167
+ } else {
168
+ await log(' Warning: Could not push CLAUDE.md revert', { verbose: true });
169
+ }
170
+ } else {
171
+ await log(' Warning: Could not complete revert after conflict resolution', { verbose: true });
172
+ await log(` Continue output: ${continueResult.stderr || continueResult.stdout}`, { verbose: true });
173
+ }
174
+ } else {
175
+ // Conflict in some other file, not CLAUDE.md - this is unexpected
176
+ await log(' Warning: Revert conflict in unexpected file(s), aborting revert', { verbose: true });
177
+ await $({ cwd: tempDir })`git revert --abort 2>&1`;
178
+ }
179
+ } else {
180
+ // Non-conflict error
181
+ await log(' Warning: Could not revert CLAUDE.md commit', { verbose: true });
182
+ await log(` Revert output: ${revertOutput}`, { verbose: true });
183
+ }
184
+ }
185
+ }
186
+ } catch (e) {
187
+ reportError(e, {
188
+ context: 'cleanup_claude_file',
189
+ tempDir,
190
+ operation: 'revert_claude_md_commit'
191
+ });
192
+ // If revert fails, that's okay - the task is still complete
193
+ await log(' CLAUDE.md revert failed or not needed', { verbose: true });
194
+ }
195
+ };
196
+
197
+ // Show session summary and handle limit reached scenarios
198
+ export const showSessionSummary = async (sessionId, limitReached, argv, issueUrl, tempDir, shouldAttachLogs = false) => {
199
+ await log('\n=== Session Summary ===');
200
+
201
+ if (sessionId) {
202
+ await log(`āœ… Session ID: ${sessionId}`);
203
+ // Always use absolute path for log file display
204
+ const path = (await use('path'));
205
+ const absoluteLogPath = path.resolve(getLogFile());
206
+ await log(`āœ… Complete log file: ${absoluteLogPath}`);
207
+
208
+ if (limitReached) {
209
+ await log('\nā° LIMIT REACHED DETECTED!');
210
+
211
+ if (argv.autoContinueOnLimitReset && global.limitResetTime) {
212
+ await log(`\nšŸ”„ AUTO-CONTINUE ON LIMIT RESET ENABLED - Will resume at ${global.limitResetTime}`);
213
+ await autoContinueWhenLimitResets(issueUrl, sessionId, argv, shouldAttachLogs);
214
+ } else {
215
+ // Only show resume recommendation if --no-auto-cleanup was passed
216
+ if (argv.autoCleanup === false) {
217
+ await log('\nšŸ”„ To resume when limit resets, use:\n');
218
+ await log(`./solve.mjs "${issueUrl}" --resume ${sessionId}`);
219
+
220
+ if (global.limitResetTime) {
221
+ await log(`\nšŸ’” Or enable auto-continue-on-limit-reset to wait until ${global.limitResetTime}:\n`);
222
+ await log(`./solve.mjs "${issueUrl}" --resume ${sessionId} --auto-continue-on-limit-reset`);
223
+ }
224
+
225
+ await log('\n This will continue from where it left off with full context.\n');
226
+ } else {
227
+ await log('\nāš ļø Note: Temporary directory will be automatically cleaned up.');
228
+ await log(' To keep the directory for debugging or resuming, use --no-auto-cleanup');
229
+ }
230
+ }
231
+ } else {
232
+ // Show command to resume session in interactive mode only if --no-auto-cleanup was passed
233
+ if (argv.autoCleanup === false) {
234
+ await log('\nšŸ’” To continue this session in Claude Code interactive mode:\n');
235
+ await log(` (cd ${tempDir} && claude --resume ${sessionId})`);
236
+ await log('');
237
+ } else {
238
+ await log('\nāš ļø Note: Temporary directory will be automatically cleaned up.');
239
+ await log(' To keep the directory for debugging or resuming, use --no-auto-cleanup');
240
+ }
241
+ }
242
+
243
+ // Don't show log preview, it's too technical
244
+ } else {
245
+ await log('āŒ No session ID extracted');
246
+ // Always use absolute path for log file display
247
+ const logFilePath = path.resolve(getLogFile());
248
+ await log(`šŸ“ Log file available: ${logFilePath}`);
249
+ }
250
+ };
251
+
252
+ // Verify results by searching for new PRs and comments
253
+ export const verifyResults = async (owner, repo, branchName, issueNumber, prNumber, prUrl, referenceTime, argv, shouldAttachLogs, shouldRestart = false, sessionId = null, tempDir = null, anthropicTotalCostUSD = null, publicPricingEstimate = null, pricingInfo = null) => {
254
+ await log('\nšŸ” Searching for created pull requests or comments...');
255
+
256
+ try {
257
+ // Get the current user's GitHub username
258
+ const userResult = await $`gh api user --jq .login`;
259
+
260
+ if (userResult.code !== 0) {
261
+ throw new Error(`Failed to get current user: ${userResult.stderr ? userResult.stderr.toString() : 'Unknown error'}`);
262
+ }
263
+
264
+ const currentUser = userResult.stdout.toString().trim();
265
+ if (!currentUser) {
266
+ throw new Error('Unable to determine current GitHub user');
267
+ }
268
+
269
+ // Search for pull requests created from our branch
270
+ await log('\nšŸ” Checking for pull requests from branch ' + branchName + '...');
271
+
272
+ // First, get all PRs from our branch
273
+ const allBranchPrsResult = await $`gh pr list --repo ${owner}/${repo} --head ${branchName} --json number,url,createdAt,headRefName,title,state,updatedAt,isDraft`;
274
+
275
+ if (allBranchPrsResult.code !== 0) {
276
+ await log(' āš ļø Failed to check pull requests');
277
+ // Continue with empty list
278
+ }
279
+
280
+ const allBranchPrs = allBranchPrsResult.stdout.toString().trim() ? JSON.parse(allBranchPrsResult.stdout.toString().trim()) : [];
281
+
282
+ // Check if we have any PRs from our branch
283
+ // If auto-PR was created, it should be the one we're working on
284
+ if (allBranchPrs.length > 0) {
285
+ const pr = allBranchPrs[0]; // Get the most recent PR from our branch
286
+
287
+ // If we created a PR earlier in this session, it would be prNumber
288
+ // Or if the PR was updated during the session (updatedAt > referenceTime)
289
+ const isPrFromSession = (prNumber && pr.number.toString() === prNumber) ||
290
+ (prUrl && pr.url === prUrl) ||
291
+ new Date(pr.updatedAt) > referenceTime ||
292
+ new Date(pr.createdAt) > referenceTime;
293
+
294
+ if (isPrFromSession) {
295
+ await log(` āœ… Found pull request #${pr.number}: "${pr.title}"`);
296
+
297
+ // Check if PR body has proper issue linking keywords
298
+ const prBodyResult = await $`gh pr view ${pr.number} --repo ${owner}/${repo} --json body --jq .body`;
299
+ if (prBodyResult.code === 0) {
300
+ const prBody = prBodyResult.stdout.toString();
301
+ const issueRef = argv.fork ? `${owner}/${repo}#${issueNumber}` : `#${issueNumber}`;
302
+
303
+ // Use the new GitHub linking detection library to check for valid keywords
304
+ // This ensures we only detect actual GitHub-recognized linking keywords
305
+ // (fixes, closes, resolves and their variants) in proper format
306
+ // See: https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue
307
+ const hasLinkingKeyword = hasGitHubLinkingKeyword(
308
+ prBody,
309
+ issueNumber,
310
+ argv.fork ? owner : null,
311
+ argv.fork ? repo : null
312
+ );
313
+
314
+ if (!hasLinkingKeyword) {
315
+ await log(` šŸ“ Updating PR body to link issue #${issueNumber}...`);
316
+
317
+ // Add proper issue reference to the PR body
318
+ const linkingText = `\n\nFixes ${issueRef}`;
319
+ const updatedBody = prBody + linkingText;
320
+
321
+ // Use --body-file instead of --body to avoid command-line length limits
322
+ // and special character escaping issues that can cause hangs/timeouts
323
+ const fs = (await use('fs')).promises;
324
+ const tempBodyFile = `/tmp/pr-body-update-${pr.number}-${Date.now()}.md`;
325
+ await fs.writeFile(tempBodyFile, updatedBody);
326
+
327
+ try {
328
+ const updateResult = await $`gh pr edit ${pr.number} --repo ${owner}/${repo} --body-file "${tempBodyFile}"`;
329
+
330
+ // Clean up temp file
331
+ await fs.unlink(tempBodyFile).catch(() => {});
332
+
333
+ if (updateResult.code === 0) {
334
+ await log(` āœ… Updated PR body to include "Fixes ${issueRef}"`);
335
+ } else {
336
+ await log(` āš ļø Could not update PR body: ${updateResult.stderr ? updateResult.stderr.toString().trim() : 'Unknown error'}`);
337
+ }
338
+ } catch (updateError) {
339
+ // Clean up temp file on error
340
+ await fs.unlink(tempBodyFile).catch(() => {});
341
+ throw updateError;
342
+ }
343
+ } else {
344
+ await log(' āœ… PR body already contains issue reference');
345
+ }
346
+ }
347
+
348
+ // Check if PR is ready for review (convert from draft if necessary)
349
+ if (pr.isDraft) {
350
+ await log(' šŸ”„ Converting PR from draft to ready for review...');
351
+ const readyResult = await $`gh pr ready ${pr.number} --repo ${owner}/${repo}`;
352
+ if (readyResult.code === 0) {
353
+ await log(' āœ… PR converted to ready for review');
354
+ } else {
355
+ await log(` āš ļø Could not convert PR to ready (${readyResult.stderr ? readyResult.stderr.toString().trim() : 'unknown error'})`);
356
+ }
357
+ } else {
358
+ await log(' āœ… PR is already ready for review', { verbose: true });
359
+ }
360
+
361
+ // Upload log file to PR if requested
362
+ let logUploadSuccess = false;
363
+ if (shouldAttachLogs) {
364
+ await log('\nšŸ“Ž Uploading solution draft log to Pull Request...');
365
+ logUploadSuccess = await attachLogToGitHub({
366
+ logFile: getLogFile(),
367
+ targetType: 'pr',
368
+ targetNumber: pr.number,
369
+ owner,
370
+ repo,
371
+ $,
372
+ log,
373
+ sanitizeLogContent,
374
+ verbose: argv.verbose,
375
+ sessionId,
376
+ tempDir,
377
+ anthropicTotalCostUSD,
378
+ // Pass agent tool pricing data when available
379
+ publicPricingEstimate,
380
+ pricingInfo
381
+ });
382
+ }
383
+
384
+ await log('\nšŸŽ‰ SUCCESS: A solution draft has been prepared as a pull request');
385
+ await log(`šŸ“ URL: ${pr.url}`);
386
+ if (shouldAttachLogs && logUploadSuccess) {
387
+ await log('šŸ“Ž Solution draft log has been attached to the Pull Request');
388
+ } else if (shouldAttachLogs && !logUploadSuccess) {
389
+ await log('āš ļø Solution draft log upload was requested but failed');
390
+ }
391
+ await log('\n✨ Please review the pull request for the proposed solution draft.');
392
+ // Don't exit if watch mode is enabled OR if auto-restart is needed for uncommitted changes
393
+ if (!argv.watch && !shouldRestart) {
394
+ await safeExit(0, 'Process completed successfully');
395
+ }
396
+ return; // Return normally for watch mode or auto-restart
397
+ } else {
398
+ await log(` ā„¹ļø Found pull request #${pr.number} but it appears to be from a different session`);
399
+ }
400
+ } else {
401
+ await log(` ā„¹ļø No pull requests found from branch ${branchName}`);
402
+ }
403
+
404
+ // If no PR found, search for recent comments on the issue
405
+ await log('\nšŸ” Checking for new comments on issue #' + issueNumber + '...');
406
+
407
+ // Get all comments and filter them
408
+ const allCommentsResult = await $`gh api repos/${owner}/${repo}/issues/${issueNumber}/comments`;
409
+
410
+ if (allCommentsResult.code !== 0) {
411
+ await log(' āš ļø Failed to check comments');
412
+ // Continue with empty list
413
+ }
414
+
415
+ const allComments = JSON.parse(allCommentsResult.stdout.toString().trim() || '[]');
416
+
417
+ // Filter for new comments by current user
418
+ const newCommentsByUser = allComments.filter(comment =>
419
+ comment.user.login === currentUser && new Date(comment.created_at) > referenceTime
420
+ );
421
+
422
+ if (newCommentsByUser.length > 0) {
423
+ const lastComment = newCommentsByUser[newCommentsByUser.length - 1];
424
+ await log(` āœ… Found new comment by ${currentUser}`);
425
+
426
+ // Upload log file to issue if requested
427
+ if (shouldAttachLogs) {
428
+ await log('\nšŸ“Ž Uploading solution draft log to issue...');
429
+ await attachLogToGitHub({
430
+ logFile: getLogFile(),
431
+ targetType: 'issue',
432
+ targetNumber: issueNumber,
433
+ owner,
434
+ repo,
435
+ $,
436
+ log,
437
+ sanitizeLogContent,
438
+ verbose: argv.verbose,
439
+ sessionId,
440
+ tempDir,
441
+ anthropicTotalCostUSD,
442
+ // Pass agent tool pricing data when available
443
+ publicPricingEstimate,
444
+ pricingInfo
445
+ });
446
+ }
447
+
448
+ await log('\nšŸ’¬ SUCCESS: Comment posted on issue');
449
+ await log(`šŸ“ URL: ${lastComment.html_url}`);
450
+ if (shouldAttachLogs) {
451
+ await log('šŸ“Ž Solution draft log has been attached to the issue');
452
+ }
453
+ await log('\n✨ A clarifying comment has been added to the issue.');
454
+ // Don't exit if watch mode is enabled OR if auto-restart is needed for uncommitted changes
455
+ if (!argv.watch && !shouldRestart) {
456
+ await safeExit(0, 'Process completed successfully');
457
+ }
458
+ return; // Return normally for watch mode or auto-restart
459
+ } else if (allComments.length > 0) {
460
+ await log(` ā„¹ļø Issue has ${allComments.length} existing comment(s)`);
461
+ } else {
462
+ await log(' ā„¹ļø No comments found on issue');
463
+ }
464
+
465
+ // If neither found, it might not have been necessary to create either
466
+ await log('\nšŸ“‹ No new pull request or comment was created.');
467
+ await log(' The issue may have been resolved differently or required no action.');
468
+ await log('\nšŸ’” Review the session log for details:');
469
+ // Always use absolute path for log file display
470
+ const reviewLogPath = path.resolve(getLogFile());
471
+ await log(` ${reviewLogPath}`);
472
+ // Don't exit if watch mode is enabled - it needs to continue monitoring
473
+ if (!argv.watch) {
474
+ await safeExit(0, 'Process completed successfully');
475
+ }
476
+ return; // Return normally for watch mode
477
+
478
+ } catch (searchError) {
479
+ reportError(searchError, {
480
+ context: 'verify_pr_creation',
481
+ issueNumber,
482
+ operation: 'search_for_pr'
483
+ });
484
+ await log('\nāš ļø Could not verify results:', searchError.message);
485
+ await log('\nšŸ’” Check the log file for details:');
486
+ // Always use absolute path for log file display
487
+ const checkLogPath = path.resolve(getLogFile());
488
+ await log(` ${checkLogPath}`);
489
+ // Don't exit if watch mode is enabled - it needs to continue monitoring
490
+ if (!argv.watch) {
491
+ await safeExit(0, 'Process completed successfully');
492
+ }
493
+ return; // Return normally for watch mode
494
+ }
495
+ };
496
+
497
+ // Handle execution errors with log attachment
498
+ export const handleExecutionError = async (error, shouldAttachLogs, owner, repo, argv = {}) => {
499
+ const { cleanErrorMessage } = await import('./lib.mjs');
500
+ await log('Error executing command:', cleanErrorMessage(error));
501
+ await log(`Stack trace: ${error.stack}`, { verbose: true });
502
+
503
+ // If --attach-logs is enabled, try to attach failure logs
504
+ if (shouldAttachLogs && getLogFile()) {
505
+ await log('\nšŸ“„ Attempting to attach failure logs...');
506
+
507
+ // Try to attach to existing PR first
508
+ if (global.createdPR && global.createdPR.number) {
509
+ try {
510
+ const logUploadSuccess = await attachLogToGitHub({
511
+ logFile: getLogFile(),
512
+ targetType: 'pr',
513
+ targetNumber: global.createdPR.number,
514
+ owner,
515
+ repo,
516
+ $,
517
+ log,
518
+ sanitizeLogContent,
519
+ verbose: argv.verbose || false,
520
+ errorMessage: cleanErrorMessage(error)
521
+ });
522
+
523
+ if (logUploadSuccess) {
524
+ await log('šŸ“Ž Failure log attached to Pull Request');
525
+ }
526
+ } catch (attachError) {
527
+ reportError(attachError, {
528
+ context: 'attach_success_log',
529
+ prNumber: global.createdPR?.number,
530
+ operation: 'attach_log_to_pr'
531
+ });
532
+ await log(`āš ļø Could not attach failure log: ${attachError.message}`, { level: 'warning' });
533
+ }
534
+ }
535
+ }
536
+
537
+ // If --auto-close-pull-request-on-fail is enabled, close the PR
538
+ if (argv.autoClosePullRequestOnFail && global.createdPR && global.createdPR.number) {
539
+ await log('\nšŸ”’ Auto-closing pull request due to failure...');
540
+ try {
541
+ const result = await $`gh pr close ${global.createdPR.number} --repo ${owner}/${repo} --comment "Auto-closed due to execution failure. Logs have been attached for debugging."`;
542
+ if (result.exitCode === 0) {
543
+ await log('āœ… Pull request closed successfully');
544
+ } else {
545
+ await log(`āš ļø Could not close pull request: ${result.stderr}`, { level: 'warning' });
546
+ }
547
+ } catch (closeError) {
548
+ reportError(closeError, {
549
+ context: 'close_success_pr',
550
+ prNumber: global.createdPR?.number,
551
+ operation: 'close_pull_request'
552
+ });
553
+ await log(`āš ļø Could not close pull request: ${closeError.message}`, { level: 'warning' });
554
+ }
555
+ }
556
+
557
+ await safeExit(1, 'Execution error');
558
+ };