@link-assistant/hive-mind 0.50.11 → 0.51.0

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,36 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 0.51.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 36f23fb: Add fork parent validation to prevent nested fork hierarchy issues (#967)
8
+
9
+ This release adds early validation of fork parent relationships to prevent issues where a fork was created from an intermediate fork (fork of a fork) instead of directly from the intended upstream repository.
10
+
11
+ **Problem solved:**
12
+ When a user's fork was created from an intermediate fork (e.g., `user/repo` forked from `someone-else/repo` which was itself forked from `upstream/repo`), any pull requests created would include all commits that exist in the intermediate fork but not in the upstream. This could result in PRs with hundreds or thousands of unexpected commits.
13
+
14
+ **Case study (Issue #967):**
15
+ A fork `konard/zamtmn-zcad` was created from `veb86/zcadvelecAI` (intermediate fork with 1,678 extra commits) instead of `zamtmn/zcad` (the upstream). This resulted in a PR with 1,681 commits instead of the expected 3 commits.
16
+
17
+ **Changes:**
18
+ - **New function `validateForkParent()`**: Validates that a fork's parent matches the expected upstream repository before using it. Checks both the immediate parent and ultimate source (root) of the fork hierarchy.
19
+ - **Early validation**: Fork parent is now validated immediately after an existing fork is found, BEFORE syncing or creating branches. This prevents wasted work and provides clear error messages early.
20
+ - **Detailed error messages**: When a fork parent mismatch is detected, users receive comprehensive information including:
21
+ - The actual fork hierarchy (parent and source repositories)
22
+ - Why this is a problem (unexpected commits in PRs)
23
+ - Three concrete fix options:
24
+ 1. Delete the problematic fork and create a fresh one
25
+ 2. Use `--prefix-fork-name-with-owner-name` to create a new fork with a different name
26
+ 3. Work directly on the repository with `--no-fork` if you have write access
27
+ - **Unit tests**: Added comprehensive test suite (`tests/test-fork-parent-validation.mjs`) with 10 tests covering the validation logic, error handling, and documentation.
28
+
29
+ **Technical details:**
30
+ - Uses GitHub API to fetch fork relationship: `gh api repos/{fork} --jq '{fork: .fork, parent: .parent.full_name, source: .source.full_name}'`
31
+ - Validates in two code paths: when finding existing forks (strict error) and when using forkOwner from PR mode (warning only)
32
+ - Reports validation errors to Sentry for monitoring
33
+
3
34
  ## 0.50.11
4
35
 
5
36
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "0.50.11",
3
+ "version": "0.51.0",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -97,6 +97,100 @@ export const checkExistingForkOfRoot = async rootRepo => {
97
97
  }
98
98
  };
99
99
 
100
+ /**
101
+ * Validate that a fork's parent matches the expected upstream repository.
102
+ * This prevents issues where a fork was created from an intermediate fork (fork of a fork)
103
+ * instead of directly from the intended upstream repository.
104
+ *
105
+ * @param {string} forkRepo - The fork repository to validate (e.g., "user/repo")
106
+ * @param {string} expectedUpstream - The expected upstream repository (e.g., "owner/repo")
107
+ * @returns {Promise<{isValid: boolean, isFork: boolean, parent: string|null, source: string|null, error: string|null}>}
108
+ */
109
+ export const validateForkParent = async (forkRepo, expectedUpstream) => {
110
+ try {
111
+ const forkInfoResult = await $`gh api repos/${forkRepo} --jq '{fork: .fork, parent: .parent.full_name, source: .source.full_name}'`;
112
+
113
+ if (forkInfoResult.code !== 0) {
114
+ return {
115
+ isValid: false,
116
+ isFork: false,
117
+ parent: null,
118
+ source: null,
119
+ error: `Failed to get fork info for ${forkRepo}`,
120
+ };
121
+ }
122
+
123
+ const forkInfo = JSON.parse(forkInfoResult.stdout.toString().trim());
124
+ const isFork = forkInfo.fork === true;
125
+ const parent = forkInfo.parent || null;
126
+ const source = forkInfo.source || null;
127
+
128
+ // If not a fork at all, it's invalid for our purposes
129
+ if (!isFork) {
130
+ return {
131
+ isValid: false,
132
+ isFork: false,
133
+ parent: null,
134
+ source: null,
135
+ error: `Repository ${forkRepo} is not a GitHub fork`,
136
+ };
137
+ }
138
+
139
+ // The fork's PARENT (immediate upstream) should match expectedUpstream
140
+ // The SOURCE (ultimate root) is also acceptable as it indicates the fork
141
+ // is part of the correct hierarchy, just at a different level
142
+ const parentMatches = parent === expectedUpstream;
143
+ const sourceMatches = source === expectedUpstream;
144
+
145
+ // Ideal case: parent matches directly (fork was made from expected upstream)
146
+ if (parentMatches) {
147
+ return {
148
+ isValid: true,
149
+ isFork: true,
150
+ parent,
151
+ source,
152
+ error: null,
153
+ };
154
+ }
155
+
156
+ // Special case: source matches but parent doesn't
157
+ // This means the fork was made from an intermediate fork
158
+ // For issue #967, this is the problematic case we want to catch
159
+ if (sourceMatches && !parentMatches) {
160
+ return {
161
+ isValid: false,
162
+ isFork: true,
163
+ parent,
164
+ source,
165
+ error: `Fork ${forkRepo} was created from ${parent} (intermediate fork), not directly from ${expectedUpstream}. ` + `This can cause pull requests to include unexpected commits from the intermediate fork.`,
166
+ };
167
+ }
168
+
169
+ // Neither parent nor source matches - completely different repository tree
170
+ return {
171
+ isValid: false,
172
+ isFork: true,
173
+ parent,
174
+ source,
175
+ error: `Fork ${forkRepo} is from a different repository tree (parent: ${parent}, source: ${source}) and cannot be used with ${expectedUpstream}`,
176
+ };
177
+ } catch (error) {
178
+ reportError(error, {
179
+ context: 'validate_fork_parent',
180
+ forkRepo,
181
+ expectedUpstream,
182
+ operation: 'check_fork_hierarchy',
183
+ });
184
+ return {
185
+ isValid: false,
186
+ isFork: false,
187
+ parent: null,
188
+ source: null,
189
+ error: `Error validating fork parent: ${error.message}`,
190
+ };
191
+ }
192
+ };
193
+
100
194
  // Create or find temporary directory for cloning the repository
101
195
  export const setupTempDirectory = async argv => {
102
196
  let tempDir;
@@ -315,8 +409,61 @@ export const setupRepository = async (argv, owner, repo, forkOwner = null, issue
315
409
  }
316
410
 
317
411
  if (existingForkName) {
318
- // Fork exists
412
+ // Fork exists - validate that its parent matches the expected upstream
319
413
  await log(`${formatAligned('✅', 'Fork exists:', existingForkName)}`);
414
+ await log(`${formatAligned('🔍', 'Validating fork parent...', '')}`);
415
+
416
+ const forkValidation = await validateForkParent(existingForkName, `${owner}/${repo}`);
417
+
418
+ if (!forkValidation.isValid) {
419
+ // Fork parent mismatch detected - this prevents issue #967
420
+ await log('');
421
+ await log(`${formatAligned('❌', 'FORK PARENT MISMATCH DETECTED', '')}`, { level: 'error' });
422
+ await log('');
423
+ await log(' 🔍 What happened:');
424
+ if (!forkValidation.isFork) {
425
+ await log(` The repository ${existingForkName} is NOT a GitHub fork.`);
426
+ await log(' It may have been created by cloning and pushing instead of forking.');
427
+ } else {
428
+ await log(` Your fork ${existingForkName} was created from an intermediate fork,`);
429
+ await log(` not directly from the target repository ${owner}/${repo}.`);
430
+ }
431
+ await log('');
432
+ await log(' 📦 Fork relationship:');
433
+ await log(` • Your fork: ${existingForkName}`);
434
+ await log(` • Fork parent: ${forkValidation.parent || 'N/A (not a fork)'}`);
435
+ await log(` • Fork source (root): ${forkValidation.source || 'N/A'}`);
436
+ await log(` • Expected parent: ${owner}/${repo}`);
437
+ await log('');
438
+ await log(' ⚠️ Why this is a problem:');
439
+ await log(' When a fork is created from an intermediate fork (a "fork of a fork"),');
440
+ await log(' any commits that exist in the intermediate fork but not in the target');
441
+ await log(' repository will be included in your pull requests. This can result in');
442
+ await log(' pull requests with hundreds or thousands of unexpected commits.');
443
+ await log('');
444
+ await log(' 📖 Case study: See issue #967');
445
+ await log(' A fork created from veb86/zcadvelecAI (which had 1,678 extra commits)');
446
+ await log(' instead of zamtmn/zcad resulted in a PR with 1,681 commits');
447
+ await log(' instead of the expected 3 commits.');
448
+ await log('');
449
+ await log(' 💡 How to fix:');
450
+ await log('');
451
+ await log(' Option 1: Delete the problematic fork and create a fresh one');
452
+ await log(` gh repo delete ${existingForkName}`);
453
+ await log(` Then run this command again to create a proper fork of ${owner}/${repo}`);
454
+ await log('');
455
+ await log(' Option 2: Use --prefix-fork-name-with-owner-name to create a new fork');
456
+ await log(` This creates a fork named ${currentUser}/${owner}-${repo} instead`);
457
+ await log(` ./solve.mjs "${issueUrl || `https://github.com/${owner}/${repo}/issues/<number>`}" --prefix-fork-name-with-owner-name --fork`);
458
+ await log('');
459
+ await log(' Option 3: Work directly on the repository (if you have write access)');
460
+ await log(` ./solve.mjs "${issueUrl || `https://github.com/${owner}/${repo}/issues/<number>`}" --no-fork`);
461
+ await log('');
462
+
463
+ await safeExit(1, 'Fork parent mismatch - fork was created from intermediate fork');
464
+ }
465
+
466
+ await log(`${formatAligned('✅', 'Fork parent validated:', `${forkValidation.parent}`)}`);
320
467
  repoToClone = existingForkName;
321
468
  forkedRepo = existingForkName;
322
469
  upstreamRemote = `${owner}/${repo}`;
@@ -555,6 +702,39 @@ Thank you!`;
555
702
 
556
703
  if (forkCheckResult.code === 0) {
557
704
  await log(`${formatAligned('✅', 'Fork verified:', `${actualForkName} is accessible`)}`);
705
+
706
+ // Validate fork parent before using it (prevents issue #967)
707
+ await log(`${formatAligned('🔍', 'Validating fork parent...', '')}`);
708
+ const forkValidation = await validateForkParent(actualForkName, `${owner}/${repo}`);
709
+
710
+ if (!forkValidation.isValid) {
711
+ // Fork parent mismatch detected
712
+ await log('');
713
+ await log(`${formatAligned('⚠️', 'FORK PARENT MISMATCH WARNING', '')}`, { level: 'warning' });
714
+ await log('');
715
+ await log(' 🔍 Issue detected:');
716
+ if (!forkValidation.isFork) {
717
+ await log(` The repository ${actualForkName} is NOT a GitHub fork.`);
718
+ } else {
719
+ await log(` The fork ${actualForkName} was created from ${forkValidation.parent},`);
720
+ await log(` not directly from the target repository ${owner}/${repo}.`);
721
+ }
722
+ await log('');
723
+ await log(' 📦 Fork relationship:');
724
+ await log(` • Fork: ${actualForkName}`);
725
+ await log(` • Fork parent: ${forkValidation.parent || 'N/A'}`);
726
+ await log(` • Fork source (root): ${forkValidation.source || 'N/A'}`);
727
+ await log(` • Expected parent: ${owner}/${repo}`);
728
+ await log('');
729
+ await log(' ⚠️ This may cause pull requests to include unexpected commits.');
730
+ await log(' Consider using --fork to create your own fork instead.');
731
+ await log('');
732
+ // Note: We don't exit here since this is someone else's fork and we're just using it
733
+ // The user should be aware but can proceed (they didn't create this fork)
734
+ } else {
735
+ await log(`${formatAligned('✅', 'Fork parent validated:', `${forkValidation.parent}`)}`);
736
+ }
737
+
558
738
  repoToClone = actualForkName;
559
739
  forkedRepo = actualForkName;
560
740
  upstreamRemote = `${owner}/${repo}`;