@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/src/solve.mjs
CHANGED
|
@@ -73,6 +73,8 @@ const { createUncaughtExceptionHandler, createUnhandledRejectionHandler, handleM
|
|
|
73
73
|
|
|
74
74
|
const watchLib = await import('./solve.watch.lib.mjs');
|
|
75
75
|
const { startWatchMode } = watchLib;
|
|
76
|
+
const autoMergeLib = await import('./solve.auto-merge.lib.mjs');
|
|
77
|
+
const { startAutoRestartUntilMergable } = autoMergeLib;
|
|
76
78
|
const exitHandler = await import('./exit-handler.lib.mjs');
|
|
77
79
|
const { initializeExitHandler, installGlobalExitHandlers, safeExit } = exitHandler;
|
|
78
80
|
const getResourceSnapshot = memoryCheck.getResourceSnapshot;
|
|
@@ -1309,6 +1311,42 @@ try {
|
|
|
1309
1311
|
}
|
|
1310
1312
|
}
|
|
1311
1313
|
|
|
1314
|
+
// Start auto-restart-until-mergable mode if enabled
|
|
1315
|
+
// This runs after the normal watch mode completes (if any)
|
|
1316
|
+
// --auto-merge implies --auto-restart-until-mergable
|
|
1317
|
+
if (argv.autoMerge || argv.autoRestartUntilMergable) {
|
|
1318
|
+
const autoMergeResult = await startAutoRestartUntilMergable({
|
|
1319
|
+
issueUrl,
|
|
1320
|
+
owner,
|
|
1321
|
+
repo,
|
|
1322
|
+
issueNumber,
|
|
1323
|
+
prNumber,
|
|
1324
|
+
prBranch,
|
|
1325
|
+
branchName,
|
|
1326
|
+
tempDir,
|
|
1327
|
+
argv,
|
|
1328
|
+
});
|
|
1329
|
+
|
|
1330
|
+
// Update session data with latest from auto-merge mode for accurate pricing
|
|
1331
|
+
if (autoMergeResult && autoMergeResult.latestSessionId) {
|
|
1332
|
+
sessionId = autoMergeResult.latestSessionId;
|
|
1333
|
+
anthropicTotalCostUSD = autoMergeResult.latestAnthropicCost;
|
|
1334
|
+
if (argv.verbose) {
|
|
1335
|
+
await log('');
|
|
1336
|
+
await log('đ Updated session data from auto-restart-until-mergable mode:', { verbose: true });
|
|
1337
|
+
await log(` Session ID: ${sessionId}`, { verbose: true });
|
|
1338
|
+
if (anthropicTotalCostUSD !== null && anthropicTotalCostUSD !== undefined) {
|
|
1339
|
+
await log(` Anthropic cost: $${anthropicTotalCostUSD.toFixed(6)}`, { verbose: true });
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
// If auto-merge succeeded, update logs attached status
|
|
1345
|
+
if (autoMergeResult && autoMergeResult.success) {
|
|
1346
|
+
logsAttached = true;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1312
1350
|
// End work session using the new module
|
|
1313
1351
|
await endWorkSession({
|
|
1314
1352
|
isContinueMode,
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared utilities for watch mode and auto-restart-until-mergable mode
|
|
5
|
+
*
|
|
6
|
+
* This module contains common functions used by both:
|
|
7
|
+
* - solve.watch.lib.mjs (--watch mode and temporary auto-restart)
|
|
8
|
+
* - solve.auto-merge.lib.mjs (--auto-merge and --auto-restart-until-mergable)
|
|
9
|
+
*
|
|
10
|
+
* Functions extracted to reduce duplication and ensure consistent behavior.
|
|
11
|
+
*
|
|
12
|
+
* @see https://github.com/link-assistant/hive-mind/issues/1190
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// Check if use is already defined globally (when imported from solve.mjs)
|
|
16
|
+
// If not, fetch it (when running standalone)
|
|
17
|
+
if (typeof globalThis.use === 'undefined') {
|
|
18
|
+
globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
|
|
19
|
+
}
|
|
20
|
+
const use = globalThis.use;
|
|
21
|
+
|
|
22
|
+
// Use command-stream for consistent $ behavior across runtimes
|
|
23
|
+
const { $ } = await use('command-stream');
|
|
24
|
+
|
|
25
|
+
// Import path and fs for cleanup operations
|
|
26
|
+
const path = (await use('path')).default;
|
|
27
|
+
const fs = (await use('fs')).promises;
|
|
28
|
+
|
|
29
|
+
// Import shared library functions
|
|
30
|
+
const lib = await import('./lib.mjs');
|
|
31
|
+
const { log, formatAligned } = lib;
|
|
32
|
+
|
|
33
|
+
// Import Sentry integration
|
|
34
|
+
const sentryLib = await import('./sentry.lib.mjs');
|
|
35
|
+
const { reportError } = sentryLib;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Check if PR has been merged
|
|
39
|
+
* @param {string} owner - Repository owner
|
|
40
|
+
* @param {string} repo - Repository name
|
|
41
|
+
* @param {number} prNumber - Pull request number
|
|
42
|
+
* @returns {Promise<boolean>}
|
|
43
|
+
*/
|
|
44
|
+
export const checkPRMerged = async (owner, repo, prNumber) => {
|
|
45
|
+
try {
|
|
46
|
+
const prResult = await $`gh api repos/${owner}/${repo}/pulls/${prNumber} --jq '.merged'`;
|
|
47
|
+
if (prResult.code === 0) {
|
|
48
|
+
return prResult.stdout.toString().trim() === 'true';
|
|
49
|
+
}
|
|
50
|
+
} catch (error) {
|
|
51
|
+
reportError(error, {
|
|
52
|
+
context: 'check_pr_merged',
|
|
53
|
+
owner,
|
|
54
|
+
repo,
|
|
55
|
+
prNumber,
|
|
56
|
+
operation: 'check_merge_status',
|
|
57
|
+
});
|
|
58
|
+
// If we can't check, assume not merged
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check if PR is closed (but not merged)
|
|
66
|
+
* @param {string} owner - Repository owner
|
|
67
|
+
* @param {string} repo - Repository name
|
|
68
|
+
* @param {number} prNumber - Pull request number
|
|
69
|
+
* @returns {Promise<boolean>}
|
|
70
|
+
*/
|
|
71
|
+
export const checkPRClosed = async (owner, repo, prNumber) => {
|
|
72
|
+
try {
|
|
73
|
+
const prResult = await $`gh api repos/${owner}/${repo}/pulls/${prNumber} --jq '.state'`;
|
|
74
|
+
if (prResult.code === 0) {
|
|
75
|
+
return prResult.stdout.toString().trim() === 'closed';
|
|
76
|
+
}
|
|
77
|
+
} catch (error) {
|
|
78
|
+
reportError(error, {
|
|
79
|
+
context: 'check_pr_closed',
|
|
80
|
+
owner,
|
|
81
|
+
repo,
|
|
82
|
+
prNumber,
|
|
83
|
+
operation: 'check_close_status',
|
|
84
|
+
});
|
|
85
|
+
// If we can't check, assume not closed
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Clean up .playwright-mcp/ folder to prevent browser automation artifacts
|
|
93
|
+
* from triggering auto-restart (Issue #1124)
|
|
94
|
+
* @param {string} tempDir - Temporary directory path
|
|
95
|
+
* @param {Object} argv - Command line arguments
|
|
96
|
+
*/
|
|
97
|
+
export const cleanupPlaywrightMcpFolder = async (tempDir, argv = {}) => {
|
|
98
|
+
if (argv.playwrightMcpAutoCleanup !== false) {
|
|
99
|
+
const playwrightMcpDir = path.join(tempDir, '.playwright-mcp');
|
|
100
|
+
try {
|
|
101
|
+
const playwrightMcpExists = await fs
|
|
102
|
+
.stat(playwrightMcpDir)
|
|
103
|
+
.then(() => true)
|
|
104
|
+
.catch(() => false);
|
|
105
|
+
if (playwrightMcpExists) {
|
|
106
|
+
await fs.rm(playwrightMcpDir, { recursive: true, force: true });
|
|
107
|
+
await log('đ§š Cleaned up .playwright-mcp/ folder (browser automation artifacts)', { verbose: true });
|
|
108
|
+
}
|
|
109
|
+
} catch (cleanupError) {
|
|
110
|
+
// Non-critical error, just log and continue
|
|
111
|
+
await log(`â ī¸ Could not clean up .playwright-mcp/ folder: ${cleanupError.message}`, { verbose: true });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Check if there are uncommitted changes in the repository
|
|
118
|
+
* @param {string} tempDir - Temporary directory path
|
|
119
|
+
* @param {Object} argv - Command line arguments (optional)
|
|
120
|
+
* @returns {Promise<boolean>}
|
|
121
|
+
*/
|
|
122
|
+
export const checkForUncommittedChanges = async (tempDir, argv = {}) => {
|
|
123
|
+
// First, clean up .playwright-mcp/ folder to prevent false positives (Issue #1124)
|
|
124
|
+
await cleanupPlaywrightMcpFolder(tempDir, argv);
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const gitStatusResult = await $({ cwd: tempDir })`git status --porcelain 2>&1`;
|
|
128
|
+
if (gitStatusResult.code === 0) {
|
|
129
|
+
const statusOutput = gitStatusResult.stdout.toString().trim();
|
|
130
|
+
return statusOutput.length > 0;
|
|
131
|
+
}
|
|
132
|
+
} catch (error) {
|
|
133
|
+
reportError(error, {
|
|
134
|
+
context: 'check_uncommitted_changes',
|
|
135
|
+
tempDir,
|
|
136
|
+
operation: 'git_status',
|
|
137
|
+
});
|
|
138
|
+
// If we can't check, assume no uncommitted changes
|
|
139
|
+
}
|
|
140
|
+
return false;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Get uncommitted changes details for display
|
|
145
|
+
* @param {string} tempDir - Temporary directory path
|
|
146
|
+
* @returns {Promise<string[]>}
|
|
147
|
+
*/
|
|
148
|
+
export const getUncommittedChangesDetails = async tempDir => {
|
|
149
|
+
const changes = [];
|
|
150
|
+
try {
|
|
151
|
+
const gitStatusResult = await $({ cwd: tempDir })`git status --porcelain 2>&1`;
|
|
152
|
+
if (gitStatusResult.code === 0) {
|
|
153
|
+
const statusOutput = gitStatusResult.stdout.toString().trim();
|
|
154
|
+
if (statusOutput) {
|
|
155
|
+
changes.push(...statusOutput.split('\n'));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
} catch (error) {
|
|
159
|
+
reportError(error, {
|
|
160
|
+
context: 'get_uncommitted_changes_details',
|
|
161
|
+
tempDir,
|
|
162
|
+
operation: 'git_status',
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
return changes;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Execute the AI tool (Claude, OpenCode, Codex, Agent) for a restart iteration
|
|
170
|
+
* This is the shared tool execution logic used by both watch mode and auto-restart-until-mergable mode
|
|
171
|
+
* @param {Object} params - Execution parameters
|
|
172
|
+
* @returns {Promise<Object>} - Tool execution result
|
|
173
|
+
*/
|
|
174
|
+
export const executeToolIteration = async params => {
|
|
175
|
+
const { issueUrl, owner, repo, issueNumber, prNumber, branchName, tempDir, mergeStateStatus, feedbackLines, argv } = params;
|
|
176
|
+
|
|
177
|
+
// Import necessary modules for tool execution
|
|
178
|
+
const memoryCheck = await import('./memory-check.mjs');
|
|
179
|
+
const { getResourceSnapshot } = memoryCheck;
|
|
180
|
+
|
|
181
|
+
let toolResult;
|
|
182
|
+
if (argv.tool === 'opencode') {
|
|
183
|
+
// Use OpenCode
|
|
184
|
+
const opencodeExecLib = await import('./opencode.lib.mjs');
|
|
185
|
+
const { executeOpenCode } = opencodeExecLib;
|
|
186
|
+
const opencodePath = argv.opencodePath || 'opencode';
|
|
187
|
+
|
|
188
|
+
toolResult = await executeOpenCode({
|
|
189
|
+
issueUrl,
|
|
190
|
+
issueNumber,
|
|
191
|
+
prNumber,
|
|
192
|
+
prUrl: `https://github.com/${owner}/${repo}/pull/${prNumber}`,
|
|
193
|
+
branchName,
|
|
194
|
+
tempDir,
|
|
195
|
+
isContinueMode: true,
|
|
196
|
+
mergeStateStatus,
|
|
197
|
+
forkedRepo: argv.fork,
|
|
198
|
+
feedbackLines,
|
|
199
|
+
owner,
|
|
200
|
+
repo,
|
|
201
|
+
argv,
|
|
202
|
+
log,
|
|
203
|
+
formatAligned,
|
|
204
|
+
getResourceSnapshot,
|
|
205
|
+
opencodePath,
|
|
206
|
+
$,
|
|
207
|
+
});
|
|
208
|
+
} else if (argv.tool === 'codex') {
|
|
209
|
+
// Use Codex
|
|
210
|
+
const codexExecLib = await import('./codex.lib.mjs');
|
|
211
|
+
const { executeCodex } = codexExecLib;
|
|
212
|
+
const codexPath = argv.codexPath || 'codex';
|
|
213
|
+
|
|
214
|
+
toolResult = await executeCodex({
|
|
215
|
+
issueUrl,
|
|
216
|
+
issueNumber,
|
|
217
|
+
prNumber,
|
|
218
|
+
prUrl: `https://github.com/${owner}/${repo}/pull/${prNumber}`,
|
|
219
|
+
branchName,
|
|
220
|
+
tempDir,
|
|
221
|
+
isContinueMode: true,
|
|
222
|
+
mergeStateStatus,
|
|
223
|
+
forkedRepo: argv.fork,
|
|
224
|
+
feedbackLines,
|
|
225
|
+
forkActionsUrl: null,
|
|
226
|
+
owner,
|
|
227
|
+
repo,
|
|
228
|
+
argv,
|
|
229
|
+
log,
|
|
230
|
+
setLogFile: () => {},
|
|
231
|
+
getLogFile: () => '',
|
|
232
|
+
formatAligned,
|
|
233
|
+
getResourceSnapshot,
|
|
234
|
+
codexPath,
|
|
235
|
+
$,
|
|
236
|
+
});
|
|
237
|
+
} else if (argv.tool === 'agent') {
|
|
238
|
+
// Use Agent
|
|
239
|
+
const agentExecLib = await import('./agent.lib.mjs');
|
|
240
|
+
const { executeAgent } = agentExecLib;
|
|
241
|
+
const agentPath = argv.agentPath || 'agent';
|
|
242
|
+
|
|
243
|
+
toolResult = await executeAgent({
|
|
244
|
+
issueUrl,
|
|
245
|
+
issueNumber,
|
|
246
|
+
prNumber,
|
|
247
|
+
prUrl: `https://github.com/${owner}/${repo}/pull/${prNumber}`,
|
|
248
|
+
branchName,
|
|
249
|
+
tempDir,
|
|
250
|
+
isContinueMode: true,
|
|
251
|
+
mergeStateStatus,
|
|
252
|
+
forkedRepo: argv.fork,
|
|
253
|
+
feedbackLines,
|
|
254
|
+
forkActionsUrl: null,
|
|
255
|
+
owner,
|
|
256
|
+
repo,
|
|
257
|
+
argv,
|
|
258
|
+
log,
|
|
259
|
+
formatAligned,
|
|
260
|
+
getResourceSnapshot,
|
|
261
|
+
agentPath,
|
|
262
|
+
$,
|
|
263
|
+
});
|
|
264
|
+
} else {
|
|
265
|
+
// Use Claude (default)
|
|
266
|
+
const claudeExecLib = await import('./claude.lib.mjs');
|
|
267
|
+
const { executeClaude, checkPlaywrightMcpAvailability } = claudeExecLib;
|
|
268
|
+
const claudePath = argv.claudePath || 'claude';
|
|
269
|
+
|
|
270
|
+
// Check for Playwright MCP availability if using Claude tool
|
|
271
|
+
if (argv.tool === 'claude' || !argv.tool) {
|
|
272
|
+
if (argv.promptPlaywrightMcp) {
|
|
273
|
+
const playwrightMcpAvailable = await checkPlaywrightMcpAvailability();
|
|
274
|
+
if (playwrightMcpAvailable) {
|
|
275
|
+
await log('đ Playwright MCP detected - enabling browser automation hints', { verbose: true });
|
|
276
|
+
} else {
|
|
277
|
+
await log('âšī¸ Playwright MCP not detected - browser automation hints will be disabled', { verbose: true });
|
|
278
|
+
argv.promptPlaywrightMcp = false;
|
|
279
|
+
}
|
|
280
|
+
} else {
|
|
281
|
+
await log('âšī¸ Playwright MCP explicitly disabled via --no-prompt-playwright-mcp', { verbose: true });
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
toolResult = await executeClaude({
|
|
286
|
+
issueUrl,
|
|
287
|
+
issueNumber,
|
|
288
|
+
prNumber,
|
|
289
|
+
prUrl: `https://github.com/${owner}/${repo}/pull/${prNumber}`,
|
|
290
|
+
branchName,
|
|
291
|
+
tempDir,
|
|
292
|
+
isContinueMode: true,
|
|
293
|
+
mergeStateStatus,
|
|
294
|
+
forkedRepo: argv.fork,
|
|
295
|
+
feedbackLines,
|
|
296
|
+
owner,
|
|
297
|
+
repo,
|
|
298
|
+
argv,
|
|
299
|
+
log,
|
|
300
|
+
formatAligned,
|
|
301
|
+
getResourceSnapshot,
|
|
302
|
+
claudePath,
|
|
303
|
+
$,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return toolResult;
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Build standard instructions for auto-restart modes
|
|
312
|
+
* These instructions ensure the AI agent addresses all aspects needed for mergeability
|
|
313
|
+
* @returns {string[]} Array of instruction lines
|
|
314
|
+
*/
|
|
315
|
+
export const buildAutoRestartInstructions = () => {
|
|
316
|
+
return ['', '='.repeat(60), 'đ¯ AUTO-RESTART MODE INSTRUCTIONS:', '='.repeat(60), '', 'Ensure to get latest version of default branch to make all conflicts resolved if present.', 'Ensure you comply with all CI/CD check requirements, and they pass.', 'Ensure all changes are correct, consistent and fully meet all discussed requirements', '(check issue description and all comments in issue and in pull request).', ''];
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Build feedback lines for uncommitted changes
|
|
321
|
+
* @param {string[]} changes - Array of uncommitted change lines from git status
|
|
322
|
+
* @param {number} restartCount - Current restart iteration number
|
|
323
|
+
* @param {number} maxIterations - Maximum restart iterations
|
|
324
|
+
* @returns {string[]} Array of feedback lines
|
|
325
|
+
*/
|
|
326
|
+
export const buildUncommittedChangesFeedback = (changes, restartCount = 0, maxIterations = 0) => {
|
|
327
|
+
const feedbackLines = [];
|
|
328
|
+
const iterationInfo = maxIterations > 0 ? ` (Auto-restart ${restartCount}/${maxIterations})` : '';
|
|
329
|
+
|
|
330
|
+
feedbackLines.push('');
|
|
331
|
+
feedbackLines.push(`â ī¸ UNCOMMITTED CHANGES DETECTED${iterationInfo}:`);
|
|
332
|
+
feedbackLines.push('The following uncommitted changes were found in the repository:');
|
|
333
|
+
feedbackLines.push('');
|
|
334
|
+
|
|
335
|
+
for (const line of changes) {
|
|
336
|
+
feedbackLines.push(` ${line}`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
feedbackLines.push('');
|
|
340
|
+
feedbackLines.push('IMPORTANT: You MUST handle these uncommitted changes by either:');
|
|
341
|
+
feedbackLines.push('1. COMMITTING them if they are part of the solution (git add + git commit + git push)');
|
|
342
|
+
feedbackLines.push('2. REVERTING them if they are not needed (git checkout -- <file> or git clean -fd)');
|
|
343
|
+
feedbackLines.push('');
|
|
344
|
+
feedbackLines.push('DO NOT leave uncommitted changes behind. The session will auto-restart until all changes are resolved.');
|
|
345
|
+
|
|
346
|
+
return feedbackLines;
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Check if a tool result indicates an API error
|
|
351
|
+
* @param {Object} toolResult - Tool execution result
|
|
352
|
+
* @returns {boolean}
|
|
353
|
+
*/
|
|
354
|
+
export const isApiError = toolResult => {
|
|
355
|
+
if (!toolResult || !toolResult.result) return false;
|
|
356
|
+
|
|
357
|
+
const errorPatterns = ['API Error:', 'not_found_error', 'authentication_error', 'invalid_request_error'];
|
|
358
|
+
|
|
359
|
+
return errorPatterns.some(pattern => toolResult.result.includes(pattern));
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
export default {
|
|
363
|
+
checkPRMerged,
|
|
364
|
+
checkPRClosed,
|
|
365
|
+
cleanupPlaywrightMcpFolder,
|
|
366
|
+
checkForUncommittedChanges,
|
|
367
|
+
getUncommittedChangesDetails,
|
|
368
|
+
executeToolIteration,
|
|
369
|
+
buildAutoRestartInstructions,
|
|
370
|
+
buildUncommittedChangesFeedback,
|
|
371
|
+
isApiError,
|
|
372
|
+
};
|