@link-assistant/hive-mind 1.73.2 → 1.73.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/package.json +1 -1
- package/src/config.lib.mjs +33 -13
- package/src/github-rate-limit.lib.mjs +39 -1
- package/src/models/index.mjs +13 -7
- package/src/solve.auto-pr-compare-readiness.lib.mjs +280 -0
- package/src/solve.auto-pr.lib.mjs +31 -181
- package/src/solve.config.lib.mjs +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.73.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- bfdc3fe: Add support for Claude Opus 4.8 (issue #1832). The bare `opus` alias for the `claude` tool now resolves to `claude-opus-4-8`, and the explicit `opus-4-8` / `claude-opus-4-8` aliases (plus their `[1m]` variants for the 1M-token context window) are accepted everywhere existing Opus aliases are. All earlier aliases keep working unchanged — `opus-4-7` / `claude-opus-4-7`, `opus-4-6` / `claude-opus-4-6`, `opus-4-5`, `sonnet`, `haiku`, and `opusplan` continue to map to the same model IDs as before. The `--fallback-model` default chain for the `claude` tool extends to `opus`/`opus-4-8` → `opus-4-7` → `opus-4-6`; the `--think xhigh`/`max` levels remain supported (4.8 shares Opus 4.7's effort surface and adaptive-only thinking, so Claude Code never emits `MAX_THINKING_TOKENS` for it); `--show-thinking-content` still opts into thinking output on 4.8 the same way it does on 4.7. Adds the deep case study under `docs/case-studies/issue-1832/` (covering the requirements, solution plan, and verification matrix) and `tests/test-opus-48-model-support.mjs` (175 assertions) alongside the existing 4.7 regression test. The English `docs/CONFIGURATION.md` row text is left unchanged in this PR to keep all four language siblings in sync; the case study is the authoritative user-facing documentation for the 4.8 behavior.
|
|
8
|
+
|
|
9
|
+
## 1.73.3
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- a3eab04: Fix `solve` aborting with `GitHub compare API not ready - cannot create PR safely` when the compare/diff endpoint returns a transient HTTP 500 (`this diff is temporarily unavailable due to heavy server load`, code `not_available`). The auto-PR readiness gate polled `/repos/{owner}/{repo}/compare/{base}...{head}` to confirm the pushed commits were visible, but GitHub renders that diff lazily and returns 500 under load even though the branch and commits were already pushed and `gh pr create` (which does not render the full diff) would have succeeded. A new `isTransientCompareApiError` detector recognises the "heavy server load" / `not_available` 500 and the standard 5xx gateway codes (but NOT 404 fork mismatch or a literal `0`), and the gate now degrades gracefully — marking the compare ready and proceeding to PR creation, still guarded by branch verification and the local `git rev-list` commit check. The fork-404 mismatch and genuine 0-commits paths remain fatal.
|
|
14
|
+
|
|
3
15
|
## 1.73.2
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/package.json
CHANGED
package/src/config.lib.mjs
CHANGED
|
@@ -177,7 +177,7 @@ export const DEFAULT_MAX_THINKING_BUDGET = 31999;
|
|
|
177
177
|
export const DEFAULT_MAX_THINKING_BUDGET_OPUS_46 = parseIntWithDefault('HIVE_MIND_MAX_THINKING_BUDGET_OPUS_46', 31999);
|
|
178
178
|
|
|
179
179
|
/**
|
|
180
|
-
* Check if a model is Opus 4.6 or later (Issue #1221, updated in Issue #1238)
|
|
180
|
+
* Check if a model is Opus 4.6 or later (Issue #1221, updated in Issue #1238, Issue #1832)
|
|
181
181
|
* @param {string} model - The model name or ID
|
|
182
182
|
* @returns {boolean} True if the model is Opus 4.6 or later
|
|
183
183
|
*/
|
|
@@ -185,22 +185,22 @@ export const isOpus46OrLater = model => {
|
|
|
185
185
|
if (!model) return false;
|
|
186
186
|
const normalizedModel = model.toLowerCase();
|
|
187
187
|
// Check for explicit opus-4-6 or later versions, or opusplan (Issue #1223)
|
|
188
|
-
// Note: The 'opus' alias now maps to Opus 4.
|
|
188
|
+
// Note: The 'opus' alias now maps to Opus 4.8 (Issue #1832), so we also check for the alias directly
|
|
189
189
|
// opusplan uses Opus for planning, so it should get Opus-level settings
|
|
190
|
-
return normalizedModel === 'opus' || normalizedModel === 'opusplan' || normalizedModel.includes('opus-4-6') || normalizedModel.includes('opus-4-7') || normalizedModel.includes('opus-5');
|
|
190
|
+
return normalizedModel === 'opus' || normalizedModel === 'opusplan' || normalizedModel.includes('opus-4-6') || normalizedModel.includes('opus-4-7') || normalizedModel.includes('opus-4-8') || normalizedModel.includes('opus-5');
|
|
191
191
|
};
|
|
192
192
|
|
|
193
193
|
const isOpus47 = model => {
|
|
194
194
|
if (!model) return false;
|
|
195
195
|
const normalizedModel = model.toLowerCase();
|
|
196
|
-
// 'opus' alias now maps to Opus 4.
|
|
196
|
+
// 'opus' alias now maps to Opus 4.8 (Issue #1832), which inherits 4.7 behaviour
|
|
197
197
|
// opusplan uses Opus for planning, so it gets Opus-level settings
|
|
198
|
-
return normalizedModel === 'opus' || normalizedModel === 'opusplan' || normalizedModel.includes('opus-4-7');
|
|
198
|
+
return normalizedModel === 'opus' || normalizedModel === 'opusplan' || normalizedModel.includes('opus-4-7') || normalizedModel.includes('opus-4-8');
|
|
199
199
|
};
|
|
200
200
|
|
|
201
201
|
/**
|
|
202
|
-
* Check if a model is Opus 4.7 or later (Issue #1620)
|
|
203
|
-
* These models use Opus 4.7+ adaptive thinking behavior.
|
|
202
|
+
* Check if a model is Opus 4.7 or later (Issue #1620, Issue #1832)
|
|
203
|
+
* These models use Opus 4.7+ adaptive thinking behavior (also applies to Opus 4.8).
|
|
204
204
|
* @param {string} model - The model name or ID
|
|
205
205
|
* @returns {boolean} True if the model is Opus 4.7 or later
|
|
206
206
|
*/
|
|
@@ -210,6 +210,22 @@ export const isOpus47OrLater = model => {
|
|
|
210
210
|
return isOpus47(model) || normalizedModel.includes('opus-5');
|
|
211
211
|
};
|
|
212
212
|
|
|
213
|
+
/**
|
|
214
|
+
* Check if a model is Opus 4.8 or later (Issue #1832)
|
|
215
|
+
* Opus 4.8 inherits all Opus 4.7 API constraints (adaptive thinking only, no sampling
|
|
216
|
+
* params) and adds new features such as mid-conversation system messages, refusal stop
|
|
217
|
+
* details, and fast mode. These are not exposed through Claude Code today, but this
|
|
218
|
+
* helper enables finer-grained control for future wiring.
|
|
219
|
+
* @param {string} model - The model name or ID
|
|
220
|
+
* @returns {boolean} True if the model is Opus 4.8 or later
|
|
221
|
+
*/
|
|
222
|
+
export const isOpus48OrLater = model => {
|
|
223
|
+
if (!model) return false;
|
|
224
|
+
const normalizedModel = model.toLowerCase();
|
|
225
|
+
// 'opus' alias now maps to Opus 4.8 (Issue #1832)
|
|
226
|
+
return normalizedModel === 'opus' || normalizedModel === 'opusplan' || normalizedModel.includes('opus-4-8') || normalizedModel.includes('opus-5');
|
|
227
|
+
};
|
|
228
|
+
|
|
213
229
|
const isOpus45 = model => {
|
|
214
230
|
if (!model) return false;
|
|
215
231
|
const m = model.toLowerCase();
|
|
@@ -247,7 +263,7 @@ export const supportsEffortLevel = model => {
|
|
|
247
263
|
|
|
248
264
|
/**
|
|
249
265
|
* Check if a model supports the xhigh effort level.
|
|
250
|
-
* Official docs list xhigh
|
|
266
|
+
* Official docs list xhigh for Claude Opus 4.7 and Opus 4.8 (Issue #1832).
|
|
251
267
|
* @param {string} model - The model name or ID
|
|
252
268
|
* @returns {boolean} True if the model supports xhigh effort
|
|
253
269
|
*/
|
|
@@ -336,8 +352,10 @@ export const tokensToThinkingLevel = getTokensToThinkingLevel(DEFAULT_MAX_THINKI
|
|
|
336
352
|
export const OPUS_46_EFFORT_LEVELS = ['low', 'medium', 'high', 'max'];
|
|
337
353
|
|
|
338
354
|
/**
|
|
339
|
-
* Valid effort levels for Opus 4.7 (Issue #1620)
|
|
340
|
-
*
|
|
355
|
+
* Valid effort levels for Opus 4.7 and Opus 4.8 (Issue #1620, Issue #1832)
|
|
356
|
+
* Both models support the additional 'xhigh' level.
|
|
357
|
+
* Opus 4.8 keeps the same effort level set; the default effort level is 'high'
|
|
358
|
+
* (enforced by Claude Code itself, not by this module).
|
|
341
359
|
* See: https://platform.claude.com/docs/en/build-with-claude/effort
|
|
342
360
|
* @type {string[]}
|
|
343
361
|
*/
|
|
@@ -438,12 +456,13 @@ export const getClaudeEnv = (options = {}) => {
|
|
|
438
456
|
MCP_TOOL_TIMEOUT: String(claudeCode.mcpToolTimeout),
|
|
439
457
|
});
|
|
440
458
|
|
|
441
|
-
// Opus 4.7+ always uses adaptive thinking — MAX_THINKING_TOKENS has no effect (Issue #1620)
|
|
459
|
+
// Opus 4.7+ always uses adaptive thinking — MAX_THINKING_TOKENS has no effect (Issue #1620, Issue #1832)
|
|
460
|
+
// Opus 4.8 inherits this constraint: adaptive thinking is the only thinking mode.
|
|
442
461
|
// For Opus 4.6 and earlier, MAX_THINKING_TOKENS controls extended thinking (Claude Code >= 2.1.12)
|
|
443
462
|
// Default is 0 (thinking disabled) per Issue #1238.
|
|
444
463
|
const opus47 = options.model && isOpus47OrLater(options.model);
|
|
445
464
|
if (opus47) {
|
|
446
|
-
// Remove any inherited MAX_THINKING_TOKENS from process.env — Opus 4.7 ignores it
|
|
465
|
+
// Remove any inherited MAX_THINKING_TOKENS from process.env — Opus 4.7+ ignores it
|
|
447
466
|
delete env.MAX_THINKING_TOKENS;
|
|
448
467
|
} else {
|
|
449
468
|
env.MAX_THINKING_TOKENS = String(options.thinkingBudget ?? 0);
|
|
@@ -467,8 +486,9 @@ export const getClaudeEnv = (options = {}) => {
|
|
|
467
486
|
}
|
|
468
487
|
}
|
|
469
488
|
|
|
470
|
-
// Opus 4.7 omits thinking content by default; opt in with --show-thinking-content (Issue #1620)
|
|
489
|
+
// Opus 4.7+ omits thinking content by default; opt in with --show-thinking-content (Issue #1620, Issue #1832)
|
|
471
490
|
// Sets CLAUDE_CODE_SHOW_THINKING=1 which Claude Code uses to request display: "summarized"
|
|
491
|
+
// Applies to Opus 4.8 as well, which inherits Opus 4.7 thinking display behaviour.
|
|
472
492
|
if (options.showThinkingContent) {
|
|
473
493
|
env.CLAUDE_CODE_SHOW_THINKING = '1';
|
|
474
494
|
}
|
|
@@ -305,6 +305,43 @@ const isTransientNetworkError = error => {
|
|
|
305
305
|
return TRANSIENT_NETWORK_PATTERNS.some(pattern => text.includes(pattern));
|
|
306
306
|
};
|
|
307
307
|
|
|
308
|
+
/**
|
|
309
|
+
* Patterns that identify a *transient* failure of GitHub's compare/diff
|
|
310
|
+
* rendering endpoint (`/repos/{owner}/{repo}/compare/{base}...{head}`).
|
|
311
|
+
*
|
|
312
|
+
* Issue #1829: under heavy server load GitHub returns
|
|
313
|
+
* `HTTP 500: {"message":"...","errors":[{"code":"not_available",...}]}`
|
|
314
|
+
* with the body "this diff is temporarily unavailable due to heavy server
|
|
315
|
+
* load". This is NOT a "commits not indexed yet" condition — the branch and
|
|
316
|
+
* commits are already pushed and `gh pr create` (which does not render the
|
|
317
|
+
* full diff) would succeed. The readiness gate in `solve.auto-pr.lib.mjs`
|
|
318
|
+
* used to treat this as fatal and abort the whole session. These patterns let
|
|
319
|
+
* callers recognise the transient case and degrade gracefully instead.
|
|
320
|
+
*
|
|
321
|
+
* Note: HTTP 500 is deliberately matched here (and NOT in
|
|
322
|
+
* `TRANSIENT_NETWORK_PATTERNS`) because a bare 500 from arbitrary endpoints is
|
|
323
|
+
* too broad to retry blindly; it is only safe to treat as transient for the
|
|
324
|
+
* compare endpoint, alongside the explicit "not_available" / "heavy server
|
|
325
|
+
* load" markers.
|
|
326
|
+
*/
|
|
327
|
+
const TRANSIENT_COMPARE_API_PATTERNS = ['this diff is temporarily unavailable', 'temporarily unavailable due to heavy server load', 'heavy server load', 'not_available', 'http 500', 'http 502', 'http 503', 'http 504'];
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Detect whether `error` represents a transient failure of GitHub's
|
|
331
|
+
* compare/diff endpoint (issue #1829). Returns true for the documented
|
|
332
|
+
* "heavy server load" / `not_available` HTTP 500 response as well as the
|
|
333
|
+
* standard transient gateway codes (502/503/504), so the auto-PR readiness
|
|
334
|
+
* gate can fall through to PR creation rather than aborting.
|
|
335
|
+
*
|
|
336
|
+
* @param {unknown} error
|
|
337
|
+
* @returns {boolean}
|
|
338
|
+
*/
|
|
339
|
+
const isTransientCompareApiError = error => {
|
|
340
|
+
const text = collectErrorText(error).toLowerCase();
|
|
341
|
+
if (!text) return false;
|
|
342
|
+
return TRANSIENT_COMPARE_API_PATTERNS.some(pattern => text.includes(pattern));
|
|
343
|
+
};
|
|
344
|
+
|
|
308
345
|
/**
|
|
309
346
|
* Wrap `fn` so that GitHub rate-limit errors are converted into a sleep until
|
|
310
347
|
* (resetTime + bufferMs + jitterMs) followed by a retry. Transient network
|
|
@@ -438,11 +475,12 @@ export const wrapDollarWithGhRetry = (dollar, options = {}) => {
|
|
|
438
475
|
return wrapped;
|
|
439
476
|
};
|
|
440
477
|
|
|
441
|
-
export { isTransientNetworkError };
|
|
478
|
+
export { isTransientNetworkError, isTransientCompareApiError };
|
|
442
479
|
|
|
443
480
|
export default {
|
|
444
481
|
isRateLimitError,
|
|
445
482
|
isTransientNetworkError,
|
|
483
|
+
isTransientCompareApiError,
|
|
446
484
|
parseRateLimitReset,
|
|
447
485
|
fetchNextRateLimitReset,
|
|
448
486
|
fetchGitHubRateLimitUsage,
|
package/src/models/index.mjs
CHANGED
|
@@ -28,23 +28,25 @@ const execFileAsync = promisify(execFile);
|
|
|
28
28
|
// ─── MODEL DATA ──────────────────────────────────────────────────────────────
|
|
29
29
|
|
|
30
30
|
// Claude models (Anthropic API)
|
|
31
|
-
// Updated for Opus 4.5/4.6/4.7 and Sonnet 4.6 support (Issue #1221, Issue #1238, Issue #1329, Issue #1433, Issue #1620)
|
|
31
|
+
// Updated for Opus 4.5/4.6/4.7/4.8 and Sonnet 4.6 support (Issue #1221, Issue #1238, Issue #1329, Issue #1433, Issue #1620, Issue #1832)
|
|
32
32
|
export const claudeModels = {
|
|
33
33
|
sonnet: 'claude-sonnet-4-6', // Sonnet 4.6 (default, Issue #1329)
|
|
34
|
-
opus: 'claude-opus-4-
|
|
34
|
+
opus: 'claude-opus-4-8', // Opus 4.8 (Issue #1832)
|
|
35
35
|
haiku: 'claude-haiku-4-5-20251001', // Haiku 4.5
|
|
36
36
|
'haiku-3-5': 'claude-3-5-haiku-20241022', // Haiku 3.5
|
|
37
37
|
'haiku-3': 'claude-3-haiku-20240307', // Haiku 3
|
|
38
38
|
opusplan: 'opusplan', // Special mode: Opus for planning, Sonnet for execution (Issue #1223)
|
|
39
39
|
// Shorter version aliases (Issue #1221, Issue #1329 - PR comment feedback)
|
|
40
40
|
'sonnet-4-6': 'claude-sonnet-4-6', // Sonnet 4.6 short alias (Issue #1329)
|
|
41
|
-
'opus-4-
|
|
41
|
+
'opus-4-8': 'claude-opus-4-8', // Opus 4.8 short alias (Issue #1832)
|
|
42
|
+
'opus-4-7': 'claude-opus-4-7', // Opus 4.7 short alias (backward compatibility)
|
|
42
43
|
'opus-4-6': 'claude-opus-4-6', // Opus 4.6 short alias (backward compatibility)
|
|
43
44
|
'opus-4-5': 'claude-opus-4-5-20251101', // Opus 4.5 short alias
|
|
44
45
|
'sonnet-4-5': 'claude-sonnet-4-5-20250929', // Sonnet 4.5 short alias (backward compatibility)
|
|
45
46
|
'haiku-4-5': 'claude-haiku-4-5-20251001', // Haiku 4.5 short alias
|
|
46
|
-
// Version aliases for backward compatibility (Issue #1221, Issue #1329, Issue #1620)
|
|
47
|
-
'claude-opus-4-
|
|
47
|
+
// Version aliases for backward compatibility (Issue #1221, Issue #1329, Issue #1620, Issue #1832)
|
|
48
|
+
'claude-opus-4-8': 'claude-opus-4-8', // Opus 4.8 (Issue #1832)
|
|
49
|
+
'claude-opus-4-7': 'claude-opus-4-7', // Opus 4.7 (backward compatibility)
|
|
48
50
|
'claude-sonnet-4-6': 'claude-sonnet-4-6', // Sonnet 4.6 (Issue #1329)
|
|
49
51
|
'claude-opus-4-6': 'claude-opus-4-6', // Opus 4.6 (backward compatibility)
|
|
50
52
|
'claude-opus-4-5': 'claude-opus-4-5-20251101', // Opus 4.5
|
|
@@ -175,9 +177,10 @@ export const defaultModels = {
|
|
|
175
177
|
gemini: 'flash',
|
|
176
178
|
};
|
|
177
179
|
|
|
178
|
-
// Models that support 1M token context window via [1m] suffix (Issue #1221, Issue #1238, Issue #1329)
|
|
180
|
+
// Models that support 1M token context window via [1m] suffix (Issue #1221, Issue #1238, Issue #1329, Issue #1832)
|
|
179
181
|
// See: https://code.claude.com/docs/en/model-config
|
|
180
182
|
export const MODELS_SUPPORTING_1M_CONTEXT = [
|
|
183
|
+
'claude-opus-4-8', // Opus 4.8 (Issue #1832)
|
|
181
184
|
'claude-opus-4-7', // Opus 4.7 (Issue #1620)
|
|
182
185
|
'claude-opus-4-6',
|
|
183
186
|
'claude-opus-4-5-20251101',
|
|
@@ -186,7 +189,8 @@ export const MODELS_SUPPORTING_1M_CONTEXT = [
|
|
|
186
189
|
'claude-sonnet-4-5',
|
|
187
190
|
'sonnet', // Now maps to Sonnet 4.6 (Issue #1329)
|
|
188
191
|
'sonnet-4-6', // Short alias (Issue #1329)
|
|
189
|
-
'opus', // Now maps to Opus 4.
|
|
192
|
+
'opus', // Now maps to Opus 4.8 (Issue #1832)
|
|
193
|
+
'opus-4-8', // Short alias (Issue #1832)
|
|
190
194
|
'opus-4-7', // Short alias (Issue #1620)
|
|
191
195
|
'opus-4-6', // Short alias (Issue #1221 - PR comment feedback)
|
|
192
196
|
'opus-4-5', // Short alias (Issue #1238)
|
|
@@ -216,6 +220,7 @@ export const freeToBaseModelMap = {
|
|
|
216
220
|
|
|
217
221
|
export const CLAUDE_MODELS = {
|
|
218
222
|
...claudeModels,
|
|
223
|
+
'claude-opus-4-8': 'claude-opus-4-8', // Opus 4.8 full ID (Issue #1832)
|
|
219
224
|
'claude-opus-4-7': 'claude-opus-4-7', // Opus 4.7 full ID (Issue #1620)
|
|
220
225
|
'claude-sonnet-4-5-20250929': 'claude-sonnet-4-5-20250929',
|
|
221
226
|
'claude-opus-4-5-20251101': 'claude-opus-4-5-20251101',
|
|
@@ -989,6 +994,7 @@ export const resolveModelId = (requestedModel, tool) => {
|
|
|
989
994
|
|
|
990
995
|
export const defaultFallbackModels = {
|
|
991
996
|
claude: {
|
|
997
|
+
'claude-opus-4-8': 'opus-4-7',
|
|
992
998
|
'claude-opus-4-7': 'opus-4-6',
|
|
993
999
|
},
|
|
994
1000
|
codex: {
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compare-API readiness handler for the auto-PR pipeline.
|
|
3
|
+
*
|
|
4
|
+
* After a branch is pushed, the auto-PR flow polls GitHub's compare endpoint
|
|
5
|
+
* (`/repos/{owner}/{repo}/compare/{base}...{head}`) to confirm the pushed
|
|
6
|
+
* commits are visible before calling `gh pr create`. When that poll never
|
|
7
|
+
* reports commits ahead, this helper decides what the failure means:
|
|
8
|
+
*
|
|
9
|
+
* • HTTP 404 (fork mode) → repository mismatch. Investigate the fork
|
|
10
|
+
* relationship and abort with an actionable error (FATAL).
|
|
11
|
+
* • Issue #1829: a transient compare/diff failure (HTTP 500 "this diff is
|
|
12
|
+
* temporarily unavailable due to heavy server load" / code
|
|
13
|
+
* `not_available`, or a 5xx gateway error). The branch and commits were
|
|
14
|
+
* already pushed and `gh pr create` does not render the full diff, so this
|
|
15
|
+
* is a diff-RENDERING failure, NOT missing commits. Degrade gracefully and
|
|
16
|
+
* return `true` so the caller proceeds to PR creation — still guarded by
|
|
17
|
+
* branch verification and the LOCAL `git rev-list` commit check.
|
|
18
|
+
* • Anything else (genuinely 0 commits ahead / unknown failure) → abort with
|
|
19
|
+
* the original "GitHub compare API not ready" error (FATAL).
|
|
20
|
+
*
|
|
21
|
+
* Extracted from solve.auto-pr.lib.mjs to keep that file under the 1500-line
|
|
22
|
+
* CI cap.
|
|
23
|
+
*
|
|
24
|
+
* @see https://github.com/link-assistant/hive-mind/issues/1829
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { isTransientCompareApiError } from './github-rate-limit.lib.mjs'; // Issue #1829: lets the compare-API readiness gate degrade gracefully on transient diff-render failures.
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Handle the case where the compare-API readiness poll never saw commits.
|
|
31
|
+
*
|
|
32
|
+
* @param {object} params
|
|
33
|
+
* @param {object} params.argv
|
|
34
|
+
* @param {string|null} params.forkedRepo
|
|
35
|
+
* @param {string} params.owner
|
|
36
|
+
* @param {string} params.repo
|
|
37
|
+
* @param {string|number} params.issueNumber
|
|
38
|
+
* @param {string} params.branchName
|
|
39
|
+
* @param {string} params.targetBranchForCompare
|
|
40
|
+
* @param {number} params.maxCompareAttempts
|
|
41
|
+
* @param {object} params.compareResult - last command-stream compare result.
|
|
42
|
+
* @param {(msg: string, opts?: object) => Promise<void>} params.log
|
|
43
|
+
* @param {(symbol: string, label: string, value: string) => string} params.formatAligned
|
|
44
|
+
* @param {Function} params.$ - command-stream tagged function from solve.
|
|
45
|
+
* @returns {Promise<boolean>} `true` when the failure was transient and PR
|
|
46
|
+
* creation should proceed (degraded mode). Throws on fatal failures.
|
|
47
|
+
*/
|
|
48
|
+
export async function handleCompareApiNotReady({ argv, forkedRepo, owner, repo, issueNumber, branchName, targetBranchForCompare, maxCompareAttempts, compareResult, log, formatAligned, $ }) {
|
|
49
|
+
// Issue #1829: build the last compare-API output as a STRING. The
|
|
50
|
+
// command-stream result exposes stdout/stderr as Buffers, and both
|
|
51
|
+
// the 404 check below and isTransientCompareApiError expect a string
|
|
52
|
+
// (the rate-limit lib's collectErrorText returns '' for a raw Buffer).
|
|
53
|
+
const lastCompareOutput = `${compareResult?.stdout?.toString?.() ?? ''}${compareResult?.stderr?.toString?.() ?? ''}`;
|
|
54
|
+
|
|
55
|
+
// Check if this is a repository mismatch error (HTTP 404 from compare API)
|
|
56
|
+
let isRepositoryMismatch = false;
|
|
57
|
+
if (argv.fork && forkedRepo) {
|
|
58
|
+
// For fork mode, check the last compare API call result for 404
|
|
59
|
+
if (lastCompareOutput.includes('HTTP 404') || lastCompareOutput.includes('Not Found')) {
|
|
60
|
+
isRepositoryMismatch = true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Issue #1829: GitHub's compare/diff endpoint can return a transient
|
|
65
|
+
// HTTP 500 ("this diff is temporarily unavailable due to heavy server
|
|
66
|
+
// load" / code "not_available") or a 5xx gateway error under load.
|
|
67
|
+
// That is a diff-RENDERING failure, NOT a "commits not indexed yet"
|
|
68
|
+
// condition: the branch and commits were already pushed, and
|
|
69
|
+
// `gh pr create` does not render the full diff, so it would still
|
|
70
|
+
// succeed. Aborting here used to kill the whole session needlessly.
|
|
71
|
+
// Treat a purely transient compare failure as non-fatal and fall
|
|
72
|
+
// through to PR creation — still guarded by the branch verification
|
|
73
|
+
// and the LOCAL `git rev-list` commit check below (and `gh pr create`
|
|
74
|
+
// itself retries transient 5xx via execGhWithRetry).
|
|
75
|
+
const compareFailedTransiently = !isRepositoryMismatch && isTransientCompareApiError(lastCompareOutput);
|
|
76
|
+
|
|
77
|
+
if (isRepositoryMismatch) {
|
|
78
|
+
// BEFORE showing any error, verify if the repository is actually a GitHub fork
|
|
79
|
+
await log('');
|
|
80
|
+
await log(formatAligned('🔍', 'Investigating:', 'Checking fork relationship...'));
|
|
81
|
+
|
|
82
|
+
const forkInfoResult = await $({
|
|
83
|
+
silent: true,
|
|
84
|
+
})`gh api repos/${forkedRepo} --jq '{fork: .fork, parent: .parent.full_name, source: .source.full_name}' 2>&1`;
|
|
85
|
+
|
|
86
|
+
let isFork = false;
|
|
87
|
+
let parentRepo = null;
|
|
88
|
+
let sourceRepo = null;
|
|
89
|
+
|
|
90
|
+
if (forkInfoResult.code === 0) {
|
|
91
|
+
try {
|
|
92
|
+
const forkInfo = JSON.parse(forkInfoResult.stdout.toString().trim());
|
|
93
|
+
isFork = forkInfo.fork === true;
|
|
94
|
+
parentRepo = forkInfo.parent || null;
|
|
95
|
+
sourceRepo = forkInfo.source || null;
|
|
96
|
+
} catch {
|
|
97
|
+
// Failed to parse fork info
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!isFork) {
|
|
102
|
+
// Repository is NOT a fork at all
|
|
103
|
+
await log('');
|
|
104
|
+
await log(formatAligned('❌', 'NOT A GITHUB FORK:', 'Repository is not a fork'), { level: 'error' });
|
|
105
|
+
await log('');
|
|
106
|
+
await log(' 🔍 What happened:');
|
|
107
|
+
await log(` The repository ${forkedRepo} is NOT a GitHub fork.`);
|
|
108
|
+
await log(' GitHub API reports: fork=false, parent=null');
|
|
109
|
+
await log('');
|
|
110
|
+
await log(' 💡 Why this happens:');
|
|
111
|
+
await log(' This repository was likely created by cloning and pushing (git clone + git push)');
|
|
112
|
+
await log(" instead of using GitHub's Fork button or API.");
|
|
113
|
+
await log('');
|
|
114
|
+
await log(' When a repository is created this way:');
|
|
115
|
+
await log(' • GitHub does not track it as a fork');
|
|
116
|
+
await log(' • It has no parent relationship with the original repository');
|
|
117
|
+
await log(' • Pull requests cannot be created to the original repository');
|
|
118
|
+
await log(' • Compare API returns 404 when comparing with unrelated repositories');
|
|
119
|
+
await log('');
|
|
120
|
+
await log(' 📦 Repository details:');
|
|
121
|
+
await log(' • Target repository: ' + `${owner}/${repo}`);
|
|
122
|
+
await log(' • Your repository: ' + forkedRepo);
|
|
123
|
+
await log(' • Fork status: false (NOT A FORK)');
|
|
124
|
+
await log('');
|
|
125
|
+
await log(' 🔧 How to fix:');
|
|
126
|
+
await log(' Option 1: Delete the non-fork repository and create a proper fork');
|
|
127
|
+
await log(` gh repo delete ${forkedRepo}`);
|
|
128
|
+
await log(` Then run this command again to create a proper GitHub fork of ${owner}/${repo}`);
|
|
129
|
+
await log('');
|
|
130
|
+
await log(' Option 2: Use --prefix-fork-name-with-owner-name to avoid name conflicts');
|
|
131
|
+
await log(` ./solve.mjs "https://github.com/${owner}/${repo}/issues/${issueNumber}" --prefix-fork-name-with-owner-name`);
|
|
132
|
+
await log(' This creates forks with names like "owner-repo" instead of just "repo"');
|
|
133
|
+
await log('');
|
|
134
|
+
await log(' Option 3: Work directly on the repository (if you have write access)');
|
|
135
|
+
await log(` ./solve.mjs "https://github.com/${owner}/${repo}/issues/${issueNumber}" --no-fork`);
|
|
136
|
+
await log('');
|
|
137
|
+
|
|
138
|
+
throw new Error('Repository is not a GitHub fork - cannot create PR to unrelated repository');
|
|
139
|
+
} else if (parentRepo !== `${owner}/${repo}` && sourceRepo !== `${owner}/${repo}`) {
|
|
140
|
+
// Repository IS a fork, but of a different repository
|
|
141
|
+
await log('');
|
|
142
|
+
await log(formatAligned('❌', 'WRONG FORK PARENT:', 'Fork is from different repository'), {
|
|
143
|
+
level: 'error',
|
|
144
|
+
});
|
|
145
|
+
await log('');
|
|
146
|
+
await log(' 🔍 What happened:');
|
|
147
|
+
await log(` The repository ${forkedRepo} IS a GitHub fork,`);
|
|
148
|
+
await log(` but it's a fork of a DIFFERENT repository than ${owner}/${repo}.`);
|
|
149
|
+
await log('');
|
|
150
|
+
await log(' 📦 Fork relationship:');
|
|
151
|
+
await log(' • Your fork: ' + forkedRepo);
|
|
152
|
+
await log(' • Fork parent: ' + (parentRepo || 'unknown'));
|
|
153
|
+
await log(' • Fork source: ' + (sourceRepo || 'unknown'));
|
|
154
|
+
await log(' • Target repository: ' + `${owner}/${repo}`);
|
|
155
|
+
await log('');
|
|
156
|
+
await log(' 💡 Why this happens:');
|
|
157
|
+
await log(' You have an existing fork from a different repository');
|
|
158
|
+
await log(' that shares the same name but is from a different source.');
|
|
159
|
+
await log(' GitHub treats forks hierarchically - each fork tracks its root repository.');
|
|
160
|
+
await log('');
|
|
161
|
+
await log(' 🔧 How to fix:');
|
|
162
|
+
await log(' Option 1: Delete the conflicting fork and create a new one');
|
|
163
|
+
await log(` gh repo delete ${forkedRepo}`);
|
|
164
|
+
await log(` Then run this command again to create a proper fork of ${owner}/${repo}`);
|
|
165
|
+
await log('');
|
|
166
|
+
await log(' Option 2: Use --prefix-fork-name-with-owner-name to avoid conflicts');
|
|
167
|
+
await log(` ./solve.mjs "https://github.com/${owner}/${repo}/issues/${issueNumber}" --prefix-fork-name-with-owner-name`);
|
|
168
|
+
await log(' This creates forks with names like "owner-repo" instead of just "repo"');
|
|
169
|
+
await log('');
|
|
170
|
+
await log(' Option 3: Work directly on the repository (if you have write access)');
|
|
171
|
+
await log(` ./solve.mjs "https://github.com/${owner}/${repo}/issues/${issueNumber}" --no-fork`);
|
|
172
|
+
await log('');
|
|
173
|
+
|
|
174
|
+
throw new Error('Fork parent mismatch - fork is from different repository tree');
|
|
175
|
+
} else {
|
|
176
|
+
// Repository is a fork of the correct parent, but compare API still failed
|
|
177
|
+
// This is unexpected - show detailed error
|
|
178
|
+
await log('');
|
|
179
|
+
await log(formatAligned('❌', 'COMPARE API ERROR:', 'Unexpected failure'), { level: 'error' });
|
|
180
|
+
await log('');
|
|
181
|
+
await log(' 🔍 What happened:');
|
|
182
|
+
await log(` The repository ${forkedRepo} is a valid fork of ${owner}/${repo},`);
|
|
183
|
+
await log(" but GitHub's compare API still returned an error.");
|
|
184
|
+
await log('');
|
|
185
|
+
await log(' 📦 Fork verification:');
|
|
186
|
+
await log(' • Your fork: ' + forkedRepo);
|
|
187
|
+
await log(' • Fork status: true (VALID FORK)');
|
|
188
|
+
await log(' • Fork parent: ' + (parentRepo || 'unknown'));
|
|
189
|
+
await log(' • Target repository: ' + `${owner}/${repo}`);
|
|
190
|
+
await log('');
|
|
191
|
+
await log(' 💡 This is unexpected:');
|
|
192
|
+
await log(' The fork relationship is correct, but the compare API failed.');
|
|
193
|
+
await log(' This might be a temporary GitHub API issue.');
|
|
194
|
+
await log('');
|
|
195
|
+
await log(' 🔧 How to fix:');
|
|
196
|
+
await log(' 1. Wait a minute and try creating the PR manually:');
|
|
197
|
+
if (argv.fork && forkedRepo) {
|
|
198
|
+
const forkUser = forkedRepo.split('/')[0];
|
|
199
|
+
await log(` gh pr create --draft --repo ${owner}/${repo} --base ${targetBranchForCompare} --head ${forkUser}:${branchName}`);
|
|
200
|
+
}
|
|
201
|
+
await log(' 2. Check if the issue persists - it might be a GitHub API outage');
|
|
202
|
+
await log('');
|
|
203
|
+
|
|
204
|
+
throw new Error('Compare API failed unexpectedly despite valid fork relationship');
|
|
205
|
+
}
|
|
206
|
+
} else if (compareFailedTransiently) {
|
|
207
|
+
// Issue #1829: the compare API failed only with a transient server
|
|
208
|
+
// error. Degrade gracefully — proceed to PR creation rather than
|
|
209
|
+
// aborting the whole session.
|
|
210
|
+
await log('');
|
|
211
|
+
await log(formatAligned('⚠️', 'COMPARE API DEGRADED:', 'Transient server error — proceeding'), {
|
|
212
|
+
level: 'warning',
|
|
213
|
+
});
|
|
214
|
+
await log('');
|
|
215
|
+
await log(' 🔍 What happened:');
|
|
216
|
+
await log(` GitHub's compare API failed with a transient server error after ${maxCompareAttempts} attempts`);
|
|
217
|
+
await log(' (e.g. HTTP 500 "this diff is temporarily unavailable due to heavy server');
|
|
218
|
+
await log(' load", or a 5xx gateway error).');
|
|
219
|
+
await log('');
|
|
220
|
+
await log(' 💡 Why this is safe to ignore:');
|
|
221
|
+
await log(' • The branch and commits were already pushed successfully.');
|
|
222
|
+
await log(' • This is a diff-RENDERING failure, not missing commits.');
|
|
223
|
+
await log(' • `gh pr create` does not render the full diff, so it can still succeed.');
|
|
224
|
+
await log(' • The branch is verified on GitHub and the local commit count is');
|
|
225
|
+
await log(' re-checked before PR creation; `gh pr create` retries 5xx errors too.');
|
|
226
|
+
await log('');
|
|
227
|
+
await log(' ➡️ Proceeding to PR creation despite the compare API error (issue #1829).');
|
|
228
|
+
await log('');
|
|
229
|
+
const firstLine =
|
|
230
|
+
lastCompareOutput
|
|
231
|
+
.split('\n')
|
|
232
|
+
.map(s => s.trim())
|
|
233
|
+
.filter(Boolean)[0] || 'unknown';
|
|
234
|
+
await log(` Last compare API output: ${firstLine}`, { verbose: true });
|
|
235
|
+
// Fall through to branch verification + local commit check + PR creation.
|
|
236
|
+
return true;
|
|
237
|
+
} else {
|
|
238
|
+
// Original timeout error for other cases
|
|
239
|
+
await log('');
|
|
240
|
+
await log(formatAligned('❌', 'GITHUB SYNC TIMEOUT:', 'Compare API not ready after retries'), {
|
|
241
|
+
level: 'error',
|
|
242
|
+
});
|
|
243
|
+
await log('');
|
|
244
|
+
await log(' 🔍 What happened:');
|
|
245
|
+
await log(` After ${maxCompareAttempts} attempts, GitHub's compare API still shows no commits`);
|
|
246
|
+
await log(` between ${targetBranchForCompare} and ${branchName}.`);
|
|
247
|
+
await log('');
|
|
248
|
+
await log(' 💡 This usually means:');
|
|
249
|
+
await log(" • GitHub's backend systems haven't finished indexing the push");
|
|
250
|
+
await log(" • There's a temporary issue with GitHub's API");
|
|
251
|
+
await log(' • The commits may not have been pushed correctly');
|
|
252
|
+
await log('');
|
|
253
|
+
await log(' 🔧 How to fix:');
|
|
254
|
+
await log(' 1. Wait a minute and try creating the PR manually:');
|
|
255
|
+
// For fork mode, use the correct head reference format
|
|
256
|
+
if (argv.fork && forkedRepo) {
|
|
257
|
+
const forkUser = forkedRepo.split('/')[0];
|
|
258
|
+
await log(` gh pr create --draft --repo ${owner}/${repo} --base ${targetBranchForCompare} --head ${forkUser}:${branchName}`);
|
|
259
|
+
} else {
|
|
260
|
+
await log(` gh pr create --draft --repo ${owner}/${repo} --base ${targetBranchForCompare} --head ${branchName}`);
|
|
261
|
+
}
|
|
262
|
+
await log(' 2. Check if the branch exists on GitHub:');
|
|
263
|
+
// Show the correct repository where the branch was pushed
|
|
264
|
+
const branchRepo = argv.fork && forkedRepo ? forkedRepo : `${owner}/${repo}`;
|
|
265
|
+
await log(` https://github.com/${branchRepo}/tree/${branchName}`);
|
|
266
|
+
await log(' 3. Check the commit is on GitHub:');
|
|
267
|
+
// Use the correct head reference for the compare API check
|
|
268
|
+
if (argv.fork && forkedRepo) {
|
|
269
|
+
const forkUser = forkedRepo.split('/')[0];
|
|
270
|
+
await log(` gh api repos/${owner}/${repo}/compare/${targetBranchForCompare}...${forkUser}:${branchName} --paginate`);
|
|
271
|
+
} else {
|
|
272
|
+
await log(` gh api repos/${owner}/${repo}/compare/${targetBranchForCompare}...${branchName} --paginate`);
|
|
273
|
+
}
|
|
274
|
+
await log('');
|
|
275
|
+
|
|
276
|
+
throw new Error('GitHub compare API not ready - cannot create PR safely');
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export default { handleCompareApiNotReady };
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
import { closingIssueNumbersContain, parseClosingIssueNumbers } from './pr-issue-linking.lib.mjs';
|
|
7
7
|
import { handleRejectedPushForAutoPr, synchronizeExistingIssueBranchBeforeAutoPrCreation } from './solve.branch-divergence.lib.mjs';
|
|
8
8
|
import { emitForkAwareDiagnostic } from './solve.auto-pr-fork-diagnostic.lib.mjs';
|
|
9
|
+
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.
|
|
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.
|
|
11
|
+
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.
|
|
11
12
|
import { stagePlaceholderFileOrExplain, explainNothingStagedAndThrow } from './solve.auto-pr-placeholder.lib.mjs'; // Issue #1825: handles the seed placeholder when the target repo gitignores it.
|
|
12
13
|
|
|
13
14
|
export async function handleAutoPrCreation({ argv, tempDir, branchName, issueNumber, owner, repo, defaultBranch, forkedRepo, isContinueMode, prNumber, log, formatAligned, $, reportError, path, fs }) {
|
|
@@ -596,191 +597,40 @@ Proceed.
|
|
|
596
597
|
await log(` ⚠️ GitHub compare API shows 0 commits ahead (attempt ${compareAttempts}/${maxCompareAttempts})`, { level: 'warning' });
|
|
597
598
|
}
|
|
598
599
|
} else {
|
|
599
|
-
|
|
600
|
-
|
|
600
|
+
// Issue #1829: surface compare-API failures in normal output (not
|
|
601
|
+
// only verbose) so the degraded-mode decision below is explainable
|
|
602
|
+
// from the logs. Build the text as a STRING — the command-stream
|
|
603
|
+
// result exposes stdout/stderr as Buffers, and the transient
|
|
604
|
+
// detectors call String.prototype.toLowerCase().
|
|
605
|
+
const errorText = `${compareResult.stdout?.toString?.() ?? ''}${compareResult.stderr?.toString?.() ?? ''}`.trim();
|
|
606
|
+
const firstLine =
|
|
607
|
+
errorText
|
|
608
|
+
.split('\n')
|
|
609
|
+
.map(s => s.trim())
|
|
610
|
+
.filter(Boolean)[0] || 'unknown';
|
|
611
|
+
const transientNote = isTransientCompareApiError(errorText) ? ' (transient server error)' : '';
|
|
612
|
+
await log(` ⚠️ GitHub compare API error${transientNote} (attempt ${compareAttempts}/${maxCompareAttempts}): ${firstLine}`, { level: 'warning' });
|
|
613
|
+
if (argv.verbose && errorText) {
|
|
614
|
+
await log(` Compare API full output: ${errorText}`, { verbose: true });
|
|
601
615
|
}
|
|
602
616
|
}
|
|
603
617
|
}
|
|
604
618
|
|
|
605
619
|
if (!compareReady) {
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
const forkInfoResult = await $({
|
|
622
|
-
silent: true,
|
|
623
|
-
})`gh api repos/${forkedRepo} --jq '{fork: .fork, parent: .parent.full_name, source: .source.full_name}' 2>&1`;
|
|
624
|
-
|
|
625
|
-
let isFork = false;
|
|
626
|
-
let parentRepo = null;
|
|
627
|
-
let sourceRepo = null;
|
|
628
|
-
|
|
629
|
-
if (forkInfoResult.code === 0) {
|
|
630
|
-
try {
|
|
631
|
-
const forkInfo = JSON.parse(forkInfoResult.stdout.toString().trim());
|
|
632
|
-
isFork = forkInfo.fork === true;
|
|
633
|
-
parentRepo = forkInfo.parent || null;
|
|
634
|
-
sourceRepo = forkInfo.source || null;
|
|
635
|
-
} catch {
|
|
636
|
-
// Failed to parse fork info
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
if (!isFork) {
|
|
641
|
-
// Repository is NOT a fork at all
|
|
642
|
-
await log('');
|
|
643
|
-
await log(formatAligned('❌', 'NOT A GITHUB FORK:', 'Repository is not a fork'), { level: 'error' });
|
|
644
|
-
await log('');
|
|
645
|
-
await log(' 🔍 What happened:');
|
|
646
|
-
await log(` The repository ${forkedRepo} is NOT a GitHub fork.`);
|
|
647
|
-
await log(' GitHub API reports: fork=false, parent=null');
|
|
648
|
-
await log('');
|
|
649
|
-
await log(' 💡 Why this happens:');
|
|
650
|
-
await log(' This repository was likely created by cloning and pushing (git clone + git push)');
|
|
651
|
-
await log(" instead of using GitHub's Fork button or API.");
|
|
652
|
-
await log('');
|
|
653
|
-
await log(' When a repository is created this way:');
|
|
654
|
-
await log(' • GitHub does not track it as a fork');
|
|
655
|
-
await log(' • It has no parent relationship with the original repository');
|
|
656
|
-
await log(' • Pull requests cannot be created to the original repository');
|
|
657
|
-
await log(' • Compare API returns 404 when comparing with unrelated repositories');
|
|
658
|
-
await log('');
|
|
659
|
-
await log(' 📦 Repository details:');
|
|
660
|
-
await log(' • Target repository: ' + `${owner}/${repo}`);
|
|
661
|
-
await log(' • Your repository: ' + forkedRepo);
|
|
662
|
-
await log(' • Fork status: false (NOT A FORK)');
|
|
663
|
-
await log('');
|
|
664
|
-
await log(' 🔧 How to fix:');
|
|
665
|
-
await log(' Option 1: Delete the non-fork repository and create a proper fork');
|
|
666
|
-
await log(` gh repo delete ${forkedRepo}`);
|
|
667
|
-
await log(` Then run this command again to create a proper GitHub fork of ${owner}/${repo}`);
|
|
668
|
-
await log('');
|
|
669
|
-
await log(' Option 2: Use --prefix-fork-name-with-owner-name to avoid name conflicts');
|
|
670
|
-
await log(` ./solve.mjs "https://github.com/${owner}/${repo}/issues/${issueNumber}" --prefix-fork-name-with-owner-name`);
|
|
671
|
-
await log(' This creates forks with names like "owner-repo" instead of just "repo"');
|
|
672
|
-
await log('');
|
|
673
|
-
await log(' Option 3: Work directly on the repository (if you have write access)');
|
|
674
|
-
await log(` ./solve.mjs "https://github.com/${owner}/${repo}/issues/${issueNumber}" --no-fork`);
|
|
675
|
-
await log('');
|
|
676
|
-
|
|
677
|
-
throw new Error('Repository is not a GitHub fork - cannot create PR to unrelated repository');
|
|
678
|
-
} else if (parentRepo !== `${owner}/${repo}` && sourceRepo !== `${owner}/${repo}`) {
|
|
679
|
-
// Repository IS a fork, but of a different repository
|
|
680
|
-
await log('');
|
|
681
|
-
await log(formatAligned('❌', 'WRONG FORK PARENT:', 'Fork is from different repository'), { level: 'error' });
|
|
682
|
-
await log('');
|
|
683
|
-
await log(' 🔍 What happened:');
|
|
684
|
-
await log(` The repository ${forkedRepo} IS a GitHub fork,`);
|
|
685
|
-
await log(` but it's a fork of a DIFFERENT repository than ${owner}/${repo}.`);
|
|
686
|
-
await log('');
|
|
687
|
-
await log(' 📦 Fork relationship:');
|
|
688
|
-
await log(' • Your fork: ' + forkedRepo);
|
|
689
|
-
await log(' • Fork parent: ' + (parentRepo || 'unknown'));
|
|
690
|
-
await log(' • Fork source: ' + (sourceRepo || 'unknown'));
|
|
691
|
-
await log(' • Target repository: ' + `${owner}/${repo}`);
|
|
692
|
-
await log('');
|
|
693
|
-
await log(' 💡 Why this happens:');
|
|
694
|
-
await log(' You have an existing fork from a different repository');
|
|
695
|
-
await log(' that shares the same name but is from a different source.');
|
|
696
|
-
await log(' GitHub treats forks hierarchically - each fork tracks its root repository.');
|
|
697
|
-
await log('');
|
|
698
|
-
await log(' 🔧 How to fix:');
|
|
699
|
-
await log(' Option 1: Delete the conflicting fork and create a new one');
|
|
700
|
-
await log(` gh repo delete ${forkedRepo}`);
|
|
701
|
-
await log(` Then run this command again to create a proper fork of ${owner}/${repo}`);
|
|
702
|
-
await log('');
|
|
703
|
-
await log(' Option 2: Use --prefix-fork-name-with-owner-name to avoid conflicts');
|
|
704
|
-
await log(` ./solve.mjs "https://github.com/${owner}/${repo}/issues/${issueNumber}" --prefix-fork-name-with-owner-name`);
|
|
705
|
-
await log(' This creates forks with names like "owner-repo" instead of just "repo"');
|
|
706
|
-
await log('');
|
|
707
|
-
await log(' Option 3: Work directly on the repository (if you have write access)');
|
|
708
|
-
await log(` ./solve.mjs "https://github.com/${owner}/${repo}/issues/${issueNumber}" --no-fork`);
|
|
709
|
-
await log('');
|
|
710
|
-
|
|
711
|
-
throw new Error('Fork parent mismatch - fork is from different repository tree');
|
|
712
|
-
} else {
|
|
713
|
-
// Repository is a fork of the correct parent, but compare API still failed
|
|
714
|
-
// This is unexpected - show detailed error
|
|
715
|
-
await log('');
|
|
716
|
-
await log(formatAligned('❌', 'COMPARE API ERROR:', 'Unexpected failure'), { level: 'error' });
|
|
717
|
-
await log('');
|
|
718
|
-
await log(' 🔍 What happened:');
|
|
719
|
-
await log(` The repository ${forkedRepo} is a valid fork of ${owner}/${repo},`);
|
|
720
|
-
await log(" but GitHub's compare API still returned an error.");
|
|
721
|
-
await log('');
|
|
722
|
-
await log(' 📦 Fork verification:');
|
|
723
|
-
await log(' • Your fork: ' + forkedRepo);
|
|
724
|
-
await log(' • Fork status: true (VALID FORK)');
|
|
725
|
-
await log(' • Fork parent: ' + (parentRepo || 'unknown'));
|
|
726
|
-
await log(' • Target repository: ' + `${owner}/${repo}`);
|
|
727
|
-
await log('');
|
|
728
|
-
await log(' 💡 This is unexpected:');
|
|
729
|
-
await log(' The fork relationship is correct, but the compare API failed.');
|
|
730
|
-
await log(' This might be a temporary GitHub API issue.');
|
|
731
|
-
await log('');
|
|
732
|
-
await log(' 🔧 How to fix:');
|
|
733
|
-
await log(' 1. Wait a minute and try creating the PR manually:');
|
|
734
|
-
if (argv.fork && forkedRepo) {
|
|
735
|
-
const forkUser = forkedRepo.split('/')[0];
|
|
736
|
-
await log(` gh pr create --draft --repo ${owner}/${repo} --base ${targetBranchForCompare} --head ${forkUser}:${branchName}`);
|
|
737
|
-
}
|
|
738
|
-
await log(' 2. Check if the issue persists - it might be a GitHub API outage');
|
|
739
|
-
await log('');
|
|
740
|
-
|
|
741
|
-
throw new Error('Compare API failed unexpectedly despite valid fork relationship');
|
|
742
|
-
}
|
|
743
|
-
} else {
|
|
744
|
-
// Original timeout error for other cases
|
|
745
|
-
await log('');
|
|
746
|
-
await log(formatAligned('❌', 'GITHUB SYNC TIMEOUT:', 'Compare API not ready after retries'), {
|
|
747
|
-
level: 'error',
|
|
748
|
-
});
|
|
749
|
-
await log('');
|
|
750
|
-
await log(' 🔍 What happened:');
|
|
751
|
-
await log(` After ${maxCompareAttempts} attempts, GitHub's compare API still shows no commits`);
|
|
752
|
-
await log(` between ${targetBranchForCompare} and ${branchName}.`);
|
|
753
|
-
await log('');
|
|
754
|
-
await log(' 💡 This usually means:');
|
|
755
|
-
await log(" • GitHub's backend systems haven't finished indexing the push");
|
|
756
|
-
await log(" • There's a temporary issue with GitHub's API");
|
|
757
|
-
await log(' • The commits may not have been pushed correctly');
|
|
758
|
-
await log('');
|
|
759
|
-
await log(' 🔧 How to fix:');
|
|
760
|
-
await log(' 1. Wait a minute and try creating the PR manually:');
|
|
761
|
-
// For fork mode, use the correct head reference format
|
|
762
|
-
if (argv.fork && forkedRepo) {
|
|
763
|
-
const forkUser = forkedRepo.split('/')[0];
|
|
764
|
-
await log(` gh pr create --draft --repo ${owner}/${repo} --base ${targetBranchForCompare} --head ${forkUser}:${branchName}`);
|
|
765
|
-
} else {
|
|
766
|
-
await log(` gh pr create --draft --repo ${owner}/${repo} --base ${targetBranchForCompare} --head ${branchName}`);
|
|
767
|
-
}
|
|
768
|
-
await log(' 2. Check if the branch exists on GitHub:');
|
|
769
|
-
// Show the correct repository where the branch was pushed
|
|
770
|
-
const branchRepo = argv.fork && forkedRepo ? forkedRepo : `${owner}/${repo}`;
|
|
771
|
-
await log(` https://github.com/${branchRepo}/tree/${branchName}`);
|
|
772
|
-
await log(' 3. Check the commit is on GitHub:');
|
|
773
|
-
// Use the correct head reference for the compare API check
|
|
774
|
-
if (argv.fork && forkedRepo) {
|
|
775
|
-
const forkUser = forkedRepo.split('/')[0];
|
|
776
|
-
await log(` gh api repos/${owner}/${repo}/compare/${targetBranchForCompare}...${forkUser}:${branchName} --paginate`);
|
|
777
|
-
} else {
|
|
778
|
-
await log(` gh api repos/${owner}/${repo}/compare/${targetBranchForCompare}...${branchName} --paginate`);
|
|
779
|
-
}
|
|
780
|
-
await log('');
|
|
781
|
-
|
|
782
|
-
throw new Error('GitHub compare API not ready - cannot create PR safely');
|
|
783
|
-
}
|
|
620
|
+
compareReady = await handleCompareApiNotReady({
|
|
621
|
+
argv,
|
|
622
|
+
forkedRepo,
|
|
623
|
+
owner,
|
|
624
|
+
repo,
|
|
625
|
+
issueNumber,
|
|
626
|
+
branchName,
|
|
627
|
+
targetBranchForCompare,
|
|
628
|
+
maxCompareAttempts,
|
|
629
|
+
compareResult,
|
|
630
|
+
log,
|
|
631
|
+
formatAligned,
|
|
632
|
+
$,
|
|
633
|
+
});
|
|
784
634
|
}
|
|
785
635
|
|
|
786
636
|
// Verify the push actually worked by checking GitHub API
|
package/src/solve.config.lib.mjs
CHANGED
|
@@ -285,7 +285,7 @@ export const SOLVE_OPTION_DEFINITIONS = {
|
|
|
285
285
|
},
|
|
286
286
|
think: {
|
|
287
287
|
type: 'string',
|
|
288
|
-
description: 'Thinking level hint. For Claude, translated to --thinking-budget for Claude Code >= 2.1.12 (off=0, low=~8000, medium=~16000, high=~24000, xhigh/max=31999) and to CLAUDE_CODE_EFFORT_LEVEL when supported. Opus 4.7
|
|
288
|
+
description: 'Thinking level hint. For Claude, translated to --thinking-budget for Claude Code >= 2.1.12 (off=0, low=~8000, medium=~16000, high=~24000, xhigh/max=31999) and to CLAUDE_CODE_EFFORT_LEVEL when supported. Opus 4.8/4.7 support xhigh and max; Opus 4.6/Sonnet 4.6/Mythos support max; Opus 4.5 uses high for xhigh/max. For Codex, mapped to reasoning effort (off=none, low=low, medium=medium, high=high, xhigh/max=xhigh).',
|
|
289
289
|
choices: ['off', 'low', 'medium', 'high', 'xhigh', 'max'],
|
|
290
290
|
default: undefined,
|
|
291
291
|
},
|
|
@@ -316,12 +316,12 @@ export const SOLVE_OPTION_DEFINITIONS = {
|
|
|
316
316
|
},
|
|
317
317
|
'fallback-model': {
|
|
318
318
|
type: 'string',
|
|
319
|
-
description: 'Fallback model to switch to on model capacity/overload errors. When supported, retries resume the same session with this model. Defaults: claude opus/opus-4-7 -> opus-4-6; codex gpt-5.5 -> gpt-5.4; all others unset.',
|
|
319
|
+
description: 'Fallback model to switch to on model capacity/overload errors. When supported, retries resume the same session with this model. Defaults: claude opus/opus-4-8 -> opus-4-7; claude opus-4-7 -> opus-4-6; codex gpt-5.5 -> gpt-5.4; all others unset.',
|
|
320
320
|
default: undefined,
|
|
321
321
|
},
|
|
322
322
|
'show-thinking-content': {
|
|
323
323
|
type: 'boolean',
|
|
324
|
-
description: 'Show thinking content in Claude responses. Opus 4.7 omits thinking content by default; this option opts in to receive summarized thinking blocks. Disabled by default. Only affects --tool claude.',
|
|
324
|
+
description: 'Show thinking content in Claude responses. Opus 4.7+ omits thinking content by default (applies to Opus 4.8 as well); this option opts in to receive summarized thinking blocks. Disabled by default. Only affects --tool claude.',
|
|
325
325
|
default: false,
|
|
326
326
|
},
|
|
327
327
|
'prompt-plan-sub-agent': {
|