@link-assistant/hive-mind 1.69.9 ā 1.69.11
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 +26 -0
- package/CLAUDE.md +5 -0
- package/package.json +1 -1
- package/src/github-rate-limit.lib.mjs +126 -1
- package/src/github.lib.mjs +9 -2
- package/src/solve.config.lib.mjs +5 -0
- package/src/solve.mjs +16 -1
- package/src/solve.pre-pr-failure-notifier.lib.mjs +153 -37
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.69.11
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- bdca974: Notify existing pull requests when solver pre-exit failures happen before a working session can post its own failure comment.
|
|
8
|
+
|
|
9
|
+
## 1.69.10
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 7d58938: feat: add opt-in GitHub API rate-limit usage logging
|
|
14
|
+
|
|
15
|
+
Adds optional logging of current GitHub API rate-limit usage through the centralized `gh` retry wrapper so every wrapped GitHub CLI call can report quota usage while debugging.
|
|
16
|
+
|
|
17
|
+
Features:
|
|
18
|
+
- Disabled by default for backward compatibility
|
|
19
|
+
- Enable with `--github-rate-limits-logging` when debugging API usage
|
|
20
|
+
- Logs current `core`, `graphql`, and `search` rate-limit buckets after each centralized wrapped `gh` attempt
|
|
21
|
+
- Keeps the logging probe non-fatal so quota logging cannot break solve workflows
|
|
22
|
+
|
|
23
|
+
Example output:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
š GitHub rate limits after $gh (gh api repos): core: 780/5000 used (+29 since last check), 4220 remaining, resets 2026-05-12T10:30:00.000Z; graphql: 10/5000 used (no change), 4990 remaining, resets 2026-05-12T10:30:00.000Z
|
|
27
|
+
```
|
|
28
|
+
|
|
3
29
|
## 1.69.9
|
|
4
30
|
|
|
5
31
|
### Patch Changes
|
package/CLAUDE.md
ADDED
package/package.json
CHANGED
|
@@ -24,8 +24,16 @@ import { limitReset, retryLimits } from './config.lib.mjs';
|
|
|
24
24
|
|
|
25
25
|
const exec = promisify(execCb);
|
|
26
26
|
|
|
27
|
+
const GITHUB_RATE_LIMIT_USAGE_RESOURCES = ['core', 'graphql', 'search'];
|
|
27
28
|
const RATE_LIMIT_PATTERNS = ['api rate limit exceeded', 'rate limit exceeded', 'you have exceeded a secondary rate limit', 'secondary rate limit', 'abuse detection', 'was submitted too quickly'];
|
|
28
29
|
|
|
30
|
+
const githubRateLimitLogging = {
|
|
31
|
+
enabled: false,
|
|
32
|
+
log: null,
|
|
33
|
+
fetchUsage: null,
|
|
34
|
+
lastUsageByResource: null,
|
|
35
|
+
};
|
|
36
|
+
|
|
29
37
|
/**
|
|
30
38
|
* Pull every plausible string out of a thrown error/result so pattern matches
|
|
31
39
|
* survive whatever shape the upstream caller gave us (Error, exec result with
|
|
@@ -120,6 +128,116 @@ export const fetchNextRateLimitReset = async () => {
|
|
|
120
128
|
}
|
|
121
129
|
};
|
|
122
130
|
|
|
131
|
+
const toFiniteNumber = value => {
|
|
132
|
+
const number = Number(value);
|
|
133
|
+
return Number.isFinite(number) ? number : null;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const normalizeRateLimitUsageEntry = (resource, entry) => {
|
|
137
|
+
if (!resource || !entry) return null;
|
|
138
|
+
const limit = toFiniteNumber(entry.limit);
|
|
139
|
+
const used = toFiniteNumber(entry.used);
|
|
140
|
+
const remaining = toFiniteNumber(entry.remaining);
|
|
141
|
+
const reset = toFiniteNumber(entry.reset);
|
|
142
|
+
if (limit === null || used === null || remaining === null) return null;
|
|
143
|
+
return {
|
|
144
|
+
resource,
|
|
145
|
+
limit,
|
|
146
|
+
used,
|
|
147
|
+
remaining,
|
|
148
|
+
reset,
|
|
149
|
+
resetDate: reset === null ? null : new Date(reset * 1000),
|
|
150
|
+
};
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Fetch the current GitHub API usage buckets we commonly exercise via `gh`.
|
|
155
|
+
* This intentionally calls `gh api rate_limit` directly so the logging probe
|
|
156
|
+
* does not recursively pass through the retry/logging wrapper it supports.
|
|
157
|
+
*
|
|
158
|
+
* @returns {Promise<Array<{resource: string, limit: number, used: number, remaining: number, reset: number|null, resetDate: Date|null}>>}
|
|
159
|
+
*/
|
|
160
|
+
export const fetchGitHubRateLimitUsage = async () => {
|
|
161
|
+
try {
|
|
162
|
+
// eslint-disable-next-line gh-rate-limit/no-direct-gh-exec -- this IS the centralized rate-limit helper; routing through itself would recurse.
|
|
163
|
+
const { stdout } = await exec('gh api rate_limit');
|
|
164
|
+
const data = JSON.parse(stdout);
|
|
165
|
+
const resources = data?.resources || {};
|
|
166
|
+
return GITHUB_RATE_LIMIT_USAGE_RESOURCES.map(resource => normalizeRateLimitUsageEntry(resource, resources[resource])).filter(Boolean);
|
|
167
|
+
} catch {
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Enable optional debug logging of actual GitHub API quota usage after each
|
|
174
|
+
* centralized `gh` wrapper attempt. Disabled by default for backward
|
|
175
|
+
* compatibility and to avoid extra `gh api rate_limit` probes in normal runs.
|
|
176
|
+
*
|
|
177
|
+
* @param {object} [options]
|
|
178
|
+
* @param {boolean} [options.enabled=false]
|
|
179
|
+
* @param {(msg: string, options?: object) => Promise<void>|void} [options.log]
|
|
180
|
+
* @param {() => Promise<Array<object>>} [options.fetchUsage] - injectable for tests.
|
|
181
|
+
*/
|
|
182
|
+
export const configureGitHubRateLimitLogging = ({ enabled = false, log = null, fetchUsage = null } = {}) => {
|
|
183
|
+
githubRateLimitLogging.enabled = enabled === true;
|
|
184
|
+
githubRateLimitLogging.log = typeof log === 'function' ? log : null;
|
|
185
|
+
githubRateLimitLogging.fetchUsage = typeof fetchUsage === 'function' ? fetchUsage : fetchGitHubRateLimitUsage;
|
|
186
|
+
githubRateLimitLogging.lastUsageByResource = null;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
export const isGitHubRateLimitLoggingEnabled = () => githubRateLimitLogging.enabled;
|
|
190
|
+
|
|
191
|
+
const formatUsageReset = entry => {
|
|
192
|
+
if (!(entry.resetDate instanceof Date) || Number.isNaN(entry.resetDate.getTime())) return '';
|
|
193
|
+
return `, resets ${entry.resetDate.toISOString()}`;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const formatRateLimitUsageEntry = entry => {
|
|
197
|
+
const previous = githubRateLimitLogging.lastUsageByResource?.[entry.resource];
|
|
198
|
+
let deltaText = '';
|
|
199
|
+
if (previous && Number.isFinite(previous.used)) {
|
|
200
|
+
const delta = entry.used - previous.used;
|
|
201
|
+
if (delta > 0) {
|
|
202
|
+
deltaText = ` (+${delta} since last check)`;
|
|
203
|
+
} else if (delta === 0) {
|
|
204
|
+
deltaText = ' (no change)';
|
|
205
|
+
} else {
|
|
206
|
+
deltaText = ' (usage reset since last check)';
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return `${entry.resource}: ${entry.used}/${entry.limit} used${deltaText}, ${entry.remaining} remaining${formatUsageReset(entry)}`;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const safelyLogRateLimitUsage = async (logger, message, options) => {
|
|
213
|
+
try {
|
|
214
|
+
await Promise.resolve(logger(message, options));
|
|
215
|
+
} catch {
|
|
216
|
+
// Debug logging must never replace the original gh result or error.
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
export const logGitHubRateLimitUsage = async ({ label = 'gh' } = {}) => {
|
|
221
|
+
if (!githubRateLimitLogging.enabled) return [];
|
|
222
|
+
const logger = githubRateLimitLogging.log || (msg => console.warn(msg));
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
const rawUsage = await githubRateLimitLogging.fetchUsage();
|
|
226
|
+
const usage = (Array.isArray(rawUsage) ? rawUsage : []).map(entry => normalizeRateLimitUsageEntry(entry.resource || entry.name, entry)).filter(Boolean);
|
|
227
|
+
if (usage.length === 0) return [];
|
|
228
|
+
|
|
229
|
+
const details = usage.map(formatRateLimitUsageEntry).join('; ');
|
|
230
|
+
await safelyLogRateLimitUsage(logger, `š GitHub rate limits after ${label}: ${details}`);
|
|
231
|
+
githubRateLimitLogging.lastUsageByResource = Object.fromEntries(usage.map(entry => [entry.resource, entry]));
|
|
232
|
+
return usage;
|
|
233
|
+
} catch (error) {
|
|
234
|
+
if (global.verboseMode) {
|
|
235
|
+
await safelyLogRateLimitUsage(logger, `ā ļø GitHub rate-limit logging failed after ${label}: ${error.message}`, { verbose: true });
|
|
236
|
+
}
|
|
237
|
+
return [];
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
123
241
|
/**
|
|
124
242
|
* Compute the absolute wait deadline that satisfies issue #1726:
|
|
125
243
|
* reset + bufferMs (default 10 min) + random(0..jitterMs) (default 0-5 min)
|
|
@@ -226,8 +344,11 @@ export const ghWithRateLimitRetry = async (fn, options = {}) => {
|
|
|
226
344
|
|
|
227
345
|
for (let i = 0; i < hardCap; i++) {
|
|
228
346
|
try {
|
|
229
|
-
|
|
347
|
+
const result = await fn();
|
|
348
|
+
await logGitHubRateLimitUsage({ label });
|
|
349
|
+
return result;
|
|
230
350
|
} catch (error) {
|
|
351
|
+
await logGitHubRateLimitUsage({ label });
|
|
231
352
|
lastError = error;
|
|
232
353
|
|
|
233
354
|
if (isRateLimitError(error)) {
|
|
@@ -324,6 +445,10 @@ export default {
|
|
|
324
445
|
isTransientNetworkError,
|
|
325
446
|
parseRateLimitReset,
|
|
326
447
|
fetchNextRateLimitReset,
|
|
448
|
+
fetchGitHubRateLimitUsage,
|
|
449
|
+
configureGitHubRateLimitLogging,
|
|
450
|
+
isGitHubRateLimitLoggingEnabled,
|
|
451
|
+
logGitHubRateLimitUsage,
|
|
327
452
|
computeRateLimitWait,
|
|
328
453
|
ghWithRateLimitRetry,
|
|
329
454
|
execGhWithRetry,
|
package/src/github.lib.mjs
CHANGED
|
@@ -35,6 +35,11 @@ const buildIssueFailureActionSection = targetType => {
|
|
|
35
35
|
|
|
36
36
|
Administrator-only CLI details, if any, are printed in the solver terminal log rather than in this issue comment.`;
|
|
37
37
|
};
|
|
38
|
+
const normalizeFailureActionSection = section => {
|
|
39
|
+
const text = section || '';
|
|
40
|
+
if (!text) return '';
|
|
41
|
+
return text.startsWith('\n') ? text : `\n\n${text}`;
|
|
42
|
+
};
|
|
38
43
|
export const checkFileInBranch = async (owner, repo, fileName, branchName) => {
|
|
39
44
|
const { $ } = await use('command-stream');
|
|
40
45
|
|
|
@@ -353,6 +358,7 @@ export async function attachLogToGitHub(options) {
|
|
|
353
358
|
tool = null, // The tool used (claude, agent, opencode, codex)
|
|
354
359
|
resultModelUsage = null, // Issue #1454
|
|
355
360
|
budgetStatsData = null, // Issue #1491: budget stats for comment
|
|
361
|
+
failureActionSection = null,
|
|
356
362
|
} = options;
|
|
357
363
|
const budgetStats = budgetStatsData ? buildBudgetStatsString(budgetStatsData.tokenUsage, budgetStatsData.subAgentCalls) : '';
|
|
358
364
|
const targetName = targetType === 'pr' ? 'Pull Request' : 'Issue';
|
|
@@ -444,6 +450,7 @@ export async function attachLogToGitHub(options) {
|
|
|
444
450
|
await log(' š§ Escaping code blocks in log content for safe embedding...', { verbose: true });
|
|
445
451
|
}
|
|
446
452
|
logContent = escapeCodeBlocksInLog(logContent);
|
|
453
|
+
const failureAction = normalizeFailureActionSection(failureActionSection ?? buildIssueFailureActionSection(targetType));
|
|
447
454
|
// Create formatted comment
|
|
448
455
|
let logComment;
|
|
449
456
|
// Usage limit comments should be shown whenever isUsageLimit is true,
|
|
@@ -520,7 +527,7 @@ ${footerNote}`;
|
|
|
520
527
|
The automated solution draft encountered an error:
|
|
521
528
|
\`\`\`
|
|
522
529
|
${errorMessage}
|
|
523
|
-
\`\`\`${
|
|
530
|
+
\`\`\`${failureAction}${modelInfoString}
|
|
524
531
|
|
|
525
532
|
<details>
|
|
526
533
|
<summary>Click to expand failure log (${Math.round(logStats.size / 1024)}KB)</summary>
|
|
@@ -718,7 +725,7 @@ ${uploadFooterNote}`;
|
|
|
718
725
|
The automated solution draft encountered an error:
|
|
719
726
|
\`\`\`
|
|
720
727
|
${errorMessage}
|
|
721
|
-
\`\`\`${
|
|
728
|
+
\`\`\`${failureAction}${modelInfoString}
|
|
722
729
|
|
|
723
730
|
### š **Failure log uploaded as ${uploadTypeLabel}${chunkInfo}** (${Math.round(logStats.size / 1024)}KB)
|
|
724
731
|
- [View complete failure log](${logUrl})
|
package/src/solve.config.lib.mjs
CHANGED
|
@@ -461,6 +461,11 @@ export const SOLVE_OPTION_DEFINITIONS = {
|
|
|
461
461
|
description: 'Include prompt to check related/sibling pull requests when studying related work. Enabled by default, use --no-prompt-check-sibling-pull-requests to disable.',
|
|
462
462
|
default: true,
|
|
463
463
|
},
|
|
464
|
+
'github-rate-limits-logging': {
|
|
465
|
+
type: 'boolean',
|
|
466
|
+
description: 'Log GitHub API rate-limit usage after each centralized gh command retry wrapper call. Disabled by default; use --github-rate-limits-logging to enable.',
|
|
467
|
+
default: false,
|
|
468
|
+
},
|
|
464
469
|
'prompt-experiments-folder': {
|
|
465
470
|
type: 'string',
|
|
466
471
|
description: 'Path to experiments folder used in system prompt. Set to empty string to disable experiments folder prompt. Default: ./experiments',
|
package/src/solve.mjs
CHANGED
|
@@ -8,7 +8,7 @@ await handleSolveEarlyExit(earlyArgs);
|
|
|
8
8
|
const { use } = eval(await (await fetch('https://unpkg.com/use-m/use.js')).text());
|
|
9
9
|
globalThis.use = use;
|
|
10
10
|
const { $: __rawDollar$ } = await use('command-stream');
|
|
11
|
-
const { wrapDollarWithGhRetry } = await import('./github-rate-limit.lib.mjs');
|
|
11
|
+
const { configureGitHubRateLimitLogging, wrapDollarWithGhRetry } = await import('./github-rate-limit.lib.mjs');
|
|
12
12
|
const $ = wrapDollarWithGhRetry(__rawDollar$);
|
|
13
13
|
const config = await import('./solve.config.lib.mjs');
|
|
14
14
|
const { initializeConfig, parseArguments } = config;
|
|
@@ -86,6 +86,10 @@ global.verboseMode = argv.verbose;
|
|
|
86
86
|
|
|
87
87
|
setupVerboseLogInterceptor(); // Issue #1466: capture [VERBOSE] output in log files
|
|
88
88
|
setupStdioLogInterceptor(); // Issue #1549: capture ALL terminal output in log file
|
|
89
|
+
configureGitHubRateLimitLogging({
|
|
90
|
+
enabled: argv.githubRateLimitsLogging === true,
|
|
91
|
+
log,
|
|
92
|
+
});
|
|
89
93
|
|
|
90
94
|
// Early logs go to cwd; custom log dir takes effect after argv is parsed
|
|
91
95
|
// Conditionally import tool-specific functions after argv is parsed
|
|
@@ -144,6 +148,14 @@ const cleanupWrapper = async () => {
|
|
|
144
148
|
const interruptWrapper = createInterruptWrapper({ cleanupContext, checkForUncommittedChanges, shouldAttachLogs, attachLogToGitHub, getLogFile, sanitizeLogContent, $, log });
|
|
145
149
|
initializeExitHandler(getAbsoluteLogPath, log, cleanupWrapper, interruptWrapper, ({ code, reason }) => notifyIssueAboutPrePullRequestFailure({ code, reason, argv, globalState: global, $, log, getLogFile, shouldAttachLogs, attachLogToGitHub, sanitizeLogContent, rawCommand }));
|
|
146
150
|
installGlobalExitHandlers();
|
|
151
|
+
const markFailureNotificationPosted = targetType => {
|
|
152
|
+
global.preExitFailureNotificationPosted = true;
|
|
153
|
+
if (targetType === 'pr') {
|
|
154
|
+
global.pullRequestFailureNotificationPosted = true;
|
|
155
|
+
} else if (targetType === 'issue') {
|
|
156
|
+
global.prePullRequestFailureNotificationPosted = true;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
147
159
|
|
|
148
160
|
// Now handle argument validation that was moved from early checks
|
|
149
161
|
let issueUrl = argv['issue-url'] || argv._[0];
|
|
@@ -896,6 +908,7 @@ try {
|
|
|
896
908
|
});
|
|
897
909
|
|
|
898
910
|
if (logUploadSuccess) {
|
|
911
|
+
markFailureNotificationPosted('pr');
|
|
899
912
|
await log(' ā
Logs uploaded successfully');
|
|
900
913
|
} else {
|
|
901
914
|
// Issue #1212: Always show log upload failures (not just verbose)
|
|
@@ -920,6 +933,7 @@ try {
|
|
|
920
933
|
|
|
921
934
|
const posted = await postTrackedComment({ $, owner, repo, targetNumber: prNumber, body: failureComment });
|
|
922
935
|
if (posted.ok) {
|
|
936
|
+
markFailureNotificationPosted('pr');
|
|
923
937
|
await log(` Posted failure comment to PR${posted.commentId ? ` (id=${posted.commentId})` : ''}`);
|
|
924
938
|
}
|
|
925
939
|
} catch (error) {
|
|
@@ -1074,6 +1088,7 @@ try {
|
|
|
1074
1088
|
});
|
|
1075
1089
|
|
|
1076
1090
|
if (logUploadSuccess) {
|
|
1091
|
+
markFailureNotificationPosted(logTargetType);
|
|
1077
1092
|
await log(` š Failure logs posted to ${logTargetLabel}`);
|
|
1078
1093
|
} else {
|
|
1079
1094
|
// Issue #1212: Always show log upload failures (not just verbose)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { getTrackedToolCommentIds, postTrackedComment, SOLUTION_DRAFT_FAILED_MARKER } from './tool-comments.lib.mjs';
|
|
2
2
|
|
|
3
|
+
export const FORK_DIVERGENCE_RESOLUTION_OPTION = '--allow-fork-divergence-resolution-using-force-push-with-lease';
|
|
4
|
+
|
|
3
5
|
const truncate = (value, maxLength = 2000) => {
|
|
4
6
|
const text = value === null || value === undefined ? '' : String(value);
|
|
5
7
|
if (text.length <= maxLength) return text;
|
|
@@ -8,10 +10,24 @@ const truncate = (value, maxLength = 2000) => {
|
|
|
8
10
|
|
|
9
11
|
const fence = value => truncate(value || 'Unknown error').replaceAll('```', '` ` `');
|
|
10
12
|
|
|
13
|
+
const isForkDivergenceFailure = reason => {
|
|
14
|
+
const normalizedReason = String(reason || '').toLowerCase();
|
|
15
|
+
return normalizedReason.includes('fork divergence') || (normalizedReason.includes('fork') && normalizedReason.includes('non-fast-forward')) || normalizedReason.includes('force-with-lease');
|
|
16
|
+
};
|
|
17
|
+
|
|
11
18
|
export function buildPrePullRequestFailureActionSection(reason = '') {
|
|
12
19
|
const normalizedReason = String(reason || '').toLowerCase();
|
|
13
20
|
const isForkOrRecoveryFailure = normalizedReason.includes('fork') || normalizedReason.includes('auto-recovery') || normalizedReason.includes('repository setup');
|
|
14
21
|
|
|
22
|
+
if (isForkDivergenceFailure(reason)) {
|
|
23
|
+
return `### What you can do
|
|
24
|
+
- If the fork's default branch can be overwritten safely, rerun with \`${FORK_DIVERGENCE_RESOLUTION_OPTION}\` to allow a guarded force-with-lease sync.
|
|
25
|
+
- If the fork has commits you need to preserve, resolve the divergence manually, then rerun the solver.
|
|
26
|
+
- If this requires elevated Hive Mind access, ask a Hive Mind administrator to handle the affected fork or repository.
|
|
27
|
+
|
|
28
|
+
Administrator-only CLI details, if any, are printed in the solver terminal log rather than in this GitHub comment.`;
|
|
29
|
+
}
|
|
30
|
+
|
|
15
31
|
if (isForkOrRecoveryFailure) {
|
|
16
32
|
return `### What you can do
|
|
17
33
|
- If the affected fork or repository belongs to you, remove, rename, archive, initialize, or otherwise repair it in GitHub, then rerun the solver.
|
|
@@ -36,6 +52,42 @@ export function shouldNotifyIssueAboutPrePullRequestFailure({ code, globalState
|
|
|
36
52
|
return getTrackedToolCommentIds().size === 0;
|
|
37
53
|
}
|
|
38
54
|
|
|
55
|
+
export function resolvePreExitFailureNotificationTarget({ code, globalState }) {
|
|
56
|
+
if (code === 0) return null;
|
|
57
|
+
if (!globalState?.owner || !globalState?.repo) return null;
|
|
58
|
+
if (globalState.preExitFailureNotificationPosted || globalState.preExitFailureNotificationInProgress) return null;
|
|
59
|
+
|
|
60
|
+
const owner = globalState.owner;
|
|
61
|
+
const repo = globalState.repo;
|
|
62
|
+
const issueNumber = globalState.issueNumber || null;
|
|
63
|
+
const prNumber = globalState.createdPR?.number || globalState.prNumber || null;
|
|
64
|
+
|
|
65
|
+
if (prNumber) {
|
|
66
|
+
if (globalState.pullRequestFailureNotificationPosted || globalState.pullRequestFailureNotificationInProgress) return null;
|
|
67
|
+
return {
|
|
68
|
+
targetType: 'pr',
|
|
69
|
+
targetNumber: prNumber,
|
|
70
|
+
owner,
|
|
71
|
+
repo,
|
|
72
|
+
issueNumber,
|
|
73
|
+
prNumber,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!issueNumber) return null;
|
|
78
|
+
if (globalState.prePullRequestFailureNotificationPosted || globalState.prePullRequestFailureNotificationInProgress) return null;
|
|
79
|
+
if (getTrackedToolCommentIds().size !== 0) return null;
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
targetType: 'issue',
|
|
83
|
+
targetNumber: issueNumber,
|
|
84
|
+
owner,
|
|
85
|
+
repo,
|
|
86
|
+
issueNumber,
|
|
87
|
+
prNumber: null,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
39
91
|
export function buildPrePullRequestFailureComment({ reason, owner, repo, issueNumber, argv = {}, logAttachmentAttempted = false }) {
|
|
40
92
|
const tool = argv.tool || 'claude';
|
|
41
93
|
const modelLine = argv.model ? `\n- **Requested model**: \`${argv.model}\`` : '';
|
|
@@ -62,60 +114,124 @@ ${logLine}
|
|
|
62
114
|
`;
|
|
63
115
|
}
|
|
64
116
|
|
|
117
|
+
export function buildExistingPullRequestFailureComment({ reason, owner, repo, prNumber, issueNumber = null, argv = {}, logAttachmentAttempted = false }) {
|
|
118
|
+
const tool = argv.tool || 'claude';
|
|
119
|
+
const modelLine = argv.model ? `\n- **Requested model**: \`${argv.model}\`` : '';
|
|
120
|
+
const issueLine = issueNumber ? `\n- **Linked issue**: #${issueNumber}` : '';
|
|
121
|
+
const logLine = logAttachmentAttempted ? 'Log attachment was attempted but failed. Check the solver terminal log for the complete failure output.' : 'Logs were not attached because `--attach-logs` was not enabled.';
|
|
122
|
+
const actionSection = buildPrePullRequestFailureActionSection(reason);
|
|
123
|
+
|
|
124
|
+
return `## šØ ${SOLUTION_DRAFT_FAILED_MARKER}
|
|
125
|
+
|
|
126
|
+
The automated solver stopped while continuing this existing pull request, so the failure details are posted here for review.
|
|
127
|
+
|
|
128
|
+
### Failure
|
|
129
|
+
- **Repository**: \`${owner}/${repo}\`
|
|
130
|
+
- **Pull request**: #${prNumber}${issueLine}
|
|
131
|
+
- **Tool**: \`${tool}\`${modelLine}
|
|
132
|
+
|
|
133
|
+
**Reason**
|
|
134
|
+
\`\`\`text
|
|
135
|
+
${fence(reason)}
|
|
136
|
+
\`\`\`
|
|
137
|
+
|
|
138
|
+
${actionSection}
|
|
139
|
+
|
|
140
|
+
${logLine}
|
|
141
|
+
`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const markNotificationPosted = ({ globalState, targetType }) => {
|
|
145
|
+
globalState.preExitFailureNotificationPosted = true;
|
|
146
|
+
if (targetType === 'pr') {
|
|
147
|
+
globalState.pullRequestFailureNotificationPosted = true;
|
|
148
|
+
} else {
|
|
149
|
+
globalState.prePullRequestFailureNotificationPosted = true;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
65
153
|
export async function notifyIssueAboutPrePullRequestFailure(options) {
|
|
66
154
|
const { code, reason, argv = {}, globalState = globalThis, $, log = async () => {}, getLogFile, shouldAttachLogs = false, attachLogToGitHub, sanitizeLogContent, rawCommand = null, postComment = postTrackedComment } = options;
|
|
67
155
|
|
|
68
|
-
|
|
156
|
+
const target = resolvePreExitFailureNotificationTarget({ code, globalState });
|
|
157
|
+
if (!target) {
|
|
69
158
|
return { notified: false, skipped: true };
|
|
70
159
|
}
|
|
71
160
|
|
|
72
|
-
const owner =
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
161
|
+
const { owner, repo, issueNumber, prNumber, targetType, targetNumber } = target;
|
|
162
|
+
const targetLabel = targetType === 'pr' ? `pull request #${targetNumber}` : `issue #${targetNumber}`;
|
|
163
|
+
globalState.preExitFailureNotificationInProgress = true;
|
|
164
|
+
if (targetType === 'pr') {
|
|
165
|
+
globalState.pullRequestFailureNotificationInProgress = true;
|
|
166
|
+
} else {
|
|
167
|
+
globalState.prePullRequestFailureNotificationInProgress = true;
|
|
168
|
+
}
|
|
76
169
|
|
|
77
170
|
try {
|
|
78
171
|
if (shouldAttachLogs && getLogFile && attachLogToGitHub && sanitizeLogContent) {
|
|
79
|
-
await log(`\nš Notifying
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
172
|
+
await log(`\nš Notifying ${targetLabel} about solver failure with logs...`);
|
|
173
|
+
const errorPrefix = targetType === 'pr' ? `The solver stopped while continuing pull request #${targetNumber}.` : 'The solver stopped before creating a pull request.';
|
|
174
|
+
try {
|
|
175
|
+
const uploaded = await attachLogToGitHub({
|
|
176
|
+
logFile: getLogFile(),
|
|
177
|
+
targetType,
|
|
178
|
+
targetNumber,
|
|
179
|
+
owner,
|
|
180
|
+
repo,
|
|
181
|
+
$,
|
|
182
|
+
log,
|
|
183
|
+
sanitizeLogContent,
|
|
184
|
+
verbose: argv.verbose,
|
|
185
|
+
errorMessage: `${errorPrefix}\n\nReason: ${reason || 'Unknown error'}`,
|
|
186
|
+
failureActionSection: buildPrePullRequestFailureActionSection(reason),
|
|
187
|
+
requestedModel: argv.originalModel || argv.model,
|
|
188
|
+
tool: argv.tool || 'claude',
|
|
189
|
+
});
|
|
190
|
+
if (uploaded) {
|
|
191
|
+
markNotificationPosted({ globalState, targetType });
|
|
192
|
+
return { notified: true, method: 'log-upload' };
|
|
193
|
+
}
|
|
194
|
+
} catch (error) {
|
|
195
|
+
const message = error && error.message ? error.message : String(error);
|
|
196
|
+
await log(` ā ļø Could not upload solver failure log: ${message}`, { level: 'warning' });
|
|
97
197
|
}
|
|
98
198
|
}
|
|
99
199
|
|
|
100
|
-
await log(`\nš¬ Notifying
|
|
101
|
-
const body =
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
200
|
+
await log(`\nš¬ Notifying ${targetLabel} about solver failure...`);
|
|
201
|
+
const body =
|
|
202
|
+
targetType === 'pr'
|
|
203
|
+
? buildExistingPullRequestFailureComment({
|
|
204
|
+
reason,
|
|
205
|
+
owner,
|
|
206
|
+
repo,
|
|
207
|
+
prNumber,
|
|
208
|
+
issueNumber,
|
|
209
|
+
argv,
|
|
210
|
+
rawCommand,
|
|
211
|
+
logAttachmentAttempted: shouldAttachLogs,
|
|
212
|
+
})
|
|
213
|
+
: buildPrePullRequestFailureComment({
|
|
214
|
+
reason,
|
|
215
|
+
owner,
|
|
216
|
+
repo,
|
|
217
|
+
issueNumber,
|
|
218
|
+
argv,
|
|
219
|
+
rawCommand,
|
|
220
|
+
logAttachmentAttempted: shouldAttachLogs,
|
|
221
|
+
});
|
|
222
|
+
const posted = await postComment({ $, owner, repo, targetNumber, body });
|
|
111
223
|
if (posted.ok) {
|
|
112
|
-
globalState
|
|
113
|
-
await log(` ā
|
|
224
|
+
markNotificationPosted({ globalState, targetType });
|
|
225
|
+
await log(` ā
Solver failure comment posted to ${targetLabel}${posted.commentId ? ` (id=${posted.commentId})` : ''}`);
|
|
114
226
|
return { notified: true, method: 'comment', commentId: posted.commentId || null };
|
|
115
227
|
}
|
|
116
|
-
await log(` ā ļø Could not post
|
|
228
|
+
await log(` ā ļø Could not post solver failure comment: ${posted.stderr || 'unknown error'}`, { level: 'warning' });
|
|
117
229
|
return { notified: false, error: posted.stderr || 'unknown error' };
|
|
118
230
|
} finally {
|
|
231
|
+
globalState.preExitFailureNotificationInProgress = false;
|
|
232
|
+
if (targetType === 'pr') {
|
|
233
|
+
globalState.pullRequestFailureNotificationInProgress = false;
|
|
234
|
+
}
|
|
119
235
|
globalState.prePullRequestFailureNotificationInProgress = false;
|
|
120
236
|
}
|
|
121
237
|
}
|