@link-assistant/hive-mind 2.11.13 → 2.12.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/package.json +4 -1
  3. package/src/agent-command.lib.mjs +74 -0
  4. package/src/agent.lib.mjs +59 -34
  5. package/src/agentic-cli-updater.lib.mjs +241 -0
  6. package/src/claude.connection.lib.mjs +209 -0
  7. package/src/claude.lib.mjs +6 -202
  8. package/src/codex.lib.mjs +0 -128
  9. package/src/formal-ai-isolation.lib.mjs +62 -0
  10. package/src/formal-ai-maintenance.lib.mjs +106 -0
  11. package/src/formal-ai-model.lib.mjs +25 -0
  12. package/src/formal-ai-runtime.lib.mjs +10 -0
  13. package/src/formal-ai-sidecar.lib.mjs +565 -0
  14. package/src/formal-ai-updater.lib.mjs +294 -0
  15. package/src/formal-ai-version.lib.mjs +100 -0
  16. package/src/formal-ai.lib.mjs +11 -16
  17. package/src/github-rate-limit.lib.mjs +3 -0
  18. package/src/github-url-parser.lib.mjs +255 -0
  19. package/src/github.lib.mjs +22 -343
  20. package/src/hive.mjs +0 -152
  21. package/src/interactive-mode.lib.mjs +0 -43
  22. package/src/isolation-runner.lib.mjs +44 -173
  23. package/src/limits.lib.mjs +0 -89
  24. package/src/model-args.lib.mjs +32 -0
  25. package/src/models/index.mjs +5 -19
  26. package/src/session-monitor.lib.mjs +14 -172
  27. package/src/solve.auto-merge.lib.mjs +70 -164
  28. package/src/solve.mjs +31 -193
  29. package/src/solve.repository.lib.mjs +0 -83
  30. package/src/solve.results.lib.mjs +2 -92
  31. package/src/solve.session.lib.mjs +52 -19
  32. package/src/solve.tool-uncommitted.lib.mjs +22 -0
  33. package/src/state-lock.lib.mjs +82 -0
  34. package/src/telegram-bot.mjs +17 -65
  35. package/src/telegram-fix-command.lib.mjs +1 -8
  36. package/src/telegram-merge-queue.lib.mjs +3 -155
  37. package/src/telegram-solve-queue.lib.mjs +9 -168
  38. package/src/telegram-task-command.lib.mjs +1 -8
  39. package/src/use-m-bootstrap.lib.mjs +6 -5
  40. package/src/use-with-retry.lib.mjs +128 -2
  41. package/src/working-session-summary.lib.mjs +47 -1
@@ -9,14 +9,12 @@ import { ensureUseM } from './use-m-bootstrap.lib.mjs';
9
9
  *
10
10
  * @see https://github.com/link-assistant/hive-mind/issues/1190
11
11
  */
12
-
13
12
  // Check if use is already defined globally (when imported from solve.mjs)
14
13
  // If not, fetch it (when running standalone)
15
14
  if (typeof globalThis.use === 'undefined') {
16
15
  await ensureUseM();
17
16
  }
18
17
  const use = globalThis.use;
19
-
20
18
  // Use command-stream for consistent $ behavior across runtimes
21
19
  const { $: __rawDollar$ } = await use('command-stream');
22
20
  const { wrapDollarWithGhRetry } = await import('./github-rate-limit.lib.mjs');
@@ -24,19 +22,15 @@ const $ = wrapDollarWithGhRetry(__rawDollar$);
24
22
  // Import shared library functions
25
23
  const lib = await import('./lib.mjs');
26
24
  const { log, cleanErrorMessage, formatAligned, formatToolExecutionFailure, extractToolErrorCore, getLogFile } = lib;
27
-
28
25
  // Note: We don't use detectAndCountFeedback from solve.feedback.lib.mjs
29
26
  // because we have our own non-bot comment detection logic that's more
30
27
  // appropriate for auto-restart-until-mergeable mode
31
-
32
28
  // Import Sentry integration
33
29
  const sentryLib = await import('./sentry.lib.mjs');
34
30
  const { reportError } = sentryLib;
35
-
36
31
  // Import GitHub merge functions
37
32
  const githubMergeLib = await import('./github-merge.lib.mjs');
38
33
  const { checkMergePermissions, mergePullRequest, getRepoVisibility, BILLING_LIMIT_ERROR_PATTERN, getDetailedCIStatus, rerunWorkflowRun, getWorkflowRunsForSha, getAllActiveRepoRuns, checkCIConsensus } = githubMergeLib;
39
-
40
34
  // Import GitHub functions for log attachment
41
35
  const githubLib = await import('./github.lib.mjs');
42
36
  const { sanitizeLogContent, attachLogToGitHub } = githubLib;
@@ -44,52 +38,46 @@ const { sanitizeLogContent, attachLogToGitHub } = githubLib;
44
38
  // Import shared utilities from the restart-shared module
45
39
  const restartShared = await import('./solve.restart-shared.lib.mjs');
46
40
  const { checkForUncommittedChanges, getUncommittedChangesDetails, executeToolIteration, buildAutoRestartInstructions, isUsageLimitReached } = restartShared;
47
-
48
41
  // Issue #1931: deleted/inaccessible repositories, PRs, issues, and branches
49
42
  // are terminal states for long-running watch loops, not retryable CI states.
50
43
  const terminalStateLib = await import('./github-terminal-state.lib.mjs');
51
44
  const { checkGitHubTerminalState } = terminalStateLib;
52
-
53
45
  // Issue #2144: these probes answer with a ~33 KB pull request object and a full
54
46
  // issue object on every iteration. Issue #2130 made the helper's own default
55
47
  // runner quiet, but passing `$` here bypassed it and the payloads were still
56
48
  // mirrored into the attached log. Bind the quiet options to the injected `$`.
57
49
  const { quietProbe } = await import('./quiet-probe.lib.mjs');
58
-
59
50
  // Issue #2144: a closed linked issue is NOT terminal — it only blocks the final
60
51
  // automatic merge. Every stop of this loop is also published as a GitHub comment
61
52
  // stating exactly why it stopped.
62
53
  const stopReportingLib = await import('./automation-stop-reporting.lib.mjs');
63
54
  const { reportAutomationStop } = stopReportingLib;
64
-
65
55
  // Import validation functions for time parsing (used for usage limit wait)
66
56
  const validation = await import('./solve.validation.lib.mjs');
67
57
  const { calculateWaitTime } = validation;
68
-
69
58
  // Import configuration (used for limit reset buffer and jitter)
70
59
  import { limitReset } from './config.lib.mjs';
71
-
72
60
  // Import helper functions extracted for file size management (Issue #1593)
73
61
  const autoMergeHelpers = await import('./solve.auto-merge-helpers.lib.mjs');
74
62
  const { checkForExistingComment, checkForNonBotComments, checkForIssueMetadataChanges, getMergeBlockers, shouldResetNoRunsCounter, trackAuthenticatedUserCommentsSince, nextMonotonicCheckTime } = autoMergeHelpers;
75
-
76
63
  // Issue #1769: cancelled/stale CI re-run failures need a human action stop, not polling forever.
77
64
  const cancelledCiRerunLib = await import('./cancelled-ci-rerun.lib.mjs');
78
65
  const { buildCancelledCIReviewComment, getRetriggerableWorkflowRuns, shouldStopForCancelledCIReview } = cancelledCiRerunLib;
79
66
 
80
67
  // Issue #1625: Shared marker constants + posting/tracking helpers
81
68
  const toolComments = await import('./tool-comments.lib.mjs');
82
- const { READY_TO_MERGE_MARKER, READY_FOR_REVIEW_MARKER, AUTO_RESTART_MARKER, AUTO_RESTART_UNTIL_MERGEABLE_LOG_MARKER, AUTO_MERGED_MARKER, postTrackedComment } = toolComments;
83
-
69
+ const { READY_TO_MERGE_MARKER, READY_FOR_REVIEW_MARKER, AUTO_RESUME_ON_LIMIT_RESET_MARKER, AUTO_RESTART_MARKER, AUTO_RESTART_UNTIL_MERGEABLE_LOG_MARKER, AUTO_MERGED_MARKER, postTrackedComment } = toolComments;
70
+ // Issue #2148: in-process usage-limit continuations bypass startWorkSession,
71
+ // so post their session boundary explicitly before invoking `--resume`.
72
+ const sessionLib = await import('./solve.session.lib.mjs');
73
+ const { postWorkSessionStartComment, SESSION_TYPES } = sessionLib;
84
74
  const externalReviewLimitLib = await import('./external-review-limit.lib.mjs');
85
75
  const { buildReadyForReviewComment } = externalReviewLimitLib;
86
-
87
76
  // Issue #1728: Per-iteration working session summary attachment helper
88
77
  // Issue #1763: Per-iteration PR ↔ issue link verification (so a clobbered
89
78
  // PR body is restored before the next stop condition fires).
90
79
  const resultsLib = await import('./solve.results.lib.mjs');
91
80
  const { maybeAttachWorkingSessionSummary, ensurePullRequestIssueLink } = resultsLib;
92
-
93
81
  // Issue #1574: Interruptible sleep so CTRL+C is never blocked by a lingering timer
94
82
  const { interruptibleSleep } = await import('./interruptible-sleep.lib.mjs');
95
83
  const { formatAutoIterationLimit, hasReachedAutoIterationLimit, normalizeAutoIterationLimit, shouldSyncBeforeRestart } = await import('./auto-iteration-limits.lib.mjs');
@@ -101,14 +89,11 @@ const autoRestartBudget = await import('./auto-restart-budget.lib.mjs');
101
89
  const { beginAutoRestartBudget, consumeAutoRestartIteration, formatAutoRestartLabel, formatAutoRestartLimit, hasExhaustedAutoRestartBudget } = autoRestartBudget;
102
90
  const { failOnAutoRestartBudgetExhausted } = await import('./auto-restart-exhaustion.lib.mjs');
103
91
  const { ensurePullRequestBaseBranch } = await import('./solve.pr-base-guard.lib.mjs');
104
-
105
92
  // Issue #2119: an empty pull request must not be reported as ready to merge.
106
93
  const { buildEmptyPullRequestBlocker, getPullRequestChangeStats } = await import('./pull-request-changes.lib.mjs');
107
-
108
94
  // Issue #1895: explicitly close linked issues after merging a PR into a
109
95
  // non-default branch, where GitHub does not auto-close them.
110
96
  const { ensureLinkedIssueClosedAfterMerge } = await import('./github-issue-auto-close.lib.mjs');
111
-
112
97
  const shouldDeleteBranchAfterMerge = argv => argv.autoDeleteBranchOnMerge || argv.deleteBranchAfterMerge || false;
113
98
 
114
99
  /**
@@ -117,7 +102,6 @@ const shouldDeleteBranchAfterMerge = argv => argv.autoDeleteBranchOnMerge || arg
117
102
  */
118
103
  export const watchUntilMergeable = async params => {
119
104
  const { issueUrl, owner, repo, issueNumber, prNumber, prBranch, branchName, tempDir, argv } = params;
120
-
121
105
  const rawWatchInterval = argv.watchInterval || 60; // seconds
122
106
  // Issue #1567: Minimum 120s interval to conserve API rate limits while keeping responsiveness
123
107
  const MIN_CI_CHECK_INTERVAL_SECONDS = 120;
@@ -129,25 +113,19 @@ export const watchUntilMergeable = async params => {
129
113
  // Issue #1503/#1573/#1612: repo-wide action gating is opt-in strict mode.
130
114
  // The config default may be bypassed when this module is reused directly, so normalize here.
131
115
  const waitForAllRepoActionsFlag = argv.waitForAllActionsInRepositoryBeforeMergeable ?? argv['wait-for-all-actions-in-repository-before-mergeable'] ?? argv.waitForAllActionsInRepositoryBeforeMergable ?? argv['wait-for-all-actions-in-repository-before-mergable'] ?? false;
132
-
133
116
  // Track latest session data across all iterations for accurate pricing
134
117
  let latestSessionId = null;
135
118
  let latestAnthropicCost = null;
136
-
137
119
  // Issue #1323: Track actual AI restarts separately from check cycle iterations
138
120
  // Issue #2119: the count now lives in the shared budget module, so restarts
139
121
  // already spent by the watch loop earlier in this run are counted here too.
140
122
  let limitResumeCount = 0;
141
-
142
123
  // Issue #1371: In-memory dedup for "Ready to merge" comment (per-session, not all-time)
143
124
  let readyToMergeCommentPosted = false;
144
-
145
125
  let currentBackoffSeconds = watchInterval;
146
-
147
126
  // Issue #1503: Track consecutive "no workflow runs" checks per-SHA (reset on new push)
148
127
  let consecutiveNoRunsChecks = 0;
149
128
  let lastKnownHeadSha = null;
150
-
151
129
  // Issue #1567: Initial cooldown to let CI register and solution logs post
152
130
  const INITIAL_COOLDOWN_SECONDS = MIN_CI_CHECK_INTERVAL_SECONDS;
153
131
 
@@ -170,26 +148,21 @@ export const watchUntilMergeable = async params => {
170
148
  await log('');
171
149
  await log('Press Ctrl+C to stop watching manually');
172
150
  await log('');
173
-
174
151
  // Issue #1567: Wait for initial cooldown before first check.
175
152
  // This gives CI/CD time to start and solution logs time to be posted.
176
153
  await log(formatAligned('⏳', 'Initial cooldown:', `Waiting ${INITIAL_COOLDOWN_SECONDS}s before first check...`));
177
154
  await interruptibleSleep(INITIAL_COOLDOWN_SECONDS * 1000);
178
155
  await log(formatAligned('✅', 'Cooldown complete:', 'Starting monitoring loop'));
179
156
  await log('');
180
-
181
157
  let iteration = 0;
182
158
  let lastCheckTime = new Date();
183
-
184
159
  // Issue #2007: Track the issue title/body across iterations so the
185
160
  // restart/resume fallback can detect user edits to those surfaces and deliver
186
161
  // them as feedback to the next session. The first check seeds the baseline.
187
162
  let issueMetadataSnapshot = null;
188
-
189
163
  while (true) {
190
164
  iteration++;
191
165
  const currentTime = new Date();
192
-
193
166
  const terminalState = await checkGitHubTerminalState({
194
167
  owner,
195
168
  repo,
@@ -217,7 +190,6 @@ export const watchUntilMergeable = async params => {
217
190
  await reportAutomationStop({ $, owner, repo, targetNumber: prNumber, reason: terminalState.reason, mode: 'auto-restart-until-mergeable', message: terminalState.message, details: terminalState.details, verbose: argv.verbose, log });
218
191
  return { success: false, reason: terminalState.reason, latestSessionId, latestAnthropicCost };
219
192
  }
220
-
221
193
  // Issue #2144: issue-scoped problems (closed / deleted linked issue) never
222
194
  // stop this loop. They are carried to the merge decision below.
223
195
  const issueMergeBlockers = terminalState.mergeBlockers || [];
@@ -226,7 +198,6 @@ export const watchUntilMergeable = async params => {
226
198
  await log(formatAligned('⚠️', 'Linked issue:', `${blocker.message} Continuing to make the pull request mergeable.`, 2), { level: 'warning' });
227
199
  }
228
200
  }
229
-
230
201
  await log(formatAligned('🔍', `Check #${iteration}:`, currentTime.toLocaleTimeString()));
231
202
 
232
203
  try {
@@ -250,13 +221,10 @@ export const watchUntilMergeable = async params => {
250
221
  // so a new "Ready to merge" comment can be posted for the new commit's CI results.
251
222
  readyToMergeCommentPosted = false;
252
223
  }
253
-
254
224
  // Issue #1503: Increment counter; getMergeBlockers uses it as a safety valve
255
225
  consecutiveNoRunsChecks++;
256
-
257
226
  // Get merge blockers
258
227
  const { blockers, noCiConfigured, noCiTriggered, workflowRunConclusions, ciStatus, noWorkflowRunsForCommit } = await getMergeBlockers(owner, repo, prNumber, argv.verbose, consecutiveNoRunsChecks, prBranch);
259
-
260
228
  const terminalGitHubBlocker = blockers.find(b => b.type === 'terminal_github_entity_error');
261
229
  if (terminalGitHubBlocker) {
262
230
  await log('');
@@ -269,7 +237,6 @@ export const watchUntilMergeable = async params => {
269
237
  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 });
270
238
  return { success: false, reason: 'terminal_github_entity_error', latestSessionId, latestAnthropicCost };
271
239
  }
272
-
273
240
  // Issue #1503/#1918: Reset counter when CI checks exist (safety valve only for
274
241
  // consecutive "no runs"). Issue #1918: do NOT reset while getMergeBlockers is still
275
242
  // waiting for PR-triggered workflow runs to register (noWorkflowRunsForCommit). A
@@ -283,14 +250,12 @@ export const watchUntilMergeable = async params => {
283
250
  // CI was definitively determined: either not configured or not triggered.
284
251
  // Keep the counter as-is (it reached the safety valve or wasn't needed).
285
252
  }
286
-
287
253
  // Check for new comments from non-bot users. At this point the AI tool
288
254
  // is not executing, so same-account non-tool comments can be trusted as
289
255
  // human feedback while known tool comments remain filtered by markers/IDs.
290
256
  const { hasNewComments, comments } = await checkForNonBotComments(owner, repo, prNumber, issueNumber, lastCheckTime, argv.verbose, $, {
291
257
  trustAuthenticatedUserComments: true,
292
258
  });
293
-
294
259
  // Issue #2007: Detect issue title/description edits (user-owned feedback
295
260
  // surfaces) so the fallback resumes the AI with them. The first iteration
296
261
  // seeds the baseline and never reports a change.
@@ -298,7 +263,6 @@ export const watchUntilMergeable = async params => {
298
263
  issueMetadataSnapshot = metadataCheck.snapshot || issueMetadataSnapshot;
299
264
  const hasIssueMetadataChanges = metadataCheck.changed === true;
300
265
  const issueMetadataChanges = metadataCheck.changes || [];
301
-
302
266
  // Check for uncommitted changes using shared utility
303
267
  const hasUncommittedChanges = await checkForUncommittedChanges(tempDir, argv);
304
268
 
@@ -312,7 +276,6 @@ export const watchUntilMergeable = async params => {
312
276
  await log(formatAligned('ℹ️', 'CI not triggered:', 'Workflows exist but no workflow runs for this commit (fork PR, paths-ignore, workflow conditions)', 2));
313
277
  }
314
278
  }
315
-
316
279
  // Issue #2119: an empty pull request is not "ready to merge". The Kotlin
317
280
  // reproduction run posted "✅ Ready to merge - No pending changes" for a
318
281
  // pull request whose net diff was empty, so merging it would have closed
@@ -323,7 +286,6 @@ export const watchUntilMergeable = async params => {
323
286
  if (isEmptyPullRequest) {
324
287
  await log(formatAligned('⚠️', 'PR is empty:', changeStats.placeholderOnly ? 'only the solver placeholder file is in the diff - not treating it as mergeable' : 'net diff contains no files - not treating it as mergeable', 2), { level: 'warning' });
325
288
  }
326
-
327
289
  // If PR is mergeable, no blockers, no new comments, no issue metadata
328
290
  // edits, no uncommitted changes and it actually changes something
329
291
  if (blockers.length === 0 && !hasNewComments && !hasIssueMetadataChanges && !hasUncommittedChanges && !isEmptyPullRequest) {
@@ -335,7 +297,6 @@ export const watchUntilMergeable = async params => {
335
297
  const DOUBLE_CHECK_DELAY_MS = 10000; // 10 seconds
336
298
  await log(formatAligned('🔍', 'Multi-mechanism CI consensus check:', `Waiting ${DOUBLE_CHECK_DELAY_MS / 1000}s then verifying...`, 2));
337
299
  await interruptibleSleep(DOUBLE_CHECK_DELAY_MS);
338
-
339
300
  // Run multi-mechanism consensus: Check Runs API + Workflow Runs API + Repo-wide actions
340
301
  const consensus = await checkCIConsensus({
341
302
  owner,
@@ -347,7 +308,6 @@ export const watchUntilMergeable = async params => {
347
308
  getDetailedCIStatus,
348
309
  getWorkflowRunsForSha,
349
310
  });
350
-
351
311
  if (!consensus.allAgree) {
352
312
  const m = consensus.mechanisms;
353
313
  const repoLabel = m.repoActions.skipped ? 'skipped' : `${m.repoActions.count} active`;
@@ -377,9 +337,7 @@ export const watchUntilMergeable = async params => {
377
337
  continue;
378
338
  }
379
339
  }
380
-
381
340
  await log(formatAligned('✅', 'PR IS MERGEABLE!', ''));
382
-
383
341
  // Issue #2144: the pull request is ready. A closed/unavailable linked
384
342
  // issue blocks only the *automatic* merge — the loop already did its
385
343
  // job of making the pull request mergeable. Ask the user to reopen the
@@ -388,7 +346,6 @@ export const watchUntilMergeable = async params => {
388
346
  await reportAutoMergeBlockedByIssue({ owner, repo, prNumber, issueNumber, mergeBlockers: issueMergeBlockers, verbose: argv.verbose });
389
347
  return { success: false, reason: issueMergeBlockers[0].reason, mergeBlockers: issueMergeBlockers, latestSessionId, latestAnthropicCost };
390
348
  }
391
-
392
349
  if (isAutoMerge) {
393
350
  // Attempt to merge the PR
394
351
  await log(formatAligned('🔀', 'Auto-merging PR...', ''));
@@ -401,7 +358,6 @@ export const watchUntilMergeable = async params => {
401
358
  if (mergeResult.success) {
402
359
  await log(formatAligned('🎉', 'PR MERGED SUCCESSFULLY!', ''));
403
360
  await log(formatAligned('', 'Pull request:', `#${prNumber} has been auto-merged`, 2));
404
-
405
361
  // Post success comment
406
362
  try {
407
363
  // Issue #1345: Differentiate message when no CI is configured
@@ -411,7 +367,6 @@ export const watchUntilMergeable = async params => {
411
367
  } catch {
412
368
  // Don't fail if comment posting fails
413
369
  }
414
-
415
370
  // Issue #1895: when the PR targeted a non-default branch GitHub does
416
371
  // not auto-close the linked issue. Close it explicitly so the issue
417
372
  // is not left open after its PR merges.
@@ -427,7 +382,6 @@ export const watchUntilMergeable = async params => {
427
382
  await log(formatAligned('⚠️', 'Issue auto-close:', `error closing issue #${issueNumber}: ${closeError.message}`, 2), { level: 'warning' });
428
383
  }
429
384
  }
430
-
431
385
  return { success: true, reason: 'auto-merged', latestSessionId, latestAnthropicCost };
432
386
  } else {
433
387
  await log(formatAligned('⚠️', 'Auto-merge failed:', mergeResult.error || 'Unknown error', 2));
@@ -437,7 +391,6 @@ export const watchUntilMergeable = async params => {
437
391
  // Just report that PR is mergeable and exit
438
392
  await log(formatAligned('', 'PR is ready to be merged manually', '', 2));
439
393
  await log(formatAligned('', 'Exiting auto-restart-until-mergeable mode', '', 2));
440
-
441
394
  // Issue #1371: Post success comment only if not already posted in this session.
442
395
  // Issue #1567: Also check PR comment history as a cross-process guard.
443
396
  // Two layers of deduplication:
@@ -478,16 +431,13 @@ export const watchUntilMergeable = async params => {
478
431
  } catch {
479
432
  // Don't fail if comment posting fails
480
433
  }
481
-
482
434
  return { success: true, reason: 'mergeable', latestSessionId, latestAnthropicCost };
483
435
  }
484
436
  }
485
-
486
437
  // Determine if we need to restart
487
438
  let shouldRestart = false;
488
439
  let restartReason = '';
489
440
  let feedbackLines = [];
490
-
491
441
  // Reason 1: New comments from non-bot users
492
442
  if (hasNewComments) {
493
443
  shouldRestart = true;
@@ -508,7 +458,6 @@ export const watchUntilMergeable = async params => {
508
458
  feedbackLines.push('');
509
459
  feedbackLines.push('Implement the requested change and commit it to the pull request branch. Do not report the work as done while the diff is empty.');
510
460
  }
511
-
512
461
  // Issue #2007: Reason 1b: Issue title/description edited by the user.
513
462
  if (hasIssueMetadataChanges) {
514
463
  shouldRestart = true;
@@ -523,7 +472,6 @@ export const watchUntilMergeable = async params => {
523
472
  feedbackLines.push('');
524
473
  feedbackLines.push('Please re-read the updated issue and make sure your solution still matches the requirements.');
525
474
  }
526
-
527
475
  // Issue #1314: Check for billing limit errors BEFORE regular CI failures
528
476
  // Billing limits require human intervention and should NOT trigger AI restarts
529
477
  const billingBlocker = blockers.find(b => b.type === 'billing_limit');
@@ -533,15 +481,12 @@ export const watchUntilMergeable = async params => {
533
481
  await log(formatAligned('', 'Affected jobs:', billingBlocker.details.join(', '), 2));
534
482
  await log(formatAligned('', 'All jobs affected:', billingBlocker.allJobsAffected ? 'Yes' : 'No', 2));
535
483
  await log('');
536
-
537
484
  // Check if this is a private repository
538
485
  const repoInfo = await getRepoVisibility(owner, repo, argv.verbose);
539
-
540
486
  if (repoInfo.isPrivate) {
541
487
  // For private repos, human intervention is required - stop and post comment
542
488
  await log(formatAligned('🛑', 'STOPPING', 'Private repository - billing limit requires human intervention'));
543
489
  await log(formatAligned('', 'Action required:', "Check the 'Billing & plans' section in your GitHub settings", 2));
544
-
545
490
  // Post comment explaining the billing limit issue
546
491
  try {
547
492
  const commentBody = `## 💳 GitHub Actions Billing Limit Reached
@@ -576,20 +521,17 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
576
521
  });
577
522
  await log(formatAligned('', '⚠️ Could not post comment to PR', '', 2));
578
523
  }
579
-
580
524
  return { success: false, reason: 'billing_limit', latestSessionId, latestAnthropicCost };
581
525
  } else {
582
526
  // For public repos (unusual case), apply exponential backoff and wait
583
527
  // Public repos typically have unlimited free CI, so this is unexpected
584
528
  await log(formatAligned('⏳', 'Public repository with billing limit (unusual)', 'Applying exponential backoff'));
585
529
  await log(formatAligned('', 'Next check in:', `${currentBackoffSeconds} seconds`, 2));
586
-
587
530
  // Don't trigger AI restart - just wait and check again
588
531
  // The backoff will be applied at the end of the loop
589
532
  currentBackoffSeconds = Math.min(currentBackoffSeconds * 2, 3600); // Max 1 hour
590
533
  }
591
534
  }
592
-
593
535
  // Issue #1314: Handle cancelled CI/CD checks - re-trigger them instead of restarting AI
594
536
  // Cancelled checks (e.g., manually cancelled, cancelled by another workflow) should be
595
537
  // re-triggered automatically. We should NOT restart the AI for these.
@@ -612,18 +554,15 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
612
554
  let rerunTriggered = false;
613
555
  let rerunAttempted = false;
614
556
  const rerunFailures = [];
615
-
616
557
  if (sha) {
617
558
  runs = await getWorkflowRunsForSha(owner, repo, sha, argv.verbose);
618
559
  retriggerable = getRetriggerableWorkflowRuns(runs);
619
-
620
560
  if (retriggerable.length === 0) {
621
561
  await log(formatAligned('', '⚠️ No cancelled/stale workflow run found for this SHA', '', 2));
622
562
  rerunFailures.push({
623
563
  error: 'No cancelled/stale workflow run was found for this commit SHA.',
624
564
  });
625
565
  }
626
-
627
566
  for (const run of retriggerable) {
628
567
  await log(formatAligned('', `Re-triggering workflow "${run.name}" (${run.id})...`, '', 2));
629
568
  rerunAttempted = true;
@@ -636,7 +575,6 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
636
575
  rerunFailures.push({ run, error: rerunResult.error });
637
576
  }
638
577
  }
639
-
640
578
  if (rerunTriggered) {
641
579
  await log(formatAligned('⏳', 'Waiting for re-triggered CI to complete...', '', 2));
642
580
  // Don't restart AI - just wait for re-triggered jobs to complete
@@ -648,10 +586,8 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
648
586
  error: 'Cancelled CI blocker did not include a commit SHA, so automatic workflow re-run could not identify the run.',
649
587
  });
650
588
  }
651
-
652
589
  if (shouldStopForCancelledCIReview({ retriggerableRuns: retriggerable, rerunTriggered, rerunFailures })) {
653
590
  await log(formatAligned('🛑', 'CANCELLED CI/CD NEEDS HUMAN REVIEW', 'Automatic re-run could not be started'));
654
-
655
591
  try {
656
592
  const commentBody = buildCancelledCIReviewComment({
657
593
  blocker: cancelledBlocker,
@@ -672,7 +608,6 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
672
608
  });
673
609
  await log(formatAligned('', '⚠️ Could not post cancelled CI review comment to PR', '', 2));
674
610
  }
675
-
676
611
  return { success: false, reason: 'ci_cancelled_requires_review', latestSessionId, latestAnthropicCost };
677
612
  }
678
613
  // Don't set shouldRestart for cancelled checks - wait for re-triggered jobs instead
@@ -692,7 +627,6 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
692
627
  await log(formatAligned('', 'Check not executed:', detail, 2));
693
628
  }
694
629
  await log(formatAligned('', 'Action:', 'Stopping auto-restart without starting another AI session', 2));
695
-
696
630
  try {
697
631
  const commentSignature = `## 🟡 ${READY_FOR_REVIEW_MARKER}`;
698
632
  const hasExistingReadyForReviewComment = await checkForExistingComment(owner, repo, prNumber, commentSignature, argv.verbose);
@@ -716,10 +650,8 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
716
650
  });
717
651
  await log(formatAligned('', '⚠️ Could not post ready-for-review comment to PR', '', 2));
718
652
  }
719
-
720
653
  return { success: false, reason: 'external_review_limit', latestSessionId, latestAnthropicCost };
721
654
  }
722
-
723
655
  if (ciBlocker && !billingBlocker) {
724
656
  shouldRestart = true;
725
657
  restartReason = restartReason ? `${restartReason}; CI failures` : 'CI failures detected';
@@ -736,7 +668,6 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
736
668
  feedbackLines.push('');
737
669
  feedbackLines.push('Please fix the failing CI checks.');
738
670
  }
739
-
740
671
  // Reason 3: Merge conflicts or other merge issues
741
672
  const mergeBlocker = blockers.find(b => b.type === 'not_mergeable');
742
673
  if (mergeBlocker && mergeBlocker.message.includes('conflicts')) {
@@ -747,12 +678,10 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
747
678
  feedbackLines.push('');
748
679
  feedbackLines.push('Please resolve the merge conflicts.');
749
680
  }
750
-
751
681
  // Reason 4: Uncommitted changes
752
682
  if (hasUncommittedChanges) {
753
683
  shouldRestart = true;
754
684
  restartReason = restartReason ? `${restartReason}; Uncommitted changes` : 'Uncommitted changes detected';
755
-
756
685
  // Get uncommitted changes for display using shared utility
757
686
  const changes = await getUncommittedChangesDetails(tempDir);
758
687
  feedbackLines.push('📝 Uncommitted changes detected:');
@@ -764,7 +693,6 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
764
693
  feedbackLines.push('1. COMMITTING them if they are part of the solution (git add + git commit + git push)');
765
694
  feedbackLines.push('2. REVERTING them if they are not needed (git checkout -- <file> or git clean -fd)');
766
695
  }
767
-
768
696
  if (shouldRestart) {
769
697
  // Issue #2119: the run-wide budget is exhausted (it may already have been
770
698
  // spent by the watch loop). Fail and auto-commit through the same shared
@@ -788,11 +716,9 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
788
716
 
789
717
  // Add standard instructions for auto-restart-until-mergeable mode using shared utility
790
718
  feedbackLines.push(...buildAutoRestartInstructions());
791
-
792
719
  // Get PR merge state status
793
720
  const prStateResult = await $`gh api repos/${owner}/${repo}/pulls/${prNumber} --jq '.mergeStateStatus'`;
794
721
  const mergeStateStatus = prStateResult.code === 0 ? prStateResult.stdout.toString().trim() : null;
795
-
796
722
  // Issue #1572: Sync clean local branches with remote before restarting to avoid push failures.
797
723
  // Issue #1664: Do not run git pull over an unfinished merge or other uncommitted state.
798
724
  // The tool must see that state and either commit, continue, abort, or otherwise resolve it.
@@ -818,15 +744,12 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
818
744
  } else {
819
745
  await log(formatAligned('↪️', 'Skipping branch sync:', 'Local uncommitted/merge state must be resolved by the AI session', 2));
820
746
  }
821
-
822
747
  // Issue #1323: Increment restart count only when a tool execution is about to start.
823
748
  // Issue #2119: claim it from the run-wide shared budget.
824
749
  const restartCount = consumeAutoRestartIteration();
825
-
826
750
  await log(formatAligned('🔄', 'RESTART TRIGGERED:', restartReason));
827
751
  await log(formatAligned('', 'Restart iteration:', formatAutoRestartLabel(restartCount), 2));
828
752
  await log('');
829
-
830
753
  // Post a comment to PR about the restart after preflight succeeds, so every
831
754
  // posted restart notification corresponds to an actual tool session.
832
755
  try {
@@ -848,15 +771,13 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
848
771
  });
849
772
  await log(formatAligned('', '⚠️ Could not post comment to PR', '', 2));
850
773
  }
851
-
852
774
  // Execute the AI tool using shared utility
853
775
  await log(formatAligned('🔄', 'Restarting:', `Running ${argv.tool.toUpperCase()} to address issues...`));
854
-
855
776
  // Issue #1728: Scope the AI-comment check that gates --auto-attach-solution-summary
856
777
  // to comments posted during *this* iteration only, not across the whole watch loop.
857
778
  const iterationStartTime = new Date();
858
779
 
859
- const toolResult = await executeToolIteration({
780
+ let toolResult = await executeToolIteration({
860
781
  issueUrl,
861
782
  owner,
862
783
  repo,
@@ -868,7 +789,7 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
868
789
  feedbackLines,
869
790
  argv,
870
791
  });
871
-
792
+ let resumedAfterUsageLimit = false;
872
793
  if (!toolResult.success) {
873
794
  // Issue #1356: Check for usage limit errors FIRST (most specific)
874
795
  // When usage limit is reached, wait for limitResetTime + buffer + jitter,
@@ -885,7 +806,6 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
885
806
  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 });
886
807
  return { success: false, reason: 'auto_resume_limit_reached', latestSessionId, latestAnthropicCost };
887
808
  }
888
-
889
809
  limitResumeCount++;
890
810
  const resumeSessionId = toolResult.sessionId;
891
811
  const resetTime = toolResult.limitResetTime;
@@ -896,14 +816,12 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
896
816
  const bufferMinutes = Math.round(bufferMs / 60000);
897
817
  const jitterSeconds = Math.round(jitterMs / 1000);
898
818
  const waitMinutes = Math.round(waitMs / 60000);
899
-
900
819
  // Issue #1570: Calculate the actual resume time for user display
901
820
  const resumeDate = new Date(Date.now() + waitMs);
902
821
  const resumeTimeUTC = resumeDate
903
822
  .toISOString()
904
823
  .replace('T', ' ')
905
824
  .replace(/\.\d+Z$/, ' UTC');
906
-
907
825
  await log('');
908
826
  await log(formatAligned('⏳', 'USAGE LIMIT REACHED', ''));
909
827
  await log(formatAligned('', 'Reset time:', resetTime || 'Unknown', 2));
@@ -915,7 +833,6 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
915
833
  await log(formatAligned('', 'Session ID:', resumeSessionId, 2));
916
834
  }
917
835
  await log('');
918
-
919
836
  // Issue #1570: Post a GitHub comment to notify the user about the usage limit delay.
920
837
  // This follows the same pattern as solve.watch.lib.mjs to ensure consistent user experience.
921
838
  const shouldAttachLogs = argv.attachLogs || argv['attach-logs'];
@@ -940,7 +857,7 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
940
857
  limitResetTime: resetTime,
941
858
  toolName: `Anthropic ${(argv.tool || 'claude').charAt(0).toUpperCase() + (argv.tool || 'claude').slice(1)} Code`,
942
859
  isAutoResumeEnabled: true,
943
- autoResumeMode: 'restart',
860
+ autoResumeMode: 'resume',
944
861
  requestedModel: argv.originalModel || argv.model,
945
862
  tool: argv.tool || 'claude',
946
863
  publicPricingEstimate: toolResult.publicPricingEstimate,
@@ -960,16 +877,26 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
960
877
  await log(formatAligned('', `⚠️ Usage limit comment upload error: ${cleanErrorMessage(commentError)}`, '', 2));
961
878
  }
962
879
  }
963
-
964
880
  // Wait until the limit resets
965
881
  await interruptibleSleep(waitMs);
966
-
967
882
  await log(formatAligned('✅', 'Usage limit wait complete', 'Resuming session...'));
968
883
  await log('');
969
884
 
970
885
  // Resume the session: execute with --resume <sessionId> and a "Continue" prompt
971
886
  // This preserves context and the system message from the original session
972
887
  if (resumeSessionId) {
888
+ // Issue #2148: this continuation stays in the same Claude session,
889
+ // but it does not pass through the top-level startWorkSession path.
890
+ // Publish the existing auto-resume marker at the real boundary.
891
+ await postWorkSessionStartComment({
892
+ owner,
893
+ repo,
894
+ prNumber,
895
+ $,
896
+ log,
897
+ formatAligned,
898
+ sessionType: SESSION_TYPES.AUTO_RESUME,
899
+ });
973
900
  const resumeArgv = { ...argv, resume: resumeSessionId };
974
901
  const resumeResult = await executeToolIteration({
975
902
  issueUrl,
@@ -983,14 +910,12 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
983
910
  feedbackLines: ['Continue'],
984
911
  argv: resumeArgv,
985
912
  });
986
-
987
913
  if (resumeResult.success) {
988
- // Resume succeeded - capture session data
989
- currentBackoffSeconds = watchInterval;
990
- if (resumeResult.sessionId) {
991
- latestSessionId = resumeResult.sessionId;
992
- latestAnthropicCost = resumeResult.anthropicTotalCostUSD;
993
- }
914
+ // Issue #2148: feed the resumed result into the same success
915
+ // path as every other iteration. That path publishes the
916
+ // summary and log, verifies the issue link, and tracks comments.
917
+ toolResult = resumeResult;
918
+ resumedAfterUsageLimit = true;
994
919
  await log(formatAligned('✅', `${argv.tool.toUpperCase()} resume completed:`, 'Checking if PR is now mergeable...'));
995
920
  } else if (isUsageLimitReached(resumeResult)) {
996
921
  // Hit the limit again immediately after resume — store for next outer iteration
@@ -1043,62 +968,64 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
1043
968
  // No session ID available — cannot resume, restart fresh in next iteration
1044
969
  await log(formatAligned('⚠️', 'No session ID for resume', 'Will restart fresh in next check cycle', 2));
1045
970
  }
1046
-
1047
- lastCheckTime = new Date();
1048
- continue;
971
+ if (!toolResult.success) {
972
+ lastCheckTime = new Date();
973
+ continue;
974
+ }
1049
975
  }
1050
-
1051
976
  // Any other failure (not usage limit): stop the auto-restart loop
1052
977
  // Per reviewer feedback: non-limit failures should fail and stop attempts
1053
- await log('');
1054
- await log(formatAligned('', `${argv.tool.toUpperCase()} EXECUTION FAILED`, ''));
1055
- // Issue #1845: surface the core error in the terminal, not just in the GitHub log.
1056
- await log(formatAligned('', 'Error details:', extractToolErrorCore({ toolResult }) || 'Unknown error', 2));
1057
- await log(formatAligned('', 'Action:', 'Stopping auto-restart tool execution failed', 2));
1058
- // Issue #1439: Attach failure log before stopping, so user can see what happened
1059
- const shouldAttachLogsOnFail = argv.attachLogs || argv['attach-logs'];
1060
- if (prNumber && shouldAttachLogsOnFail) {
1061
- try {
1062
- const logFile = getLogFile();
1063
- if (logFile) {
1064
- await attachLogToGitHub({
1065
- logFile,
1066
- targetType: 'pr',
1067
- targetNumber: prNumber,
978
+ if (!toolResult.success) {
979
+ await log('');
980
+ await log(formatAligned('❌', `${argv.tool.toUpperCase()} EXECUTION FAILED`, ''));
981
+ // Issue #1845: surface the core error in the terminal, not just in the GitHub log.
982
+ await log(formatAligned('', 'Error details:', extractToolErrorCore({ toolResult }) || 'Unknown error', 2));
983
+ await log(formatAligned('', 'Action:', 'Stopping auto-restart tool execution failed', 2));
984
+ // Issue #1439: Attach failure log before stopping, so user can see what happened
985
+ const shouldAttachLogsOnFail = argv.attachLogs || argv['attach-logs'];
986
+ if (prNumber && shouldAttachLogsOnFail) {
987
+ try {
988
+ const logFile = getLogFile();
989
+ if (logFile) {
990
+ await attachLogToGitHub({
991
+ logFile,
992
+ targetType: 'pr',
993
+ targetNumber: prNumber,
994
+ owner,
995
+ repo,
996
+ $,
997
+ log,
998
+ sanitizeLogContent,
999
+ verbose: argv.verbose,
1000
+ errorMessage: formatToolExecutionFailure({ tool: argv.tool, toolResult }),
1001
+ sessionId: latestSessionId,
1002
+ tempDir,
1003
+ requestedModel: argv.originalModel || argv.model,
1004
+ tool: argv.tool || 'claude',
1005
+ });
1006
+ }
1007
+ } catch (logUploadError) {
1008
+ reportError(logUploadError, {
1009
+ context: 'attach_auto_restart_failure_log',
1010
+ prNumber,
1068
1011
  owner,
1069
1012
  repo,
1070
- $,
1071
- log,
1072
- sanitizeLogContent,
1073
- verbose: argv.verbose,
1074
- errorMessage: formatToolExecutionFailure({ tool: argv.tool, toolResult }),
1075
- sessionId: latestSessionId,
1076
- tempDir,
1077
- requestedModel: argv.originalModel || argv.model,
1078
- tool: argv.tool || 'claude',
1013
+ operation: 'upload_failure_log',
1079
1014
  });
1015
+ await log(formatAligned('', `⚠️ Failure log upload error: ${cleanErrorMessage(logUploadError)}`, '', 2));
1080
1016
  }
1081
- } catch (logUploadError) {
1082
- reportError(logUploadError, {
1083
- context: 'attach_auto_restart_failure_log',
1084
- prNumber,
1085
- owner,
1086
- repo,
1087
- operation: 'upload_failure_log',
1088
- });
1089
- await log(formatAligned('', `⚠️ Failure log upload error: ${cleanErrorMessage(logUploadError)}`, '', 2));
1090
1017
  }
1018
+ 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 });
1019
+ return { success: false, reason: 'tool_failure', latestSessionId, latestAnthropicCost };
1091
1020
  }
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 });
1093
- return { success: false, reason: 'tool_failure', latestSessionId, latestAnthropicCost };
1094
- } else {
1021
+ }
1022
+ if (toolResult.success) {
1095
1023
  // Success - capture latest session data
1096
1024
  currentBackoffSeconds = watchInterval;
1097
1025
  if (toolResult.sessionId) {
1098
1026
  latestSessionId = toolResult.sessionId;
1099
1027
  latestAnthropicCost = toolResult.anthropicTotalCostUSD;
1100
1028
  }
1101
-
1102
1029
  // Issue #1508: Compute budget stats for auto-restart-until-mergeable log comment.
1103
1030
  // Issue #2132: shared with the top-level run and the watch loop via
1104
1031
  // buildSessionBudgetStatsData, so every working session derives its own
@@ -1113,7 +1040,6 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
1113
1040
  subAgentCalls: toolResult.subAgentCalls || null,
1114
1041
  pricingInfo: toolResult.pricingInfo || null,
1115
1042
  });
1116
-
1117
1043
  // Issue #1761: Post the working session **summary** BEFORE uploading
1118
1044
  // the working session **log** so the summary always appears above
1119
1045
  // the log in PR comment chronological order. The summary acts as a
@@ -1164,7 +1090,8 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
1164
1090
  // Issue #1323: Use the restart count (actual AI executions) instead of iteration (check cycles)
1165
1091
  // Issue #2119: `N/M` like every other auto-restart label, so the
1166
1092
  // limit is visible in the log title too.
1167
- const customTitle = `🔄 ${AUTO_RESTART_UNTIL_MERGEABLE_LOG_MARKER} ${formatAutoRestartLabel()}`;
1093
+ const autoResumeLabel = maxAutoResumeIterations === 0 ? `${limitResumeCount}` : `${limitResumeCount}/${maxAutoResumeIterations}`;
1094
+ const customTitle = resumedAfterUsageLimit ? `⏰ ${AUTO_RESUME_ON_LIMIT_RESET_MARKER} ${autoResumeLabel} Log` : `🔄 ${AUTO_RESTART_UNTIL_MERGEABLE_LOG_MARKER} ${formatAutoRestartLabel()}`;
1168
1095
  await attachLogToGitHub({
1169
1096
  logFile,
1170
1097
  targetType: 'pr',
@@ -1202,7 +1129,6 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
1202
1129
  await log(formatAligned('', `⚠️ Log upload error: ${cleanErrorMessage(logUploadError)}`, '', 2));
1203
1130
  }
1204
1131
  }
1205
-
1206
1132
  // Issue #1763: Re-verify the PR body contains a closing keyword for
1207
1133
  // the issue after every auto-restart-until-mergeable iteration. The
1208
1134
  // AI agent can rewrite the PR description mid-session and any
@@ -1231,11 +1157,9 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
1231
1157
  await log(formatAligned('', `⚠️ PR issue link check error: ${cleanErrorMessage(issueLinkError)}`, '', 2));
1232
1158
  }
1233
1159
  }
1234
-
1235
1160
  await log('');
1236
1161
  await log(formatAligned('✅', `${argv.tool.toUpperCase()} execution completed:`, 'Checking if PR is now mergeable...'));
1237
1162
  }
1238
-
1239
1163
  // Issue #1827: Register every comment the authenticated account posted
1240
1164
  // during this AI session (free-form status comments like "✅ CI now
1241
1165
  // green" the agent writes itself, which bypass postTrackedComment and
@@ -1255,7 +1179,6 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
1255
1179
  operation: 'track_session_comments',
1256
1180
  });
1257
1181
  }
1258
-
1259
1182
  // Update last check time after restart
1260
1183
  lastCheckTime = new Date();
1261
1184
  } else if (blockers.length > 0) {
@@ -1264,7 +1187,6 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
1264
1187
  const pendingBlocker = blockers.find(b => b.type === 'ci_pending');
1265
1188
  const cancelledOnly = blockers.every(b => b.type === 'ci_cancelled' || b.type === 'ci_pending');
1266
1189
  const cancelledBlocker = blockers.find(b => b.type === 'ci_cancelled');
1267
-
1268
1190
  // Issue #1712: When `details` contain URLs (which they now always do for ci_pending /
1269
1191
  // ci_cancelled blockers), comma-joining them produces an unreadable single-line wall
1270
1192
  // of text. Render the first detail inline (with the message as the header) and any
@@ -1284,7 +1206,6 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
1284
1206
  }
1285
1207
  })();
1286
1208
  };
1287
-
1288
1209
  if (cancelledOnly && cancelledBlocker) {
1289
1210
  await renderBlocker('🔄', 'Waiting for re-triggered CI:', cancelledBlocker);
1290
1211
  } else if (pendingBlocker) {
@@ -1295,7 +1216,6 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
1295
1216
  } else {
1296
1217
  await log(formatAligned('', 'No action needed', 'Continuing to monitor...', 2));
1297
1218
  }
1298
-
1299
1219
  // Issue #1827: Advance the check window monotonically — never move it
1300
1220
  // backwards. In the restart branch above, lastCheckTime was already set
1301
1221
  // to a moment *after* the AI session (and after any comments the agent
@@ -1325,33 +1245,27 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
1325
1245
  await interruptibleSleep(actualWaitSeconds * 1000);
1326
1246
  }
1327
1247
  };
1328
-
1329
1248
  // Issue #2144: the one-shot `--auto-merge` attempt moved to its own module so
1330
1249
  // both files stay under the 1500-line limit. Re-exported for API compatibility.
1331
1250
  const autoMergeAttempt = await import('./solve.auto-merge-attempt.lib.mjs');
1332
1251
  export const { attemptAutoMerge, reportAutoMergeBlockedByIssue } = autoMergeAttempt;
1333
-
1334
1252
  /**
1335
1253
  * Start auto-restart-until-mergeable mode
1336
1254
  */
1337
1255
  export const startAutoRestartUntilMergeable = async params => {
1338
1256
  const { argv, owner, repo, prNumber } = params;
1339
-
1340
1257
  // Determine the mode
1341
1258
  const isAutoMerge = argv.autoMerge || false;
1342
1259
  const isAutoRestartUntilMergeable = argv.autoRestartUntilMergeable || false;
1343
-
1344
1260
  if (!isAutoMerge && !isAutoRestartUntilMergeable) {
1345
1261
  return null; // Neither mode enabled
1346
1262
  }
1347
-
1348
1263
  if (!prNumber) {
1349
1264
  await log('');
1350
1265
  await log(formatAligned('⚠️', 'Auto-restart-until-mergeable:', 'Requires a pull request'));
1351
1266
  await log(formatAligned('', 'Note:', 'This mode only works with existing PRs', 2));
1352
1267
  return null;
1353
1268
  }
1354
-
1355
1269
  await ensurePullRequestBaseBranch({
1356
1270
  owner,
1357
1271
  repo,
@@ -1363,7 +1277,6 @@ export const startAutoRestartUntilMergeable = async params => {
1363
1277
  onMismatch: isAutoMerge ? 'throw' : 'restore',
1364
1278
  operation: isAutoMerge ? 'auto-merge' : 'auto-restart-until-mergeable',
1365
1279
  });
1366
-
1367
1280
  // Issue #1226: Check if running in fork mode — auto-merge cannot work without write access
1368
1281
  if (argv.fork && isAutoMerge) {
1369
1282
  await log('');
@@ -1387,10 +1300,8 @@ export const startAutoRestartUntilMergeable = async params => {
1387
1300
  } catch {
1388
1301
  // Don't fail if comment posting fails
1389
1302
  }
1390
-
1391
1303
  return { success: false, reason: 'fork_no_write_access' };
1392
1304
  }
1393
-
1394
1305
  // Issue #1226: Verify merge permissions before entering the auto-merge/restart loop
1395
1306
  if (isAutoMerge && owner && repo) {
1396
1307
  const { canMerge, permission } = await checkMergePermissions(owner, repo, argv.verbose);
@@ -1401,7 +1312,6 @@ export const startAutoRestartUntilMergeable = async params => {
1401
1312
  await log(formatAligned('', 'Required:', 'push, maintain, or admin access', 2));
1402
1313
  await log(formatAligned('', 'Action:', 'PR is ready for manual merge by a repository maintainer', 2));
1403
1314
  await log('');
1404
-
1405
1315
  // Issue #1323: Post a comment to the PR notifying the maintainer (with deduplication)
1406
1316
  try {
1407
1317
  const readyToMergeSignature = `## ✅ ${READY_TO_MERGE_MARKER}`;
@@ -1417,20 +1327,16 @@ export const startAutoRestartUntilMergeable = async params => {
1417
1327
  } catch {
1418
1328
  // Don't fail if comment posting fails
1419
1329
  }
1420
-
1421
1330
  return { success: false, reason: 'insufficient_permissions' };
1422
1331
  }
1423
1332
  }
1424
-
1425
1333
  // If --auto-merge implies --auto-restart-until-mergeable
1426
1334
  if (isAutoMerge) {
1427
1335
  argv.autoRestartUntilMergeable = true;
1428
1336
  }
1429
-
1430
1337
  // Start the watch loop
1431
1338
  return await watchUntilMergeable(params);
1432
1339
  };
1433
-
1434
1340
  export default {
1435
1341
  watchUntilMergeable,
1436
1342
  attemptAutoMerge,