@link-assistant/hive-mind 2.13.4 → 2.13.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 +19 -0
- package/package.json +1 -1
- package/src/agent.lib.mjs +5 -1
- package/src/claude.lib.mjs +5 -158
- package/src/claude.session-tokens.lib.mjs +180 -0
- package/src/codex.diagnostics.lib.mjs +135 -0
- package/src/codex.lib.mjs +8 -121
- package/src/config.lib.mjs +9 -0
- package/src/gemini.lib.mjs +5 -1
- package/src/git-retry.lib.mjs +97 -0
- package/src/github-pr-idempotency.lib.mjs +83 -0
- package/src/github-rate-limit.lib.mjs +44 -41
- package/src/hive.mjs +8 -150
- package/src/hive.repository-fallback.lib.mjs +125 -0
- package/src/hive.startup-checks.lib.mjs +57 -0
- package/src/isolation-runner.lib.mjs +4 -277
- package/src/isolation-runner.parsers.lib.mjs +292 -0
- package/src/lib.mjs +79 -18
- package/src/opencode.lib.mjs +5 -1
- package/src/qwen.lib.mjs +5 -1
- package/src/session-monitor.lib.mjs +12 -272
- package/src/session-monitor.queries.lib.mjs +304 -0
- package/src/solve.auto-pr-push-sync.lib.mjs +176 -0
- package/src/solve.auto-pr.lib.mjs +40 -154
- package/src/solve.mjs +8 -158
- package/src/solve.mode.lib.mjs +191 -0
- package/src/telegram-solve-queue.lib.mjs +19 -272
- package/src/telegram-solve-queue.throttling.lib.mjs +323 -0
- package/src/transient-errors.lib.mjs +238 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-push GitHub synchronization for auto-PR creation.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from src/solve.auto-pr.lib.mjs (issue #2175) so that file stays
|
|
5
|
+
* under the 1350-line early-warning threshold that protects concurrent merges
|
|
6
|
+
* (#1593). Behaviour is unchanged; the collaborators that were closure bindings
|
|
7
|
+
* are now parameters.
|
|
8
|
+
*
|
|
9
|
+
* A push is accepted by git receive long before GitHub's compare/PR API can see
|
|
10
|
+
* the new commits, so `gh pr create` run immediately after a push fails with
|
|
11
|
+
* "No commits between branches". Both helpers here close that gap: the first
|
|
12
|
+
* polls the compare API until it reports commits ahead, the second confirms the
|
|
13
|
+
* branch itself is visible and re-pushes it (never forced) when it is not.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Poll GitHub's compare API until it reports commits ahead of the base branch.
|
|
18
|
+
*
|
|
19
|
+
* @param {object} deps
|
|
20
|
+
* @returns {Promise<{compareReady: boolean, targetBranchForCompare: string}>}
|
|
21
|
+
*/
|
|
22
|
+
export async function waitForCompareApiReady({ argv, defaultBranch, branchName, forkedRepo, owner, repo, issueNumber, log, formatAligned, $, isTransientCompareApiError, handleCompareApiNotReady }) {
|
|
23
|
+
// CRITICAL: Wait for GitHub to process the push before creating PR
|
|
24
|
+
// This prevents "No commits between branches" error
|
|
25
|
+
await log(' Waiting for GitHub to sync...');
|
|
26
|
+
|
|
27
|
+
// Use exponential backoff to wait for GitHub's compare API to see the commits
|
|
28
|
+
// This is essential because GitHub has multiple backend systems:
|
|
29
|
+
// - Git receive: Accepts push immediately
|
|
30
|
+
// - Branch API: Returns quickly from cache
|
|
31
|
+
// - Compare/PR API: May take longer to index commits
|
|
32
|
+
let compareReady = false;
|
|
33
|
+
let compareAttempts = 0;
|
|
34
|
+
const maxCompareAttempts = 5;
|
|
35
|
+
const targetBranchForCompare = argv.baseBranch || defaultBranch;
|
|
36
|
+
let compareResult; // Declare outside loop so it's accessible for error checking
|
|
37
|
+
|
|
38
|
+
while (!compareReady && compareAttempts < maxCompareAttempts) {
|
|
39
|
+
compareAttempts++;
|
|
40
|
+
const waitTime = Math.min(2000 * compareAttempts, 10000); // 2s, 4s, 6s, 8s, 10s
|
|
41
|
+
|
|
42
|
+
if (compareAttempts > 1) {
|
|
43
|
+
await log(` Retry ${compareAttempts}/${maxCompareAttempts}: Waiting ${waitTime}ms for GitHub to index commits...`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
47
|
+
|
|
48
|
+
// Check if GitHub's compare API can see commits between base and head
|
|
49
|
+
// This is the SAME API that gh pr create uses internally, so if this works,
|
|
50
|
+
// PR creation should work too
|
|
51
|
+
// For fork mode, we need to use forkUser:branchName format for the head
|
|
52
|
+
let headRef;
|
|
53
|
+
if (argv.fork && forkedRepo) {
|
|
54
|
+
const forkUser = forkedRepo.split('/')[0];
|
|
55
|
+
headRef = `${forkUser}:${branchName}`;
|
|
56
|
+
} else {
|
|
57
|
+
headRef = branchName;
|
|
58
|
+
}
|
|
59
|
+
compareResult = await $({
|
|
60
|
+
silent: true,
|
|
61
|
+
})`gh api repos/${owner}/${repo}/compare/${targetBranchForCompare}...${headRef} --paginate --jq '.ahead_by' 2>&1`;
|
|
62
|
+
|
|
63
|
+
if (compareResult.code === 0) {
|
|
64
|
+
const aheadBy = parseInt(compareResult.stdout.toString().trim(), 10);
|
|
65
|
+
if (argv.verbose) {
|
|
66
|
+
await log(` Compare API check: ${aheadBy} commit(s) ahead of ${targetBranchForCompare}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (aheadBy > 0) {
|
|
70
|
+
compareReady = true;
|
|
71
|
+
await log(` GitHub compare API ready: ${aheadBy} commit(s) found`);
|
|
72
|
+
} else {
|
|
73
|
+
await log(` ⚠️ GitHub compare API shows 0 commits ahead (attempt ${compareAttempts}/${maxCompareAttempts})`, { level: 'warning' });
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
// Issue #1829: surface compare-API failures in normal output (not
|
|
77
|
+
// only verbose) so the degraded-mode decision below is explainable
|
|
78
|
+
// from the logs. Build the text as a STRING — the command-stream
|
|
79
|
+
// result exposes stdout/stderr as Buffers, and the transient
|
|
80
|
+
// detectors call String.prototype.toLowerCase().
|
|
81
|
+
const errorText = `${compareResult.stdout?.toString?.() ?? ''}${compareResult.stderr?.toString?.() ?? ''}`.trim();
|
|
82
|
+
const firstLine =
|
|
83
|
+
errorText
|
|
84
|
+
.split('\n')
|
|
85
|
+
.map(s => s.trim())
|
|
86
|
+
.filter(Boolean)[0] || 'unknown';
|
|
87
|
+
const transientNote = isTransientCompareApiError(errorText) ? ' (transient server error)' : '';
|
|
88
|
+
await log(` ⚠️ GitHub compare API error${transientNote} (attempt ${compareAttempts}/${maxCompareAttempts}): ${firstLine}`, { level: 'warning' });
|
|
89
|
+
if (argv.verbose && errorText) {
|
|
90
|
+
await log(` Compare API full output: ${errorText}`, { verbose: true });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!compareReady) {
|
|
96
|
+
compareReady = await handleCompareApiNotReady({
|
|
97
|
+
argv,
|
|
98
|
+
forkedRepo,
|
|
99
|
+
owner,
|
|
100
|
+
repo,
|
|
101
|
+
issueNumber,
|
|
102
|
+
branchName,
|
|
103
|
+
targetBranchForCompare,
|
|
104
|
+
maxCompareAttempts,
|
|
105
|
+
compareResult,
|
|
106
|
+
log,
|
|
107
|
+
formatAligned,
|
|
108
|
+
$,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { compareReady, targetBranchForCompare };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Confirm the pushed branch is visible on GitHub, re-pushing it (never forced)
|
|
117
|
+
* when it is not.
|
|
118
|
+
*
|
|
119
|
+
* @param {object} deps
|
|
120
|
+
* @returns {Promise<void>}
|
|
121
|
+
*/
|
|
122
|
+
export async function verifyBranchOnGitHub({ argv, tempDir, branchName, forkedRepo, owner, repo, log, $ }) {
|
|
123
|
+
// Verify the push actually worked by checking GitHub API
|
|
124
|
+
// When using fork mode, check the fork repository; otherwise check the original repository
|
|
125
|
+
const repoToCheck = argv.fork && forkedRepo ? forkedRepo : `${owner}/${repo}`;
|
|
126
|
+
const branchCheckResult = await $({
|
|
127
|
+
silent: true,
|
|
128
|
+
})`gh api repos/${repoToCheck}/branches/${branchName} --jq .name 2>&1`;
|
|
129
|
+
if (branchCheckResult.code === 0 && branchCheckResult.stdout.toString().trim() === branchName) {
|
|
130
|
+
await log(` Branch verified on GitHub: ${branchName}`);
|
|
131
|
+
|
|
132
|
+
// Get the commit SHA from GitHub
|
|
133
|
+
const shaCheckResult = await $({
|
|
134
|
+
silent: true,
|
|
135
|
+
})`gh api repos/${repoToCheck}/branches/${branchName} --jq .commit.sha 2>&1`;
|
|
136
|
+
if (shaCheckResult.code === 0) {
|
|
137
|
+
const remoteSha = shaCheckResult.stdout.toString().trim();
|
|
138
|
+
await log(` Remote commit SHA: ${remoteSha.substring(0, 7)}...`);
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
await log(' Warning: Branch not found on GitHub!');
|
|
142
|
+
await log(' This will cause PR creation to fail.');
|
|
143
|
+
|
|
144
|
+
if (argv.verbose) {
|
|
145
|
+
await log(` Branch check result: ${branchCheckResult.stdout || branchCheckResult.stderr || 'empty'}`);
|
|
146
|
+
|
|
147
|
+
// Show all branches on GitHub
|
|
148
|
+
const allBranchesResult = await $({
|
|
149
|
+
silent: true,
|
|
150
|
+
})`gh api repos/${repoToCheck}/branches --paginate --jq '.[].name' 2>&1`;
|
|
151
|
+
if (allBranchesResult.code === 0) {
|
|
152
|
+
await log(` All GitHub branches: ${allBranchesResult.stdout.toString().split('\n').slice(0, 5).join(', ')}...`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Try one more push with explicit ref (without force)
|
|
157
|
+
await log(' Attempting explicit push...');
|
|
158
|
+
const explicitPushCmd = `git push origin HEAD:refs/heads/${branchName}`;
|
|
159
|
+
if (argv.verbose) {
|
|
160
|
+
await log(` Command: ${explicitPushCmd}`);
|
|
161
|
+
}
|
|
162
|
+
const explicitPushResult = await $`cd ${tempDir} && ${explicitPushCmd} 2>&1`;
|
|
163
|
+
if (explicitPushResult.code === 0) {
|
|
164
|
+
await log(' Explicit push completed');
|
|
165
|
+
if (argv.verbose && explicitPushResult.stdout) {
|
|
166
|
+
await log(` Output: ${explicitPushResult.stdout.toString().trim()}`);
|
|
167
|
+
}
|
|
168
|
+
// Wait a bit more for GitHub to process
|
|
169
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
170
|
+
} else {
|
|
171
|
+
await log(' ERROR: Cannot push to GitHub!');
|
|
172
|
+
await log(` Error: ${explicitPushResult.stderr || explicitPushResult.stdout || 'Unknown'}`);
|
|
173
|
+
await log(' Force push is not allowed to preserve history');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -8,11 +8,13 @@ import { classifyIssueLinkStatus, buildNonDefaultBranchExplanation } from './git
|
|
|
8
8
|
import { handleRejectedPushForAutoPr, synchronizeExistingIssueBranchBeforeAutoPrCreation } from './solve.branch-divergence.lib.mjs';
|
|
9
9
|
import { emitForkAwareDiagnostic } from './solve.auto-pr-fork-diagnostic.lib.mjs';
|
|
10
10
|
import { handleCompareApiNotReady } from './solve.auto-pr-compare-readiness.lib.mjs'; // Issue #1829: decides whether a failed compare-API readiness poll is fatal (fork mismatch / 0 commits) or a transient diff-render failure to degrade past.
|
|
11
|
+
import { waitForCompareApiReady, verifyBranchOnGitHub } from './solve.auto-pr-push-sync.lib.mjs'; // Issue #2175: extracted to keep this file under the 1350-line warning threshold.
|
|
11
12
|
|
|
12
13
|
import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry, execGhWithRetry, isTransientCompareApiError } 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. Issue #1829: isTransientCompareApiError lets the compare-API readiness gate degrade gracefully on transient diff-render failures.
|
|
13
14
|
import { quietProbe } from './quiet-probe.lib.mjs'; // issue #2130: keep read-only probe payloads out of the attached log
|
|
14
15
|
import { stagePlaceholderFileOrExplain, explainNothingStagedAndThrow } from './solve.auto-pr-placeholder.lib.mjs'; // Issue #1825: handles the seed placeholder when the target repo gitignores it.
|
|
15
16
|
import { sanitizeForPublication, writeSanitizedPublicationFile } from './token-sanitization.lib.mjs';
|
|
17
|
+
import { isPullRequestAlreadyExistsError, findExistingPullRequestUrl } from './github-pr-idempotency.lib.mjs'; // Issue #2168: a retried `gh pr create` must not fail because the first (5xx'd) attempt already created the PR.
|
|
16
18
|
|
|
17
19
|
export async function handleAutoPrCreation({ argv, tempDir, branchName, issueNumber, owner, repo, defaultBranch, forkedRepo, isContinueMode, prNumber, log, formatAligned, $, reportError, path, fs }) {
|
|
18
20
|
// Skip auto-PR creation if:
|
|
@@ -547,148 +549,11 @@ Proceed.
|
|
|
547
549
|
await log(` Push output: ${pushResult.stdout.toString().trim()}`, { verbose: true });
|
|
548
550
|
}
|
|
549
551
|
|
|
550
|
-
//
|
|
551
|
-
//
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
// This is essential because GitHub has multiple backend systems:
|
|
556
|
-
// - Git receive: Accepts push immediately
|
|
557
|
-
// - Branch API: Returns quickly from cache
|
|
558
|
-
// - Compare/PR API: May take longer to index commits
|
|
559
|
-
let compareReady = false;
|
|
560
|
-
let compareAttempts = 0;
|
|
561
|
-
const maxCompareAttempts = 5;
|
|
562
|
-
const targetBranchForCompare = argv.baseBranch || defaultBranch;
|
|
563
|
-
let compareResult; // Declare outside loop so it's accessible for error checking
|
|
564
|
-
|
|
565
|
-
while (!compareReady && compareAttempts < maxCompareAttempts) {
|
|
566
|
-
compareAttempts++;
|
|
567
|
-
const waitTime = Math.min(2000 * compareAttempts, 10000); // 2s, 4s, 6s, 8s, 10s
|
|
568
|
-
|
|
569
|
-
if (compareAttempts > 1) {
|
|
570
|
-
await log(` Retry ${compareAttempts}/${maxCompareAttempts}: Waiting ${waitTime}ms for GitHub to index commits...`);
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
574
|
-
|
|
575
|
-
// Check if GitHub's compare API can see commits between base and head
|
|
576
|
-
// This is the SAME API that gh pr create uses internally, so if this works,
|
|
577
|
-
// PR creation should work too
|
|
578
|
-
// For fork mode, we need to use forkUser:branchName format for the head
|
|
579
|
-
let headRef;
|
|
580
|
-
if (argv.fork && forkedRepo) {
|
|
581
|
-
const forkUser = forkedRepo.split('/')[0];
|
|
582
|
-
headRef = `${forkUser}:${branchName}`;
|
|
583
|
-
} else {
|
|
584
|
-
headRef = branchName;
|
|
585
|
-
}
|
|
586
|
-
compareResult = await $({
|
|
587
|
-
silent: true,
|
|
588
|
-
})`gh api repos/${owner}/${repo}/compare/${targetBranchForCompare}...${headRef} --paginate --jq '.ahead_by' 2>&1`;
|
|
589
|
-
|
|
590
|
-
if (compareResult.code === 0) {
|
|
591
|
-
const aheadBy = parseInt(compareResult.stdout.toString().trim(), 10);
|
|
592
|
-
if (argv.verbose) {
|
|
593
|
-
await log(` Compare API check: ${aheadBy} commit(s) ahead of ${targetBranchForCompare}`);
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
if (aheadBy > 0) {
|
|
597
|
-
compareReady = true;
|
|
598
|
-
await log(` GitHub compare API ready: ${aheadBy} commit(s) found`);
|
|
599
|
-
} else {
|
|
600
|
-
await log(` ⚠️ GitHub compare API shows 0 commits ahead (attempt ${compareAttempts}/${maxCompareAttempts})`, { level: 'warning' });
|
|
601
|
-
}
|
|
602
|
-
} else {
|
|
603
|
-
// Issue #1829: surface compare-API failures in normal output (not
|
|
604
|
-
// only verbose) so the degraded-mode decision below is explainable
|
|
605
|
-
// from the logs. Build the text as a STRING — the command-stream
|
|
606
|
-
// result exposes stdout/stderr as Buffers, and the transient
|
|
607
|
-
// detectors call String.prototype.toLowerCase().
|
|
608
|
-
const errorText = `${compareResult.stdout?.toString?.() ?? ''}${compareResult.stderr?.toString?.() ?? ''}`.trim();
|
|
609
|
-
const firstLine =
|
|
610
|
-
errorText
|
|
611
|
-
.split('\n')
|
|
612
|
-
.map(s => s.trim())
|
|
613
|
-
.filter(Boolean)[0] || 'unknown';
|
|
614
|
-
const transientNote = isTransientCompareApiError(errorText) ? ' (transient server error)' : '';
|
|
615
|
-
await log(` ⚠️ GitHub compare API error${transientNote} (attempt ${compareAttempts}/${maxCompareAttempts}): ${firstLine}`, { level: 'warning' });
|
|
616
|
-
if (argv.verbose && errorText) {
|
|
617
|
-
await log(` Compare API full output: ${errorText}`, { verbose: true });
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
if (!compareReady) {
|
|
623
|
-
compareReady = await handleCompareApiNotReady({
|
|
624
|
-
argv,
|
|
625
|
-
forkedRepo,
|
|
626
|
-
owner,
|
|
627
|
-
repo,
|
|
628
|
-
issueNumber,
|
|
629
|
-
branchName,
|
|
630
|
-
targetBranchForCompare,
|
|
631
|
-
maxCompareAttempts,
|
|
632
|
-
compareResult,
|
|
633
|
-
log,
|
|
634
|
-
formatAligned,
|
|
635
|
-
$,
|
|
636
|
-
});
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
// Verify the push actually worked by checking GitHub API
|
|
640
|
-
// When using fork mode, check the fork repository; otherwise check the original repository
|
|
641
|
-
const repoToCheck = argv.fork && forkedRepo ? forkedRepo : `${owner}/${repo}`;
|
|
642
|
-
const branchCheckResult = await $({
|
|
643
|
-
silent: true,
|
|
644
|
-
})`gh api repos/${repoToCheck}/branches/${branchName} --jq .name 2>&1`;
|
|
645
|
-
if (branchCheckResult.code === 0 && branchCheckResult.stdout.toString().trim() === branchName) {
|
|
646
|
-
await log(` Branch verified on GitHub: ${branchName}`);
|
|
647
|
-
|
|
648
|
-
// Get the commit SHA from GitHub
|
|
649
|
-
const shaCheckResult = await $({
|
|
650
|
-
silent: true,
|
|
651
|
-
})`gh api repos/${repoToCheck}/branches/${branchName} --jq .commit.sha 2>&1`;
|
|
652
|
-
if (shaCheckResult.code === 0) {
|
|
653
|
-
const remoteSha = shaCheckResult.stdout.toString().trim();
|
|
654
|
-
await log(` Remote commit SHA: ${remoteSha.substring(0, 7)}...`);
|
|
655
|
-
}
|
|
656
|
-
} else {
|
|
657
|
-
await log(' Warning: Branch not found on GitHub!');
|
|
658
|
-
await log(' This will cause PR creation to fail.');
|
|
659
|
-
|
|
660
|
-
if (argv.verbose) {
|
|
661
|
-
await log(` Branch check result: ${branchCheckResult.stdout || branchCheckResult.stderr || 'empty'}`);
|
|
662
|
-
|
|
663
|
-
// Show all branches on GitHub
|
|
664
|
-
const allBranchesResult = await $({
|
|
665
|
-
silent: true,
|
|
666
|
-
})`gh api repos/${repoToCheck}/branches --paginate --jq '.[].name' 2>&1`;
|
|
667
|
-
if (allBranchesResult.code === 0) {
|
|
668
|
-
await log(` All GitHub branches: ${allBranchesResult.stdout.toString().split('\n').slice(0, 5).join(', ')}...`);
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
// Try one more push with explicit ref (without force)
|
|
673
|
-
await log(' Attempting explicit push...');
|
|
674
|
-
const explicitPushCmd = `git push origin HEAD:refs/heads/${branchName}`;
|
|
675
|
-
if (argv.verbose) {
|
|
676
|
-
await log(` Command: ${explicitPushCmd}`);
|
|
677
|
-
}
|
|
678
|
-
const explicitPushResult = await $`cd ${tempDir} && ${explicitPushCmd} 2>&1`;
|
|
679
|
-
if (explicitPushResult.code === 0) {
|
|
680
|
-
await log(' Explicit push completed');
|
|
681
|
-
if (argv.verbose && explicitPushResult.stdout) {
|
|
682
|
-
await log(` Output: ${explicitPushResult.stdout.toString().trim()}`);
|
|
683
|
-
}
|
|
684
|
-
// Wait a bit more for GitHub to process
|
|
685
|
-
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
686
|
-
} else {
|
|
687
|
-
await log(' ERROR: Cannot push to GitHub!');
|
|
688
|
-
await log(` Error: ${explicitPushResult.stderr || explicitPushResult.stdout || 'Unknown'}`);
|
|
689
|
-
await log(' Force push is not allowed to preserve history');
|
|
690
|
-
}
|
|
691
|
-
}
|
|
552
|
+
// Issue #2175: the post-push GitHub synchronization (compare-API readiness
|
|
553
|
+
// poll + branch visibility check) lives in solve.auto-pr-push-sync.lib.mjs
|
|
554
|
+
// to keep this file under the 1350-line warning threshold.
|
|
555
|
+
await waitForCompareApiReady({ argv, defaultBranch, branchName, forkedRepo, owner, repo, issueNumber, log, formatAligned, $, isTransientCompareApiError, handleCompareApiNotReady });
|
|
556
|
+
await verifyBranchOnGitHub({ argv, tempDir, branchName, forkedRepo, owner, repo, log, $ });
|
|
692
557
|
|
|
693
558
|
// Get issue title for PR title
|
|
694
559
|
await log(formatAligned('📋', 'Getting issue:', 'Title from GitHub...'), { verbose: true });
|
|
@@ -950,16 +815,41 @@ ${prBody}`,
|
|
|
950
815
|
|
|
951
816
|
const prCreateExecOptions = { encoding: 'utf8', cwd: tempDir, env: process.env };
|
|
952
817
|
const prCreateRetryLogger = msg => log(msg, { level: 'warn' });
|
|
818
|
+
const prHeadRef = argv.fork && forkedRepo ? `${forkedRepo.split('/')[0]}:${branchName}` : branchName;
|
|
819
|
+
|
|
820
|
+
// Issue #2168: `execGhWithRetry` now retries GitHub 5xx / GraphQL
|
|
821
|
+
// internal errors. A mutation that GitHub committed but failed to
|
|
822
|
+
// acknowledge would make the retry report "a pull request already
|
|
823
|
+
// exists" — which is success, not failure. Resolve the existing PR
|
|
824
|
+
// instead of aborting the session.
|
|
825
|
+
const runPrCreate = async (cmd, label) => {
|
|
826
|
+
try {
|
|
827
|
+
const result = await execGhWithRetry(cmd, {
|
|
828
|
+
execOptions: prCreateExecOptions,
|
|
829
|
+
label,
|
|
830
|
+
log: prCreateRetryLogger,
|
|
831
|
+
});
|
|
832
|
+
return { stdout: result.stdout, stderr: result.stderr || '' };
|
|
833
|
+
} catch (error) {
|
|
834
|
+
if (!isPullRequestAlreadyExistsError(error)) throw error;
|
|
835
|
+
await log(` ${label}: GitHub reports a pull request already exists for ${prHeadRef} — treating the retried creation as already applied.`, { level: 'warn' });
|
|
836
|
+
const existingUrl = await findExistingPullRequestUrl({
|
|
837
|
+
owner,
|
|
838
|
+
repo,
|
|
839
|
+
headRef: prHeadRef,
|
|
840
|
+
execGh: (lookupCommand, lookupOptions) => execGhWithRetry(lookupCommand, { execOptions: prCreateExecOptions, log: prCreateRetryLogger, ...lookupOptions }),
|
|
841
|
+
log: msg => log(msg),
|
|
842
|
+
});
|
|
843
|
+
if (!existingUrl) throw error;
|
|
844
|
+
return { stdout: existingUrl, stderr: '' };
|
|
845
|
+
}
|
|
846
|
+
};
|
|
953
847
|
|
|
954
848
|
// Try to create PR with assignee first (if specified)
|
|
955
849
|
try {
|
|
956
|
-
const result = await
|
|
957
|
-
execOptions: prCreateExecOptions,
|
|
958
|
-
label: 'gh pr create',
|
|
959
|
-
log: prCreateRetryLogger,
|
|
960
|
-
});
|
|
850
|
+
const result = await runPrCreate(command, 'gh pr create');
|
|
961
851
|
output = result.stdout;
|
|
962
|
-
prCreateStderr = result.stderr
|
|
852
|
+
prCreateStderr = result.stderr;
|
|
963
853
|
} catch (firstError) {
|
|
964
854
|
// Check if the error is specifically about assignee validation
|
|
965
855
|
const errorMsg = firstError.message || '';
|
|
@@ -985,13 +875,9 @@ ${prBody}`,
|
|
|
985
875
|
}
|
|
986
876
|
|
|
987
877
|
// Retry without assignee - if this fails, let the error propagate to outer catch
|
|
988
|
-
const retryResult = await
|
|
989
|
-
execOptions: prCreateExecOptions,
|
|
990
|
-
label: 'gh pr create (no assignee)',
|
|
991
|
-
log: prCreateRetryLogger,
|
|
992
|
-
});
|
|
878
|
+
const retryResult = await runPrCreate(command, 'gh pr create (no assignee)');
|
|
993
879
|
output = retryResult.stdout;
|
|
994
|
-
prCreateStderr = retryResult.stderr
|
|
880
|
+
prCreateStderr = retryResult.stderr;
|
|
995
881
|
} else {
|
|
996
882
|
// Not an assignee error, re-throw the original error
|
|
997
883
|
throw firstError;
|
package/src/solve.mjs
CHANGED
|
@@ -291,164 +291,14 @@ if (argv.autoCleanup === undefined) {
|
|
|
291
291
|
}
|
|
292
292
|
// Issue #1716: When the upstream repository is private and the user has direct write access, fork-based workflows should be skipped — even if the existing PR was originally created from a fork. Forks of private repositories often become inaccessible (renamed, deleted, parent re-private'd) and there's no reason to use them when we can push branches and PRs to the upstream repo.
|
|
293
293
|
const skipForkForPrivateUpstream = !isRepoPublic && !argv.fork && hasWriteAccess;
|
|
294
|
-
// Determine mode and get issue details
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
let
|
|
302
|
-
let isContinueMode = false;
|
|
303
|
-
// Auto-continue logic: check for existing PRs if --auto-continue is enabled
|
|
304
|
-
const autoContinueResult = await processAutoContinueForIssue(argv, isIssueUrl, urlNumber, owner, repo);
|
|
305
|
-
if (autoContinueResult.isContinueMode) {
|
|
306
|
-
isContinueMode = true;
|
|
307
|
-
prNumber = autoContinueResult.prNumber;
|
|
308
|
-
prBranch = autoContinueResult.prBranch;
|
|
309
|
-
issueNumber = autoContinueResult.issueNumber;
|
|
310
|
-
// Only check PR details if we have a PR number
|
|
311
|
-
if (prNumber) {
|
|
312
|
-
// Store PR info globally for error handlers
|
|
313
|
-
global.createdPR = { number: prNumber };
|
|
314
|
-
// Check if PR is from a fork and get fork owner, merge status, and PR state
|
|
315
|
-
if (argv.verbose) {
|
|
316
|
-
await log(' Checking if PR is from a fork...', { verbose: true });
|
|
317
|
-
}
|
|
318
|
-
try {
|
|
319
|
-
const prCheckResult = await $`gh pr view ${prNumber} --repo ${owner}/${repo} --json headRepositoryOwner,headRepository,mergeStateStatus,state`;
|
|
320
|
-
if (prCheckResult.code === 0) {
|
|
321
|
-
const prCheckData = JSON.parse(prCheckResult.stdout.toString());
|
|
322
|
-
// Extract merge status and PR state
|
|
323
|
-
mergeStateStatus = prCheckData.mergeStateStatus;
|
|
324
|
-
prState = prCheckData.state;
|
|
325
|
-
if (argv.verbose) {
|
|
326
|
-
await log(` PR state: ${prState || 'UNKNOWN'}`, { verbose: true });
|
|
327
|
-
await log(` Merge status: ${mergeStateStatus || 'UNKNOWN'}`, { verbose: true });
|
|
328
|
-
}
|
|
329
|
-
if (prCheckData.headRepositoryOwner && prCheckData.headRepositoryOwner.login !== owner) {
|
|
330
|
-
const detectedForkOwner = prCheckData.headRepositoryOwner.login;
|
|
331
|
-
const detectedForkRepoName = prCheckData.headRepository && prCheckData.headRepository.name ? prCheckData.headRepository.name : null;
|
|
332
|
-
// Issue #1716: Skip fork mode for private upstream repos with write access.
|
|
333
|
-
if (skipForkForPrivateUpstream) {
|
|
334
|
-
await log(`🔒 Detected fork PR from ${detectedForkOwner}/${detectedForkRepoName || repo}, but upstream ${owner}/${repo} is private and you have write access.`);
|
|
335
|
-
await log(' Working directly on the private upstream repository (Issue #1716).');
|
|
336
|
-
} else {
|
|
337
|
-
forkOwner = detectedForkOwner;
|
|
338
|
-
// Get actual fork repository name (may be prefixed) and store for use in setupRepository
|
|
339
|
-
forkRepoName = detectedForkRepoName;
|
|
340
|
-
await log(`🍴 Detected fork PR from ${forkOwner}/${forkRepoName || repo}`);
|
|
341
|
-
if (argv.verbose) {
|
|
342
|
-
await log(` Fork owner: ${forkOwner}`, { verbose: true });
|
|
343
|
-
await log(' Will clone fork repository for continue mode', { verbose: true });
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
// Check if maintainer can push to the fork when --allow-to-push-to-contributors-pull-requests-as-maintainer is enabled
|
|
347
|
-
if (forkOwner && argv.allowToPushToContributorsPullRequestsAsMaintainer && argv.autoFork) {
|
|
348
|
-
await handleMaintainerForkAccess({ owner, repo, prNumber });
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
} catch (forkCheckError) {
|
|
353
|
-
if (argv.verbose) {
|
|
354
|
-
await log(` Warning: Could not check fork status: ${forkCheckError.message}`, { verbose: true });
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
} else {
|
|
358
|
-
// We have a branch but no PR - we'll use the existing branch and create a PR later
|
|
359
|
-
await log(`🔄 Using existing branch: ${prBranch} (no PR yet - will create one)`);
|
|
360
|
-
await log(' This branch was created by an earlier run; this run is reusing it rather than creating a fresh branch.');
|
|
361
|
-
if (argv.verbose) {
|
|
362
|
-
await log(' Branch will be checked out and PR will be created during auto-PR creation phase', {
|
|
363
|
-
verbose: true,
|
|
364
|
-
});
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
} else if (isIssueUrl) {
|
|
368
|
-
issueNumber = autoContinueResult.issueNumber || urlNumber;
|
|
369
|
-
}
|
|
370
|
-
if (isPrUrl) {
|
|
371
|
-
isContinueMode = true;
|
|
372
|
-
prNumber = urlNumber;
|
|
373
|
-
// Store PR info globally for error handlers
|
|
374
|
-
global.createdPR = { number: prNumber, url: issueUrl };
|
|
375
|
-
await log(`🔄 Continue mode: Working with PR #${prNumber}`);
|
|
376
|
-
if (argv.verbose) {
|
|
377
|
-
await log(' Continue mode activated: PR URL provided directly', { verbose: true });
|
|
378
|
-
await log(` PR Number set to: ${prNumber}`, { verbose: true });
|
|
379
|
-
await log(' Will fetch PR details and linked issue', { verbose: true });
|
|
380
|
-
}
|
|
381
|
-
// Get PR details to find the linked issue and branch
|
|
382
|
-
try {
|
|
383
|
-
const prResult = await githubLib.ghPrView({
|
|
384
|
-
prNumber,
|
|
385
|
-
owner,
|
|
386
|
-
repo,
|
|
387
|
-
jsonFields: 'headRefName,body,number,mergeStateStatus,state,headRepositoryOwner,headRepository',
|
|
388
|
-
});
|
|
389
|
-
if (prResult.code !== 0 || !prResult.data) {
|
|
390
|
-
await log('Error: Failed to get PR details', { level: 'error' });
|
|
391
|
-
if (prResult.output.includes('Could not resolve to a PullRequest')) {
|
|
392
|
-
await githubLib.handlePRNotFoundError({ prNumber, owner, repo, argv, shouldAttachLogs });
|
|
393
|
-
} else {
|
|
394
|
-
await log(`Error: ${prResult.stderr || 'Unknown error'}`, { level: 'error' });
|
|
395
|
-
}
|
|
396
|
-
await safeExit(1, 'Failed to get PR details');
|
|
397
|
-
}
|
|
398
|
-
const prData = prResult.data;
|
|
399
|
-
prBranch = prData.headRefName;
|
|
400
|
-
mergeStateStatus = prData.mergeStateStatus;
|
|
401
|
-
prState = prData.state;
|
|
402
|
-
// Check if this is a fork PR
|
|
403
|
-
if (prData.headRepositoryOwner && prData.headRepositoryOwner.login !== owner) {
|
|
404
|
-
const detectedForkOwner = prData.headRepositoryOwner.login;
|
|
405
|
-
const detectedForkRepoName = prData.headRepository && prData.headRepository.name ? prData.headRepository.name : null;
|
|
406
|
-
// Issue #1716: Skip fork mode for private upstream repos with write access.
|
|
407
|
-
if (skipForkForPrivateUpstream) {
|
|
408
|
-
await log(`🔒 Detected fork PR from ${detectedForkOwner}/${detectedForkRepoName || repo}, but upstream ${owner}/${repo} is private and you have write access.`);
|
|
409
|
-
await log(' Working directly on the private upstream repository (Issue #1716).');
|
|
410
|
-
} else {
|
|
411
|
-
forkOwner = detectedForkOwner;
|
|
412
|
-
// Get actual fork repository name and store for use in setupRepository
|
|
413
|
-
forkRepoName = detectedForkRepoName;
|
|
414
|
-
await log(`🍴 Detected fork PR from ${forkOwner}/${forkRepoName || repo}`);
|
|
415
|
-
if (argv.verbose) {
|
|
416
|
-
await log(` Fork owner: ${forkOwner}`, { verbose: true });
|
|
417
|
-
await log(' Will clone fork repository for continue mode', { verbose: true });
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
// Check if maintainer can push to the fork when --allow-to-push-to-contributors-pull-requests-as-maintainer is enabled
|
|
421
|
-
if (forkOwner && argv.allowToPushToContributorsPullRequestsAsMaintainer && argv.autoFork) {
|
|
422
|
-
await handleMaintainerForkAccess({ owner, repo, prNumber });
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
await log(`📝 PR branch: ${prBranch}`);
|
|
426
|
-
const prBody = prData.body || '';
|
|
427
|
-
const extractedIssueNumber = extractLinkedIssueNumber(prBody);
|
|
428
|
-
if (extractedIssueNumber) {
|
|
429
|
-
issueNumber = extractedIssueNumber;
|
|
430
|
-
await log(`🔗 Found linked issue #${issueNumber}`);
|
|
431
|
-
} else {
|
|
432
|
-
// If no linked issue found, we can still continue but warn
|
|
433
|
-
await log('⚠️ Warning: No linked issue found in PR body', { level: 'warning' });
|
|
434
|
-
await log(' The PR should contain "Fixes #123" or similar to link an issue', { level: 'warning' });
|
|
435
|
-
// Set issueNumber to PR number as fallback
|
|
436
|
-
issueNumber = prNumber;
|
|
437
|
-
}
|
|
438
|
-
} catch (error) {
|
|
439
|
-
reportError(error, {
|
|
440
|
-
context: 'pr_processing',
|
|
441
|
-
prNumber,
|
|
442
|
-
operation: 'process_pull_request',
|
|
443
|
-
});
|
|
444
|
-
await log(`Error: Failed to process PR: ${cleanErrorMessage(error)}`, { level: 'error' });
|
|
445
|
-
await safeExit(1, 'Failed to process PR');
|
|
446
|
-
}
|
|
447
|
-
} else {
|
|
448
|
-
// Traditional issue mode
|
|
449
|
-
issueNumber = urlNumber;
|
|
450
|
-
await log(`📝 Issue mode: Working with issue #${issueNumber}`);
|
|
451
|
-
}
|
|
294
|
+
// Determine mode and get issue details.
|
|
295
|
+
// Issue #2175: the mode/fork/linked-issue resolution lives in solve.mode.lib.mjs
|
|
296
|
+
// so this file stays under the 1350-line early-warning threshold (issue #1593).
|
|
297
|
+
const solveMode = await import('./solve.mode.lib.mjs');
|
|
298
|
+
const resolvedMode = await solveMode.resolveSolveMode({ argv, owner, repo, urlNumber, issueUrl, isIssueUrl, isPrUrl, skipForkForPrivateUpstream, shouldAttachLogs, log, safeExit, githubLib, processAutoContinueForIssue, handleMaintainerForkAccess, extractLinkedIssueNumber, reportError, cleanErrorMessage });
|
|
299
|
+
const { issueNumber, prBranch, mergeStateStatus, prState, forkOwner, forkRepoName, isContinueMode } = resolvedMode;
|
|
300
|
+
// `prNumber` is reassigned below when auto-PR creation opens the pull request.
|
|
301
|
+
let prNumber = resolvedMode.prNumber;
|
|
452
302
|
// Issues #1212, #1462: Store issueNumber globally for error handlers (attach failure logs to issue when no PR exists)
|
|
453
303
|
global.issueNumber = issueNumber;
|
|
454
304
|
// Issues #1595 and #1596: detect the issue type so analysis and logging prompts use bug vs feature/task wording.
|