@link-assistant/hive-mind 1.37.0 → 1.37.2

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,23 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.37.2
4
+
5
+ ### Patch Changes
6
+
7
+ - f07ae29: fix false positive "Ready to merge" by cross-validating CI success status with GitHub Actions workflow runs API and removing unreliable commit-age-based grace period (Issue #1480)
8
+
9
+ ## 1.37.1
10
+
11
+ ### Patch Changes
12
+
13
+ - 8df5a3d: Treat ENOSPC as immediate failure at all stages (issues #1212, #1211)
14
+
15
+ When disk space runs out during any stage — including git clone, execution, and log
16
+ upload — ENOSPC is now treated as a hard failure (not partial success). Added ENOSPC
17
+ detection to git clone error classification so disk-full clone failures are not
18
+ retried. The isENOSPC utility now detects git-specific patterns like "unable to write
19
+ file" and "cannot create directory". Actionable disk cleanup guidance is provided.
20
+
3
21
  ## 1.37.0
4
22
 
5
23
  ### 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.2",
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,
@@ -33,7 +33,7 @@ const { reportError } = sentryLib;
33
33
 
34
34
  // Import GitHub merge functions
35
35
  const githubMergeLib = await import('./github-merge.lib.mjs');
36
- const { checkPRMergeable, checkMergePermissions, mergePullRequest, waitForCI, checkForBillingLimitError, getRepoVisibility, BILLING_LIMIT_ERROR_PATTERN, getDetailedCIStatus, rerunWorkflowRun, getWorkflowRunsForSha, getActiveRepoWorkflows, getCommitDate, checkPreviousPRCommitsHadCI, checkWorkflowsHavePRTriggers } = githubMergeLib;
36
+ const { checkPRMergeable, checkMergePermissions, mergePullRequest, waitForCI, checkForBillingLimitError, getRepoVisibility, BILLING_LIMIT_ERROR_PATTERN, getDetailedCIStatus, rerunWorkflowRun, getWorkflowRunsForSha, getActiveRepoWorkflows, getCommitDate, checkWorkflowsHavePRTriggers } = githubMergeLib;
37
37
 
38
38
  // Import GitHub functions for log attachment
39
39
  const githubLib = await import('./github.lib.mjs');
@@ -187,7 +187,7 @@ const checkForNonBotComments = async (owner, repo, prNumber, issueNumber, lastCh
187
187
  * - billing_limit: Billing/spending limit reached → stop (private) or wait (public)
188
188
  * - no_checks: No CI checks yet (race condition) → wait
189
189
  */
190
- const getMergeBlockers = async (owner, repo, prNumber, verbose = false) => {
190
+ const getMergeBlockers = async (owner, repo, prNumber, verbose = false, checkCount = 1) => {
191
191
  const blockers = [];
192
192
 
193
193
  // Use detailed CI status to distinguish between all possible states
@@ -280,60 +280,54 @@ const getMergeBlockers = async (owner, repo, prNumber, verbose = false) => {
280
280
  return { blockers, ciStatus, noCiConfigured: false, noCiTriggered: true };
281
281
  }
282
282
 
283
- if (commitInfo.ageSeconds !== null && commitInfo.ageSeconds < WORKFLOW_RUN_GRACE_PERIOD_SECONDS) {
284
- // Commit is recent workflow runs may not have appeared in the API yet
285
- if (verbose) {
286
- console.log(`[VERBOSE] /merge: PR #${prNumber} has no workflow runs for SHA ${ciStatus.sha.substring(0, 7)}, but commit is only ${commitInfo.ageSeconds}s old (grace period: ${WORKFLOW_RUN_GRACE_PERIOD_SECONDS}s) — treating as potential race condition`);
287
- }
288
-
289
- if (prTriggers.hasPRTriggers) {
290
- // Workflows have PR/push triggers AND commit is recent almost certainly a race condition
283
+ if (prTriggers.hasPRTriggers) {
284
+ // Issue #1480 (enhanced): Workflows have PR/push triggers but no runs yet.
285
+ // This is almost certainly a race condition — GitHub takes 30-120s to register
286
+ // workflow runs after a push. We MUST wait regardless of commit age, because
287
+ // commit date reflects authoring time, NOT push time.
288
+ //
289
+ // The commit may have been authored hours ago but pushed just now (rebased branches,
290
+ // amended commits, cherry-picks). Using commit age as a proxy for push age caused
291
+ // false positives in Case 1 of Issue #1480.
292
+ //
293
+ // Safety valve: after MAX_NO_RUNS_CHECKS consecutive checks (typically 5 × 60s = 5 min),
294
+ // conclude CI was not triggered. This handles cases like paths-ignore excluding all
295
+ // changed files, conditional workflows that don't match, etc.
296
+ const MAX_NO_RUNS_CHECKS = 5;
297
+ if (checkCount >= MAX_NO_RUNS_CHECKS) {
298
+ // We've waited long enough — CI was genuinely not triggered
291
299
  if (verbose) {
292
- console.log(`[VERBOSE] /merge: Workflow files confirm PR/push triggers exist (${prTriggers.workflows.map(w => w.name).join(', ')}) waiting for workflow runs to appear`);
300
+ console.log(`[VERBOSE] /merge: PR #${prNumber} has no workflow runs after ${checkCount} consecutive checksconcluding CI was not triggered despite PR triggers existing`);
293
301
  }
294
- blockers.push({
295
- type: 'ci_pending',
296
- message: `CI/CD workflow runs have not appeared yet — commit is ${commitInfo.ageSeconds}s old, waiting for GitHub to register workflow runs (grace period: ${WORKFLOW_RUN_GRACE_PERIOD_SECONDS}s)`,
297
- details: prTriggers.workflows.map(w => w.name),
298
- });
299
- } else {
300
- // No PR triggers found in workflow files — but commit is still recent, be safe and wait
301
- if (verbose) {
302
- console.log(`[VERBOSE] /merge: No PR/push triggers found in workflow files, but commit is only ${commitInfo.ageSeconds}s old waiting to be safe`);
303
- }
304
- blockers.push({
305
- type: 'ci_pending',
306
- message: `CI/CD workflow runs have not appeared yet commit is ${commitInfo.ageSeconds}s old, waiting for GitHub to register workflow runs (grace period: ${WORKFLOW_RUN_GRACE_PERIOD_SECONDS}s)`,
307
- details: [],
308
- });
302
+ return { blockers, ciStatus, noCiConfigured: false, noCiTriggered: true };
303
+ }
304
+
305
+ if (verbose) {
306
+ console.log(`[VERBOSE] /merge: PR #${prNumber} has no workflow runs for SHA ${ciStatus.sha.substring(0, 7)}, but workflows have PR/push triggers (${prTriggers.workflows.map(w => w.name).join(', ')}) — waiting for workflow runs to appear (check ${checkCount}/${MAX_NO_RUNS_CHECKS}, commit age: ${commitInfo.ageSeconds ?? 'unknown'}s)`);
307
+ }
308
+ blockers.push({
309
+ type: 'ci_pending',
310
+ message: `CI/CD workflow runs have not appeared yet workflows have PR/push triggers (${prTriggers.workflows.map(w => w.name).join(', ')}), waiting for GitHub to register workflow runs (check ${checkCount}/${MAX_NO_RUNS_CHECKS})`,
311
+ details: prTriggers.workflows.map(w => w.name),
312
+ });
313
+ } else if (commitInfo.ageSeconds !== null && commitInfo.ageSeconds < WORKFLOW_RUN_GRACE_PERIOD_SECONDS) {
314
+ // No PR triggers found in workflow files, but commit is still recent be safe and wait
315
+ if (verbose) {
316
+ console.log(`[VERBOSE] /merge: No PR/push triggers found in workflow files, but commit is only ${commitInfo.ageSeconds}s old — waiting to be safe`);
309
317
  }
318
+ blockers.push({
319
+ type: 'ci_pending',
320
+ message: `CI/CD workflow runs have not appeared yet — commit is ${commitInfo.ageSeconds}s old, waiting for GitHub to register workflow runs (grace period: ${WORKFLOW_RUN_GRACE_PERIOD_SECONDS}s)`,
321
+ details: [],
322
+ });
310
323
  } else {
311
- // Commit is old enough (grace period elapsed) but check additional signals before concluding
312
- // Issue #1480: Layer 3 Check if previous commits in this PR had CI runs.
313
- // If earlier commits had CI, the HEAD commit should also have CI unless conditions changed.
314
- const previousCI = await checkPreviousPRCommitsHadCI(owner, repo, prNumber, ciStatus.sha, verbose);
315
-
316
- if (previousCI.hadPreviousCI && prTriggers.hasPRTriggers) {
317
- // Previous commits had CI AND workflow files have PR triggers — something is wrong,
318
- // this could be a GitHub API glitch or delayed registration beyond the grace period.
319
- // Wait one more cycle to be safe.
320
- if (verbose) {
321
- console.log(`[VERBOSE] /merge: PR #${prNumber} previous commits had CI (${previousCI.previousCommitsWithCI}/${previousCI.totalPreviousCommits}) and workflows have PR triggers, but HEAD has no runs — waiting as safety measure`);
322
- }
323
- blockers.push({
324
- type: 'ci_pending',
325
- message: `CI/CD workflow runs missing for HEAD — previous PR commits had CI (${previousCI.previousCommitsWithCI} of ${previousCI.totalPreviousCommits}), workflows have PR triggers, possible API delay`,
326
- details: prTriggers.workflows.map(w => w.name),
327
- });
328
- } else {
329
- // CI was definitively NOT triggered
330
- // Issue #1442: Fork PRs needing maintainer approval, paths-ignore filtering,
331
- // workflow conditions not matching, etc. all result in zero workflow runs.
332
- if (verbose) {
333
- console.log(`[VERBOSE] /merge: PR #${prNumber} has no CI checks and no workflow runs for SHA ${ciStatus.sha.substring(0, 7)} (commit age: ${commitInfo.ageSeconds ?? 'unknown'}s, grace period: ${WORKFLOW_RUN_GRACE_PERIOD_SECONDS}s elapsed, previous CI: ${previousCI.hadPreviousCI}, PR triggers: ${prTriggers.hasPRTriggers}) — CI was not triggered`);
334
- }
335
- return { blockers, ciStatus, noCiConfigured: false, noCiTriggered: true };
324
+ // No PR triggers AND commit is old enough — CI was definitively NOT triggered
325
+ // Issue #1442: Fork PRs needing maintainer approval, paths-ignore filtering,
326
+ // workflow conditions not matching, etc. all result in zero workflow runs.
327
+ if (verbose) {
328
+ console.log(`[VERBOSE] /merge: PR #${prNumber} has no CI checks and no workflow runs for SHA ${ciStatus.sha.substring(0, 7)} (commit age: ${commitInfo.ageSeconds ?? 'unknown'}s, no PR/push triggers in workflow files) — CI was not triggered`);
336
329
  }
330
+ return { blockers, ciStatus, noCiConfigured: false, noCiTriggered: true };
337
331
  }
338
332
  }
339
333
  } else {
@@ -355,6 +349,60 @@ const getMergeBlockers = async (owner, repo, prNumber, verbose = false) => {
355
349
  details: [],
356
350
  });
357
351
  }
352
+ } else if (ciStatus.status === 'success') {
353
+ // Issue #1480: Cross-validate "success" with workflow runs API.
354
+ // A fast external check (e.g., CodeFactor) can register and pass before the main CI
355
+ // pipeline starts, causing getDetailedCIStatus to return 'success' prematurely.
356
+ // We must verify that all expected workflow runs have actually completed.
357
+ const workflowRuns = await getWorkflowRunsForSha(owner, repo, ciStatus.sha, verbose);
358
+
359
+ if (workflowRuns.length > 0) {
360
+ // Workflow runs exist — check if any are still running
361
+ const incompleteRuns = workflowRuns.filter(r => r.status !== 'completed');
362
+ if (incompleteRuns.length > 0) {
363
+ // Some workflow runs are still in progress — more check-runs may appear
364
+ if (verbose) {
365
+ console.log(`[VERBOSE] /merge: PR #${prNumber} CI status is 'success' (${ciStatus.passedChecks.length} checks passed), but ${incompleteRuns.length} workflow run(s) still in progress — waiting for completion`);
366
+ }
367
+ blockers.push({
368
+ type: 'ci_pending',
369
+ message: `CI checks show success (${ciStatus.passedChecks.length} passed) but ${incompleteRuns.length} workflow run(s) still in progress — waiting for all to complete`,
370
+ details: incompleteRuns.map(r => r.name),
371
+ });
372
+ }
373
+ // All workflow runs completed — the check-runs we see are the final set, trust the 'success' status
374
+ } else {
375
+ // No workflow runs for this SHA — the passed checks are from external services only
376
+ // (e.g., CodeFactor, Codecov). Check if the repo has workflows that should produce runs.
377
+ const repoWorkflows = await getActiveRepoWorkflows(owner, repo, verbose);
378
+ if (repoWorkflows.hasWorkflows) {
379
+ const prTriggers = await checkWorkflowsHavePRTriggers(owner, repo, verbose);
380
+ if (prTriggers.hasPRTriggers) {
381
+ // Repo has workflows with PR triggers but no runs yet — CI hasn't started
382
+ // This is the exact scenario from Case 2 of Issue #1480
383
+ //
384
+ // Safety valve: after MAX_NO_RUNS_CHECKS consecutive checks, trust the external checks
385
+ const MAX_NO_RUNS_CHECKS = 5;
386
+ if (checkCount >= MAX_NO_RUNS_CHECKS) {
387
+ if (verbose) {
388
+ console.log(`[VERBOSE] /merge: PR #${prNumber} CI 'success' with ${ciStatus.passedChecks.length} external checks, no workflow runs after ${checkCount} checks — trusting external checks`);
389
+ }
390
+ // Fall through — trust the success status from external checks
391
+ } else {
392
+ if (verbose) {
393
+ console.log(`[VERBOSE] /merge: PR #${prNumber} CI status is 'success' (${ciStatus.passedChecks.length} external checks), but repo has PR-triggered workflows with 0 workflow runs — likely race condition (check ${checkCount}/${MAX_NO_RUNS_CHECKS})`);
394
+ }
395
+ // Wait for GitHub Actions to register workflow runs
396
+ blockers.push({
397
+ type: 'ci_pending',
398
+ message: `CI shows ${ciStatus.passedChecks.length} passed check(s) from external services, but repo has PR-triggered workflows that haven't started yet — waiting for GitHub Actions to register (check ${checkCount}/${MAX_NO_RUNS_CHECKS})`,
399
+ details: prTriggers.workflows.map(w => w.name),
400
+ });
401
+ }
402
+ }
403
+ }
404
+ // No repo workflows → external checks are the only CI, trust the 'success' status
405
+ }
358
406
  } else if (ciStatus.status === 'pending') {
359
407
  // CI is still running or queued - wait for completion
360
408
  const pendingNames = [...ciStatus.pendingChecks, ...ciStatus.queuedChecks].map(c => c.name);
@@ -507,7 +555,7 @@ export const watchUntilMergeable = async params => {
507
555
 
508
556
  try {
509
557
  // Get merge blockers
510
- const { blockers, noCiConfigured, noCiTriggered, workflowRunConclusions } = await getMergeBlockers(owner, repo, prNumber, argv.verbose);
558
+ const { blockers, noCiConfigured, noCiTriggered, workflowRunConclusions } = await getMergeBlockers(owner, repo, prNumber, argv.verbose, iteration);
511
559
 
512
560
  // Check for new comments from non-bot users
513
561
  const { hasNewComments, comments } = await checkForNonBotComments(owner, repo, prNumber, issueNumber, lastCheckTime, argv.verbose);
@@ -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;