@link-assistant/hive-mind 2.0.16 → 2.0.18
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
|
+
## 2.0.18
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 08c2f07: Clarify fork auto-recovery safety failures so terminal exits and failure comments explain the mismatched repository, expected upstream, safety-check result, and recovery options.
|
|
8
|
+
|
|
9
|
+
## 2.0.17
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 1cec7b4: Allow bare indented LINO option/value links in .lenv configuration, matching parenthesized links.
|
|
14
|
+
|
|
3
15
|
## 2.0.16
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/package.json
CHANGED
package/src/lenv-reader.lib.mjs
CHANGED
|
@@ -39,10 +39,6 @@ const LinoParser = linoModule.Parser || linoModule.default?.Parser;
|
|
|
39
39
|
|
|
40
40
|
const fs = await import('fs');
|
|
41
41
|
|
|
42
|
-
function isCliOptionToken(value) {
|
|
43
|
-
return /^--[a-zA-Z0-9][a-zA-Z0-9=_.-]*$/.test(value) || /^-[a-zA-Z]$/.test(value);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
42
|
function collectStringValues(value, result = []) {
|
|
47
43
|
if (value && typeof value === 'object' && Array.isArray(value.values)) {
|
|
48
44
|
if (value.id !== null && value.id !== undefined) {
|
|
@@ -57,25 +53,6 @@ function collectStringValues(value, result = []) {
|
|
|
57
53
|
return result;
|
|
58
54
|
}
|
|
59
55
|
|
|
60
|
-
function validateNoBareSameLineOptions(content) {
|
|
61
|
-
let currentVar = 'configuration';
|
|
62
|
-
|
|
63
|
-
for (const line of content.split(/\r?\n/)) {
|
|
64
|
-
const trimmed = line.trim();
|
|
65
|
-
if (!trimmed || trimmed === '(' || trimmed === ')') continue;
|
|
66
|
-
|
|
67
|
-
const topLevelMatch = !/^\s/.test(line) ? trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*):(?:\s*(.*))?$/) : null;
|
|
68
|
-
const valueText = topLevelMatch ? (topLevelMatch[2] || '').trim() : trimmed;
|
|
69
|
-
if (topLevelMatch) currentVar = topLevelMatch[1];
|
|
70
|
-
if (!valueText || valueText === '(' || valueText === ')' || valueText.startsWith('(')) continue;
|
|
71
|
-
|
|
72
|
-
const parts = valueText.split(/\s+/).filter(Boolean);
|
|
73
|
-
if (parts.length > 1 && isCliOptionToken(parts[0])) {
|
|
74
|
-
throw new Error(`Invalid LINO format in "${currentVar}": Multiple values on the same line are not supported.\n` + `Found: "${parts.join(' ')}"\n` + `Each value must be on its own line with proper indentation, or grouped explicitly as a parenthesized link.`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
56
|
/**
|
|
80
57
|
* LenvReader - Reads and parses .lenv files using LINO notation
|
|
81
58
|
*/
|
|
@@ -97,8 +74,6 @@ export class LenvReader {
|
|
|
97
74
|
const result = {};
|
|
98
75
|
|
|
99
76
|
try {
|
|
100
|
-
validateNoBareSameLineOptions(content);
|
|
101
|
-
|
|
102
77
|
// Parse the entire content as LINO
|
|
103
78
|
const parsed = this.parser.parse(content);
|
|
104
79
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getTrackedToolCommentIds, postTrackedComment, SOLUTION_DRAFT_FAILED_MARKER } from './tool-comments.lib.mjs';
|
|
2
|
+
import { extractForkReplacementBlockedDetails, isForkReplacementBlockedReason } from './solve.repository-recovery-message.lib.mjs';
|
|
2
3
|
|
|
3
4
|
export const FORK_DIVERGENCE_RESOLUTION_OPTION = '--allow-fork-divergence-resolution-using-force-push-with-lease';
|
|
4
5
|
|
|
@@ -29,6 +30,23 @@ export function buildPrePullRequestFailureActionSection(reason = '') {
|
|
|
29
30
|
const normalizedReason = String(reason || '').toLowerCase();
|
|
30
31
|
const isForkOrRecoveryFailure = normalizedReason.includes('fork') || normalizedReason.includes('auto-recovery') || normalizedReason.includes('repository setup');
|
|
31
32
|
|
|
33
|
+
if (isForkReplacementBlockedReason(reason)) {
|
|
34
|
+
const details = extractForkReplacementBlockedDetails(reason);
|
|
35
|
+
const repository = details.existingRepository || 'the existing repository';
|
|
36
|
+
const upstream = details.expectedUpstream || 'the expected upstream repository';
|
|
37
|
+
|
|
38
|
+
return `### What happened
|
|
39
|
+
- Hive Mind found \`${repository}\` where it needed a GitHub fork of \`${upstream}\`.
|
|
40
|
+
- It could not prove whether the existing repository has unique commits, so it did not delete it automatically.
|
|
41
|
+
|
|
42
|
+
### What you can do
|
|
43
|
+
- If you control \`${repository}\`, back up any needed work, then delete, rename, archive, or repair it in GitHub and rerun the solver.
|
|
44
|
+
- If you do not control \`${repository}\`, ask the repository owner or a Hive Mind administrator to clean it up and rerun the solver.
|
|
45
|
+
- Use \`--allow-force-non-fork-repository-deletion\` only after confirming that deleting \`${repository}\` is acceptable and any unique commits can be lost.
|
|
46
|
+
|
|
47
|
+
Administrator-only CLI details, if any, are printed in the solver terminal log rather than in this GitHub comment.`;
|
|
48
|
+
}
|
|
49
|
+
|
|
32
50
|
if (isForkDivergenceFailure(reason)) {
|
|
33
51
|
return `### What you can do
|
|
34
52
|
- If the fork's default branch can be overwritten safely, rerun with \`${FORK_DIVERGENCE_RESOLUTION_OPTION}\` to allow a guarded force-with-lease sync.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export const FORK_REPLACEMENT_BLOCKED_REASON_PREFIX = 'Repository setup halted - existing fork replacement could lose commits.';
|
|
2
|
+
|
|
3
|
+
const fallback = (value, replacement) => {
|
|
4
|
+
const text = value === null || value === undefined ? '' : String(value).trim();
|
|
5
|
+
return text || replacement;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function summarizeGitHubCompareFailure(output = '') {
|
|
9
|
+
const text = fallback(output, 'unknown compare error');
|
|
10
|
+
const status = text.match(/HTTP\s+(\d+)/i)?.[1] || text.match(/"status"\s*:\s*"?(\d+)"?/i)?.[1] || null;
|
|
11
|
+
const message = text.match(/"message"\s*:\s*"([^"]+)"/i)?.[1] || text.match(/gh:\s*([^\n(]+)/i)?.[1]?.trim() || null;
|
|
12
|
+
|
|
13
|
+
if (status && message) return `${status} ${message}`;
|
|
14
|
+
return message || status || text.split('\n')[0].slice(0, 160);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function buildForkReplacementSafetyCheckDescription({ aheadBy = null, compareFailureOutput = '' } = {}) {
|
|
18
|
+
if (Number.isFinite(aheadBy) && aheadBy > 0) {
|
|
19
|
+
return `GitHub compare reported ${aheadBy} commit(s) ahead of upstream, so deleting the repository could lose commits.`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (compareFailureOutput) {
|
|
23
|
+
return `GitHub compare returned ${summarizeGitHubCompareFailure(compareFailureOutput)}, so Hive Mind could not prove the repository has no unique commits.`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return 'Hive Mind could not prove the repository has no unique commits.';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function buildForkReplacementBlockedReason({ existingRepository, expectedUpstream, relationshipDescription, safetyCheckDescription } = {}) {
|
|
30
|
+
const repository = fallback(existingRepository, 'the existing repository');
|
|
31
|
+
const upstream = fallback(expectedUpstream, 'the expected upstream repository');
|
|
32
|
+
const relationship = fallback(relationshipDescription, `${repository} is not a confirmed GitHub fork of ${upstream}.`);
|
|
33
|
+
const safetyCheck = fallback(safetyCheckDescription, buildForkReplacementSafetyCheckDescription());
|
|
34
|
+
|
|
35
|
+
return `${FORK_REPLACEMENT_BLOCKED_REASON_PREFIX}
|
|
36
|
+
|
|
37
|
+
What happened:
|
|
38
|
+
- Expected fork or replacement repository: ${repository}
|
|
39
|
+
- Expected upstream: ${upstream}
|
|
40
|
+
- Actual state: ${relationship}
|
|
41
|
+
- Safety check: ${safetyCheck}
|
|
42
|
+
|
|
43
|
+
Why it stopped:
|
|
44
|
+
Hive Mind did not delete ${repository} because the repository may contain commits that would be lost.
|
|
45
|
+
|
|
46
|
+
Options:
|
|
47
|
+
1. Back up any needed work in ${repository}, then delete, rename, archive, or repair that repository in GitHub and rerun the solver.
|
|
48
|
+
2. If you do not control ${repository}, ask the repository owner or a Hive Mind administrator to clean it up and rerun the solver.
|
|
49
|
+
3. Rerun with --allow-force-non-fork-repository-deletion only after confirming that deleting ${repository} is acceptable.`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function isForkReplacementBlockedReason(reason = '') {
|
|
53
|
+
const text = String(reason || '');
|
|
54
|
+
const normalized = text.toLowerCase();
|
|
55
|
+
return normalized.includes(FORK_REPLACEMENT_BLOCKED_REASON_PREFIX.toLowerCase()) || normalized.includes('auto-recovery skipped - repository may contain commits that would be lost');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const extractLineValue = (reason, label) => {
|
|
59
|
+
const pattern = new RegExp(`^- ${label}:\\s*(.+)$`, 'im');
|
|
60
|
+
return (
|
|
61
|
+
String(reason || '')
|
|
62
|
+
.match(pattern)?.[1]
|
|
63
|
+
?.trim() || null
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export function extractForkReplacementBlockedDetails(reason = '') {
|
|
68
|
+
return {
|
|
69
|
+
existingRepository: extractLineValue(reason, 'Expected fork or replacement repository'),
|
|
70
|
+
expectedUpstream: extractLineValue(reason, 'Expected upstream'),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -30,6 +30,7 @@ const { log, formatAligned } = lib;
|
|
|
30
30
|
// Import exit handler
|
|
31
31
|
import { safeExit } from './exit-handler.lib.mjs';
|
|
32
32
|
import { parseForkFullNameFromGhOutput } from './github-repository-names.lib.mjs';
|
|
33
|
+
import { buildForkReplacementBlockedReason, buildForkReplacementSafetyCheckDescription } from './solve.repository-recovery-message.lib.mjs';
|
|
33
34
|
|
|
34
35
|
// Import GitHub utilities for permission checks
|
|
35
36
|
const githubLib = await import('./github.lib.mjs');
|
|
@@ -521,22 +522,30 @@ export const setupRepository = async (argv, owner, repo, forkOwner = null, issue
|
|
|
521
522
|
await log('');
|
|
522
523
|
await log(`${formatAligned('⚠️', 'FORK PARENT MISMATCH DETECTED', '')}`, { level: 'warning' });
|
|
523
524
|
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)`;
|
|
525
|
+
const relationshipDescription = !forkValidation.isFork ? `${existingForkName} is not a GitHub fork of ${owner}/${repo}.` : `${existingForkName} is a GitHub fork of ${forkValidation.parent || 'unknown parent'}, not ${owner}/${repo}.`;
|
|
524
526
|
await log(`${formatAligned('', '', detail)}`);
|
|
525
527
|
await log(`${formatAligned('', '', `Fork parent: ${forkValidation.parent || 'N/A (not a fork)'}, source: ${forkValidation.source || 'N/A'}, expected: ${owner}/${repo}`)}`);
|
|
526
528
|
// Safety check: compare commits before deleting to avoid data loss
|
|
527
529
|
await log(`${formatAligned('🔍', 'Safety check:', 'Comparing commits against upstream...')}`);
|
|
528
530
|
let safeToDelete = false;
|
|
531
|
+
let safetyCheckDescription = buildForkReplacementSafetyCheckDescription();
|
|
529
532
|
try {
|
|
530
533
|
const cmp = await $`gh api repos/${owner}/${repo}/compare/${owner}:HEAD...${existingForkName.split('/')[0]}:HEAD --paginate --jq '.ahead_by' 2>&1`;
|
|
531
|
-
|
|
534
|
+
const aheadBy = parseInt(cmp.stdout.toString().trim(), 10);
|
|
535
|
+
if (cmp.code === 0 && aheadBy === 0) {
|
|
532
536
|
await log(`${formatAligned('✅', 'Safe to delete:', 'No additional commits in non-fork repository')}`);
|
|
533
537
|
safeToDelete = true;
|
|
534
538
|
} else if (cmp.code === 0) {
|
|
535
|
-
|
|
539
|
+
safetyCheckDescription = buildForkReplacementSafetyCheckDescription({ aheadBy });
|
|
540
|
+
const aheadDescription = Number.isFinite(aheadBy) ? `${aheadBy} commit(s)` : 'an unknown number of commit(s)';
|
|
541
|
+
await log(`${formatAligned('⚠️', 'UNSAFE:', `Repository has ${aheadDescription} ahead of upstream that would be lost`)}`, { level: 'warning' });
|
|
536
542
|
} else {
|
|
537
|
-
|
|
543
|
+
const compareOutput = (cmp.stderr?.toString() || '') + (cmp.stdout?.toString() || '');
|
|
544
|
+
safetyCheckDescription = buildForkReplacementSafetyCheckDescription({ compareFailureOutput: compareOutput });
|
|
545
|
+
await log(`${formatAligned('⚠️', 'Compare failed:', compareOutput.split('\n')[0])}`, { level: 'warning' });
|
|
538
546
|
}
|
|
539
547
|
} catch (e) {
|
|
548
|
+
safetyCheckDescription = buildForkReplacementSafetyCheckDescription({ compareFailureOutput: e.message });
|
|
540
549
|
await log(`${formatAligned('⚠️', 'Compare error:', e.message)}`, { level: 'warning' });
|
|
541
550
|
}
|
|
542
551
|
if (!safeToDelete) {
|
|
@@ -547,7 +556,15 @@ export const setupRepository = async (argv, owner, repo, forkOwner = null, issue
|
|
|
547
556
|
await log(` 💡 Manual fix required: back up work, then: gh repo delete ${existingForkName} --yes`);
|
|
548
557
|
await log(` Then run this command again to create a proper fork of ${owner}/${repo}`);
|
|
549
558
|
await log(` 🔧 Or force deletion (DANGEROUS): solve ${argv.url || argv['issue-url'] || argv._[0] || '<issue-url>'} --allow-force-non-fork-repository-deletion`);
|
|
550
|
-
await safeExit(
|
|
559
|
+
await safeExit(
|
|
560
|
+
1,
|
|
561
|
+
buildForkReplacementBlockedReason({
|
|
562
|
+
existingRepository: existingForkName,
|
|
563
|
+
expectedUpstream: `${owner}/${repo}`,
|
|
564
|
+
relationshipDescription,
|
|
565
|
+
safetyCheckDescription,
|
|
566
|
+
})
|
|
567
|
+
);
|
|
551
568
|
}
|
|
552
569
|
}
|
|
553
570
|
await log(`${formatAligned('🔄', 'Auto-recovery:', 'Deleting non-fork repository and creating fresh fork...')}`);
|