@link-assistant/hive-mind 1.35.4 → 1.35.6

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 CHANGED
@@ -1,5 +1,22 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.35.6
4
+
5
+ ### Patch Changes
6
+
7
+ - 4b0beaf: Fix interactive mode PR comment output: use stdin for GitHub API calls to prevent shell quoting corruption, flush comment queue before tool result timeout to prevent stuck "Waiting for result..." comments, and guard against duplicate session started comments from late system.init events
8
+
9
+ ## 1.35.5
10
+
11
+ ### Patch Changes
12
+
13
+ - 37481da: fix: improve PR creation failure error messaging and log upload fallback (Issue #1462)
14
+ - Consolidate triple error output into a single clear error message when PR creation fails
15
+ - Upload failure logs to the issue as fallback when PR is not available (--attach-logs)
16
+ - Capture and log `gh pr create` stdout/stderr in verbose mode for root cause diagnosis
17
+ - Add fallback GitHub user detection via `gh auth status` when `gh api user` fails
18
+ - Rename `github-issue-creator.lib.mjs` to `github-error-reporter.lib.mjs` for clarity
19
+
3
20
  ## 1.35.4
4
21
 
5
22
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.35.4",
3
+ "version": "1.35.6",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Automatic GitHub issue creation for error reporting
4
+ * GitHub error reporter - handles error reporting via GitHub issues and comments
5
5
  */
6
6
 
7
7
  import { createInterface } from 'readline';
@@ -52,7 +52,8 @@ const getCurrentGitHubUser = async () => {
52
52
  try {
53
53
  const result = await $`gh api user --jq .login`;
54
54
  if (result.exitCode === 0) {
55
- return result.stdout.toString().trim();
55
+ const user = result.stdout.toString().trim();
56
+ if (user) return user;
56
57
  }
57
58
  } catch (error) {
58
59
  reportError(error, {
@@ -60,6 +61,18 @@ const getCurrentGitHubUser = async () => {
60
61
  operation: 'gh_api_user',
61
62
  });
62
63
  }
64
+ // Issue #1462: Fallback to gh auth status when gh api user fails
65
+ // This handles OAuth tokens (gho_****) that may lack the 'user' API scope
66
+ try {
67
+ const authResult = await $`gh auth status --hostname github.com 2>&1`;
68
+ const output = (authResult.stdout?.toString() || '') + (authResult.stderr?.toString() || '');
69
+ const userMatch = output.match(/Logged in to github\.com account (\S+)/i) || output.match(/Logged in to github\.com as (\S+)/i);
70
+ if (userMatch) {
71
+ return userMatch[1];
72
+ }
73
+ } catch {
74
+ // Silently ignore - will return null below
75
+ }
63
76
  return null;
64
77
  };
65
78
 
@@ -52,6 +52,13 @@ const CONFIG = {
52
52
  // See: https://github.com/link-assistant/hive-mind/issues/1324
53
53
  import { sanitizeUnicode } from './unicode-sanitization.lib.mjs';
54
54
 
55
+ // Use child_process for stdin-based API calls to avoid shell quoting issues
56
+ // with large/complex comment bodies containing backticks, quotes, etc.
57
+ // See: https://github.com/link-assistant/hive-mind/issues/1458
58
+ import { execFile } from 'node:child_process';
59
+ import { promisify } from 'node:util';
60
+ const execFileAsync = promisify(execFile);
61
+
55
62
  /**
56
63
  * Truncate content in the middle, keeping start and end
57
64
  * This helps show context while reducing size for large outputs
@@ -237,7 +244,9 @@ const getToolIcon = toolName => {
237
244
  * @returns {Object} Handler object with event processing methods
238
245
  */
239
246
  export const createInteractiveHandler = options => {
240
- const { owner, repo, prNumber, $, log, verbose = false } = options;
247
+ const { owner, repo, prNumber, log, verbose = false, execFile: execFileFn } = options;
248
+ // Use injected execFile for testability, or the real one by default
249
+ const runGhApi = execFileFn || execFileAsync;
241
250
 
242
251
  // State tracking for the handler
243
252
  const state = {
@@ -291,24 +300,35 @@ export const createInteractiveHandler = options => {
291
300
  }
292
301
 
293
302
  try {
294
- // Post comment and capture the output to get the comment URL/ID
295
- const result = await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${body}`;
303
+ // Post comment via gh api with stdin to avoid shell quoting issues
304
+ // with complex markdown bodies containing backticks, quotes, etc.
305
+ // See: https://github.com/link-assistant/hive-mind/issues/1458
306
+ const apiUrl = `repos/${owner}/${repo}/issues/${prNumber}/comments`;
307
+ const jsonPayload = JSON.stringify({ body });
308
+ const { stdout } = await runGhApi('gh', ['api', apiUrl, '-X', 'POST', '--input', '-'], {
309
+ input: jsonPayload,
310
+ maxBuffer: 10 * 1024 * 1024, // 10MB
311
+ });
296
312
  state.lastCommentTime = Date.now();
297
313
 
298
- // Extract comment ID from the result (gh outputs the comment URL)
299
- // Format: https://github.com/owner/repo/pull/123#issuecomment-1234567890
300
- // Note: command-stream returns stdout as a Buffer, so we need to call .toString()
301
- const output = result.stdout?.toString() || result.toString() || '';
302
- const match = output.match(/issuecomment-(\d+)/);
303
- const commentId = match ? match[1] : null;
314
+ // Extract comment ID from the API response JSON
315
+ let commentId = null;
316
+ try {
317
+ const response = JSON.parse(stdout);
318
+ commentId = response.id ? String(response.id) : null;
319
+ } catch {
320
+ // Fallback: try to extract from URL pattern
321
+ const match = stdout.match(/issuecomment-(\d+)|"id":\s*(\d+)/);
322
+ commentId = match ? match[1] || match[2] : null;
323
+ }
304
324
 
305
325
  if (verbose) {
306
- await log(`✅ Interactive mode: Comment posted${commentId ? ` (ID: ${commentId})` : ''}`, { verbose: true });
326
+ await log(`✅ Interactive mode: Comment posted${commentId ? ` (ID: ${commentId})` : ''} (body: ${body.length} chars)`, { verbose: true });
307
327
  }
308
328
  return commentId;
309
329
  } catch (error) {
310
330
  if (verbose) {
311
- await log(`⚠️ Interactive mode: Failed to post comment: ${error.message}`, { verbose: true });
331
+ await log(`⚠️ Interactive mode: Failed to post comment: ${error.message} (body: ${body.length} chars)`, { verbose: true });
312
332
  }
313
333
  return null;
314
334
  }
@@ -330,14 +350,22 @@ export const createInteractiveHandler = options => {
330
350
  }
331
351
 
332
352
  try {
333
- await $`gh api repos/${owner}/${repo}/issues/comments/${commentId} -X PATCH -f body=${body}`;
353
+ // Edit comment via gh api with stdin to avoid shell quoting issues
354
+ // with complex markdown bodies containing backticks, quotes, etc.
355
+ // See: https://github.com/link-assistant/hive-mind/issues/1458
356
+ const apiUrl = `repos/${owner}/${repo}/issues/comments/${commentId}`;
357
+ const jsonPayload = JSON.stringify({ body });
358
+ await runGhApi('gh', ['api', apiUrl, '-X', 'PATCH', '--input', '-'], {
359
+ input: jsonPayload,
360
+ maxBuffer: 10 * 1024 * 1024, // 10MB
361
+ });
334
362
  if (verbose) {
335
- await log(`✅ Interactive mode: Comment ${commentId} updated`, { verbose: true });
363
+ await log(`✅ Interactive mode: Comment ${commentId} updated (body: ${body.length} chars, payload: ${jsonPayload.length} chars)`, { verbose: true });
336
364
  }
337
365
  return true;
338
366
  } catch (error) {
339
367
  if (verbose) {
340
- await log(`⚠️ Interactive mode: Failed to edit comment: ${error.message}`, { verbose: true });
368
+ await log(`⚠️ Interactive mode: Failed to edit comment ${commentId}: ${error.message} (body: ${body.length} chars)`, { verbose: true });
341
369
  }
342
370
  return false;
343
371
  }
@@ -398,6 +426,16 @@ export const createInteractiveHandler = options => {
398
426
  * @param {Object} data - Event data
399
427
  */
400
428
  const handleSystemInit = async data => {
429
+ // Guard against duplicate init events (e.g., when a late task_notification
430
+ // arrives after the result event and triggers a new conversation turn)
431
+ // See: https://github.com/link-assistant/hive-mind/issues/1458
432
+ if (state.sessionId) {
433
+ if (verbose) {
434
+ await log(`⚠️ Interactive mode: Ignoring duplicate system.init event (session already initialized: ${state.sessionId})`, { verbose: true });
435
+ }
436
+ return;
437
+ }
438
+
401
439
  state.sessionId = data.session_id;
402
440
  state.startTime = Date.now();
403
441
 
@@ -672,21 +710,44 @@ ${createRawJsonSection(data)}`;
672
710
  // If comment ID is not yet available (comment was queued), wait for it
673
711
  // But use a timeout to avoid blocking forever
674
712
  if (!commentId && commentIdPromise) {
675
- if (verbose) {
676
- await log(`⏳ Interactive mode: Waiting for tool use comment to be posted (tool: ${toolUseId})`, {
677
- verbose: true,
678
- });
713
+ // First, try to flush the queue — the tool_use comment may still be
714
+ // waiting for rate-limit clearance. Processing it here avoids the 30s
715
+ // timeout that previously caused many comments to stay stuck on
716
+ // "Waiting for result...".
717
+ // See: https://github.com/link-assistant/hive-mind/issues/1458
718
+ if (state.commentQueue.length > 0) {
719
+ if (verbose) {
720
+ await log(`🔄 Interactive mode: Flushing comment queue (${state.commentQueue.length} items) before waiting for tool use comment`, {
721
+ verbose: true,
722
+ });
723
+ }
724
+ // Temporarily reset isProcessing to allow processQueue to run
725
+ const wasProcessing = state.isProcessing;
726
+ state.isProcessing = false;
727
+ await processQueue();
728
+ state.isProcessing = wasProcessing;
679
729
  }
680
- // Wait for the comment to be posted (with 30 second timeout)
681
- const timeoutPromise = new Promise(resolve => setTimeout(() => resolve(null), 30000));
682
- commentId = await Promise.race([commentIdPromise, timeoutPromise]);
730
+
731
+ // Check again after queue flush
732
+ commentId = pendingCall.commentId;
683
733
 
684
734
  if (!commentId) {
685
735
  if (verbose) {
686
- await log('⚠️ Interactive mode: Timeout waiting for tool use comment, posting result separately', {
736
+ await log(`⏳ Interactive mode: Waiting for tool use comment to be posted (tool: ${toolUseId})`, {
687
737
  verbose: true,
688
738
  });
689
739
  }
740
+ // Wait for the comment to be posted (with 30 second timeout)
741
+ const timeoutPromise = new Promise(resolve => setTimeout(() => resolve(null), 30000));
742
+ commentId = await Promise.race([commentIdPromise, timeoutPromise]);
743
+
744
+ if (!commentId) {
745
+ if (verbose) {
746
+ await log('⚠️ Interactive mode: Timeout waiting for tool use comment, posting result separately', {
747
+ verbose: true,
748
+ });
749
+ }
750
+ }
690
751
  }
691
752
  }
692
753
 
@@ -1113,12 +1113,14 @@ ${prBody}`,
1113
1113
  }
1114
1114
 
1115
1115
  let output;
1116
+ let prCreateStderr = '';
1116
1117
  let assigneeFailed = false;
1117
1118
 
1118
1119
  // Try to create PR with assignee first (if specified)
1119
1120
  try {
1120
1121
  const result = await execAsync(command, { encoding: 'utf8', cwd: tempDir, env: process.env });
1121
1122
  output = result.stdout;
1123
+ prCreateStderr = result.stderr || '';
1122
1124
  } catch (firstError) {
1123
1125
  // Check if the error is specifically about assignee validation
1124
1126
  const errorMsg = firstError.message || '';
@@ -1146,6 +1148,7 @@ ${prBody}`,
1146
1148
  // Retry without assignee - if this fails, let the error propagate to outer catch
1147
1149
  const retryResult = await execAsync(command, { encoding: 'utf8', cwd: tempDir, env: process.env });
1148
1150
  output = retryResult.stdout;
1151
+ prCreateStderr = retryResult.stderr || '';
1149
1152
  } else {
1150
1153
  // Not an assignee error, re-throw the original error
1151
1154
  throw firstError;
@@ -1168,6 +1171,14 @@ ${prBody}`,
1168
1171
  });
1169
1172
  });
1170
1173
 
1174
+ // Log gh pr create output for debugging (Issue #1462)
1175
+ if (argv.verbose) {
1176
+ await log(` gh pr create stdout: ${(output || '').trim() || '(empty)'}`, { verbose: true });
1177
+ if (prCreateStderr) {
1178
+ await log(` gh pr create stderr: ${prCreateStderr.trim()}`, { verbose: true });
1179
+ }
1180
+ }
1181
+
1171
1182
  // Extract PR URL from output - gh pr create outputs the URL to stdout
1172
1183
  prUrl = output.trim();
1173
1184
 
@@ -1214,22 +1225,11 @@ ${prBody}`,
1214
1225
  }
1215
1226
  } else {
1216
1227
  // PR does not exist - gh pr create must have failed silently
1217
- await log('');
1218
- await log(formatAligned('❌', 'FATAL ERROR:', 'PR creation failed'), { level: 'error' });
1219
- await log('');
1220
- await log(' 🔍 What happened:');
1221
- await log(' The gh pr create command returned a URL, but the PR does not exist on GitHub.');
1222
- await log('');
1223
- await log(' 🔧 How to fix:');
1224
- await log(' 1. Check if PR exists manually:');
1225
- await log(` gh pr list --repo ${owner}/${repo} --head ${branchName}`);
1226
- await log(' 2. Try creating PR manually:');
1227
- await log(` cd ${tempDir}`);
1228
- await log(` gh pr create --draft --title "Fix issue #${issueNumber}" --body "Fixes #${issueNumber}"`);
1229
- await log(' 3. Check GitHub authentication:');
1230
- await log(' gh auth status');
1231
- await log('');
1232
- throw new Error('PR creation failed - PR does not exist on GitHub');
1228
+ // Issue #1462: Include gh pr create stderr for root cause diagnosis
1229
+ const verifyStderr = verifyResult.stderr ? verifyResult.stderr.toString().trim() : '';
1230
+ const stderrInfo = prCreateStderr ? ` (gh pr create stderr: ${prCreateStderr.trim()})` : '';
1231
+ const verifyInfo = verifyStderr ? ` (gh pr view stderr: ${verifyStderr})` : '';
1232
+ throw new Error(`PR verification failed - gh pr create returned URL "${prUrl}" but PR #${localPrNumber} does not exist on GitHub${stderrInfo}${verifyInfo}`);
1233
1233
  }
1234
1234
  // Store PR info globally for error handlers
1235
1235
  global.createdPR = { number: localPrNumber, url: prUrl };
@@ -1374,73 +1374,22 @@ ${prBody}`,
1374
1374
  branchName,
1375
1375
  operation: 'create_pull_request',
1376
1376
  });
1377
+ // Issue #1462: Don't log verbose error block here - let the outer catch
1378
+ // handler produce a single consolidated error message to avoid triple error output.
1379
+ // Extract clean error message and re-throw with context.
1377
1380
  const errorMsg = prCreateError.message || '';
1378
-
1379
- // Clean up the error message - extract the meaningful part
1380
1381
  let cleanError = errorMsg;
1381
1382
  if (errorMsg.includes('pull request create failed:')) {
1382
1383
  cleanError = errorMsg.split('pull request create failed:')[1].trim();
1383
1384
  } else if (errorMsg.includes('Command failed:')) {
1384
- // Extract just the error part, not the full command
1385
1385
  const lines = errorMsg.split('\n');
1386
1386
  cleanError = lines[lines.length - 1] || errorMsg;
1387
1387
  }
1388
1388
 
1389
- // Check for specific error types
1390
- // Note: Assignee errors are now handled by automatic retry in the try block above
1391
- // This catch block only handles other types of PR creation failures
1392
1389
  if (errorMsg.includes('No commits between') || errorMsg.includes("Head sha can't be blank")) {
1393
- // Empty PR error
1394
- await log('');
1395
- await log(formatAligned('❌', 'PR CREATION FAILED', ''), { level: 'error' });
1396
- await log('');
1397
- await log(' 🔍 What happened:');
1398
- await log(' Cannot create PR - no commits between branches.');
1399
- await log('');
1400
- await log(' 📦 Error details:');
1401
- for (const line of cleanError.split('\n')) {
1402
- if (line.trim()) await log(` ${line.trim()}`);
1403
- }
1404
- await log('');
1405
- await log(' 💡 Possible causes:');
1406
- await log(" • The branch wasn't pushed properly");
1407
- await log(" • The commit wasn't created");
1408
- await log(' • GitHub sync issue');
1409
- await log('');
1410
- await log(' 🔧 How to fix:');
1411
- await log(' 1. Verify commit exists:');
1412
- await log(` cd ${tempDir} && git log --format="%h %s" -5`);
1413
- await log(' 2. Push again with tracking:');
1414
- await log(` cd ${tempDir} && git push -u origin ${branchName}`);
1415
- await log(' 3. Create PR manually:');
1416
- await log(` cd ${tempDir} && gh pr create --draft`);
1417
- await log('');
1418
- await log(` 📂 Working directory: ${tempDir}`);
1419
- await log(` 🌿 Current branch: ${branchName}`);
1420
- await log('');
1421
- throw new Error('PR creation failed - no commits between branches');
1390
+ throw new Error(`PR creation failed - no commits between branches: ${cleanError}`);
1422
1391
  } else {
1423
- // Generic PR creation error
1424
- await log('');
1425
- await log(formatAligned('❌', 'PR CREATION FAILED', ''), { level: 'error' });
1426
- await log('');
1427
- await log(' 🔍 What happened:');
1428
- await log(' Failed to create pull request.');
1429
- await log('');
1430
- await log(' 📦 Error details:');
1431
- for (const line of cleanError.split('\n')) {
1432
- if (line.trim()) await log(` ${line.trim()}`);
1433
- }
1434
- await log('');
1435
- await log(' 🔧 How to fix:');
1436
- await log(' 1. Try creating PR manually:');
1437
- await log(` cd ${tempDir} && gh pr create --draft`);
1438
- await log(' 2. Check branch status:');
1439
- await log(` cd ${tempDir} && git status`);
1440
- await log(' 3. Verify GitHub authentication:');
1441
- await log(' gh auth status');
1442
- await log('');
1443
- throw new Error('PR creation failed');
1392
+ throw new Error(`PR creation failed: ${cleanError}`);
1444
1393
  }
1445
1394
  }
1446
1395
  }
@@ -1452,23 +1401,22 @@ ${prBody}`,
1452
1401
  operation: 'handle_auto_pr',
1453
1402
  });
1454
1403
 
1455
- // CRITICAL: PR creation failure should stop the entire process
1456
- // We cannot continue without a PR when auto-PR creation is enabled
1404
+ // Issue #1462: Single consolidated error message for PR creation failure.
1405
+ // Previously this was the third of three error blocks, causing confusing output.
1406
+ // Now this is the ONLY error block shown for PR creation failures.
1457
1407
  await log('');
1458
1408
  await log(formatAligned('❌', 'FATAL ERROR:', 'PR creation failed'), { level: 'error' });
1459
1409
  await log('');
1460
- await log(' 🔍 What this means:');
1461
- await log(' The solve command cannot continue without a pull request.');
1462
- await log(' Auto-PR creation is enabled but failed to create the PR.');
1463
- await log('');
1464
- await log(' 📦 Error details:');
1410
+ await log(' 🔍 What happened:');
1465
1411
  await log(` ${prError.message}`);
1466
1412
  await log('');
1413
+ await log(' 💡 The solve command cannot continue without a pull request.');
1414
+ await log('');
1467
1415
  await log(' 🔧 How to fix:');
1468
1416
  await log('');
1469
1417
  await log(' Option 1: Retry without auto-PR creation');
1470
1418
  await log(` ./solve.mjs "${issueUrl}" --no-auto-pull-request-creation`);
1471
- await log(' (Claude will create the PR during the session)');
1419
+ await log(' (The AI agent will create the PR during the session)');
1472
1420
  await log('');
1473
1421
  await log(' Option 2: Create PR manually first');
1474
1422
  await log(` cd ${tempDir}`);
@@ -1482,8 +1430,9 @@ ${prBody}`,
1482
1430
  await log(' gh pr create --draft # Try manually to see detailed error');
1483
1431
  await log('');
1484
1432
 
1485
- // Re-throw the error to stop execution
1486
- throw new Error(`PR creation failed: ${prError.message}`);
1433
+ // Re-throw the error to stop execution - use prError.message directly
1434
+ // to avoid "PR creation failed: PR creation failed" redundancy (Issue #1462)
1435
+ throw prError;
1487
1436
  }
1488
1437
 
1489
1438
  return { prUrl, prNumber: localPrNumber, claudeCommitHash };
@@ -8,8 +8,8 @@ import { safeExit } from './exit-handler.lib.mjs';
8
8
  // Import Sentry integration
9
9
  import { reportError } from './sentry.lib.mjs';
10
10
 
11
- // Import GitHub issue creator
12
- import { handleErrorWithIssueCreation } from './github-issue-creator.lib.mjs';
11
+ // Import GitHub error reporter
12
+ import { handleErrorWithIssueCreation } from './github-error-reporter.lib.mjs';
13
13
 
14
14
  /**
15
15
  * Handles log attachment and PR closing on failure
@@ -40,35 +40,45 @@ export const handleFailure = async options => {
40
40
  }
41
41
 
42
42
  // If --attach-logs is enabled, try to attach failure logs
43
- if (shouldAttachLogs && getLogFile() && global.createdPR && global.createdPR.number) {
44
- await log('\n📄 Attempting to attach failure logs...');
45
- try {
46
- const logUploadSuccess = await attachLogToGitHub({
47
- logFile: getLogFile(),
48
- targetType: 'pr',
49
- targetNumber: global.createdPR.number,
50
- owner: global.owner || owner,
51
- repo: global.repo || repo,
52
- $,
53
- log,
54
- sanitizeLogContent,
55
- verbose: argv.verbose,
56
- errorMessage: cleanErrorMessage(error),
57
- // Issue #1225: Pass model and tool info for PR comments
58
- requestedModel: argv.model,
59
- tool: argv.tool || 'claude',
60
- });
61
- if (logUploadSuccess) {
62
- await log('📎 Failure log attached to Pull Request');
43
+ if (shouldAttachLogs && getLogFile()) {
44
+ // Issue #1462: Upload logs to PR if available, otherwise fall back to the issue
45
+ const hasPR = global.createdPR && global.createdPR.number;
46
+ const hasIssue = global.issueNumber;
47
+ const targetType = hasPR ? 'pr' : hasIssue ? 'issue' : null;
48
+ const targetNumber = hasPR ? global.createdPR.number : hasIssue ? global.issueNumber : null;
49
+ const targetLabel = hasPR ? 'Pull Request' : 'Issue';
50
+
51
+ if (targetType && targetNumber) {
52
+ await log(`\n📄 Attempting to attach failure logs to ${targetLabel}...`);
53
+ try {
54
+ const logUploadSuccess = await attachLogToGitHub({
55
+ logFile: getLogFile(),
56
+ targetType,
57
+ targetNumber,
58
+ owner: global.owner || owner,
59
+ repo: global.repo || repo,
60
+ $,
61
+ log,
62
+ sanitizeLogContent,
63
+ verbose: argv.verbose,
64
+ errorMessage: cleanErrorMessage(error),
65
+ // Issue #1225: Pass model and tool info for PR comments
66
+ requestedModel: argv.model,
67
+ tool: argv.tool || 'claude',
68
+ });
69
+ if (logUploadSuccess) {
70
+ await log(`📎 Failure log attached to ${targetLabel}`);
71
+ }
72
+ } catch (attachError) {
73
+ reportError(attachError, {
74
+ context: 'attach_failure_log',
75
+ targetType,
76
+ targetNumber,
77
+ errorType,
78
+ operation: `attach_log_to_${targetType}`,
79
+ });
80
+ await log(`⚠️ Could not attach failure log to ${targetLabel}: ${attachError.message}`, { level: 'warning' });
63
81
  }
64
- } catch (attachError) {
65
- reportError(attachError, {
66
- context: 'attach_failure_log',
67
- prNumber: global.createdPR?.number,
68
- errorType,
69
- operation: 'attach_log_to_pr',
70
- });
71
- await log(`⚠️ Could not attach failure log: ${attachError.message}`, { level: 'warning' });
72
82
  }
73
83
  }
74
84
 
package/src/solve.mjs CHANGED
@@ -496,6 +496,9 @@ if (isPrUrl) {
496
496
  issueNumber = urlNumber;
497
497
  await log(`📝 Issue mode: Working with issue #${issueNumber}`);
498
498
  }
499
+ // Issue #1462: Store issueNumber in global so error handlers can upload logs to the issue
500
+ // as a fallback when PR creation fails and global.createdPR is not available
501
+ global.issueNumber = issueNumber;
499
502
  const workspaceInfo = argv.enableWorkspaces ? { owner, repo, issueNumber } : null;
500
503
  const { tempDir, workspaceTmpDir, needsClone } = await setupTempDirectory(argv, workspaceInfo);
501
504
  cleanupContext.tempDir = tempDir;
@@ -1076,19 +1079,25 @@ try {
1076
1079
  await log('');
1077
1080
  }
1078
1081
 
1079
- // If --attach-logs is enabled and we have a PR, attach failure logs before exiting
1082
+ // If --attach-logs is enabled, attach failure logs before exiting
1080
1083
  // Note: sessionId is not required - logs should be uploaded even if agent failed before establishing a session
1081
- // This aligns with the pattern in handleFailure() in solve.error-handlers.lib.mjs
1082
- if (shouldAttachLogs && global.createdPR && global.createdPR.number) {
1083
- await log('\n📄 Attaching failure logs to Pull Request...');
1084
+ // Issue #1462: Fall back to uploading logs to the issue if PR is not available
1085
+ const hasPR = global.createdPR && global.createdPR.number;
1086
+ const hasIssue = global.issueNumber;
1087
+ const logTargetType = hasPR ? 'pr' : hasIssue ? 'issue' : null;
1088
+ const logTargetNumber = hasPR ? global.createdPR.number : hasIssue ? global.issueNumber : null;
1089
+ const logTargetLabel = hasPR ? 'Pull Request' : 'Issue';
1090
+
1091
+ if (shouldAttachLogs && logTargetType && logTargetNumber) {
1092
+ await log(`\n📄 Attaching failure logs to ${logTargetLabel}...`);
1084
1093
  try {
1085
1094
  // Build Claude CLI resume command
1086
1095
  const tool = argv.tool || 'claude';
1087
1096
  const resumeCommand = sessionId && tool === 'claude' ? buildClaudeResumeCommand({ tempDir, sessionId, model: argv.model }) : null;
1088
1097
  const logUploadSuccess = await attachLogToGitHub({
1089
1098
  logFile: getLogFile(),
1090
- targetType: 'pr',
1091
- targetNumber: global.createdPR.number,
1099
+ targetType: logTargetType,
1100
+ targetNumber: logTargetNumber,
1092
1101
  owner,
1093
1102
  repo,
1094
1103
  $,
@@ -1110,7 +1119,7 @@ try {
1110
1119
  });
1111
1120
 
1112
1121
  if (logUploadSuccess) {
1113
- await log(' ✅ Failure logs uploaded successfully');
1122
+ await log(` ✅ Failure logs uploaded to ${logTargetLabel} successfully`);
1114
1123
  } else {
1115
1124
  await log(' ⚠️ Failed to upload logs', { verbose: true });
1116
1125
  }