@link-assistant/hive-mind 1.46.0 β 1.46.2
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 +20 -0
- package/package.json +1 -1
- package/src/agent.lib.mjs +13 -0
- package/src/option-suggestions.lib.mjs +1 -0
- package/src/solve.config.lib.mjs +5 -0
- package/src/solve.repository.lib.mjs +74 -67
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.46.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 37daeb7: Auto-recover from non-fork repositories during fork validation (Issue #1518)
|
|
8
|
+
- When a repository exists but is NOT a proper GitHub fork (or has wrong parent), safely auto-recover by comparing commits against upstream first β only delete and re-fork if no additional commits would be lost
|
|
9
|
+
- Add verbose logging of fork commands for debugging non-fork creation scenarios
|
|
10
|
+
- Add post-creation fork validation to detect non-fork repos immediately after `gh repo fork`
|
|
11
|
+
- Report non-fork creation to Sentry for monitoring
|
|
12
|
+
- Add `--allow-force-non-fork-repository-deletion` flag to force deletion even when additional commits would be lost
|
|
13
|
+
- Add case study documenting the root cause analysis of konard/MixaByk1996-elements-app
|
|
14
|
+
- Document all previously undocumented solve options in CONFIGURATION.md (12 options including --allow-force-non-fork-repository-deletion)
|
|
15
|
+
- Add CI/CD test to verify documentation stays in sync with code options (prevents drift)
|
|
16
|
+
|
|
17
|
+
## 1.46.1
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- 84aacf7: fix: pass LINK_ASSISTANT_AGENT_VERBOSE env var to agent process for HTTP logging (#1521)
|
|
22
|
+
|
|
3
23
|
## 1.46.0
|
|
4
24
|
|
|
5
25
|
### Minor Changes
|
package/package.json
CHANGED
package/src/agent.lib.mjs
CHANGED
|
@@ -508,9 +508,22 @@ export const executeAgentCommand = async params => {
|
|
|
508
508
|
try {
|
|
509
509
|
// Pipe the prompt file to agent via stdin
|
|
510
510
|
// Use agentArgs which includes --model and optionally --verbose
|
|
511
|
+
|
|
512
|
+
// Issue #1521: Build environment for agent process
|
|
513
|
+
// Pass LINK_ASSISTANT_AGENT_VERBOSE env var when --verbose is enabled
|
|
514
|
+
// This ensures Flag.LINK_ASSISTANT_AGENT_VERBOSE is true at module load time inside the agent,
|
|
515
|
+
// which is required for HTTP request/response logging to work.
|
|
516
|
+
// The --verbose CLI flag alone is not sufficient because the agent's Flag module
|
|
517
|
+
// reads the env var at initialization, before yargs middleware calls Flag.setVerbose().
|
|
518
|
+
const agentEnv = { ...process.env };
|
|
519
|
+
if (argv.verbose) {
|
|
520
|
+
agentEnv.LINK_ASSISTANT_AGENT_VERBOSE = 'true';
|
|
521
|
+
}
|
|
522
|
+
|
|
511
523
|
execCommand = $({
|
|
512
524
|
cwd: tempDir,
|
|
513
525
|
mirror: false,
|
|
526
|
+
env: agentEnv,
|
|
514
527
|
})`cat ${promptFile} | ${agentPath} ${agentArgs}`;
|
|
515
528
|
|
|
516
529
|
await log(`${formatAligned('π', 'Command details:', '')}`);
|
|
@@ -199,6 +199,7 @@ const KNOWN_OPTION_NAMES = [
|
|
|
199
199
|
'only-prepare-command',
|
|
200
200
|
'auto-merge-default-branch-to-pull-request-branch',
|
|
201
201
|
'allow-fork-divergence-resolution-using-force-push-with-lease',
|
|
202
|
+
'allow-force-non-fork-repository-deletion',
|
|
202
203
|
'allow-to-push-to-contributors-pull-requests-as-maintainer',
|
|
203
204
|
'prefix-fork-name-with-owner-name',
|
|
204
205
|
'auto-restart-max-iterations',
|
package/src/solve.config.lib.mjs
CHANGED
|
@@ -273,6 +273,11 @@ export const SOLVE_OPTION_DEFINITIONS = {
|
|
|
273
273
|
description: 'Allow automatic force-push (--force-with-lease) when fork diverges from upstream (DANGEROUS: can overwrite fork history)',
|
|
274
274
|
default: false,
|
|
275
275
|
},
|
|
276
|
+
'allow-force-non-fork-repository-deletion': {
|
|
277
|
+
type: 'boolean',
|
|
278
|
+
description: 'Allow deletion of non-fork repositories even when they contain additional commits that would be lost (DANGEROUS: data loss possible)',
|
|
279
|
+
default: false,
|
|
280
|
+
},
|
|
276
281
|
'allow-to-push-to-contributors-pull-requests-as-maintainer': {
|
|
277
282
|
type: 'boolean',
|
|
278
283
|
description: 'When continuing a fork PR as a maintainer, attempt to push directly to the contributor\'s fork if "Allow edits by maintainers" is enabled. Requires --auto-fork to be enabled.',
|
|
@@ -490,78 +490,77 @@ export const setupRepository = async (argv, owner, repo, forkOwner = null, issue
|
|
|
490
490
|
|
|
491
491
|
const forkValidation = await validateForkParent(existingForkName, `${owner}/${repo}`);
|
|
492
492
|
|
|
493
|
-
if (
|
|
493
|
+
if (forkValidation.isValid) {
|
|
494
|
+
// Fork is valid β use it
|
|
495
|
+
await log(`${formatAligned('β
', 'Fork parent validated:', `${forkValidation.parent}`)}`);
|
|
496
|
+
repoToClone = existingForkName;
|
|
497
|
+
forkedRepo = existingForkName;
|
|
498
|
+
upstreamRemote = `${owner}/${repo}`;
|
|
499
|
+
} else if (forkValidation.isNetworkError) {
|
|
494
500
|
// Issue #1311: Handle network errors separately from fork mismatch errors
|
|
495
|
-
if (forkValidation.isNetworkError) {
|
|
496
|
-
await log('');
|
|
497
|
-
await log(`${formatAligned('β', 'NETWORK ERROR DURING FORK VALIDATION', '')}`, { level: 'error' });
|
|
498
|
-
await log('');
|
|
499
|
-
await log(' π What happened:');
|
|
500
|
-
await log(` Failed to connect to GitHub API while validating fork.`);
|
|
501
|
-
await log(` Error: ${forkValidation.error}`);
|
|
502
|
-
await log('');
|
|
503
|
-
await log(' π‘ This is likely a temporary network issue. You can:');
|
|
504
|
-
await log(' 1. Wait a moment and try again');
|
|
505
|
-
await log(' 2. Check your internet connection');
|
|
506
|
-
await log(' 3. Check GitHub status: https://www.githubstatus.com/');
|
|
507
|
-
await log('');
|
|
508
|
-
await log(' Or use --no-fork to skip fork validation if you have write access.');
|
|
509
|
-
await log('');
|
|
510
|
-
await safeExit(1, 'Network error during fork validation - please retry');
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
// Fork parent mismatch detected - this prevents issue #967
|
|
514
501
|
await log('');
|
|
515
|
-
await log(`${formatAligned('β', '
|
|
502
|
+
await log(`${formatAligned('β', 'NETWORK ERROR DURING FORK VALIDATION', '')}`, { level: 'error' });
|
|
516
503
|
await log('');
|
|
517
504
|
await log(' π What happened:');
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
await log(' It may have been created by cloning and pushing instead of forking.');
|
|
521
|
-
} else {
|
|
522
|
-
await log(` Your fork ${existingForkName} was created from an intermediate fork,`);
|
|
523
|
-
await log(` not directly from the target repository ${owner}/${repo}.`);
|
|
524
|
-
}
|
|
525
|
-
await log('');
|
|
526
|
-
await log(' π¦ Fork relationship:');
|
|
527
|
-
await log(` β’ Your fork: ${existingForkName}`);
|
|
528
|
-
await log(` β’ Fork parent: ${forkValidation.parent || 'N/A (not a fork)'}`);
|
|
529
|
-
await log(` β’ Fork source (root): ${forkValidation.source || 'N/A'}`);
|
|
530
|
-
await log(` β’ Expected parent: ${owner}/${repo}`);
|
|
531
|
-
await log('');
|
|
532
|
-
await log(' β οΈ Why this is a problem:');
|
|
533
|
-
await log(' When a fork is created from an intermediate fork (a "fork of a fork"),');
|
|
534
|
-
await log(' any commits that exist in the intermediate fork but not in the target');
|
|
535
|
-
await log(' repository will be included in your pull requests. This can result in');
|
|
536
|
-
await log(' pull requests with hundreds or thousands of unexpected commits.');
|
|
505
|
+
await log(` Failed to connect to GitHub API while validating fork.`);
|
|
506
|
+
await log(` Error: ${forkValidation.error}`);
|
|
537
507
|
await log('');
|
|
538
|
-
await log('
|
|
539
|
-
await log('
|
|
540
|
-
await log('
|
|
541
|
-
await log('
|
|
508
|
+
await log(' π‘ This is likely a temporary network issue. You can:');
|
|
509
|
+
await log(' 1. Wait a moment and try again');
|
|
510
|
+
await log(' 2. Check your internet connection');
|
|
511
|
+
await log(' 3. Check GitHub status: https://www.githubstatus.com/');
|
|
542
512
|
await log('');
|
|
543
|
-
await log('
|
|
513
|
+
await log(' Or use --no-fork to skip fork validation if you have write access.');
|
|
544
514
|
await log('');
|
|
545
|
-
await
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
await log('');
|
|
549
|
-
await log(' Option 2: Use --prefix-fork-name-with-owner-name to create a new fork');
|
|
550
|
-
await log(` This creates a fork named ${currentUser}/${owner}-${repo} instead`);
|
|
551
|
-
await log(` ./solve.mjs "${issueUrl || `https://github.com/${owner}/${repo}/issues/<number>`}" --prefix-fork-name-with-owner-name --fork`);
|
|
552
|
-
await log('');
|
|
553
|
-
await log(' Option 3: Work directly on the repository (if you have write access)');
|
|
554
|
-
await log(` ./solve.mjs "${issueUrl || `https://github.com/${owner}/${repo}/issues/<number>`}" --no-fork`);
|
|
515
|
+
await safeExit(1, 'Network error during fork validation - please retry');
|
|
516
|
+
} else {
|
|
517
|
+
// Issue #1518: Auto-recovery β delete non-fork/mismatched repo and re-fork, but only if no commits would be lost
|
|
555
518
|
await log('');
|
|
556
|
-
|
|
557
|
-
|
|
519
|
+
await log(`${formatAligned('β οΈ', 'FORK PARENT MISMATCH DETECTED', '')}`, { level: 'warning' });
|
|
520
|
+
const detail = !forkValidation.isFork ? `Repository ${existingForkName} is NOT a GitHub fork (see issue #1518)` : `Fork ${existingForkName} was created from ${forkValidation.parent} instead of ${owner}/${repo} (see issue #967)`;
|
|
521
|
+
await log(`${formatAligned('', '', detail)}`);
|
|
522
|
+
await log(`${formatAligned('', '', `Fork parent: ${forkValidation.parent || 'N/A (not a fork)'}, source: ${forkValidation.source || 'N/A'}, expected: ${owner}/${repo}`)}`);
|
|
523
|
+
// Safety check: compare commits before deleting to avoid data loss
|
|
524
|
+
await log(`${formatAligned('π', 'Safety check:', 'Comparing commits against upstream...')}`);
|
|
525
|
+
let safeToDelete = false;
|
|
526
|
+
try {
|
|
527
|
+
const cmp = await $`gh api repos/${owner}/${repo}/compare/${owner}:HEAD...${existingForkName.split('/')[0]}:HEAD --jq '.ahead_by' 2>&1`;
|
|
528
|
+
if (cmp.code === 0 && parseInt(cmp.stdout.toString().trim(), 10) === 0) {
|
|
529
|
+
await log(`${formatAligned('β
', 'Safe to delete:', 'No additional commits in non-fork repository')}`);
|
|
530
|
+
safeToDelete = true;
|
|
531
|
+
} else if (cmp.code === 0) {
|
|
532
|
+
await log(`${formatAligned('β οΈ', 'UNSAFE:', `Repository has ${cmp.stdout.toString().trim()} commit(s) ahead of upstream that would be lost`)}`, { level: 'warning' });
|
|
533
|
+
} else {
|
|
534
|
+
await log(`${formatAligned('β οΈ', 'Compare failed:', ((cmp.stderr?.toString() || '') + (cmp.stdout?.toString() || '')).split('\n')[0])}`, { level: 'warning' });
|
|
535
|
+
}
|
|
536
|
+
} catch (e) {
|
|
537
|
+
await log(`${formatAligned('β οΈ', 'Compare error:', e.message)}`, { level: 'warning' });
|
|
538
|
+
}
|
|
539
|
+
if (!safeToDelete) {
|
|
540
|
+
if (argv.allowForceNonForkRepositoryDeletion) {
|
|
541
|
+
await log(`${formatAligned('β οΈ', 'Force deletion ENABLED:', '--allow-force-non-fork-repository-deletion β proceeding despite potential data loss')}`, { level: 'warning' });
|
|
542
|
+
safeToDelete = true;
|
|
543
|
+
} else {
|
|
544
|
+
await log(` π‘ Manual fix required: back up work, then: gh repo delete ${existingForkName} --yes`);
|
|
545
|
+
await log(` Then run this command again to create a proper fork of ${owner}/${repo}`);
|
|
546
|
+
await log(` π§ Or force deletion (DANGEROUS): solve ${argv.url || argv['issue-url'] || argv._[0] || '<issue-url>'} --allow-force-non-fork-repository-deletion`);
|
|
547
|
+
await safeExit(1, 'Auto-recovery skipped - repository may contain commits that would be lost');
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
await log(`${formatAligned('π', 'Auto-recovery:', 'Deleting non-fork repository and creating fresh fork...')}`);
|
|
551
|
+
const deleteResult = await $`gh repo delete ${existingForkName} --yes 2>&1`;
|
|
552
|
+
if (deleteResult.code !== 0) {
|
|
553
|
+
const delOut = (deleteResult.stderr?.toString() || '') + (deleteResult.stdout?.toString() || '');
|
|
554
|
+
await log(`${formatAligned('β', 'Delete failed:', delOut.split('\n')[0])}`, { level: 'error' });
|
|
555
|
+
await log(` π‘ Manual fix: gh repo delete ${existingForkName} --yes, then re-run`);
|
|
556
|
+
await safeExit(1, 'Auto-recovery failed - could not delete problematic repository');
|
|
557
|
+
}
|
|
558
|
+
await log(`${formatAligned('β
', 'Deleted:', existingForkName)}`);
|
|
559
|
+
existingForkName = null; // Fall through to fork creation below
|
|
558
560
|
}
|
|
561
|
+
}
|
|
559
562
|
|
|
560
|
-
|
|
561
|
-
repoToClone = existingForkName;
|
|
562
|
-
forkedRepo = existingForkName;
|
|
563
|
-
upstreamRemote = `${owner}/${repo}`;
|
|
564
|
-
} else {
|
|
563
|
+
if (!existingForkName) {
|
|
565
564
|
// Need to create fork with retry logic for concurrent scenarios
|
|
566
565
|
await log(`${formatAligned('π', 'Creating fork...', '')}`);
|
|
567
566
|
|
|
@@ -575,19 +574,18 @@ export const setupRepository = async (argv, owner, repo, forkOwner = null, issue
|
|
|
575
574
|
let actualForkName = `${currentUser}/${defaultForkName}`;
|
|
576
575
|
|
|
577
576
|
for (let attempt = 1; attempt <= maxForkRetries; attempt++) {
|
|
578
|
-
// Try to create fork with optional custom name
|
|
579
577
|
let forkResult;
|
|
578
|
+
// Issue #1518: Log the exact fork command for debugging non-fork creation scenarios
|
|
579
|
+
if (argv.verbose) await log(`${formatAligned('π§', 'Fork command:', argv.prefixForkNameWithOwnerName ? `gh repo fork ${owner}/${repo} --fork-name ${owner}-${repo} --clone=false` : `gh repo fork ${owner}/${repo} --clone=false`)}`);
|
|
580
580
|
if (argv.prefixForkNameWithOwnerName) {
|
|
581
|
-
// Use --fork-name flag to create fork with owner prefix
|
|
582
581
|
forkResult = await $`gh repo fork ${owner}/${repo} --fork-name ${owner}-${repo} --clone=false 2>&1`;
|
|
583
582
|
} else {
|
|
584
|
-
// Standard fork creation (no custom name)
|
|
585
583
|
forkResult = await $`gh repo fork ${owner}/${repo} --clone=false 2>&1`;
|
|
586
584
|
}
|
|
587
585
|
|
|
588
586
|
// Always capture output to parse actual fork name
|
|
589
587
|
const forkOutput = (forkResult.stderr ? forkResult.stderr.toString() : '') + (forkResult.stdout ? forkResult.stdout.toString() : '');
|
|
590
|
-
|
|
588
|
+
if (argv.verbose) await log(`${formatAligned('π§', 'Fork output:', forkOutput.split('\n')[0] || '(empty)')}`); // Issue #1518
|
|
591
589
|
// Parse actual fork name from output (e.g., "konard/netkeep80-jsonRVM already exists")
|
|
592
590
|
// GitHub may create forks with modified names to avoid conflicts
|
|
593
591
|
// Use regex that won't match domain names like "github.com/user" -> "com/user"
|
|
@@ -792,11 +790,20 @@ Thank you!`;
|
|
|
792
790
|
await safeExit(1, 'Repository setup failed');
|
|
793
791
|
}
|
|
794
792
|
|
|
795
|
-
// Wait a moment for fork to be fully ready
|
|
796
793
|
if (forkCreated) {
|
|
794
|
+
// Wait a moment for fork to be fully ready
|
|
797
795
|
await log(`${formatAligned('β³', 'Waiting:', 'For fork to be fully ready...')}`);
|
|
798
796
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
799
797
|
}
|
|
798
|
+
// Issue #1518: Validate fork parent after creation/discovery to detect non-fork repos early (covers concurrent worker scenarios too)
|
|
799
|
+
await log(`${formatAligned('π', 'Validating fork parent...', '')}`);
|
|
800
|
+
const pcv = await validateForkParent(actualForkName, `${owner}/${repo}`);
|
|
801
|
+
if (pcv.isValid) {
|
|
802
|
+
await log(`${formatAligned('β
', 'Fork parent validated:', `${pcv.parent}`)}`);
|
|
803
|
+
} else if (!pcv.isNetworkError) {
|
|
804
|
+
await log(`${formatAligned('β οΈ', 'WARNING:', `Fork failed validation (possible gh CLI bug, see issue #1518): ${pcv.error}`)}`, { level: 'warning' });
|
|
805
|
+
reportError(new Error(`Fork created as non-fork: ${pcv.error}`), { context: 'fork_creation_validation', forkRepo: actualForkName, expectedUpstream: `${owner}/${repo}`, isFork: pcv.isFork, parent: pcv.parent, source: pcv.source });
|
|
806
|
+
}
|
|
800
807
|
}
|
|
801
808
|
|
|
802
809
|
repoToClone = actualForkName;
|