@link-assistant/hive-mind 1.50.7 → 1.50.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,16 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.50.8
4
+
5
+ ### Patch Changes
6
+
7
+ - 5760755: fix: default to PR-branch-only CI check, add pagination and typo fix (#1573)
8
+ - Fix typo: `--wait-for-all-actions-in-repository-before-mergable` → `--wait-for-all-actions-in-repository-before-mergeable` (deprecated alias kept for backward compatibility)
9
+ - When repo-wide flag is enabled, block on ALL active runs regardless of branch (no branch filtering) to ensure safety when CI/CD pipelines interact
10
+ - Add `--paginate` to `getPRCommitShas()` to load all PR commits (not just first page)
11
+ - Add all-commits CI check: verify CI completes for every commit on the PR branch, not just HEAD
12
+ - Add `getPRCommitShas()` and `checkAllPRCommitsCI()` for per-commit CI verification
13
+
3
14
  ## 1.50.7
4
15
 
5
16
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.50.7",
3
+ "version": "1.50.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",
@@ -69,9 +69,61 @@ export async function waitForAllRepoActions(owner, repo, options = {}, verbose =
69
69
  return { success: false, waitedForRuns: true, timedOut: true, remainingRuns: finalRuns.runs };
70
70
  }
71
71
 
72
+ /**
73
+ * Get all commit SHAs for a pull request branch.
74
+ * @param {string} owner - Repository owner
75
+ * @param {string} repo - Repository name
76
+ * @param {number} prNumber - Pull request number
77
+ * @param {boolean} verbose - Whether to log verbose output
78
+ * @returns {Promise<string[]>} Array of commit SHAs
79
+ */
80
+ export async function getPRCommitShas(owner, repo, prNumber, verbose = false) {
81
+ try {
82
+ const { stdout } = await exec(`gh api "repos/${owner}/${repo}/pulls/${prNumber}/commits" --paginate --jq '[.[].sha]'`);
83
+ const shas = JSON.parse(stdout.trim() || '[]');
84
+ if (verbose && shas.length > 1) {
85
+ console.log(`[VERBOSE] pr-commits: ${shas.length} commits on PR #${prNumber}`);
86
+ }
87
+ return shas;
88
+ } catch {
89
+ return [];
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Check that workflow runs for ALL commits on the PR branch have completed.
95
+ * @param {string} owner - Repository owner
96
+ * @param {string} repo - Repository name
97
+ * @param {number} prNumber - Pull request number
98
+ * @param {boolean} verbose - Whether to log verbose output
99
+ * @param {Function} getWorkflowRunsForSha - Function to get workflow runs for a SHA
100
+ * @returns {Promise<{allComplete: boolean, totalCommits: number, pendingCommits: string[], details: Object[]}>}
101
+ */
102
+ export async function checkAllPRCommitsCI(owner, repo, prNumber, verbose, getWorkflowRunsForSha) {
103
+ const shas = await getPRCommitShas(owner, repo, prNumber, verbose);
104
+ if (shas.length === 0) return { allComplete: true, totalCommits: 0, pendingCommits: [], details: [] };
105
+
106
+ const details = [];
107
+ const pendingCommits = [];
108
+ for (const sha of shas) {
109
+ const runs = await getWorkflowRunsForSha(owner, repo, sha, false);
110
+ const inProgress = runs.filter(r => r.status !== 'completed');
111
+ const complete = inProgress.length === 0;
112
+ details.push({ sha: sha.substring(0, 7), total: runs.length, inProgress: inProgress.length, complete });
113
+ if (!complete) pendingCommits.push(sha.substring(0, 7));
114
+ }
115
+
116
+ if (verbose && pendingCommits.length > 0) {
117
+ console.log(`[VERBOSE] pr-commits: ${pendingCommits.length}/${shas.length} commits have in-progress CI: ${pendingCommits.join(', ')}`);
118
+ }
119
+
120
+ return { allComplete: pendingCommits.length === 0, totalCommits: shas.length, pendingCommits, details };
121
+ }
122
+
72
123
  /**
73
124
  * Multi-mechanism CI consensus check. Requires Check Runs API, Workflow Runs API,
74
125
  * and optionally repo-wide active runs to ALL agree before concluding CI is complete.
126
+ * Issue #1573: Also checks all PR commits' CI (not just head SHA) by default.
75
127
  * @param {Object} params
76
128
  * @returns {Promise<{allAgree: boolean, mechanisms: Object, ciStatus: Object, workflowRuns: Array}>}
77
129
  */
@@ -82,6 +134,15 @@ export async function checkCIConsensus({ owner, repo, prNumber, sha, waitForAllR
82
134
  const workflowRuns = await getWorkflowRunsForSha(owner, repo, sha, verbose);
83
135
  const workflowsOK = workflowRuns.length === 0 || workflowRuns.every(r => r.status === 'completed');
84
136
 
137
+ let allCommitsOK = true;
138
+ let allCommitsInfo = null;
139
+ if (checkRunsOK && workflowsOK && prNumber) {
140
+ allCommitsInfo = await checkAllPRCommitsCI(owner, repo, prNumber, verbose, getWorkflowRunsForSha);
141
+ allCommitsOK = allCommitsInfo.allComplete;
142
+ }
143
+
144
+ // When enabled, block on ANY active CI/CD run in the repository regardless of branch.
145
+ // This ensures safety when CI/CD pipelines interact or depend on each other.
85
146
  let repoOK = true;
86
147
  let repoInfo = null;
87
148
  if (waitForAllRepoActionsFlag) {
@@ -89,15 +150,18 @@ export async function checkCIConsensus({ owner, repo, prNumber, sha, waitForAllR
89
150
  repoOK = !repoInfo.hasActiveRuns;
90
151
  }
91
152
 
92
- const allAgree = checkRunsOK && workflowsOK && repoOK;
153
+ const allAgree = checkRunsOK && workflowsOK && allCommitsOK && repoOK;
93
154
  const mechanisms = {
94
155
  checkRunsAPI: { complete: checkRunsOK, status: ciStatus.status },
95
156
  workflowRunsAPI: { complete: workflowsOK, total: workflowRuns.length, inProgress: workflowRuns.filter(r => r.status !== 'completed').length },
157
+ allCommitsCI: allCommitsInfo ? { complete: allCommitsOK, totalCommits: allCommitsInfo.totalCommits, pendingCommits: allCommitsInfo.pendingCommits } : { skipped: true },
96
158
  repoActions: waitForAllRepoActionsFlag ? { complete: repoOK, count: repoInfo?.count ?? 0 } : { skipped: true },
97
159
  };
98
160
 
99
161
  if (verbose) {
100
- console.log(`[VERBOSE] consensus: CheckRuns=${checkRunsOK}(${ciStatus.status}), WorkflowRuns=${workflowsOK}(${workflowRuns.length}), RepoActions=${waitForAllRepoActionsFlag ? repoOK : 'skip'}${allAgree ? 'AGREE' : 'DISAGREE'}`);
162
+ const repoLabel = waitForAllRepoActionsFlag ? `${repoOK}(${repoInfo?.count ?? 0} active)` : 'skip';
163
+ const commitsLabel = allCommitsInfo ? `${allCommitsOK}(${allCommitsInfo.totalCommits} commits${allCommitsInfo.pendingCommits.length > 0 ? `, ${allCommitsInfo.pendingCommits.length} pending` : ''})` : 'skip';
164
+ console.log(`[VERBOSE] consensus: CheckRuns=${checkRunsOK}(${ciStatus.status}), WorkflowRuns=${workflowsOK}(${workflowRuns.length}), AllCommits=${commitsLabel}, RepoActions=${repoLabel} → ${allAgree ? 'AGREE' : 'DISAGREE'}`);
101
165
  }
102
166
  return { allAgree, mechanisms, ciStatus, workflowRuns };
103
167
  }
@@ -1332,7 +1332,7 @@ export async function getCommitDate(owner, repo, sha, verbose = false) {
1332
1332
  export async function checkPreviousPRCommitsHadCI(owner, repo, prNumber, headSha, verbose = false) {
1333
1333
  try {
1334
1334
  // Get all commits in the PR
1335
- const { stdout: commitsJson } = await exec(`gh api "repos/${owner}/${repo}/pulls/${prNumber}/commits?per_page=100" --jq '[.[].sha]'`);
1335
+ const { stdout: commitsJson } = await exec(`gh api "repos/${owner}/${repo}/pulls/${prNumber}/commits" --paginate --jq '[.[].sha]'`);
1336
1336
  const allShas = JSON.parse(commitsJson.trim() || '[]');
1337
1337
 
1338
1338
  // Exclude the current HEAD SHA
@@ -1455,8 +1455,8 @@ export async function checkWorkflowsHavePRTriggers(owner, repo, verbose = false,
1455
1455
  import { waitForCommitCI, checkBranchCIHealth, getMergeCommitSha } from './github-merge-ci.lib.mjs';
1456
1456
  export { waitForCommitCI, checkBranchCIHealth, getMergeCommitSha };
1457
1457
 
1458
- import { getAllActiveRepoRuns, waitForAllRepoActions, checkCIConsensus } from './github-merge-repo-actions.lib.mjs'; // Issue #1503
1459
- export { getAllActiveRepoRuns, waitForAllRepoActions, checkCIConsensus };
1458
+ import { getAllActiveRepoRuns, waitForAllRepoActions, checkCIConsensus, checkAllPRCommitsCI, getPRCommitShas } from './github-merge-repo-actions.lib.mjs'; // Issue #1503
1459
+ export { getAllActiveRepoRuns, waitForAllRepoActions, checkCIConsensus, checkAllPRCommitsCI, getPRCommitShas };
1460
1460
 
1461
1461
  export default {
1462
1462
  READY_LABEL,
@@ -69,8 +69,9 @@ export const watchUntilMergeable = async params => {
69
69
  const MIN_CI_CHECK_INTERVAL_SECONDS = 120;
70
70
  const watchInterval = Math.max(rawWatchInterval, MIN_CI_CHECK_INTERVAL_SECONDS);
71
71
  const isAutoMerge = argv.autoMerge || false;
72
- // Issue #1503: --wait-for-all-actions-in-repository-before-mergable (default: true)
73
- const waitForAllRepoActionsFlag = argv.waitForAllActionsInRepositoryBeforeMergable ?? argv['wait-for-all-actions-in-repository-before-mergable'] ?? true;
72
+ // Issue #1503/#1573: --wait-for-all-actions-in-repository-before-mergeable
73
+ // When enabled (default: true), blocks merge if ANY CI/CD run in the repo is active — ensures safety when pipelines interact.
74
+ const waitForAllRepoActionsFlag = argv.waitForAllActionsInRepositoryBeforeMergeable ?? argv['wait-for-all-actions-in-repository-before-mergeable'] ?? argv.waitForAllActionsInRepositoryBeforeMergable ?? argv['wait-for-all-actions-in-repository-before-mergable'] ?? false;
74
75
 
75
76
  // Track latest session data across all iterations for accurate pricing
76
77
  let latestSessionId = null;
@@ -219,7 +220,9 @@ export const watchUntilMergeable = async params => {
219
220
 
220
221
  if (!consensus.allAgree) {
221
222
  const m = consensus.mechanisms;
222
- await log(formatAligned('🔄', 'CI mechanisms DISAGREE:', `CheckRuns=${m.checkRunsAPI.status}, WorkflowRuns=${m.workflowRunsAPI.inProgress} in-progress, RepoActions=${m.repoActions.skipped ? 'skipped' : m.repoActions.count + ' active'}`, 2));
223
+ const repoLabel = m.repoActions.skipped ? 'skipped' : `${m.repoActions.count} active`;
224
+ const commitsLabel = m.allCommitsCI.skipped ? 'skipped' : `${m.allCommitsCI.pendingCommits.length} pending of ${m.allCommitsCI.totalCommits}`;
225
+ await log(formatAligned('🔄', 'CI mechanisms DISAGREE:', `CheckRuns=${m.checkRunsAPI.status}, WorkflowRuns=${m.workflowRunsAPI.inProgress} in-progress, AllCommits=${commitsLabel}, RepoActions=${repoLabel}`, 2));
223
226
  await log(formatAligned('⏳', 'Continuing to monitor...', 'Mechanisms must agree before declaring mergeable', 2));
224
227
  consecutiveNoRunsChecks = 0;
225
228
  lastCheckTime = currentTime;
@@ -229,7 +232,8 @@ export const watchUntilMergeable = async params => {
229
232
  await interruptibleSleep(actualWaitSeconds * 1000);
230
233
  continue;
231
234
  }
232
- await log(formatAligned('✅', 'All CI mechanisms agree:', `CheckRuns=${consensus.mechanisms.checkRunsAPI.status}, WorkflowRuns=complete(${consensus.mechanisms.workflowRunsAPI.total}), RepoActions=${consensus.mechanisms.repoActions.skipped ? 'skipped' : 'clear'}`, 2));
235
+ const acLabel = consensus.mechanisms.allCommitsCI.skipped ? '' : `, AllCommits=complete(${consensus.mechanisms.allCommitsCI.totalCommits})`;
236
+ await log(formatAligned('✅', 'All CI mechanisms agree:', `CheckRuns=${consensus.mechanisms.checkRunsAPI.status}, WorkflowRuns=complete(${consensus.mechanisms.workflowRunsAPI.total})${acLabel}, RepoActions=${consensus.mechanisms.repoActions.skipped ? 'skipped' : 'clear'}`, 2));
233
237
  } else if (waitForAllRepoActionsFlag) {
234
238
  // Even with no CI configured, check repo-wide actions for absolute safety
235
239
  const repoRuns = await getAllActiveRepoRuns(owner, repo, argv.verbose);
@@ -186,11 +186,16 @@ export const SOLVE_OPTION_DEFINITIONS = {
186
186
  description: 'Auto-restart until PR becomes mergeable (no iteration limit). Restarts on new comments from non-bot users, CI failures, merge conflicts, or other issues. Does NOT auto-merge.',
187
187
  default: true,
188
188
  },
189
- 'wait-for-all-actions-in-repository-before-mergable': {
189
+ 'wait-for-all-actions-in-repository-before-mergeable': {
190
190
  type: 'boolean',
191
- description: 'Wait for ALL active GitHub Actions workflow runs in the entire repository to complete before declaring PR mergeable. Provides absolute safety against interacting CI/CD pipelines. Enabled by default.',
191
+ description: 'Wait for ALL active GitHub Actions workflow runs in the entire repository to complete before declaring PR mergeable. When enabled, blocks merge if ANY CI/CD run in the repository is active, regardless of branch — this ensures safety when CI/CD pipelines interact or depend on each other. Enabled by default.',
192
192
  default: true,
193
193
  },
194
+ 'wait-for-all-actions-in-repository-before-mergable': {
195
+ type: 'boolean',
196
+ description: 'Deprecated alias for --wait-for-all-actions-in-repository-before-mergeable (fixes typo).',
197
+ hidden: true,
198
+ },
194
199
  'auto-restart-on-non-updated-pull-request-description': {
195
200
  type: 'boolean',
196
201
  description: 'Automatically restart if PR title or description still contains auto-generated placeholder text after agent execution. Restarts with a hint about what was not updated.',