@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.
@@ -3,6 +3,8 @@
3
3
  /**
4
4
  * Watch mode module for solve.mjs
5
5
  * Monitors for feedback continuously and restarts when changes are detected
6
+ *
7
+ * Uses shared utilities from solve.restart-shared.lib.mjs for common functions.
6
8
  */
7
9
 
8
10
  // Check if use is already defined globally (when imported from solve.mjs)
@@ -15,10 +17,6 @@ const use = globalThis.use;
15
17
  // Use command-stream for consistent $ behavior across runtimes
16
18
  const { $ } = await use('command-stream');
17
19
 
18
- // Import path and fs for cleanup operations
19
- const path = (await use('path')).default;
20
- const fs = (await use('fs')).promises;
21
-
22
20
  // Import shared library functions
23
21
  const lib = await import('./lib.mjs');
24
22
  const { log, cleanErrorMessage, formatAligned, getLogFile } = lib;
@@ -35,75 +33,9 @@ const { sanitizeLogContent, attachLogToGitHub } = githubLib;
35
33
 
36
34
  const { detectAndCountFeedback } = feedbackLib;
37
35
 
38
- /**
39
- * Check if PR has been merged
40
- */
41
- const checkPRMerged = async (owner, repo, prNumber) => {
42
- try {
43
- const prResult = await $`gh api repos/${owner}/${repo}/pulls/${prNumber} --jq '.merged'`;
44
- if (prResult.code === 0) {
45
- return prResult.stdout.toString().trim() === 'true';
46
- }
47
- } catch (error) {
48
- reportError(error, {
49
- context: 'check_pr_merged',
50
- owner,
51
- repo,
52
- prNumber,
53
- operation: 'check_merge_status',
54
- });
55
- // If we can't check, assume not merged
56
- return false;
57
- }
58
- return false;
59
- };
60
-
61
- /**
62
- * Clean up .playwright-mcp/ folder to prevent browser automation artifacts
63
- * from triggering auto-restart (Issue #1124)
64
- */
65
- const cleanupPlaywrightMcpFolder = async (tempDir, argv) => {
66
- if (argv.playwrightMcpAutoCleanup !== false) {
67
- const playwrightMcpDir = path.join(tempDir, '.playwright-mcp');
68
- try {
69
- const playwrightMcpExists = await fs
70
- .stat(playwrightMcpDir)
71
- .then(() => true)
72
- .catch(() => false);
73
- if (playwrightMcpExists) {
74
- await fs.rm(playwrightMcpDir, { recursive: true, force: true });
75
- await log('🧹 Cleaned up .playwright-mcp/ folder (browser automation artifacts)', { verbose: true });
76
- }
77
- } catch (cleanupError) {
78
- // Non-critical error, just log and continue
79
- await log(`âš ī¸ Could not clean up .playwright-mcp/ folder: ${cleanupError.message}`, { verbose: true });
80
- }
81
- }
82
- };
83
-
84
- /**
85
- * Check if there are uncommitted changes in the repository
86
- */
87
- const checkForUncommittedChanges = async (tempDir, $, argv = {}) => {
88
- // First, clean up .playwright-mcp/ folder to prevent false positives (Issue #1124)
89
- await cleanupPlaywrightMcpFolder(tempDir, argv);
90
-
91
- try {
92
- const gitStatusResult = await $({ cwd: tempDir })`git status --porcelain 2>&1`;
93
- if (gitStatusResult.code === 0) {
94
- const statusOutput = gitStatusResult.stdout.toString().trim();
95
- return statusOutput.length > 0;
96
- }
97
- } catch (error) {
98
- reportError(error, {
99
- context: 'check_pr_closed',
100
- tempDir,
101
- operation: 'check_close_status',
102
- });
103
- // If we can't check, assume no uncommitted changes
104
- }
105
- return false;
106
- };
36
+ // Import shared utilities from the restart-shared module
37
+ const restartShared = await import('./solve.restart-shared.lib.mjs');
38
+ const { checkPRMerged, checkForUncommittedChanges, getUncommittedChangesDetails, executeToolIteration, buildUncommittedChangesFeedback, isApiError } = restartShared;
107
39
 
108
40
  /**
109
41
  * Monitor for feedback in a loop and trigger restart when detected
@@ -143,7 +75,6 @@ export const watchForFeedback = async params => {
143
75
  await log('Press Ctrl+C to stop watching manually');
144
76
  await log('');
145
77
 
146
- // let lastCheckTime = new Date(); // Not currently used
147
78
  let iteration = 0;
148
79
  let autoRestartCount = 0;
149
80
  let firstIterationInTemporaryMode = isTemporaryWatch;
@@ -164,7 +95,7 @@ export const watchForFeedback = async params => {
164
95
 
165
96
  // In temporary watch mode, check if all changes have been committed
166
97
  if (isTemporaryWatch && !firstIterationInTemporaryMode) {
167
- const hasUncommitted = await checkForUncommittedChanges(tempDir, $, argv);
98
+ const hasUncommitted = await checkForUncommittedChanges(tempDir, argv);
168
99
  if (!hasUncommitted) {
169
100
  await log('');
170
101
  await log(formatAligned('✅', 'CHANGES COMMITTED!', 'Exiting auto-restart mode'));
@@ -219,7 +150,7 @@ export const watchForFeedback = async params => {
219
150
  // In temporary watch mode, also check for uncommitted changes as a restart trigger
220
151
  let hasUncommittedInTempMode = false;
221
152
  if (isTemporaryWatch && !firstIterationInTemporaryMode) {
222
- hasUncommittedInTempMode = await checkForUncommittedChanges(tempDir, $, argv);
153
+ hasUncommittedInTempMode = await checkForUncommittedChanges(tempDir, argv);
223
154
  }
224
155
 
225
156
  const shouldRestart = hasFeedback || firstIterationInTemporaryMode || hasUncommittedInTempMode;
@@ -228,24 +159,10 @@ export const watchForFeedback = async params => {
228
159
  // Handle uncommitted changes in temporary watch mode (first iteration or subsequent)
229
160
  if (firstIterationInTemporaryMode || hasUncommittedInTempMode) {
230
161
  await log(formatAligned('📝', 'UNCOMMITTED CHANGES:', '', 2));
231
- // Get uncommitted changes for display
232
- try {
233
- const gitStatusResult = await $({ cwd: tempDir })`git status --porcelain 2>&1`;
234
- if (gitStatusResult.code === 0) {
235
- const statusOutput = gitStatusResult.stdout.toString().trim();
236
- for (const line of statusOutput.split('\n')) {
237
- await log(formatAligned('', `â€ĸ ${line}`, '', 4));
238
- }
239
- }
240
- } catch (e) {
241
- reportError(e, {
242
- context: 'check_claude_file_exists',
243
- owner,
244
- repo,
245
- branchName,
246
- operation: 'check_file_in_branch',
247
- });
248
- // Ignore errors
162
+ // Get uncommitted changes for display using shared utility
163
+ const changes = await getUncommittedChangesDetails(tempDir);
164
+ for (const line of changes) {
165
+ await log(formatAligned('', `â€ĸ ${line}`, '', 4));
249
166
  }
250
167
  await log('');
251
168
 
@@ -261,16 +178,8 @@ export const watchForFeedback = async params => {
261
178
 
262
179
  // Get uncommitted files list for the comment
263
180
  let uncommittedFilesList = '';
264
- try {
265
- const gitStatusResult = await $({ cwd: tempDir })`git status --porcelain 2>&1`;
266
- if (gitStatusResult.code === 0) {
267
- const statusOutput = gitStatusResult.stdout.toString().trim();
268
- if (statusOutput) {
269
- uncommittedFilesList = '\n\n**Uncommitted files:**\n```\n' + statusOutput + '\n```';
270
- }
271
- }
272
- } catch {
273
- // If we can't get the file list, continue without it
181
+ if (changes.length > 0) {
182
+ uncommittedFilesList = '\n\n**Uncommitted files:**\n```\n' + changes.join('\n') + '\n```';
274
183
  }
275
184
 
276
185
  const commentBody = `## 🔄 Auto-restart ${autoRestartCount}/${maxAutoRestartIterations}\n\nDetected uncommitted changes from previous run. Starting new session to review and commit them.${uncommittedFilesList}\n\n---\n*Auto-restart will stop after changes are committed or after ${remainingIterations} more iteration${remainingIterations !== 1 ? 's' : ''}. Please wait until working session will end and give your feedback.*`;
@@ -289,39 +198,12 @@ export const watchForFeedback = async params => {
289
198
  }
290
199
  }
291
200
 
292
- // Add uncommitted changes info to feedbackLines for the run
201
+ // Add uncommitted changes info to feedbackLines using shared utility
293
202
  if (!feedbackLines) {
294
203
  feedbackLines = [];
295
204
  }
296
- feedbackLines.push('');
297
- feedbackLines.push(`âš ī¸ UNCOMMITTED CHANGES DETECTED (Auto-restart ${autoRestartCount}/${maxAutoRestartIterations}):`);
298
- feedbackLines.push('The following uncommitted changes were found in the repository:');
299
-
300
- try {
301
- const gitStatusResult = await $({ cwd: tempDir })`git status --porcelain 2>&1`;
302
- if (gitStatusResult.code === 0) {
303
- const statusOutput = gitStatusResult.stdout.toString().trim();
304
- feedbackLines.push('');
305
- for (const line of statusOutput.split('\n')) {
306
- feedbackLines.push(` ${line}`);
307
- }
308
- feedbackLines.push('');
309
- feedbackLines.push('IMPORTANT: You MUST handle these uncommitted changes by either:');
310
- feedbackLines.push('1. COMMITTING them if they are part of the solution (git add + git commit + git push)');
311
- feedbackLines.push('2. REVERTING them if they are not needed (git checkout -- <file> or git clean -fd)');
312
- feedbackLines.push('');
313
- feedbackLines.push('DO NOT leave uncommitted changes behind. The session will auto-restart until all changes are resolved.');
314
- }
315
- } catch (e) {
316
- reportError(e, {
317
- context: 'recheck_claude_file',
318
- owner,
319
- repo,
320
- branchName,
321
- operation: 'verify_file_in_branch',
322
- });
323
- // Ignore errors
324
- }
205
+ const uncommittedFeedback = buildUncommittedChangesFeedback(changes, autoRestartCount, maxAutoRestartIterations);
206
+ feedbackLines.push(...uncommittedFeedback);
325
207
  } else {
326
208
  await log(formatAligned('đŸ“ĸ', 'FEEDBACK DETECTED!', '', 2));
327
209
  feedbackLines.forEach(async line => {
@@ -331,152 +213,23 @@ export const watchForFeedback = async params => {
331
213
  await log(formatAligned('🔄', 'Restarting:', `Re-running ${argv.tool.toUpperCase()} to handle feedback...`));
332
214
  }
333
215
 
334
- // Import necessary modules for tool execution
335
- const memoryCheck = await import('./memory-check.mjs');
336
- const { getResourceSnapshot } = memoryCheck;
337
-
338
- let toolResult;
339
- if (argv.tool === 'opencode') {
340
- // Use OpenCode
341
- const opencodeExecLib = await import('./opencode.lib.mjs');
342
- const { executeOpenCode } = opencodeExecLib;
343
-
344
- // Get opencode path
345
- const opencodePath = argv.opencodePath || 'opencode';
346
-
347
- toolResult = await executeOpenCode({
348
- issueUrl,
349
- issueNumber,
350
- prNumber,
351
- prUrl: `https://github.com/${owner}/${repo}/pull/${prNumber}`,
352
- branchName,
353
- tempDir,
354
- isContinueMode: true,
355
- mergeStateStatus,
356
- forkedRepo: argv.fork,
357
- feedbackLines,
358
- owner,
359
- repo,
360
- argv,
361
- log,
362
- formatAligned,
363
- getResourceSnapshot,
364
- opencodePath,
365
- $,
366
- });
367
- } else if (argv.tool === 'codex') {
368
- // Use Codex
369
- const codexExecLib = await import('./codex.lib.mjs');
370
- const { executeCodex } = codexExecLib;
371
-
372
- // Get codex path
373
- const codexPath = argv.codexPath || 'codex';
374
-
375
- toolResult = await executeCodex({
376
- issueUrl,
377
- issueNumber,
378
- prNumber,
379
- prUrl: `https://github.com/${owner}/${repo}/pull/${prNumber}`,
380
- branchName,
381
- tempDir,
382
- isContinueMode: true,
383
- mergeStateStatus,
384
- forkedRepo: argv.fork,
385
- feedbackLines,
386
- forkActionsUrl: null,
387
- owner,
388
- repo,
389
- argv,
390
- log,
391
- setLogFile: () => {},
392
- getLogFile: () => '',
393
- formatAligned,
394
- getResourceSnapshot,
395
- codexPath,
396
- $,
397
- });
398
- } else if (argv.tool === 'agent') {
399
- // Use Agent
400
- const agentExecLib = await import('./agent.lib.mjs');
401
- const { executeAgent } = agentExecLib;
402
-
403
- // Get agent path
404
- const agentPath = argv.agentPath || 'agent';
405
-
406
- toolResult = await executeAgent({
407
- issueUrl,
408
- issueNumber,
409
- prNumber,
410
- prUrl: `https://github.com/${owner}/${repo}/pull/${prNumber}`,
411
- branchName,
412
- tempDir,
413
- isContinueMode: true,
414
- mergeStateStatus,
415
- forkedRepo: argv.fork,
416
- feedbackLines,
417
- forkActionsUrl: null,
418
- owner,
419
- repo,
420
- argv,
421
- log,
422
- formatAligned,
423
- getResourceSnapshot,
424
- agentPath,
425
- $,
426
- });
427
- } else {
428
- // Use Claude (default)
429
- const claudeExecLib = await import('./claude.lib.mjs');
430
- const { executeClaude, checkPlaywrightMcpAvailability } = claudeExecLib;
431
-
432
- // Get claude path
433
- const claudePath = argv.claudePath || 'claude';
434
-
435
- // Check for Playwright MCP availability if using Claude tool
436
- if (argv.tool === 'claude' || !argv.tool) {
437
- // If flag is true (default), check if Playwright MCP is actually available
438
- if (argv.promptPlaywrightMcp) {
439
- const playwrightMcpAvailable = await checkPlaywrightMcpAvailability();
440
- if (playwrightMcpAvailable) {
441
- await log('🎭 Playwright MCP detected - enabling browser automation hints', { verbose: true });
442
- } else {
443
- await log('â„šī¸ Playwright MCP not detected - browser automation hints will be disabled', {
444
- verbose: true,
445
- });
446
- argv.promptPlaywrightMcp = false;
447
- }
448
- } else {
449
- await log('â„šī¸ Playwright MCP explicitly disabled via --no-prompt-playwright-mcp', { verbose: true });
450
- }
451
- }
452
-
453
- toolResult = await executeClaude({
454
- issueUrl,
455
- issueNumber,
456
- prNumber,
457
- prUrl: `https://github.com/${owner}/${repo}/pull/${prNumber}`,
458
- branchName,
459
- tempDir,
460
- isContinueMode: true,
461
- mergeStateStatus,
462
- forkedRepo: argv.fork,
463
- feedbackLines,
464
- owner,
465
- repo,
466
- argv,
467
- log,
468
- formatAligned,
469
- getResourceSnapshot,
470
- claudePath,
471
- $,
472
- });
473
- }
216
+ // Execute tool using shared utility
217
+ const toolResult = await executeToolIteration({
218
+ issueUrl,
219
+ owner,
220
+ repo,
221
+ issueNumber,
222
+ prNumber,
223
+ branchName: prBranch || branchName,
224
+ tempDir,
225
+ mergeStateStatus,
226
+ feedbackLines,
227
+ argv,
228
+ });
474
229
 
475
230
  if (!toolResult.success) {
476
- // Check if this is an API error (404, 401, 400, etc.) from the result
477
- const isApiError = toolResult.result && (toolResult.result.includes('API Error:') || toolResult.result.includes('not_found_error') || toolResult.result.includes('authentication_error') || toolResult.result.includes('invalid_request_error'));
478
-
479
- if (isApiError) {
231
+ // Check if this is an API error using shared utility
232
+ if (isApiError(toolResult)) {
480
233
  consecutiveApiErrors++;
481
234
  await log(formatAligned('âš ī¸', `${argv.tool.toUpperCase()} execution failed`, `API error detected (${consecutiveApiErrors}/${MAX_API_ERROR_RETRIES})`, 2));
482
235
 
@@ -578,8 +331,6 @@ export const watchForFeedback = async params => {
578
331
  }
579
332
  }
580
333
 
581
- // Note: lastCheckTime tracking removed as it was not being used
582
-
583
334
  // Clear the first iteration flag after handling initial uncommitted changes
584
335
  if (firstIterationInTemporaryMode) {
585
336
  firstIterationInTemporaryMode = false;