@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.72.3",
3
+ "version": "1.72.5",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -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 { buildPushRejectionExplanation, getRemoteBranchDivergenceSnapshot, synchronizeExistingIssueBranchBeforeAutoPrCreation } from './solve.branch-divergence.lib.mjs';
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 if (errorOutput.includes('non-fast-forward') || errorOutput.includes('rejected') || errorOutput.includes('! [rejected]')) {
543
- const divergence = await getRemoteBranchDivergenceSnapshot({ $, tempDir, branchName });
544
- // Push rejected due to conflicts or diverged history
545
- await log('');
546
- await log(formatAligned('❌', 'PUSH REJECTED:', 'Branch has diverged from remote'), { level: 'error' });
547
- await log('');
548
- await log(' 🔍 What happened:');
549
- await log(' The remote branch has changes that conflict with your local changes.');
550
- await log(' This typically means someone else has pushed to this branch.');
551
- for (const line of buildPushRejectionExplanation({ branchName, isContinueMode, prNumber, divergence })) {
552
- await log(line);
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
- await log('');
555
- await log(' 💡 Why we cannot fix this automatically:');
556
- await log(' • We never use force push to preserve history');
557
- await log(' • We never use rebase or reset to avoid altering git history');
558
- await log(' • Manual conflict resolution is required');
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
- // Other push errors
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
- } else {
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
- export function buildPushRejectionExplanation({ branchName, isContinueMode, prNumber, divergence = null }) {
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
- await log('Error executing command:', cleanErrorMessage(error));
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
- // GitHub may create forks with modified names to avoid conflicts
593
- // Use regex that won't match domain names like "github.com/user" -> "com/user"
594
- const forkNameMatch = forkOutput.match(/(?:github\.com\/|^|\s)([a-zA-Z0-9_-]+\/[a-zA-Z0-9_-]+)/);
595
- if (forkNameMatch) {
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) {