@link-assistant/hive-mind 1.65.0 → 1.65.1

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,11 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.65.1
4
+
5
+ ### Patch Changes
6
+
7
+ - d5cd096: Add a solve flag to disable separate error-report issue creation while preserving original issue failure comments, and improve pre-PR branch divergence diagnostics.
8
+
3
9
  ## 1.65.0
4
10
 
5
11
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.65.0",
3
+ "version": "1.65.1",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -271,7 +271,7 @@ export const handleErrorWithIssueCreation = async options => {
271
271
 
272
272
  // --disable-report-issue takes highest precedence
273
273
  if (disableReport) {
274
- await log('ℹ️ Issue reporting disabled via --disable-report-issue.');
274
+ await log('ℹ️ Error issue creation is disabled by CLI configuration.');
275
275
  return null;
276
276
  }
277
277
 
@@ -4,8 +4,10 @@
4
4
  */
5
5
 
6
6
  import { closingIssueNumbersContain, parseClosingIssueNumbers } from './pr-issue-linking.lib.mjs';
7
+ import { buildPushRejectionExplanation, getRemoteBranchDivergenceSnapshot, synchronizeExistingIssueBranchBeforeAutoPrCreation } from './solve.branch-divergence.lib.mjs';
7
8
 
8
9
  import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): gh API calls flow through $ wrapped by caller
10
+
9
11
  export async function handleAutoPrCreation({ argv, tempDir, branchName, issueNumber, owner, repo, defaultBranch, forkedRepo, isContinueMode, prNumber, log, formatAligned, $, reportError, path, fs }) {
10
12
  // Skip auto-PR creation if:
11
13
  // 1. Auto-PR creation is disabled AND we're not in continue mode with no PR
@@ -33,6 +35,16 @@ export async function handleAutoPrCreation({ argv, tempDir, branchName, issueNum
33
35
  const issueUrl = argv['issue-url'] || argv._[0];
34
36
 
35
37
  try {
38
+ await synchronizeExistingIssueBranchBeforeAutoPrCreation({
39
+ tempDir,
40
+ branchName,
41
+ isContinueMode,
42
+ prNumber,
43
+ log,
44
+ formatAligned,
45
+ $,
46
+ });
47
+
36
48
  // Determine which file to create based on CLI flags
37
49
  let useClaudeFile = argv.claudeFile !== false;
38
50
  const useAutoGitkeepFile = argv.autoGitkeepFile !== false;
@@ -527,6 +539,7 @@ Proceed.
527
539
  await log('');
528
540
  throw new Error('Permission denied - need fork or collaborator access');
529
541
  } else if (errorOutput.includes('non-fast-forward') || errorOutput.includes('rejected') || errorOutput.includes('! [rejected]')) {
542
+ const divergence = await getRemoteBranchDivergenceSnapshot({ $, tempDir, branchName });
530
543
  // Push rejected due to conflicts or diverged history
531
544
  await log('');
532
545
  await log(formatAligned('❌', 'PUSH REJECTED:', 'Branch has diverged from remote'), { level: 'error' });
@@ -534,6 +547,9 @@ Proceed.
534
547
  await log(' 🔍 What happened:');
535
548
  await log(' The remote branch has changes that conflict with your local changes.');
536
549
  await log(' This typically means someone else has pushed to this branch.');
550
+ for (const line of buildPushRejectionExplanation({ branchName, isContinueMode, prNumber, divergence })) {
551
+ await log(line);
552
+ }
537
553
  await log('');
538
554
  await log(' 💡 Why we cannot fix this automatically:');
539
555
  await log(' • We never use force push to preserve history');
@@ -0,0 +1,93 @@
1
+ const toCount = value => {
2
+ const parsed = Number.parseInt(String(value || '').trim(), 10);
3
+ return Number.isFinite(parsed) ? parsed : null;
4
+ };
5
+
6
+ const outputOf = result => {
7
+ const stdout = result?.stdout ? result.stdout.toString().trim() : '';
8
+ const stderr = result?.stderr ? result.stderr.toString().trim() : '';
9
+ return stdout || stderr;
10
+ };
11
+
12
+ export function buildPushRejectionExplanation({ branchName, isContinueMode, prNumber, divergence = null }) {
13
+ const lines = [];
14
+
15
+ if (isContinueMode && !prNumber) {
16
+ lines.push(' This run reused an existing issue branch because auto-continue found a matching branch with no PR.');
17
+ lines.push(' It is not a fresh branch created by this run, even though auto-PR creation is running now.');
18
+ } else {
19
+ lines.push(' The remote branch changed after the local branch state used for this push.');
20
+ }
21
+
22
+ if (divergence?.remoteExists && divergence.ahead !== null && divergence.behind !== null) {
23
+ lines.push(` Current branch state for ${branchName}: ${divergence.ahead} commit(s) ahead, ${divergence.behind} commit(s) behind origin/${branchName}.`);
24
+ } else if (divergence?.fetchError) {
25
+ lines.push(` Could not inspect origin/${branchName}: ${divergence.fetchError}`);
26
+ }
27
+
28
+ return lines;
29
+ }
30
+
31
+ export async function getRemoteBranchDivergenceSnapshot({ $, tempDir, branchName }) {
32
+ const fetchResult = await $({ cwd: tempDir, silent: true })`git fetch origin refs/heads/${branchName}:refs/remotes/origin/${branchName} 2>&1`;
33
+ if (fetchResult.code !== 0) {
34
+ return {
35
+ remoteExists: false,
36
+ ahead: null,
37
+ behind: null,
38
+ fetchError: outputOf(fetchResult) || 'remote branch not found',
39
+ };
40
+ }
41
+
42
+ const aheadResult = await $({ cwd: tempDir, silent: true })`git rev-list --count origin/${branchName}..HEAD 2>&1`;
43
+ const behindResult = await $({ cwd: tempDir, silent: true })`git rev-list --count HEAD..origin/${branchName} 2>&1`;
44
+
45
+ return {
46
+ remoteExists: aheadResult.code === 0 && behindResult.code === 0,
47
+ ahead: aheadResult.code === 0 ? toCount(aheadResult.stdout) : null,
48
+ behind: behindResult.code === 0 ? toCount(behindResult.stdout) : null,
49
+ fetchError: aheadResult.code === 0 && behindResult.code === 0 ? null : outputOf(aheadResult) || outputOf(behindResult) || 'could not compare local and remote branch',
50
+ };
51
+ }
52
+
53
+ export async function synchronizeExistingIssueBranchBeforeAutoPrCreation({ tempDir, branchName, isContinueMode, prNumber, log, formatAligned, $ }) {
54
+ if (!(isContinueMode && !prNumber)) {
55
+ return null;
56
+ }
57
+
58
+ await log(formatAligned('🔎', 'Existing branch sync:', branchName));
59
+ const divergence = await getRemoteBranchDivergenceSnapshot({ $, tempDir, branchName });
60
+ if (!divergence.remoteExists) {
61
+ await log(` ⚠️ Could not inspect origin/${branchName}: ${divergence.fetchError || 'unknown error'}`, { level: 'warning' });
62
+ return divergence;
63
+ }
64
+
65
+ await log(` Branch state before PR bootstrap commit: ${divergence.ahead} commit(s) ahead, ${divergence.behind} commit(s) behind origin/${branchName}`);
66
+
67
+ if (divergence.behind > 0 && divergence.ahead === 0) {
68
+ await log(` Fast-forwarding ${branchName} to origin/${branchName} before creating the PR bootstrap commit...`);
69
+ const mergeResult = await $({ cwd: tempDir })`git merge --ff-only origin/${branchName} 2>&1`;
70
+ if (mergeResult.code !== 0) {
71
+ await log(` ⚠️ Fast-forward failed: ${outputOf(mergeResult) || 'unknown error'}`, {
72
+ level: 'warning',
73
+ });
74
+ throw new Error('Existing issue branch could not be fast-forwarded before PR creation');
75
+ }
76
+ await log(` ✅ Branch fast-forwarded to origin/${branchName}`);
77
+ return await getRemoteBranchDivergenceSnapshot({ $, tempDir, branchName });
78
+ }
79
+
80
+ if (divergence.behind > 0 && divergence.ahead > 0) {
81
+ for (const line of buildPushRejectionExplanation({
82
+ branchName,
83
+ isContinueMode,
84
+ prNumber,
85
+ divergence,
86
+ })) {
87
+ await log(line);
88
+ }
89
+ throw new Error('Existing issue branch has diverged before PR creation; manual resolution required');
90
+ }
91
+
92
+ return divergence;
93
+ }
@@ -512,6 +512,11 @@ export const SOLVE_OPTION_DEFINITIONS = {
512
512
  description: 'Disable error issue creation entirely (no prompt, no automatic creation). Overrides --auto-report-issue if both are specified.',
513
513
  default: false,
514
514
  },
515
+ 'disable-issue-auto-creation-on-error': {
516
+ type: 'boolean',
517
+ description: 'Disable creating a new GitHub error-report issue when solve fails, including the interactive prompt. This does not disable posting failure logs or comments to the original issue or pull request.',
518
+ default: false,
519
+ },
515
520
  'attach-solution-summary': {
516
521
  type: 'boolean',
517
522
  description: 'Attach the AI working session summary (from the result field) as a comment to the PR/issue after every working session. The summary is extracted from the AI tool JSON output and posted under a "Working session summary" header. Applies to the top-level run, auto-restart-until-mergeable iterations, and watch-mode iterations.',
@@ -718,6 +723,12 @@ export const parseArguments = async (yargs = getLinoYargsFactory(), hideBinFn =
718
723
  if (argv.toolCheck === false) {
719
724
  argv.toolConnectionCheck = false;
720
725
  }
726
+ // Issue #1752: new flag is the explicit user-facing switch for disabling
727
+ // creation of separate solver-error issues. Keep the existing internal
728
+ // disableReportIssue path as the single behavior flag.
729
+ if (argv.disableIssueAutoCreationOnError) {
730
+ argv.disableReportIssue = true;
731
+ }
721
732
  }
722
733
 
723
734
  // --finalize normalization
@@ -12,11 +12,14 @@ import { reportError } from './sentry.lib.mjs';
12
12
  // Import GitHub error reporter
13
13
  import { handleErrorWithIssueCreation } from './github-error-reporter.lib.mjs';
14
14
 
15
+ export const isErrorIssueAutoCreationDisabled = argv => !!(argv?.disableReportIssue || argv?.disableIssueAutoCreationOnError);
16
+
15
17
  /**
16
18
  * Handles log attachment and PR closing on failure
17
19
  */
18
20
  export const handleFailure = async options => {
19
21
  const { error, errorType, shouldAttachLogs, argv, global, owner, repo, log, getLogFile, attachLogToGitHub, cleanErrorMessage, sanitizeLogContent, $ } = options;
22
+ const disableIssueCreation = isErrorIssueAutoCreationDisabled(argv);
20
23
 
21
24
  // Offer to create GitHub issue for the error
22
25
  try {
@@ -30,9 +33,9 @@ export const handleFailure = async options => {
30
33
  prNumber: global.createdPR?.number,
31
34
  errorType,
32
35
  },
33
- skipPrompt: !process.stdin.isTTY || argv.noIssueCreation,
36
+ skipPrompt: !process.stdin.isTTY || argv.noIssueCreation || disableIssueCreation,
34
37
  autoReport: argv.autoReportIssue,
35
- disableReport: argv.disableReportIssue,
38
+ disableReport: disableIssueCreation,
36
39
  });
37
40
  } catch (issueError) {
38
41
  reportError(issueError, {
@@ -49,7 +52,7 @@ export const handleFailure = async options => {
49
52
  const hasIssue = global.issueNumber;
50
53
  const targetType = hasPR ? 'pr' : hasIssue ? 'issue' : null;
51
54
  const targetNumber = hasPR ? global.createdPR.number : hasIssue ? global.issueNumber : null;
52
- const targetLabel = hasPR ? 'Pull Request' : 'Issue';
55
+ const targetLabel = hasPR ? 'Pull Request' : `original issue #${targetNumber}`;
53
56
 
54
57
  if (targetType && targetNumber) {
55
58
  await log(`\n📄 Attempting to attach failure logs to ${targetLabel}...`);
@@ -70,7 +73,7 @@ export const handleFailure = async options => {
70
73
  tool: argv.tool || 'claude',
71
74
  });
72
75
  if (logUploadSuccess) {
73
- await log(`📎 Failure log attached to ${targetLabel}`);
76
+ await log(`📎 Failure log posted to ${targetLabel}`);
74
77
  if (!hasPR && hasIssue) global.prePullRequestFailureNotificationPosted = true;
75
78
  }
76
79
  } catch (attachError) {
@@ -81,7 +84,7 @@ export const handleFailure = async options => {
81
84
  errorType,
82
85
  operation: `attach_log_to_${targetType}`,
83
86
  });
84
- await log(`⚠️ Could not attach failure log to ${targetLabel}: ${attachError.message}`, { level: 'warning' });
87
+ await log(`⚠️ Could not post failure log to ${targetLabel}: ${attachError.message}`, { level: 'warning' });
85
88
  }
86
89
  }
87
90
  }
package/src/solve.mjs CHANGED
@@ -162,6 +162,12 @@ const { isIssueUrl, isPrUrl, normalizedUrl, owner, repo, number: urlNumber } = u
162
162
  issueUrl = normalizedUrl || issueUrl;
163
163
  global.owner = owner;
164
164
  global.repo = repo;
165
+ // Issue #1752: failures before PR creation can happen during checks that run
166
+ // before the normal issue-mode setup below. Record the source issue as soon as
167
+ // the URL is validated so the pre-exit notifier can still comment on it.
168
+ if (isIssueUrl) {
169
+ global.issueNumber = urlNumber;
170
+ }
165
171
  cleanupContext.owner = owner;
166
172
  cleanupContext.repo = repo;
167
173
  // Setup unhandled error handlers to ensure log path is always shown
@@ -331,6 +337,7 @@ if (autoContinueResult.isContinueMode) {
331
337
  } else {
332
338
  // We have a branch but no PR - we'll use the existing branch and create a PR later
333
339
  await log(`🔄 Using existing branch: ${prBranch} (no PR yet - will create one)`);
340
+ await log(' This branch was created by an earlier run; this run is reusing it rather than creating a fresh branch.');
334
341
  if (argv.verbose) {
335
342
  await log(' Branch will be checked out and PR will be created during auto-PR creation phase', {
336
343
  verbose: true,
@@ -1021,7 +1028,7 @@ try {
1021
1028
  const hasIssue = global.issueNumber;
1022
1029
  const logTargetType = hasPR ? 'pr' : hasIssue ? 'issue' : null;
1023
1030
  const logTargetNumber = hasPR ? global.createdPR.number : hasIssue ? global.issueNumber : null;
1024
- const logTargetLabel = hasPR ? 'Pull Request' : 'Issue';
1031
+ const logTargetLabel = hasPR ? 'Pull Request' : `original issue #${logTargetNumber}`;
1025
1032
 
1026
1033
  if (shouldAttachLogs && logTargetType && logTargetNumber) {
1027
1034
  await log(`\n📄 Attaching failure logs to ${logTargetLabel}...`);
@@ -1054,7 +1061,7 @@ try {
1054
1061
  });
1055
1062
 
1056
1063
  if (logUploadSuccess) {
1057
- await log(` 📎 Failure logs attached to ${logTargetLabel}`);
1064
+ await log(` 📎 Failure logs posted to ${logTargetLabel}`);
1058
1065
  } else {
1059
1066
  // Issue #1212: Always show log upload failures (not just verbose)
1060
1067
  await log(' ⚠️ Failed to upload failure logs');