@link-assistant/hive-mind 2.15.0 → 2.15.2
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 +40 -0
- package/package.json +1 -1
- package/src/git-auth-transport.lib.mjs +309 -0
- package/src/github-merge.lib.mjs +24 -28
- package/src/lib.mjs +7 -0
- package/src/merge-error-classification.lib.mjs +134 -0
- package/src/option-suggestions.lib.mjs +1 -0
- package/src/pr-draft-state.lib.mjs +96 -0
- package/src/review.mjs +6 -0
- package/src/solve.auto-merge-attempt.lib.mjs +42 -3
- package/src/solve.auto-merge-guards.lib.mjs +152 -0
- package/src/solve.auto-merge-helpers.lib.mjs +20 -1
- package/src/solve.auto-merge-preflight.lib.mjs +122 -0
- package/src/solve.auto-merge.lib.mjs +70 -84
- package/src/solve.config.lib.mjs +8 -0
- package/src/solve.interrupt.lib.mjs +18 -1
- package/src/solve.mjs +19 -0
- package/src/solve.repo-setup.lib.mjs +10 -0
- package/src/solve.repository.lib.mjs +26 -0
- package/src/solve.restart-shared.lib.mjs +302 -278
- package/src/solve.results.lib.mjs +6 -12
- package/src/solve.session.lib.mjs +15 -4
- package/src/telegram-merge-queue.lib.mjs +3 -1
- package/src/telegram-merge-wait.lib.mjs +7 -0
- package/src/transient-errors.lib.mjs +34 -3
|
@@ -30,7 +30,13 @@ const sentryLib = await import('./sentry.lib.mjs');
|
|
|
30
30
|
const { reportError } = sentryLib;
|
|
31
31
|
// Import GitHub merge functions
|
|
32
32
|
const githubMergeLib = await import('./github-merge.lib.mjs');
|
|
33
|
-
const {
|
|
33
|
+
const { mergePullRequest, getRepoVisibility, BILLING_LIMIT_ERROR_PATTERN, getDetailedCIStatus, rerunWorkflowRun, getWorkflowRunsForSha, getAllActiveRepoRuns, checkCIConsensus } = githubMergeLib;
|
|
34
|
+
// Issue #2182: guard rails for this loop (wall-clock ceiling, draft self-heal,
|
|
35
|
+
// classified merge failures). See solve.auto-merge-guards.lib.mjs.
|
|
36
|
+
const autoMergeGuards = await import('./solve.auto-merge-guards.lib.mjs');
|
|
37
|
+
const { DRAFT_RECHECK_DELAY_MS, evaluateWatchTimeout, resolveDraftBlocker, resolveMergeFailure } = autoMergeGuards;
|
|
38
|
+
// Re-exported so callers and tests keep a single entry point for the watch loop.
|
|
39
|
+
export const { DEFAULT_WATCH_TIMEOUT_HOURS, normalizeWatchTimeoutHours } = autoMergeGuards;
|
|
34
40
|
// Import GitHub functions for log attachment
|
|
35
41
|
const githubLib = await import('./github.lib.mjs');
|
|
36
42
|
const { sanitizeLogContent, attachLogToGitHub } = githubLib;
|
|
@@ -88,7 +94,6 @@ const { formatAutoIterationLimit, hasReachedAutoIterationLimit, normalizeAutoIte
|
|
|
88
94
|
const autoRestartBudget = await import('./auto-restart-budget.lib.mjs');
|
|
89
95
|
const { beginAutoRestartBudget, consumeAutoRestartIteration, formatAutoRestartLabel, formatAutoRestartLimit, hasExhaustedAutoRestartBudget } = autoRestartBudget;
|
|
90
96
|
const { failOnAutoRestartBudgetExhausted } = await import('./auto-restart-exhaustion.lib.mjs');
|
|
91
|
-
const { ensurePullRequestBaseBranch } = await import('./solve.pr-base-guard.lib.mjs');
|
|
92
97
|
// Issue #2119: an empty pull request must not be reported as ready to merge.
|
|
93
98
|
const { buildEmptyPullRequestBlocker, getPullRequestChangeStats } = await import('./pull-request-changes.lib.mjs');
|
|
94
99
|
// Issue #1895: explicitly close linked issues after merging a PR into a
|
|
@@ -128,6 +133,16 @@ export const watchUntilMergeable = async params => {
|
|
|
128
133
|
let lastKnownHeadSha = null;
|
|
129
134
|
// Issue #1567: Initial cooldown to let CI register and solution logs post
|
|
130
135
|
const INITIAL_COOLDOWN_SECONDS = MIN_CI_CHECK_INTERVAL_SECONDS;
|
|
136
|
+
// Issue #2182: this loop used to be `while (true)` with no wall-clock ceiling
|
|
137
|
+
// at all, so a permanently failing merge kept a single task "processing" for
|
|
138
|
+
// 4d 12h 13m. The timeout is the last-resort backstop below the specific
|
|
139
|
+
// fixes (draft detection, merge-failure classification).
|
|
140
|
+
const watchTimeoutHours = normalizeWatchTimeoutHours(argv.autoRestartUntilMergeableTimeoutHours ?? argv['auto-restart-until-mergeable-timeout-hours']);
|
|
141
|
+
const watchStartedAt = Date.now();
|
|
142
|
+
// Issue #2182: consecutive failed `gh pr merge` attempts, and how many times
|
|
143
|
+
// we silently restored "ready for review" on a pull request left as a draft.
|
|
144
|
+
// Mutable on purpose: the guard helpers update it in place.
|
|
145
|
+
const guardState = { consecutiveMergeFailures: 0, draftSelfHealCount: 0 };
|
|
131
146
|
|
|
132
147
|
await log('');
|
|
133
148
|
await log(formatAligned('🔄', 'AUTO-RESTART-UNTIL-MERGEABLE MODE ACTIVE', ''));
|
|
@@ -137,6 +152,7 @@ export const watchUntilMergeable = async params => {
|
|
|
137
152
|
await log(formatAligned('', 'Initial cooldown:', `${INITIAL_COOLDOWN_SECONDS} seconds`, 2));
|
|
138
153
|
await log(formatAligned('', 'Max restart iterations:', formatAutoRestartLimit(), 2));
|
|
139
154
|
await log(formatAligned('', 'Max limit resumes:', formatAutoIterationLimit(maxAutoResumeIterations), 2));
|
|
155
|
+
await log(formatAligned('', 'Watch timeout:', watchTimeoutHours > 0 ? `${watchTimeoutHours} hour(s)` : 'unlimited', 2));
|
|
140
156
|
await log(formatAligned('', 'Wait for all repo actions:', waitForAllRepoActionsFlag ? 'Yes (strict repo-wide safety)' : 'No (PR-scoped CI only)', 2));
|
|
141
157
|
await log(formatAligned('', 'Stop conditions:', 'PR merged, PR closed, or becomes mergeable', 2));
|
|
142
158
|
await log(formatAligned('', 'Restart triggers:', 'New non-bot comments, issue title/description edits, CI failures, merge conflicts', 2));
|
|
@@ -163,6 +179,15 @@ export const watchUntilMergeable = async params => {
|
|
|
163
179
|
while (true) {
|
|
164
180
|
iteration++;
|
|
165
181
|
const currentTime = new Date();
|
|
182
|
+
// Issue #2182: hard wall-clock ceiling for the whole monitoring loop.
|
|
183
|
+
const watchTimeout = evaluateWatchTimeout({ watchTimeoutHours, watchStartedAt, now: Date.now(), checksCompleted: iteration - 1 });
|
|
184
|
+
if (watchTimeout) {
|
|
185
|
+
await log('');
|
|
186
|
+
await log(formatAligned('⏱️', 'WATCH TIMEOUT REACHED:', watchTimeout.message, 2), { level: 'error' });
|
|
187
|
+
await log('');
|
|
188
|
+
await reportAutomationStop({ $, owner, repo, targetNumber: prNumber, reason: 'watch_timeout', mode: 'auto-restart-until-mergeable', message: watchTimeout.message, details: watchTimeout.details, verbose: argv.verbose, log });
|
|
189
|
+
return { success: false, reason: 'watch_timeout', latestSessionId, latestAnthropicCost };
|
|
190
|
+
}
|
|
166
191
|
const terminalState = await checkGitHubTerminalState({
|
|
167
192
|
owner,
|
|
168
193
|
repo,
|
|
@@ -217,6 +242,8 @@ export const watchUntilMergeable = async params => {
|
|
|
217
242
|
}
|
|
218
243
|
lastKnownHeadSha = currentHeadSha;
|
|
219
244
|
consecutiveNoRunsChecks = 0;
|
|
245
|
+
// Issue #2182: a new commit is a genuinely new merge attempt.
|
|
246
|
+
guardState.consecutiveMergeFailures = 0;
|
|
220
247
|
// Issue #1503: Also reset the readyToMergeCommentPosted flag when SHA changes,
|
|
221
248
|
// so a new "Ready to merge" comment can be posted for the new commit's CI results.
|
|
222
249
|
readyToMergeCommentPosted = false;
|
|
@@ -237,6 +264,23 @@ export const watchUntilMergeable = async params => {
|
|
|
237
264
|
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 });
|
|
238
265
|
return { success: false, reason: 'terminal_github_entity_error', latestSessionId, latestAnthropicCost };
|
|
239
266
|
}
|
|
267
|
+
// Issue #2182: a pull request that is still a draft while no AI session is
|
|
268
|
+
// running is a leftover from a restart iteration (executeToolIteration
|
|
269
|
+
// drafts the PR on entry). GitHub answers mergeable=MERGEABLE/CLEAN for it,
|
|
270
|
+
// so nothing else in this loop notices — the merge then fails with
|
|
271
|
+
// "Pull Request is still a draft" on every single check. Restore
|
|
272
|
+
// "ready for review" here instead of burning an AI restart iteration.
|
|
273
|
+
if (blockers.find(b => b.type === 'draft')) {
|
|
274
|
+
const decision = await resolveDraftBlocker({ owner, repo, prNumber, $, log, formatAligned, reportError, reportAutomationStop, verbose: argv.verbose, state: guardState });
|
|
275
|
+
if (decision.action === 'stop') {
|
|
276
|
+
return { success: false, reason: decision.reason, latestSessionId, latestAnthropicCost };
|
|
277
|
+
}
|
|
278
|
+
if (decision.action === 'retry') {
|
|
279
|
+
lastCheckTime = currentTime;
|
|
280
|
+
await interruptibleSleep(DRAFT_RECHECK_DELAY_MS);
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
240
284
|
// Issue #1503/#1918: Reset counter when CI checks exist (safety valve only for
|
|
241
285
|
// consecutive "no runs"). Issue #1918: do NOT reset while getMergeBlockers is still
|
|
242
286
|
// waiting for PR-triggered workflow runs to register (noWorkflowRunsForCommit). A
|
|
@@ -384,7 +428,21 @@ export const watchUntilMergeable = async params => {
|
|
|
384
428
|
}
|
|
385
429
|
return { success: true, reason: 'auto-merged', latestSessionId, latestAnthropicCost };
|
|
386
430
|
} else {
|
|
387
|
-
|
|
431
|
+
// Issue #2182: an unclassified merge failure used to be logged as
|
|
432
|
+
// "Will continue monitoring..." and retried every 120 seconds
|
|
433
|
+
// forever (5384 identical failures in the reported run). Classify
|
|
434
|
+
// it: self-heal what we can, stop on terminal causes, and cap the
|
|
435
|
+
// number of consecutive failures for everything else.
|
|
436
|
+
const decision = await resolveMergeFailure({ error: mergeResult.error, owner, repo, prNumber, $, log, formatAligned, reportError, reportAutomationStop, verbose: argv.verbose, state: guardState });
|
|
437
|
+
if (decision.action === 'stop') {
|
|
438
|
+
return { success: false, reason: decision.reason, error: mergeResult.error, latestSessionId, latestAnthropicCost };
|
|
439
|
+
}
|
|
440
|
+
if (decision.action === 'retry') {
|
|
441
|
+
lastCheckTime = currentTime;
|
|
442
|
+
await interruptibleSleep(DRAFT_RECHECK_DELAY_MS);
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
|
|
388
446
|
await log(formatAligned('', 'Will continue monitoring...', '', 2));
|
|
389
447
|
}
|
|
390
448
|
} else {
|
|
@@ -1250,89 +1308,17 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
|
|
|
1250
1308
|
const autoMergeAttempt = await import('./solve.auto-merge-attempt.lib.mjs');
|
|
1251
1309
|
export const { attemptAutoMerge, reportAutoMergeBlockedByIssue } = autoMergeAttempt;
|
|
1252
1310
|
/**
|
|
1253
|
-
* Start auto-restart-until-mergeable mode
|
|
1311
|
+
* Start auto-restart-until-mergeable mode.
|
|
1312
|
+
*
|
|
1313
|
+
* The pre-flight checks (mode detection, base-branch guard, fork detection,
|
|
1314
|
+
* merge permissions) live in solve.auto-merge-preflight.lib.mjs so this file
|
|
1315
|
+
* stays under the 1350-line advisory threshold (issue #1593).
|
|
1254
1316
|
*/
|
|
1317
|
+
const { runAutoMergePreflight } = await import('./solve.auto-merge-preflight.lib.mjs');
|
|
1255
1318
|
export const startAutoRestartUntilMergeable = async params => {
|
|
1256
|
-
const
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
const isAutoRestartUntilMergeable = argv.autoRestartUntilMergeable || false;
|
|
1260
|
-
if (!isAutoMerge && !isAutoRestartUntilMergeable) {
|
|
1261
|
-
return null; // Neither mode enabled
|
|
1262
|
-
}
|
|
1263
|
-
if (!prNumber) {
|
|
1264
|
-
await log('');
|
|
1265
|
-
await log(formatAligned('⚠️', 'Auto-restart-until-mergeable:', 'Requires a pull request'));
|
|
1266
|
-
await log(formatAligned('', 'Note:', 'This mode only works with existing PRs', 2));
|
|
1267
|
-
return null;
|
|
1268
|
-
}
|
|
1269
|
-
await ensurePullRequestBaseBranch({
|
|
1270
|
-
owner,
|
|
1271
|
-
repo,
|
|
1272
|
-
prNumber,
|
|
1273
|
-
argv,
|
|
1274
|
-
log,
|
|
1275
|
-
formatAligned,
|
|
1276
|
-
$,
|
|
1277
|
-
onMismatch: isAutoMerge ? 'throw' : 'restore',
|
|
1278
|
-
operation: isAutoMerge ? 'auto-merge' : 'auto-restart-until-mergeable',
|
|
1279
|
-
});
|
|
1280
|
-
// Issue #1226: Check if running in fork mode — auto-merge cannot work without write access
|
|
1281
|
-
if (argv.fork && isAutoMerge) {
|
|
1282
|
-
await log('');
|
|
1283
|
-
await log(formatAligned('⚠️', 'Auto-merge:', 'Cannot auto-merge fork PRs'));
|
|
1284
|
-
await log(formatAligned('', 'Reason:', 'Fork contributors do not have write access to merge PRs to upstream repositories', 2));
|
|
1285
|
-
await log(formatAligned('', 'Action:', 'PR is ready for manual merge by a repository maintainer', 2));
|
|
1286
|
-
await log('');
|
|
1287
|
-
|
|
1288
|
-
// Issue #1323: Post a comment to the PR notifying the maintainer (with deduplication)
|
|
1289
|
-
try {
|
|
1290
|
-
const readyToMergeSignature = `## ✅ ${READY_TO_MERGE_MARKER}`;
|
|
1291
|
-
const hasExistingComment = await checkForExistingComment(owner, repo, prNumber, readyToMergeSignature, argv.verbose);
|
|
1292
|
-
if (!hasExistingComment) {
|
|
1293
|
-
const commentBody = `## ✅ ${READY_TO_MERGE_MARKER}\n\nThis pull request is ready to be merged. Auto-merge was requested (\`--auto-merge\`) but cannot be performed because this PR was created from a fork (no write access to the target repository).\n\nPlease merge manually.\n\n---\n*hive-mind with --auto-merge flag (fork mode)*`;
|
|
1294
|
-
// Issue #1625: Track so this doesn't falsely count as AI-authored.
|
|
1295
|
-
await postTrackedComment({ $, owner, repo, targetNumber: prNumber, body: commentBody });
|
|
1296
|
-
await log(formatAligned('', '💬 Posted merge readiness notification to PR', '', 2));
|
|
1297
|
-
} else {
|
|
1298
|
-
await log(formatAligned('', `Skipping duplicate "${READY_TO_MERGE_MARKER}" comment`, '', 2));
|
|
1299
|
-
}
|
|
1300
|
-
} catch {
|
|
1301
|
-
// Don't fail if comment posting fails
|
|
1302
|
-
}
|
|
1303
|
-
return { success: false, reason: 'fork_no_write_access' };
|
|
1304
|
-
}
|
|
1305
|
-
// Issue #1226: Verify merge permissions before entering the auto-merge/restart loop
|
|
1306
|
-
if (isAutoMerge && owner && repo) {
|
|
1307
|
-
const { canMerge, permission } = await checkMergePermissions(owner, repo, argv.verbose);
|
|
1308
|
-
if (!canMerge) {
|
|
1309
|
-
await log('');
|
|
1310
|
-
await log(formatAligned('⚠️', 'Auto-merge:', 'Insufficient permissions to merge'));
|
|
1311
|
-
await log(formatAligned('', 'Permission level:', permission || 'unknown', 2));
|
|
1312
|
-
await log(formatAligned('', 'Required:', 'push, maintain, or admin access', 2));
|
|
1313
|
-
await log(formatAligned('', 'Action:', 'PR is ready for manual merge by a repository maintainer', 2));
|
|
1314
|
-
await log('');
|
|
1315
|
-
// Issue #1323: Post a comment to the PR notifying the maintainer (with deduplication)
|
|
1316
|
-
try {
|
|
1317
|
-
const readyToMergeSignature = `## ✅ ${READY_TO_MERGE_MARKER}`;
|
|
1318
|
-
const hasExistingComment = await checkForExistingComment(owner, repo, prNumber, readyToMergeSignature, argv.verbose);
|
|
1319
|
-
if (!hasExistingComment) {
|
|
1320
|
-
const commentBody = `## ✅ ${READY_TO_MERGE_MARKER}\n\nThis pull request is ready to be merged. Auto-merge was requested (\`--auto-merge\`) but cannot be performed because the authenticated user lacks write access to \`${owner}/${repo}\` (current permission: \`${permission || 'unknown'}\`).\n\nPlease merge manually.\n\n---\n*hive-mind with --auto-merge flag*`;
|
|
1321
|
-
// Issue #1625: Track so this doesn't falsely count as AI-authored.
|
|
1322
|
-
await postTrackedComment({ $, owner, repo, targetNumber: prNumber, body: commentBody });
|
|
1323
|
-
await log(formatAligned('', '💬 Posted merge readiness notification to PR', '', 2));
|
|
1324
|
-
} else {
|
|
1325
|
-
await log(formatAligned('', `Skipping duplicate "${READY_TO_MERGE_MARKER}" comment`, '', 2));
|
|
1326
|
-
}
|
|
1327
|
-
} catch {
|
|
1328
|
-
// Don't fail if comment posting fails
|
|
1329
|
-
}
|
|
1330
|
-
return { success: false, reason: 'insufficient_permissions' };
|
|
1331
|
-
}
|
|
1332
|
-
}
|
|
1333
|
-
// If --auto-merge implies --auto-restart-until-mergeable
|
|
1334
|
-
if (isAutoMerge) {
|
|
1335
|
-
argv.autoRestartUntilMergeable = true;
|
|
1319
|
+
const preflight = await runAutoMergePreflight(params);
|
|
1320
|
+
if (preflight.stop) {
|
|
1321
|
+
return preflight.result ?? null;
|
|
1336
1322
|
}
|
|
1337
1323
|
// Start the watch loop
|
|
1338
1324
|
return await watchUntilMergeable(params);
|
package/src/solve.config.lib.mjs
CHANGED
|
@@ -230,6 +230,14 @@ export const SOLVE_OPTION_DEFINITIONS = {
|
|
|
230
230
|
description: 'Maximum number of auto-restart iterations before stopping (default: 5, 0 = unlimited)',
|
|
231
231
|
default: 5,
|
|
232
232
|
},
|
|
233
|
+
// Issue #2182: the auto-restart-until-mergeable watch loop had no wall-clock
|
|
234
|
+
// ceiling, so a pull request that could never merge kept a task "processing"
|
|
235
|
+
// for 4d 12h 13m.
|
|
236
|
+
'auto-restart-until-mergeable-timeout-hours': {
|
|
237
|
+
type: 'number',
|
|
238
|
+
description: 'Maximum wall-clock hours the --auto-restart-until-mergeable/--auto-merge monitoring loop may run before stopping and reporting (default: 24, 0 = unlimited)',
|
|
239
|
+
default: 24,
|
|
240
|
+
},
|
|
233
241
|
'resume-on-auto-restart': {
|
|
234
242
|
type: 'boolean',
|
|
235
243
|
description: '[EXPERIMENTAL] Resume the previous Claude session on uncommitted-change auto-restart and send only a minimal restart prompt. Disabled by default.',
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Interrupt wrapper factory for CTRL+C handling in solve sessions.
|
|
3
3
|
*
|
|
4
|
-
* On SIGINT, auto-commits uncommitted changes
|
|
4
|
+
* On SIGINT, auto-commits uncommitted changes, restores the pull request draft state and
|
|
5
|
+
* uploads session logs if --attach-logs is enabled.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -59,6 +60,22 @@ export const createInterruptWrapper = ({ cleanupContext, checkForUncommittedChan
|
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
// Issue #2182: CTRL+C ends the working session, so the pull request must go back to
|
|
64
|
+
// "ready for review" exactly like a normal session end. This runs before the log upload
|
|
65
|
+
// on purpose: it is two fast gh calls, while attaching a multi-MB log can be cut off by
|
|
66
|
+
// the isolation backend's SIGKILL (#2052). A pull request left in draft can never be
|
|
67
|
+
// merged by --auto-merge, so restoring it is the more important of the two.
|
|
68
|
+
try {
|
|
69
|
+
const { restorePullRequestsLeftInDraft } = await import('./pr-draft-state.lib.mjs');
|
|
70
|
+
await trace('draft-restore: start');
|
|
71
|
+
await restorePullRequestsLeftInDraft({ $, log, reason: 'session interrupted (CTRL+C)' });
|
|
72
|
+
await trace('draft-restore: done');
|
|
73
|
+
} catch (restoreError) {
|
|
74
|
+
await log(`⚠️ Could not restore pull request draft state on interrupt: ${restoreError.message}`, {
|
|
75
|
+
level: 'warning',
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
62
79
|
// Upload logs if --attach-logs is enabled and we have a PR
|
|
63
80
|
if (shouldAttachLogs && ctx.prNumber && ctx.owner && ctx.repo) {
|
|
64
81
|
await log('📎 Uploading interrupted session logs to Pull Request...');
|
package/src/solve.mjs
CHANGED
|
@@ -1185,6 +1185,17 @@ try {
|
|
|
1185
1185
|
logsAttached = true;
|
|
1186
1186
|
}
|
|
1187
1187
|
}
|
|
1188
|
+
// Issue #2182: the AI working session is over at this point — everything below is
|
|
1189
|
+
// monitoring and merging, not working. The pull request must therefore be back in
|
|
1190
|
+
// "ready for review" BEFORE the auto-merge watch loop starts, because that loop can
|
|
1191
|
+
// run for days and endWorkSession() (further down) is unreachable until it returns.
|
|
1192
|
+
// In the reported run the PR was left in draft by an auto-restart iteration and
|
|
1193
|
+
// `gh pr merge` answered "Pull Request is still a draft" 2692 times over 4d 12h.
|
|
1194
|
+
if (prNumber) {
|
|
1195
|
+
const { ensurePullRequestIsReady } = await import('./pr-draft-state.lib.mjs');
|
|
1196
|
+
await ensurePullRequestIsReady({ owner, repo, prNumber, $, log, formatAligned, reason: 'AI working session finished', reportError });
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1188
1199
|
// Start auto-restart-until-mergeable mode if enabled This runs after the normal watch mode completes (if any) --auto-merge implies --auto-restart-until-mergeable
|
|
1189
1200
|
if (argv.autoMerge || argv.autoRestartUntilMergeable) {
|
|
1190
1201
|
const autoMergeResult = await startAutoRestartUntilMergeable({
|
|
@@ -1220,6 +1231,14 @@ try {
|
|
|
1220
1231
|
await endWorkSession({ isContinueMode, prNumber, argv, log, formatAligned, $, logsAttached });
|
|
1221
1232
|
} catch (error) {
|
|
1222
1233
|
await finalizeDevelopmentLog(); // Preserve failed/interrupted sessions too.
|
|
1234
|
+
// Issue #2182: a failed session is still a finished session. Restore every pull request
|
|
1235
|
+
// this process put into draft, otherwise the failure leaves it permanently unmergeable.
|
|
1236
|
+
try {
|
|
1237
|
+
const { restorePullRequestsLeftInDraft } = await import('./pr-draft-state.lib.mjs');
|
|
1238
|
+
await restorePullRequestsLeftInDraft({ $, log, formatAligned, reason: 'working session failed', reportError });
|
|
1239
|
+
} catch (restoreError) {
|
|
1240
|
+
await log(`Warning: Could not restore pull request draft state: ${restoreError.message}`, { level: 'warning' });
|
|
1241
|
+
}
|
|
1223
1242
|
// Don't report authentication errors to Sentry as they are user configuration issues
|
|
1224
1243
|
if (!error.isAuthError) {
|
|
1225
1244
|
reportError(error, {
|
|
@@ -7,8 +7,18 @@
|
|
|
7
7
|
// issue comment so it's excluded from --auto-attach-solution-summary's check.
|
|
8
8
|
import { REPOSITORY_INITIALIZATION_REQUIRED_MARKER, postTrackedComment } from './tool-comments.lib.mjs';
|
|
9
9
|
import { QUIET_PROBE } from './quiet-probe.lib.mjs'; // issue #2130: keep read-only probe payloads out of the attached log
|
|
10
|
+
// Issue #2192: a credential helper is never consulted for a *public* clone
|
|
11
|
+
// (github.com answers 200, so git never asks), which is why an authenticated
|
|
12
|
+
// container still got throttled as anonymous. The token has to be sent
|
|
13
|
+
// preemptively, before the first git network call.
|
|
14
|
+
import { ensureAuthenticatedGitTransport } from './git-auth-transport.lib.mjs';
|
|
10
15
|
|
|
11
16
|
export async function setupRepositoryAndClone({ argv, owner, repo, forkOwner, forkRepoName, tempDir, isContinueMode, issueUrl, log, $, needsClone = true }) {
|
|
17
|
+
// Issue #2192: authenticate git *before* the first clone/fetch. Doing this
|
|
18
|
+
// afterwards (as setupGitCredentialHelper does) is too late — the clone is the
|
|
19
|
+
// call GitHub rejected with "temporarily limiting some unauthenticated downloads".
|
|
20
|
+
await ensureAuthenticatedGitTransport({ $, log, reason: 'repository setup' });
|
|
21
|
+
|
|
12
22
|
// Set up repository and handle forking
|
|
13
23
|
const { repoToClone, forkedRepo, upstreamRemote, prForkOwner } = await setupRepository(argv, owner, repo, forkOwner, issueUrl, forkRepoName);
|
|
14
24
|
|
|
@@ -31,6 +31,9 @@ import { ensureAiToolScratchIgnored } from './ai-tool-scratch.lib.mjs';
|
|
|
31
31
|
import { parseForkFullNameFromGhOutput } from './github-repository-names.lib.mjs';
|
|
32
32
|
import { checkReplacementRepositoryBranchSafety } from './solve.repository-safety.lib.mjs';
|
|
33
33
|
import { buildForkReplacementBlockedReason, buildForkReplacementSafetyCheckDescription } from './solve.repository-recovery-message.lib.mjs';
|
|
34
|
+
// Issue #2192: GitHub throttles *anonymous* git downloads; a token must be sent
|
|
35
|
+
// preemptively (a credential helper is never consulted for a public repository).
|
|
36
|
+
import { GIT_AUTH_TRANSPORT_DISABLE, ensureAuthenticatedGitTransport, isAnonymousDownloadLimit } from './git-auth-transport.lib.mjs';
|
|
34
37
|
|
|
35
38
|
// Import GitHub utilities for permission checks
|
|
36
39
|
const githubLib = await import('./github.lib.mjs');
|
|
@@ -949,6 +952,14 @@ export const classifyCloneError = errorOutput => {
|
|
|
949
952
|
return { type: 'NETWORK', retryable: true, description: 'Network connectivity issue (interrupted transfer)' };
|
|
950
953
|
}
|
|
951
954
|
|
|
955
|
+
// Issue #2192: GitHub refusing an *unauthenticated* download. Retryable, but
|
|
956
|
+
// waiting is not the remedy — the clone has to be authenticated. Checked
|
|
957
|
+
// before PERMISSION/NOT_FOUND/RATE_LIMIT because GitHub's wording ("limiting",
|
|
958
|
+
// "retry later or authenticate") overlaps all three.
|
|
959
|
+
if (isAnonymousDownloadLimit(errorOutput)) {
|
|
960
|
+
return { type: 'ANONYMOUS_RATE_LIMIT', retryable: true, description: 'GitHub is limiting unauthenticated downloads (this clone was not authenticated)' };
|
|
961
|
+
}
|
|
962
|
+
|
|
952
963
|
// Authentication/permission errors - not retryable
|
|
953
964
|
if (output.includes('error: 401') || output.includes('error: 403') || output.includes('authentication failed') || output.includes('permission denied')) {
|
|
954
965
|
return { type: 'PERMISSION', retryable: false, description: 'Authentication or permission error' };
|
|
@@ -1075,6 +1086,9 @@ export const cloneRepository = async (repoToClone, tempDir, argv, owner, repo) =
|
|
|
1075
1086
|
await log(' • Network connectivity issues');
|
|
1076
1087
|
if (errorClassification.type === 'TRANSIENT') await log(' • GitHub server issues (temporary)');
|
|
1077
1088
|
if (errorClassification.type === 'RATE_LIMIT') await log(' • API rate limiting exceeded');
|
|
1089
|
+
// Issue #2192: the request never carried an Authorization header, so
|
|
1090
|
+
// GitHub counted it against the anonymous budget regardless of `gh auth status`.
|
|
1091
|
+
if (errorClassification.type === 'ANONYMOUS_RATE_LIMIT') await log(' • The clone was sent anonymously — GitHub throttles unauthenticated downloads');
|
|
1078
1092
|
// Issue #1957: the transfer started but was interrupted (e.g. the connection
|
|
1079
1093
|
// dropped while reading the pack). The retries above were already exhausted.
|
|
1080
1094
|
if (errorClassification.type === 'NETWORK') await log(' • Connection dropped mid-transfer (the clone was interrupted before completing)');
|
|
@@ -1087,6 +1101,11 @@ export const cloneRepository = async (repoToClone, tempDir, argv, owner, repo) =
|
|
|
1087
1101
|
if (argv.fork) await log(` 4. Check fork: gh repo view ${repoToClone}`);
|
|
1088
1102
|
if (errorClassification.type === 'TRANSIENT') await log(' 5. Wait and retry / check: https://www.githubstatus.com');
|
|
1089
1103
|
if (errorClassification.type === 'RATE_LIMIT') await log(' 5. Wait for rate limit to reset or use --token with different token');
|
|
1104
|
+
if (errorClassification.type === 'ANONYMOUS_RATE_LIMIT') {
|
|
1105
|
+
await log(' 5. Make sure a token is available to git: gh auth token (or set GH_TOKEN)');
|
|
1106
|
+
await log(' 6. Repair the git/gh state non-interactively: gh-setup-git-identity --repair');
|
|
1107
|
+
await log(` 7. Hive Mind normally authenticates git itself; if that was turned off, unset ${GIT_AUTH_TRANSPORT_DISABLE}`);
|
|
1108
|
+
}
|
|
1090
1109
|
if (errorClassification.type === 'NETWORK') {
|
|
1091
1110
|
await log(' 5. Check your network connection / VPN / proxy, then re-run the command');
|
|
1092
1111
|
await log(' 6. On slow or unstable links, a shallower history transfers faster and is less');
|
|
@@ -1100,6 +1119,13 @@ export const cloneRepository = async (repoToClone, tempDir, argv, owner, repo) =
|
|
|
1100
1119
|
// Retryable error and we have attempts left
|
|
1101
1120
|
const delay = baseDelay * Math.pow(2, attempt - 1); // Exponential backoff
|
|
1102
1121
|
await log(`${formatAligned('⚠️', 'Clone failed:', errorClassification.description)}`);
|
|
1122
|
+
// Issue #2192: auto-recovery. GitHub rejected the download as anonymous, so a
|
|
1123
|
+
// plain retry would be rejected the same way. Authenticate the transport (and,
|
|
1124
|
+
// if no token is reachable, let `gh-setup-git-identity --repair` restore the
|
|
1125
|
+
// gh state non-interactively) before spending the next attempt.
|
|
1126
|
+
if (errorClassification.type === 'ANONYMOUS_RATE_LIMIT') {
|
|
1127
|
+
await ensureAuthenticatedGitTransport({ $, log, repair: true, reason: 'GitHub rejected the clone as unauthenticated' });
|
|
1128
|
+
}
|
|
1103
1129
|
await log(`${formatAligned('⏳', 'Retrying:', `Waiting ${delay / 1000}s before attempt ${attempt + 1}/${maxRetries}...`)}`);
|
|
1104
1130
|
if (errorClassification.type === 'RATE_LIMIT') {
|
|
1105
1131
|
await log(' 💡 Tip: Rate limiting detected - using longer delay');
|