@link-assistant/hive-mind 2.11.12 → 2.12.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.
@@ -23,13 +23,16 @@ if (typeof globalThis.use === 'undefined') {
23
23
  }
24
24
 
25
25
  import { log } from '../lib.mjs';
26
+ import { FORMAL_AI_MODEL_ALIAS, FORMAL_AI_PROVIDER_MODEL_ID, isFormalAiModel } from '../formal-ai-model.lib.mjs';
26
27
 
27
28
  const execFileAsync = promisify(execFile);
28
29
 
29
30
  // ─── MODEL DATA ──────────────────────────────────────────────────────────────
30
31
 
31
- export const FORMAL_AI_MODEL_ALIAS = 'formal-ai';
32
- export const FORMAL_AI_PROVIDER_MODEL_ID = 'formalai/formal-ai';
32
+ // Defined in a leaf module so callers that only need the identity check (the
33
+ // Formal AI sidecar lifecycle, issue #2146) do not have to import this catalogue
34
+ // and its `use-m` bootstrap. Re-exported here so the public surface is unchanged.
35
+ export { FORMAL_AI_MODEL_ALIAS, FORMAL_AI_PROVIDER_MODEL_ID, isFormalAiModel } from '../formal-ai-model.lib.mjs';
33
36
 
34
37
  const formalAiNativeModelAliases = {
35
38
  [FORMAL_AI_MODEL_ALIAS]: FORMAL_AI_MODEL_ALIAS,
@@ -41,8 +44,6 @@ const formalAiProviderModelAliases = {
41
44
  [FORMAL_AI_PROVIDER_MODEL_ID]: FORMAL_AI_PROVIDER_MODEL_ID,
42
45
  };
43
46
 
44
- export const isFormalAiModel = model => model === FORMAL_AI_MODEL_ALIAS || model === FORMAL_AI_PROVIDER_MODEL_ID;
45
-
46
47
  // Claude models (Anthropic API)
47
48
  // Updated for Opus 4.5/4.6/4.7/4.8/5, Sonnet 4.6/5, and Fable 5 / Mythos 5 support
48
49
  // (Issue #1221, Issue #1238, Issue #1329, Issue #1433, Issue #1620, Issue #1832, Issue #1875, Issue #2003, Issue #2096)
@@ -0,0 +1,245 @@
1
+ #!/usr/bin/env node
2
+ import { ensureUseM } from './use-m-bootstrap.lib.mjs';
3
+
4
+ /**
5
+ * One-shot `--auto-merge` attempt after a session ends.
6
+ *
7
+ * Extracted from solve.auto-merge.lib.mjs (Issue #2144) to keep both files
8
+ * under the 1500-line limit while the stop-reporting paths were added.
9
+ *
10
+ * Issue #2144 behaviour: a closed or missing linked issue is *not* a terminal
11
+ * state here either. The merge requirements are still evaluated, and only the
12
+ * final merge is held back — with a comment asking the user to reopen the
13
+ * issue or merge manually. Every other stop path reports its exact reason to
14
+ * the pull request.
15
+ *
16
+ * @see https://github.com/link-assistant/hive-mind/issues/2144
17
+ */
18
+
19
+ if (typeof globalThis.use === 'undefined') {
20
+ await ensureUseM();
21
+ }
22
+ const use = globalThis.use;
23
+
24
+ const { $: __rawDollar$ } = await use('command-stream');
25
+ const { wrapDollarWithGhRetry } = await import('./github-rate-limit.lib.mjs');
26
+ const $ = wrapDollarWithGhRetry(__rawDollar$);
27
+
28
+ const lib = await import('./lib.mjs');
29
+ const { log, formatAligned } = lib;
30
+
31
+ const githubMergeLib = await import('./github-merge.lib.mjs');
32
+ const { checkPRMergeable, checkMergePermissions, mergePullRequest, waitForCI } = githubMergeLib;
33
+
34
+ const terminalStateLib = await import('./github-terminal-state.lib.mjs');
35
+ const { checkGitHubTerminalState } = terminalStateLib;
36
+
37
+ // Issue #2144: these probes answer with a ~33 KB pull request object and a full
38
+ // issue object on every iteration. Issue #2130 made the helper's own default
39
+ // runner quiet, but passing `$` here bypassed it and the payloads were still
40
+ // mirrored into the attached log. Bind the quiet options to the injected `$`.
41
+ const { quietProbe } = await import('./quiet-probe.lib.mjs');
42
+
43
+ const toolComments = await import('./tool-comments.lib.mjs');
44
+ const { AUTO_MERGED_MARKER, postTrackedComment } = toolComments;
45
+
46
+ const stopReporting = await import('./automation-stop-reporting.lib.mjs');
47
+ const { AUTO_MERGE_BLOCKED_MARKER, buildAutoMergeBlockedComment, reportAutomationStop } = stopReporting;
48
+
49
+ const { ensureLinkedIssueClosedAfterMerge } = await import('./github-issue-auto-close.lib.mjs');
50
+
51
+ const shouldDeleteBranchAfterMerge = argv => argv.autoDeleteBranchOnMerge || argv.deleteBranchAfterMerge || false;
52
+
53
+ /**
54
+ * Report the merge blockers that prevent an automatic merge of a pull request
55
+ * which otherwise satisfies every merge requirement (Issue #2144).
56
+ *
57
+ * @returns {Promise<{posted: boolean, reason: string, skipped?: string, error?: string}>}
58
+ */
59
+ export const reportAutoMergeBlockedByIssue = async ({ owner, repo, prNumber, issueNumber, mergeBlockers, verbose = false, commandRunner = $ }) => {
60
+ const blockers = (mergeBlockers || []).filter(Boolean);
61
+ if (blockers.length === 0) {
62
+ return { posted: false, reason: 'no_blockers', skipped: 'no_blockers' };
63
+ }
64
+
65
+ await log('');
66
+ await log(formatAligned('⚠️', 'AUTO-MERGE HELD BACK:', blockers.map(b => b.message).join('; '), 2), { level: 'warning' });
67
+ for (const blocker of blockers) {
68
+ if (blocker.resolution) {
69
+ await log(formatAligned('', 'Action:', blocker.resolution, 4), { level: 'warning' });
70
+ }
71
+ }
72
+
73
+ return reportAutomationStop({
74
+ $: commandRunner,
75
+ owner,
76
+ repo,
77
+ targetNumber: prNumber,
78
+ reason: blockers[0].reason,
79
+ mode: 'auto-merge',
80
+ verbose,
81
+ log,
82
+ body: buildAutoMergeBlockedComment({ blockers, issueNumber }),
83
+ signature: AUTO_MERGE_BLOCKED_MARKER,
84
+ });
85
+ };
86
+
87
+ /**
88
+ * Attempt to auto-merge a PR after the session ends.
89
+ * Implements the one-shot `--auto-merge` path.
90
+ */
91
+ export const attemptAutoMerge = async params => {
92
+ const { owner, repo, prNumber, issueNumber = null, argv } = params;
93
+
94
+ await log('');
95
+ await log(formatAligned('🔀', 'AUTO-MERGE:', 'Checking if PR can be merged...'));
96
+
97
+ const terminalState = await checkGitHubTerminalState({
98
+ owner,
99
+ repo,
100
+ issueNumber,
101
+ prNumber,
102
+ commandRunner: quietProbe($),
103
+ });
104
+ if (terminalState.terminal) {
105
+ if (terminalState.success) {
106
+ await log(formatAligned('🎉', 'PR already merged:', `#${prNumber}`, 2));
107
+ return { success: true, reason: 'merged' };
108
+ }
109
+ await log(formatAligned('❌', 'GITHUB TARGET UNAVAILABLE:', terminalState.message, 2), { level: 'error' });
110
+ for (const detail of terminalState.details || []) {
111
+ await log(formatAligned('', 'Detail:', detail, 4), { level: 'error' });
112
+ }
113
+ // Issue #2144: never stop silently — publish the exact reason.
114
+ await reportAutomationStop({
115
+ $,
116
+ owner,
117
+ repo,
118
+ targetNumber: prNumber,
119
+ reason: terminalState.reason,
120
+ mode: 'auto-merge',
121
+ message: terminalState.message,
122
+ details: terminalState.details,
123
+ verbose: argv.verbose,
124
+ log,
125
+ });
126
+ return { success: false, reason: terminalState.reason, error: terminalState.message };
127
+ }
128
+
129
+ // Issue #2144: a closed/unavailable linked issue blocks only the merge step.
130
+ const issueMergeBlockers = terminalState.mergeBlockers || [];
131
+
132
+ // Issue #1226: Check merge permissions before attempting
133
+ const { canMerge, permission } = await checkMergePermissions(owner, repo, argv.verbose);
134
+ if (!canMerge) {
135
+ await log(formatAligned('⚠️', 'Cannot merge:', `Insufficient permissions (${permission || 'unknown'})`, 2));
136
+ return { success: false, reason: 'insufficient_permissions', error: `User has ${permission || 'unknown'} access, needs push/maintain/admin` };
137
+ }
138
+
139
+ // Wait for CI to complete (with timeout)
140
+ const ciWaitResult = await waitForCI(
141
+ owner,
142
+ repo,
143
+ prNumber,
144
+ {
145
+ timeout: argv.autoMergeCiTimeout || 30 * 60 * 1000, // 30 minutes default
146
+ pollInterval: argv.autoMergeCiPollInterval || 30 * 1000, // 30 seconds default
147
+ onStatusUpdate: async status => {
148
+ if (argv.verbose) {
149
+ await log(` CI status: ${status.status}`, { verbose: true });
150
+ }
151
+ },
152
+ },
153
+ argv.verbose
154
+ );
155
+
156
+ if (!ciWaitResult.success) {
157
+ await log(formatAligned('⚠️', 'CI check failed or timed out:', ciWaitResult.error || ciWaitResult.status, 2));
158
+ return { success: false, reason: ciWaitResult.status, error: ciWaitResult.error };
159
+ }
160
+
161
+ await log(formatAligned('✅', 'CI checks passed:', 'Checking mergeability...', 2));
162
+
163
+ // Check if PR is mergeable
164
+ const mergeStatus = await checkPRMergeable(owner, repo, prNumber, argv.verbose);
165
+ if (mergeStatus.terminal) {
166
+ await log(formatAligned('❌', 'GITHUB TARGET UNAVAILABLE:', mergeStatus.reason || 'GitHub repository, pull request, issue, or branch is no longer accessible', 2), { level: 'error' });
167
+ await reportAutomationStop({
168
+ $,
169
+ owner,
170
+ repo,
171
+ targetNumber: prNumber,
172
+ reason: 'terminal_github_entity_error',
173
+ mode: 'auto-merge',
174
+ message: mergeStatus.reason,
175
+ verbose: argv.verbose,
176
+ log,
177
+ });
178
+ return { success: false, reason: 'terminal_github_entity_error', error: mergeStatus.reason };
179
+ }
180
+
181
+ if (!mergeStatus.mergeable) {
182
+ await log(formatAligned('⚠️', 'PR not mergeable:', mergeStatus.reason || 'Unknown reason', 2));
183
+ return { success: false, reason: 'not_mergeable', error: mergeStatus.reason };
184
+ }
185
+
186
+ // Issue #2144: the pull request is ready. If the linked issue is closed or
187
+ // gone, do not merge automatically — ask the user to reopen it or merge
188
+ // manually, and say so on the pull request.
189
+ if (issueMergeBlockers.length > 0) {
190
+ await reportAutoMergeBlockedByIssue({ owner, repo, prNumber, issueNumber, mergeBlockers: issueMergeBlockers, verbose: argv.verbose });
191
+ return { success: false, reason: issueMergeBlockers[0].reason, error: issueMergeBlockers[0].message, mergeBlockers: issueMergeBlockers };
192
+ }
193
+
194
+ await log(formatAligned('✅', 'PR is mergeable:', 'Attempting to merge...', 2));
195
+
196
+ // Attempt to merge
197
+ const deleteAfterMerge = shouldDeleteBranchAfterMerge(argv);
198
+ if (deleteAfterMerge) {
199
+ await log(formatAligned('', 'Branch cleanup:', 'will delete branch after successful merge', 2));
200
+ }
201
+ const mergeResult = await mergePullRequest(owner, repo, prNumber, { squash: argv.squash || false, deleteAfter: deleteAfterMerge }, argv.verbose);
202
+
203
+ if (mergeResult.success) {
204
+ await log(formatAligned('🎉', 'PR MERGED SUCCESSFULLY!', ''));
205
+
206
+ // Post success comment
207
+ try {
208
+ const commentBody = `## 🎉 ${AUTO_MERGED_MARKER}\n\nThis pull request has been automatically merged by hive-mind after all CI checks passed and the PR became mergeable.\n\n---\n*Auto-merged by hive-mind with --auto-merge flag*`;
209
+ await postTrackedComment({ $, owner, repo, targetNumber: prNumber, body: commentBody });
210
+ } catch {
211
+ // Don't fail if comment posting fails
212
+ }
213
+
214
+ // Issue #1895: close linked issue explicitly when GitHub will not (non-default base branch).
215
+ try {
216
+ const closeResult = await ensureLinkedIssueClosedAfterMerge({ $, log, owner, repo, prNumber, issueNumber, verbose: argv.verbose });
217
+ if (!closeResult.closed && !closeResult.skipped) {
218
+ await log(formatAligned('⚠️', 'Issue auto-close:', `could not close linked issue (${closeResult.reason})`, 2), { level: 'warning' });
219
+ }
220
+ } catch (closeError) {
221
+ await log(formatAligned('⚠️', 'Issue auto-close:', `error: ${closeError.message}`, 2), { level: 'warning' });
222
+ }
223
+
224
+ return { success: true, reason: 'merged' };
225
+ } else {
226
+ await log(formatAligned('⚠️', 'Merge failed:', mergeResult.error || 'Unknown error', 2));
227
+ await reportAutomationStop({
228
+ $,
229
+ owner,
230
+ repo,
231
+ targetNumber: prNumber,
232
+ reason: 'merge_failed',
233
+ mode: 'auto-merge',
234
+ message: mergeResult.error || 'GitHub rejected the merge request.',
235
+ verbose: argv.verbose,
236
+ log,
237
+ });
238
+ return { success: false, reason: 'merge_failed', error: mergeResult.error };
239
+ }
240
+ };
241
+
242
+ export default {
243
+ attemptAutoMerge,
244
+ reportAutoMergeBlockedByIssue,
245
+ };
@@ -35,7 +35,7 @@ const { reportError } = sentryLib;
35
35
 
36
36
  // Import GitHub merge functions
37
37
  const githubMergeLib = await import('./github-merge.lib.mjs');
38
- const { checkPRMergeable, checkMergePermissions, mergePullRequest, waitForCI, getRepoVisibility, BILLING_LIMIT_ERROR_PATTERN, getDetailedCIStatus, rerunWorkflowRun, getWorkflowRunsForSha, getAllActiveRepoRuns, checkCIConsensus } = githubMergeLib;
38
+ const { checkMergePermissions, mergePullRequest, getRepoVisibility, BILLING_LIMIT_ERROR_PATTERN, getDetailedCIStatus, rerunWorkflowRun, getWorkflowRunsForSha, getAllActiveRepoRuns, checkCIConsensus } = githubMergeLib;
39
39
 
40
40
  // Import GitHub functions for log attachment
41
41
  const githubLib = await import('./github.lib.mjs');
@@ -50,6 +50,18 @@ const { checkForUncommittedChanges, getUncommittedChangesDetails, executeToolIte
50
50
  const terminalStateLib = await import('./github-terminal-state.lib.mjs');
51
51
  const { checkGitHubTerminalState } = terminalStateLib;
52
52
 
53
+ // Issue #2144: these probes answer with a ~33 KB pull request object and a full
54
+ // issue object on every iteration. Issue #2130 made the helper's own default
55
+ // runner quiet, but passing `$` here bypassed it and the payloads were still
56
+ // mirrored into the attached log. Bind the quiet options to the injected `$`.
57
+ const { quietProbe } = await import('./quiet-probe.lib.mjs');
58
+
59
+ // Issue #2144: a closed linked issue is NOT terminal — it only blocks the final
60
+ // automatic merge. Every stop of this loop is also published as a GitHub comment
61
+ // stating exactly why it stopped.
62
+ const stopReportingLib = await import('./automation-stop-reporting.lib.mjs');
63
+ const { reportAutomationStop } = stopReportingLib;
64
+
53
65
  // Import validation functions for time parsing (used for usage limit wait)
54
66
  const validation = await import('./solve.validation.lib.mjs');
55
67
  const { calculateWaitTime } = validation;
@@ -184,7 +196,7 @@ export const watchUntilMergeable = async params => {
184
196
  issueNumber,
185
197
  prNumber,
186
198
  sourceBranchName: prBranch || branchName,
187
- commandRunner: $,
199
+ commandRunner: quietProbe($),
188
200
  });
189
201
  if (terminalState.terminal && terminalState.success) {
190
202
  await log('');
@@ -201,9 +213,20 @@ export const watchUntilMergeable = async params => {
201
213
  }
202
214
  await log(formatAligned('', 'Action:', 'Stopping auto-restart-until-mergeable mode', 2), { level: 'error' });
203
215
  await log('');
216
+ // Issue #2144: report the stop on GitHub instead of exiting silently.
217
+ await reportAutomationStop({ $, owner, repo, targetNumber: prNumber, reason: terminalState.reason, mode: 'auto-restart-until-mergeable', message: terminalState.message, details: terminalState.details, verbose: argv.verbose, log });
204
218
  return { success: false, reason: terminalState.reason, latestSessionId, latestAnthropicCost };
205
219
  }
206
220
 
221
+ // Issue #2144: issue-scoped problems (closed / deleted linked issue) never
222
+ // stop this loop. They are carried to the merge decision below.
223
+ const issueMergeBlockers = terminalState.mergeBlockers || [];
224
+ if (issueMergeBlockers.length > 0 && iteration === 1) {
225
+ for (const blocker of issueMergeBlockers) {
226
+ await log(formatAligned('⚠️', 'Linked issue:', `${blocker.message} Continuing to make the pull request mergeable.`, 2), { level: 'warning' });
227
+ }
228
+ }
229
+
207
230
  await log(formatAligned('🔍', `Check #${iteration}:`, currentTime.toLocaleTimeString()));
208
231
 
209
232
  try {
@@ -243,6 +266,7 @@ export const watchUntilMergeable = async params => {
243
266
  }
244
267
  await log(formatAligned('', 'Action:', 'Stopping auto-restart-until-mergeable mode', 2), { level: 'error' });
245
268
  await log('');
269
+ await reportAutomationStop({ $, owner, repo, targetNumber: prNumber, reason: 'terminal_github_entity_error', mode: 'auto-restart-until-mergeable', message: terminalGitHubBlocker.message, details: terminalGitHubBlocker.details, verbose: argv.verbose, log });
246
270
  return { success: false, reason: 'terminal_github_entity_error', latestSessionId, latestAnthropicCost };
247
271
  }
248
272
 
@@ -356,6 +380,15 @@ export const watchUntilMergeable = async params => {
356
380
 
357
381
  await log(formatAligned('✅', 'PR IS MERGEABLE!', ''));
358
382
 
383
+ // Issue #2144: the pull request is ready. A closed/unavailable linked
384
+ // issue blocks only the *automatic* merge — the loop already did its
385
+ // job of making the pull request mergeable. Ask the user to reopen the
386
+ // issue or merge manually instead of merging behind their back.
387
+ if (isAutoMerge && issueMergeBlockers.length > 0) {
388
+ await reportAutoMergeBlockedByIssue({ owner, repo, prNumber, issueNumber, mergeBlockers: issueMergeBlockers, verbose: argv.verbose });
389
+ return { success: false, reason: issueMergeBlockers[0].reason, mergeBlockers: issueMergeBlockers, latestSessionId, latestAnthropicCost };
390
+ }
391
+
359
392
  if (isAutoMerge) {
360
393
  // Attempt to merge the PR
361
394
  await log(formatAligned('🔀', 'Auto-merging PR...', ''));
@@ -424,7 +457,17 @@ export const watchUntilMergeable = async params => {
424
457
  } else {
425
458
  // Issue #1345: Differentiate message when no CI is configured
426
459
  const ciLine = noCiConfigured ? '- No CI/CD checks are configured for this repository' : noCiTriggered ? (workflowRunConclusions ? `- CI workflows completed without executing (${workflowRunConclusions})` : '- CI workflows exist but were not triggered for this commit') : '- All CI checks have passed';
427
- const commentBody = `## ${READY_TO_MERGE_MARKER}\n\nThis pull request is now ready to be merged:\n${ciLine}\n- No merge conflicts\n- No pending changes\n\n---\n*Monitored by hive-mind with --auto-restart-until-mergeable flag*`;
460
+ // Issue #2144: a closed/unavailable linked issue does not stop this
461
+ // mode, but it is worth stating in the comment so the reader knows
462
+ // why no automatic merge will follow.
463
+ const issueLine =
464
+ issueMergeBlockers.length > 0
465
+ ? `\n\nNote: ${issueMergeBlockers.map(b => b.message).join(' ')} ${issueMergeBlockers
466
+ .map(b => b.resolution)
467
+ .filter(Boolean)
468
+ .join(' ')}`
469
+ : '';
470
+ const commentBody = `## ✅ ${READY_TO_MERGE_MARKER}\n\nThis pull request is now ready to be merged:\n${ciLine}\n- No merge conflicts\n- No pending changes${issueLine}\n\n---\n*Monitored by hive-mind with --auto-restart-until-mergeable flag*`;
428
471
  // Issue #1625: Track this comment ID so it can't falsely count as an AI-authored comment
429
472
  await postTrackedComment({ $, owner, repo, targetNumber: prNumber, body: commentBody });
430
473
  readyToMergeCommentPosted = true;
@@ -838,6 +881,8 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
838
881
  await log(formatAligned('⚠️', 'AUTO-RESUME LIMIT REACHED', `Stopping after ${limitResumeCount} limit-reset continuation${limitResumeCount !== 1 ? 's' : ''}`));
839
882
  await log(formatAligned('', 'Configured limit:', formatAutoIterationLimit(maxAutoResumeIterations), 2));
840
883
  await log('');
884
+ // Issue #2144: publish why the automation stopped.
885
+ await reportAutomationStop({ $, owner, repo, targetNumber: prNumber, reason: 'auto_resume_limit_reached', mode: 'auto-restart-until-mergeable', message: `Stopped after ${limitResumeCount} usage-limit continuation${limitResumeCount !== 1 ? 's' : ''} (limit: ${formatAutoIterationLimit(maxAutoResumeIterations)}).`, verbose: argv.verbose, log });
841
886
  return { success: false, reason: 'auto_resume_limit_reached', latestSessionId, latestAnthropicCost };
842
887
  }
843
888
 
@@ -991,6 +1036,7 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
991
1036
  await log(formatAligned('', `⚠️ Failure log upload error: ${cleanErrorMessage(logUploadError)}`, '', 2));
992
1037
  }
993
1038
  }
1039
+ await reportAutomationStop({ $, owner, repo, targetNumber: prNumber, reason: 'tool_failure_after_resume', mode: 'auto-restart-until-mergeable', message: extractToolErrorCore({ toolResult: resumeResult }) || formatToolExecutionFailure({ tool: argv.tool, toolResult: resumeResult }), verbose: argv.verbose, log });
994
1040
  return { success: false, reason: 'tool_failure_after_resume', latestSessionId, latestAnthropicCost };
995
1041
  }
996
1042
  } else {
@@ -1043,6 +1089,7 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
1043
1089
  await log(formatAligned('', `⚠️ Failure log upload error: ${cleanErrorMessage(logUploadError)}`, '', 2));
1044
1090
  }
1045
1091
  }
1092
+ await reportAutomationStop({ $, owner, repo, targetNumber: prNumber, reason: 'tool_failure', mode: 'auto-restart-until-mergeable', message: extractToolErrorCore({ toolResult }) || formatToolExecutionFailure({ tool: argv.tool, toolResult }), verbose: argv.verbose, log });
1046
1093
  return { success: false, reason: 'tool_failure', latestSessionId, latestAnthropicCost };
1047
1094
  } else {
1048
1095
  // Success - capture latest session data
@@ -1279,114 +1326,10 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
1279
1326
  }
1280
1327
  };
1281
1328
 
1282
- /**
1283
- * Attempt to auto-merge PR after session ends
1284
- * This implements the --auto-merge functionality for one-shot merge attempts
1285
- */
1286
- export const attemptAutoMerge = async params => {
1287
- const { owner, repo, prNumber, issueNumber = null, argv } = params;
1288
-
1289
- await log('');
1290
- await log(formatAligned('🔀', 'AUTO-MERGE:', 'Checking if PR can be merged...'));
1291
-
1292
- const terminalState = await checkGitHubTerminalState({
1293
- owner,
1294
- repo,
1295
- issueNumber,
1296
- prNumber,
1297
- commandRunner: $,
1298
- });
1299
- if (terminalState.terminal) {
1300
- if (terminalState.success) {
1301
- await log(formatAligned('🎉', 'PR already merged:', `#${prNumber}`, 2));
1302
- return { success: true, reason: 'merged' };
1303
- }
1304
- await log(formatAligned('❌', 'GITHUB TARGET UNAVAILABLE:', terminalState.message, 2), { level: 'error' });
1305
- for (const detail of terminalState.details || []) {
1306
- await log(formatAligned('', 'Detail:', detail, 4), { level: 'error' });
1307
- }
1308
- return { success: false, reason: terminalState.reason, error: terminalState.message };
1309
- }
1310
-
1311
- // Issue #1226: Check merge permissions before attempting
1312
- const { canMerge, permission } = await checkMergePermissions(owner, repo, argv.verbose);
1313
- if (!canMerge) {
1314
- await log(formatAligned('⚠️', 'Cannot merge:', `Insufficient permissions (${permission || 'unknown'})`, 2));
1315
- return { success: false, reason: 'insufficient_permissions', error: `User has ${permission || 'unknown'} access, needs push/maintain/admin` };
1316
- }
1317
-
1318
- // Wait for CI to complete (with timeout)
1319
- const ciWaitResult = await waitForCI(
1320
- owner,
1321
- repo,
1322
- prNumber,
1323
- {
1324
- timeout: argv.autoMergeCiTimeout || 30 * 60 * 1000, // 30 minutes default
1325
- pollInterval: argv.autoMergeCiPollInterval || 30 * 1000, // 30 seconds default
1326
- onStatusUpdate: async status => {
1327
- if (argv.verbose) {
1328
- await log(` CI status: ${status.status}`, { verbose: true });
1329
- }
1330
- },
1331
- },
1332
- argv.verbose
1333
- );
1334
-
1335
- if (!ciWaitResult.success) {
1336
- await log(formatAligned('⚠️', 'CI check failed or timed out:', ciWaitResult.error || ciWaitResult.status, 2));
1337
- return { success: false, reason: ciWaitResult.status, error: ciWaitResult.error };
1338
- }
1339
-
1340
- await log(formatAligned('✅', 'CI checks passed:', 'Checking mergeability...', 2));
1341
-
1342
- // Check if PR is mergeable
1343
- const mergeStatus = await checkPRMergeable(owner, repo, prNumber, argv.verbose);
1344
- if (mergeStatus.terminal) {
1345
- await log(formatAligned('❌', 'GITHUB TARGET UNAVAILABLE:', mergeStatus.reason || 'GitHub repository, pull request, issue, or branch is no longer accessible', 2), { level: 'error' });
1346
- return { success: false, reason: 'terminal_github_entity_error', error: mergeStatus.reason };
1347
- }
1348
-
1349
- if (!mergeStatus.mergeable) {
1350
- await log(formatAligned('⚠️', 'PR not mergeable:', mergeStatus.reason || 'Unknown reason', 2));
1351
- return { success: false, reason: 'not_mergeable', error: mergeStatus.reason };
1352
- }
1353
-
1354
- await log(formatAligned('✅', 'PR is mergeable:', 'Attempting to merge...', 2));
1355
-
1356
- // Attempt to merge
1357
- const deleteAfterMerge = shouldDeleteBranchAfterMerge(argv);
1358
- if (deleteAfterMerge) {
1359
- await log(formatAligned('', 'Branch cleanup:', 'will delete branch after successful merge', 2));
1360
- }
1361
- const mergeResult = await mergePullRequest(owner, repo, prNumber, { squash: argv.squash || false, deleteAfter: deleteAfterMerge }, argv.verbose);
1362
-
1363
- if (mergeResult.success) {
1364
- await log(formatAligned('🎉', 'PR MERGED SUCCESSFULLY!', ''));
1365
-
1366
- // Post success comment
1367
- try {
1368
- const commentBody = `## 🎉 ${AUTO_MERGED_MARKER}\n\nThis pull request has been automatically merged by hive-mind after all CI checks passed and the PR became mergeable.\n\n---\n*Auto-merged by hive-mind with --auto-merge flag*`;
1369
- await postTrackedComment({ $, owner, repo, targetNumber: prNumber, body: commentBody });
1370
- } catch {
1371
- // Don't fail if comment posting fails
1372
- }
1373
-
1374
- // Issue #1895: close linked issue explicitly when GitHub will not (non-default base branch).
1375
- try {
1376
- const closeResult = await ensureLinkedIssueClosedAfterMerge({ $, log, owner, repo, prNumber, issueNumber, verbose: argv.verbose });
1377
- if (!closeResult.closed && !closeResult.skipped) {
1378
- await log(formatAligned('⚠️', 'Issue auto-close:', `could not close linked issue (${closeResult.reason})`, 2), { level: 'warning' });
1379
- }
1380
- } catch (closeError) {
1381
- await log(formatAligned('⚠️', 'Issue auto-close:', `error: ${closeError.message}`, 2), { level: 'warning' });
1382
- }
1383
-
1384
- return { success: true, reason: 'merged' };
1385
- } else {
1386
- await log(formatAligned('⚠️', 'Merge failed:', mergeResult.error || 'Unknown error', 2));
1387
- return { success: false, reason: 'merge_failed', error: mergeResult.error };
1388
- }
1389
- };
1329
+ // Issue #2144: the one-shot `--auto-merge` attempt moved to its own module so
1330
+ // both files stay under the 1500-line limit. Re-exported for API compatibility.
1331
+ const autoMergeAttempt = await import('./solve.auto-merge-attempt.lib.mjs');
1332
+ export const { attemptAutoMerge, reportAutoMergeBlockedByIssue } = autoMergeAttempt;
1390
1333
 
1391
1334
  /**
1392
1335
  * Start auto-restart-until-mergeable mode
@@ -68,7 +68,7 @@ const { buildIssueReference, ensureIssueLinkInPullRequestBody } = prIssueLinking
68
68
 
69
69
  // Issue #2119: the one place that decides whether a pull request changed anything.
70
70
  const { formatChangeSummary, getPullRequestChangeStats } = await import('./pull-request-changes.lib.mjs');
71
- const { buildNoChangesNotice, redactWorkspacePaths } = await import('./working-session-summary.lib.mjs');
71
+ const { buildNoChangesNotice, formatWorkingSessionSummaryMarkdown, redactWorkspacePaths } = await import('./working-session-summary.lib.mjs');
72
72
 
73
73
  /**
74
74
  * Placeholder patterns used to detect auto-generated PR content that was not updated by the agent.
@@ -1309,7 +1309,7 @@ export const attachSolutionSummary = async ({ resultSummary, prNumber, issueNumb
1309
1309
  // summary said "The `pwd` command completed" and printed the solver's own
1310
1310
  // /tmp workspace, on a pull request that was still empty.
1311
1311
  const noChangesNotice = buildNoChangesNotice(changeStats);
1312
- const summaryBody = redactWorkspacePaths(resultSummary);
1312
+ const summaryBody = formatWorkingSessionSummaryMarkdown(redactWorkspacePaths(resultSummary));
1313
1313
 
1314
1314
  const comment = `${toolComments.WORKING_SESSION_SUMMARY_AUTOMATION_MARKER}
1315
1315
  ## ${toolComments.WORKING_SESSION_SUMMARY_MARKER}
@@ -44,6 +44,18 @@ const { checkPRMerged, checkForUncommittedChanges, getUncommittedChangesDetails,
44
44
  const terminalStateLib = await import('./github-terminal-state.lib.mjs');
45
45
  const { checkGitHubTerminalState } = terminalStateLib;
46
46
 
47
+ // Issue #2144: these probes answer with a ~33 KB pull request object and a full
48
+ // issue object on every iteration. Issue #2130 made the helper's own default
49
+ // runner quiet, but passing `$` here bypassed it and the payloads were still
50
+ // mirrored into the attached log. Bind the quiet options to the injected `$`.
51
+ const { quietProbe } = await import('./quiet-probe.lib.mjs');
52
+
53
+ // Issue #2144: watch mode must never exit silently — every stop is published as
54
+ // a GitHub comment naming the exact reason. A closed linked issue is not a stop
55
+ // condition here at all.
56
+ const stopReportingLib = await import('./automation-stop-reporting.lib.mjs');
57
+ const { reportAutomationStop } = stopReportingLib;
58
+
47
59
  // Issue #1574: Interruptible sleep so CTRL+C is never blocked by a lingering timer
48
60
  const { interruptibleSleep } = await import('./interruptible-sleep.lib.mjs');
49
61
  // Issue #2119: one auto-restart budget shared with solve.auto-merge.lib.mjs, so
@@ -142,7 +154,7 @@ export const watchForFeedback = async params => {
142
154
  issueNumber,
143
155
  prNumber,
144
156
  sourceBranchName: prBranch || branchName,
145
- commandRunner: $,
157
+ commandRunner: quietProbe($),
146
158
  });
147
159
  if (terminalState.terminal && !terminalState.success) {
148
160
  await log('');
@@ -152,9 +164,19 @@ export const watchForFeedback = async params => {
152
164
  }
153
165
  await log(formatAligned('', 'Action:', 'Stopping watch mode', 2), { level: 'error' });
154
166
  await log('');
167
+ // Issue #2144: report the stop on GitHub instead of exiting silently.
168
+ await reportAutomationStop({ $, owner, repo, targetNumber: prNumber, reason: terminalState.reason, mode: 'watch', message: terminalState.message, details: terminalState.details, verbose: argv.verbose, log });
155
169
  break;
156
170
  }
157
171
 
172
+ // Issue #2144: issue-scoped problems (closed / deleted linked issue) are not
173
+ // watch-mode stop conditions; the loop keeps working on the pull request.
174
+ if ((terminalState.mergeBlockers || []).length > 0 && iteration === 1) {
175
+ for (const blocker of terminalState.mergeBlockers) {
176
+ await log(formatAligned('⚠️', 'Linked issue:', `${blocker.message} Watch mode continues.`, 2), { level: 'warning' });
177
+ }
178
+ }
179
+
158
180
  // Check if PR is merged
159
181
  const isMerged = terminalState.terminal && terminalState.success ? true : await checkPRMerged(owner, repo, prNumber);
160
182
  if (isMerged) {
@@ -472,6 +494,18 @@ export const watchForFeedback = async params => {
472
494
  await log(' 2. You have proper authentication configured');
473
495
  await log(' 3. The API endpoint is accessible');
474
496
  await log('');
497
+ // Issue #2144: say on GitHub why the loop stopped.
498
+ await reportAutomationStop({
499
+ $,
500
+ owner,
501
+ repo,
502
+ targetNumber: prNumber,
503
+ reason: 'tool_failure',
504
+ mode: 'watch',
505
+ message: `${argv.tool.toUpperCase()} failed ${consecutiveApiErrors} times in a row: ${extractToolErrorCore({ toolResult }) || 'unknown API error'}`,
506
+ verbose: argv.verbose,
507
+ log,
508
+ });
475
509
  break; // Exit the watch loop
476
510
  }
477
511