@link-assistant/hive-mind 1.69.2 → 1.69.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 +15 -0
- package/package.json +1 -1
- package/src/agents-md-claude-support.lib.mjs +158 -0
- package/src/cancelled-ci-rerun.lib.mjs +87 -0
- package/src/claude.lib.mjs +29 -30
- package/src/solve.auto-merge.lib.mjs +54 -4
- package/src/solve.auto-pr-fork-diagnostic.lib.mjs +70 -0
- package/src/solve.auto-pr.lib.mjs +13 -6
- package/src/solve.branch.lib.mjs +122 -0
- package/src/solve.config.lib.mjs +5 -0
- package/src/solve.error-handlers.lib.mjs +5 -2
- package/src/solve.mjs +1 -1
- package/src/tool-comments.lib.mjs +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.69.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 105172b: Fix auto-PR creation failure on fork-of-fork repositories. When `solve` runs against an issue in a repository that is itself a GitHub fork and the user has direct write access, `gh pr create` previously resolved the base repository to the upstream parent (because `gh repo clone` auto-adds an `upstream` remote for forks), producing a misleading "No commits between" error. The auto-PR command builder now always passes `--repo ${owner}/${repo}` so the PR is created against the explicit target. The fatal error block also detects the failure mode and prints a fork-aware diagnostic with the resolved remotes and a manual recovery command.
|
|
8
|
+
- d89243f: Stabilize the version-info timing test that broke CI/CD by using the same 30 second reasonable bound as the broader version-info structure test. The version collector still runs commands in parallel, but individual commands can legally spend 5 seconds on a timeout and then another 5 seconds on a fallback, so the previous 10 second wall-clock assertion was too tight for GitHub-hosted runners.
|
|
9
|
+
- db56b5a: Sync custom fork base branches proactively. When a user passes `--base-branch` in fork mode, the solver now copies the requested branch from `upstream` to the user's fork before creating the issue branch, and falls back to the same recovery if branch creation still trips on a missing `origin/<baseBranch>`. This prevents the `fatal: 'origin/<baseBranch>' is not a commit` failure that surfaced for issue #1772 when an existing fork pre-dated upstream's custom branch.
|
|
10
|
+
|
|
11
|
+
## 1.69.3
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- d7f95e8: Add experimental `--auto-support-agents-md-as-claude-md` support for temporarily exposing AGENTS.md as CLAUDE.md during Claude runs.
|
|
16
|
+
- 890e81f: Stop auto-merge from waiting forever when cancelled CI cannot be re-run automatically.
|
|
17
|
+
|
|
3
18
|
## 1.69.2
|
|
4
19
|
|
|
5
20
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
const AGENTS_MD_FILENAMES = Object.freeze(['AGENTS.md', 'agents.md']);
|
|
2
|
+
const CLAUDE_MD_FILENAME = 'CLAUDE.md';
|
|
3
|
+
|
|
4
|
+
const noopLog = async () => {};
|
|
5
|
+
const fallbackFormatAligned = (_icon, label, value) => `${label} ${value}`;
|
|
6
|
+
|
|
7
|
+
const readFileIfExists = async (fs, filePath) => {
|
|
8
|
+
try {
|
|
9
|
+
return { exists: true, content: await fs.readFile(filePath, 'utf8') };
|
|
10
|
+
} catch (error) {
|
|
11
|
+
if (error.code === 'ENOENT') return { exists: false, content: null };
|
|
12
|
+
throw error;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const userInputMentionsClaudeMd = input => /\bCLAUDE\.md\b/i.test(String(input || ''));
|
|
17
|
+
|
|
18
|
+
export const findAgentsMdFile = async ({ tempDir, fs, path }) => {
|
|
19
|
+
for (const fileName of AGENTS_MD_FILENAMES) {
|
|
20
|
+
const filePath = path.join(tempDir, fileName);
|
|
21
|
+
const result = await readFileIfExists(fs, filePath);
|
|
22
|
+
if (result.exists) return { fileName, filePath, content: result.content };
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const prepareAgentsMdAsClaudeMd = async params => {
|
|
28
|
+
const { tempDir, argv, prompt = '', fs, path, log = noopLog, formatAligned = fallbackFormatAligned } = params;
|
|
29
|
+
const state = {
|
|
30
|
+
enabled: !!argv?.autoSupportAgentsMdAsClaudeMd,
|
|
31
|
+
created: false,
|
|
32
|
+
cleanupCandidate: false,
|
|
33
|
+
userInputMentionsClaudeMd: userInputMentionsClaudeMd(prompt),
|
|
34
|
+
claudePath: path.join(tempDir, CLAUDE_MD_FILENAME),
|
|
35
|
+
agentsPath: null,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
if (!state.enabled) return state;
|
|
39
|
+
|
|
40
|
+
const tool = argv?.tool || 'claude';
|
|
41
|
+
if (tool !== 'claude') {
|
|
42
|
+
await log(' AGENTS.md as CLAUDE.md support skipped: only supported for --tool claude', { verbose: true });
|
|
43
|
+
return { ...state, skippedReason: 'non-claude-tool' };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const agentsFile = await findAgentsMdFile({ tempDir, fs, path });
|
|
47
|
+
if (!agentsFile) {
|
|
48
|
+
await log(' AGENTS.md as CLAUDE.md support enabled, but no AGENTS.md file was found', { verbose: true });
|
|
49
|
+
return { ...state, skippedReason: 'missing-agents-md' };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
state.agentsPath = agentsFile.filePath;
|
|
53
|
+
const claudeFile = await readFileIfExists(fs, state.claudePath);
|
|
54
|
+
if (claudeFile.exists) {
|
|
55
|
+
if (claudeFile.content === agentsFile.content) {
|
|
56
|
+
state.cleanupCandidate = !state.userInputMentionsClaudeMd;
|
|
57
|
+
const action = state.cleanupCandidate ? 'will remove it after Claude exits' : 'leaving it untouched because user input mentions CLAUDE.md';
|
|
58
|
+
await log(` CLAUDE.md already matches AGENTS.md; ${action}`, { verbose: true });
|
|
59
|
+
return { ...state, skippedReason: 'claude-md-already-matches-agents-md' };
|
|
60
|
+
}
|
|
61
|
+
await log(' Existing CLAUDE.md differs from AGENTS.md; leaving it untouched', { verbose: true });
|
|
62
|
+
return { ...state, skippedReason: 'claude-md-differs' };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
await fs.writeFile(state.claudePath, agentsFile.content);
|
|
66
|
+
state.created = true;
|
|
67
|
+
await log(formatAligned('AGENTS.md', 'Temporary CLAUDE.md:', `created from ${agentsFile.fileName}`));
|
|
68
|
+
return state;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const gitStatusForClaudeMd = async ({ $, tempDir }) => {
|
|
72
|
+
if (!$) return '';
|
|
73
|
+
const result = await $({ cwd: tempDir })`git status --porcelain -- ${CLAUDE_MD_FILENAME} 2>&1`;
|
|
74
|
+
return result.stdout?.toString().trim() || '';
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const isClaudeMdTracked = async ({ $, tempDir }) => {
|
|
78
|
+
if (!$) return false;
|
|
79
|
+
const result = await $({ cwd: tempDir })`git ls-files --error-unmatch -- ${CLAUDE_MD_FILENAME} 2>&1`;
|
|
80
|
+
return result.code === 0;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const removeTrackedClaudeMd = async ({ $, tempDir }) => {
|
|
84
|
+
if (!$) return { code: 1, stdout: '', stderr: 'No git runner provided' };
|
|
85
|
+
return await $({ cwd: tempDir })`git rm -f -- ${CLAUDE_MD_FILENAME} 2>&1`;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const commitClaudeMdRemoval = async ({ $, tempDir }) => {
|
|
89
|
+
const message = 'Remove temporary CLAUDE.md copy from AGENTS.md';
|
|
90
|
+
return await $({ cwd: tempDir })`git commit -m ${message} -- ${CLAUDE_MD_FILENAME} 2>&1`;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const cleanupAgentsMdAsClaudeMd = async params => {
|
|
94
|
+
const { state, tempDir, branchName, fs, path, $, log = noopLog, formatAligned = fallbackFormatAligned } = params;
|
|
95
|
+
if (!state?.created && !state?.cleanupCandidate) return { action: 'skipped' };
|
|
96
|
+
|
|
97
|
+
const claudePath = state.claudePath || path.join(tempDir, CLAUDE_MD_FILENAME);
|
|
98
|
+
const claudeFile = await readFileIfExists(fs, claudePath);
|
|
99
|
+
if (!claudeFile.exists) return { action: 'already-removed' };
|
|
100
|
+
|
|
101
|
+
const agentsFile = await findAgentsMdFile({ tempDir, fs, path });
|
|
102
|
+
if (!agentsFile || claudeFile.content !== agentsFile.content) {
|
|
103
|
+
await log(' Temporary CLAUDE.md changed or AGENTS.md is missing; leaving it untouched', { verbose: true });
|
|
104
|
+
return { action: 'left-modified' };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const status = await gitStatusForClaudeMd({ $, tempDir });
|
|
108
|
+
const tracked = await isClaudeMdTracked({ $, tempDir });
|
|
109
|
+
const isUntracked = status.split('\n').some(line => line.startsWith('??'));
|
|
110
|
+
|
|
111
|
+
if (isUntracked || (!tracked && !status)) {
|
|
112
|
+
await fs.rm(claudePath, { force: true });
|
|
113
|
+
await log(formatAligned('AGENTS.md', 'Temporary CLAUDE.md:', 'removed'));
|
|
114
|
+
return { action: 'removed-untracked' };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (state.userInputMentionsClaudeMd) {
|
|
118
|
+
await log(' Temporary CLAUDE.md matches AGENTS.md but user input mentions CLAUDE.md; leaving it untouched', { verbose: true });
|
|
119
|
+
return { action: 'left-user-mentioned-claude-md' };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const rmResult = await removeTrackedClaudeMd({ $, tempDir });
|
|
123
|
+
if (rmResult.code !== 0) {
|
|
124
|
+
await log(` Warning: could not remove temporary CLAUDE.md from git: ${rmResult.stderr || rmResult.stdout}`, { verbose: true });
|
|
125
|
+
return { action: 'remove-failed' };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const postRemoveStatus = await gitStatusForClaudeMd({ $, tempDir });
|
|
129
|
+
if (!postRemoveStatus) {
|
|
130
|
+
await log(formatAligned('AGENTS.md', 'Temporary CLAUDE.md:', 'removed from git index'));
|
|
131
|
+
return { action: 'removed-staged-addition' };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const commitResult = await commitClaudeMdRemoval({ $, tempDir });
|
|
135
|
+
if (commitResult.code !== 0) {
|
|
136
|
+
await log(` Warning: temporary CLAUDE.md was removed locally but commit failed: ${commitResult.stderr || commitResult.stdout}`, { verbose: true });
|
|
137
|
+
return { action: 'removed-uncommitted' };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (branchName) {
|
|
141
|
+
const pushResult = await $({ cwd: tempDir })`git push origin ${branchName} 2>&1`;
|
|
142
|
+
if (pushResult.code !== 0) {
|
|
143
|
+
await log(` Warning: temporary CLAUDE.md removal commit was created but push failed: ${pushResult.stderr || pushResult.stdout}`, { verbose: true });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
await log(formatAligned('AGENTS.md', 'Temporary CLAUDE.md:', 'removed from committed changes'));
|
|
148
|
+
return { action: 'removed-committed-copy' };
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export const withAgentsMdAsClaudeMd = async (params, execute) => {
|
|
152
|
+
const state = await prepareAgentsMdAsClaudeMd(params);
|
|
153
|
+
try {
|
|
154
|
+
return await execute();
|
|
155
|
+
} finally {
|
|
156
|
+
await cleanupAgentsMdAsClaudeMd({ ...params, state });
|
|
157
|
+
}
|
|
158
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Helpers for cancelled/stale CI handling in auto-merge mode.
|
|
5
|
+
*
|
|
6
|
+
* GitHub can expose several operational cases as a cancelled/stale status:
|
|
7
|
+
* manual cancellation, concurrency cancellation, queue/run limits, or a
|
|
8
|
+
* workflow/job timeout. If hive-mind cannot re-trigger the run, a human has to
|
|
9
|
+
* inspect the logs because polling the same conclusion will not make progress.
|
|
10
|
+
*
|
|
11
|
+
* @see https://github.com/link-assistant/hive-mind/issues/1769
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { CANCELLED_CI_REVIEW_MARKER } from './tool-comments.lib.mjs';
|
|
15
|
+
|
|
16
|
+
const CANCELLED_OR_STALE_CONCLUSIONS = new Set(['cancelled', 'stale']);
|
|
17
|
+
|
|
18
|
+
export { CANCELLED_CI_REVIEW_MARKER };
|
|
19
|
+
|
|
20
|
+
export const getRetriggerableWorkflowRuns = (runs = []) => runs.filter(run => CANCELLED_OR_STALE_CONCLUSIONS.has(run?.conclusion));
|
|
21
|
+
|
|
22
|
+
export const shouldStopForCancelledCIReview = ({ retriggerableRuns = [], rerunTriggered = false, rerunFailures = [] }) => {
|
|
23
|
+
if (rerunTriggered) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return retriggerableRuns.length === 0 || rerunFailures.length > 0;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const formatMarkdownList = (items, fallback) => {
|
|
31
|
+
if (!items || items.length === 0) {
|
|
32
|
+
return `- ${fallback}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return items.map(item => `- ${item}`).join('\n');
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const formatRunReference = run => {
|
|
39
|
+
if (!run) {
|
|
40
|
+
return 'Unknown workflow run';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const name = run.name || run.path || (run.id ? `Workflow run ${run.id}` : 'Workflow run');
|
|
44
|
+
const runId = run.id ? ` (${run.id})` : '';
|
|
45
|
+
const status = [run.status, run.conclusion].filter(Boolean).join('/');
|
|
46
|
+
const statusPart = status ? ` [${status}]` : '';
|
|
47
|
+
const urlPart = run.html_url ? ` - ${run.html_url}` : '';
|
|
48
|
+
|
|
49
|
+
return `${name}${runId}${statusPart}${urlPart}`;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const formatRerunFailure = failure => {
|
|
53
|
+
const runPart = formatRunReference(failure?.run);
|
|
54
|
+
const error = failure?.error || 'Unknown error';
|
|
55
|
+
return `${runPart}: ${error}`;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const buildCancelledCIReviewComment = ({ blocker, runs = [], rerunFailures = [], rerunAttempted = false, sha }) => {
|
|
59
|
+
const cancelledDetails = blocker?.details || [];
|
|
60
|
+
const effectiveSha = sha || blocker?.sha;
|
|
61
|
+
const shaLine = effectiveSha ? `\n\n**Commit:** ${effectiveSha}` : '';
|
|
62
|
+
const rerunSummary = rerunAttempted ? 'Automatic re-run was attempted, but no workflow run was successfully re-triggered.' : 'Automatic re-run was not possible.';
|
|
63
|
+
|
|
64
|
+
return `## ${CANCELLED_CI_REVIEW_MARKER}
|
|
65
|
+
|
|
66
|
+
Hive Mind detected cancelled or stale CI/CD checks and cannot get them running automatically.${shaLine}
|
|
67
|
+
|
|
68
|
+
**Cancelled checks**
|
|
69
|
+
${formatMarkdownList(cancelledDetails, 'No cancelled check details were available.')}
|
|
70
|
+
|
|
71
|
+
**Workflow runs inspected**
|
|
72
|
+
${formatMarkdownList(runs.map(formatRunReference), 'No cancelled/stale workflow run was found for this commit SHA.')}
|
|
73
|
+
|
|
74
|
+
**Automatic re-run result**
|
|
75
|
+
${rerunSummary}
|
|
76
|
+
${formatMarkdownList(rerunFailures.map(formatRerunFailure), 'No successful automatic re-run was recorded.')}
|
|
77
|
+
|
|
78
|
+
**Action required**
|
|
79
|
+
1. Review the cancelled CI logs to decide whether this was a real timeout/failure or a deliberate manual cancellation.
|
|
80
|
+
2. If the cancelled check is required, re-run the workflow manually from GitHub Actions or push a new commit.
|
|
81
|
+
3. If the cancellation was deliberate and non-blocking, decide whether this PR can be merged outside automation.
|
|
82
|
+
|
|
83
|
+
If workflow/job \`timeout-minutes\` or a runner execution limit caused the cancellation, treat it as a CI failure and fix the timeout, test, or infrastructure before merging.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
Hive Mind is stopping because continuing to poll the same cancelled/stale check would not change the mergeability result.`;
|
|
87
|
+
};
|
package/src/claude.lib.mjs
CHANGED
|
@@ -26,6 +26,7 @@ import { ensureClaudeQuietConfig } from './claude-quiet-config.lib.mjs';
|
|
|
26
26
|
import { fetchModelInfo } from './model-info.lib.mjs';
|
|
27
27
|
import { classifyRetryableError, maybeSwitchToFallbackModel } from './tool-retry.lib.mjs';
|
|
28
28
|
import { resolveSubSessionSize } from './sub-session-size.lib.mjs'; // Issue #1706
|
|
29
|
+
import { withAgentsMdAsClaudeMd } from './agents-md-claude-support.lib.mjs';
|
|
29
30
|
export { availableModels }; // Re-export for backward compatibility
|
|
30
31
|
export { fetchModelInfo };
|
|
31
32
|
const showResumeCommand = async (sessionId, tempDir, claudePath, model, log) => {
|
|
@@ -294,7 +295,6 @@ export const executeClaude = async params => {
|
|
|
294
295
|
if (argv.verbose) {
|
|
295
296
|
await log(`👁️ Model vision capability: ${modelSupportsVision ? 'supported' : 'not supported'}`, { verbose: true });
|
|
296
297
|
}
|
|
297
|
-
// Build the user prompt
|
|
298
298
|
const prompt = buildUserPrompt({
|
|
299
299
|
issueUrl,
|
|
300
300
|
issueNumber,
|
|
@@ -313,7 +313,6 @@ export const executeClaude = async params => {
|
|
|
313
313
|
argv,
|
|
314
314
|
claudeVersion: getClaudeVersion(),
|
|
315
315
|
});
|
|
316
|
-
// Build the system prompt
|
|
317
316
|
const systemPrompt = buildSystemPrompt({
|
|
318
317
|
owner,
|
|
319
318
|
repo,
|
|
@@ -329,7 +328,6 @@ export const executeClaude = async params => {
|
|
|
329
328
|
argv,
|
|
330
329
|
modelSupportsVision,
|
|
331
330
|
});
|
|
332
|
-
// Log prompt details in verbose mode
|
|
333
331
|
if (argv.verbose) {
|
|
334
332
|
await log('\n📝 Final prompt structure:', { verbose: true });
|
|
335
333
|
await log(` Characters: ${prompt.length}`, { verbose: true });
|
|
@@ -349,35 +347,36 @@ export const executeClaude = async params => {
|
|
|
349
347
|
await log('---END SYSTEM PROMPT---', { verbose: true });
|
|
350
348
|
}
|
|
351
349
|
}
|
|
352
|
-
// Escape prompts for shell usage
|
|
353
350
|
const escapedPrompt = prompt.replace(/"/g, '\\"').replace(/\$/g, '\\$');
|
|
354
351
|
const escapedSystemPrompt = systemPrompt.replace(/"/g, '\\"').replace(/\$/g, '\\$');
|
|
355
|
-
|
|
356
|
-
return await
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
352
|
+
|
|
353
|
+
return await withAgentsMdAsClaudeMd({ tempDir, branchName, argv, prompt, fs, path, $, log, formatAligned }, () =>
|
|
354
|
+
executeClaudeCommand({
|
|
355
|
+
tempDir,
|
|
356
|
+
branchName,
|
|
357
|
+
prompt,
|
|
358
|
+
systemPrompt,
|
|
359
|
+
escapedPrompt,
|
|
360
|
+
escapedSystemPrompt,
|
|
361
|
+
argv,
|
|
362
|
+
log,
|
|
363
|
+
setLogFile,
|
|
364
|
+
getLogFile,
|
|
365
|
+
formatAligned,
|
|
366
|
+
getResourceSnapshot,
|
|
367
|
+
forkedRepo,
|
|
368
|
+
feedbackLines,
|
|
369
|
+
claudePath,
|
|
370
|
+
$,
|
|
371
|
+
// For interactive mode
|
|
372
|
+
owner,
|
|
373
|
+
repo,
|
|
374
|
+
prNumber,
|
|
375
|
+
// Issue #1708: forwarded so the bidirectional handler can poll
|
|
376
|
+
// issue title/body changes and uncommitted changes during the session.
|
|
377
|
+
issueNumber,
|
|
378
|
+
})
|
|
379
|
+
);
|
|
381
380
|
};
|
|
382
381
|
/** Check if a model supports vision (image input) using models.dev API @returns {Promise<boolean>} */
|
|
383
382
|
export const checkModelVisionCapability = async modelId => {
|
|
@@ -55,6 +55,10 @@ import { limitReset } from './config.lib.mjs';
|
|
|
55
55
|
const autoMergeHelpers = await import('./solve.auto-merge-helpers.lib.mjs');
|
|
56
56
|
const { checkForExistingComment, checkForNonBotComments, getMergeBlockers } = autoMergeHelpers;
|
|
57
57
|
|
|
58
|
+
// Issue #1769: cancelled/stale CI re-run failures need a human action stop, not polling forever.
|
|
59
|
+
const cancelledCiRerunLib = await import('./cancelled-ci-rerun.lib.mjs');
|
|
60
|
+
const { buildCancelledCIReviewComment, getRetriggerableWorkflowRuns, shouldStopForCancelledCIReview } = cancelledCiRerunLib;
|
|
61
|
+
|
|
58
62
|
// Issue #1625: Shared marker constants + posting/tracking helpers
|
|
59
63
|
const toolComments = await import('./tool-comments.lib.mjs');
|
|
60
64
|
const { READY_TO_MERGE_MARKER, AUTO_RESTART_MARKER, AUTO_MERGED_MARKER, postTrackedComment } = toolComments;
|
|
@@ -432,23 +436,37 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
|
|
|
432
436
|
if (cancelledBlocker && !billingBlocker) {
|
|
433
437
|
await log('');
|
|
434
438
|
await log(formatAligned('🔄', 'CANCELLED CI/CD CHECKS DETECTED', ''));
|
|
435
|
-
await log(formatAligned('', 'Cancelled checks:', cancelledBlocker.details.join(', '), 2));
|
|
439
|
+
await log(formatAligned('', 'Cancelled checks:', (cancelledBlocker.details || []).join(', '), 2));
|
|
436
440
|
|
|
437
441
|
// Attempt to re-trigger the cancelled/stale workflow runs
|
|
438
442
|
const sha = cancelledBlocker.sha;
|
|
443
|
+
let runs = [];
|
|
444
|
+
let retriggerable = [];
|
|
445
|
+
let rerunTriggered = false;
|
|
446
|
+
let rerunAttempted = false;
|
|
447
|
+
const rerunFailures = [];
|
|
448
|
+
|
|
439
449
|
if (sha) {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
450
|
+
runs = await getWorkflowRunsForSha(owner, repo, sha, argv.verbose);
|
|
451
|
+
retriggerable = getRetriggerableWorkflowRuns(runs);
|
|
452
|
+
|
|
453
|
+
if (retriggerable.length === 0) {
|
|
454
|
+
await log(formatAligned('', '⚠️ No cancelled/stale workflow run found for this SHA', '', 2));
|
|
455
|
+
rerunFailures.push({
|
|
456
|
+
error: 'No cancelled/stale workflow run was found for this commit SHA.',
|
|
457
|
+
});
|
|
458
|
+
}
|
|
443
459
|
|
|
444
460
|
for (const run of retriggerable) {
|
|
445
461
|
await log(formatAligned('', `Re-triggering workflow "${run.name}" (${run.id})...`, '', 2));
|
|
462
|
+
rerunAttempted = true;
|
|
446
463
|
const rerunResult = await rerunWorkflowRun(owner, repo, run.id, argv.verbose);
|
|
447
464
|
if (rerunResult.success) {
|
|
448
465
|
await log(formatAligned('', `✅ Re-triggered: ${run.name}`, '', 2));
|
|
449
466
|
rerunTriggered = true;
|
|
450
467
|
} else {
|
|
451
468
|
await log(formatAligned('', `⚠️ Could not re-trigger ${run.name}: ${rerunResult.error}`, '', 2));
|
|
469
|
+
rerunFailures.push({ run, error: rerunResult.error });
|
|
452
470
|
}
|
|
453
471
|
}
|
|
454
472
|
|
|
@@ -457,6 +475,38 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
|
|
|
457
475
|
// Don't restart AI - just wait for re-triggered jobs to complete
|
|
458
476
|
// The next iteration of the loop will check the new status
|
|
459
477
|
}
|
|
478
|
+
} else {
|
|
479
|
+
await log(formatAligned('', '⚠️ Cancelled CI blocker did not include a commit SHA', '', 2));
|
|
480
|
+
rerunFailures.push({
|
|
481
|
+
error: 'Cancelled CI blocker did not include a commit SHA, so automatic workflow re-run could not identify the run.',
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (shouldStopForCancelledCIReview({ retriggerableRuns: retriggerable, rerunTriggered, rerunFailures })) {
|
|
486
|
+
await log(formatAligned('🛑', 'CANCELLED CI/CD NEEDS HUMAN REVIEW', 'Automatic re-run could not be started'));
|
|
487
|
+
|
|
488
|
+
try {
|
|
489
|
+
const commentBody = buildCancelledCIReviewComment({
|
|
490
|
+
blocker: cancelledBlocker,
|
|
491
|
+
runs,
|
|
492
|
+
rerunFailures,
|
|
493
|
+
rerunAttempted,
|
|
494
|
+
sha,
|
|
495
|
+
});
|
|
496
|
+
await postTrackedComment({ $, owner, repo, targetNumber: prNumber, body: commentBody });
|
|
497
|
+
await log(formatAligned('', '💬 Posted cancelled CI review notification to PR', '', 2));
|
|
498
|
+
} catch (commentError) {
|
|
499
|
+
reportError(commentError, {
|
|
500
|
+
context: 'post_cancelled_ci_review_comment',
|
|
501
|
+
owner,
|
|
502
|
+
repo,
|
|
503
|
+
prNumber,
|
|
504
|
+
operation: 'comment_on_pr',
|
|
505
|
+
});
|
|
506
|
+
await log(formatAligned('', '⚠️ Could not post cancelled CI review comment to PR', '', 2));
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return { success: false, reason: 'ci_cancelled_requires_review', latestSessionId, latestAnthropicCost };
|
|
460
510
|
}
|
|
461
511
|
// Don't set shouldRestart for cancelled checks - wait for re-triggered jobs instead
|
|
462
512
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fork-aware diagnostic for the auto-PR fatal error block.
|
|
3
|
+
*
|
|
4
|
+
* Issue #1774: when `gh pr create` fails with "No commits between" or
|
|
5
|
+
* "Head sha can't be blank", the most common cause is base-repo resolution
|
|
6
|
+
* picking the upstream parent of a fork (because `gh repo clone` auto-adds
|
|
7
|
+
* an `upstream` remote for forks). This helper inspects the local remotes
|
|
8
|
+
* and prints a self-explanatory diagnostic.
|
|
9
|
+
*
|
|
10
|
+
* Extracted from solve.auto-pr.lib.mjs to keep that file under the 1500-line
|
|
11
|
+
* CI cap.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {object} params
|
|
16
|
+
* @param {string} params.errorMessage - prError.message from the auto-PR catch.
|
|
17
|
+
* @param {string} params.tempDir
|
|
18
|
+
* @param {string} params.owner
|
|
19
|
+
* @param {string} params.repo
|
|
20
|
+
* @param {string} params.defaultBranch
|
|
21
|
+
* @param {string} params.branchName
|
|
22
|
+
* @param {string|number} params.issueNumber
|
|
23
|
+
* @param {(msg: string, opts?: object) => Promise<void>} params.log
|
|
24
|
+
* @param {Function} params.$ - command-stream tagged function from solve.
|
|
25
|
+
* @param {Function} params.reportError
|
|
26
|
+
*/
|
|
27
|
+
export async function emitForkAwareDiagnostic({ errorMessage, tempDir, owner, repo, defaultBranch, branchName, issueNumber, log, $, reportError }) {
|
|
28
|
+
const errMsg = errorMessage || '';
|
|
29
|
+
if (!errMsg.includes('No commits between') && !errMsg.includes("Head sha can't be blank")) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const remotesResult = await $({ cwd: tempDir, silent: true })`git remote -v`;
|
|
35
|
+
const remotesText = remotesResult.code === 0 ? remotesResult.stdout.toString().trim() : '';
|
|
36
|
+
const originLine = remotesText.split('\n').find(line => line.startsWith('origin\t') && line.includes('(fetch)'));
|
|
37
|
+
const upstreamLine = remotesText.split('\n').find(line => line.startsWith('upstream\t') && line.includes('(fetch)'));
|
|
38
|
+
|
|
39
|
+
await log(' 🔬 Fork-aware diagnostic (Issue #1774):');
|
|
40
|
+
await log(` Target repository: ${owner}/${repo}`);
|
|
41
|
+
if (originLine) {
|
|
42
|
+
await log(` origin remote: ${originLine.replace(/^origin\t/, '').replace(/\s+\(fetch\)$/, '')}`);
|
|
43
|
+
}
|
|
44
|
+
if (upstreamLine) {
|
|
45
|
+
await log(` upstream remote: ${upstreamLine.replace(/^upstream\t/, '').replace(/\s+\(fetch\)$/, '')}`);
|
|
46
|
+
await log('');
|
|
47
|
+
await log(' `gh repo clone` automatically adds an `upstream` remote when the');
|
|
48
|
+
await log(' cloned repository is a fork. Without --repo, `gh pr create`');
|
|
49
|
+
await log(' resolves the base to that upstream parent instead of the fork');
|
|
50
|
+
await log(' where this branch was pushed, producing the misleading');
|
|
51
|
+
await log(' "No commits between" error. This version already pins --repo');
|
|
52
|
+
await log(' to the explicit target, so a fresh `solve` invocation should');
|
|
53
|
+
await log(' succeed. See docs/case-studies/issue-1774/README.md.');
|
|
54
|
+
} else {
|
|
55
|
+
await log(' (no `upstream` remote found locally)');
|
|
56
|
+
}
|
|
57
|
+
await log('');
|
|
58
|
+
await log(' Manual recovery command:');
|
|
59
|
+
await log(` gh pr create --draft --base ${defaultBranch} --head ${branchName} --repo ${owner}/${repo}`);
|
|
60
|
+
await log('');
|
|
61
|
+
} catch (diagError) {
|
|
62
|
+
reportError(diagError, {
|
|
63
|
+
context: 'auto_pr_fork_diagnostic',
|
|
64
|
+
issueNumber,
|
|
65
|
+
operation: 'collect_fork_diagnostic',
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export default { emitForkAwareDiagnostic };
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { closingIssueNumbersContain, parseClosingIssueNumbers } from './pr-issue-linking.lib.mjs';
|
|
7
7
|
import { buildPushRejectionExplanation, getRemoteBranchDivergenceSnapshot, synchronizeExistingIssueBranchBeforeAutoPrCreation } from './solve.branch-divergence.lib.mjs';
|
|
8
|
+
import { emitForkAwareDiagnostic } from './solve.auto-pr-fork-diagnostic.lib.mjs';
|
|
8
9
|
|
|
9
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.
|
|
10
11
|
|
|
@@ -1112,13 +1113,15 @@ ${prBody}`,
|
|
|
1112
1113
|
// Build command with optional assignee and handle forks
|
|
1113
1114
|
// Note: targetBranch is already defined above
|
|
1114
1115
|
// IMPORTANT: Use --title-file instead of --title to avoid shell parsing issues with special characters
|
|
1116
|
+
// --repo is always passed (Issue #1774) so a fork-of-fork target
|
|
1117
|
+
// does not silently switch to the upstream parent via `gh repo clone`'s
|
|
1118
|
+
// auto-added `upstream` remote.
|
|
1115
1119
|
let command;
|
|
1116
1120
|
if (argv.fork && forkedRepo) {
|
|
1117
|
-
// For forks, specify the full head reference
|
|
1118
1121
|
const forkUser = forkedRepo.split('/')[0];
|
|
1119
1122
|
command = `cd "${tempDir}" && gh pr create --draft --title "$(cat '${prTitleFile}')" --body-file "${prBodyFile}" --base ${targetBranch} --head ${forkUser}:${branchName} --repo ${owner}/${repo}`;
|
|
1120
1123
|
} else {
|
|
1121
|
-
command = `cd "${tempDir}" && gh pr create --draft --title "$(cat '${prTitleFile}')" --body-file "${prBodyFile}" --base ${targetBranch} --head ${branchName}`;
|
|
1124
|
+
command = `cd "${tempDir}" && gh pr create --draft --title "$(cat '${prTitleFile}')" --body-file "${prBodyFile}" --base ${targetBranch} --head ${branchName} --repo ${owner}/${repo}`;
|
|
1122
1125
|
}
|
|
1123
1126
|
// Only add assignee if user has permissions
|
|
1124
1127
|
if (currentUser && canAssign) {
|
|
@@ -1157,12 +1160,12 @@ ${prBody}`,
|
|
|
1157
1160
|
});
|
|
1158
1161
|
await log(' Retrying PR creation without assignee...');
|
|
1159
1162
|
|
|
1160
|
-
// Rebuild command without --assignee flag
|
|
1163
|
+
// Rebuild command without --assignee flag (Issue #1774: --repo always pinned)
|
|
1161
1164
|
if (argv.fork && forkedRepo) {
|
|
1162
1165
|
const forkUser = forkedRepo.split('/')[0];
|
|
1163
1166
|
command = `cd "${tempDir}" && gh pr create --draft --title "$(cat '${prTitleFile}')" --body-file "${prBodyFile}" --base ${targetBranch} --head ${forkUser}:${branchName} --repo ${owner}/${repo}`;
|
|
1164
1167
|
} else {
|
|
1165
|
-
command = `cd "${tempDir}" && gh pr create --draft --title "$(cat '${prTitleFile}')" --body-file "${prBodyFile}" --base ${targetBranch} --head ${branchName}`;
|
|
1168
|
+
command = `cd "${tempDir}" && gh pr create --draft --title "$(cat '${prTitleFile}')" --body-file "${prBodyFile}" --base ${targetBranch} --head ${branchName} --repo ${owner}/${repo}`;
|
|
1166
1169
|
}
|
|
1167
1170
|
|
|
1168
1171
|
if (argv.verbose) {
|
|
@@ -1460,6 +1463,10 @@ ${prBody}`,
|
|
|
1460
1463
|
await log(' 🔍 What happened:');
|
|
1461
1464
|
await log(` ${prError.message}`);
|
|
1462
1465
|
await log('');
|
|
1466
|
+
|
|
1467
|
+
// Issue #1774: fork-base resolution failure diagnostic.
|
|
1468
|
+
await emitForkAwareDiagnostic({ errorMessage: prError.message, tempDir, owner, repo, defaultBranch, branchName, issueNumber, log, $, reportError });
|
|
1469
|
+
|
|
1463
1470
|
await log(' 💡 The solve command cannot continue without a pull request.');
|
|
1464
1471
|
await log('');
|
|
1465
1472
|
await log(' 🔧 How to fix:');
|
|
@@ -1470,14 +1477,14 @@ ${prBody}`,
|
|
|
1470
1477
|
await log('');
|
|
1471
1478
|
await log(' Option 2: Create PR manually first');
|
|
1472
1479
|
await log(` cd ${tempDir}`);
|
|
1473
|
-
await log(` gh pr create --draft --title "Fix issue #${issueNumber}" --body "Fixes #${issueNumber}"`);
|
|
1480
|
+
await log(` gh pr create --draft --title "Fix issue #${issueNumber}" --body "Fixes #${issueNumber}" --repo ${owner}/${repo}`);
|
|
1474
1481
|
await log(` Then use: ./solve.mjs "${issueUrl}" --continue`);
|
|
1475
1482
|
await log('');
|
|
1476
1483
|
await log(' Option 3: Debug the issue');
|
|
1477
1484
|
await log(` cd ${tempDir}`);
|
|
1478
1485
|
await log(' git status');
|
|
1479
1486
|
await log(' git log --oneline -5');
|
|
1480
|
-
await log(
|
|
1487
|
+
await log(` gh pr create --draft --repo ${owner}/${repo} # Try manually to see detailed error`);
|
|
1481
1488
|
await log('');
|
|
1482
1489
|
|
|
1483
1490
|
// Re-throw the error to stop execution - use prError.message directly
|
package/src/solve.branch.lib.mjs
CHANGED
|
@@ -205,6 +205,105 @@ export function validateBranchInArgs(args) {
|
|
|
205
205
|
return null;
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
function isMissingOriginBaseRefError(errorOutput, baseBranch) {
|
|
209
|
+
return errorOutput.includes(`origin/${baseBranch}`) && (errorOutput.includes('is not a commit') || errorOutput.includes('not a valid object name') || errorOutput.includes('unknown revision'));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function hasUpstreamRemote(tempDir, $) {
|
|
213
|
+
const result = await $({ cwd: tempDir })`git remote get-url upstream 2>/dev/null`;
|
|
214
|
+
return result.code === 0;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function originHasBaseBranch(tempDir, baseBranch, $) {
|
|
218
|
+
const result = await $({ cwd: tempDir })`git show-ref --verify --quiet refs/remotes/origin/${baseBranch}`;
|
|
219
|
+
return result.code === 0;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Ensure the requested base branch exists on the fork (origin) by syncing it from upstream.
|
|
224
|
+
* Returns true if origin/<baseBranch> exists at the end of the call (was already there or was synced).
|
|
225
|
+
* Returns false if syncing was attempted and failed (caller should propagate the original error).
|
|
226
|
+
*
|
|
227
|
+
* Design note (issue #1772 follow-up): when working in fork mode against a public upstream, the user
|
|
228
|
+
* expects their fork to mirror the upstream branch they want to base work on. Before relying on
|
|
229
|
+
* git's create-from-origin path, we proactively copy the upstream branch into the fork so that the
|
|
230
|
+
* branch creation, the later PR comparison, and any ahead/behind checks all see a consistent state.
|
|
231
|
+
*/
|
|
232
|
+
async function ensureBaseBranchInFork({ baseBranch, tempDir, log, formatAligned, $, reason = 'proactive' }) {
|
|
233
|
+
if (!(await hasUpstreamRemote(tempDir, $))) {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (reason === 'proactive') {
|
|
238
|
+
await log(`${formatAligned('🔄', 'Syncing base branch:', `ensuring origin/${baseBranch} matches upstream`)}`);
|
|
239
|
+
} else {
|
|
240
|
+
await log(`${formatAligned('🔄', 'Base branch not in fork:', `checking upstream/${baseBranch}`)}`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const fetchResult = await $({ cwd: tempDir })`git fetch upstream`;
|
|
244
|
+
if (fetchResult.code !== 0) {
|
|
245
|
+
await log(`${formatAligned('⚠️', 'Warning:', 'Failed to fetch upstream base branch')}`, { level: 'warning' });
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const upstreamRefResult = await $({ cwd: tempDir })`git show-ref --verify --quiet refs/remotes/upstream/${baseBranch}`;
|
|
250
|
+
if (upstreamRefResult.code !== 0) {
|
|
251
|
+
await log(`${formatAligned('⚠️', 'Warning:', `Base branch not found in upstream/${baseBranch}`)}`, { level: 'warning' });
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
await log(`${formatAligned('✅', 'Base branch found:', `upstream/${baseBranch}`)}`);
|
|
256
|
+
const checkoutBaseResult = await $({ cwd: tempDir })`git checkout -B ${baseBranch} upstream/${baseBranch}`;
|
|
257
|
+
if (checkoutBaseResult.code !== 0) {
|
|
258
|
+
await log(`${formatAligned('⚠️', 'Warning:', `Failed to prepare local ${baseBranch}`)}`, { level: 'warning' });
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
await log(`${formatAligned('🔄', 'Pushing to fork:', `${baseBranch} branch`)}`);
|
|
263
|
+
const pushBaseResult = await $({ cwd: tempDir })`git push origin ${baseBranch} 2>&1`;
|
|
264
|
+
if (pushBaseResult.code !== 0) {
|
|
265
|
+
const pushError = (pushBaseResult.stderr || pushBaseResult.stdout || 'Unknown error').toString().trim();
|
|
266
|
+
await log(`${formatAligned('⚠️', 'Warning:', `Failed to push ${baseBranch} to fork`)}`, { level: 'warning' });
|
|
267
|
+
if (pushError) await log(`${formatAligned('', 'Push error:', pushError)}`, { level: 'warning' });
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
await log(`${formatAligned('✅', 'Fork updated:', `Custom base branch ${baseBranch} pushed to fork`)}`);
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function proactivelySyncBaseBranchToFork({ baseBranch, defaultBranch, tempDir, log, formatAligned, $ }) {
|
|
276
|
+
// Default branch is already synced by setupUpstreamAndSync; skip to avoid redundant work.
|
|
277
|
+
if (baseBranch === defaultBranch) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (!(await hasUpstreamRemote(tempDir, $))) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (await originHasBaseBranch(tempDir, baseBranch, $)) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
await ensureBaseBranchInFork({ baseBranch, tempDir, log, formatAligned, $, reason: 'proactive' });
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function retryBranchCreationFromUpstreamBase({ checkoutResult, branchName, baseBranch, tempDir, log, formatAligned, $ }) {
|
|
293
|
+
const errorOutput = `${checkoutResult.stderr || ''}${checkoutResult.stdout || ''}`;
|
|
294
|
+
if (!isMissingOriginBaseRefError(errorOutput, baseBranch)) {
|
|
295
|
+
return checkoutResult;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const synced = await ensureBaseBranchInFork({ baseBranch, tempDir, log, formatAligned, $, reason: 'reactive' });
|
|
299
|
+
if (!synced) {
|
|
300
|
+
return checkoutResult;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
await log(`${formatAligned('🌿', 'Retrying branch:', `${branchName} from ${baseBranch}`)}`);
|
|
304
|
+
return await $({ cwd: tempDir })`git checkout -b ${branchName} ${baseBranch}`;
|
|
305
|
+
}
|
|
306
|
+
|
|
208
307
|
export async function createOrCheckoutBranch({ isContinueMode, prBranch, issueNumber, tempDir, defaultBranch, argv, log, formatAligned, $, crypto, owner, repo, prNumber }) {
|
|
209
308
|
// Create a branch for the issue or checkout existing PR branch
|
|
210
309
|
let branchName;
|
|
@@ -234,10 +333,33 @@ export async function createOrCheckoutBranch({ isContinueMode, prBranch, issueNu
|
|
|
234
333
|
|
|
235
334
|
await log(`\n${formatAligned('🌿', 'Creating branch:', `${branchName} from ${baseBranch} (${branchSource})`)}`);
|
|
236
335
|
|
|
336
|
+
// Issue #1772: when a custom base branch is requested in fork mode, proactively copy it from
|
|
337
|
+
// upstream to the fork before branch creation. The fork's `gh repo fork` snapshot may pre-date
|
|
338
|
+
// upstream's custom branches, so we cannot assume origin already has the requested base.
|
|
339
|
+
if (argv.baseBranch) {
|
|
340
|
+
await proactivelySyncBaseBranchToFork({
|
|
341
|
+
baseBranch,
|
|
342
|
+
defaultBranch,
|
|
343
|
+
tempDir,
|
|
344
|
+
log,
|
|
345
|
+
formatAligned,
|
|
346
|
+
$,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
237
350
|
// IMPORTANT: Don't use 2>&1 here as it can interfere with exit codes
|
|
238
351
|
// Git checkout -b outputs to stderr but that's normal
|
|
239
352
|
// Create branch from the specified base branch (origin/baseBranch)
|
|
240
353
|
checkoutResult = await $({ cwd: tempDir })`git checkout -b ${branchName} origin/${baseBranch}`;
|
|
354
|
+
checkoutResult = await retryBranchCreationFromUpstreamBase({
|
|
355
|
+
checkoutResult,
|
|
356
|
+
branchName,
|
|
357
|
+
baseBranch,
|
|
358
|
+
tempDir,
|
|
359
|
+
log,
|
|
360
|
+
formatAligned,
|
|
361
|
+
$,
|
|
362
|
+
});
|
|
241
363
|
}
|
|
242
364
|
|
|
243
365
|
if (checkoutResult.code !== 0) {
|
package/src/solve.config.lib.mjs
CHANGED
|
@@ -110,6 +110,11 @@ export const SOLVE_OPTION_DEFINITIONS = {
|
|
|
110
110
|
description: 'Automatically use .gitkeep if CLAUDE.md is in .gitignore (pre-checks before creating file)',
|
|
111
111
|
default: true,
|
|
112
112
|
},
|
|
113
|
+
'auto-support-agents-md-as-claude-md': {
|
|
114
|
+
type: 'boolean',
|
|
115
|
+
description: '[EXPERIMENTAL] Temporarily copy AGENTS.md/agents.md to CLAUDE.md while Claude runs, then remove the temporary copy',
|
|
116
|
+
default: false,
|
|
117
|
+
},
|
|
113
118
|
'attach-logs': {
|
|
114
119
|
type: 'boolean',
|
|
115
120
|
description: 'Upload the solution draft log file to the Pull Request on completion (⚠️ WARNING: May expose sensitive data)',
|
|
@@ -175,7 +175,10 @@ export const createUnhandledRejectionHandler = options => {
|
|
|
175
175
|
/**
|
|
176
176
|
* Handles the case where no PR is available when one is required
|
|
177
177
|
*/
|
|
178
|
-
export const handleNoPrAvailableError = async ({ isContinueMode, tempDir, issueNumber, issueUrl, log, formatAligned }) => {
|
|
178
|
+
export const handleNoPrAvailableError = async ({ isContinueMode, tempDir, issueNumber, issueUrl, owner, repo, log, formatAligned }) => {
|
|
179
|
+
// Issue #1774: when an explicit target repo is known, surface --repo in the
|
|
180
|
+
// recovery hint so users do not hit the same fork-base resolution trap.
|
|
181
|
+
const repoFlag = owner && repo ? ` --repo ${owner}/${repo}` : '';
|
|
179
182
|
await log('');
|
|
180
183
|
await log(formatAligned('❌', 'FATAL ERROR:', 'No pull request available'), { level: 'error' });
|
|
181
184
|
await log('');
|
|
@@ -199,7 +202,7 @@ export const handleNoPrAvailableError = async ({ isContinueMode, tempDir, issueN
|
|
|
199
202
|
await log('');
|
|
200
203
|
await log(' Option 1: Create PR manually and use --continue');
|
|
201
204
|
await log(` cd ${tempDir}`);
|
|
202
|
-
await log(` gh pr create --draft --title "Fix issue #${issueNumber}" --body "Fixes #${issueNumber}"`);
|
|
205
|
+
await log(` gh pr create --draft --title "Fix issue #${issueNumber}" --body "Fixes #${issueNumber}"${repoFlag}`);
|
|
203
206
|
await log(' # Then use the PR URL with solve.mjs');
|
|
204
207
|
await log('');
|
|
205
208
|
await log(' Option 2: Start fresh without continue mode');
|
package/src/solve.mjs
CHANGED
|
@@ -576,7 +576,7 @@ try {
|
|
|
576
576
|
// CRITICAL: Validate that we have a PR number when required
|
|
577
577
|
// This prevents continuing without a PR when one was supposed to be created
|
|
578
578
|
if ((isContinueMode || argv.autoPullRequestCreation) && !prNumber) {
|
|
579
|
-
await handleNoPrAvailableError({ isContinueMode, tempDir, issueNumber, issueUrl, log, formatAligned });
|
|
579
|
+
await handleNoPrAvailableError({ isContinueMode, tempDir, issueNumber, issueUrl, owner, repo, log, formatAligned });
|
|
580
580
|
}
|
|
581
581
|
|
|
582
582
|
if (isContinueMode) {
|
|
@@ -55,6 +55,9 @@ export const AUTO_MERGED_MARKER = 'Auto-merged';
|
|
|
55
55
|
// solve.auto-merge.lib.mjs — billing-limit notification (spending cap / free tier)
|
|
56
56
|
export const BILLING_LIMIT_MARKER = 'GitHub Actions Billing Limit';
|
|
57
57
|
|
|
58
|
+
// solve.auto-merge.lib.mjs — cancelled/stale CI needs manual review
|
|
59
|
+
export const CANCELLED_CI_REVIEW_MARKER = 'Cancelled CI/CD Requires Review';
|
|
60
|
+
|
|
58
61
|
// solve.results.lib.mjs — working session summary comments posted by
|
|
59
62
|
// --attach-solution-summary / --auto-attach-solution-summary at the end of
|
|
60
63
|
// every working session (top-level solve, auto-restart-until-mergeable
|
|
@@ -99,7 +102,7 @@ export const USAGE_LIMIT_REACHED_MARKER = 'Usage Limit Reached';
|
|
|
99
102
|
* named constants above so that adding a new marker only requires adding
|
|
100
103
|
* the constant and appending it here.
|
|
101
104
|
*/
|
|
102
|
-
export const TOOL_GENERATED_COMMENT_MARKERS = [AI_WORK_SESSION_STARTED_MARKER, AI_WORK_SESSION_COMPLETED_MARKER, AI_WORK_SESSION_RESUMED_MARKER, AUTO_RESUME_ON_LIMIT_RESET_MARKER, AUTO_RESTART_ON_LIMIT_RESET_MARKER, SOLUTION_DRAFT_LOG_MARKER, AUTO_RESTART_MARKER, AUTO_RESTART_UNTIL_MERGEABLE_LOG_MARKER, READY_TO_MERGE_MARKER, AUTO_MERGED_MARKER, BILLING_LIMIT_MARKER, MAINTAINER_ACCESS_REQUEST_MARKER, LIVE_PROGRESS_SECTION_START_MARKER, SESSION_FORCE_KILLED_MARKER, REPOSITORY_INITIALIZATION_REQUIRED_MARKER, INTERACTIVE_SESSION_STARTED_MARKER, INTERACTIVE_SESSION_ENDED_MARKER, NOW_WORKING_SESSION_IS_ENDED_MARKER, SOLUTION_DRAFT_FAILED_MARKER, SOLUTION_DRAFT_FINISHED_WITH_ERRORS_MARKER, USAGE_LIMIT_REACHED_MARKER, WORKING_SESSION_SUMMARY_MARKER];
|
|
105
|
+
export const TOOL_GENERATED_COMMENT_MARKERS = [AI_WORK_SESSION_STARTED_MARKER, AI_WORK_SESSION_COMPLETED_MARKER, AI_WORK_SESSION_RESUMED_MARKER, AUTO_RESUME_ON_LIMIT_RESET_MARKER, AUTO_RESTART_ON_LIMIT_RESET_MARKER, SOLUTION_DRAFT_LOG_MARKER, AUTO_RESTART_MARKER, AUTO_RESTART_UNTIL_MERGEABLE_LOG_MARKER, READY_TO_MERGE_MARKER, AUTO_MERGED_MARKER, BILLING_LIMIT_MARKER, CANCELLED_CI_REVIEW_MARKER, MAINTAINER_ACCESS_REQUEST_MARKER, LIVE_PROGRESS_SECTION_START_MARKER, SESSION_FORCE_KILLED_MARKER, REPOSITORY_INITIALIZATION_REQUIRED_MARKER, INTERACTIVE_SESSION_STARTED_MARKER, INTERACTIVE_SESSION_ENDED_MARKER, NOW_WORKING_SESSION_IS_ENDED_MARKER, SOLUTION_DRAFT_FAILED_MARKER, SOLUTION_DRAFT_FINISHED_WITH_ERRORS_MARKER, USAGE_LIMIT_REACHED_MARKER, WORKING_SESSION_SUMMARY_MARKER];
|
|
103
106
|
|
|
104
107
|
/**
|
|
105
108
|
* Markers that indicate the end of a working session. Used by
|