@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.35.6",
3
+ "version": "1.35.8",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
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
- // Workflow runs exist but check-runs haven't appeared yet genuine race condition
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
- await log(formatAligned('ℹ️', 'CI not triggered:', 'Workflows exist but no workflow runs for this commit (fork PR, paths-ignore, workflow conditions)', 2));
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
- const verifyResult = await $({
1208
- silent: true,
1209
- })`gh pr view ${localPrNumber} --repo ${owner}/${repo} --json number,url,state 2>&1`;
1210
-
1211
- if (verifyResult.code === 0) {
1212
- try {
1213
- const prData = JSON.parse(verifyResult.stdout.toString().trim());
1214
- if (prData.number && prData.url) {
1215
- await log(formatAligned('✅', 'Verification:', 'PR exists on GitHub'), { verbose: true });
1216
- // Update prUrl and localPrNumber from verified data
1217
- prUrl = prData.url;
1218
- localPrNumber = String(prData.number);
1219
- } else {
1220
- throw new Error('PR data incomplete');
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
- } catch {
1223
- await log('❌ PR verification failed: Could not parse PR data', { level: 'error' });
1224
- throw new Error('PR creation verification failed - invalid response');
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
- } else {
1227
- // PR does not exist - gh pr create must have failed silently
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 = verifyResult.stderr ? verifyResult.stderr.toString().trim() : '';
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