@link-assistant/hive-mind 0.50.10 → 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 +39 -0
- package/README.md +4 -0
- package/package.json +1 -1
- package/src/solve.repository.lib.mjs +181 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
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
|
+
|
|
34
|
+
## 0.50.11
|
|
35
|
+
|
|
36
|
+
### Patch Changes
|
|
37
|
+
|
|
38
|
+
- 6f51d29: fix: add screen terminal multiplexer to Docker image
|
|
39
|
+
|
|
40
|
+
The screen package is now installed by default in the Docker image, resolving issue #986 where users encountered "command not found" errors when attempting to use screen. Includes comprehensive case study documenting the issue analysis, root cause, and solution evaluation.
|
|
41
|
+
|
|
3
42
|
## 0.50.10
|
|
4
43
|
|
|
5
44
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -217,10 +217,12 @@ See [docs/HELM.md](./docs/HELM.md) for detailed Helm configuration options.
|
|
|
217
217
|
--attach-logs
|
|
218
218
|
--verbose
|
|
219
219
|
--no-tool-check
|
|
220
|
+
--auto-continue-on-limit-reset
|
|
220
221
|
TELEGRAM_SOLVE_OVERRIDES:
|
|
221
222
|
--attach-logs
|
|
222
223
|
--verbose
|
|
223
224
|
--no-tool-check
|
|
225
|
+
--auto-continue-on-limit-reset
|
|
224
226
|
TELEGRAM_BOT_VERBOSE: true
|
|
225
227
|
"
|
|
226
228
|
|
|
@@ -242,10 +244,12 @@ See [docs/HELM.md](./docs/HELM.md) for detailed Helm configuration options.
|
|
|
242
244
|
--attach-logs
|
|
243
245
|
--verbose
|
|
244
246
|
--no-tool-check
|
|
247
|
+
--auto-continue-on-limit-reset
|
|
245
248
|
)" --solve-overrides "(
|
|
246
249
|
--attach-logs
|
|
247
250
|
--verbose
|
|
248
251
|
--no-tool-check
|
|
252
|
+
--auto-continue-on-limit-reset
|
|
249
253
|
)" --verbose
|
|
250
254
|
|
|
251
255
|
# Press CTRL + A + D for detach from screen
|
package/package.json
CHANGED
|
@@ -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}`;
|