@link-assistant/hive-mind 1.34.4 → 1.34.6

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,27 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.34.6
4
+
5
+ ### Patch Changes
6
+
7
+ - 3157192: Optimize CI/CD to skip checks for .gitkeep-only changes and harden .gitkeep cleanup logic (Issue #1436).
8
+
9
+ CI/CD jobs `version-check` and `helm-pr-check` now skip when only `.gitkeep` files changed, saving ~21 seconds of runner time per PR on the initial commit. The `detect-code-changes.mjs` script now excludes `.gitkeep` files from code change detection and outputs a `gitkeep-only` flag.
10
+
11
+ The `.gitkeep` cleanup logic in `solve.results.lib.mjs` is hardened with: (1) full commit message body detection (`%B` instead of `%s`) so `.gitkeep` references in commit body are found, (2) fallback file detection via `git diff-tree`, and (3) post-cleanup verification with direct removal fallback to prevent leftover `.gitkeep` files.
12
+
13
+ Also removes the leftover `.gitkeep` file from the repository that was left behind by PR #1420.
14
+
15
+ ## 1.34.5
16
+
17
+ ### Patch Changes
18
+
19
+ - ab070db: Use workflow runs API to detect when CI is not triggered, preventing infinite loop (Issue #1442)
20
+
21
+ 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.
22
+
23
+ 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.
24
+
3
25
  ## 1.34.4
4
26
 
5
27
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.34.4",
3
+ "version": "1.34.6",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -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 — this is a race condition, not "no CI configured"
219
- // Wait for CI checks to appear
220
- if (verbose) {
221
- console.log(`[VERBOSE] /merge: PR #${prNumber} has no CI checks yet, but repo has ${repoWorkflows.count} active workflow(s) - treating as race condition (CI hasn't started)`);
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;
@@ -888,63 +906,14 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
888
906
  // Issue #1314: Distinguish between different waiting reasons
889
907
  const pendingBlocker = blockers.find(b => b.type === 'ci_pending');
890
908
  const cancelledOnly = blockers.every(b => b.type === 'ci_cancelled' || b.type === 'ci_pending');
909
+ const cancelledBlocker = blockers.find(b => b.type === 'ci_cancelled');
891
910
 
892
- // Issue #1335: Detect permanent 'no_checks' state (repo has no CI workflows).
893
- // The 'ci_pending' blocker with message 'have not started yet' means GitHub returned
894
- // zero check-runs and zero commit statuses for this PR's HEAD SHA. This is ambiguous:
895
- // (a) Transient race condition CI workflows exist but haven't queued yet after push.
896
- // (b) Permanent state — the repository has no CI/CD workflows configured at all.
897
- // We resolve the ambiguity by checking if the repo actually has workflow files via the
898
- // GitHub API. If it has none, the 'no_checks' state is permanent and the PR should be
899
- // treated as CI-passing (no CI = nothing to wait for).
900
- const isNoCIChecks = pendingBlocker && pendingBlocker.message.includes('have not started yet');
901
- if (isNoCIChecks) {
902
- // Lazy-check whether the repo has workflows (cache result to avoid repeated API calls)
903
- if (repoHasWorkflows === null) {
904
- const workflowCheck = await getActiveRepoWorkflows(owner, repo, argv.verbose);
905
- repoHasWorkflows = workflowCheck.hasWorkflows;
906
- if (argv.verbose) {
907
- 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));
908
- }
909
- }
910
-
911
- if (!repoHasWorkflows) {
912
- // Root cause confirmed: repo has no CI. The 'no_checks' state is permanent.
913
- // Treat the PR as CI-passing and exit the monitoring loop immediately.
914
- await log('');
915
- await log(formatAligned('ℹ️', 'NO CI WORKFLOWS CONFIGURED', 'Repository has no GitHub Actions workflows'));
916
- await log(formatAligned('', 'Conclusion:', 'No CI expected — treating PR as CI-passing', 2));
917
- await log(formatAligned('', 'Action:', 'Exiting monitoring loop', 2));
918
- await log('');
919
-
920
- // Post a comment explaining the situation
921
- try {
922
- const commentBody = `## ℹ️ No CI Workflows Detected
923
-
924
- No CI/CD checks are configured for this pull request. The repository has no GitHub Actions workflow files in \`.github/workflows/\`.
925
-
926
- 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.
927
-
928
- ---
929
- *Monitored by hive-mind with --auto-restart-until-mergeable flag*`;
930
- await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${commentBody}`;
931
- } catch {
932
- // Don't fail if comment posting fails
933
- }
934
-
935
- return { success: true, reason: 'no_ci_checks', latestSessionId, latestAnthropicCost };
936
- } else {
937
- // Repo has workflows but CI hasn't started yet — transient race condition, keep waiting
938
- await log(formatAligned('⏳', 'Waiting for CI:', 'No checks yet (CI workflows exist, waiting for them to start)', 2));
939
- }
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));
940
915
  } else {
941
- if (cancelledOnly && cancelledBlocker) {
942
- await log(formatAligned('🔄', 'Waiting for re-triggered CI:', cancelledBlocker.details.join(', '), 2));
943
- } else if (pendingBlocker) {
944
- await log(formatAligned('⏳', 'Waiting for CI:', pendingBlocker.details.length > 0 ? pendingBlocker.details.join(', ') : pendingBlocker.message, 2));
945
- } else {
946
- await log(formatAligned('⏳', 'Waiting for:', blockers.map(b => b.message).join(', '), 2));
947
- }
916
+ await log(formatAligned('⏳', 'Waiting for:', blockers.map(b => b.message).join(', '), 2));
948
917
  }
949
918
  } else {
950
919
  await log(formatAligned('', 'No action needed', 'Continuing to monitor...', 2));
@@ -251,11 +251,19 @@ export const cleanupClaudeFile = async (tempDir, branchName, claudeCommitHash =
251
251
  await log(` Detected initial commit: ${claudeCommitHash.substring(0, 7)}`, { verbose: true });
252
252
  }
253
253
 
254
- // Determine which file was used based on the commit message or flags
255
- // Check the commit message to determine which file was committed
256
- const commitMsgResult = await $({ cwd: tempDir })`git log -1 --format=%s ${claudeCommitHash} 2>&1`;
254
+ // Determine which file was used based on the commit message or actual files changed
255
+ // Use %B (full message including body) instead of %s (subject only) to catch ".gitkeep" in body
256
+ // Also check the actual files changed as a fallback (Issue #1436)
257
+ const commitMsgResult = await $({ cwd: tempDir })`git log -1 --format=%B ${claudeCommitHash} 2>&1`;
257
258
  const commitMsg = commitMsgResult.stdout?.trim() || '';
258
- const isGitkeepFile = commitMsg.includes('.gitkeep');
259
+ let isGitkeepFile = commitMsg.includes('.gitkeep');
260
+
261
+ // Fallback: check actual files changed in the commit if message doesn't mention .gitkeep
262
+ if (!isGitkeepFile) {
263
+ const filesResult = await $({ cwd: tempDir })`git diff-tree --no-commit-id --name-only -r ${claudeCommitHash} 2>&1`;
264
+ const files = filesResult.stdout?.trim().split('\n').filter(Boolean) || [];
265
+ isGitkeepFile = files.includes('.gitkeep');
266
+ }
259
267
  const fileName = isGitkeepFile ? '.gitkeep' : 'CLAUDE.md';
260
268
 
261
269
  await log(formatAligned('🔄', 'Cleanup:', `Reverting ${fileName} commit`));
@@ -381,6 +389,31 @@ export const cleanupClaudeFile = async (tempDir, branchName, claudeCommitHash =
381
389
  }
382
390
  }
383
391
  }
392
+ // Post-cleanup verification: check if the file was actually removed (Issue #1436)
393
+ // This catches cases where revert/push succeeded in logs but file still exists
394
+ const verifyResult = await $({ cwd: tempDir })`git ls-files ${fileName} 2>&1`;
395
+ const fileStillExists = verifyResult.code === 0 && verifyResult.stdout && verifyResult.stdout.trim();
396
+ if (fileStillExists) {
397
+ await log(` ⚠️ WARNING: ${fileName} still exists after cleanup — attempting direct removal...`);
398
+ // Check if the file existed before the initial commit (parent)
399
+ const parentCommit = `${claudeCommitHash}~1`;
400
+ const parentFileExists = await $({ cwd: tempDir })`git cat-file -e ${parentCommit}:${fileName} 2>&1`;
401
+ if (parentFileExists.code !== 0) {
402
+ // File didn't exist before the session — force remove it
403
+ await $({ cwd: tempDir })`git rm -f ${fileName} 2>&1`;
404
+ const fallbackCommit = await $({ cwd: tempDir })`git commit -m "Remove leftover ${fileName} (post-cleanup fallback, Issue #1436)" 2>&1`;
405
+ if (fallbackCommit.code === 0) {
406
+ const fallbackPush = await $({ cwd: tempDir })`git push origin ${branchName} 2>&1`;
407
+ if (fallbackPush.code === 0) {
408
+ await log(` ✅ ${fileName} removed via post-cleanup fallback`);
409
+ } else {
410
+ await log(` ⚠️ ${fileName} removed locally but push failed`, { verbose: true });
411
+ }
412
+ }
413
+ } else {
414
+ await log(` ℹ️ ${fileName} existed before this session — keeping pre-existing file`, { verbose: true });
415
+ }
416
+ }
384
417
  } catch (e) {
385
418
  reportError(e, {
386
419
  context: 'cleanup_claude_file',