@link-assistant/hive-mind 1.31.0 → 1.31.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 CHANGED
@@ -1,5 +1,48 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.31.2
4
+
5
+ ### Patch Changes
6
+
7
+ - efe3506: fix: /merge command no longer falsely fails when latest CI is in progress (Issue #1425)
8
+
9
+ The `checkBranchCIHealth` function previously queried only `status=completed` runs
10
+ to determine if the default branch CI was healthy. When a new commit had an in-progress
11
+ CI run, the function returned the previous (now superseded) commit's failure as the
12
+ "latest" CI status, causing the merge queue to be blocked with a false positive error.
13
+
14
+ The fix resolves the actual HEAD SHA of the branch first, then queries CI runs
15
+ specifically for that SHA (without a status filter). If the latest commit's runs are
16
+ in progress, the function returns `pending: true` (healthy) instead of reporting a
17
+ failure from an older commit. The merge queue then proceeds to the existing
18
+ `waitForTargetBranchCI` step which correctly waits for those runs to complete.
19
+
20
+ ## 1.31.1
21
+
22
+ ### Patch Changes
23
+
24
+ - 5108367: fix: fix root causes of 20-32h process hang after session ends (Issue #1335)
25
+
26
+ Two separate bugs caused `solve` processes to run for 20–32 hours after work was complete:
27
+
28
+ **Bug A — Infinite loop for repos without CI:** When `--auto-restart-until-mergeable` is used
29
+ on a repository with no CI/CD workflows, the `watchUntilMergeable` loop was permanently stuck
30
+ on "CI/CD checks have not started yet" with no exit condition. The root cause was that the code
31
+ treated `no_checks` identically for both transient race conditions (CI hasn't started yet after
32
+ a push) and permanent states (repo has no CI at all). Fixed by checking whether the repository
33
+ actually has GitHub Actions workflows configured (`hasRepoWorkflows()`). If none exist, the
34
+ `no_checks` state is permanent and the monitor exits immediately, treating the PR as CI-passing.
35
+ If workflows exist, the state is a transient race condition and the loop keeps waiting.
36
+
37
+ **Bug B — No process exit after session ends:** After a successful run (PR became mergeable,
38
+ work session ended), `solve.mjs` never called `process.exit()`. Sentry's profiling integration
39
+ (`@sentry/profiling-node`) kept the Node.js event loop alive indefinitely. Fixed by calling
40
+ `safeExit(0)` at the end of the `finally` block in `solve.mjs`, which flushes Sentry events
41
+ (up to 2 seconds) and then calls `process.exit(0)`.
42
+
43
+ Also adds `--verbose` debug logging of active Node.js handles at exit to aid diagnosis of
44
+ future occurrences.
45
+
3
46
  ## 1.31.0
4
47
 
5
48
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.31.0",
3
+ "version": "1.31.2",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -141,62 +141,105 @@ export async function waitForCommitCI(owner, repo, sha, options = {}, verbose =
141
141
  /**
142
142
  * Check if the default branch has any recent failed CI runs
143
143
  * Issue #1341: Used to detect pre-existing failures before starting the merge queue
144
+ * Issue #1425: Fixed to resolve the actual HEAD SHA first, then check CI for that SHA,
145
+ * so that in-progress runs on the latest commit are not mistaken for failures.
144
146
  *
145
147
  * @param {string} owner - Repository owner
146
148
  * @param {string} repo - Repository name
147
149
  * @param {string} branch - Branch name (usually 'main' or 'master')
148
- * @param {Object} options - Check options
149
- * @param {number} options.lookbackCount - Number of recent runs to check (default: 5)
150
+ * @param {Object} options - Check options (currently unused, kept for API compatibility)
150
151
  * @param {boolean} verbose - Whether to log verbose output
151
- * @returns {Promise<{healthy: boolean, failedRuns: Array, error: string|null}>}
152
+ * @returns {Promise<{healthy: boolean, pending: boolean, failedRuns: Array, pendingRuns: Array, error: string|null}>}
152
153
  */
153
- export async function checkBranchCIHealth(owner, repo, branch = 'main', options = {}, verbose = false) {
154
- const { lookbackCount = 5 } = options;
155
-
154
+ export async function checkBranchCIHealth(owner, repo, branch = 'main', options, verbose = false) {
156
155
  try {
157
- // Get recent completed workflow runs on the branch
158
- const { stdout } = await exec(`gh api "repos/${owner}/${repo}/actions/runs?branch=${branch}&status=completed&per_page=${lookbackCount}" --jq '[.workflow_runs[] | {id: .id, name: .name, status: .status, conclusion: .conclusion, html_url: .html_url, head_sha: .head_sha, created_at: .created_at}]'`);
156
+ // Issue #1425: First, resolve the actual HEAD SHA of the branch.
157
+ // This avoids the bug where only completed runs are queried: if the latest commit has
158
+ // an in-progress CI run, querying ?status=completed would return the previous commit's
159
+ // runs and could incorrectly report a failure from an older (now superseded) commit.
160
+ let headSha;
161
+ try {
162
+ const { stdout: refOut } = await exec(`gh api "repos/${owner}/${repo}/git/ref/heads/${branch}" --jq '.object.sha'`);
163
+ headSha = refOut.trim();
164
+ } catch (refError) {
165
+ if (verbose) {
166
+ console.log(`[VERBOSE] /merge: Error resolving HEAD SHA for ${branch}: ${refError.message}`);
167
+ }
168
+ // On error, assume healthy to avoid blocking merges due to API issues
169
+ return { healthy: true, pending: false, failedRuns: [], pendingRuns: [], error: null };
170
+ }
171
+
172
+ if (!headSha) {
173
+ if (verbose) {
174
+ console.log(`[VERBOSE] /merge: Could not resolve HEAD SHA for ${branch}, assuming healthy`);
175
+ }
176
+ return { healthy: true, pending: false, failedRuns: [], pendingRuns: [], error: null };
177
+ }
178
+
179
+ if (verbose) {
180
+ console.log(`[VERBOSE] /merge: Checking CI for latest ${branch} commit ${headSha.substring(0, 7)}`);
181
+ }
182
+
183
+ // Issue #1425: Query CI runs specifically for the HEAD SHA (no status filter).
184
+ // This ensures we see in-progress runs for the latest commit, not just completed ones.
185
+ const { stdout } = await exec(`gh api "repos/${owner}/${repo}/actions/runs?head_sha=${headSha}&per_page=20" --jq '[.workflow_runs[] | {id: .id, name: .name, status: .status, conclusion: .conclusion, html_url: .html_url, head_sha: .head_sha, created_at: .created_at}]'`);
159
186
  const runs = JSON.parse(stdout.trim() || '[]');
160
187
 
161
188
  if (verbose) {
162
- console.log(`[VERBOSE] /merge: Checking ${runs.length} recent CI runs on ${owner}/${repo} branch ${branch}`);
189
+ console.log(`[VERBOSE] /merge: Found ${runs.length} CI run(s) for HEAD commit ${headSha.substring(0, 7)} on ${owner}/${repo} branch ${branch}`);
163
190
  }
164
191
 
165
192
  if (runs.length === 0) {
166
- // No recent runs - assume healthy
167
- return { healthy: true, failedRuns: [], error: null };
193
+ // No runs for the latest commit - CI may not have started yet or is not configured.
194
+ // Assume healthy to avoid blocking merges.
195
+ return { healthy: true, pending: false, failedRuns: [], pendingRuns: [], error: null };
196
+ }
197
+
198
+ // Issue #1425: Check for in-progress runs on the latest commit.
199
+ // If the latest commit's CI is still running, we should NOT report failure —
200
+ // the previous commit's failure (which may appear in completed runs) is no longer relevant.
201
+ const pendingRuns = runs.filter(r => r.status === 'in_progress' || r.status === 'queued' || r.status === 'waiting' || r.status === 'requested' || r.status === 'pending');
202
+ if (pendingRuns.length > 0) {
203
+ if (verbose) {
204
+ console.log(`[VERBOSE] /merge: ${pendingRuns.length} CI run(s) still in progress on ${branch} (latest commit ${headSha.substring(0, 7)})`);
205
+ for (const run of pendingRuns) {
206
+ console.log(`[VERBOSE] /merge: - ${run.name}: ${run.status} (${run.html_url})`);
207
+ }
208
+ }
209
+ // Healthy but pending: caller should wait for CI rather than block the queue
210
+ return { healthy: true, pending: true, failedRuns: [], pendingRuns, error: null };
168
211
  }
169
212
 
170
- // Check for failures in the most recent run(s)
171
- const latestSha = runs[0].head_sha;
172
- const latestRuns = runs.filter(r => r.head_sha === latestSha);
173
- const failedRuns = latestRuns.filter(r => r.conclusion === 'failure' || r.conclusion === 'timed_out');
213
+ // All runs for the latest commit are completed — check for failures
214
+ const failedRuns = runs.filter(r => r.conclusion === 'failure' || r.conclusion === 'timed_out');
174
215
 
175
216
  if (failedRuns.length > 0) {
176
217
  if (verbose) {
177
- console.log(`[VERBOSE] /merge: Found ${failedRuns.length} failed CI run(s) on ${branch}:`);
218
+ console.log(`[VERBOSE] /merge: Found ${failedRuns.length} failed CI run(s) on ${branch} (latest commit ${headSha.substring(0, 7)}):`);
178
219
  for (const run of failedRuns) {
179
220
  console.log(`[VERBOSE] /merge: - ${run.name}: ${run.conclusion} (${run.html_url})`);
180
221
  }
181
222
  }
182
223
  return {
183
224
  healthy: false,
225
+ pending: false,
184
226
  failedRuns,
227
+ pendingRuns: [],
185
228
  error: `${failedRuns.length} CI run(s) failed on ${branch}: ${failedRuns.map(r => r.name).join(', ')}`,
186
229
  };
187
230
  }
188
231
 
189
232
  if (verbose) {
190
- console.log(`[VERBOSE] /merge: Branch ${branch} CI is healthy (${latestRuns.length} runs checked)`);
233
+ console.log(`[VERBOSE] /merge: Branch ${branch} CI is healthy (${runs.length} run(s) passed for commit ${headSha.substring(0, 7)})`);
191
234
  }
192
235
 
193
- return { healthy: true, failedRuns: [], error: null };
236
+ return { healthy: true, pending: false, failedRuns: [], pendingRuns: [], error: null };
194
237
  } catch (error) {
195
238
  if (verbose) {
196
239
  console.log(`[VERBOSE] /merge: Error checking branch CI health: ${error.message}`);
197
240
  }
198
241
  // On error, assume healthy to avoid blocking merges due to API issues
199
- return { healthy: true, failedRuns: [], error: null };
242
+ return { healthy: true, pending: false, failedRuns: [], pendingRuns: [], error: null };
200
243
  }
201
244
  }
202
245
 
@@ -368,6 +368,12 @@ export const watchUntilMergeable = async params => {
368
368
  let iteration = 0;
369
369
  let lastCheckTime = new Date();
370
370
 
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
+
371
377
  while (true) {
372
378
  iteration++;
373
379
  const currentTime = new Date();
@@ -812,12 +818,62 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
812
818
  const pendingBlocker = blockers.find(b => b.type === 'ci_pending');
813
819
  const cancelledOnly = blockers.every(b => b.type === 'ci_cancelled' || b.type === 'ci_pending');
814
820
 
815
- if (cancelledOnly && cancelledBlocker) {
816
- await log(formatAligned('🔄', 'Waiting for re-triggered CI:', cancelledBlocker.details.join(', '), 2));
817
- } else if (pendingBlocker) {
818
- await log(formatAligned('⏳', 'Waiting for CI:', pendingBlocker.details.length > 0 ? pendingBlocker.details.join(', ') : pendingBlocker.message, 2));
821
+ // Issue #1335: Detect permanent 'no_checks' state (repo has no CI workflows).
822
+ // The 'ci_pending' blocker with message 'have not started yet' means GitHub returned
823
+ // zero check-runs and zero commit statuses for this PR's HEAD SHA. This is ambiguous:
824
+ // (a) Transient race condition CI workflows exist but haven't queued yet after push.
825
+ // (b) Permanent state — the repository has no CI/CD workflows configured at all.
826
+ // We resolve the ambiguity by checking if the repo actually has workflow files via the
827
+ // GitHub API. If it has none, the 'no_checks' state is permanent and the PR should be
828
+ // treated as CI-passing (no CI = nothing to wait for).
829
+ const isNoCIChecks = pendingBlocker && pendingBlocker.message.includes('have not started yet');
830
+ if (isNoCIChecks) {
831
+ // Lazy-check whether the repo has workflows (cache result to avoid repeated API calls)
832
+ if (repoHasWorkflows === null) {
833
+ const workflowCheck = await getActiveRepoWorkflows(owner, repo, argv.verbose);
834
+ repoHasWorkflows = workflowCheck.hasWorkflows;
835
+ if (argv.verbose) {
836
+ 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));
837
+ }
838
+ }
839
+
840
+ if (!repoHasWorkflows) {
841
+ // Root cause confirmed: repo has no CI. The 'no_checks' state is permanent.
842
+ // Treat the PR as CI-passing and exit the monitoring loop immediately.
843
+ await log('');
844
+ await log(formatAligned('ℹ️', 'NO CI WORKFLOWS CONFIGURED', 'Repository has no GitHub Actions workflows'));
845
+ await log(formatAligned('', 'Conclusion:', 'No CI expected — treating PR as CI-passing', 2));
846
+ await log(formatAligned('', 'Action:', 'Exiting monitoring loop', 2));
847
+ await log('');
848
+
849
+ // Post a comment explaining the situation
850
+ try {
851
+ const commentBody = `## ℹ️ No CI Workflows Detected
852
+
853
+ No CI/CD checks are configured for this pull request. The repository has no GitHub Actions workflow files in \`.github/workflows/\`.
854
+
855
+ 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.
856
+
857
+ ---
858
+ *Monitored by hive-mind with --auto-restart-until-mergeable flag*`;
859
+ await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${commentBody}`;
860
+ } catch {
861
+ // Don't fail if comment posting fails
862
+ }
863
+
864
+ return { success: true, reason: 'no_ci_checks', latestSessionId, latestAnthropicCost };
865
+ } else {
866
+ // Repo has workflows but CI hasn't started yet — transient race condition, keep waiting
867
+ await log(formatAligned('⏳', 'Waiting for CI:', 'No checks yet (CI workflows exist, waiting for them to start)', 2));
868
+ }
819
869
  } else {
820
- await log(formatAligned('⏳', 'Waiting for:', blockers.map(b => b.message).join(', '), 2));
870
+ if (cancelledOnly && cancelledBlocker) {
871
+ await log(formatAligned('🔄', 'Waiting for re-triggered CI:', cancelledBlocker.details.join(', '), 2));
872
+ } else if (pendingBlocker) {
873
+ await log(formatAligned('⏳', 'Waiting for CI:', pendingBlocker.details.length > 0 ? pendingBlocker.details.join(', ') : pendingBlocker.message, 2));
874
+ } else {
875
+ await log(formatAligned('⏳', 'Waiting for:', blockers.map(b => b.message).join(', '), 2));
876
+ }
821
877
  }
822
878
  } else {
823
879
  await log(formatAligned('', 'No action needed', 'Continuing to monitor...', 2));
@@ -152,6 +152,45 @@ export const createUnhandledRejectionHandler = options => {
152
152
  };
153
153
  };
154
154
 
155
+ /**
156
+ * Handles the case where no PR is available when one is required
157
+ */
158
+ export const handleNoPrAvailableError = async ({ isContinueMode, tempDir, issueNumber, issueUrl, log, formatAligned }) => {
159
+ await log('');
160
+ await log(formatAligned('❌', 'FATAL ERROR:', 'No pull request available'), { level: 'error' });
161
+ await log('');
162
+ await log(' 🔍 What happened:');
163
+ if (isContinueMode) {
164
+ await log(' Continue mode is active but no PR number is available.');
165
+ await log(' This usually means PR creation failed or was skipped incorrectly.');
166
+ } else {
167
+ await log(' Auto-PR creation is enabled but no PR was created.');
168
+ await log(' PR creation may have failed without throwing an error.');
169
+ }
170
+ await log('');
171
+ await log(' 💡 Why this is critical:');
172
+ await log(' The solve command requires a PR for:');
173
+ await log(' • Tracking work progress');
174
+ await log(' • Receiving and processing feedback');
175
+ await log(' • Managing code changes');
176
+ await log(' • Auto-merging when complete');
177
+ await log('');
178
+ await log(' 🔧 How to fix:');
179
+ await log('');
180
+ await log(' Option 1: Create PR manually and use --continue');
181
+ await log(` cd ${tempDir}`);
182
+ await log(` gh pr create --draft --title "Fix issue #${issueNumber}" --body "Fixes #${issueNumber}"`);
183
+ await log(' # Then use the PR URL with solve.mjs');
184
+ await log('');
185
+ await log(' Option 2: Start fresh without continue mode');
186
+ await log(` ./solve.mjs "${issueUrl}" --auto-pull-request-creation`);
187
+ await log('');
188
+ await log(' Option 3: Disable auto-PR creation (Claude will create it)');
189
+ await log(` ./solve.mjs "${issueUrl}" --no-auto-pull-request-creation`);
190
+ await log('');
191
+ await safeExit(1, 'No PR available');
192
+ };
193
+
155
194
  /**
156
195
  * Handles execution errors in the main catch block
157
196
  */
package/src/solve.mjs CHANGED
@@ -69,7 +69,7 @@ const usageLimitLib = await import('./usage-limit.lib.mjs');
69
69
  const { formatResetTimeWithRelative } = usageLimitLib;
70
70
 
71
71
  const errorHandlers = await import('./solve.error-handlers.lib.mjs');
72
- const { createUncaughtExceptionHandler, createUnhandledRejectionHandler, handleMainExecutionError } = errorHandlers;
72
+ const { createUncaughtExceptionHandler, createUnhandledRejectionHandler, handleMainExecutionError, handleNoPrAvailableError } = errorHandlers;
73
73
 
74
74
  const watchLib = await import('./solve.watch.lib.mjs');
75
75
  const { startWatchMode } = watchLib;
@@ -650,39 +650,7 @@ try {
650
650
  // CRITICAL: Validate that we have a PR number when required
651
651
  // This prevents continuing without a PR when one was supposed to be created
652
652
  if ((isContinueMode || argv.autoPullRequestCreation) && !prNumber) {
653
- await log('');
654
- await log(formatAligned('❌', 'FATAL ERROR:', 'No pull request available'), { level: 'error' });
655
- await log('');
656
- await log(' 🔍 What happened:');
657
- if (isContinueMode) {
658
- await log(' Continue mode is active but no PR number is available.');
659
- await log(' This usually means PR creation failed or was skipped incorrectly.');
660
- } else {
661
- await log(' Auto-PR creation is enabled but no PR was created.');
662
- await log(' PR creation may have failed without throwing an error.');
663
- }
664
- await log('');
665
- await log(' 💡 Why this is critical:');
666
- await log(' The solve command requires a PR for:');
667
- await log(' • Tracking work progress');
668
- await log(' • Receiving and processing feedback');
669
- await log(' • Managing code changes');
670
- await log(' • Auto-merging when complete');
671
- await log('');
672
- await log(' 🔧 How to fix:');
673
- await log('');
674
- await log(' Option 1: Create PR manually and use --continue');
675
- await log(` cd ${tempDir}`);
676
- await log(` gh pr create --draft --title "Fix issue #${issueNumber}" --body "Fixes #${issueNumber}"`);
677
- await log(' # Then use the PR URL with solve.mjs');
678
- await log('');
679
- await log(' Option 2: Start fresh without continue mode');
680
- await log(` ./solve.mjs "${issueUrl}" --auto-pull-request-creation`);
681
- await log('');
682
- await log(' Option 3: Disable auto-PR creation (Claude will create it)');
683
- await log(` ./solve.mjs "${issueUrl}" --no-auto-pull-request-creation`);
684
- await log('');
685
- await safeExit(1, 'No PR available');
653
+ await handleNoPrAvailableError({ isContinueMode, tempDir, issueNumber, issueUrl, log, formatAligned });
686
654
  }
687
655
 
688
656
  if (isContinueMode) {
@@ -1495,4 +1463,15 @@ try {
1495
1463
  // Issue #1346: Flush Sentry events before exit.
1496
1464
  // closeSentry() uses a hard Promise.race deadline so it cannot block indefinitely.
1497
1465
  await closeSentry();
1466
+
1467
+ // Issue #1335: Log active handles at exit to diagnose future process hang.
1468
+ if (argv.verbose) {
1469
+ const handles = process._getActiveHandles();
1470
+ const requests = process._getActiveRequests();
1471
+ if (handles.length > 0 || requests.length > 0) {
1472
+ await log(`\n🔍 Active Node.js handles at exit (${handles.length} handles, ${requests.length} requests):`, { verbose: true });
1473
+ for (const h of handles) await log(` Handle: ${h.constructor?.name || typeof h}`, { verbose: true });
1474
+ for (const r of requests) await log(` Request: ${r.constructor?.name || typeof r}`, { verbose: true });
1475
+ }
1476
+ }
1498
1477
  }
@@ -552,6 +552,19 @@ export class MergeQueueProcessor {
552
552
  };
553
553
  }
554
554
 
555
+ // Issue #1425: If the latest commit's CI is still in progress, wait for it to complete
556
+ // rather than proceeding immediately. The WAIT_FOR_TARGET_BRANCH_CI step (below) will
557
+ // also wait, but checking here ensures we don't skip the health check entirely.
558
+ if (healthResult.pending) {
559
+ this.log(`Branch ${targetBranch} has ${healthResult.pendingRuns.length} CI run(s) in progress on the latest commit. Will wait for them to complete.`);
560
+ // Return healthy so the queue proceeds to the waitForTargetBranchCI step which handles waiting
561
+ return {
562
+ healthy: true,
563
+ failedRuns: [],
564
+ error: null,
565
+ };
566
+ }
567
+
555
568
  this.log(`Branch ${targetBranch} CI is healthy. Ready to proceed.`);
556
569
  return {
557
570
  healthy: true,