@link-assistant/hive-mind 2.1.1 → 2.1.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 +6 -0
- package/package.json +1 -1
- package/src/exit-handler.lib.mjs +4 -2
- package/src/github.lib.mjs +2 -4
- package/src/solve.branch-divergence.lib.mjs +233 -3
- package/src/solve.fork-sync.lib.mjs +94 -38
- package/src/solve.mjs +1 -1
- package/src/solve.pre-pr-failure-notifier.lib.mjs +25 -30
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 2.1.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 39164e5: Improve fork-divergence failure comments and logs with inspected branch state, actor-aware admin guidance, exact fork-only commit lists, and less generic repository setup wording.
|
|
8
|
+
|
|
3
9
|
## 2.1.1
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/package.json
CHANGED
package/src/exit-handler.lib.mjs
CHANGED
|
@@ -235,14 +235,16 @@ export const logActiveHandles = async (log = null) => {
|
|
|
235
235
|
* @param {object} [options]
|
|
236
236
|
* @param {boolean} [options.skipPreExit=false] - Issue #1823: skip the pre-exit failure notifier
|
|
237
237
|
* (e.g. on graceful shutdown, which is NOT a failure and must not post a "solver failed" comment).
|
|
238
|
+
* @param {string|null} [options.failureActionSection=null] - Optional prebuilt user-facing recovery
|
|
239
|
+
* guidance for the pre-exit notifier.
|
|
238
240
|
*/
|
|
239
|
-
export const safeExit = async (code = 0, reason = 'Process completed', { skipPreExit = false } = {}) => {
|
|
241
|
+
export const safeExit = async (code = 0, reason = 'Process completed', { skipPreExit = false, failureActionSection = null } = {}) => {
|
|
240
242
|
await showExitMessage(reason, code);
|
|
241
243
|
|
|
242
244
|
if (!skipPreExit && code !== 0 && preExitFunction && !preExitHandlerRan) {
|
|
243
245
|
preExitHandlerRan = true;
|
|
244
246
|
try {
|
|
245
|
-
await preExitFunction({ code, reason });
|
|
247
|
+
await preExitFunction({ code, reason, failureActionSection });
|
|
246
248
|
} catch (error) {
|
|
247
249
|
const message = error && error.message ? error.message : String(error);
|
|
248
250
|
if (logFunction) {
|
package/src/github.lib.mjs
CHANGED
|
@@ -31,10 +31,8 @@ const buildIssueFailureActionSection = targetType => {
|
|
|
31
31
|
|
|
32
32
|
### What you can do
|
|
33
33
|
- Resolve the repository, account, permissions, or environment problem described above, then rerun the solver.
|
|
34
|
-
-
|
|
35
|
-
- Repository deletion can require a separate GitHub account or token with repository deletion permission; Hive Mind does not rely on that permission by default
|
|
36
|
-
|
|
37
|
-
Administrator-only CLI details, if any, are printed in the solver terminal log rather than in this issue comment.`;
|
|
34
|
+
- Repository owner or Hive Mind administrator path: handle manual recreation or fix of the repository when the required action is outside the requester access.
|
|
35
|
+
- Repository deletion can require a separate GitHub account or token with repository deletion permission; Hive Mind does not rely on that permission by default.`;
|
|
38
36
|
};
|
|
39
37
|
const normalizeFailureActionSection = section => {
|
|
40
38
|
const text = section || '';
|
|
@@ -11,11 +11,243 @@ const outputOf = result => {
|
|
|
11
11
|
|
|
12
12
|
const shortSha = sha => (sha ? String(sha).slice(0, 12) : null);
|
|
13
13
|
|
|
14
|
+
export const FORK_DIVERGENCE_RESOLUTION_OPTION = '--allow-fork-divergence-resolution-using-force-push-with-lease';
|
|
15
|
+
|
|
14
16
|
const encodeRefForGitHubUrl = ref =>
|
|
15
17
|
encodeURI(String(ref || ''))
|
|
16
18
|
.replaceAll('#', '%23')
|
|
17
19
|
.replaceAll('?', '%3F');
|
|
18
20
|
|
|
21
|
+
const firstLine = value =>
|
|
22
|
+
String(value || '')
|
|
23
|
+
.split('\n')
|
|
24
|
+
.map(line => line.trim())
|
|
25
|
+
.find(Boolean) || '';
|
|
26
|
+
|
|
27
|
+
const sameLogin = (left, right) => Boolean(left && right && String(left).toLowerCase() === String(right).toLowerCase());
|
|
28
|
+
|
|
29
|
+
const formatCommitLine = commit => {
|
|
30
|
+
const sha = commit?.shortSha || shortSha(commit?.sha) || 'unknown';
|
|
31
|
+
const author = String(commit?.author || 'unknown author').trim();
|
|
32
|
+
const subject = String(commit?.subject || 'no subject').trim();
|
|
33
|
+
return `${sha} ${author} ${subject}`;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const formatCommitBullets = commits => {
|
|
37
|
+
const safeCommits = Array.isArray(commits) ? commits : [];
|
|
38
|
+
if (safeCommits.length === 0) return ['- No fork-only commits were returned by the inspection.'];
|
|
39
|
+
return safeCommits.map(commit => `- \`${formatCommitLine(commit)}\``);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const buildForkCompareUrl = ({ upstreamRepo, forkedRepo, branchName }) => {
|
|
43
|
+
const [upstreamOwner, upstreamName] = String(upstreamRepo || '').split('/');
|
|
44
|
+
const [forkOwner] = String(forkedRepo || '').split('/');
|
|
45
|
+
if (!upstreamOwner || !upstreamName || !forkOwner || !branchName) return null;
|
|
46
|
+
return `https://github.com/${upstreamRepo}/compare/${encodeRefForGitHubUrl(branchName)}...${encodeRefForGitHubUrl(`${forkOwner}:${branchName}`)}`;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const normalizeForkDivergenceSnapshot = (snapshot = {}) => {
|
|
50
|
+
const branchName = snapshot.branchName || 'the default branch';
|
|
51
|
+
const upstreamRepo = snapshot.upstreamRepo || 'the upstream repository';
|
|
52
|
+
const forkedRepo = snapshot.forkedRepo || 'the fork repository';
|
|
53
|
+
const forkRef = snapshot.forkRef || `origin/${branchName}`;
|
|
54
|
+
const upstreamRef = snapshot.upstreamRef || `upstream/${branchName}`;
|
|
55
|
+
const compareUrl = snapshot.compareUrl || buildForkCompareUrl({ upstreamRepo, forkedRepo, branchName });
|
|
56
|
+
const uniqueCommits = Array.isArray(snapshot.uniqueCommits) ? snapshot.uniqueCommits : [];
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
...snapshot,
|
|
60
|
+
branchName,
|
|
61
|
+
upstreamRepo,
|
|
62
|
+
forkedRepo,
|
|
63
|
+
forkRef,
|
|
64
|
+
upstreamRef,
|
|
65
|
+
compareUrl,
|
|
66
|
+
uniqueCommits,
|
|
67
|
+
forkUniqueCount: Number.isFinite(snapshot.forkUniqueCount) ? snapshot.forkUniqueCount : null,
|
|
68
|
+
upstreamUniqueCount: Number.isFinite(snapshot.upstreamUniqueCount) ? snapshot.upstreamUniqueCount : null,
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const buildActorLine = ({ currentUser, taskRequester }) => {
|
|
73
|
+
if (sameLogin(currentUser, taskRequester)) {
|
|
74
|
+
return `Hive Mind is authenticated as \`${currentUser}\`, the same GitHub user that requested this task.`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (currentUser && taskRequester) {
|
|
78
|
+
return `Hive Mind is authenticated as \`${currentUser}\`; the task requester is \`${taskRequester}\`.`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (currentUser) {
|
|
82
|
+
return `Hive Mind is authenticated as \`${currentUser}\`; the task requester could not be resolved.`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (taskRequester) {
|
|
86
|
+
return `The task requester is \`${taskRequester}\`; the authenticated Hive Mind user could not be resolved.`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return 'Hive Mind could not resolve the authenticated user or task requester.';
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const buildManualForkRepairCommands = ({ snapshot, solveCommand }) => {
|
|
93
|
+
const { branchName, forkedRepo, upstreamRef } = snapshot;
|
|
94
|
+
const commands = ['```bash', `gh repo delete ${forkedRepo} --yes`, `${solveCommand || 'solve <issue-url>'}`, '', '# Manual branch repair after preserving any fork-only commits:', 'git fetch upstream', `git reset --hard ${upstreamRef}`, `git push --force-with-lease origin ${branchName}`, '```'];
|
|
95
|
+
return commands.join('\n');
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export async function getForkDefaultBranchDivergenceSnapshot({ $, tempDir, branchName, forkedRepo, upstreamRepo }) {
|
|
99
|
+
const forkRef = `origin/${branchName}`;
|
|
100
|
+
const upstreamRef = `upstream/${branchName}`;
|
|
101
|
+
const compareUrl = buildForkCompareUrl({ upstreamRepo, forkedRepo, branchName });
|
|
102
|
+
|
|
103
|
+
const forkFetchResult = await $({ cwd: tempDir, silent: true })`git fetch origin refs/heads/${branchName}:refs/remotes/origin/${branchName} 2>&1`;
|
|
104
|
+
if (forkFetchResult.code !== 0) {
|
|
105
|
+
return {
|
|
106
|
+
branchName,
|
|
107
|
+
forkedRepo,
|
|
108
|
+
upstreamRepo,
|
|
109
|
+
forkRef,
|
|
110
|
+
upstreamRef,
|
|
111
|
+
compareUrl,
|
|
112
|
+
forkUniqueCount: null,
|
|
113
|
+
upstreamUniqueCount: null,
|
|
114
|
+
uniqueCommits: [],
|
|
115
|
+
fetchError: outputOf(forkFetchResult) || `failed to fetch ${forkRef}`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const upstreamFetchResult = await $({ cwd: tempDir, silent: true })`git fetch upstream refs/heads/${branchName}:refs/remotes/upstream/${branchName} 2>&1`;
|
|
120
|
+
if (upstreamFetchResult.code !== 0) {
|
|
121
|
+
return {
|
|
122
|
+
branchName,
|
|
123
|
+
forkedRepo,
|
|
124
|
+
upstreamRepo,
|
|
125
|
+
forkRef,
|
|
126
|
+
upstreamRef,
|
|
127
|
+
compareUrl,
|
|
128
|
+
forkUniqueCount: null,
|
|
129
|
+
upstreamUniqueCount: null,
|
|
130
|
+
uniqueCommits: [],
|
|
131
|
+
fetchError: outputOf(upstreamFetchResult) || `failed to fetch ${upstreamRef}`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const forkUniqueResult = await $({ cwd: tempDir, silent: true })`git rev-list --count ${upstreamRef}..${forkRef} 2>&1`;
|
|
136
|
+
const upstreamUniqueResult = await $({ cwd: tempDir, silent: true })`git rev-list --count ${forkRef}..${upstreamRef} 2>&1`;
|
|
137
|
+
const forkHeadResult = await $({ cwd: tempDir, silent: true })`git rev-parse ${forkRef} 2>&1`;
|
|
138
|
+
const upstreamHeadResult = await $({ cwd: tempDir, silent: true })`git rev-parse ${upstreamRef} 2>&1`;
|
|
139
|
+
const commitsResult = await $({ cwd: tempDir, silent: true })`git log --format=%H%x09%an%x09%s --max-count=20 ${upstreamRef}..${forkRef} 2>&1`;
|
|
140
|
+
|
|
141
|
+
const uniqueCommits =
|
|
142
|
+
commitsResult.code === 0
|
|
143
|
+
? commitsResult.stdout
|
|
144
|
+
.toString()
|
|
145
|
+
.trim()
|
|
146
|
+
.split('\n')
|
|
147
|
+
.filter(Boolean)
|
|
148
|
+
.map(line => {
|
|
149
|
+
const [sha, author, ...subjectParts] = line.split('\t');
|
|
150
|
+
return {
|
|
151
|
+
sha,
|
|
152
|
+
shortSha: shortSha(sha),
|
|
153
|
+
author,
|
|
154
|
+
subject: subjectParts.join('\t'),
|
|
155
|
+
};
|
|
156
|
+
})
|
|
157
|
+
: [];
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
branchName,
|
|
161
|
+
forkedRepo,
|
|
162
|
+
upstreamRepo,
|
|
163
|
+
forkRef,
|
|
164
|
+
upstreamRef,
|
|
165
|
+
compareUrl,
|
|
166
|
+
forkUniqueCount: forkUniqueResult.code === 0 ? toCount(forkUniqueResult.stdout) : null,
|
|
167
|
+
upstreamUniqueCount: upstreamUniqueResult.code === 0 ? toCount(upstreamUniqueResult.stdout) : null,
|
|
168
|
+
forkHead: forkHeadResult.code === 0 ? firstLine(forkHeadResult.stdout) : null,
|
|
169
|
+
upstreamHead: upstreamHeadResult.code === 0 ? firstLine(upstreamHeadResult.stdout) : null,
|
|
170
|
+
uniqueCommits,
|
|
171
|
+
inspectError: commitsResult.code === 0 ? null : outputOf(commitsResult),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function buildForkDivergenceBlockedReason({ snapshot }) {
|
|
176
|
+
const details = normalizeForkDivergenceSnapshot(snapshot);
|
|
177
|
+
const lines = ['Repository setup halted - fork divergence requires user decision.', `Fork: ${details.forkedRepo}`, `Upstream: ${details.upstreamRepo}`, `Branch: ${details.branchName}`];
|
|
178
|
+
|
|
179
|
+
if (details.fetchError || details.inspectError) {
|
|
180
|
+
lines.push(`Inspection error: ${details.fetchError || details.inspectError}`);
|
|
181
|
+
lines.push('Hive Mind did not recommend automatic force-with-lease because it could not prove whether fork-only commits would be lost.');
|
|
182
|
+
return lines.join('\n');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
lines.push(`Fork-only commits that would be overwritten: ${details.forkUniqueCount ?? 'unknown'}`);
|
|
186
|
+
lines.push(`Upstream-only commits missing from fork: ${details.upstreamUniqueCount ?? 'unknown'}`);
|
|
187
|
+
if (details.compareUrl) lines.push(`Compare: ${details.compareUrl}`);
|
|
188
|
+
|
|
189
|
+
if ((details.forkUniqueCount ?? 0) > 0) {
|
|
190
|
+
lines.push('Fork-only commit list:');
|
|
191
|
+
for (const commit of details.uniqueCommits) {
|
|
192
|
+
lines.push(`- ${formatCommitLine(commit)}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return lines.join('\n');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function buildForkDivergenceFailureActionSection({ snapshot = {}, currentUser = null, taskRequester = null, solveCommand = null } = {}) {
|
|
200
|
+
const details = normalizeForkDivergenceSnapshot(snapshot);
|
|
201
|
+
const actorLine = buildActorLine({ currentUser, taskRequester });
|
|
202
|
+
const sameUser = sameLogin(currentUser, taskRequester);
|
|
203
|
+
const hasInspectionError = Boolean(details.fetchError || details.inspectError);
|
|
204
|
+
const forkUniqueCount = details.forkUniqueCount;
|
|
205
|
+
const upstreamUniqueCount = details.upstreamUniqueCount;
|
|
206
|
+
|
|
207
|
+
const header = ['### What happened', `- Hive Mind checked \`${details.forkRef}\` in \`${details.forkedRepo}\` against \`${details.upstreamRef}\` in \`${details.upstreamRepo}\`.`, `- ${actorLine}`];
|
|
208
|
+
if (details.compareUrl) {
|
|
209
|
+
header.push(`- Compare the branch state: ${details.compareUrl}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (hasInspectionError) {
|
|
213
|
+
header.push(`- The safety inspection failed: ${details.fetchError || details.inspectError}`);
|
|
214
|
+
return `${header.join('\n')}
|
|
215
|
+
|
|
216
|
+
### What you can do
|
|
217
|
+
- Hive Mind did not recommend automatic force-with-lease because it could not prove whether fork-only commits would be overwritten.
|
|
218
|
+
- Ask a Hive Mind administrator to handle manual recreation or fix of the repository.
|
|
219
|
+
- Repository owner path: inspect \`${details.forkedRepo}\`, preserve any needed commits, then recreate or repair the fork default branch.`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (forkUniqueCount === 0) {
|
|
223
|
+
const rerunLine = sameUser ? `Rerun with \`${FORK_DIVERGENCE_RESOLUTION_OPTION}\` to let Hive Mind update \`${details.forkedRepo}\` using \`git push --force-with-lease origin ${details.branchName}\`.` : `Ask a Hive Mind administrator to rerun with \`${FORK_DIVERGENCE_RESOLUTION_OPTION}\` so Hive Mind can update \`${details.forkedRepo}\` using \`git push --force-with-lease origin ${details.branchName}\`.`;
|
|
224
|
+
|
|
225
|
+
return `${header.join('\n')}
|
|
226
|
+
- GitHub inspection found 0 commit(s) unique to \`${details.forkRef}\`; \`${details.forkRef}\` is ${upstreamUniqueCount ?? 'an unknown number of'} commit(s) behind \`${details.upstreamRef}\`.
|
|
227
|
+
|
|
228
|
+
### What you can do
|
|
229
|
+
- ${rerunLine}
|
|
230
|
+
- Delete or recreate the fork repository, or fix the repository manually, using these commands when you own the fork or are acting as the Hive Mind administrator:
|
|
231
|
+
|
|
232
|
+
${buildManualForkRepairCommands({ snapshot: details, solveCommand })}`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const administratorLine = sameUser ? 'Delete or recreate the fork repository, or fix the repository manually, after preserving the fork-only commits listed above.' : 'Ask a Hive Mind administrator to handle manual recreation or fix of the repository.';
|
|
236
|
+
|
|
237
|
+
return `${header.join('\n')}
|
|
238
|
+
- \`${details.forkRef}\` has ${forkUniqueCount ?? 'an unknown number of'} commit(s) unique to \`${details.forkRef}\`; replacing it with \`${details.upstreamRef}\` would remove them from the fork default branch history.
|
|
239
|
+
|
|
240
|
+
### Commits that would be lost
|
|
241
|
+
${formatCommitBullets(details.uniqueCommits).join('\n')}
|
|
242
|
+
|
|
243
|
+
### What you can do
|
|
244
|
+
- ${administratorLine}
|
|
245
|
+
- Preserve the listed commit(s) first by moving them to another branch, cherry-picking them later, or confirming they are disposable.
|
|
246
|
+
- Manual repair example after preserving the listed commits:
|
|
247
|
+
|
|
248
|
+
${buildManualForkRepairCommands({ snapshot: details, solveCommand })}`;
|
|
249
|
+
}
|
|
250
|
+
|
|
19
251
|
export function classifyPushRejection(errorOutput = '') {
|
|
20
252
|
const normalized = String(errorOutput || '').toLowerCase();
|
|
21
253
|
|
|
@@ -138,9 +370,7 @@ export function buildPushRejectionFailureActionSection({ owner, repo, branchName
|
|
|
138
370
|
- Inspect the remote branch: ${links.branchUrl}
|
|
139
371
|
- Compare the base and head branches: ${links.compareUrl}
|
|
140
372
|
- If the remote branch already contains the intended commit, rerun the solver. Matching remote branches are treated as usable after this fix.
|
|
141
|
-
-
|
|
142
|
-
|
|
143
|
-
Administrator-only CLI details, if any, are printed in the solver terminal log rather than in this GitHub comment.`;
|
|
373
|
+
- Diverged-history path: merge or resolve \`${links.headBranchRef}\` against \`${links.baseBranchRef}\`, then rerun the solver.`;
|
|
144
374
|
}
|
|
145
375
|
|
|
146
376
|
export function buildPushRejectionExplanation({ branchName, isContinueMode, prNumber, divergence = null, owner = null, repo = null, defaultBranch = null, forkedRepo = null, classification = 'unknown' }) {
|
|
@@ -26,7 +26,80 @@ import { safeExit } from './exit-handler.lib.mjs';
|
|
|
26
26
|
// Issue #1893: helpers that decide whether the fork's default branch may be
|
|
27
27
|
// pushed and that distinguish a permission-denied rejection from a genuine
|
|
28
28
|
// fork divergence.
|
|
29
|
-
const { isPermissionDeniedPushError, shouldPushDefaultBranchToFork } = await import('./solve.branch-divergence.lib.mjs');
|
|
29
|
+
const { buildForkDivergenceBlockedReason, buildForkDivergenceFailureActionSection, getForkDefaultBranchDivergenceSnapshot, isPermissionDeniedPushError, shouldPushDefaultBranchToFork } = await import('./solve.branch-divergence.lib.mjs');
|
|
30
|
+
|
|
31
|
+
const firstNonEmptyLine = value =>
|
|
32
|
+
String(value || '')
|
|
33
|
+
.split('\n')
|
|
34
|
+
.map(line => line.trim())
|
|
35
|
+
.find(Boolean) || '';
|
|
36
|
+
|
|
37
|
+
const resolveSolveCommand = argv => {
|
|
38
|
+
const issueUrl = argv.url || argv['issue-url'] || argv._?.[0] || '<issue-url>';
|
|
39
|
+
return `solve ${issueUrl}`;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const extractGitHubUrlNumber = argv => {
|
|
43
|
+
const url = String(argv.url || argv['issue-url'] || argv._?.[0] || '');
|
|
44
|
+
const match = url.match(/github\.com\/[^/]+\/[^/]+\/(issues|pull)\/(\d+)/i);
|
|
45
|
+
return match ? { type: match[1] === 'pull' ? 'pulls' : 'issues', number: match[2] } : null;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const resolveTaskRequester = async ({ $, owner, repo, argv }) => {
|
|
49
|
+
const target = extractGitHubUrlNumber(argv);
|
|
50
|
+
if (!target) return null;
|
|
51
|
+
const result = await $({ silent: true })`gh api repos/${owner}/${repo}/${target.type}/${target.number} --jq .user.login 2>&1`;
|
|
52
|
+
return result.code === 0 ? firstNonEmptyLine(result.stdout) : null;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const logForkDivergenceDetails = async ({ snapshot, currentUser, taskRequester, solveCommand, includeAction = true }) => {
|
|
56
|
+
await log('');
|
|
57
|
+
await log(' 🔍 What happened:');
|
|
58
|
+
await log(` Git rejected updating ${snapshot.forkedRepo}:${snapshot.branchName} after Hive Mind synced the local branch to ${snapshot.upstreamRepo}:${snapshot.branchName}.`);
|
|
59
|
+
await log('');
|
|
60
|
+
await log(' 📦 Current state:');
|
|
61
|
+
await log(` Fork: ${snapshot.forkedRepo}`);
|
|
62
|
+
await log(` Upstream: ${snapshot.upstreamRepo}`);
|
|
63
|
+
await log(` Branch: ${snapshot.branchName}`);
|
|
64
|
+
if (currentUser) await log(` Authenticated user: ${currentUser}`);
|
|
65
|
+
if (taskRequester) await log(` Task requester: ${taskRequester}`);
|
|
66
|
+
if (snapshot.compareUrl) await log(` Compare: ${snapshot.compareUrl}`);
|
|
67
|
+
|
|
68
|
+
if (snapshot.fetchError || snapshot.inspectError) {
|
|
69
|
+
await log('');
|
|
70
|
+
await log(' ⚠️ Safety check incomplete:');
|
|
71
|
+
await log(` ${snapshot.fetchError || snapshot.inspectError}`);
|
|
72
|
+
await log(' Hive Mind cannot prove whether force-with-lease would overwrite fork-only commits.');
|
|
73
|
+
} else {
|
|
74
|
+
await log(` Fork-only commits: ${snapshot.forkUniqueCount ?? 'unknown'}`);
|
|
75
|
+
await log(` Upstream-only commits missing from fork: ${snapshot.upstreamUniqueCount ?? 'unknown'}`);
|
|
76
|
+
if ((snapshot.forkUniqueCount ?? 0) > 0) {
|
|
77
|
+
await log('');
|
|
78
|
+
await log(' ⚠️ Commits that would be lost from the fork default branch:');
|
|
79
|
+
for (const commit of snapshot.uniqueCommits) {
|
|
80
|
+
await log(` - ${commit.shortSha || commit.sha} ${commit.author || 'unknown author'} ${commit.subject || 'no subject'}`);
|
|
81
|
+
}
|
|
82
|
+
if (snapshot.uniqueCommits.length < snapshot.forkUniqueCount) {
|
|
83
|
+
await log(` - ... ${snapshot.forkUniqueCount - snapshot.uniqueCommits.length} more commit(s) not shown`);
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
await log('');
|
|
87
|
+
await log(' ✅ Safety check:');
|
|
88
|
+
await log(` No commits unique to ${snapshot.forkRef} were found.`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const actionSection = buildForkDivergenceFailureActionSection({ snapshot, currentUser, taskRequester, solveCommand });
|
|
93
|
+
if (includeAction) {
|
|
94
|
+
await log('');
|
|
95
|
+
await log(' 🔧 GitHub comment guidance:');
|
|
96
|
+
for (const line of actionSection.split('\n')) {
|
|
97
|
+
await log(` ${line}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
await log('');
|
|
101
|
+
return actionSection;
|
|
102
|
+
};
|
|
30
103
|
|
|
31
104
|
// Set up upstream remote and sync fork
|
|
32
105
|
export const setupUpstreamAndSync = async (tempDir, forkedRepo, upstreamRemote, owner, repo, argv) => {
|
|
@@ -170,16 +243,23 @@ export const setupUpstreamAndSync = async (tempDir, forkedRepo, upstreamRemote,
|
|
|
170
243
|
// Fork has diverged from upstream
|
|
171
244
|
await log('');
|
|
172
245
|
await log(`${formatAligned('⚠️', 'FORK DIVERGENCE DETECTED', '')}`, { level: 'warn' });
|
|
173
|
-
await
|
|
174
|
-
|
|
175
|
-
await
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
await
|
|
246
|
+
const taskRequester = await resolveTaskRequester({ $, owner, repo, argv });
|
|
247
|
+
const solveCommand = resolveSolveCommand(argv);
|
|
248
|
+
const divergenceSnapshot = await getForkDefaultBranchDivergenceSnapshot({
|
|
249
|
+
$,
|
|
250
|
+
tempDir,
|
|
251
|
+
branchName: upstreamDefaultBranch,
|
|
252
|
+
forkedRepo,
|
|
253
|
+
upstreamRepo: `${owner}/${repo}`,
|
|
254
|
+
});
|
|
255
|
+
const failureActionSection = await logForkDivergenceDetails({
|
|
256
|
+
snapshot: divergenceSnapshot,
|
|
257
|
+
currentUser,
|
|
258
|
+
taskRequester,
|
|
259
|
+
solveCommand,
|
|
260
|
+
includeAction: !argv.allowForkDivergenceResolutionUsingForcePushWithLease,
|
|
261
|
+
});
|
|
262
|
+
const blockedReason = buildForkDivergenceBlockedReason({ snapshot: divergenceSnapshot });
|
|
183
263
|
|
|
184
264
|
// Check if user has enabled automatic force push
|
|
185
265
|
if (argv.allowForkDivergenceResolutionUsingForcePushWithLease) {
|
|
@@ -225,36 +305,12 @@ export const setupUpstreamAndSync = async (tempDir, forkedRepo, upstreamRemote,
|
|
|
225
305
|
await log(' 3. Manually sync fork with upstream:');
|
|
226
306
|
await log(' git fetch upstream');
|
|
227
307
|
await log(` git reset --hard upstream/${upstreamDefaultBranch}`);
|
|
228
|
-
await log(` git push --force origin ${upstreamDefaultBranch}`);
|
|
308
|
+
await log(` git push --force-with-lease origin ${upstreamDefaultBranch}`);
|
|
229
309
|
await log('');
|
|
230
|
-
await safeExit(1, 'Repository setup failed - fork sync failed');
|
|
310
|
+
await safeExit(1, 'Repository setup failed - fork sync failed', { failureActionSection });
|
|
231
311
|
}
|
|
232
312
|
} else {
|
|
233
|
-
|
|
234
|
-
await log(' 💡 Your options:');
|
|
235
|
-
await log('');
|
|
236
|
-
await log(' Option 1: Delete your fork and recreate it (SIMPLEST)');
|
|
237
|
-
await log(` gh repo delete ${forkedRepo}`);
|
|
238
|
-
await log(' Then run the solve command again - the fork will be recreated automatically');
|
|
239
|
-
await log(' ⚠️ Only use this if your fork has no unique commits you need to preserve');
|
|
240
|
-
await log('');
|
|
241
|
-
await log(' Option 2: Enable automatic force-push (DANGEROUS)');
|
|
242
|
-
await log(' Add --allow-fork-divergence-resolution-using-force-push-with-lease flag to your command');
|
|
243
|
-
await log(' This will automatically sync your fork with upstream using force-with-lease');
|
|
244
|
-
await log(' ⚠️ Overwrites fork history - any unique commits will be LOST');
|
|
245
|
-
await log('');
|
|
246
|
-
await log(' Option 3: Manually resolve the divergence');
|
|
247
|
-
await log(' 1. Decide if you need any commits unique to your fork');
|
|
248
|
-
await log(' 2. If yes, cherry-pick them after syncing');
|
|
249
|
-
await log(' 3. If no, manually force-push:');
|
|
250
|
-
await log(' git fetch upstream');
|
|
251
|
-
await log(` git reset --hard upstream/${upstreamDefaultBranch}`);
|
|
252
|
-
await log(` git push --force origin ${upstreamDefaultBranch}`);
|
|
253
|
-
await log('');
|
|
254
|
-
await log(' 🔧 To proceed with auto-resolution, restart with:');
|
|
255
|
-
await log(` solve ${argv.url || argv['issue-url'] || argv._[0] || '<issue-url>'} --allow-fork-divergence-resolution-using-force-push-with-lease`);
|
|
256
|
-
await log('');
|
|
257
|
-
await safeExit(1, 'Repository setup halted - fork divergence requires user decision');
|
|
313
|
+
await safeExit(1, blockedReason, { failureActionSection });
|
|
258
314
|
}
|
|
259
315
|
} else {
|
|
260
316
|
// Some other push error (not divergence-related)
|
package/src/solve.mjs
CHANGED
|
@@ -163,7 +163,7 @@ const cleanupWrapper = async () => {
|
|
|
163
163
|
}
|
|
164
164
|
};
|
|
165
165
|
const interruptWrapper = createInterruptWrapper({ cleanupContext, checkForUncommittedChanges, shouldAttachLogs, attachLogToGitHub, getLogFile, sanitizeLogContent, $, log });
|
|
166
|
-
initializeExitHandler(getAbsoluteLogPath, log, cleanupWrapper, interruptWrapper, ({ code, reason }) => notifyIssueAboutPrePullRequestFailure({ code, reason, argv, globalState: global, $, log, getLogFile, shouldAttachLogs, attachLogToGitHub, sanitizeLogContent, rawCommand }));
|
|
166
|
+
initializeExitHandler(getAbsoluteLogPath, log, cleanupWrapper, interruptWrapper, ({ code, reason, failureActionSection }) => notifyIssueAboutPrePullRequestFailure({ code, reason, failureActionSection, argv, globalState: global, $, log, getLogFile, shouldAttachLogs, attachLogToGitHub, sanitizeLogContent, rawCommand }));
|
|
167
167
|
installGlobalExitHandlers();
|
|
168
168
|
// Issue #1823: Configure the working-session guard. When the experimental
|
|
169
169
|
// --do-not-shutdown-in-the-middle-of-working-session flag is set (hive passes it to every
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { getTrackedToolCommentIds, postTrackedComment, SOLUTION_DRAFT_FAILED_MARKER } from './tool-comments.lib.mjs';
|
|
2
2
|
import { extractForkReplacementBlockedDetails, isForkReplacementBlockedReason } from './solve.repository-recovery-message.lib.mjs';
|
|
3
|
+
import { FORK_DIVERGENCE_RESOLUTION_OPTION, buildForkDivergenceFailureActionSection } from './solve.branch-divergence.lib.mjs';
|
|
3
4
|
|
|
4
|
-
export
|
|
5
|
+
export { FORK_DIVERGENCE_RESOLUTION_OPTION };
|
|
5
6
|
|
|
6
7
|
const truncate = (value, maxLength = 2000) => {
|
|
7
8
|
const text = value === null || value === undefined ? '' : String(value);
|
|
@@ -40,20 +41,17 @@ export function buildPrePullRequestFailureActionSection(reason = '') {
|
|
|
40
41
|
- It could not prove whether the existing repository has unique commits, so it did not delete it automatically.
|
|
41
42
|
|
|
42
43
|
### What you can do
|
|
43
|
-
-
|
|
44
|
-
-
|
|
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.`;
|
|
44
|
+
- Repository owner path: back up any needed work, then delete, rename, archive, or repair \`${repository}\` in GitHub and rerun the solver.
|
|
45
|
+
- External-owner path: ask the repository owner or a Hive Mind administrator to clean it up and rerun the solver.
|
|
46
|
+
- Use \`--allow-force-non-fork-repository-deletion\` only after confirming that deleting \`${repository}\` is acceptable and any unique commits can be lost.`;
|
|
48
47
|
}
|
|
49
48
|
|
|
50
49
|
if (isForkDivergenceFailure(reason)) {
|
|
51
|
-
return
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
Administrator-only CLI details, if any, are printed in the solver terminal log rather than in this GitHub comment.`;
|
|
50
|
+
return buildForkDivergenceFailureActionSection({
|
|
51
|
+
snapshot: {
|
|
52
|
+
fetchError: 'No fork divergence inspection data was attached to this failure reason.',
|
|
53
|
+
},
|
|
54
|
+
});
|
|
57
55
|
}
|
|
58
56
|
|
|
59
57
|
if (isPushRejectionFailure(reason)) {
|
|
@@ -62,26 +60,20 @@ Administrator-only CLI details, if any, are printed in the solver terminal log r
|
|
|
62
60
|
return `### What you can do
|
|
63
61
|
- Inspect the remote branch${branchUrl ? `: ${branchUrl}` : ' named in the failure'}.
|
|
64
62
|
- Compare the base and head histories${compareUrl ? `: ${compareUrl}` : ' using the compare link named in the failure'}.
|
|
65
|
-
-
|
|
66
|
-
-
|
|
67
|
-
|
|
68
|
-
Administrator-only CLI details, if any, are printed in the solver terminal log rather than in this GitHub comment.`;
|
|
63
|
+
- Branch owner path: merge the remote branch state or choose a new branch name, then rerun the solver.
|
|
64
|
+
- Manual force-push remains blocked until a human confirms that overwriting the remote branch is safe.`;
|
|
69
65
|
}
|
|
70
66
|
|
|
71
67
|
if (isForkOrRecoveryFailure) {
|
|
72
68
|
return `### What you can do
|
|
73
|
-
-
|
|
74
|
-
-
|
|
75
|
-
- Repository deletion can require a separate GitHub account or token with repository deletion permission; Hive Mind does not rely on that permission by default
|
|
76
|
-
|
|
77
|
-
Administrator-only CLI details, if any, are printed in the solver terminal log rather than in this issue comment.`;
|
|
69
|
+
- Repository owner path: remove, rename, archive, initialize, or otherwise repair the affected fork or repository in GitHub, then rerun the solver.
|
|
70
|
+
- External-owner path: ask a Hive Mind administrator to handle manual recreation or fix of the repository and rerun the solver.
|
|
71
|
+
- Repository deletion can require a separate GitHub account or token with repository deletion permission; Hive Mind does not rely on that permission by default.`;
|
|
78
72
|
}
|
|
79
73
|
|
|
80
74
|
return `### What you can do
|
|
81
75
|
- Resolve the repository, account, permissions, or environment problem described above, then rerun the solver.
|
|
82
|
-
-
|
|
83
|
-
|
|
84
|
-
Administrator-only CLI details, if any, are printed in the solver terminal log rather than in this issue comment.`;
|
|
76
|
+
- Repository owner or Hive Mind administrator path: handle manual recreation or fix of the repository when the required action is outside the requester access.`;
|
|
85
77
|
}
|
|
86
78
|
|
|
87
79
|
export function shouldNotifyIssueAboutPrePullRequestFailure({ code, globalState }) {
|
|
@@ -128,11 +120,11 @@ export function resolvePreExitFailureNotificationTarget({ code, globalState }) {
|
|
|
128
120
|
};
|
|
129
121
|
}
|
|
130
122
|
|
|
131
|
-
export function buildPrePullRequestFailureComment({ reason, owner, repo, issueNumber, argv = {}, logAttachmentAttempted = false }) {
|
|
123
|
+
export function buildPrePullRequestFailureComment({ reason, owner, repo, issueNumber, argv = {}, logAttachmentAttempted = false, failureActionSection = null }) {
|
|
132
124
|
const tool = argv.tool || 'claude';
|
|
133
125
|
const modelLine = argv.model ? `\n- **Requested model**: \`${argv.model}\`` : '';
|
|
134
126
|
const logLine = logAttachmentAttempted ? 'Log attachment was attempted but failed. Check the solver terminal log for the complete failure output.' : 'Logs were not attached because `--attach-logs` was not enabled.';
|
|
135
|
-
const actionSection = buildPrePullRequestFailureActionSection(reason);
|
|
127
|
+
const actionSection = failureActionSection || buildPrePullRequestFailureActionSection(reason);
|
|
136
128
|
|
|
137
129
|
return `## 🚨 ${SOLUTION_DRAFT_FAILED_MARKER}
|
|
138
130
|
|
|
@@ -154,12 +146,12 @@ ${logLine}
|
|
|
154
146
|
`;
|
|
155
147
|
}
|
|
156
148
|
|
|
157
|
-
export function buildExistingPullRequestFailureComment({ reason, owner, repo, prNumber, issueNumber = null, argv = {}, logAttachmentAttempted = false }) {
|
|
149
|
+
export function buildExistingPullRequestFailureComment({ reason, owner, repo, prNumber, issueNumber = null, argv = {}, logAttachmentAttempted = false, failureActionSection = null }) {
|
|
158
150
|
const tool = argv.tool || 'claude';
|
|
159
151
|
const modelLine = argv.model ? `\n- **Requested model**: \`${argv.model}\`` : '';
|
|
160
152
|
const issueLine = issueNumber ? `\n- **Linked issue**: #${issueNumber}` : '';
|
|
161
153
|
const logLine = logAttachmentAttempted ? 'Log attachment was attempted but failed. Check the solver terminal log for the complete failure output.' : 'Logs were not attached because `--attach-logs` was not enabled.';
|
|
162
|
-
const actionSection = buildPrePullRequestFailureActionSection(reason);
|
|
154
|
+
const actionSection = failureActionSection || buildPrePullRequestFailureActionSection(reason);
|
|
163
155
|
|
|
164
156
|
return `## 🚨 ${SOLUTION_DRAFT_FAILED_MARKER}
|
|
165
157
|
|
|
@@ -191,7 +183,7 @@ const markNotificationPosted = ({ globalState, targetType }) => {
|
|
|
191
183
|
};
|
|
192
184
|
|
|
193
185
|
export async function notifyIssueAboutPrePullRequestFailure(options) {
|
|
194
|
-
const { code, reason, argv = {}, globalState = globalThis, $, log = async () => {}, getLogFile, shouldAttachLogs = false, attachLogToGitHub, sanitizeLogContent, rawCommand = null, postComment = postTrackedComment } = options;
|
|
186
|
+
const { code, reason, failureActionSection = null, argv = {}, globalState = globalThis, $, log = async () => {}, getLogFile, shouldAttachLogs = false, attachLogToGitHub, sanitizeLogContent, rawCommand = null, postComment = postTrackedComment } = options;
|
|
195
187
|
|
|
196
188
|
const target = resolvePreExitFailureNotificationTarget({ code, globalState });
|
|
197
189
|
if (!target) {
|
|
@@ -200,6 +192,7 @@ export async function notifyIssueAboutPrePullRequestFailure(options) {
|
|
|
200
192
|
|
|
201
193
|
const { owner, repo, issueNumber, prNumber, targetType, targetNumber } = target;
|
|
202
194
|
const targetLabel = targetType === 'pr' ? `pull request #${targetNumber}` : `issue #${targetNumber}`;
|
|
195
|
+
const actionSection = failureActionSection || buildPrePullRequestFailureActionSection(reason);
|
|
203
196
|
globalState.preExitFailureNotificationInProgress = true;
|
|
204
197
|
if (targetType === 'pr') {
|
|
205
198
|
globalState.pullRequestFailureNotificationInProgress = true;
|
|
@@ -223,7 +216,7 @@ export async function notifyIssueAboutPrePullRequestFailure(options) {
|
|
|
223
216
|
sanitizeLogContent,
|
|
224
217
|
verbose: argv.verbose,
|
|
225
218
|
errorMessage: `${errorPrefix}\n\nReason: ${reason || 'Unknown error'}`,
|
|
226
|
-
failureActionSection:
|
|
219
|
+
failureActionSection: actionSection,
|
|
227
220
|
argv,
|
|
228
221
|
requestedModel: argv.originalModel || argv.model,
|
|
229
222
|
tool: argv.tool || 'claude',
|
|
@@ -250,6 +243,7 @@ export async function notifyIssueAboutPrePullRequestFailure(options) {
|
|
|
250
243
|
argv,
|
|
251
244
|
rawCommand,
|
|
252
245
|
logAttachmentAttempted: shouldAttachLogs,
|
|
246
|
+
failureActionSection: actionSection,
|
|
253
247
|
})
|
|
254
248
|
: buildPrePullRequestFailureComment({
|
|
255
249
|
reason,
|
|
@@ -259,6 +253,7 @@ export async function notifyIssueAboutPrePullRequestFailure(options) {
|
|
|
259
253
|
argv,
|
|
260
254
|
rawCommand,
|
|
261
255
|
logAttachmentAttempted: shouldAttachLogs,
|
|
256
|
+
failureActionSection: actionSection,
|
|
262
257
|
});
|
|
263
258
|
const posted = await postComment({ $, owner, repo, targetNumber, body });
|
|
264
259
|
if (posted.ok) {
|