@link-assistant/hive-mind 1.35.6 → 1.35.8
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 +19 -0
- package/package.json +1 -1
- package/src/hive.mjs +4 -1
- package/src/lib.mjs +38 -0
- package/src/solve.auto-merge.lib.mjs +32 -6
- package/src/solve.auto-pr.lib.mjs +47 -21
- package/src/solve.mjs +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.35.8
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- ca57154: fix: add retry with exponential backoff for PR verification after creation (Issue #1468)
|
|
8
|
+
- Add retry logic with exponential backoff (up to 5 attempts: 2s, 4s, 6s, 8s, 10s) to PR verification step in solve.auto-pr.lib.mjs to handle GitHub API eventual consistency
|
|
9
|
+
- Add case study with timeline reconstruction and root cause analysis
|
|
10
|
+
- Add 11 unit tests covering retry behavior, backoff timing, and edge cases
|
|
11
|
+
|
|
12
|
+
## 1.35.7
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- fca8460: fix: prevent infinite CI waiting loop when workflows complete with action_required (Issue #1466)
|
|
17
|
+
- Detect when all workflow runs completed with non-executing conclusions (action_required, cancelled, stale, skipped) and treat as "CI not triggered" instead of waiting indefinitely for check-runs that will never appear
|
|
18
|
+
- Add verbose log interceptor (setupVerboseLogInterceptor) to capture [VERBOSE] console.log output in log files, fixing the discrepancy between terminal and log file output
|
|
19
|
+
- Add case study with root cause analysis and timeline reconstruction from 5 production log files
|
|
20
|
+
- Add 14 unit tests covering action_required handling, non-executing conclusions, race conditions, and edge cases
|
|
21
|
+
|
|
3
22
|
## 1.35.6
|
|
4
23
|
|
|
5
24
|
### Patch Changes
|
package/package.json
CHANGED
package/src/hive.mjs
CHANGED
|
@@ -95,7 +95,7 @@ if (isDirectExecution) {
|
|
|
95
95
|
const fs = (await withTimeout(use('fs'), 30000, 'loading fs')).promises;
|
|
96
96
|
// Import shared library functions
|
|
97
97
|
const lib = await import('./lib.mjs');
|
|
98
|
-
const { log, setLogFile, getAbsoluteLogPath, formatTimestamp, cleanErrorMessage, cleanupTempDirectories } = lib;
|
|
98
|
+
const { log, setLogFile, getAbsoluteLogPath, formatTimestamp, cleanErrorMessage, cleanupTempDirectories, setupVerboseLogInterceptor } = lib;
|
|
99
99
|
const yargsConfigLib = await import('./hive.config.lib.mjs');
|
|
100
100
|
const { createYargsConfig } = yargsConfigLib;
|
|
101
101
|
const claudeLib = await import('./claude.lib.mjs');
|
|
@@ -311,6 +311,9 @@ if (isDirectExecution) {
|
|
|
311
311
|
// Set global verbose mode
|
|
312
312
|
global.verboseMode = argv.verbose;
|
|
313
313
|
|
|
314
|
+
// Issue #1466: Intercept console.log to capture [VERBOSE] output in log files
|
|
315
|
+
setupVerboseLogInterceptor();
|
|
316
|
+
|
|
314
317
|
// Use the universal GitHub URL parser
|
|
315
318
|
if (githubUrl) {
|
|
316
319
|
const parsedUrl = parseGitHubUrl(githubUrl);
|
package/src/lib.mjs
CHANGED
|
@@ -114,6 +114,43 @@ export const log = async (message, options = {}) => {
|
|
|
114
114
|
}
|
|
115
115
|
};
|
|
116
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Issue #1466: Intercept console.log to capture [VERBOSE] output in the log file.
|
|
119
|
+
*
|
|
120
|
+
* Functions in github-merge.lib.mjs and github-merge-ci.lib.mjs use console.log()
|
|
121
|
+
* directly for verbose output (e.g., `console.log('[VERBOSE] /merge: ...')`).
|
|
122
|
+
* This means verbose diagnostic data only appears in the terminal, not in log files,
|
|
123
|
+
* making debugging harder.
|
|
124
|
+
*
|
|
125
|
+
* This interceptor wraps console.log so that any message containing '[VERBOSE]'
|
|
126
|
+
* is also appended to the log file. It preserves the original console.log behavior.
|
|
127
|
+
*
|
|
128
|
+
* Call this once after setLogFile() to enable the interceptor.
|
|
129
|
+
*/
|
|
130
|
+
let verboseInterceptorInstalled = false;
|
|
131
|
+
export const setupVerboseLogInterceptor = () => {
|
|
132
|
+
if (verboseInterceptorInstalled) return;
|
|
133
|
+
verboseInterceptorInstalled = true;
|
|
134
|
+
|
|
135
|
+
const originalConsoleLog = console.log.bind(console);
|
|
136
|
+
console.log = (...args) => {
|
|
137
|
+
// Always call original console.log first
|
|
138
|
+
originalConsoleLog(...args);
|
|
139
|
+
|
|
140
|
+
// If a log file is set and the message looks like a [VERBOSE] log, append to file
|
|
141
|
+
if (logFile && args.length > 0) {
|
|
142
|
+
const firstArg = String(args[0]);
|
|
143
|
+
if (firstArg.includes('[VERBOSE]')) {
|
|
144
|
+
const message = args.map(a => String(a)).join(' ');
|
|
145
|
+
const logMessage = `[${new Date().toISOString()}] [VERBOSE] ${message}`;
|
|
146
|
+
fs.appendFile(logFile, logMessage + '\n').catch(() => {
|
|
147
|
+
// Silent fail to avoid infinite loops
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
|
|
117
154
|
/**
|
|
118
155
|
* Mask sensitive tokens in text
|
|
119
156
|
* @param {string} token - Token to mask
|
|
@@ -469,6 +506,7 @@ export default {
|
|
|
469
506
|
formatAligned,
|
|
470
507
|
displayFormattedError,
|
|
471
508
|
cleanupTempDirectories,
|
|
509
|
+
setupVerboseLogInterceptor,
|
|
472
510
|
};
|
|
473
511
|
|
|
474
512
|
/**
|
|
@@ -224,7 +224,28 @@ const getMergeBlockers = async (owner, repo, prNumber, verbose = false) => {
|
|
|
224
224
|
// - workflow_runs.length === 0 → CI was NOT triggered (fork PR, paths-ignore, etc.)
|
|
225
225
|
const workflowRuns = await getWorkflowRunsForSha(owner, repo, ciStatus.sha, verbose);
|
|
226
226
|
if (workflowRuns.length > 0) {
|
|
227
|
-
//
|
|
227
|
+
// Issue #1466: Check if ALL workflow runs are completed without producing check-runs.
|
|
228
|
+
// This happens when workflows require manual approval (first-time fork contributors,
|
|
229
|
+
// deployment approvals) — they complete with conclusion=action_required but never
|
|
230
|
+
// create check-runs. Waiting for check-runs in this case is an infinite loop.
|
|
231
|
+
//
|
|
232
|
+
// Also covers other non-executing conclusions: cancelled, stale workflows that
|
|
233
|
+
// completed without producing check-runs won't produce them in the future either.
|
|
234
|
+
const allRunsCompleted = workflowRuns.every(r => r.status === 'completed');
|
|
235
|
+
const allRunsNonExecuting = allRunsCompleted && workflowRuns.every(r => r.conclusion === 'action_required' || r.conclusion === 'cancelled' || r.conclusion === 'stale' || r.conclusion === 'skipped');
|
|
236
|
+
|
|
237
|
+
if (allRunsNonExecuting) {
|
|
238
|
+
// All workflow runs completed without executing jobs — check-runs will never appear.
|
|
239
|
+
// Treat the same as "CI not triggered" to avoid infinite waiting.
|
|
240
|
+
const conclusions = [...new Set(workflowRuns.map(r => r.conclusion))].join(', ');
|
|
241
|
+
if (verbose) {
|
|
242
|
+
console.log(`[VERBOSE] /merge: PR #${prNumber} has ${workflowRuns.length} workflow run(s) for SHA ${ciStatus.sha.substring(0, 7)}, but all completed without executing (conclusions: ${conclusions}) — check-runs will never appear`);
|
|
243
|
+
}
|
|
244
|
+
await log(formatAligned('ℹ️', 'CI workflows completed without executing:', `${conclusions} (${workflowRuns.map(r => r.name).join(', ')})`, 2));
|
|
245
|
+
return { blockers, ciStatus, noCiConfigured: false, noCiTriggered: true, workflowRunConclusions: conclusions };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Some workflow runs are still in progress or produced results — genuine race condition
|
|
228
249
|
if (verbose) {
|
|
229
250
|
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
251
|
}
|
|
@@ -414,7 +435,7 @@ export const watchUntilMergeable = async params => {
|
|
|
414
435
|
|
|
415
436
|
try {
|
|
416
437
|
// Get merge blockers
|
|
417
|
-
const { blockers, noCiConfigured, noCiTriggered } = await getMergeBlockers(owner, repo, prNumber, argv.verbose);
|
|
438
|
+
const { blockers, noCiConfigured, noCiTriggered, workflowRunConclusions } = await getMergeBlockers(owner, repo, prNumber, argv.verbose);
|
|
418
439
|
|
|
419
440
|
// Check for new comments from non-bot users
|
|
420
441
|
const { hasNewComments, comments } = await checkForNonBotComments(owner, repo, prNumber, issueNumber, lastCheckTime, argv.verbose);
|
|
@@ -422,10 +443,15 @@ export const watchUntilMergeable = async params => {
|
|
|
422
443
|
// Check for uncommitted changes using shared utility
|
|
423
444
|
const hasUncommittedChanges = await checkForUncommittedChanges(tempDir, argv);
|
|
424
445
|
|
|
425
|
-
// Issue #1442: If CI workflows exist but were not triggered for this commit,
|
|
446
|
+
// Issue #1442/#1466: If CI workflows exist but were not triggered for this commit,
|
|
426
447
|
// log why before proceeding to the mergeable path.
|
|
427
448
|
if (noCiTriggered) {
|
|
428
|
-
|
|
449
|
+
if (workflowRunConclusions) {
|
|
450
|
+
// Issue #1466: Workflow runs exist but completed without executing (action_required, cancelled, etc.)
|
|
451
|
+
await log(formatAligned('ℹ️', 'CI not executed:', `Workflow runs completed with: ${workflowRunConclusions} (likely needs maintainer approval)`, 2));
|
|
452
|
+
} else {
|
|
453
|
+
await log(formatAligned('ℹ️', 'CI not triggered:', 'Workflows exist but no workflow runs for this commit (fork PR, paths-ignore, workflow conditions)', 2));
|
|
454
|
+
}
|
|
429
455
|
}
|
|
430
456
|
|
|
431
457
|
// If PR is mergeable, no blockers, no new comments, and no uncommitted changes
|
|
@@ -444,7 +470,7 @@ export const watchUntilMergeable = async params => {
|
|
|
444
470
|
// Post success comment
|
|
445
471
|
try {
|
|
446
472
|
// Issue #1345: Differentiate message when no CI is configured
|
|
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';
|
|
473
|
+
const ciLine = noCiConfigured ? '- No CI/CD checks are configured for this repository' : noCiTriggered ? (workflowRunConclusions ? `- CI workflows completed without executing (${workflowRunConclusions})` : '- CI workflows exist but were not triggered for this commit') : '- All CI checks have passed';
|
|
448
474
|
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*`;
|
|
449
475
|
await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${commentBody}`;
|
|
450
476
|
} catch {
|
|
@@ -468,7 +494,7 @@ export const watchUntilMergeable = async params => {
|
|
|
468
494
|
try {
|
|
469
495
|
if (!readyToMergeCommentPosted) {
|
|
470
496
|
// Issue #1345: Differentiate message when no CI is configured
|
|
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';
|
|
497
|
+
const ciLine = noCiConfigured ? '- No CI/CD checks are configured for this repository' : noCiTriggered ? (workflowRunConclusions ? `- CI workflows completed without executing (${workflowRunConclusions})` : '- CI workflows exist but were not triggered for this commit') : '- All CI checks have passed';
|
|
472
498
|
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*`;
|
|
473
499
|
await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${commentBody}`;
|
|
474
500
|
readyToMergeCommentPosted = true;
|
|
@@ -1203,33 +1203,59 @@ ${prBody}`,
|
|
|
1203
1203
|
|
|
1204
1204
|
// CRITICAL: Verify the PR was actually created by querying GitHub API
|
|
1205
1205
|
// This is essential because gh pr create can return a URL but PR creation might have failed
|
|
1206
|
+
// Issue #1468: Use retry with exponential backoff because GitHub's API is eventually
|
|
1207
|
+
// consistent — a newly created PR may not be visible via gh pr view for several seconds.
|
|
1208
|
+
// This matches the existing retry pattern used for the compare API (lines 571-624).
|
|
1206
1209
|
await log(formatAligned('🔍', 'Verifying:', 'PR creation...'), { verbose: true });
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1210
|
+
|
|
1211
|
+
let prVerified = false;
|
|
1212
|
+
let verifyAttempts = 0;
|
|
1213
|
+
const maxVerifyAttempts = 5;
|
|
1214
|
+
let lastVerifyResult = null;
|
|
1215
|
+
|
|
1216
|
+
while (!prVerified && verifyAttempts < maxVerifyAttempts) {
|
|
1217
|
+
verifyAttempts++;
|
|
1218
|
+
const waitTime = Math.min(2000 * verifyAttempts, 10000); // 2s, 4s, 6s, 8s, 10s
|
|
1219
|
+
|
|
1220
|
+
if (verifyAttempts > 1) {
|
|
1221
|
+
await log(` Retry ${verifyAttempts}/${maxVerifyAttempts}: Waiting ${waitTime}ms for GitHub to propagate PR...`);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
1225
|
+
|
|
1226
|
+
lastVerifyResult = await $({
|
|
1227
|
+
silent: true,
|
|
1228
|
+
})`gh pr view ${localPrNumber} --repo ${owner}/${repo} --json number,url,state 2>&1`;
|
|
1229
|
+
|
|
1230
|
+
if (lastVerifyResult.code === 0) {
|
|
1231
|
+
try {
|
|
1232
|
+
const prData = JSON.parse(lastVerifyResult.stdout.toString().trim());
|
|
1233
|
+
if (prData.number && prData.url) {
|
|
1234
|
+
await log(formatAligned('✅', 'Verification:', `PR exists on GitHub (attempt ${verifyAttempts}/${maxVerifyAttempts})`), { verbose: true });
|
|
1235
|
+
// Update prUrl and localPrNumber from verified data
|
|
1236
|
+
prUrl = prData.url;
|
|
1237
|
+
localPrNumber = String(prData.number);
|
|
1238
|
+
prVerified = true;
|
|
1239
|
+
}
|
|
1240
|
+
} catch {
|
|
1241
|
+
// Parse failed, will retry
|
|
1242
|
+
if (argv.verbose) {
|
|
1243
|
+
await log(` Verify attempt ${verifyAttempts}: Could not parse PR data, retrying...`, { verbose: true });
|
|
1244
|
+
}
|
|
1221
1245
|
}
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
|
|
1246
|
+
} else if (argv.verbose) {
|
|
1247
|
+
const attemptStderr = lastVerifyResult.stderr ? lastVerifyResult.stderr.toString().trim() : '';
|
|
1248
|
+
await log(` Verify attempt ${verifyAttempts}: PR not found yet${attemptStderr ? ` (${attemptStderr})` : ''}`, { verbose: true });
|
|
1225
1249
|
}
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
if (!prVerified) {
|
|
1253
|
+
// PR does not exist after all retries - gh pr create must have failed silently
|
|
1228
1254
|
// Issue #1462: Include gh pr create stderr for root cause diagnosis
|
|
1229
|
-
const verifyStderr =
|
|
1255
|
+
const verifyStderr = lastVerifyResult && lastVerifyResult.stderr ? lastVerifyResult.stderr.toString().trim() : '';
|
|
1230
1256
|
const stderrInfo = prCreateStderr ? ` (gh pr create stderr: ${prCreateStderr.trim()})` : '';
|
|
1231
1257
|
const verifyInfo = verifyStderr ? ` (gh pr view stderr: ${verifyStderr})` : '';
|
|
1232
|
-
throw new Error(`PR verification failed - gh pr create returned URL "${prUrl}" but PR #${localPrNumber} does not exist on GitHub${stderrInfo}${verifyInfo}`);
|
|
1258
|
+
throw new Error(`PR verification failed - gh pr create returned URL "${prUrl}" but PR #${localPrNumber} does not exist on GitHub after ${maxVerifyAttempts} verification attempts${stderrInfo}${verifyInfo}`);
|
|
1233
1259
|
}
|
|
1234
1260
|
// Store PR info globally for error handlers
|
|
1235
1261
|
global.createdPR = { number: localPrNumber, url: prUrl };
|
package/src/solve.mjs
CHANGED
|
@@ -48,7 +48,7 @@ const fs = (await use('fs')).promises;
|
|
|
48
48
|
const crypto = (await use('crypto')).default;
|
|
49
49
|
const memoryCheck = await import('./memory-check.mjs');
|
|
50
50
|
const lib = await import('./lib.mjs');
|
|
51
|
-
const { log, setLogFile, getLogFile, getAbsoluteLogPath, cleanErrorMessage, formatAligned, getVersionInfo } = lib;
|
|
51
|
+
const { log, setLogFile, getLogFile, getAbsoluteLogPath, cleanErrorMessage, formatAligned, getVersionInfo, setupVerboseLogInterceptor } = lib;
|
|
52
52
|
const githubLib = await import('./github.lib.mjs');
|
|
53
53
|
const { sanitizeLogContent, attachLogToGitHub, getToolDisplayName } = githubLib;
|
|
54
54
|
const validation = await import('./solve.validation.lib.mjs');
|
|
@@ -111,6 +111,9 @@ try {
|
|
|
111
111
|
}
|
|
112
112
|
global.verboseMode = argv.verbose;
|
|
113
113
|
|
|
114
|
+
// Issue #1466: Intercept console.log to capture [VERBOSE] output in log files
|
|
115
|
+
setupVerboseLogInterceptor();
|
|
116
|
+
|
|
114
117
|
// Early logs go to cwd; custom log dir takes effect after argv is parsed
|
|
115
118
|
|
|
116
119
|
// Conditionally import tool-specific functions after argv is parsed
|