@link-assistant/hive-mind 1.12.0 → 1.13.0
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 +18 -0
- package/package.json +1 -1
- package/src/hive.config.lib.mjs +10 -0
- package/src/hive.mjs +2 -0
- package/src/solve.auto-merge.lib.mjs +598 -0
- package/src/solve.config.lib.mjs +10 -0
- package/src/solve.mjs +38 -0
- package/src/solve.restart-shared.lib.mjs +372 -0
- package/src/solve.watch.lib.mjs +31 -280
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.13.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 03adcb6: Add --auto-merge and --auto-restart-until-mergable options for autonomous PR management
|
|
8
|
+
|
|
9
|
+
New CLI options:
|
|
10
|
+
- `--auto-merge`: Automatically merge the pull request when CI passes and PR is mergeable. Implies --auto-restart-until-mergable.
|
|
11
|
+
- `--auto-restart-until-mergable`: Auto-restart the AI agent until PR becomes mergeable (no iteration limit). Restarts on new comments from non-bot users, CI failures, merge conflicts, or uncommitted changes. Does NOT auto-merge.
|
|
12
|
+
|
|
13
|
+
Features:
|
|
14
|
+
- Non-bot comment detection with configurable bot patterns
|
|
15
|
+
- Automatic detection of CI/CD status and merge readiness
|
|
16
|
+
- Continuous monitoring loop with configurable check intervals
|
|
17
|
+
- Progress and status reporting throughout the process
|
|
18
|
+
- Graceful handling of API errors with exponential backoff
|
|
19
|
+
- Session data tracking for accurate pricing across iterations
|
|
20
|
+
|
|
3
21
|
## 1.12.0
|
|
4
22
|
|
|
5
23
|
### Minor Changes
|
package/package.json
CHANGED
package/src/hive.config.lib.mjs
CHANGED
|
@@ -239,6 +239,16 @@ export const createYargsConfig = yargsInstance => {
|
|
|
239
239
|
alias: 'w',
|
|
240
240
|
default: false,
|
|
241
241
|
})
|
|
242
|
+
.option('auto-merge', {
|
|
243
|
+
type: 'boolean',
|
|
244
|
+
description: 'Automatically merge the pull request when the working session is finished and all CI/CD statuses pass and PR is mergeable. Implies --auto-restart-until-mergable.',
|
|
245
|
+
default: false,
|
|
246
|
+
})
|
|
247
|
+
.option('auto-restart-until-mergable', {
|
|
248
|
+
type: 'boolean',
|
|
249
|
+
description: 'Auto-restart until PR becomes mergeable (no iteration limit). Restarts on new comments from non-bot users, CI failures, merge conflicts, or other issues. Does NOT auto-merge.',
|
|
250
|
+
default: false,
|
|
251
|
+
})
|
|
242
252
|
.option('issue-order', {
|
|
243
253
|
type: 'string',
|
|
244
254
|
description: 'Order issues by publication date: "asc" (oldest first) or "desc" (newest first)',
|
package/src/hive.mjs
CHANGED
|
@@ -771,6 +771,8 @@ if (isDirectExecution) {
|
|
|
771
771
|
if (argv.promptCaseStudies) args.push('--prompt-case-studies');
|
|
772
772
|
if (argv.promptPlaywrightMcp !== undefined) args.push(argv.promptPlaywrightMcp ? '--prompt-playwright-mcp' : '--no-prompt-playwright-mcp');
|
|
773
773
|
if (argv.executeToolWithBun) args.push('--execute-tool-with-bun');
|
|
774
|
+
if (argv.autoMerge) args.push('--auto-merge');
|
|
775
|
+
if (argv.autoRestartUntilMergable) args.push('--auto-restart-until-mergable');
|
|
774
776
|
// Log the actual command being executed so users can investigate/reproduce
|
|
775
777
|
await log(` 📋 Command: ${solveCommand} ${args.join(' ')}`);
|
|
776
778
|
|
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Auto-merge and auto-restart-until-mergable module for solve.mjs
|
|
5
|
+
* Handles automatic merging of PRs and continuous restart until PR becomes mergeable
|
|
6
|
+
*
|
|
7
|
+
* Uses shared utilities from solve.restart-shared.lib.mjs for common functions.
|
|
8
|
+
*
|
|
9
|
+
* @see https://github.com/link-assistant/hive-mind/issues/1190
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Check if use is already defined globally (when imported from solve.mjs)
|
|
13
|
+
// If not, fetch it (when running standalone)
|
|
14
|
+
if (typeof globalThis.use === 'undefined') {
|
|
15
|
+
globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
|
|
16
|
+
}
|
|
17
|
+
const use = globalThis.use;
|
|
18
|
+
|
|
19
|
+
// Use command-stream for consistent $ behavior across runtimes
|
|
20
|
+
const { $ } = await use('command-stream');
|
|
21
|
+
|
|
22
|
+
// Import shared library functions
|
|
23
|
+
const lib = await import('./lib.mjs');
|
|
24
|
+
const { log, cleanErrorMessage, formatAligned, getLogFile } = lib;
|
|
25
|
+
|
|
26
|
+
// Note: We don't use detectAndCountFeedback from solve.feedback.lib.mjs
|
|
27
|
+
// because we have our own non-bot comment detection logic that's more
|
|
28
|
+
// appropriate for auto-restart-until-mergable mode
|
|
29
|
+
|
|
30
|
+
// Import Sentry integration
|
|
31
|
+
const sentryLib = await import('./sentry.lib.mjs');
|
|
32
|
+
const { reportError } = sentryLib;
|
|
33
|
+
|
|
34
|
+
// Import GitHub merge functions
|
|
35
|
+
const githubMergeLib = await import('./github-merge.lib.mjs');
|
|
36
|
+
const { checkPRCIStatus, checkPRMergeable, mergePullRequest, waitForCI } = githubMergeLib;
|
|
37
|
+
|
|
38
|
+
// Import GitHub functions for log attachment
|
|
39
|
+
const githubLib = await import('./github.lib.mjs');
|
|
40
|
+
const { sanitizeLogContent, attachLogToGitHub } = githubLib;
|
|
41
|
+
|
|
42
|
+
// Import shared utilities from the restart-shared module
|
|
43
|
+
const restartShared = await import('./solve.restart-shared.lib.mjs');
|
|
44
|
+
const { checkPRMerged, checkPRClosed, checkForUncommittedChanges, getUncommittedChangesDetails, executeToolIteration, buildAutoRestartInstructions, isApiError } = restartShared;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Check for new comments from non-bot users since last commit
|
|
48
|
+
* @returns {Promise<{hasNewComments: boolean, comments: Array}>}
|
|
49
|
+
*/
|
|
50
|
+
const checkForNonBotComments = async (owner, repo, prNumber, issueNumber, lastCheckTime, verbose = false) => {
|
|
51
|
+
try {
|
|
52
|
+
// Get current GitHub user to identify which comments are from the bot/hive-mind
|
|
53
|
+
let currentUser = null;
|
|
54
|
+
try {
|
|
55
|
+
const userResult = await $`gh api user --jq .login`;
|
|
56
|
+
if (userResult.code === 0) {
|
|
57
|
+
currentUser = userResult.stdout.toString().trim();
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// If we can't get the current user, continue without filtering
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Common bot usernames and patterns to filter out
|
|
64
|
+
// Note: Patterns use word boundaries or end-of-string to avoid false positives
|
|
65
|
+
// (e.g., "claudeuser" should NOT match as a bot)
|
|
66
|
+
const botPatterns = [
|
|
67
|
+
/\[bot\]$/i, // Any username ending with [bot]
|
|
68
|
+
/^github-actions$/i, // GitHub Actions
|
|
69
|
+
/^dependabot$/i, // Dependabot
|
|
70
|
+
/^renovate$/i, // Renovate
|
|
71
|
+
/^codecov$/i, // Codecov
|
|
72
|
+
/^netlify$/i, // Netlify
|
|
73
|
+
/^vercel$/i, // Vercel
|
|
74
|
+
/^hive-?mind$/i, // Hive Mind (with or without hyphen)
|
|
75
|
+
/^claude$/i, // Claude (exact match only)
|
|
76
|
+
/^copilot$/i, // GitHub Copilot
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
const isBot = login => {
|
|
80
|
+
if (!login) return false;
|
|
81
|
+
// Check if it's the current user (the bot running hive-mind)
|
|
82
|
+
if (currentUser && login === currentUser) return true;
|
|
83
|
+
// Check against known bot patterns
|
|
84
|
+
return botPatterns.some(pattern => pattern.test(login));
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Fetch PR conversation comments
|
|
88
|
+
const prCommentsResult = await $`gh api repos/${owner}/${repo}/issues/${prNumber}/comments --paginate`;
|
|
89
|
+
let prComments = [];
|
|
90
|
+
if (prCommentsResult.code === 0 && prCommentsResult.stdout) {
|
|
91
|
+
prComments = JSON.parse(prCommentsResult.stdout.toString() || '[]');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Fetch PR review comments (inline code comments)
|
|
95
|
+
const prReviewCommentsResult = await $`gh api repos/${owner}/${repo}/pulls/${prNumber}/comments --paginate`;
|
|
96
|
+
let prReviewComments = [];
|
|
97
|
+
if (prReviewCommentsResult.code === 0 && prReviewCommentsResult.stdout) {
|
|
98
|
+
prReviewComments = JSON.parse(prReviewCommentsResult.stdout.toString() || '[]');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Fetch issue comments if we have an issue number
|
|
102
|
+
let issueComments = [];
|
|
103
|
+
if (issueNumber && issueNumber !== prNumber) {
|
|
104
|
+
const issueCommentsResult = await $`gh api repos/${owner}/${repo}/issues/${issueNumber}/comments --paginate`;
|
|
105
|
+
if (issueCommentsResult.code === 0 && issueCommentsResult.stdout) {
|
|
106
|
+
issueComments = JSON.parse(issueCommentsResult.stdout.toString() || '[]');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Combine all comments
|
|
111
|
+
const allComments = [...prComments, ...prReviewComments, ...issueComments];
|
|
112
|
+
|
|
113
|
+
// Filter for new comments from non-bot users
|
|
114
|
+
const newNonBotComments = allComments.filter(comment => {
|
|
115
|
+
const commentTime = new Date(comment.created_at);
|
|
116
|
+
const isAfterLastCheck = commentTime > lastCheckTime;
|
|
117
|
+
const isFromNonBot = !isBot(comment.user?.login);
|
|
118
|
+
|
|
119
|
+
if (verbose && isAfterLastCheck && isFromNonBot) {
|
|
120
|
+
console.log(`[VERBOSE] New non-bot comment from ${comment.user?.login} at ${comment.created_at}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return isAfterLastCheck && isFromNonBot;
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
hasNewComments: newNonBotComments.length > 0,
|
|
128
|
+
comments: newNonBotComments,
|
|
129
|
+
};
|
|
130
|
+
} catch (error) {
|
|
131
|
+
reportError(error, {
|
|
132
|
+
context: 'check_non_bot_comments',
|
|
133
|
+
owner,
|
|
134
|
+
repo,
|
|
135
|
+
prNumber,
|
|
136
|
+
operation: 'fetch_comments',
|
|
137
|
+
});
|
|
138
|
+
return { hasNewComments: false, comments: [] };
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Get the reasons why PR is not mergeable
|
|
144
|
+
*/
|
|
145
|
+
const getMergeBlockers = async (owner, repo, prNumber, verbose = false) => {
|
|
146
|
+
const blockers = [];
|
|
147
|
+
|
|
148
|
+
// Check CI status
|
|
149
|
+
const ciStatus = await checkPRCIStatus(owner, repo, prNumber, verbose);
|
|
150
|
+
if (ciStatus.status === 'failure') {
|
|
151
|
+
blockers.push({
|
|
152
|
+
type: 'ci_failure',
|
|
153
|
+
message: 'CI/CD checks are failing',
|
|
154
|
+
details: ciStatus.checks.filter(c => c.conclusion === 'failure').map(c => c.name),
|
|
155
|
+
});
|
|
156
|
+
} else if (ciStatus.status === 'pending') {
|
|
157
|
+
blockers.push({
|
|
158
|
+
type: 'ci_pending',
|
|
159
|
+
message: 'CI/CD checks are still running',
|
|
160
|
+
details: ciStatus.checks.filter(c => c.status !== 'completed').map(c => c.name),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Check mergeability
|
|
165
|
+
const mergeStatus = await checkPRMergeable(owner, repo, prNumber, verbose);
|
|
166
|
+
if (!mergeStatus.mergeable) {
|
|
167
|
+
blockers.push({
|
|
168
|
+
type: 'not_mergeable',
|
|
169
|
+
message: mergeStatus.reason || 'PR is not mergeable',
|
|
170
|
+
details: [],
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return blockers;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Main function: Watch and restart until PR becomes mergeable
|
|
179
|
+
* This implements --auto-restart-until-mergable functionality
|
|
180
|
+
*/
|
|
181
|
+
export const watchUntilMergable = async params => {
|
|
182
|
+
const { issueUrl, owner, repo, issueNumber, prNumber, prBranch, branchName, tempDir, argv } = params;
|
|
183
|
+
|
|
184
|
+
const watchInterval = argv.watchInterval || 60; // seconds
|
|
185
|
+
const isAutoMerge = argv.autoMerge || false;
|
|
186
|
+
|
|
187
|
+
// Track latest session data across all iterations for accurate pricing
|
|
188
|
+
let latestSessionId = null;
|
|
189
|
+
let latestAnthropicCost = null;
|
|
190
|
+
|
|
191
|
+
// Track consecutive API errors for retry limit
|
|
192
|
+
const MAX_API_ERROR_RETRIES = 3;
|
|
193
|
+
let consecutiveApiErrors = 0;
|
|
194
|
+
let currentBackoffSeconds = watchInterval;
|
|
195
|
+
|
|
196
|
+
await log('');
|
|
197
|
+
await log(formatAligned('🔄', 'AUTO-RESTART-UNTIL-MERGABLE MODE ACTIVE', ''));
|
|
198
|
+
await log(formatAligned('', 'Monitoring PR:', `#${prNumber}`, 2));
|
|
199
|
+
await log(formatAligned('', 'Mode:', isAutoMerge ? 'Auto-merge (will merge when ready)' : 'Auto-restart-until-mergable (will NOT auto-merge)', 2));
|
|
200
|
+
await log(formatAligned('', 'Checking interval:', `${watchInterval} seconds`, 2));
|
|
201
|
+
await log(formatAligned('', 'Stop conditions:', 'PR merged, PR closed, or becomes mergeable', 2));
|
|
202
|
+
await log(formatAligned('', 'Restart triggers:', 'New non-bot comments, CI failures, merge conflicts', 2));
|
|
203
|
+
await log('');
|
|
204
|
+
await log('Press Ctrl+C to stop watching manually');
|
|
205
|
+
await log('');
|
|
206
|
+
|
|
207
|
+
let iteration = 0;
|
|
208
|
+
let lastCheckTime = new Date();
|
|
209
|
+
|
|
210
|
+
while (true) {
|
|
211
|
+
iteration++;
|
|
212
|
+
const currentTime = new Date();
|
|
213
|
+
|
|
214
|
+
// Check if PR is merged
|
|
215
|
+
const isMerged = await checkPRMerged(owner, repo, prNumber);
|
|
216
|
+
if (isMerged) {
|
|
217
|
+
await log('');
|
|
218
|
+
await log(formatAligned('🎉', 'PR MERGED!', 'Stopping auto-restart-until-mergable mode'));
|
|
219
|
+
await log(formatAligned('', 'Pull request:', `#${prNumber} has been merged`, 2));
|
|
220
|
+
await log('');
|
|
221
|
+
return { success: true, reason: 'merged', latestSessionId, latestAnthropicCost };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Check if PR is closed (not merged)
|
|
225
|
+
const isClosed = await checkPRClosed(owner, repo, prNumber);
|
|
226
|
+
if (isClosed) {
|
|
227
|
+
await log('');
|
|
228
|
+
await log(formatAligned('🚫', 'PR CLOSED!', 'Stopping auto-restart-until-mergable mode'));
|
|
229
|
+
await log(formatAligned('', 'Pull request:', `#${prNumber} has been closed without merging`, 2));
|
|
230
|
+
await log('');
|
|
231
|
+
return { success: false, reason: 'closed', latestSessionId, latestAnthropicCost };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
await log(formatAligned('🔍', `Check #${iteration}:`, currentTime.toLocaleTimeString()));
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
// Get merge blockers
|
|
238
|
+
const blockers = await getMergeBlockers(owner, repo, prNumber, argv.verbose);
|
|
239
|
+
|
|
240
|
+
// Check for new comments from non-bot users
|
|
241
|
+
const { hasNewComments, comments } = await checkForNonBotComments(owner, repo, prNumber, issueNumber, lastCheckTime, argv.verbose);
|
|
242
|
+
|
|
243
|
+
// Check for uncommitted changes using shared utility
|
|
244
|
+
const hasUncommittedChanges = await checkForUncommittedChanges(tempDir, argv);
|
|
245
|
+
|
|
246
|
+
// If PR is mergeable, no blockers, no new comments, and no uncommitted changes
|
|
247
|
+
if (blockers.length === 0 && !hasNewComments && !hasUncommittedChanges) {
|
|
248
|
+
await log(formatAligned('✅', 'PR IS MERGEABLE!', ''));
|
|
249
|
+
|
|
250
|
+
if (isAutoMerge) {
|
|
251
|
+
// Attempt to merge the PR
|
|
252
|
+
await log(formatAligned('🔀', 'Auto-merging PR...', ''));
|
|
253
|
+
const mergeResult = await mergePullRequest(owner, repo, prNumber, { squash: argv.squash || false, deleteAfter: argv.deleteBranchAfterMerge || false }, argv.verbose);
|
|
254
|
+
|
|
255
|
+
if (mergeResult.success) {
|
|
256
|
+
await log(formatAligned('🎉', 'PR MERGED SUCCESSFULLY!', ''));
|
|
257
|
+
await log(formatAligned('', 'Pull request:', `#${prNumber} has been auto-merged`, 2));
|
|
258
|
+
|
|
259
|
+
// Post success comment
|
|
260
|
+
try {
|
|
261
|
+
const commentBody = `## 🎉 Auto-merged\n\nThis pull request has been automatically merged by hive-mind after all CI checks passed and the PR became mergeable.\n\n---\n*Auto-merged by hive-mind with --auto-merge flag*`;
|
|
262
|
+
await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${commentBody}`;
|
|
263
|
+
} catch {
|
|
264
|
+
// Don't fail if comment posting fails
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return { success: true, reason: 'auto-merged', latestSessionId, latestAnthropicCost };
|
|
268
|
+
} else {
|
|
269
|
+
await log(formatAligned('⚠️', 'Auto-merge failed:', mergeResult.error || 'Unknown error', 2));
|
|
270
|
+
await log(formatAligned('', 'Will continue monitoring...', '', 2));
|
|
271
|
+
}
|
|
272
|
+
} else {
|
|
273
|
+
// Just report that PR is mergeable and exit
|
|
274
|
+
await log(formatAligned('', 'PR is ready to be merged manually', '', 2));
|
|
275
|
+
await log(formatAligned('', 'Exiting auto-restart-until-mergable mode', '', 2));
|
|
276
|
+
|
|
277
|
+
// Post success comment
|
|
278
|
+
try {
|
|
279
|
+
const commentBody = `## ✅ Ready to merge\n\nThis pull request is now ready to be merged:\n- All CI checks have passed\n- No merge conflicts\n- No pending changes\n\n---\n*Monitored by hive-mind with --auto-restart-until-mergable flag*`;
|
|
280
|
+
await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${commentBody}`;
|
|
281
|
+
} catch {
|
|
282
|
+
// Don't fail if comment posting fails
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return { success: true, reason: 'mergeable', latestSessionId, latestAnthropicCost };
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Determine if we need to restart
|
|
290
|
+
let shouldRestart = false;
|
|
291
|
+
let restartReason = '';
|
|
292
|
+
let feedbackLines = [];
|
|
293
|
+
|
|
294
|
+
// Reason 1: New comments from non-bot users
|
|
295
|
+
if (hasNewComments) {
|
|
296
|
+
shouldRestart = true;
|
|
297
|
+
restartReason = `New comment(s) from non-bot user(s): ${comments.map(c => c.user?.login).join(', ')}`;
|
|
298
|
+
feedbackLines.push('📬 New comments detected from non-bot users:');
|
|
299
|
+
for (const comment of comments) {
|
|
300
|
+
feedbackLines.push(` - ${comment.user?.login}: "${comment.body?.substring(0, 100)}${comment.body?.length > 100 ? '...' : ''}"`);
|
|
301
|
+
}
|
|
302
|
+
feedbackLines.push('');
|
|
303
|
+
feedbackLines.push('Please review and address the feedback from these comments.');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Reason 2: CI failures
|
|
307
|
+
const ciBlocker = blockers.find(b => b.type === 'ci_failure');
|
|
308
|
+
if (ciBlocker) {
|
|
309
|
+
shouldRestart = true;
|
|
310
|
+
restartReason = restartReason ? `${restartReason}; CI failures` : 'CI failures detected';
|
|
311
|
+
feedbackLines.push('❌ CI/CD checks are failing:');
|
|
312
|
+
for (const check of ciBlocker.details) {
|
|
313
|
+
feedbackLines.push(` - ${check}`);
|
|
314
|
+
}
|
|
315
|
+
feedbackLines.push('');
|
|
316
|
+
feedbackLines.push('Please fix the failing CI checks.');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Reason 3: Merge conflicts or other merge issues
|
|
320
|
+
const mergeBlocker = blockers.find(b => b.type === 'not_mergeable');
|
|
321
|
+
if (mergeBlocker && mergeBlocker.message.includes('conflicts')) {
|
|
322
|
+
shouldRestart = true;
|
|
323
|
+
restartReason = restartReason ? `${restartReason}; Merge conflicts` : 'Merge conflicts detected';
|
|
324
|
+
feedbackLines.push('⚠️ Merge conflicts detected:');
|
|
325
|
+
feedbackLines.push(` ${mergeBlocker.message}`);
|
|
326
|
+
feedbackLines.push('');
|
|
327
|
+
feedbackLines.push('Please resolve the merge conflicts.');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Reason 4: Uncommitted changes
|
|
331
|
+
if (hasUncommittedChanges) {
|
|
332
|
+
shouldRestart = true;
|
|
333
|
+
restartReason = restartReason ? `${restartReason}; Uncommitted changes` : 'Uncommitted changes detected';
|
|
334
|
+
|
|
335
|
+
// Get uncommitted changes for display using shared utility
|
|
336
|
+
const changes = await getUncommittedChangesDetails(tempDir);
|
|
337
|
+
feedbackLines.push('📝 Uncommitted changes detected:');
|
|
338
|
+
for (const line of changes) {
|
|
339
|
+
feedbackLines.push(` ${line}`);
|
|
340
|
+
}
|
|
341
|
+
feedbackLines.push('');
|
|
342
|
+
feedbackLines.push('IMPORTANT: You MUST handle these uncommitted changes by either:');
|
|
343
|
+
feedbackLines.push('1. COMMITTING them if they are part of the solution (git add + git commit + git push)');
|
|
344
|
+
feedbackLines.push('2. REVERTING them if they are not needed (git checkout -- <file> or git clean -fd)');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (shouldRestart) {
|
|
348
|
+
// Add standard instructions for auto-restart-until-mergable mode using shared utility
|
|
349
|
+
feedbackLines.push(...buildAutoRestartInstructions());
|
|
350
|
+
|
|
351
|
+
await log(formatAligned('🔄', 'RESTART TRIGGERED:', restartReason));
|
|
352
|
+
await log('');
|
|
353
|
+
|
|
354
|
+
// Post a comment to PR about the restart
|
|
355
|
+
try {
|
|
356
|
+
const commentBody = `## 🔄 Auto-restart triggered\n\n**Reason:** ${restartReason}\n\nStarting new session to address the issues.\n\n---\n*Auto-restart-until-mergable mode is active. Will continue until PR becomes mergeable.*`;
|
|
357
|
+
await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${commentBody}`;
|
|
358
|
+
await log(formatAligned('', '💬 Posted auto-restart notification to PR', '', 2));
|
|
359
|
+
} catch (commentError) {
|
|
360
|
+
reportError(commentError, {
|
|
361
|
+
context: 'post_auto_restart_comment',
|
|
362
|
+
owner,
|
|
363
|
+
repo,
|
|
364
|
+
prNumber,
|
|
365
|
+
operation: 'comment_on_pr',
|
|
366
|
+
});
|
|
367
|
+
await log(formatAligned('', '⚠️ Could not post comment to PR', '', 2));
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Get PR merge state status
|
|
371
|
+
const prStateResult = await $`gh api repos/${owner}/${repo}/pulls/${prNumber} --jq '.mergeStateStatus'`;
|
|
372
|
+
const mergeStateStatus = prStateResult.code === 0 ? prStateResult.stdout.toString().trim() : null;
|
|
373
|
+
|
|
374
|
+
// Execute the AI tool using shared utility
|
|
375
|
+
await log(formatAligned('🔄', 'Restarting:', `Running ${argv.tool.toUpperCase()} to address issues...`));
|
|
376
|
+
|
|
377
|
+
const toolResult = await executeToolIteration({
|
|
378
|
+
issueUrl,
|
|
379
|
+
owner,
|
|
380
|
+
repo,
|
|
381
|
+
issueNumber,
|
|
382
|
+
prNumber,
|
|
383
|
+
branchName: prBranch || branchName,
|
|
384
|
+
tempDir,
|
|
385
|
+
mergeStateStatus,
|
|
386
|
+
feedbackLines,
|
|
387
|
+
argv,
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
if (!toolResult.success) {
|
|
391
|
+
// Check if this is an API error using shared utility
|
|
392
|
+
if (isApiError(toolResult)) {
|
|
393
|
+
consecutiveApiErrors++;
|
|
394
|
+
await log(formatAligned('⚠️', `${argv.tool.toUpperCase()} execution failed`, `API error detected (${consecutiveApiErrors}/${MAX_API_ERROR_RETRIES})`, 2));
|
|
395
|
+
|
|
396
|
+
if (consecutiveApiErrors >= MAX_API_ERROR_RETRIES) {
|
|
397
|
+
await log('');
|
|
398
|
+
await log(formatAligned('❌', 'MAXIMUM API ERROR RETRIES REACHED', ''));
|
|
399
|
+
await log(formatAligned('', 'Error details:', toolResult.result || 'Unknown API error', 2));
|
|
400
|
+
await log(formatAligned('', 'Action:', 'Exiting to prevent infinite loop', 2));
|
|
401
|
+
return { success: false, reason: 'api_error', latestSessionId, latestAnthropicCost };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Apply exponential backoff
|
|
405
|
+
currentBackoffSeconds = Math.min(currentBackoffSeconds * 2, 300);
|
|
406
|
+
await log(formatAligned('', 'Backing off:', `Will retry after ${currentBackoffSeconds} seconds`, 2));
|
|
407
|
+
} else {
|
|
408
|
+
consecutiveApiErrors = 0;
|
|
409
|
+
currentBackoffSeconds = watchInterval;
|
|
410
|
+
await log(formatAligned('⚠️', `${argv.tool.toUpperCase()} execution failed`, 'Will retry in next check', 2));
|
|
411
|
+
}
|
|
412
|
+
} else {
|
|
413
|
+
// Success - reset error counters
|
|
414
|
+
consecutiveApiErrors = 0;
|
|
415
|
+
currentBackoffSeconds = watchInterval;
|
|
416
|
+
|
|
417
|
+
// Capture latest session data
|
|
418
|
+
if (toolResult.sessionId) {
|
|
419
|
+
latestSessionId = toolResult.sessionId;
|
|
420
|
+
latestAnthropicCost = toolResult.anthropicTotalCostUSD;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Attach log if enabled
|
|
424
|
+
const shouldAttachLogs = argv.attachLogs || argv['attach-logs'];
|
|
425
|
+
if (prNumber && shouldAttachLogs) {
|
|
426
|
+
await log('');
|
|
427
|
+
await log(formatAligned('📎', 'Uploading session log...', ''));
|
|
428
|
+
try {
|
|
429
|
+
const logFile = getLogFile();
|
|
430
|
+
if (logFile) {
|
|
431
|
+
const customTitle = `🔄 Auto-restart-until-mergable Log (iteration ${iteration})`;
|
|
432
|
+
await attachLogToGitHub({
|
|
433
|
+
logFile,
|
|
434
|
+
targetType: 'pr',
|
|
435
|
+
targetNumber: prNumber,
|
|
436
|
+
owner,
|
|
437
|
+
repo,
|
|
438
|
+
$,
|
|
439
|
+
log,
|
|
440
|
+
sanitizeLogContent,
|
|
441
|
+
verbose: argv.verbose,
|
|
442
|
+
customTitle,
|
|
443
|
+
sessionId: latestSessionId,
|
|
444
|
+
tempDir,
|
|
445
|
+
anthropicTotalCostUSD: latestAnthropicCost,
|
|
446
|
+
publicPricingEstimate: toolResult.publicPricingEstimate,
|
|
447
|
+
pricingInfo: toolResult.pricingInfo,
|
|
448
|
+
});
|
|
449
|
+
await log(formatAligned('', '✅ Session log uploaded to PR', '', 2));
|
|
450
|
+
}
|
|
451
|
+
} catch (logUploadError) {
|
|
452
|
+
reportError(logUploadError, {
|
|
453
|
+
context: 'attach_auto_restart_log',
|
|
454
|
+
prNumber,
|
|
455
|
+
owner,
|
|
456
|
+
repo,
|
|
457
|
+
iteration,
|
|
458
|
+
operation: 'upload_session_log',
|
|
459
|
+
});
|
|
460
|
+
await log(formatAligned('', `⚠️ Log upload error: ${cleanErrorMessage(logUploadError)}`, '', 2));
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
await log('');
|
|
465
|
+
await log(formatAligned('✅', `${argv.tool.toUpperCase()} execution completed:`, 'Checking if PR is now mergeable...'));
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Update last check time after restart
|
|
469
|
+
lastCheckTime = new Date();
|
|
470
|
+
} else if (blockers.length > 0) {
|
|
471
|
+
// There are blockers but none that warrant a restart (e.g., CI pending)
|
|
472
|
+
await log(formatAligned('⏳', 'Waiting for:', blockers.map(b => b.message).join(', '), 2));
|
|
473
|
+
} else {
|
|
474
|
+
await log(formatAligned('', 'No action needed', 'Continuing to monitor...', 2));
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Update last check time
|
|
478
|
+
lastCheckTime = currentTime;
|
|
479
|
+
} catch (error) {
|
|
480
|
+
reportError(error, {
|
|
481
|
+
context: 'watch_until_mergable',
|
|
482
|
+
prNumber,
|
|
483
|
+
owner,
|
|
484
|
+
repo,
|
|
485
|
+
operation: 'check_and_restart',
|
|
486
|
+
});
|
|
487
|
+
await log(formatAligned('⚠️', 'Check failed:', cleanErrorMessage(error), 2));
|
|
488
|
+
await log(formatAligned('', 'Will retry in:', `${watchInterval} seconds`, 2));
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Wait for next interval
|
|
492
|
+
const actualWaitSeconds = consecutiveApiErrors > 0 ? currentBackoffSeconds : watchInterval;
|
|
493
|
+
await log(formatAligned('⏱️', 'Next check in:', `${actualWaitSeconds} seconds...`, 2));
|
|
494
|
+
await log('');
|
|
495
|
+
await new Promise(resolve => setTimeout(resolve, actualWaitSeconds * 1000));
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Attempt to auto-merge PR after session ends
|
|
501
|
+
* This implements the --auto-merge functionality for one-shot merge attempts
|
|
502
|
+
*/
|
|
503
|
+
export const attemptAutoMerge = async params => {
|
|
504
|
+
const { owner, repo, prNumber, argv } = params;
|
|
505
|
+
|
|
506
|
+
await log('');
|
|
507
|
+
await log(formatAligned('🔀', 'AUTO-MERGE:', 'Checking if PR can be merged...'));
|
|
508
|
+
|
|
509
|
+
// Wait for CI to complete (with timeout)
|
|
510
|
+
const ciWaitResult = await waitForCI(
|
|
511
|
+
owner,
|
|
512
|
+
repo,
|
|
513
|
+
prNumber,
|
|
514
|
+
{
|
|
515
|
+
timeout: argv.autoMergeCiTimeout || 30 * 60 * 1000, // 30 minutes default
|
|
516
|
+
pollInterval: argv.autoMergeCiPollInterval || 30 * 1000, // 30 seconds default
|
|
517
|
+
onStatusUpdate: async status => {
|
|
518
|
+
if (argv.verbose) {
|
|
519
|
+
await log(` CI status: ${status.status}`, { verbose: true });
|
|
520
|
+
}
|
|
521
|
+
},
|
|
522
|
+
},
|
|
523
|
+
argv.verbose
|
|
524
|
+
);
|
|
525
|
+
|
|
526
|
+
if (!ciWaitResult.success) {
|
|
527
|
+
await log(formatAligned('⚠️', 'CI check failed or timed out:', ciWaitResult.error || ciWaitResult.status, 2));
|
|
528
|
+
return { success: false, reason: ciWaitResult.status, error: ciWaitResult.error };
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
await log(formatAligned('✅', 'CI checks passed:', 'Checking mergeability...', 2));
|
|
532
|
+
|
|
533
|
+
// Check if PR is mergeable
|
|
534
|
+
const mergeStatus = await checkPRMergeable(owner, repo, prNumber, argv.verbose);
|
|
535
|
+
if (!mergeStatus.mergeable) {
|
|
536
|
+
await log(formatAligned('⚠️', 'PR not mergeable:', mergeStatus.reason || 'Unknown reason', 2));
|
|
537
|
+
return { success: false, reason: 'not_mergeable', error: mergeStatus.reason };
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
await log(formatAligned('✅', 'PR is mergeable:', 'Attempting to merge...', 2));
|
|
541
|
+
|
|
542
|
+
// Attempt to merge
|
|
543
|
+
const mergeResult = await mergePullRequest(owner, repo, prNumber, { squash: argv.squash || false, deleteAfter: argv.deleteBranchAfterMerge || false }, argv.verbose);
|
|
544
|
+
|
|
545
|
+
if (mergeResult.success) {
|
|
546
|
+
await log(formatAligned('🎉', 'PR MERGED SUCCESSFULLY!', ''));
|
|
547
|
+
|
|
548
|
+
// Post success comment
|
|
549
|
+
try {
|
|
550
|
+
const commentBody = `## 🎉 Auto-merged\n\nThis pull request has been automatically merged by hive-mind after all CI checks passed and the PR became mergeable.\n\n---\n*Auto-merged by hive-mind with --auto-merge flag*`;
|
|
551
|
+
await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${commentBody}`;
|
|
552
|
+
} catch {
|
|
553
|
+
// Don't fail if comment posting fails
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return { success: true, reason: 'merged' };
|
|
557
|
+
} else {
|
|
558
|
+
await log(formatAligned('⚠️', 'Merge failed:', mergeResult.error || 'Unknown error', 2));
|
|
559
|
+
return { success: false, reason: 'merge_failed', error: mergeResult.error };
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Start auto-restart-until-mergable mode
|
|
565
|
+
*/
|
|
566
|
+
export const startAutoRestartUntilMergable = async params => {
|
|
567
|
+
const { argv } = params;
|
|
568
|
+
|
|
569
|
+
// Determine the mode
|
|
570
|
+
const isAutoMerge = argv.autoMerge || false;
|
|
571
|
+
const isAutoRestartUntilMergable = argv.autoRestartUntilMergable || false;
|
|
572
|
+
|
|
573
|
+
if (!isAutoMerge && !isAutoRestartUntilMergable) {
|
|
574
|
+
return null; // Neither mode enabled
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (!params.prNumber) {
|
|
578
|
+
await log('');
|
|
579
|
+
await log(formatAligned('⚠️', 'Auto-restart-until-mergable:', 'Requires a pull request'));
|
|
580
|
+
await log(formatAligned('', 'Note:', 'This mode only works with existing PRs', 2));
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// If --auto-merge implies --auto-restart-until-mergable
|
|
585
|
+
if (isAutoMerge) {
|
|
586
|
+
argv.autoRestartUntilMergable = true;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Start the watch loop
|
|
590
|
+
return await watchUntilMergable(params);
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
export default {
|
|
594
|
+
watchUntilMergable,
|
|
595
|
+
attemptAutoMerge,
|
|
596
|
+
startAutoRestartUntilMergable,
|
|
597
|
+
checkForNonBotComments,
|
|
598
|
+
};
|
package/src/solve.config.lib.mjs
CHANGED
|
@@ -200,6 +200,16 @@ export const createYargsConfig = yargsInstance => {
|
|
|
200
200
|
description: 'Maximum number of auto-restart iterations when uncommitted changes are detected (default: 3)',
|
|
201
201
|
default: 3,
|
|
202
202
|
})
|
|
203
|
+
.option('auto-merge', {
|
|
204
|
+
type: 'boolean',
|
|
205
|
+
description: 'Automatically merge the pull request when the working session is finished and all CI/CD statuses pass and PR is mergeable. Implies --auto-restart-until-mergable.',
|
|
206
|
+
default: false,
|
|
207
|
+
})
|
|
208
|
+
.option('auto-restart-until-mergable', {
|
|
209
|
+
type: 'boolean',
|
|
210
|
+
description: 'Auto-restart until PR becomes mergeable (no iteration limit). Restarts on new comments from non-bot users, CI failures, merge conflicts, or other issues. Does NOT auto-merge.',
|
|
211
|
+
default: false,
|
|
212
|
+
})
|
|
203
213
|
.option('continue-only-on-feedback', {
|
|
204
214
|
type: 'boolean',
|
|
205
215
|
description: 'Only continue if feedback is detected (works only with pull request link or issue link with --auto-continue)',
|