@link-assistant/hive-mind 1.72.3 → 1.72.5
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 +12 -0
- package/package.json +1 -1
- package/src/github-repository-names.lib.mjs +63 -0
- package/src/solve.auto-pr.lib.mjs +40 -40
- package/src/solve.branch-divergence.lib.mjs +240 -1
- package/src/solve.error-handlers.lib.mjs +4 -1
- package/src/solve.pre-pr-failure-notifier.lib.mjs +22 -0
- package/src/solve.repository.lib.mjs +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.72.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- c3a89a3: Recover auto-PR creation when a rejected push leaves the remote branch matching local HEAD, and improve push rejection diagnostics with exact branch and compare links.
|
|
8
|
+
|
|
9
|
+
## 1.72.4
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 82d440c: Fix fork creation verification for dotted repository names such as GitHub Pages forks.
|
|
14
|
+
|
|
3
15
|
## 1.72.3
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const OWNER_NAME_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
2
|
+
const REPOSITORY_NAME_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
3
|
+
const FULL_NAME_IN_TEXT_PATTERN = /(?:^|\s)([A-Za-z0-9_-]+\/[A-Za-z0-9._-]+)(?=$|\s|[),.;:])/;
|
|
4
|
+
|
|
5
|
+
function trimOutputToken(token) {
|
|
6
|
+
return token.replace(/^[<([{'"`]+/, '').replace(/[>\])}'"`.,;]+$/, '');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function stripGitSuffix(repositoryName) {
|
|
10
|
+
return repositoryName.endsWith('.git') ? repositoryName.slice(0, -4) : repositoryName;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function normalizeRepositoryFullName(owner, repositoryName) {
|
|
14
|
+
if (!owner || !repositoryName) return null;
|
|
15
|
+
if (!OWNER_NAME_PATTERN.test(owner)) return null;
|
|
16
|
+
if (!REPOSITORY_NAME_PATTERN.test(repositoryName)) return null;
|
|
17
|
+
return `${owner}/${repositoryName}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseGitHubRepositoryUrlToken(token) {
|
|
21
|
+
const cleaned = trimOutputToken(token);
|
|
22
|
+
let pathName = null;
|
|
23
|
+
|
|
24
|
+
if (cleaned.startsWith('git@github.com:')) {
|
|
25
|
+
pathName = cleaned.slice('git@github.com:'.length);
|
|
26
|
+
} else if (cleaned.startsWith('github.com/')) {
|
|
27
|
+
pathName = cleaned.slice('github.com/'.length);
|
|
28
|
+
} else {
|
|
29
|
+
try {
|
|
30
|
+
const parsed = new URL(cleaned);
|
|
31
|
+
if (parsed.hostname !== 'github.com') return null;
|
|
32
|
+
pathName = parsed.pathname.replace(/^\/+/, '');
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const [owner, repositoryName] = pathName.split('/');
|
|
39
|
+
return normalizeRepositoryFullName(owner, stripGitSuffix(repositoryName || ''));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Parse the repository full name returned by `gh repo fork`.
|
|
44
|
+
*
|
|
45
|
+
* GitHub repository names can contain dots, notably GitHub Pages names like
|
|
46
|
+
* `parking.github.io`. The previous inline regex only accepted letters,
|
|
47
|
+
* digits, underscores, and dashes, so it truncated dotted fork names and
|
|
48
|
+
* verified the wrong repository.
|
|
49
|
+
*
|
|
50
|
+
* @param {string} output
|
|
51
|
+
* @returns {string|null}
|
|
52
|
+
*/
|
|
53
|
+
export function parseForkFullNameFromGhOutput(output) {
|
|
54
|
+
const text = String(output || '');
|
|
55
|
+
|
|
56
|
+
for (const token of text.match(/\S+/g) || []) {
|
|
57
|
+
const parsed = parseGitHubRepositoryUrlToken(token);
|
|
58
|
+
if (parsed) return parsed;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const fullNameMatch = text.match(FULL_NAME_IN_TEXT_PATTERN);
|
|
62
|
+
return fullNameMatch ? fullNameMatch[1] : null;
|
|
63
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { closingIssueNumbersContain, parseClosingIssueNumbers } from './pr-issue-linking.lib.mjs';
|
|
7
|
-
import {
|
|
7
|
+
import { handleRejectedPushForAutoPr, synchronizeExistingIssueBranchBeforeAutoPrCreation } from './solve.branch-divergence.lib.mjs';
|
|
8
8
|
import { emitForkAwareDiagnostic } from './solve.auto-pr-fork-diagnostic.lib.mjs';
|
|
9
9
|
|
|
10
10
|
import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry, execGhWithRetry } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): gh API calls flow through $ wrapped by caller. Issue #1756: execGhWithRetry retries on transient 5xx (504) too.
|
|
@@ -409,6 +409,9 @@ Proceed.
|
|
|
409
409
|
}
|
|
410
410
|
}
|
|
411
411
|
|
|
412
|
+
let branchReadyForPrCreation = pushResult.code === 0;
|
|
413
|
+
let recoveredFromPushRejection = false;
|
|
414
|
+
|
|
412
415
|
if (pushResult.code !== 0) {
|
|
413
416
|
const errorOutput = pushResult.stderr ? pushResult.stderr.toString() : pushResult.stdout ? pushResult.stdout.toString() : 'Unknown error';
|
|
414
417
|
|
|
@@ -539,48 +542,41 @@ Proceed.
|
|
|
539
542
|
}
|
|
540
543
|
await log('');
|
|
541
544
|
throw new Error('Permission denied - need fork or collaborator access');
|
|
542
|
-
} else
|
|
543
|
-
const
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
545
|
+
} else {
|
|
546
|
+
const rejectedPush = await handleRejectedPushForAutoPr({
|
|
547
|
+
errorOutput,
|
|
548
|
+
$,
|
|
549
|
+
tempDir,
|
|
550
|
+
log,
|
|
551
|
+
formatAligned,
|
|
552
|
+
branchName,
|
|
553
|
+
isContinueMode,
|
|
554
|
+
prNumber,
|
|
555
|
+
owner,
|
|
556
|
+
repo,
|
|
557
|
+
defaultBranch,
|
|
558
|
+
forkedRepo,
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
if (rejectedPush.handled) {
|
|
562
|
+
branchReadyForPrCreation = rejectedPush.branchReadyForPrCreation;
|
|
563
|
+
recoveredFromPushRejection = rejectedPush.recoveredFromPushRejection;
|
|
564
|
+
} else {
|
|
565
|
+
// Other push errors
|
|
566
|
+
await log(`${formatAligned('❌', 'Failed to push:', 'See error below')}`, { level: 'error' });
|
|
567
|
+
await log(` Error: ${errorOutput}`, { level: 'error' });
|
|
568
|
+
throw new Error('Failed to push branch');
|
|
553
569
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
await log('');
|
|
560
|
-
await log(' 🔧 How to fix:');
|
|
561
|
-
await log(' 1. Clone the repository and checkout the branch:');
|
|
562
|
-
await log(` git clone https://github.com/${owner}/${repo}.git`);
|
|
563
|
-
await log(` cd ${repo}`);
|
|
564
|
-
await log(` git checkout ${branchName}`);
|
|
565
|
-
await log('');
|
|
566
|
-
await log(' 2. Pull and merge the remote changes:');
|
|
567
|
-
await log(` git pull origin ${branchName}`);
|
|
568
|
-
await log('');
|
|
569
|
-
await log(' 3. Resolve any conflicts manually, then:');
|
|
570
|
-
await log(` git push origin ${branchName}`);
|
|
571
|
-
await log('');
|
|
572
|
-
await log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
573
|
-
await log('');
|
|
574
|
-
throw new Error('Push rejected - branch has diverged, manual resolution required');
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (branchReadyForPrCreation) {
|
|
574
|
+
if (recoveredFromPushRejection) {
|
|
575
|
+
await log(`${formatAligned('✅', 'Branch available:', 'Remote branch matches local HEAD')}`);
|
|
575
576
|
} else {
|
|
576
|
-
|
|
577
|
-
await log(`${formatAligned('❌', 'Failed to push:', 'See error below')}`, { level: 'error' });
|
|
578
|
-
await log(` Error: ${errorOutput}`, { level: 'error' });
|
|
579
|
-
throw new Error('Failed to push branch');
|
|
577
|
+
await log(`${formatAligned('✅', 'Branch pushed:', 'Successfully to remote')}`);
|
|
580
578
|
}
|
|
581
|
-
|
|
582
|
-
await log(`${formatAligned('✅', 'Branch pushed:', 'Successfully to remote')}`);
|
|
583
|
-
if (argv.verbose) {
|
|
579
|
+
if (argv.verbose && pushResult.code === 0) {
|
|
584
580
|
await log(` Push output: ${pushResult.stdout.toString().trim()}`, { verbose: true });
|
|
585
581
|
}
|
|
586
582
|
|
|
@@ -1454,6 +1450,10 @@ ${prBody}`,
|
|
|
1454
1450
|
operation: 'handle_auto_pr',
|
|
1455
1451
|
});
|
|
1456
1452
|
|
|
1453
|
+
if (prError?.hiveMindUserFacingLogged) {
|
|
1454
|
+
throw prError;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
1457
|
// Issue #1462: Single consolidated error message for PR creation failure.
|
|
1458
1458
|
// Previously this was the third of three error blocks, causing confusing output.
|
|
1459
1459
|
// Now this is the ONLY error block shown for PR creation failures.
|
|
@@ -9,18 +9,117 @@ const outputOf = result => {
|
|
|
9
9
|
return stdout || stderr;
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
const shortSha = sha => (sha ? String(sha).slice(0, 12) : null);
|
|
13
|
+
|
|
14
|
+
const encodeRefForGitHubUrl = ref =>
|
|
15
|
+
encodeURI(String(ref || ''))
|
|
16
|
+
.replaceAll('#', '%23')
|
|
17
|
+
.replaceAll('?', '%3F');
|
|
18
|
+
|
|
19
|
+
export function classifyPushRejection(errorOutput = '') {
|
|
20
|
+
const normalized = String(errorOutput || '').toLowerCase();
|
|
21
|
+
|
|
22
|
+
if (normalized.includes('cannot lock ref') && normalized.includes('reference already exists')) {
|
|
23
|
+
return 'remote-ref-already-exists';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (normalized.includes('non-fast-forward') || normalized.includes('not fast-forward') || normalized.includes('fetch first') || normalized.includes('stale info') || normalized.includes('tip of your current branch is behind') || normalized.includes('updates were rejected')) {
|
|
27
|
+
return 'non-fast-forward';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (normalized.includes('remote rejected') || normalized.includes('[remote rejected]')) {
|
|
31
|
+
return 'remote-rejected';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (normalized.includes('rejected') || normalized.includes('failed to push some refs')) {
|
|
35
|
+
return 'rejected';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return 'unknown';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function shouldTreatPushRejectionAsRemoteSynchronized(divergence = null) {
|
|
42
|
+
if (!divergence?.remoteExists || divergence.ahead !== 0 || divergence.behind !== 0) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (divergence.localSha && divergence.remoteSha) {
|
|
47
|
+
return divergence.localSha === divergence.remoteSha;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function buildBranchSubjectLinks({ owner, repo, branchName, defaultBranch, forkedRepo = null }) {
|
|
54
|
+
const repository = `${owner}/${repo}`;
|
|
55
|
+
const headRepository = forkedRepo || repository;
|
|
56
|
+
const headOwner = headRepository.split('/')[0];
|
|
57
|
+
const baseBranch = defaultBranch || 'main';
|
|
58
|
+
const compareHead = forkedRepo ? `${headOwner}:${branchName}` : branchName;
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
repository,
|
|
62
|
+
headRepository,
|
|
63
|
+
baseBranchRef: `${repository}:${baseBranch}`,
|
|
64
|
+
headBranchRef: `${headRepository}:${branchName}`,
|
|
65
|
+
remoteBranchRef: `origin/${branchName}`,
|
|
66
|
+
repositoryUrl: `https://github.com/${repository}`,
|
|
67
|
+
branchUrl: `https://github.com/${headRepository}/tree/${encodeRefForGitHubUrl(branchName)}`,
|
|
68
|
+
compareUrl: `https://github.com/${repository}/compare/${encodeRefForGitHubUrl(baseBranch)}...${encodeRefForGitHubUrl(compareHead)}`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function buildPushRejectionFailureActionSection({ owner, repo, branchName, defaultBranch, forkedRepo = null }) {
|
|
73
|
+
if (!owner || !repo || !branchName) {
|
|
74
|
+
return `### What you can do
|
|
75
|
+
- Inspect the remote branch and compare it with the local branch before retrying.
|
|
76
|
+
- If the remote branch already contains the intended commit, rerun the solver.
|
|
77
|
+
- If the histories differ, merge or resolve the branch manually, then rerun the solver.`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const links = buildBranchSubjectLinks({ owner, repo, branchName, defaultBranch, forkedRepo });
|
|
81
|
+
|
|
82
|
+
return `### What you can do
|
|
83
|
+
- Inspect the remote branch: ${links.branchUrl}
|
|
84
|
+
- Compare the base and head branches: ${links.compareUrl}
|
|
85
|
+
- If the remote branch already contains the intended commit, rerun the solver. Matching remote branches are treated as usable after this fix.
|
|
86
|
+
- If the histories differ, merge or resolve \`${links.headBranchRef}\` against \`${links.baseBranchRef}\`, then rerun the solver.
|
|
87
|
+
|
|
88
|
+
Administrator-only CLI details, if any, are printed in the solver terminal log rather than in this GitHub comment.`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function buildPushRejectionExplanation({ branchName, isContinueMode, prNumber, divergence = null, owner = null, repo = null, defaultBranch = null, forkedRepo = null, classification = 'unknown' }) {
|
|
13
92
|
const lines = [];
|
|
14
93
|
|
|
15
94
|
if (isContinueMode && !prNumber) {
|
|
16
95
|
lines.push(' This run reused an existing issue branch because auto-continue found a matching branch with no PR.');
|
|
17
96
|
lines.push(' It is not a fresh branch created by this run, even though auto-PR creation is running now.');
|
|
97
|
+
} else if (classification === 'remote-ref-already-exists') {
|
|
98
|
+
lines.push(' GitHub rejected the push while creating or updating the remote ref because that ref already exists.');
|
|
18
99
|
} else {
|
|
19
100
|
lines.push(' The remote branch changed after the local branch state used for this push.');
|
|
20
101
|
}
|
|
21
102
|
|
|
103
|
+
if (owner && repo) {
|
|
104
|
+
const links = buildBranchSubjectLinks({ owner, repo, branchName, defaultBranch, forkedRepo });
|
|
105
|
+
lines.push(` Repository: ${links.repositoryUrl}`);
|
|
106
|
+
lines.push(` Base branch: ${links.baseBranchRef}`);
|
|
107
|
+
lines.push(` Remote branch: ${links.headBranchRef}`);
|
|
108
|
+
lines.push(` Branch URL: ${links.branchUrl}`);
|
|
109
|
+
lines.push(` Compare URL: ${links.compareUrl}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
22
112
|
if (divergence?.remoteExists && divergence.ahead !== null && divergence.behind !== null) {
|
|
23
113
|
lines.push(` Current branch state for ${branchName}: ${divergence.ahead} commit(s) ahead, ${divergence.behind} commit(s) behind origin/${branchName}.`);
|
|
114
|
+
if (divergence.localSha) {
|
|
115
|
+
lines.push(` Local HEAD: ${shortSha(divergence.localSha)}`);
|
|
116
|
+
}
|
|
117
|
+
if (divergence.remoteSha) {
|
|
118
|
+
lines.push(` Remote HEAD: ${shortSha(divergence.remoteSha)}`);
|
|
119
|
+
}
|
|
120
|
+
if (shouldTreatPushRejectionAsRemoteSynchronized(divergence)) {
|
|
121
|
+
lines.push(' The remote branch currently matches local HEAD, so this is not a branch divergence.');
|
|
122
|
+
}
|
|
24
123
|
} else if (divergence?.fetchError) {
|
|
25
124
|
lines.push(` Could not inspect origin/${branchName}: ${divergence.fetchError}`);
|
|
26
125
|
}
|
|
@@ -28,6 +127,142 @@ export function buildPushRejectionExplanation({ branchName, isContinueMode, prNu
|
|
|
28
127
|
return lines;
|
|
29
128
|
}
|
|
30
129
|
|
|
130
|
+
export async function logRecoverablePushRejection({ log, formatAligned, branchName, isContinueMode, prNumber, divergence, owner, repo, defaultBranch, forkedRepo, classification }) {
|
|
131
|
+
const links = buildBranchSubjectLinks({ owner, repo, branchName, defaultBranch, forkedRepo });
|
|
132
|
+
|
|
133
|
+
await log('');
|
|
134
|
+
await log(formatAligned('⚠️', 'PUSH REPORTED FAILURE:', 'Remote branch already matches local HEAD'), { level: 'warning' });
|
|
135
|
+
await log('');
|
|
136
|
+
await log(' 🔍 What happened:');
|
|
137
|
+
for (const line of buildPushRejectionExplanation({
|
|
138
|
+
branchName,
|
|
139
|
+
isContinueMode,
|
|
140
|
+
prNumber,
|
|
141
|
+
divergence,
|
|
142
|
+
owner,
|
|
143
|
+
repo,
|
|
144
|
+
defaultBranch,
|
|
145
|
+
forkedRepo,
|
|
146
|
+
classification,
|
|
147
|
+
})) {
|
|
148
|
+
await log(line);
|
|
149
|
+
}
|
|
150
|
+
await log('');
|
|
151
|
+
await log(' ✅ Recovery:');
|
|
152
|
+
await log(` The branch is available at ${links.branchUrl}.`);
|
|
153
|
+
await log(' Continuing with PR creation because no local commit would be lost.');
|
|
154
|
+
await log('');
|
|
155
|
+
|
|
156
|
+
return links;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function logBlockingPushRejection({ log, formatAligned, branchName, isContinueMode, prNumber, divergence, owner, repo, defaultBranch, forkedRepo, classification }) {
|
|
160
|
+
const links = buildBranchSubjectLinks({ owner, repo, branchName, defaultBranch, forkedRepo });
|
|
161
|
+
const isRefCollision = classification === 'remote-ref-already-exists';
|
|
162
|
+
|
|
163
|
+
await log('');
|
|
164
|
+
await log(formatAligned('❌', isRefCollision ? 'REMOTE BRANCH COLLISION:' : 'PUSH REJECTED:', isRefCollision ? 'Remote ref already exists and differs from local branch' : 'Local and remote branch histories differ'), { level: 'error' });
|
|
165
|
+
await log('');
|
|
166
|
+
await log(' 🔍 What happened:');
|
|
167
|
+
if (isRefCollision) {
|
|
168
|
+
await log(` GitHub rejected creation or update of ${links.headBranchRef} because that remote ref already exists.`);
|
|
169
|
+
await log(' The existing remote branch does not match this local branch, so hive-mind cannot assume it is safe to continue.');
|
|
170
|
+
} else {
|
|
171
|
+
await log(` Git rejected updating ${links.headBranchRef} from this local branch.`);
|
|
172
|
+
await log(' The local and remote histories are not in a state that a normal push can update safely.');
|
|
173
|
+
}
|
|
174
|
+
for (const line of buildPushRejectionExplanation({
|
|
175
|
+
branchName,
|
|
176
|
+
isContinueMode,
|
|
177
|
+
prNumber,
|
|
178
|
+
divergence,
|
|
179
|
+
owner,
|
|
180
|
+
repo,
|
|
181
|
+
defaultBranch,
|
|
182
|
+
forkedRepo,
|
|
183
|
+
classification,
|
|
184
|
+
})) {
|
|
185
|
+
await log(line);
|
|
186
|
+
}
|
|
187
|
+
await log('');
|
|
188
|
+
await log(' 💡 Why we cannot fix this automatically:');
|
|
189
|
+
await log(' • We never use force push to preserve history');
|
|
190
|
+
await log(' • We never use rebase or reset to avoid altering git history');
|
|
191
|
+
await log(` • Manual review is required before changing ${links.headBranchRef}`);
|
|
192
|
+
await log('');
|
|
193
|
+
await log(' 🔧 How to fix:');
|
|
194
|
+
await log(` 1. Inspect the remote branch: ${links.branchUrl}`);
|
|
195
|
+
await log(` 2. Compare base and head: ${links.compareUrl}`);
|
|
196
|
+
await log(' 3. Clone the repository and checkout the branch:');
|
|
197
|
+
await log(` git clone https://github.com/${links.headRepository}.git`);
|
|
198
|
+
await log(` cd ${links.headRepository.split('/')[1]}`);
|
|
199
|
+
await log(` git checkout ${branchName}`);
|
|
200
|
+
await log('');
|
|
201
|
+
await log(' 4. Merge the remote branch state, resolve conflicts if any, then push:');
|
|
202
|
+
await log(` git pull origin ${branchName}`);
|
|
203
|
+
await log(` git push origin ${branchName}`);
|
|
204
|
+
await log('');
|
|
205
|
+
await log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
206
|
+
await log('');
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
links,
|
|
210
|
+
failureActionSection: buildPushRejectionFailureActionSection({ owner, repo, branchName, defaultBranch, forkedRepo }),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export async function handleRejectedPushForAutoPr({ errorOutput, $, tempDir, log, formatAligned, branchName, isContinueMode, prNumber, owner, repo, defaultBranch, forkedRepo }) {
|
|
215
|
+
const classification = classifyPushRejection(errorOutput);
|
|
216
|
+
if (classification === 'unknown') {
|
|
217
|
+
return {
|
|
218
|
+
handled: false,
|
|
219
|
+
branchReadyForPrCreation: false,
|
|
220
|
+
recoveredFromPushRejection: false,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const divergence = await getRemoteBranchDivergenceSnapshot({ $, tempDir, branchName });
|
|
225
|
+
|
|
226
|
+
if (shouldTreatPushRejectionAsRemoteSynchronized(divergence)) {
|
|
227
|
+
await logRecoverablePushRejection({
|
|
228
|
+
log,
|
|
229
|
+
formatAligned,
|
|
230
|
+
branchName,
|
|
231
|
+
isContinueMode,
|
|
232
|
+
prNumber,
|
|
233
|
+
divergence,
|
|
234
|
+
owner,
|
|
235
|
+
repo,
|
|
236
|
+
defaultBranch,
|
|
237
|
+
forkedRepo,
|
|
238
|
+
classification,
|
|
239
|
+
});
|
|
240
|
+
return {
|
|
241
|
+
handled: true,
|
|
242
|
+
branchReadyForPrCreation: true,
|
|
243
|
+
recoveredFromPushRejection: true,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const { links, failureActionSection } = await logBlockingPushRejection({
|
|
248
|
+
log,
|
|
249
|
+
formatAligned,
|
|
250
|
+
branchName,
|
|
251
|
+
isContinueMode,
|
|
252
|
+
prNumber,
|
|
253
|
+
divergence,
|
|
254
|
+
owner,
|
|
255
|
+
repo,
|
|
256
|
+
defaultBranch,
|
|
257
|
+
forkedRepo,
|
|
258
|
+
classification,
|
|
259
|
+
});
|
|
260
|
+
const error = new Error(`Push rejected for ${links.headBranchRef}; compare ${links.compareUrl} and inspect ${links.branchUrl}`);
|
|
261
|
+
error.hiveMindUserFacingLogged = true;
|
|
262
|
+
error.failureActionSection = failureActionSection;
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
|
|
31
266
|
export async function getRemoteBranchDivergenceSnapshot({ $, tempDir, branchName }) {
|
|
32
267
|
const fetchResult = await $({ cwd: tempDir, silent: true })`git fetch origin refs/heads/${branchName}:refs/remotes/origin/${branchName} 2>&1`;
|
|
33
268
|
if (fetchResult.code !== 0) {
|
|
@@ -41,11 +276,15 @@ export async function getRemoteBranchDivergenceSnapshot({ $, tempDir, branchName
|
|
|
41
276
|
|
|
42
277
|
const aheadResult = await $({ cwd: tempDir, silent: true })`git rev-list --count origin/${branchName}..HEAD 2>&1`;
|
|
43
278
|
const behindResult = await $({ cwd: tempDir, silent: true })`git rev-list --count HEAD..origin/${branchName} 2>&1`;
|
|
279
|
+
const localShaResult = await $({ cwd: tempDir, silent: true })`git rev-parse HEAD 2>&1`;
|
|
280
|
+
const remoteShaResult = await $({ cwd: tempDir, silent: true })`git rev-parse origin/${branchName} 2>&1`;
|
|
44
281
|
|
|
45
282
|
return {
|
|
46
283
|
remoteExists: aheadResult.code === 0 && behindResult.code === 0,
|
|
47
284
|
ahead: aheadResult.code === 0 ? toCount(aheadResult.stdout) : null,
|
|
48
285
|
behind: behindResult.code === 0 ? toCount(behindResult.stdout) : null,
|
|
286
|
+
localSha: localShaResult.code === 0 ? outputOf(localShaResult) : null,
|
|
287
|
+
remoteSha: remoteShaResult.code === 0 ? outputOf(remoteShaResult) : null,
|
|
49
288
|
fetchError: aheadResult.code === 0 && behindResult.code === 0 ? null : outputOf(aheadResult) || outputOf(behindResult) || 'could not compare local and remote branch',
|
|
50
289
|
};
|
|
51
290
|
}
|
|
@@ -68,6 +68,7 @@ export const handleFailure = async options => {
|
|
|
68
68
|
sanitizeLogContent,
|
|
69
69
|
verbose: argv.verbose,
|
|
70
70
|
errorMessage: cleanErrorMessage(error),
|
|
71
|
+
failureActionSection: error?.failureActionSection || null,
|
|
71
72
|
// Issue #1225: Pass model and tool info for PR comments
|
|
72
73
|
requestedModel: argv.originalModel || argv.model,
|
|
73
74
|
tool: argv.tool || 'claude',
|
|
@@ -236,7 +237,9 @@ export const handleMainExecutionError = async options => {
|
|
|
236
237
|
return;
|
|
237
238
|
}
|
|
238
239
|
|
|
239
|
-
|
|
240
|
+
if (!error?.hiveMindUserFacingLogged) {
|
|
241
|
+
await log('Error executing command:', cleanErrorMessage(error));
|
|
242
|
+
}
|
|
240
243
|
await log(`Stack trace: ${error.stack}`, { verbose: true });
|
|
241
244
|
await log(` 📁 Full log file: ${absoluteLogPath}`, { level: 'error' });
|
|
242
245
|
|
|
@@ -15,6 +15,16 @@ const isForkDivergenceFailure = reason => {
|
|
|
15
15
|
return normalizedReason.includes('fork divergence') || (normalizedReason.includes('fork') && normalizedReason.includes('non-fast-forward')) || normalizedReason.includes('force-with-lease');
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
const extractUrlAfter = (reason, label) => {
|
|
19
|
+
const match = String(reason || '').match(new RegExp(`${label}\\s+(https://github\\.com/[^\\s;]+)`, 'i'));
|
|
20
|
+
return match ? match[1] : null;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const isPushRejectionFailure = reason => {
|
|
24
|
+
const normalizedReason = String(reason || '').toLowerCase();
|
|
25
|
+
return normalizedReason.includes('push rejected for ') || (normalizedReason.includes('failed to push') && normalizedReason.includes('github.com'));
|
|
26
|
+
};
|
|
27
|
+
|
|
18
28
|
export function buildPrePullRequestFailureActionSection(reason = '') {
|
|
19
29
|
const normalizedReason = String(reason || '').toLowerCase();
|
|
20
30
|
const isForkOrRecoveryFailure = normalizedReason.includes('fork') || normalizedReason.includes('auto-recovery') || normalizedReason.includes('repository setup');
|
|
@@ -25,6 +35,18 @@ export function buildPrePullRequestFailureActionSection(reason = '') {
|
|
|
25
35
|
- If the fork has commits you need to preserve, resolve the divergence manually, then rerun the solver.
|
|
26
36
|
- If this requires elevated Hive Mind access, ask a Hive Mind administrator to handle the affected fork or repository.
|
|
27
37
|
|
|
38
|
+
Administrator-only CLI details, if any, are printed in the solver terminal log rather than in this GitHub comment.`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (isPushRejectionFailure(reason)) {
|
|
42
|
+
const branchUrl = extractUrlAfter(reason, 'inspect');
|
|
43
|
+
const compareUrl = extractUrlAfter(reason, 'compare');
|
|
44
|
+
return `### What you can do
|
|
45
|
+
- Inspect the remote branch${branchUrl ? `: ${branchUrl}` : ' named in the failure'}.
|
|
46
|
+
- Compare the base and head histories${compareUrl ? `: ${compareUrl}` : ' using the compare link named in the failure'}.
|
|
47
|
+
- If the branch belongs to you, merge the remote branch state or choose a new branch name, then rerun the solver.
|
|
48
|
+
- Do not force-push unless you have manually confirmed that overwriting the remote branch is safe.
|
|
49
|
+
|
|
28
50
|
Administrator-only CLI details, if any, are printed in the solver terminal log rather than in this GitHub comment.`;
|
|
29
51
|
}
|
|
30
52
|
|
|
@@ -28,6 +28,7 @@ const { log, formatAligned } = lib;
|
|
|
28
28
|
|
|
29
29
|
// Import exit handler
|
|
30
30
|
import { safeExit } from './exit-handler.lib.mjs';
|
|
31
|
+
import { parseForkFullNameFromGhOutput } from './github-repository-names.lib.mjs';
|
|
31
32
|
|
|
32
33
|
// Import GitHub utilities for permission checks
|
|
33
34
|
const githubLib = await import('./github.lib.mjs');
|
|
@@ -589,11 +590,10 @@ export const setupRepository = async (argv, owner, repo, forkOwner = null, issue
|
|
|
589
590
|
const forkOutput = (forkResult.stderr ? forkResult.stderr.toString() : '') + (forkResult.stdout ? forkResult.stdout.toString() : '');
|
|
590
591
|
if (argv.verbose) await log(`${formatAligned('🔧', 'Fork output:', forkOutput.split('\n')[0] || '(empty)')}`); // Issue #1518
|
|
591
592
|
// Parse actual fork name from output (e.g., "konard/netkeep80-jsonRVM already exists")
|
|
592
|
-
//
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
actualForkName = forkNameMatch[1];
|
|
593
|
+
// Issue #1819: repository names can contain dots, such as "*.github.io".
|
|
594
|
+
const parsedForkName = parseForkFullNameFromGhOutput(forkOutput);
|
|
595
|
+
if (parsedForkName) {
|
|
596
|
+
actualForkName = parsedForkName;
|
|
597
597
|
}
|
|
598
598
|
|
|
599
599
|
if (forkResult.code === 0) {
|