@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/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
+ };