@link-assistant/hive-mind 1.37.1 → 1.37.3

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,17 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.37.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 7bc72fa: add early --base-branch/--target-branch validation in telegram bot to reject URLs and invalid branch names before spawning solve/hive processes (Issue #1482)
8
+
9
+ ## 1.37.2
10
+
11
+ ### Patch Changes
12
+
13
+ - f07ae29: fix false positive "Ready to merge" by cross-validating CI success status with GitHub Actions workflow runs API and removing unreliable commit-age-based grace period (Issue #1480)
14
+
3
15
  ## 1.37.1
4
16
 
5
17
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.37.1",
3
+ "version": "1.37.3",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -33,7 +33,7 @@ const { reportError } = sentryLib;
33
33
 
34
34
  // Import GitHub merge functions
35
35
  const githubMergeLib = await import('./github-merge.lib.mjs');
36
- const { checkPRMergeable, checkMergePermissions, mergePullRequest, waitForCI, checkForBillingLimitError, getRepoVisibility, BILLING_LIMIT_ERROR_PATTERN, getDetailedCIStatus, rerunWorkflowRun, getWorkflowRunsForSha, getActiveRepoWorkflows, getCommitDate, checkPreviousPRCommitsHadCI, checkWorkflowsHavePRTriggers } = githubMergeLib;
36
+ const { checkPRMergeable, checkMergePermissions, mergePullRequest, waitForCI, checkForBillingLimitError, getRepoVisibility, BILLING_LIMIT_ERROR_PATTERN, getDetailedCIStatus, rerunWorkflowRun, getWorkflowRunsForSha, getActiveRepoWorkflows, getCommitDate, checkWorkflowsHavePRTriggers } = githubMergeLib;
37
37
 
38
38
  // Import GitHub functions for log attachment
39
39
  const githubLib = await import('./github.lib.mjs');
@@ -187,7 +187,7 @@ const checkForNonBotComments = async (owner, repo, prNumber, issueNumber, lastCh
187
187
  * - billing_limit: Billing/spending limit reached → stop (private) or wait (public)
188
188
  * - no_checks: No CI checks yet (race condition) → wait
189
189
  */
190
- const getMergeBlockers = async (owner, repo, prNumber, verbose = false) => {
190
+ const getMergeBlockers = async (owner, repo, prNumber, verbose = false, checkCount = 1) => {
191
191
  const blockers = [];
192
192
 
193
193
  // Use detailed CI status to distinguish between all possible states
@@ -280,60 +280,54 @@ const getMergeBlockers = async (owner, repo, prNumber, verbose = false) => {
280
280
  return { blockers, ciStatus, noCiConfigured: false, noCiTriggered: true };
281
281
  }
282
282
 
283
- if (commitInfo.ageSeconds !== null && commitInfo.ageSeconds < WORKFLOW_RUN_GRACE_PERIOD_SECONDS) {
284
- // Commit is recent workflow runs may not have appeared in the API yet
285
- if (verbose) {
286
- console.log(`[VERBOSE] /merge: PR #${prNumber} has no workflow runs for SHA ${ciStatus.sha.substring(0, 7)}, but commit is only ${commitInfo.ageSeconds}s old (grace period: ${WORKFLOW_RUN_GRACE_PERIOD_SECONDS}s) — treating as potential race condition`);
287
- }
288
-
289
- if (prTriggers.hasPRTriggers) {
290
- // Workflows have PR/push triggers AND commit is recent almost certainly a race condition
283
+ if (prTriggers.hasPRTriggers) {
284
+ // Issue #1480 (enhanced): Workflows have PR/push triggers but no runs yet.
285
+ // This is almost certainly a race condition — GitHub takes 30-120s to register
286
+ // workflow runs after a push. We MUST wait regardless of commit age, because
287
+ // commit date reflects authoring time, NOT push time.
288
+ //
289
+ // The commit may have been authored hours ago but pushed just now (rebased branches,
290
+ // amended commits, cherry-picks). Using commit age as a proxy for push age caused
291
+ // false positives in Case 1 of Issue #1480.
292
+ //
293
+ // Safety valve: after MAX_NO_RUNS_CHECKS consecutive checks (typically 5 × 60s = 5 min),
294
+ // conclude CI was not triggered. This handles cases like paths-ignore excluding all
295
+ // changed files, conditional workflows that don't match, etc.
296
+ const MAX_NO_RUNS_CHECKS = 5;
297
+ if (checkCount >= MAX_NO_RUNS_CHECKS) {
298
+ // We've waited long enough — CI was genuinely not triggered
291
299
  if (verbose) {
292
- console.log(`[VERBOSE] /merge: Workflow files confirm PR/push triggers exist (${prTriggers.workflows.map(w => w.name).join(', ')}) waiting for workflow runs to appear`);
300
+ console.log(`[VERBOSE] /merge: PR #${prNumber} has no workflow runs after ${checkCount} consecutive checksconcluding CI was not triggered despite PR triggers existing`);
293
301
  }
294
- blockers.push({
295
- type: 'ci_pending',
296
- message: `CI/CD workflow runs have not appeared yet — commit is ${commitInfo.ageSeconds}s old, waiting for GitHub to register workflow runs (grace period: ${WORKFLOW_RUN_GRACE_PERIOD_SECONDS}s)`,
297
- details: prTriggers.workflows.map(w => w.name),
298
- });
299
- } else {
300
- // No PR triggers found in workflow files — but commit is still recent, be safe and wait
301
- if (verbose) {
302
- console.log(`[VERBOSE] /merge: No PR/push triggers found in workflow files, but commit is only ${commitInfo.ageSeconds}s old waiting to be safe`);
303
- }
304
- blockers.push({
305
- type: 'ci_pending',
306
- message: `CI/CD workflow runs have not appeared yet commit is ${commitInfo.ageSeconds}s old, waiting for GitHub to register workflow runs (grace period: ${WORKFLOW_RUN_GRACE_PERIOD_SECONDS}s)`,
307
- details: [],
308
- });
302
+ return { blockers, ciStatus, noCiConfigured: false, noCiTriggered: true };
303
+ }
304
+
305
+ if (verbose) {
306
+ console.log(`[VERBOSE] /merge: PR #${prNumber} has no workflow runs for SHA ${ciStatus.sha.substring(0, 7)}, but workflows have PR/push triggers (${prTriggers.workflows.map(w => w.name).join(', ')}) — waiting for workflow runs to appear (check ${checkCount}/${MAX_NO_RUNS_CHECKS}, commit age: ${commitInfo.ageSeconds ?? 'unknown'}s)`);
307
+ }
308
+ blockers.push({
309
+ type: 'ci_pending',
310
+ message: `CI/CD workflow runs have not appeared yet workflows have PR/push triggers (${prTriggers.workflows.map(w => w.name).join(', ')}), waiting for GitHub to register workflow runs (check ${checkCount}/${MAX_NO_RUNS_CHECKS})`,
311
+ details: prTriggers.workflows.map(w => w.name),
312
+ });
313
+ } else if (commitInfo.ageSeconds !== null && commitInfo.ageSeconds < WORKFLOW_RUN_GRACE_PERIOD_SECONDS) {
314
+ // No PR triggers found in workflow files, but commit is still recent be safe and wait
315
+ if (verbose) {
316
+ console.log(`[VERBOSE] /merge: No PR/push triggers found in workflow files, but commit is only ${commitInfo.ageSeconds}s old — waiting to be safe`);
309
317
  }
318
+ blockers.push({
319
+ type: 'ci_pending',
320
+ message: `CI/CD workflow runs have not appeared yet — commit is ${commitInfo.ageSeconds}s old, waiting for GitHub to register workflow runs (grace period: ${WORKFLOW_RUN_GRACE_PERIOD_SECONDS}s)`,
321
+ details: [],
322
+ });
310
323
  } else {
311
- // Commit is old enough (grace period elapsed) but check additional signals before concluding
312
- // Issue #1480: Layer 3 Check if previous commits in this PR had CI runs.
313
- // If earlier commits had CI, the HEAD commit should also have CI unless conditions changed.
314
- const previousCI = await checkPreviousPRCommitsHadCI(owner, repo, prNumber, ciStatus.sha, verbose);
315
-
316
- if (previousCI.hadPreviousCI && prTriggers.hasPRTriggers) {
317
- // Previous commits had CI AND workflow files have PR triggers — something is wrong,
318
- // this could be a GitHub API glitch or delayed registration beyond the grace period.
319
- // Wait one more cycle to be safe.
320
- if (verbose) {
321
- console.log(`[VERBOSE] /merge: PR #${prNumber} previous commits had CI (${previousCI.previousCommitsWithCI}/${previousCI.totalPreviousCommits}) and workflows have PR triggers, but HEAD has no runs — waiting as safety measure`);
322
- }
323
- blockers.push({
324
- type: 'ci_pending',
325
- message: `CI/CD workflow runs missing for HEAD — previous PR commits had CI (${previousCI.previousCommitsWithCI} of ${previousCI.totalPreviousCommits}), workflows have PR triggers, possible API delay`,
326
- details: prTriggers.workflows.map(w => w.name),
327
- });
328
- } else {
329
- // CI was definitively NOT triggered
330
- // Issue #1442: Fork PRs needing maintainer approval, paths-ignore filtering,
331
- // workflow conditions not matching, etc. all result in zero workflow runs.
332
- if (verbose) {
333
- console.log(`[VERBOSE] /merge: PR #${prNumber} has no CI checks and no workflow runs for SHA ${ciStatus.sha.substring(0, 7)} (commit age: ${commitInfo.ageSeconds ?? 'unknown'}s, grace period: ${WORKFLOW_RUN_GRACE_PERIOD_SECONDS}s elapsed, previous CI: ${previousCI.hadPreviousCI}, PR triggers: ${prTriggers.hasPRTriggers}) — CI was not triggered`);
334
- }
335
- return { blockers, ciStatus, noCiConfigured: false, noCiTriggered: true };
324
+ // No PR triggers AND commit is old enough — CI was definitively NOT triggered
325
+ // Issue #1442: Fork PRs needing maintainer approval, paths-ignore filtering,
326
+ // workflow conditions not matching, etc. all result in zero workflow runs.
327
+ if (verbose) {
328
+ console.log(`[VERBOSE] /merge: PR #${prNumber} has no CI checks and no workflow runs for SHA ${ciStatus.sha.substring(0, 7)} (commit age: ${commitInfo.ageSeconds ?? 'unknown'}s, no PR/push triggers in workflow files) — CI was not triggered`);
336
329
  }
330
+ return { blockers, ciStatus, noCiConfigured: false, noCiTriggered: true };
337
331
  }
338
332
  }
339
333
  } else {
@@ -355,6 +349,60 @@ const getMergeBlockers = async (owner, repo, prNumber, verbose = false) => {
355
349
  details: [],
356
350
  });
357
351
  }
352
+ } else if (ciStatus.status === 'success') {
353
+ // Issue #1480: Cross-validate "success" with workflow runs API.
354
+ // A fast external check (e.g., CodeFactor) can register and pass before the main CI
355
+ // pipeline starts, causing getDetailedCIStatus to return 'success' prematurely.
356
+ // We must verify that all expected workflow runs have actually completed.
357
+ const workflowRuns = await getWorkflowRunsForSha(owner, repo, ciStatus.sha, verbose);
358
+
359
+ if (workflowRuns.length > 0) {
360
+ // Workflow runs exist — check if any are still running
361
+ const incompleteRuns = workflowRuns.filter(r => r.status !== 'completed');
362
+ if (incompleteRuns.length > 0) {
363
+ // Some workflow runs are still in progress — more check-runs may appear
364
+ if (verbose) {
365
+ console.log(`[VERBOSE] /merge: PR #${prNumber} CI status is 'success' (${ciStatus.passedChecks.length} checks passed), but ${incompleteRuns.length} workflow run(s) still in progress — waiting for completion`);
366
+ }
367
+ blockers.push({
368
+ type: 'ci_pending',
369
+ message: `CI checks show success (${ciStatus.passedChecks.length} passed) but ${incompleteRuns.length} workflow run(s) still in progress — waiting for all to complete`,
370
+ details: incompleteRuns.map(r => r.name),
371
+ });
372
+ }
373
+ // All workflow runs completed — the check-runs we see are the final set, trust the 'success' status
374
+ } else {
375
+ // No workflow runs for this SHA — the passed checks are from external services only
376
+ // (e.g., CodeFactor, Codecov). Check if the repo has workflows that should produce runs.
377
+ const repoWorkflows = await getActiveRepoWorkflows(owner, repo, verbose);
378
+ if (repoWorkflows.hasWorkflows) {
379
+ const prTriggers = await checkWorkflowsHavePRTriggers(owner, repo, verbose);
380
+ if (prTriggers.hasPRTriggers) {
381
+ // Repo has workflows with PR triggers but no runs yet — CI hasn't started
382
+ // This is the exact scenario from Case 2 of Issue #1480
383
+ //
384
+ // Safety valve: after MAX_NO_RUNS_CHECKS consecutive checks, trust the external checks
385
+ const MAX_NO_RUNS_CHECKS = 5;
386
+ if (checkCount >= MAX_NO_RUNS_CHECKS) {
387
+ if (verbose) {
388
+ console.log(`[VERBOSE] /merge: PR #${prNumber} CI 'success' with ${ciStatus.passedChecks.length} external checks, no workflow runs after ${checkCount} checks — trusting external checks`);
389
+ }
390
+ // Fall through — trust the success status from external checks
391
+ } else {
392
+ if (verbose) {
393
+ console.log(`[VERBOSE] /merge: PR #${prNumber} CI status is 'success' (${ciStatus.passedChecks.length} external checks), but repo has PR-triggered workflows with 0 workflow runs — likely race condition (check ${checkCount}/${MAX_NO_RUNS_CHECKS})`);
394
+ }
395
+ // Wait for GitHub Actions to register workflow runs
396
+ blockers.push({
397
+ type: 'ci_pending',
398
+ message: `CI shows ${ciStatus.passedChecks.length} passed check(s) from external services, but repo has PR-triggered workflows that haven't started yet — waiting for GitHub Actions to register (check ${checkCount}/${MAX_NO_RUNS_CHECKS})`,
399
+ details: prTriggers.workflows.map(w => w.name),
400
+ });
401
+ }
402
+ }
403
+ }
404
+ // No repo workflows → external checks are the only CI, trust the 'success' status
405
+ }
358
406
  } else if (ciStatus.status === 'pending') {
359
407
  // CI is still running or queued - wait for completion
360
408
  const pendingNames = [...ciStatus.pendingChecks, ...ciStatus.queuedChecks].map(c => c.name);
@@ -507,7 +555,7 @@ export const watchUntilMergeable = async params => {
507
555
 
508
556
  try {
509
557
  // Get merge blockers
510
- const { blockers, noCiConfigured, noCiTriggered, workflowRunConclusions } = await getMergeBlockers(owner, repo, prNumber, argv.verbose);
558
+ const { blockers, noCiConfigured, noCiTriggered, workflowRunConclusions } = await getMergeBlockers(owner, repo, prNumber, argv.verbose, iteration);
511
559
 
512
560
  // Check for new comments from non-bot users
513
561
  const { hasNewComments, comments } = await checkForNonBotComments(owner, repo, prNumber, issueNumber, lastCheckTime, argv.verbose);
@@ -187,6 +187,24 @@ export function validateBranchName(branchName) {
187
187
  return { valid: true };
188
188
  }
189
189
 
190
+ // Issue #1482: Validate --base-branch/--target-branch values in an args array
191
+ // Used by telegram-bot.mjs for early validation before spawning processes
192
+ export function validateBranchInArgs(args) {
193
+ const branchFlags = ['--base-branch', '-b', '--target-branch', '-tb'];
194
+ for (let i = 0; i < args.length; i++) {
195
+ for (const flag of branchFlags) {
196
+ if (args[i] === flag && i + 1 < args.length) {
197
+ const v = validateBranchName(args[i + 1]);
198
+ if (!v.valid) return `Invalid ${flag} value: ${v.reason}`;
199
+ } else if (args[i].startsWith(flag + '=')) {
200
+ const v = validateBranchName(args[i].substring(flag.length + 1));
201
+ if (!v.valid) return `Invalid ${flag} value: ${v.reason}`;
202
+ }
203
+ }
204
+ }
205
+ return null;
206
+ }
207
+
190
208
  export async function createOrCheckoutBranch({ isContinueMode, prBranch, issueNumber, tempDir, defaultBranch, argv, log, formatAligned, $, crypto, owner, repo, prNumber }) {
191
209
  // Create a branch for the issue or checkout existing PR branch
192
210
  let branchName;
@@ -48,6 +48,7 @@ const { createYargsConfig: createSolveYargsConfig, detectMalformedFlags } = awai
48
48
  const { createYargsConfig: createHiveYargsConfig } = await import('./hive.config.lib.mjs');
49
49
  const { parseGitHubUrl } = await import('./github.lib.mjs');
50
50
  const { validateModelName, buildModelOptionDescription } = await import('./models/index.mjs');
51
+ const { validateBranchInArgs } = await import('./solve.branch.lib.mjs');
51
52
  const { formatUsageMessage, getAllCachedLimits } = await import('./limits.lib.mjs');
52
53
  const { getVersionInfo, formatVersionMessage } = await import('./version-info.lib.mjs');
53
54
  const { escapeMarkdown, escapeMarkdownV2, cleanNonPrintableChars, makeSpecialCharsVisible } = await import('./telegram-markdown.lib.mjs');
@@ -203,6 +204,9 @@ if (solveEnabled && solveOverrides.length > 0) {
203
204
  throw new Error(msg);
204
205
  });
205
206
  await testYargs.parse(testArgs);
207
+ // Issue #1482: Validate --base-branch in overrides early
208
+ const overrideBranchError = validateBranchInArgs(solveOverrides);
209
+ if (overrideBranchError) throw new Error(overrideBranchError);
206
210
  console.log('✅ Solve overrides validated successfully');
207
211
  } finally {
208
212
  // Restore stderr
@@ -243,6 +247,11 @@ if (hiveEnabled && hiveOverrides.length > 0) {
243
247
  throw new Error(msg);
244
248
  });
245
249
  await testYargs.parse(testArgs);
250
+ // Issue #1482: Validate --base-branch/--target-branch in overrides early
251
+ const overrideBranchError = validateBranchInArgs(hiveOverrides);
252
+ if (overrideBranchError) {
253
+ throw new Error(overrideBranchError);
254
+ }
246
255
  console.log('✅ Hive overrides validated successfully');
247
256
  } finally {
248
257
  // Restore stderr
@@ -957,6 +966,12 @@ async function handleSolveCommand(ctx) {
957
966
  await ctx.reply(`❌ ${modelError}`, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
958
967
  return;
959
968
  }
969
+ // Issue #1482: Validate --base-branch early to reject URLs and invalid branch names
970
+ const branchError = validateBranchInArgs(args);
971
+ if (branchError) {
972
+ await ctx.reply(`❌ ${branchError}`, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
973
+ return;
974
+ }
960
975
  // Issue #1092: Detect malformed flag patterns like "-- model" (space after --)
961
976
  const { malformed, errors: malformedErrors } = detectMalformedFlags(args);
962
977
  if (malformed.length > 0) {
@@ -1137,6 +1152,12 @@ async function handleHiveCommand(ctx) {
1137
1152
  await ctx.reply(`❌ ${hiveModelError}`, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
1138
1153
  return;
1139
1154
  }
1155
+ // Issue #1482: Validate branch flags early to reject URLs and invalid branch names
1156
+ const hiveBranchError = validateBranchInArgs(args);
1157
+ if (hiveBranchError) {
1158
+ await ctx.reply(`❌ ${hiveBranchError}`, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
1159
+ return;
1160
+ }
1140
1161
 
1141
1162
  // Validate merged arguments using hive's yargs config
1142
1163
  try {