@link-assistant/hive-mind 1.34.3 → 1.34.5
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 +20 -0
- package/package.json +1 -1
- package/src/claude.lib.mjs +11 -11
- package/src/solve.auto-merge.lib.mjs +111 -74
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.34.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- ab070db: Use workflow runs API to detect when CI is not triggered, preventing infinite loop (Issue #1442)
|
|
8
|
+
|
|
9
|
+
When `--auto-restart-until-mergeable` monitors a PR in a repo that has active GitHub Actions workflows but CI checks never start (e.g., fork PRs needing maintainer approval, `paths-ignore` filtering all changed files, workflow trigger conditions not matching), the monitoring loop now exits immediately instead of waiting indefinitely.
|
|
10
|
+
|
|
11
|
+
Instead of using a timeout-based approach, the fix uses the GitHub Actions workflow runs API (`repos/{owner}/{repo}/actions/runs?head_sha={sha}`) to definitively determine if any workflow runs were triggered for the PR's commit. If zero workflow runs exist, CI was not triggered and there is nothing to wait for — the system exits immediately with a diagnostic PR comment.
|
|
12
|
+
|
|
13
|
+
## 1.34.4
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- c3806b5: Fix missing log upload on tool failure and make HTTP 529 overload error retryable (Issue #1439)
|
|
18
|
+
|
|
19
|
+
Two fixes:
|
|
20
|
+
1. When `--attach-logs` is enabled and the tool execution fails during an auto-restart session, the failure log was not being uploaded to GitHub. Now the log is attached before stopping on both tool execution failure paths.
|
|
21
|
+
2. HTTP 529 (Anthropic "Overloaded") errors were not recognized as transient/retryable by the outer retry loop. The code only matched `API Error: 500` + `Overloaded`, but 529 uses `API Error: 529` + `overloaded_error`. Now both 500 and 529 overload errors trigger the retry logic with exponential backoff.
|
|
22
|
+
|
|
3
23
|
## 1.34.3
|
|
4
24
|
|
|
5
25
|
### Patch Changes
|
package/package.json
CHANGED
package/src/claude.lib.mjs
CHANGED
|
@@ -129,8 +129,8 @@ export const validateClaudeConnection = async (model = 'haiku-3') => {
|
|
|
129
129
|
return null;
|
|
130
130
|
};
|
|
131
131
|
const jsonError = checkForJsonError(stdout) || checkForJsonError(stderr);
|
|
132
|
-
// Check for API overload error pattern
|
|
133
|
-
const isOverloadError = (stdout.includes('API Error: 500') && stdout.includes('Overloaded')) || (stderr.includes('API Error: 500') && stderr.includes('Overloaded')) || (jsonError && jsonError.type === 'api_error' && jsonError.message === 'Overloaded');
|
|
132
|
+
// Check for API overload error pattern (Issue #1439: also detect 529 overloaded_error)
|
|
133
|
+
const isOverloadError = (stdout.includes('API Error: 500') && stdout.includes('Overloaded')) || (stdout.includes('API Error: 529') && stdout.includes('Overloaded')) || (stderr.includes('API Error: 500') && stderr.includes('Overloaded')) || (stderr.includes('API Error: 529') && stderr.includes('Overloaded')) || (jsonError && (jsonError.type === 'api_error' || jsonError.type === 'overloaded_error') && jsonError.message === 'Overloaded');
|
|
134
134
|
// Handle overload errors with retry
|
|
135
135
|
if (isOverloadError) {
|
|
136
136
|
if (retryCount < maxRetries) {
|
|
@@ -168,7 +168,7 @@ export const validateClaudeConnection = async (model = 'haiku-3') => {
|
|
|
168
168
|
}
|
|
169
169
|
// Check for error patterns in successful response
|
|
170
170
|
if (jsonError) {
|
|
171
|
-
if (jsonError.type === 'api_error' && jsonError.message === 'Overloaded') {
|
|
171
|
+
if ((jsonError.type === 'api_error' || jsonError.type === 'overloaded_error') && jsonError.message === 'Overloaded') {
|
|
172
172
|
if (retryCount < maxRetries) {
|
|
173
173
|
const delay = baseDelay * Math.pow(2, retryCount);
|
|
174
174
|
await log(`⚠️ API overload error in response. Retrying in ${delay / 1000} seconds...`, {
|
|
@@ -193,7 +193,7 @@ export const validateClaudeConnection = async (model = 'haiku-3') => {
|
|
|
193
193
|
return true;
|
|
194
194
|
} catch (error) {
|
|
195
195
|
const errorStr = error.message || error.toString();
|
|
196
|
-
if ((errorStr.includes('API Error: 500') && errorStr.includes('Overloaded')) || (errorStr.includes('api_error') && errorStr.includes('Overloaded'))) {
|
|
196
|
+
if ((errorStr.includes('API Error: 500') && errorStr.includes('Overloaded')) || (errorStr.includes('API Error: 529') && errorStr.includes('Overloaded')) || (errorStr.includes('api_error') && errorStr.includes('Overloaded')) || (errorStr.includes('overloaded_error') && errorStr.includes('Overloaded'))) {
|
|
197
197
|
if (retryCount < maxRetries) {
|
|
198
198
|
const delay = baseDelay * Math.pow(2, retryCount);
|
|
199
199
|
await log(`⚠️ API overload error during validation. Retrying in ${delay / 1000} seconds...`, {
|
|
@@ -1067,11 +1067,11 @@ export const executeClaudeCommand = async params => {
|
|
|
1067
1067
|
const content = Array.isArray(data.message.content) ? data.message.content : [data.message.content];
|
|
1068
1068
|
for (const item of content) {
|
|
1069
1069
|
if (item.type === 'text' && item.text) {
|
|
1070
|
-
// Check for the specific 500 overload error pattern
|
|
1071
|
-
if (item.text.includes('API Error: 500') && item.text.includes('api_error') && item.text.includes('Overloaded')) {
|
|
1070
|
+
// Check for the specific 500/529 overload error pattern (Issue #1439: 529 is also an overload)
|
|
1071
|
+
if ((item.text.includes('API Error: 500') || item.text.includes('API Error: 529')) && (item.text.includes('api_error') || item.text.includes('overloaded_error')) && item.text.includes('Overloaded')) {
|
|
1072
1072
|
isOverloadError = true;
|
|
1073
1073
|
lastMessage = item.text;
|
|
1074
|
-
await log(
|
|
1074
|
+
await log(`⚠️ Detected API overload error${item.text.includes('529') ? ' (529)' : ' (500)'}`, { verbose: true });
|
|
1075
1075
|
}
|
|
1076
1076
|
if (item.text.includes('API Error: 500') && item.text.includes('Internal server error') && !item.text.includes('Overloaded')) {
|
|
1077
1077
|
isInternalServerError = true;
|
|
@@ -1194,7 +1194,7 @@ export const executeClaudeCommand = async params => {
|
|
|
1194
1194
|
|
|
1195
1195
|
// Issues #1331, #1353: Unified handler for transient API errors (Overloaded, 503, Internal Server Error,
|
|
1196
1196
|
// Request timed out). All use exponential backoff with session preservation via --resume.
|
|
1197
|
-
const isTransientError = isOverloadError || isInternalServerError || is503Error || isRequestTimeout || (lastMessage.includes('API Error: 500') && (lastMessage.includes('Overloaded') || lastMessage.includes('Internal server error'))) || (lastMessage.includes('api_error') && lastMessage.includes('Overloaded')) || lastMessage.includes('API Error: 503') || (lastMessage.includes('503') && (lastMessage.includes('upstream connect error') || lastMessage.includes('remote connection failure'))) || lastMessage === 'Request timed out' || lastMessage.includes('Request timed out');
|
|
1197
|
+
const isTransientError = isOverloadError || isInternalServerError || is503Error || isRequestTimeout || (lastMessage.includes('API Error: 500') && (lastMessage.includes('Overloaded') || lastMessage.includes('Internal server error'))) || (lastMessage.includes('API Error: 529') && (lastMessage.includes('overloaded_error') || lastMessage.includes('Overloaded'))) || (lastMessage.includes('api_error') && lastMessage.includes('Overloaded')) || (lastMessage.includes('overloaded_error') && lastMessage.includes('Overloaded')) || lastMessage.includes('API Error: 503') || (lastMessage.includes('503') && (lastMessage.includes('upstream connect error') || lastMessage.includes('remote connection failure'))) || lastMessage === 'Request timed out' || lastMessage.includes('Request timed out');
|
|
1198
1198
|
if ((commandFailed || isTransientError) && isTransientError) {
|
|
1199
1199
|
// Issue #1353: Timeouts use longer backoff (5min–1hr) vs general transient (2min–30min)
|
|
1200
1200
|
const maxRetries = isRequestTimeout ? retryLimits.maxRequestTimeoutRetries : retryLimits.maxTransientErrorRetries;
|
|
@@ -1222,7 +1222,7 @@ export const executeClaudeCommand = async params => {
|
|
|
1222
1222
|
}
|
|
1223
1223
|
if (retryCount < maxRetries) {
|
|
1224
1224
|
const delay = Math.min(initialDelay * Math.pow(retryLimits.retryBackoffMultiplier, retryCount), maxDelay);
|
|
1225
|
-
const errorLabel = isRequestTimeout ? 'Request timeout' : isOverloadError || (lastMessage.includes('API Error: 500') && lastMessage.includes('Overloaded'))
|
|
1225
|
+
const errorLabel = isRequestTimeout ? 'Request timeout' : isOverloadError || (lastMessage.includes('API Error: 500') && lastMessage.includes('Overloaded')) || (lastMessage.includes('API Error: 529') && lastMessage.includes('Overloaded')) ? `API overload (${lastMessage.includes('529') ? '529' : '500'})` : isInternalServerError || lastMessage.includes('Internal server error') ? 'Internal server error (500)' : '503 network error';
|
|
1226
1226
|
const notRetryableHint = apiMarkedNotRetryable ? ' (API says not retryable — will stop early if no progress)' : '';
|
|
1227
1227
|
await log(`\n⚠️ ${errorLabel} detected. Retry ${retryCount + 1}/${maxRetries} in ${Math.round(delay / 60000)} min (session preserved)${notRetryableHint}...`, { level: 'warning' });
|
|
1228
1228
|
await log(` Error: ${lastMessage.substring(0, 200)}`, { verbose: true });
|
|
@@ -1393,7 +1393,7 @@ export const executeClaudeCommand = async params => {
|
|
|
1393
1393
|
// Issue #1353: Also handle "Request timed out" in exception block
|
|
1394
1394
|
// (Overloaded, 503, Internal Server Error, Request timed out) - all with session preservation
|
|
1395
1395
|
const isTimeoutException = errorStr === 'Request timed out' || errorStr.includes('Request timed out');
|
|
1396
|
-
const isTransientException = isTimeoutException || (errorStr.includes('API Error: 500') && (errorStr.includes('Overloaded') || errorStr.includes('Internal server error'))) || (errorStr.includes('api_error') && errorStr.includes('Overloaded')) || errorStr.includes('API Error: 503') || (errorStr.includes('503') && (errorStr.includes('upstream connect error') || errorStr.includes('remote connection failure')));
|
|
1396
|
+
const isTransientException = isTimeoutException || (errorStr.includes('API Error: 500') && (errorStr.includes('Overloaded') || errorStr.includes('Internal server error'))) || (errorStr.includes('API Error: 529') && (errorStr.includes('overloaded_error') || errorStr.includes('Overloaded'))) || (errorStr.includes('api_error') && errorStr.includes('Overloaded')) || (errorStr.includes('overloaded_error') && errorStr.includes('Overloaded')) || errorStr.includes('API Error: 503') || (errorStr.includes('503') && (errorStr.includes('upstream connect error') || errorStr.includes('remote connection failure')));
|
|
1397
1397
|
if (isTransientException) {
|
|
1398
1398
|
// Issue #1353: Use timeout-specific backoff for request timeouts
|
|
1399
1399
|
const maxRetries = isTimeoutException ? retryLimits.maxRequestTimeoutRetries : retryLimits.maxTransientErrorRetries;
|
|
@@ -1401,7 +1401,7 @@ export const executeClaudeCommand = async params => {
|
|
|
1401
1401
|
const maxDelay = isTimeoutException ? retryLimits.maxRequestTimeoutDelayMs : retryLimits.maxTransientErrorDelayMs;
|
|
1402
1402
|
if (retryCount < maxRetries) {
|
|
1403
1403
|
const delay = Math.min(initialDelay * Math.pow(retryLimits.retryBackoffMultiplier, retryCount), maxDelay);
|
|
1404
|
-
const errorLabel = isTimeoutException ? 'Request timeout' : errorStr.includes('Overloaded') ?
|
|
1404
|
+
const errorLabel = isTimeoutException ? 'Request timeout' : errorStr.includes('Overloaded') ? `API overload (${errorStr.includes('529') ? '529' : '500'})` : errorStr.includes('Internal server error') ? 'Internal server error (500)' : '503 network error';
|
|
1405
1405
|
await log(`\n⚠️ ${errorLabel} in exception. Retry ${retryCount + 1}/${maxRetries} in ${Math.round(delay / 60000)} min (session preserved)...`, { level: 'warning' });
|
|
1406
1406
|
if (sessionId && !argv.resume) argv.resume = sessionId;
|
|
1407
1407
|
await waitWithCountdown(delay, log);
|
|
@@ -197,6 +197,7 @@ const getMergeBlockers = async (owner, repo, prNumber, verbose = false) => {
|
|
|
197
197
|
// No CI checks exist yet - this could be:
|
|
198
198
|
// 1. A race condition after push (checks haven't started yet) - wait
|
|
199
199
|
// 2. A repository with no CI/CD configured at all - should be mergeable immediately
|
|
200
|
+
// 3. CI workflows exist but were not triggered for this commit (fork PR, paths-ignore, etc.)
|
|
200
201
|
//
|
|
201
202
|
// Issue #1345: Distinguish by checking the PR's mergeability status.
|
|
202
203
|
// If GitHub says the PR is MERGEABLE (mergeStateStatus === 'CLEAN'),
|
|
@@ -215,16 +216,33 @@ const getMergeBlockers = async (owner, repo, prNumber, verbose = false) => {
|
|
|
215
216
|
// - check_runs=[] (CI hasn't started yet — race condition)
|
|
216
217
|
const repoWorkflows = await getActiveRepoWorkflows(owner, repo, verbose);
|
|
217
218
|
if (repoWorkflows.hasWorkflows) {
|
|
218
|
-
// Repo HAS workflows —
|
|
219
|
-
//
|
|
220
|
-
if
|
|
221
|
-
|
|
219
|
+
// Repo HAS workflows — but were they triggered for this commit?
|
|
220
|
+
// Issue #1442: Use the GitHub Actions workflow runs API to definitively check
|
|
221
|
+
// if any workflow runs were triggered for this PR's HEAD SHA. This avoids
|
|
222
|
+
// the need for timeout-based detection:
|
|
223
|
+
// - workflow_runs.length > 0 → genuine race condition (CI started, check-runs not yet registered)
|
|
224
|
+
// - workflow_runs.length === 0 → CI was NOT triggered (fork PR, paths-ignore, etc.)
|
|
225
|
+
const workflowRuns = await getWorkflowRunsForSha(owner, repo, ciStatus.sha, verbose);
|
|
226
|
+
if (workflowRuns.length > 0) {
|
|
227
|
+
// Workflow runs exist but check-runs haven't appeared yet — genuine race condition
|
|
228
|
+
if (verbose) {
|
|
229
|
+
console.log(`[VERBOSE] /merge: PR #${prNumber} has no CI check-runs yet, but ${workflowRuns.length} workflow run(s) were triggered for SHA ${ciStatus.sha.substring(0, 7)} - genuine race condition (waiting for check-runs to appear)`);
|
|
230
|
+
}
|
|
231
|
+
blockers.push({
|
|
232
|
+
type: 'ci_pending',
|
|
233
|
+
message: `CI/CD checks have not started yet (${workflowRuns.length} workflow run(s) triggered, waiting for check-runs to appear)`,
|
|
234
|
+
details: workflowRuns.map(r => r.name),
|
|
235
|
+
});
|
|
236
|
+
} else {
|
|
237
|
+
// No workflow runs for this SHA — CI was definitively NOT triggered
|
|
238
|
+
// Issue #1442: This is the root cause of the infinite loop. Fork PRs needing
|
|
239
|
+
// maintainer approval, paths-ignore filtering, workflow conditions not matching,
|
|
240
|
+
// etc. all result in zero workflow runs. No need for timeout — exit immediately.
|
|
241
|
+
if (verbose) {
|
|
242
|
+
console.log(`[VERBOSE] /merge: PR #${prNumber} has no CI checks and no workflow runs for SHA ${ciStatus.sha.substring(0, 7)} — CI was not triggered (fork PR, paths-ignore, workflow conditions, etc.)`);
|
|
243
|
+
}
|
|
244
|
+
return { blockers, ciStatus, noCiConfigured: false, noCiTriggered: true };
|
|
222
245
|
}
|
|
223
|
-
blockers.push({
|
|
224
|
-
type: 'ci_pending',
|
|
225
|
-
message: `CI/CD checks have not started yet (${repoWorkflows.count} workflow(s) configured, waiting for checks to appear)`,
|
|
226
|
-
details: repoWorkflows.workflows.map(wf => wf.name),
|
|
227
|
-
});
|
|
228
246
|
} else {
|
|
229
247
|
// Repo has NO workflows — this is truly "no CI configured"
|
|
230
248
|
// PR is already mergeable with no CI checks configured.
|
|
@@ -323,7 +341,7 @@ const getMergeBlockers = async (owner, repo, prNumber, verbose = false) => {
|
|
|
323
341
|
});
|
|
324
342
|
}
|
|
325
343
|
|
|
326
|
-
return { blockers, ciStatus, noCiConfigured: false };
|
|
344
|
+
return { blockers, ciStatus, noCiConfigured: false, noCiTriggered: false };
|
|
327
345
|
};
|
|
328
346
|
|
|
329
347
|
/**
|
|
@@ -368,12 +386,6 @@ export const watchUntilMergeable = async params => {
|
|
|
368
386
|
let iteration = 0;
|
|
369
387
|
let lastCheckTime = new Date();
|
|
370
388
|
|
|
371
|
-
// Issue #1335: Cache whether the repo has CI workflows to avoid repeated API calls.
|
|
372
|
-
// When 'no_checks' is seen, we check if the repo actually has workflows configured.
|
|
373
|
-
// - If no workflows exist → 'no_checks' is permanent; treat PR as CI-passing and exit.
|
|
374
|
-
// - If workflows exist → 'no_checks' is a transient race condition; keep waiting.
|
|
375
|
-
let repoHasWorkflows = null; // null = not yet checked; true/false = cached result
|
|
376
|
-
|
|
377
389
|
while (true) {
|
|
378
390
|
iteration++;
|
|
379
391
|
const currentTime = new Date();
|
|
@@ -402,7 +414,7 @@ export const watchUntilMergeable = async params => {
|
|
|
402
414
|
|
|
403
415
|
try {
|
|
404
416
|
// Get merge blockers
|
|
405
|
-
const { blockers, noCiConfigured } = await getMergeBlockers(owner, repo, prNumber, argv.verbose);
|
|
417
|
+
const { blockers, noCiConfigured, noCiTriggered } = await getMergeBlockers(owner, repo, prNumber, argv.verbose);
|
|
406
418
|
|
|
407
419
|
// Check for new comments from non-bot users
|
|
408
420
|
const { hasNewComments, comments } = await checkForNonBotComments(owner, repo, prNumber, issueNumber, lastCheckTime, argv.verbose);
|
|
@@ -410,6 +422,12 @@ export const watchUntilMergeable = async params => {
|
|
|
410
422
|
// Check for uncommitted changes using shared utility
|
|
411
423
|
const hasUncommittedChanges = await checkForUncommittedChanges(tempDir, argv);
|
|
412
424
|
|
|
425
|
+
// Issue #1442: If CI workflows exist but were not triggered for this commit,
|
|
426
|
+
// log why before proceeding to the mergeable path.
|
|
427
|
+
if (noCiTriggered) {
|
|
428
|
+
await log(formatAligned('ℹ️', 'CI not triggered:', 'Workflows exist but no workflow runs for this commit (fork PR, paths-ignore, workflow conditions)', 2));
|
|
429
|
+
}
|
|
430
|
+
|
|
413
431
|
// If PR is mergeable, no blockers, no new comments, and no uncommitted changes
|
|
414
432
|
if (blockers.length === 0 && !hasNewComments && !hasUncommittedChanges) {
|
|
415
433
|
await log(formatAligned('✅', 'PR IS MERGEABLE!', ''));
|
|
@@ -426,7 +444,7 @@ export const watchUntilMergeable = async params => {
|
|
|
426
444
|
// Post success comment
|
|
427
445
|
try {
|
|
428
446
|
// Issue #1345: Differentiate message when no CI is configured
|
|
429
|
-
const ciLine = noCiConfigured ? '- No CI/CD checks are configured for this repository' : '- All CI checks have passed';
|
|
447
|
+
const ciLine = noCiConfigured ? '- No CI/CD checks are configured for this repository' : noCiTriggered ? '- CI workflows exist but were not triggered for this commit' : '- All CI checks have passed';
|
|
430
448
|
const commentBody = `## 🎉 Auto-merged\n\nThis pull request has been automatically merged by hive-mind.\n${ciLine}\n\n---\n*Auto-merged by hive-mind with --auto-merge flag*`;
|
|
431
449
|
await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${commentBody}`;
|
|
432
450
|
} catch {
|
|
@@ -450,7 +468,7 @@ export const watchUntilMergeable = async params => {
|
|
|
450
468
|
try {
|
|
451
469
|
if (!readyToMergeCommentPosted) {
|
|
452
470
|
// Issue #1345: Differentiate message when no CI is configured
|
|
453
|
-
const ciLine = noCiConfigured ? '- No CI/CD checks are configured for this repository' : '- All CI checks have passed';
|
|
471
|
+
const ciLine = noCiConfigured ? '- No CI/CD checks are configured for this repository' : noCiTriggered ? '- CI workflows exist but were not triggered for this commit' : '- All CI checks have passed';
|
|
454
472
|
const commentBody = `## ✅ Ready to merge\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*`;
|
|
455
473
|
await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${commentBody}`;
|
|
456
474
|
readyToMergeCommentPosted = true;
|
|
@@ -739,6 +757,40 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
|
|
|
739
757
|
await log('');
|
|
740
758
|
await log(formatAligned('❌', `${argv.tool.toUpperCase()} RESUME FAILED`, ''));
|
|
741
759
|
await log(formatAligned('', 'Action:', 'Stopping auto-restart — tool execution failed after limit reset', 2));
|
|
760
|
+
// Issue #1439: Attach failure log before stopping, so user can see what happened
|
|
761
|
+
const shouldAttachLogsOnResumeFail = argv.attachLogs || argv['attach-logs'];
|
|
762
|
+
if (prNumber && shouldAttachLogsOnResumeFail) {
|
|
763
|
+
try {
|
|
764
|
+
const logFile = getLogFile();
|
|
765
|
+
if (logFile) {
|
|
766
|
+
await attachLogToGitHub({
|
|
767
|
+
logFile,
|
|
768
|
+
targetType: 'pr',
|
|
769
|
+
targetNumber: prNumber,
|
|
770
|
+
owner,
|
|
771
|
+
repo,
|
|
772
|
+
$,
|
|
773
|
+
log,
|
|
774
|
+
sanitizeLogContent,
|
|
775
|
+
verbose: argv.verbose,
|
|
776
|
+
errorMessage: `${argv.tool.toUpperCase()} execution failed after limit reset`,
|
|
777
|
+
sessionId: latestSessionId,
|
|
778
|
+
tempDir,
|
|
779
|
+
requestedModel: argv.model,
|
|
780
|
+
tool: argv.tool || 'claude',
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
} catch (logUploadError) {
|
|
784
|
+
reportError(logUploadError, {
|
|
785
|
+
context: 'attach_auto_restart_failure_log',
|
|
786
|
+
prNumber,
|
|
787
|
+
owner,
|
|
788
|
+
repo,
|
|
789
|
+
operation: 'upload_failure_log',
|
|
790
|
+
});
|
|
791
|
+
await log(formatAligned('', `⚠️ Failure log upload error: ${cleanErrorMessage(logUploadError)}`, '', 2));
|
|
792
|
+
}
|
|
793
|
+
}
|
|
742
794
|
return { success: false, reason: 'tool_failure_after_resume', latestSessionId, latestAnthropicCost };
|
|
743
795
|
}
|
|
744
796
|
} else {
|
|
@@ -755,6 +807,40 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
|
|
|
755
807
|
await log('');
|
|
756
808
|
await log(formatAligned('❌', `${argv.tool.toUpperCase()} EXECUTION FAILED`, ''));
|
|
757
809
|
await log(formatAligned('', 'Action:', 'Stopping auto-restart — tool execution failed', 2));
|
|
810
|
+
// Issue #1439: Attach failure log before stopping, so user can see what happened
|
|
811
|
+
const shouldAttachLogsOnFail = argv.attachLogs || argv['attach-logs'];
|
|
812
|
+
if (prNumber && shouldAttachLogsOnFail) {
|
|
813
|
+
try {
|
|
814
|
+
const logFile = getLogFile();
|
|
815
|
+
if (logFile) {
|
|
816
|
+
await attachLogToGitHub({
|
|
817
|
+
logFile,
|
|
818
|
+
targetType: 'pr',
|
|
819
|
+
targetNumber: prNumber,
|
|
820
|
+
owner,
|
|
821
|
+
repo,
|
|
822
|
+
$,
|
|
823
|
+
log,
|
|
824
|
+
sanitizeLogContent,
|
|
825
|
+
verbose: argv.verbose,
|
|
826
|
+
errorMessage: `${argv.tool.toUpperCase()} execution failed`,
|
|
827
|
+
sessionId: latestSessionId,
|
|
828
|
+
tempDir,
|
|
829
|
+
requestedModel: argv.model,
|
|
830
|
+
tool: argv.tool || 'claude',
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
} catch (logUploadError) {
|
|
834
|
+
reportError(logUploadError, {
|
|
835
|
+
context: 'attach_auto_restart_failure_log',
|
|
836
|
+
prNumber,
|
|
837
|
+
owner,
|
|
838
|
+
repo,
|
|
839
|
+
operation: 'upload_failure_log',
|
|
840
|
+
});
|
|
841
|
+
await log(formatAligned('', `⚠️ Failure log upload error: ${cleanErrorMessage(logUploadError)}`, '', 2));
|
|
842
|
+
}
|
|
843
|
+
}
|
|
758
844
|
return { success: false, reason: 'tool_failure', latestSessionId, latestAnthropicCost };
|
|
759
845
|
} else {
|
|
760
846
|
// Success - capture latest session data
|
|
@@ -820,63 +906,14 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
|
|
|
820
906
|
// Issue #1314: Distinguish between different waiting reasons
|
|
821
907
|
const pendingBlocker = blockers.find(b => b.type === 'ci_pending');
|
|
822
908
|
const cancelledOnly = blockers.every(b => b.type === 'ci_cancelled' || b.type === 'ci_pending');
|
|
909
|
+
const cancelledBlocker = blockers.find(b => b.type === 'ci_cancelled');
|
|
823
910
|
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
// (b) Permanent state — the repository has no CI/CD workflows configured at all.
|
|
829
|
-
// We resolve the ambiguity by checking if the repo actually has workflow files via the
|
|
830
|
-
// GitHub API. If it has none, the 'no_checks' state is permanent and the PR should be
|
|
831
|
-
// treated as CI-passing (no CI = nothing to wait for).
|
|
832
|
-
const isNoCIChecks = pendingBlocker && pendingBlocker.message.includes('have not started yet');
|
|
833
|
-
if (isNoCIChecks) {
|
|
834
|
-
// Lazy-check whether the repo has workflows (cache result to avoid repeated API calls)
|
|
835
|
-
if (repoHasWorkflows === null) {
|
|
836
|
-
const workflowCheck = await getActiveRepoWorkflows(owner, repo, argv.verbose);
|
|
837
|
-
repoHasWorkflows = workflowCheck.hasWorkflows;
|
|
838
|
-
if (argv.verbose) {
|
|
839
|
-
await log(formatAligned('', 'Repo workflow check:', repoHasWorkflows ? `${workflowCheck.count} workflow(s) found — CI check is a transient race condition` : 'No workflows configured — no CI expected', 2));
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
if (!repoHasWorkflows) {
|
|
844
|
-
// Root cause confirmed: repo has no CI. The 'no_checks' state is permanent.
|
|
845
|
-
// Treat the PR as CI-passing and exit the monitoring loop immediately.
|
|
846
|
-
await log('');
|
|
847
|
-
await log(formatAligned('ℹ️', 'NO CI WORKFLOWS CONFIGURED', 'Repository has no GitHub Actions workflows'));
|
|
848
|
-
await log(formatAligned('', 'Conclusion:', 'No CI expected — treating PR as CI-passing', 2));
|
|
849
|
-
await log(formatAligned('', 'Action:', 'Exiting monitoring loop', 2));
|
|
850
|
-
await log('');
|
|
851
|
-
|
|
852
|
-
// Post a comment explaining the situation
|
|
853
|
-
try {
|
|
854
|
-
const commentBody = `## ℹ️ No CI Workflows Detected
|
|
855
|
-
|
|
856
|
-
No CI/CD checks are configured for this pull request. The repository has no GitHub Actions workflow files in \`.github/workflows/\`.
|
|
857
|
-
|
|
858
|
-
The auto-restart-until-mergeable monitor is stopping since there is no CI to wait for. The PR may be ready to merge if there are no other issues.
|
|
859
|
-
|
|
860
|
-
---
|
|
861
|
-
*Monitored by hive-mind with --auto-restart-until-mergeable flag*`;
|
|
862
|
-
await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${commentBody}`;
|
|
863
|
-
} catch {
|
|
864
|
-
// Don't fail if comment posting fails
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
return { success: true, reason: 'no_ci_checks', latestSessionId, latestAnthropicCost };
|
|
868
|
-
} else {
|
|
869
|
-
// Repo has workflows but CI hasn't started yet — transient race condition, keep waiting
|
|
870
|
-
await log(formatAligned('⏳', 'Waiting for CI:', 'No checks yet (CI workflows exist, waiting for them to start)', 2));
|
|
871
|
-
}
|
|
911
|
+
if (cancelledOnly && cancelledBlocker) {
|
|
912
|
+
await log(formatAligned('🔄', 'Waiting for re-triggered CI:', cancelledBlocker.details.join(', '), 2));
|
|
913
|
+
} else if (pendingBlocker) {
|
|
914
|
+
await log(formatAligned('⏳', 'Waiting for CI:', pendingBlocker.details.length > 0 ? pendingBlocker.details.join(', ') : pendingBlocker.message, 2));
|
|
872
915
|
} else {
|
|
873
|
-
|
|
874
|
-
await log(formatAligned('🔄', 'Waiting for re-triggered CI:', cancelledBlocker.details.join(', '), 2));
|
|
875
|
-
} else if (pendingBlocker) {
|
|
876
|
-
await log(formatAligned('⏳', 'Waiting for CI:', pendingBlocker.details.length > 0 ? pendingBlocker.details.join(', ') : pendingBlocker.message, 2));
|
|
877
|
-
} else {
|
|
878
|
-
await log(formatAligned('⏳', 'Waiting for:', blockers.map(b => b.message).join(', '), 2));
|
|
879
|
-
}
|
|
916
|
+
await log(formatAligned('⏳', 'Waiting for:', blockers.map(b => b.message).join(', '), 2));
|
|
880
917
|
}
|
|
881
918
|
} else {
|
|
882
919
|
await log(formatAligned('', 'No action needed', 'Continuing to monitor...', 2));
|