@link-assistant/hive-mind 1.69.10 → 1.69.11

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.69.11
4
+
5
+ ### Patch Changes
6
+
7
+ - bdca974: Notify existing pull requests when solver pre-exit failures happen before a working session can post its own failure comment.
8
+
3
9
  ## 1.69.10
4
10
 
5
11
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.69.10",
3
+ "version": "1.69.11",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -35,6 +35,11 @@ const buildIssueFailureActionSection = targetType => {
35
35
 
36
36
  Administrator-only CLI details, if any, are printed in the solver terminal log rather than in this issue comment.`;
37
37
  };
38
+ const normalizeFailureActionSection = section => {
39
+ const text = section || '';
40
+ if (!text) return '';
41
+ return text.startsWith('\n') ? text : `\n\n${text}`;
42
+ };
38
43
  export const checkFileInBranch = async (owner, repo, fileName, branchName) => {
39
44
  const { $ } = await use('command-stream');
40
45
 
@@ -353,6 +358,7 @@ export async function attachLogToGitHub(options) {
353
358
  tool = null, // The tool used (claude, agent, opencode, codex)
354
359
  resultModelUsage = null, // Issue #1454
355
360
  budgetStatsData = null, // Issue #1491: budget stats for comment
361
+ failureActionSection = null,
356
362
  } = options;
357
363
  const budgetStats = budgetStatsData ? buildBudgetStatsString(budgetStatsData.tokenUsage, budgetStatsData.subAgentCalls) : '';
358
364
  const targetName = targetType === 'pr' ? 'Pull Request' : 'Issue';
@@ -444,6 +450,7 @@ export async function attachLogToGitHub(options) {
444
450
  await log(' šŸ”§ Escaping code blocks in log content for safe embedding...', { verbose: true });
445
451
  }
446
452
  logContent = escapeCodeBlocksInLog(logContent);
453
+ const failureAction = normalizeFailureActionSection(failureActionSection ?? buildIssueFailureActionSection(targetType));
447
454
  // Create formatted comment
448
455
  let logComment;
449
456
  // Usage limit comments should be shown whenever isUsageLimit is true,
@@ -520,7 +527,7 @@ ${footerNote}`;
520
527
  The automated solution draft encountered an error:
521
528
  \`\`\`
522
529
  ${errorMessage}
523
- \`\`\`${buildIssueFailureActionSection(targetType)}${modelInfoString}
530
+ \`\`\`${failureAction}${modelInfoString}
524
531
 
525
532
  <details>
526
533
  <summary>Click to expand failure log (${Math.round(logStats.size / 1024)}KB)</summary>
@@ -718,7 +725,7 @@ ${uploadFooterNote}`;
718
725
  The automated solution draft encountered an error:
719
726
  \`\`\`
720
727
  ${errorMessage}
721
- \`\`\`${buildIssueFailureActionSection(targetType)}${modelInfoString}
728
+ \`\`\`${failureAction}${modelInfoString}
722
729
 
723
730
  ### šŸ“Ž **Failure log uploaded as ${uploadTypeLabel}${chunkInfo}** (${Math.round(logStats.size / 1024)}KB)
724
731
  - [View complete failure log](${logUrl})
package/src/solve.mjs CHANGED
@@ -148,6 +148,14 @@ const cleanupWrapper = async () => {
148
148
  const interruptWrapper = createInterruptWrapper({ cleanupContext, checkForUncommittedChanges, shouldAttachLogs, attachLogToGitHub, getLogFile, sanitizeLogContent, $, log });
149
149
  initializeExitHandler(getAbsoluteLogPath, log, cleanupWrapper, interruptWrapper, ({ code, reason }) => notifyIssueAboutPrePullRequestFailure({ code, reason, argv, globalState: global, $, log, getLogFile, shouldAttachLogs, attachLogToGitHub, sanitizeLogContent, rawCommand }));
150
150
  installGlobalExitHandlers();
151
+ const markFailureNotificationPosted = targetType => {
152
+ global.preExitFailureNotificationPosted = true;
153
+ if (targetType === 'pr') {
154
+ global.pullRequestFailureNotificationPosted = true;
155
+ } else if (targetType === 'issue') {
156
+ global.prePullRequestFailureNotificationPosted = true;
157
+ }
158
+ };
151
159
 
152
160
  // Now handle argument validation that was moved from early checks
153
161
  let issueUrl = argv['issue-url'] || argv._[0];
@@ -900,6 +908,7 @@ try {
900
908
  });
901
909
 
902
910
  if (logUploadSuccess) {
911
+ markFailureNotificationPosted('pr');
903
912
  await log(' āœ… Logs uploaded successfully');
904
913
  } else {
905
914
  // Issue #1212: Always show log upload failures (not just verbose)
@@ -924,6 +933,7 @@ try {
924
933
 
925
934
  const posted = await postTrackedComment({ $, owner, repo, targetNumber: prNumber, body: failureComment });
926
935
  if (posted.ok) {
936
+ markFailureNotificationPosted('pr');
927
937
  await log(` Posted failure comment to PR${posted.commentId ? ` (id=${posted.commentId})` : ''}`);
928
938
  }
929
939
  } catch (error) {
@@ -1078,6 +1088,7 @@ try {
1078
1088
  });
1079
1089
 
1080
1090
  if (logUploadSuccess) {
1091
+ markFailureNotificationPosted(logTargetType);
1081
1092
  await log(` šŸ“Ž Failure logs posted to ${logTargetLabel}`);
1082
1093
  } else {
1083
1094
  // Issue #1212: Always show log upload failures (not just verbose)
@@ -1,5 +1,7 @@
1
1
  import { getTrackedToolCommentIds, postTrackedComment, SOLUTION_DRAFT_FAILED_MARKER } from './tool-comments.lib.mjs';
2
2
 
3
+ export const FORK_DIVERGENCE_RESOLUTION_OPTION = '--allow-fork-divergence-resolution-using-force-push-with-lease';
4
+
3
5
  const truncate = (value, maxLength = 2000) => {
4
6
  const text = value === null || value === undefined ? '' : String(value);
5
7
  if (text.length <= maxLength) return text;
@@ -8,10 +10,24 @@ const truncate = (value, maxLength = 2000) => {
8
10
 
9
11
  const fence = value => truncate(value || 'Unknown error').replaceAll('```', '` ` `');
10
12
 
13
+ const isForkDivergenceFailure = reason => {
14
+ const normalizedReason = String(reason || '').toLowerCase();
15
+ return normalizedReason.includes('fork divergence') || (normalizedReason.includes('fork') && normalizedReason.includes('non-fast-forward')) || normalizedReason.includes('force-with-lease');
16
+ };
17
+
11
18
  export function buildPrePullRequestFailureActionSection(reason = '') {
12
19
  const normalizedReason = String(reason || '').toLowerCase();
13
20
  const isForkOrRecoveryFailure = normalizedReason.includes('fork') || normalizedReason.includes('auto-recovery') || normalizedReason.includes('repository setup');
14
21
 
22
+ if (isForkDivergenceFailure(reason)) {
23
+ return `### What you can do
24
+ - If the fork's default branch can be overwritten safely, rerun with \`${FORK_DIVERGENCE_RESOLUTION_OPTION}\` to allow a guarded force-with-lease sync.
25
+ - If the fork has commits you need to preserve, resolve the divergence manually, then rerun the solver.
26
+ - If this requires elevated Hive Mind access, ask a Hive Mind administrator to handle the affected fork or repository.
27
+
28
+ Administrator-only CLI details, if any, are printed in the solver terminal log rather than in this GitHub comment.`;
29
+ }
30
+
15
31
  if (isForkOrRecoveryFailure) {
16
32
  return `### What you can do
17
33
  - If the affected fork or repository belongs to you, remove, rename, archive, initialize, or otherwise repair it in GitHub, then rerun the solver.
@@ -36,6 +52,42 @@ export function shouldNotifyIssueAboutPrePullRequestFailure({ code, globalState
36
52
  return getTrackedToolCommentIds().size === 0;
37
53
  }
38
54
 
55
+ export function resolvePreExitFailureNotificationTarget({ code, globalState }) {
56
+ if (code === 0) return null;
57
+ if (!globalState?.owner || !globalState?.repo) return null;
58
+ if (globalState.preExitFailureNotificationPosted || globalState.preExitFailureNotificationInProgress) return null;
59
+
60
+ const owner = globalState.owner;
61
+ const repo = globalState.repo;
62
+ const issueNumber = globalState.issueNumber || null;
63
+ const prNumber = globalState.createdPR?.number || globalState.prNumber || null;
64
+
65
+ if (prNumber) {
66
+ if (globalState.pullRequestFailureNotificationPosted || globalState.pullRequestFailureNotificationInProgress) return null;
67
+ return {
68
+ targetType: 'pr',
69
+ targetNumber: prNumber,
70
+ owner,
71
+ repo,
72
+ issueNumber,
73
+ prNumber,
74
+ };
75
+ }
76
+
77
+ if (!issueNumber) return null;
78
+ if (globalState.prePullRequestFailureNotificationPosted || globalState.prePullRequestFailureNotificationInProgress) return null;
79
+ if (getTrackedToolCommentIds().size !== 0) return null;
80
+
81
+ return {
82
+ targetType: 'issue',
83
+ targetNumber: issueNumber,
84
+ owner,
85
+ repo,
86
+ issueNumber,
87
+ prNumber: null,
88
+ };
89
+ }
90
+
39
91
  export function buildPrePullRequestFailureComment({ reason, owner, repo, issueNumber, argv = {}, logAttachmentAttempted = false }) {
40
92
  const tool = argv.tool || 'claude';
41
93
  const modelLine = argv.model ? `\n- **Requested model**: \`${argv.model}\`` : '';
@@ -62,60 +114,124 @@ ${logLine}
62
114
  `;
63
115
  }
64
116
 
117
+ export function buildExistingPullRequestFailureComment({ reason, owner, repo, prNumber, issueNumber = null, argv = {}, logAttachmentAttempted = false }) {
118
+ const tool = argv.tool || 'claude';
119
+ const modelLine = argv.model ? `\n- **Requested model**: \`${argv.model}\`` : '';
120
+ const issueLine = issueNumber ? `\n- **Linked issue**: #${issueNumber}` : '';
121
+ const logLine = logAttachmentAttempted ? 'Log attachment was attempted but failed. Check the solver terminal log for the complete failure output.' : 'Logs were not attached because `--attach-logs` was not enabled.';
122
+ const actionSection = buildPrePullRequestFailureActionSection(reason);
123
+
124
+ return `## 🚨 ${SOLUTION_DRAFT_FAILED_MARKER}
125
+
126
+ The automated solver stopped while continuing this existing pull request, so the failure details are posted here for review.
127
+
128
+ ### Failure
129
+ - **Repository**: \`${owner}/${repo}\`
130
+ - **Pull request**: #${prNumber}${issueLine}
131
+ - **Tool**: \`${tool}\`${modelLine}
132
+
133
+ **Reason**
134
+ \`\`\`text
135
+ ${fence(reason)}
136
+ \`\`\`
137
+
138
+ ${actionSection}
139
+
140
+ ${logLine}
141
+ `;
142
+ }
143
+
144
+ const markNotificationPosted = ({ globalState, targetType }) => {
145
+ globalState.preExitFailureNotificationPosted = true;
146
+ if (targetType === 'pr') {
147
+ globalState.pullRequestFailureNotificationPosted = true;
148
+ } else {
149
+ globalState.prePullRequestFailureNotificationPosted = true;
150
+ }
151
+ };
152
+
65
153
  export async function notifyIssueAboutPrePullRequestFailure(options) {
66
154
  const { code, reason, argv = {}, globalState = globalThis, $, log = async () => {}, getLogFile, shouldAttachLogs = false, attachLogToGitHub, sanitizeLogContent, rawCommand = null, postComment = postTrackedComment } = options;
67
155
 
68
- if (!shouldNotifyIssueAboutPrePullRequestFailure({ code, globalState })) {
156
+ const target = resolvePreExitFailureNotificationTarget({ code, globalState });
157
+ if (!target) {
69
158
  return { notified: false, skipped: true };
70
159
  }
71
160
 
72
- const owner = globalState.owner;
73
- const repo = globalState.repo;
74
- const issueNumber = globalState.issueNumber;
75
- globalState.prePullRequestFailureNotificationInProgress = true;
161
+ const { owner, repo, issueNumber, prNumber, targetType, targetNumber } = target;
162
+ const targetLabel = targetType === 'pr' ? `pull request #${targetNumber}` : `issue #${targetNumber}`;
163
+ globalState.preExitFailureNotificationInProgress = true;
164
+ if (targetType === 'pr') {
165
+ globalState.pullRequestFailureNotificationInProgress = true;
166
+ } else {
167
+ globalState.prePullRequestFailureNotificationInProgress = true;
168
+ }
76
169
 
77
170
  try {
78
171
  if (shouldAttachLogs && getLogFile && attachLogToGitHub && sanitizeLogContent) {
79
- await log(`\nšŸ“„ Notifying issue #${issueNumber} about pre-PR failure with logs...`);
80
- const uploaded = await attachLogToGitHub({
81
- logFile: getLogFile(),
82
- targetType: 'issue',
83
- targetNumber: issueNumber,
84
- owner,
85
- repo,
86
- $,
87
- log,
88
- sanitizeLogContent,
89
- verbose: argv.verbose,
90
- errorMessage: `The solver stopped before creating a pull request.\n\nReason: ${reason || 'Unknown error'}`,
91
- requestedModel: argv.originalModel || argv.model,
92
- tool: argv.tool || 'claude',
93
- });
94
- if (uploaded) {
95
- globalState.prePullRequestFailureNotificationPosted = true;
96
- return { notified: true, method: 'log-upload' };
172
+ await log(`\nšŸ“„ Notifying ${targetLabel} about solver failure with logs...`);
173
+ const errorPrefix = targetType === 'pr' ? `The solver stopped while continuing pull request #${targetNumber}.` : 'The solver stopped before creating a pull request.';
174
+ try {
175
+ const uploaded = await attachLogToGitHub({
176
+ logFile: getLogFile(),
177
+ targetType,
178
+ targetNumber,
179
+ owner,
180
+ repo,
181
+ $,
182
+ log,
183
+ sanitizeLogContent,
184
+ verbose: argv.verbose,
185
+ errorMessage: `${errorPrefix}\n\nReason: ${reason || 'Unknown error'}`,
186
+ failureActionSection: buildPrePullRequestFailureActionSection(reason),
187
+ requestedModel: argv.originalModel || argv.model,
188
+ tool: argv.tool || 'claude',
189
+ });
190
+ if (uploaded) {
191
+ markNotificationPosted({ globalState, targetType });
192
+ return { notified: true, method: 'log-upload' };
193
+ }
194
+ } catch (error) {
195
+ const message = error && error.message ? error.message : String(error);
196
+ await log(` āš ļø Could not upload solver failure log: ${message}`, { level: 'warning' });
97
197
  }
98
198
  }
99
199
 
100
- await log(`\nšŸ’¬ Notifying issue #${issueNumber} about pre-PR failure...`);
101
- const body = buildPrePullRequestFailureComment({
102
- reason,
103
- owner,
104
- repo,
105
- issueNumber,
106
- argv,
107
- rawCommand,
108
- logAttachmentAttempted: shouldAttachLogs,
109
- });
110
- const posted = await postComment({ $, owner, repo, targetNumber: issueNumber, body });
200
+ await log(`\nšŸ’¬ Notifying ${targetLabel} about solver failure...`);
201
+ const body =
202
+ targetType === 'pr'
203
+ ? buildExistingPullRequestFailureComment({
204
+ reason,
205
+ owner,
206
+ repo,
207
+ prNumber,
208
+ issueNumber,
209
+ argv,
210
+ rawCommand,
211
+ logAttachmentAttempted: shouldAttachLogs,
212
+ })
213
+ : buildPrePullRequestFailureComment({
214
+ reason,
215
+ owner,
216
+ repo,
217
+ issueNumber,
218
+ argv,
219
+ rawCommand,
220
+ logAttachmentAttempted: shouldAttachLogs,
221
+ });
222
+ const posted = await postComment({ $, owner, repo, targetNumber, body });
111
223
  if (posted.ok) {
112
- globalState.prePullRequestFailureNotificationPosted = true;
113
- await log(` āœ… Pre-PR failure comment posted to issue #${issueNumber}${posted.commentId ? ` (id=${posted.commentId})` : ''}`);
224
+ markNotificationPosted({ globalState, targetType });
225
+ await log(` āœ… Solver failure comment posted to ${targetLabel}${posted.commentId ? ` (id=${posted.commentId})` : ''}`);
114
226
  return { notified: true, method: 'comment', commentId: posted.commentId || null };
115
227
  }
116
- await log(` āš ļø Could not post pre-PR failure comment: ${posted.stderr || 'unknown error'}`, { level: 'warning' });
228
+ await log(` āš ļø Could not post solver failure comment: ${posted.stderr || 'unknown error'}`, { level: 'warning' });
117
229
  return { notified: false, error: posted.stderr || 'unknown error' };
118
230
  } finally {
231
+ globalState.preExitFailureNotificationInProgress = false;
232
+ if (targetType === 'pr') {
233
+ globalState.pullRequestFailureNotificationInProgress = false;
234
+ }
119
235
  globalState.prePullRequestFailureNotificationInProgress = false;
120
236
  }
121
237
  }