@link-assistant/hive-mind 1.37.0 → 1.37.1

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.37.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 8df5a3d: Treat ENOSPC as immediate failure at all stages (issues #1212, #1211)
8
+
9
+ When disk space runs out during any stage — including git clone, execution, and log
10
+ upload — ENOSPC is now treated as a hard failure (not partial success). Added ENOSPC
11
+ detection to git clone error classification so disk-full clone failures are not
12
+ retried. The isENOSPC utility now detects git-specific patterns like "unable to write
13
+ file" and "cannot create directory". Actionable disk cleanup guidance is provided.
14
+
3
15
  ## 1.37.0
4
16
 
5
17
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.37.0",
3
+ "version": "1.37.1",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -6,7 +6,7 @@ if (typeof globalThis.use === 'undefined') {
6
6
  const { $ } = await use('command-stream');
7
7
  const fs = (await use('fs')).promises;
8
8
  const path = (await use('path')).default;
9
- import { log } from './lib.mjs';
9
+ import { log, isENOSPC } from './lib.mjs';
10
10
  import { reportError } from './sentry.lib.mjs';
11
11
  import { timeouts, retryLimits, claudeCode, getClaudeEnv, getThinkingLevelToTokens, getTokensToThinkingLevel, supportsThinkingBudget, DEFAULT_MAX_THINKING_BUDGET, getMaxOutputTokensForModel } from './config.lib.mjs';
12
12
  import { detectUsageLimit, formatUsageLimitMessage } from './usage-limit.lib.mjs';
@@ -808,12 +808,10 @@ export const executeClaudeCommand = async params => {
808
808
  await log(' Feedback info included: No', { verbose: true });
809
809
  }
810
810
  }
811
- // Take resource snapshot before execution
812
811
  const resourcesBefore = await getResourceSnapshot();
813
812
  await log('📈 System resources before execution:', { verbose: true });
814
813
  await log(` Memory: ${resourcesBefore.memory.split('\n')[1]}`, { verbose: true });
815
814
  await log(` Load: ${resourcesBefore.load}`, { verbose: true });
816
- // Use command-stream's async iteration for real-time streaming with file logging
817
815
  let commandFailed = false;
818
816
  let sessionId = null;
819
817
  let limitReached = false;
@@ -1027,7 +1025,12 @@ export const executeClaudeCommand = async params => {
1027
1025
  const subtype = data.subtype || 'unknown';
1028
1026
  if (subtype === 'error_during_execution') {
1029
1027
  errorDuringExecution = true;
1030
- await log(`⚠️ Error during execution (subtype: ${subtype}) - work may be completed`, { verbose: true });
1028
+ if ((data.errors || []).some(e => isENOSPC(e))) {
1029
+ commandFailed = true;
1030
+ await log('❌ ENOSPC: No space left on device. Free disk space (check ~/.claude/debug).');
1031
+ } else {
1032
+ await log(`⚠️ Error during execution (subtype: ${subtype}) - work may be completed`, { verbose: true });
1033
+ }
1031
1034
  } else {
1032
1035
  commandFailed = true;
1033
1036
  await log(`⚠️ Detected error from Claude CLI (subtype: ${subtype})`, { verbose: true });
@@ -1039,8 +1042,8 @@ export const executeClaudeCommand = async params => {
1039
1042
  if (lastMessage.includes('Internal server error') && !lastMessage.includes('Overloaded')) {
1040
1043
  isInternalServerError = true;
1041
1044
  }
1042
- // Issue #1353: Detect "Request timed out" Claude CLI emits {type:"result",is_error:true,result:"Request timed out"} after exhausting retries
1043
- if (lastMessage === 'Request timed out' || lastMessage.includes('Request timed out')) {
1045
+ // Issue #1353: Detect "Request timed out" from Claude CLI
1046
+ if (lastMessage.includes('Request timed out')) {
1044
1047
  isRequestTimeout = true;
1045
1048
  await log('⏱️ Detected request timeout from Claude CLI (will retry with --resume)', { verbose: true });
1046
1049
  }
@@ -1277,8 +1280,7 @@ export const executeClaudeCommand = async params => {
1277
1280
  }
1278
1281
  }
1279
1282
  }
1280
- // Issue #1354: Detect silent failures (no messages + stderr errors, e.g. "kill EPERM" with exit 0).
1281
- // Skip if result event confirmed success (definitive proof regardless of messageCount).
1283
+ // Issue #1354: Detect silent failures (no messages + stderr errors, skip if result confirmed success)
1282
1284
  if (!commandFailed && !resultSuccessReceived && stderrErrors.length > 0 && messageCount === 0 && toolUseCount === 0) {
1283
1285
  commandFailed = true;
1284
1286
  const errorsPreview = stderrErrors
@@ -1307,9 +1309,7 @@ export const executeClaudeCommand = async params => {
1307
1309
  resultSummary, // Issue #1263: Include result summary
1308
1310
  };
1309
1311
  }
1310
- // Issue #1088: If error_during_execution occurred but command didn't fail,
1311
- // log it as "Finished with errors" instead of pure success
1312
- // Issue #1351: Distinguish interrupted sessions (exit code 130) from normal completion
1312
+ // Issue #1088/#1351: Log execution result status
1313
1313
  if (exitCode === 130) {
1314
1314
  await log('\n\n⚠️ Claude command interrupted (CTRL+C)');
1315
1315
  } else if (errorDuringExecution) {
@@ -2,7 +2,7 @@
2
2
  // GitHub-related utility functions. Check if use is already defined (when imported from solve.mjs), if not, fetch it (when running standalone)
3
3
  if (typeof globalThis.use === 'undefined') globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
4
4
  const { $ } = await use('command-stream'); // Use command-stream for consistent $ behavior
5
- import { log, maskToken, cleanErrorMessage } from './lib.mjs';
5
+ import { log, maskToken, cleanErrorMessage, isENOSPC } from './lib.mjs';
6
6
  import { reportError } from './sentry.lib.mjs';
7
7
  import { githubLimits, timeouts } from './config.lib.mjs';
8
8
  import { batchCheckPullRequestsForIssues as batchCheckPRs, batchCheckArchivedRepositories as batchCheckArchived } from './github.batch.lib.mjs';
@@ -162,16 +162,7 @@ export const checkGitHubPermissions = async () => {
162
162
  return true; // Continue despite permission check failure
163
163
  }
164
164
  };
165
- /**
166
- * Check if the current user has write (push) permissions to a specific repository
167
- * This helps fail early before wasting AI tokens when --fork option is not used
168
- * @param {string} owner - Repository owner
169
- * @param {string} repo - Repository name
170
- * @param {Object} options - Configuration options
171
- * @param {boolean} options.useFork - Whether --fork flag is enabled
172
- * @param {string} options.issueUrl - Original issue URL for error messages
173
- * @returns {Promise<boolean>} True if has write access OR fork mode is enabled, false otherwise
174
- */
165
+ /** Check if user has write permissions to repo. Fails early if --fork not used. */
175
166
  export const checkRepositoryWritePermission = async (owner, repo, options = {}) => {
176
167
  const { useFork = false, issueUrl = '' } = options;
177
168
  // Skip check if fork mode is enabled - user will work in their own fork
@@ -379,6 +370,17 @@ export async function attachLogToGitHub(options) {
379
370
  const targetName = targetType === 'pr' ? 'Pull Request' : 'Issue';
380
371
  const ghCommand = targetType === 'pr' ? 'pr' : 'issue';
381
372
  try {
373
+ // Issue #1212: Check disk space before attempting log upload (100MB minimum)
374
+ try {
375
+ const { checkDiskSpace } = await import('./memory-check.mjs');
376
+ const diskCheck = await checkDiskSpace(100, { log: async () => {} });
377
+ if (!diskCheck.success) {
378
+ await log(` ❌ Insufficient disk space for log upload (${diskCheck.availableMB}MB available, 100MB required). Free disk space and retry.`);
379
+ return false;
380
+ }
381
+ } catch {
382
+ /* disk check failure is non-fatal — continue to actual operation */
383
+ }
382
384
  // Check if log file exists and is not empty
383
385
  const logStats = await fs.stat(logFile);
384
386
  if (logStats.size === 0) {
@@ -800,7 +802,9 @@ ${sessionNote}
800
802
  return await attachRegularComment(options, logComment);
801
803
  }
802
804
  } catch (uploadError) {
803
- await log(` ❌ Error uploading log file: ${uploadError.message}`);
805
+ // Issue #1212: ENOSPC-specific actionable guidance
806
+ const msg = isENOSPC(uploadError) ? 'ENOSPC: No space left on device during log upload. Free disk space and retry.' : `Error uploading log file: ${uploadError.message}`;
807
+ await log(` ❌ ${msg}`);
804
808
  return false;
805
809
  }
806
810
  }
package/src/lib.mjs CHANGED
@@ -340,6 +340,28 @@ export const measureTime = async (fn, label = 'Operation') => {
340
340
  }
341
341
  };
342
342
 
343
+ /**
344
+ * Check if an error is an ENOSPC (no space left on device) error
345
+ * Issue #1212: ENOSPC errors need specific handling because they cascade
346
+ * (once disk is full, all operations fail) and require user action (cleanup).
347
+ * @param {Error|string} error - Error object or message
348
+ * @returns {boolean} True if the error is an ENOSPC error
349
+ */
350
+ export const isENOSPC = error => {
351
+ if (!error) return false;
352
+ const message = error?.message || (typeof error === 'string' ? error : '');
353
+ const lowerMessage = message.toLowerCase();
354
+ return (
355
+ error?.code === 'ENOSPC' ||
356
+ message.includes('ENOSPC') ||
357
+ lowerMessage.includes('no space left on device') ||
358
+ // Issue #1211: git clone ENOSPC patterns — "unable to write file" and
359
+ // "cannot create directory" occur when disk fills during checkout
360
+ (lowerMessage.includes('unable to write file') && lowerMessage.includes('error')) ||
361
+ (lowerMessage.includes('cannot create directory') && lowerMessage.includes('no space left'))
362
+ );
363
+ };
364
+
343
365
  /**
344
366
  * Clean up error messages for better user experience
345
367
  * @param {Error|string} error - Error object or message
@@ -502,6 +524,7 @@ export default {
502
524
  retry,
503
525
  formatBytes,
504
526
  measureTime,
527
+ isENOSPC,
505
528
  cleanErrorMessage,
506
529
  formatAligned,
507
530
  displayFormattedError,
@@ -43,7 +43,7 @@ export const handleFailure = async options => {
43
43
 
44
44
  // If --attach-logs is enabled, try to attach failure logs
45
45
  if (shouldAttachLogs && getLogFile()) {
46
- // Issue #1462: Upload logs to PR if available, otherwise fall back to the issue
46
+ // Issues #1212, #1462: Upload logs to PR if available, otherwise fall back to the issue
47
47
  const hasPR = global.createdPR && global.createdPR.number;
48
48
  const hasIssue = global.issueNumber;
49
49
  const targetType = hasPR ? 'pr' : hasIssue ? 'issue' : null;
package/src/solve.mjs CHANGED
@@ -508,8 +508,7 @@ if (isPrUrl) {
508
508
  issueNumber = urlNumber;
509
509
  await log(`📝 Issue mode: Working with issue #${issueNumber}`);
510
510
  }
511
- // Issue #1462: Store issueNumber in global so error handlers can upload logs to the issue
512
- // as a fallback when PR creation fails and global.createdPR is not available
511
+ // Issues #1212, #1462: Store issueNumber globally for error handlers (attach failure logs to issue when no PR exists)
513
512
  global.issueNumber = issueNumber;
514
513
  const workspaceInfo = argv.enableWorkspaces ? { owner, repo, issueNumber } : null;
515
514
  const { tempDir, workspaceTmpDir, needsClone } = await setupTempDirectory(argv, workspaceInfo);
@@ -961,10 +960,12 @@ try {
961
960
  if (logUploadSuccess) {
962
961
  await log(' ✅ Logs uploaded successfully');
963
962
  } else {
964
- await log(' ⚠️ Failed to upload logs', { verbose: true });
963
+ // Issue #1212: Always show log upload failures (not just verbose)
964
+ await log(' ⚠️ Failed to upload logs');
965
965
  }
966
966
  } catch (uploadError) {
967
- await log(` ⚠️ Error uploading logs: ${uploadError.message}`, { verbose: true });
967
+ // Issue #1212: Always show log upload errors (not just verbose)
968
+ await log(` ⚠️ Error uploading logs: ${uploadError.message}`);
968
969
  }
969
970
  } else if (prNumber) {
970
971
  // Fallback: Post simple failure comment if logs are not attached
@@ -1029,10 +1030,12 @@ try {
1029
1030
  if (logUploadSuccess) {
1030
1031
  await log(' ✅ Logs uploaded successfully');
1031
1032
  } else {
1032
- await log(' ⚠️ Failed to upload logs', { verbose: true });
1033
+ // Issue #1212: Always show log upload failures (not just verbose)
1034
+ await log(' ⚠️ Failed to upload logs');
1033
1035
  }
1034
1036
  } catch (uploadError) {
1035
- await log(` ⚠️ Error uploading logs: ${uploadError.message}`, { verbose: true });
1037
+ // Issue #1212: Always show log upload errors (not just verbose)
1038
+ await log(` ⚠️ Error uploading logs: ${uploadError.message}`);
1036
1039
  }
1037
1040
  } else {
1038
1041
  // Fallback: Post simple waiting comment if logs are not attached
@@ -1093,7 +1096,7 @@ try {
1093
1096
 
1094
1097
  // If --attach-logs is enabled, attach failure logs before exiting
1095
1098
  // Note: sessionId is not required - logs should be uploaded even if agent failed before establishing a session
1096
- // Issue #1462: Fall back to uploading logs to the issue if PR is not available
1099
+ // Issues #1212, #1462: Fall back to uploading logs to the issue if PR is not available
1097
1100
  const hasPR = global.createdPR && global.createdPR.number;
1098
1101
  const hasIssue = global.issueNumber;
1099
1102
  const logTargetType = hasPR ? 'pr' : hasIssue ? 'issue' : null;
@@ -1133,10 +1136,12 @@ try {
1133
1136
  if (logUploadSuccess) {
1134
1137
  await log(` ✅ Failure logs uploaded to ${logTargetLabel} successfully`);
1135
1138
  } else {
1136
- await log(' ⚠️ Failed to upload logs', { verbose: true });
1139
+ // Issue #1212: Always show log upload failures (not just verbose)
1140
+ await log(' ⚠️ Failed to upload failure logs');
1137
1141
  }
1138
1142
  } catch (uploadError) {
1139
- await log(` ⚠️ Error uploading logs: ${uploadError.message}`, { verbose: true });
1143
+ // Issue #1212: Always show log upload errors (not just verbose)
1144
+ await log(` ⚠️ Error uploading failure logs: ${uploadError.message}`);
1140
1145
  }
1141
1146
  }
1142
1147
 
@@ -901,6 +901,11 @@ Thank you!`;
901
901
  export const classifyCloneError = errorOutput => {
902
902
  const output = errorOutput.toLowerCase();
903
903
 
904
+ // Issue #1211: ENOSPC (disk full) errors - NOT retryable, requires user action
905
+ if (lib.isENOSPC(errorOutput) || output.includes('no space left on device') || (output.includes('unable to write file') && output.includes('error')) || output.includes('errno -28')) {
906
+ return { type: 'ENOSPC', retryable: false, description: 'No space left on device' };
907
+ }
908
+
904
909
  // Transient server errors (5xx) - typically retryable
905
910
  if (output.includes('error: 500') || output.includes('internal server error') || output.includes('error: 502') || output.includes('error: 503') || output.includes('error: 504')) {
906
911
  return { type: 'TRANSIENT', retryable: true, description: 'GitHub server error' };
@@ -983,36 +988,34 @@ export const cloneRepository = async (repoToClone, tempDir, argv, owner, repo) =
983
988
  if (line.trim()) await log(` ${line}`);
984
989
  }
985
990
  await log('');
986
- await log(' 💡 Common causes:');
987
- await log(" • Repository doesn't exist or is private");
988
- await log(' • No GitHub authentication');
989
- await log(' Network connectivity issues');
990
- if (errorClassification.type === 'TRANSIENT') {
991
- await log(' GitHub server issues (temporary)');
992
- }
993
- if (errorClassification.type === 'RATE_LIMIT') {
994
- await log(' API rate limiting exceeded');
995
- }
996
- if (argv.fork) {
997
- await log(' Fork not ready yet (try again in a moment)');
998
- }
999
- await log('');
1000
- await log(' 🔧 How to fix:');
1001
- await log(' 1. Check authentication: gh auth status');
1002
- await log(' 2. Login if needed: gh auth login');
1003
- await log(` 3. Verify access: gh repo view ${owner}/${repo}`);
1004
- if (argv.fork) {
1005
- await log(` 4. Check fork: gh repo view ${repoToClone}`);
1006
- }
1007
- if (errorClassification.type === 'TRANSIENT') {
1008
- await log(' 5. Wait a few minutes and retry (GitHub server issue)');
1009
- await log(' 6. Check GitHub status: https://www.githubstatus.com');
1010
- }
1011
- if (errorClassification.type === 'RATE_LIMIT') {
1012
- await log(' 5. Wait for rate limit to reset (check your quota)');
1013
- await log(' 6. Use --token flag with different token if available');
991
+
992
+ // Issue #1211: ENOSPC-specific guidance
993
+ if (errorClassification.type === 'ENOSPC') {
994
+ await log(' 💡 Cause: Disk is full — not enough space to clone the repository');
995
+ await log('');
996
+ await log(' 🔧 How to fix:');
997
+ await log(' 1. Free disk space: sudo rm -rf /tmp/* /var/tmp/*');
998
+ await log(' 2. Check disk usage: df -h');
999
+ await log(' 3. Clean Docker/npm: docker system prune -af && npm cache clean --force');
1000
+ await log('');
1001
+ } else {
1002
+ await log(' 💡 Common causes:');
1003
+ await log(" • Repository doesn't exist or is private");
1004
+ await log(' • No GitHub authentication');
1005
+ await log(' Network connectivity issues');
1006
+ if (errorClassification.type === 'TRANSIENT') await log(' GitHub server issues (temporary)');
1007
+ if (errorClassification.type === 'RATE_LIMIT') await log(' API rate limiting exceeded');
1008
+ if (argv.fork) await log(' Fork not ready yet (try again in a moment)');
1009
+ await log('');
1010
+ await log(' 🔧 How to fix:');
1011
+ await log(' 1. Check authentication: gh auth status');
1012
+ await log(' 2. Login if needed: gh auth login');
1013
+ await log(` 3. Verify access: gh repo view ${owner}/${repo}`);
1014
+ if (argv.fork) await log(` 4. Check fork: gh repo view ${repoToClone}`);
1015
+ if (errorClassification.type === 'TRANSIENT') await log(' 5. Wait and retry / check: https://www.githubstatus.com');
1016
+ if (errorClassification.type === 'RATE_LIMIT') await log(' 5. Wait for rate limit to reset or use --token with different token');
1017
+ await log('');
1014
1018
  }
1015
- await log('');
1016
1019
  await safeExit(1, 'Repository setup failed');
1017
1020
  }
1018
1021
 
@@ -15,7 +15,7 @@
15
15
  */
16
16
 
17
17
  // Import shared utility from lib.mjs
18
- import { maskToken, log } from './lib.mjs';
18
+ import { maskToken, log, isENOSPC } from './lib.mjs';
19
19
  import { reportError } from './sentry.lib.mjs';
20
20
 
21
21
  // Dynamic imports for runtime dependencies
@@ -537,11 +537,18 @@ export const sanitizeLogContent = async (logContent, options = {}) => {
537
537
  }
538
538
  }
539
539
  } catch (error) {
540
+ // Issue #1212: Detect ENOSPC specifically and log at non-verbose level
541
+ const isNoSpace = isENOSPC(error);
540
542
  reportError(error, {
541
543
  context: 'sanitize_log_content',
542
- level: 'warning',
544
+ level: isNoSpace ? 'error' : 'warning',
543
545
  });
544
- await log(` ⚠️ Warning: Could not fully sanitize log content: ${error.message}`, { verbose: true });
546
+ if (isNoSpace) {
547
+ await log(` ❌ ENOSPC: No space left on device during log sanitization. Skipping sanitization.`);
548
+ await log(` Consider freeing disk space (e.g., rm -rf ~/.claude/debug/*.txt) and retrying.`);
549
+ } else {
550
+ await log(` ⚠️ Warning: Could not fully sanitize log content: ${error.message}`, { verbose: true });
551
+ }
545
552
  }
546
553
 
547
554
  return sanitized;