@link-assistant/hive-mind 1.36.1 → 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 +19 -0
- package/README.md +6 -0
- package/package.json +1 -1
- package/src/claude.lib.mjs +22 -22
- package/src/config.lib.mjs +19 -4
- package/src/github.lib.mjs +25 -26
- package/src/hive.config.lib.mjs +1 -1
- package/src/hive.mjs +20 -19
- package/src/lib.mjs +23 -0
- package/src/models/index.mjs +3 -2
- package/src/solve.config.lib.mjs +30 -2
- package/src/solve.error-handlers.lib.mjs +1 -1
- package/src/solve.mjs +23 -9
- package/src/solve.repository.lib.mjs +32 -29
- package/src/token-sanitization.lib.mjs +10 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
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
|
+
|
|
15
|
+
## 1.37.0
|
|
16
|
+
|
|
17
|
+
### Minor Changes
|
|
18
|
+
|
|
19
|
+
- f02c1fc: fix synthetic model appearing in PR comments by filtering internal Claude CLI router entries (Issue #1486)
|
|
20
|
+
- dd87b23: Add opusplan model support and --plan-model option for flexible plan/execution model pairing
|
|
21
|
+
|
|
3
22
|
## 1.36.1
|
|
4
23
|
|
|
5
24
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -859,6 +859,12 @@ ps axjf
|
|
|
859
859
|
pkill -f gh-issue-solver-1773073065743
|
|
860
860
|
```
|
|
861
861
|
|
|
862
|
+
### Kill all headless browsers spawned by ms-playwright
|
|
863
|
+
|
|
864
|
+
```bash
|
|
865
|
+
pkill -f ms-playwright/chromium_headless_shell-1200
|
|
866
|
+
```
|
|
867
|
+
|
|
862
868
|
That can be done, but not recommended as reboot have better effect.
|
|
863
869
|
|
|
864
870
|
## 📄 License
|
package/package.json
CHANGED
package/src/claude.lib.mjs
CHANGED
|
@@ -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';
|
|
@@ -586,13 +586,13 @@ export const calculateSessionTokens = async (sessionId, tempDir) => {
|
|
|
586
586
|
// Read the entire file
|
|
587
587
|
const fileContent = await fs.readFile(sessionFile, 'utf8');
|
|
588
588
|
const lines = fileContent.trim().split('\n');
|
|
589
|
-
// Parse each line and accumulate token counts per model
|
|
590
589
|
for (const line of lines) {
|
|
591
590
|
if (!line.trim()) continue;
|
|
592
591
|
try {
|
|
593
592
|
const entry = JSON.parse(line);
|
|
594
593
|
if (entry.message && entry.message.usage && entry.message.model) {
|
|
595
594
|
const model = entry.message.model;
|
|
595
|
+
if (model.startsWith('<') && model.endsWith('>')) continue; // Issue #1486: skip <synthetic> etc.
|
|
596
596
|
const usage = entry.message.usage;
|
|
597
597
|
// Initialize model entry if it doesn't exist
|
|
598
598
|
if (!modelUsage[model]) {
|
|
@@ -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;
|
|
@@ -842,11 +840,12 @@ export const executeClaudeCommand = async params => {
|
|
|
842
840
|
} else if (argv.interactiveMode) {
|
|
843
841
|
await log('⚠️ Interactive mode: Disabled - missing PR info (owner/repo/prNumber)', { verbose: true });
|
|
844
842
|
}
|
|
845
|
-
// Build claude command with optional resume flag
|
|
846
843
|
let execCommand;
|
|
847
844
|
const mappedModel = mapModelToId(argv.model);
|
|
848
|
-
|
|
849
|
-
|
|
845
|
+
const resolvedPlanModel = argv.planModel ? mapModelToId(argv.planModel) : undefined; // Issue #1223
|
|
846
|
+
const effectiveModel = resolvedPlanModel ? 'opusplan' : mappedModel;
|
|
847
|
+
const resolvedExecutionModel = resolvedPlanModel ? mappedModel : undefined;
|
|
848
|
+
let claudeArgs = `--output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel}`;
|
|
850
849
|
if (argv.resume) {
|
|
851
850
|
await log(`🔄 Resuming from session: ${argv.resume}`);
|
|
852
851
|
claudeArgs = `--resume ${argv.resume} ${claudeArgs}`;
|
|
@@ -862,22 +861,22 @@ export const executeClaudeCommand = async params => {
|
|
|
862
861
|
}
|
|
863
862
|
try {
|
|
864
863
|
const { thinkingBudget: resolvedThinkingBudget, thinkLevel, isNewVersion, maxBudget } = await resolveThinkingSettings(argv, log);
|
|
865
|
-
const claudeEnv = getClaudeEnv({ thinkingBudget: resolvedThinkingBudget, model:
|
|
864
|
+
const claudeEnv = getClaudeEnv({ thinkingBudget: resolvedThinkingBudget, model: effectiveModel, thinkLevel, maxBudget, planModel: resolvedPlanModel, executionModel: resolvedExecutionModel });
|
|
866
865
|
if (argv.verbose) claudeEnv.ANTHROPIC_LOG = 'debug';
|
|
867
|
-
const modelMaxOutputTokens = getMaxOutputTokensForModel(
|
|
866
|
+
const modelMaxOutputTokens = getMaxOutputTokensForModel(effectiveModel);
|
|
868
867
|
if (argv.verbose) {
|
|
869
868
|
await log(`📊 CLAUDE_CODE_MAX_OUTPUT_TOKENS: ${modelMaxOutputTokens}, MCP_TIMEOUT: ${claudeCode.mcpTimeout}ms, MCP_TOOL_TIMEOUT: ${claudeCode.mcpToolTimeout}ms, ANTHROPIC_LOG: debug`, { verbose: true });
|
|
869
|
+
if (resolvedPlanModel) await log(`📊 opusplan: plan=${resolvedPlanModel}, exec=${resolvedExecutionModel}`, { verbose: true });
|
|
870
870
|
if (resolvedThinkingBudget !== undefined) await log(`📊 MAX_THINKING_TOKENS: ${resolvedThinkingBudget}`, { verbose: true });
|
|
871
871
|
if (claudeEnv.CLAUDE_CODE_EFFORT_LEVEL) await log(`📊 CLAUDE_CODE_EFFORT_LEVEL: ${claudeEnv.CLAUDE_CODE_EFFORT_LEVEL}`, { verbose: true });
|
|
872
872
|
if (!isNewVersion && thinkLevel) await log(`📊 Thinking level (via keywords): ${thinkLevel}`, { verbose: true });
|
|
873
873
|
}
|
|
874
|
+
const simpleEscapedSystem = systemPrompt.replace(/"/g, '\\"');
|
|
874
875
|
if (argv.resume) {
|
|
875
876
|
const simpleEscapedPrompt = prompt.replace(/"/g, '\\"');
|
|
876
|
-
|
|
877
|
-
execCommand = $({ cwd: tempDir, mirror: false, env: claudeEnv })`${claudePath} --resume ${argv.resume} --output-format stream-json --verbose --dangerously-skip-permissions --model ${mappedModel} -p "${simpleEscapedPrompt}" --append-system-prompt "${simpleEscapedSystem}"`;
|
|
877
|
+
execCommand = $({ cwd: tempDir, mirror: false, env: claudeEnv })`${claudePath} --resume ${argv.resume} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} -p "${simpleEscapedPrompt}" --append-system-prompt "${simpleEscapedSystem}"`;
|
|
878
878
|
} else {
|
|
879
|
-
|
|
880
|
-
execCommand = $({ cwd: tempDir, stdin: prompt, mirror: false, env: claudeEnv })`${claudePath} --output-format stream-json --verbose --dangerously-skip-permissions --model ${mappedModel} --append-system-prompt "${simpleEscapedSystem}"`;
|
|
879
|
+
execCommand = $({ cwd: tempDir, stdin: prompt, mirror: false, env: claudeEnv })`${claudePath} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} --append-system-prompt "${simpleEscapedSystem}"`;
|
|
881
880
|
}
|
|
882
881
|
await log(`${formatAligned('📋', 'Command details:', '')}`);
|
|
883
882
|
await log(formatAligned('📂', 'Working directory:', tempDir, 2));
|
|
@@ -1026,7 +1025,12 @@ export const executeClaudeCommand = async params => {
|
|
|
1026
1025
|
const subtype = data.subtype || 'unknown';
|
|
1027
1026
|
if (subtype === 'error_during_execution') {
|
|
1028
1027
|
errorDuringExecution = true;
|
|
1029
|
-
|
|
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
|
+
}
|
|
1030
1034
|
} else {
|
|
1031
1035
|
commandFailed = true;
|
|
1032
1036
|
await log(`⚠️ Detected error from Claude CLI (subtype: ${subtype})`, { verbose: true });
|
|
@@ -1038,8 +1042,8 @@ export const executeClaudeCommand = async params => {
|
|
|
1038
1042
|
if (lastMessage.includes('Internal server error') && !lastMessage.includes('Overloaded')) {
|
|
1039
1043
|
isInternalServerError = true;
|
|
1040
1044
|
}
|
|
1041
|
-
// Issue #1353: Detect "Request timed out"
|
|
1042
|
-
if (lastMessage
|
|
1045
|
+
// Issue #1353: Detect "Request timed out" from Claude CLI
|
|
1046
|
+
if (lastMessage.includes('Request timed out')) {
|
|
1043
1047
|
isRequestTimeout = true;
|
|
1044
1048
|
await log('⏱️ Detected request timeout from Claude CLI (will retry with --resume)', { verbose: true });
|
|
1045
1049
|
}
|
|
@@ -1262,7 +1266,6 @@ export const executeClaudeCommand = async params => {
|
|
|
1262
1266
|
sessionId,
|
|
1263
1267
|
resumeCommand: argv.url ? `${process.argv[0]} ${process.argv[1]} --auto-continue ${argv.url}` : null,
|
|
1264
1268
|
});
|
|
1265
|
-
|
|
1266
1269
|
for (const line of messageLines) {
|
|
1267
1270
|
await log(line, { level: 'warning' });
|
|
1268
1271
|
}
|
|
@@ -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,
|
|
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:
|
|
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) {
|
package/src/config.lib.mjs
CHANGED
|
@@ -174,9 +174,10 @@ export const DEFAULT_MAX_THINKING_BUDGET_OPUS_46 = parseIntWithDefault('HIVE_MIN
|
|
|
174
174
|
export const isOpus46OrLater = model => {
|
|
175
175
|
if (!model) return false;
|
|
176
176
|
const normalizedModel = model.toLowerCase();
|
|
177
|
-
// Check for explicit opus-4-6 or later versions
|
|
177
|
+
// Check for explicit opus-4-6 or later versions, or opusplan (Issue #1223)
|
|
178
178
|
// Note: The 'opus' alias now maps to Opus 4.6 (Issue #1433), so we also check for the alias directly
|
|
179
|
-
|
|
179
|
+
// opusplan uses Opus for planning, so it should get Opus-level settings
|
|
180
|
+
return normalizedModel === 'opus' || normalizedModel === 'opusplan' || normalizedModel.includes('opus-4-6') || normalizedModel.includes('opus-4-7') || normalizedModel.includes('opus-5');
|
|
180
181
|
};
|
|
181
182
|
|
|
182
183
|
/**
|
|
@@ -318,6 +319,10 @@ export const supportsThinkingBudget = (version, minVersion = '2.1.12') => {
|
|
|
318
319
|
// Also sets MCP_TIMEOUT and MCP_TOOL_TIMEOUT for MCP tool execution (see issue #1066)
|
|
319
320
|
// Supports model-specific max output tokens for Opus 4.6 (Issue #1221)
|
|
320
321
|
// Sets CLAUDE_CODE_EFFORT_LEVEL for Opus 4.6 models (Issue #1238)
|
|
322
|
+
// Supports planModel/executionModel for opusplan mode (Issue #1223)
|
|
323
|
+
// See: https://code.claude.com/docs/en/model-config
|
|
324
|
+
// ANTHROPIC_DEFAULT_OPUS_MODEL → model used in plan mode (and for 'opus' alias)
|
|
325
|
+
// ANTHROPIC_DEFAULT_SONNET_MODEL → model used in execution mode (and for 'sonnet' alias)
|
|
321
326
|
export const getClaudeEnv = (options = {}) => {
|
|
322
327
|
// Get max output tokens based on model (Issue #1221)
|
|
323
328
|
const maxOutputTokens = options.model ? getMaxOutputTokensForModel(options.model) : claudeCode.maxOutputTokens;
|
|
@@ -327,8 +332,6 @@ export const getClaudeEnv = (options = {}) => {
|
|
|
327
332
|
CLAUDE_CODE_MAX_OUTPUT_TOKENS: String(maxOutputTokens),
|
|
328
333
|
// MCP timeout configurations to prevent tool calls from hanging indefinitely
|
|
329
334
|
// See: https://github.com/link-assistant/hive-mind/issues/1066
|
|
330
|
-
// MCP_TIMEOUT: Timeout for MCP server startup
|
|
331
|
-
// MCP_TOOL_TIMEOUT: Timeout for MCP tool execution (the one that prevents stuck tools)
|
|
332
335
|
MCP_TIMEOUT: String(claudeCode.mcpTimeout),
|
|
333
336
|
MCP_TOOL_TIMEOUT: String(claudeCode.mcpToolTimeout),
|
|
334
337
|
};
|
|
@@ -354,6 +357,17 @@ export const getClaudeEnv = (options = {}) => {
|
|
|
354
357
|
env.CLAUDE_CODE_EFFORT_LEVEL = effortLevel;
|
|
355
358
|
}
|
|
356
359
|
}
|
|
360
|
+
// Set ANTHROPIC_DEFAULT_OPUS_MODEL when planModel is specified (Issue #1223)
|
|
361
|
+
// This tells Claude Code which model to use during plan mode in opusplan
|
|
362
|
+
if (options.planModel) {
|
|
363
|
+
env.ANTHROPIC_DEFAULT_OPUS_MODEL = String(options.planModel);
|
|
364
|
+
}
|
|
365
|
+
// Set ANTHROPIC_DEFAULT_SONNET_MODEL when executionModel is specified (Issue #1223)
|
|
366
|
+
// This tells Claude Code which model to use during execution mode in opusplan
|
|
367
|
+
// Enables combinations like --plan-model opus --model haiku
|
|
368
|
+
if (options.executionModel) {
|
|
369
|
+
env.ANTHROPIC_DEFAULT_SONNET_MODEL = String(options.executionModel);
|
|
370
|
+
}
|
|
357
371
|
|
|
358
372
|
return env;
|
|
359
373
|
};
|
|
@@ -413,6 +427,7 @@ const defaultAvailableModels = `(
|
|
|
413
427
|
opus
|
|
414
428
|
sonnet
|
|
415
429
|
haiku
|
|
430
|
+
opusplan
|
|
416
431
|
)`;
|
|
417
432
|
|
|
418
433
|
export const modelConfig = {
|
package/src/github.lib.mjs
CHANGED
|
@@ -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) {
|
|
@@ -390,9 +392,7 @@ export async function attachLogToGitHub(options) {
|
|
|
390
392
|
if (useLargeFileMode && verbose) {
|
|
391
393
|
await log(` 📁 Large log file (${Math.round(logStats.size / 1024 / 1024)}MB), will use gh-upload-log`, { verbose: true });
|
|
392
394
|
}
|
|
393
|
-
|
|
394
|
-
let totalCostUSD = publicPricingEstimate;
|
|
395
|
-
// Issue #1225: Collect actual model IDs from Claude session JSON output
|
|
395
|
+
let totalCostUSD = publicPricingEstimate; // Issue #1225: token usage + actual model IDs
|
|
396
396
|
let actualModelIds = null;
|
|
397
397
|
if (totalCostUSD === null && sessionId && tempDir && !errorMessage) {
|
|
398
398
|
try {
|
|
@@ -401,23 +401,15 @@ export async function attachLogToGitHub(options) {
|
|
|
401
401
|
if (tokenUsage) {
|
|
402
402
|
if (tokenUsage.totalCostUSD !== null && tokenUsage.totalCostUSD !== undefined) {
|
|
403
403
|
totalCostUSD = tokenUsage.totalCostUSD;
|
|
404
|
-
if (verbose) {
|
|
405
|
-
await log(` 💰 Calculated cost: $${totalCostUSD.toFixed(6)}`, { verbose: true });
|
|
406
|
-
}
|
|
404
|
+
if (verbose) await log(` 💰 Calculated cost: $${totalCostUSD.toFixed(6)}`, { verbose: true });
|
|
407
405
|
}
|
|
408
|
-
// Extract actual model IDs from session data (Issue #1225)
|
|
409
406
|
if (tokenUsage.modelUsage && Object.keys(tokenUsage.modelUsage).length > 0) {
|
|
410
407
|
actualModelIds = Object.keys(tokenUsage.modelUsage);
|
|
411
|
-
if (verbose) {
|
|
412
|
-
await log(` 🤖 Actual models used: ${actualModelIds.join(', ')}`, { verbose: true });
|
|
413
|
-
}
|
|
408
|
+
if (verbose) await log(` 🤖 Actual models used: ${actualModelIds.join(', ')}`, { verbose: true });
|
|
414
409
|
}
|
|
415
410
|
}
|
|
416
411
|
} catch (tokenError) {
|
|
417
|
-
|
|
418
|
-
if (verbose) {
|
|
419
|
-
await log(` ⚠️ Could not calculate token cost: ${tokenError.message}`, { verbose: true });
|
|
420
|
-
}
|
|
412
|
+
if (verbose) await log(` ⚠️ Could not calculate token cost: ${tokenError.message}`, { verbose: true });
|
|
421
413
|
}
|
|
422
414
|
}
|
|
423
415
|
// Issue #1454: Use resultModelUsage from result JSON when it has more models (includes subagent models)
|
|
@@ -433,6 +425,11 @@ export async function attachLogToGitHub(options) {
|
|
|
433
425
|
if (!actualModelIds && pricingInfo?.modelId) {
|
|
434
426
|
actualModelIds = [pricingInfo.modelId];
|
|
435
427
|
}
|
|
428
|
+
// Issue #1486: Filter out internal/synthetic model entries (e.g., "<synthetic>" from Claude CLI's inference router)
|
|
429
|
+
if (actualModelIds) {
|
|
430
|
+
actualModelIds = actualModelIds.filter(id => !(id.startsWith('<') && id.endsWith('>')));
|
|
431
|
+
if (actualModelIds.length === 0) actualModelIds = null;
|
|
432
|
+
}
|
|
436
433
|
// Issue #1225: Fetch model information for comment using actual models from CLI output
|
|
437
434
|
let modelInfoString = '';
|
|
438
435
|
if (requestedModel || tool || actualModelIds) {
|
|
@@ -805,7 +802,9 @@ ${sessionNote}
|
|
|
805
802
|
return await attachRegularComment(options, logComment);
|
|
806
803
|
}
|
|
807
804
|
} catch (uploadError) {
|
|
808
|
-
|
|
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}`);
|
|
809
808
|
return false;
|
|
810
809
|
}
|
|
811
810
|
}
|
package/src/hive.config.lib.mjs
CHANGED
package/src/hive.mjs
CHANGED
|
@@ -152,9 +152,7 @@ if (isDirectExecution) {
|
|
|
152
152
|
// Strategy 2: Fallback to gh api --paginate approach (comprehensive but slower)
|
|
153
153
|
await log(' 📋 Using gh api --paginate approach for comprehensive coverage...', { verbose: true });
|
|
154
154
|
|
|
155
|
-
//
|
|
156
|
-
// This approach uses the GitHub API directly to fetch all repositories without any limits
|
|
157
|
-
// Include isArchived field to filter out archived repositories
|
|
155
|
+
// Get list of ALL repositories using gh api with --paginate (includes isArchived for filtering)
|
|
158
156
|
let repoListCmd;
|
|
159
157
|
if (scope === 'organization') {
|
|
160
158
|
repoListCmd = `gh api orgs/${owner}/repos --paginate --jq '.[] | {name: .name, owner: .owner.login, isArchived: .archived}'`;
|
|
@@ -436,8 +434,6 @@ if (isDirectExecution) {
|
|
|
436
434
|
initializeExitHandler(getAbsoluteLogPath, log);
|
|
437
435
|
installGlobalExitHandlers();
|
|
438
436
|
|
|
439
|
-
// Unhandled error handlers are now managed by exit-handler.lib.mjs
|
|
440
|
-
|
|
441
437
|
// Validate GitHub URL requirement
|
|
442
438
|
if (!githubUrl) {
|
|
443
439
|
await log('❌ GitHub URL is required', { level: 'error' });
|
|
@@ -470,18 +466,28 @@ if (isDirectExecution) {
|
|
|
470
466
|
}
|
|
471
467
|
}
|
|
472
468
|
|
|
473
|
-
//
|
|
474
|
-
|
|
469
|
+
// --plan flag expansion: shortcut for --plan-model opus --worker-model sonnet (Issue #1223)
|
|
470
|
+
if (argv.plan) {
|
|
471
|
+
if (!rawArgs.includes('--plan-model')) argv.planModel = 'opus';
|
|
472
|
+
if (!rawArgs.includes('--model') && !rawArgs.includes('-m') && !rawArgs.includes('--worker-model')) argv.model = 'sonnet';
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Validate model names EARLY (simple string check, always runs)
|
|
475
476
|
const tool = argv.tool || 'claude';
|
|
476
477
|
await validateAndExitOnInvalidModel(argv.model, tool, safeExit);
|
|
478
|
+
if (argv.planModel) {
|
|
479
|
+
if (tool !== 'claude') {
|
|
480
|
+
await log(`❌ --plan-model is only supported with --tool claude (current tool: ${tool})`, { level: 'error' });
|
|
481
|
+
await safeExit(1, '--plan-model requires --tool claude');
|
|
482
|
+
}
|
|
483
|
+
await validateAndExitOnInvalidModel(argv.planModel, tool, safeExit);
|
|
484
|
+
}
|
|
477
485
|
|
|
478
486
|
// Handle -s (--skip-issues-with-prs) and --auto-continue interaction
|
|
479
|
-
// Detect if user explicitly passed --auto-continue or --no-auto-continue
|
|
480
487
|
const hasExplicitAutoContinue = rawArgs.includes('--auto-continue');
|
|
481
488
|
const hasExplicitNoAutoContinue = rawArgs.includes('--no-auto-continue');
|
|
482
489
|
|
|
483
490
|
if (argv.skipIssuesWithPrs) {
|
|
484
|
-
// If user explicitly passed --auto-continue with -s, that's a conflict
|
|
485
491
|
if (hasExplicitAutoContinue) {
|
|
486
492
|
await log('❌ Conflicting options: --skip-issues-with-prs and --auto-continue cannot be used together', {
|
|
487
493
|
level: 'error',
|
|
@@ -492,8 +498,7 @@ if (isDirectExecution) {
|
|
|
492
498
|
await safeExit(1, 'Error occurred');
|
|
493
499
|
}
|
|
494
500
|
|
|
495
|
-
//
|
|
496
|
-
// This is because -s means "skip issues with PRs" which conflicts with auto-continue
|
|
501
|
+
// -s implies disabling auto-continue unless explicitly set
|
|
497
502
|
if (!hasExplicitNoAutoContinue) {
|
|
498
503
|
argv.autoContinue = false;
|
|
499
504
|
}
|
|
@@ -778,10 +783,8 @@ if (isDirectExecution) {
|
|
|
778
783
|
}
|
|
779
784
|
if (argv.skipToolConnectionCheck || argv.toolConnectionCheck === false) args.push('--skip-tool-connection-check');
|
|
780
785
|
if (argv.dryRun) args.push('--dry-run');
|
|
781
|
-
if (argv.autoCleanup) args.push('--auto-cleanup');
|
|
782
|
-
|
|
783
|
-
// Options already handled above or deprecated aliases (skip in generic loop)
|
|
784
|
-
const SKIP_AUTO_FORWARD = new Set(['model', 'base-branch', 'skip-tool-connection-check', 'tool-connection-check', 'skip-tool-check', 'skip-claude-check', 'tool-check', 'dry-run', 'auto-cleanup']);
|
|
786
|
+
if (argv.autoCleanup) args.push('--auto-cleanup');
|
|
787
|
+
const SKIP_AUTO_FORWARD = new Set(['model', 'worker-model', 'base-branch', 'skip-tool-connection-check', 'tool-connection-check', 'skip-tool-check', 'skip-claude-check', 'tool-check', 'dry-run', 'auto-cleanup']);
|
|
785
788
|
|
|
786
789
|
for (const optionName of getSolvePassthroughOptionNames()) {
|
|
787
790
|
if (SKIP_AUTO_FORWARD.has(optionName)) continue;
|
|
@@ -1431,8 +1434,7 @@ if (isDirectExecution) {
|
|
|
1431
1434
|
await safeExit(0, 'Process completed');
|
|
1432
1435
|
}
|
|
1433
1436
|
|
|
1434
|
-
//
|
|
1435
|
-
// validateClaudeConnection is now imported from lib.mjs
|
|
1437
|
+
// validateClaudeConnection is imported from lib.mjs
|
|
1436
1438
|
|
|
1437
1439
|
// Handle graceful shutdown
|
|
1438
1440
|
process.on('SIGINT', () => gracefulShutdown('interrupt'));
|
|
@@ -1484,8 +1486,7 @@ if (isDirectExecution) {
|
|
|
1484
1486
|
await safeExit(1, 'Error occurred');
|
|
1485
1487
|
}
|
|
1486
1488
|
} catch (fatalError) {
|
|
1487
|
-
// Handle
|
|
1488
|
-
// This prevents silent failures when the script hangs or crashes
|
|
1489
|
+
// Handle fatal errors during initialization or execution
|
|
1489
1490
|
console.error('\n❌ Fatal error occurred during hive initialization or execution');
|
|
1490
1491
|
console.error(` ${fatalError.message || fatalError}`);
|
|
1491
1492
|
if (fatalError.stack) {
|
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,
|
package/src/models/index.mjs
CHANGED
|
@@ -30,6 +30,7 @@ export const claudeModels = {
|
|
|
30
30
|
haiku: 'claude-haiku-4-5-20251001', // Haiku 4.5
|
|
31
31
|
'haiku-3-5': 'claude-3-5-haiku-20241022', // Haiku 3.5
|
|
32
32
|
'haiku-3': 'claude-3-haiku-20240307', // Haiku 3
|
|
33
|
+
opusplan: 'opusplan', // Special mode: Opus for planning, Sonnet for execution (Issue #1223)
|
|
33
34
|
// Shorter version aliases (Issue #1221, Issue #1329 - PR comment feedback)
|
|
34
35
|
'sonnet-4-6': 'claude-sonnet-4-6', // Sonnet 4.6 short alias (Issue #1329)
|
|
35
36
|
'opus-4-6': 'claude-opus-4-6', // Opus 4.6 short alias
|
|
@@ -258,7 +259,7 @@ export const isModelCompatibleWithTool = (tool, model) => {
|
|
|
258
259
|
|
|
259
260
|
switch (tool) {
|
|
260
261
|
case 'claude':
|
|
261
|
-
return mappedModel.startsWith('claude-');
|
|
262
|
+
return mappedModel.startsWith('claude-') || mappedModel === 'opusplan';
|
|
262
263
|
case 'agent':
|
|
263
264
|
return mappedModel.includes('/') || Object.keys(agentModels).includes(model);
|
|
264
265
|
case 'opencode':
|
|
@@ -293,7 +294,7 @@ export const getValidModelsForTool = tool => {
|
|
|
293
294
|
// Primary (non-alias, non-deprecated) short names shown in CLI help descriptions
|
|
294
295
|
// These are the recommended model names users should see in --model help text
|
|
295
296
|
export const primaryModelNames = {
|
|
296
|
-
claude: ['opus', 'sonnet', 'haiku'],
|
|
297
|
+
claude: ['opus', 'sonnet', 'haiku', 'opusplan'],
|
|
297
298
|
opencode: ['grok', 'gpt4o'],
|
|
298
299
|
codex: ['gpt5', 'gpt5-codex', 'o3'],
|
|
299
300
|
agent: ['minimax-m2.5-free', 'big-pickle', 'gpt-5-nano', 'glm-5-free', 'deepseek-r1-free'],
|
package/src/solve.config.lib.mjs
CHANGED
|
@@ -284,6 +284,21 @@ export const SOLVE_OPTION_DEFINITIONS = {
|
|
|
284
284
|
choices: ['claude', 'opencode', 'codex', 'agent'],
|
|
285
285
|
default: 'claude',
|
|
286
286
|
},
|
|
287
|
+
plan: {
|
|
288
|
+
type: 'boolean',
|
|
289
|
+
description: 'Enable plan mode: uses opus for planning, sonnet for execution (shortcut for --plan-model opus --worker-model sonnet). Only works with --tool claude.',
|
|
290
|
+
default: false,
|
|
291
|
+
},
|
|
292
|
+
'plan-model': {
|
|
293
|
+
type: 'string',
|
|
294
|
+
description: 'Model to use for plan mode (e.g., opus). When specified, auto-switches to opusplan mode and sets ANTHROPIC_DEFAULT_OPUS_MODEL. Use with --model/--worker-model to set separate plan and execution models (e.g., --plan-model opus --model sonnet). Only works with --tool claude.',
|
|
295
|
+
default: undefined,
|
|
296
|
+
},
|
|
297
|
+
'worker-model': {
|
|
298
|
+
type: 'string',
|
|
299
|
+
description: 'Alias for --model: Model to use for execution/worker mode when --plan-model is specified. When used with --plan-model, sets ANTHROPIC_DEFAULT_SONNET_MODEL for Claude Code opusplan mode.',
|
|
300
|
+
default: undefined,
|
|
301
|
+
},
|
|
287
302
|
'execute-tool-with-bun': {
|
|
288
303
|
type: 'boolean',
|
|
289
304
|
description: 'Execute the AI tool using bunx (experimental, may improve speed and memory usage)',
|
|
@@ -441,7 +456,7 @@ export const createYargsConfig = yargsInstance => {
|
|
|
441
456
|
.option('model', {
|
|
442
457
|
type: 'string',
|
|
443
458
|
description: buildModelOptionDescription(),
|
|
444
|
-
alias: 'm',
|
|
459
|
+
alias: ['m', 'worker-model'],
|
|
445
460
|
default: currentParsedArgs => {
|
|
446
461
|
// Dynamic default based on tool selection (Issue #1473: centralized in models/index.mjs)
|
|
447
462
|
return defaultModels[currentParsedArgs?.tool] || defaultModels.claude;
|
|
@@ -549,7 +564,20 @@ export const parseArguments = async (yargs, hideBin) => {
|
|
|
549
564
|
// Post-processing: Fix model default for opencode and codex tools
|
|
550
565
|
// Yargs doesn't properly handle dynamic defaults based on other arguments,
|
|
551
566
|
// so we need to handle this manually after parsing
|
|
552
|
-
const modelExplicitlyProvided = rawArgs.includes('--model') || rawArgs.includes('-m');
|
|
567
|
+
const modelExplicitlyProvided = rawArgs.includes('--model') || rawArgs.includes('-m') || rawArgs.includes('--worker-model');
|
|
568
|
+
const planModelExplicitlyProvided = rawArgs.includes('--plan-model');
|
|
569
|
+
|
|
570
|
+
// --plan flag expansion (Issue #1223)
|
|
571
|
+
// When --plan is set, it acts as a shortcut for --plan-model opus --worker-model sonnet
|
|
572
|
+
// Explicit --plan-model and --model/--worker-model values take precedence
|
|
573
|
+
if (argv && argv.plan) {
|
|
574
|
+
if (!planModelExplicitlyProvided) {
|
|
575
|
+
argv.planModel = 'opus';
|
|
576
|
+
}
|
|
577
|
+
if (!modelExplicitlyProvided) {
|
|
578
|
+
argv.model = 'sonnet';
|
|
579
|
+
}
|
|
580
|
+
}
|
|
553
581
|
|
|
554
582
|
// Normalize alias flags: legacy --skip-tool-check and --skip-claude-check behave like --skip-tool-connection-check
|
|
555
583
|
if (argv) {
|
|
@@ -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
|
-
//
|
|
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
|
@@ -211,6 +211,15 @@ if (!(await validateContinueOnlyOnFeedback(argv, isPrUrl, isIssueUrl))) {
|
|
|
211
211
|
const tool = argv.tool || 'claude';
|
|
212
212
|
await validateAndExitOnInvalidModel(argv.model, tool, safeExit);
|
|
213
213
|
|
|
214
|
+
// Validate --plan-model if provided (Issue #1223)
|
|
215
|
+
if (argv.planModel) {
|
|
216
|
+
if (tool !== 'claude') {
|
|
217
|
+
await log(`❌ --plan-model is only supported with --tool claude (current tool: ${tool})`, { level: 'error' });
|
|
218
|
+
await safeExit(1, '--plan-model requires --tool claude');
|
|
219
|
+
}
|
|
220
|
+
await validateAndExitOnInvalidModel(argv.planModel, tool, safeExit);
|
|
221
|
+
}
|
|
222
|
+
|
|
214
223
|
// Perform all system checks (skip tool connection check in dry-run or when --skip-tool-connection-check; model validation always runs)
|
|
215
224
|
const skipToolConnectionCheck = argv.dryRun || argv.skipToolConnectionCheck || argv.toolConnectionCheck === false;
|
|
216
225
|
if (!(await performSystemChecks(argv.minDiskSpace || 2048, skipToolConnectionCheck, argv.model, argv))) {
|
|
@@ -499,8 +508,7 @@ if (isPrUrl) {
|
|
|
499
508
|
issueNumber = urlNumber;
|
|
500
509
|
await log(`📝 Issue mode: Working with issue #${issueNumber}`);
|
|
501
510
|
}
|
|
502
|
-
//
|
|
503
|
-
// 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)
|
|
504
512
|
global.issueNumber = issueNumber;
|
|
505
513
|
const workspaceInfo = argv.enableWorkspaces ? { owner, repo, issueNumber } : null;
|
|
506
514
|
const { tempDir, workspaceTmpDir, needsClone } = await setupTempDirectory(argv, workspaceInfo);
|
|
@@ -952,10 +960,12 @@ try {
|
|
|
952
960
|
if (logUploadSuccess) {
|
|
953
961
|
await log(' ✅ Logs uploaded successfully');
|
|
954
962
|
} else {
|
|
955
|
-
|
|
963
|
+
// Issue #1212: Always show log upload failures (not just verbose)
|
|
964
|
+
await log(' ⚠️ Failed to upload logs');
|
|
956
965
|
}
|
|
957
966
|
} catch (uploadError) {
|
|
958
|
-
|
|
967
|
+
// Issue #1212: Always show log upload errors (not just verbose)
|
|
968
|
+
await log(` ⚠️ Error uploading logs: ${uploadError.message}`);
|
|
959
969
|
}
|
|
960
970
|
} else if (prNumber) {
|
|
961
971
|
// Fallback: Post simple failure comment if logs are not attached
|
|
@@ -1020,10 +1030,12 @@ try {
|
|
|
1020
1030
|
if (logUploadSuccess) {
|
|
1021
1031
|
await log(' ✅ Logs uploaded successfully');
|
|
1022
1032
|
} else {
|
|
1023
|
-
|
|
1033
|
+
// Issue #1212: Always show log upload failures (not just verbose)
|
|
1034
|
+
await log(' ⚠️ Failed to upload logs');
|
|
1024
1035
|
}
|
|
1025
1036
|
} catch (uploadError) {
|
|
1026
|
-
|
|
1037
|
+
// Issue #1212: Always show log upload errors (not just verbose)
|
|
1038
|
+
await log(` ⚠️ Error uploading logs: ${uploadError.message}`);
|
|
1027
1039
|
}
|
|
1028
1040
|
} else {
|
|
1029
1041
|
// Fallback: Post simple waiting comment if logs are not attached
|
|
@@ -1084,7 +1096,7 @@ try {
|
|
|
1084
1096
|
|
|
1085
1097
|
// If --attach-logs is enabled, attach failure logs before exiting
|
|
1086
1098
|
// Note: sessionId is not required - logs should be uploaded even if agent failed before establishing a session
|
|
1087
|
-
//
|
|
1099
|
+
// Issues #1212, #1462: Fall back to uploading logs to the issue if PR is not available
|
|
1088
1100
|
const hasPR = global.createdPR && global.createdPR.number;
|
|
1089
1101
|
const hasIssue = global.issueNumber;
|
|
1090
1102
|
const logTargetType = hasPR ? 'pr' : hasIssue ? 'issue' : null;
|
|
@@ -1124,10 +1136,12 @@ try {
|
|
|
1124
1136
|
if (logUploadSuccess) {
|
|
1125
1137
|
await log(` ✅ Failure logs uploaded to ${logTargetLabel} successfully`);
|
|
1126
1138
|
} else {
|
|
1127
|
-
|
|
1139
|
+
// Issue #1212: Always show log upload failures (not just verbose)
|
|
1140
|
+
await log(' ⚠️ Failed to upload failure logs');
|
|
1128
1141
|
}
|
|
1129
1142
|
} catch (uploadError) {
|
|
1130
|
-
|
|
1143
|
+
// Issue #1212: Always show log upload errors (not just verbose)
|
|
1144
|
+
await log(` ⚠️ Error uploading failure logs: ${uploadError.message}`);
|
|
1131
1145
|
}
|
|
1132
1146
|
}
|
|
1133
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
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
await log('
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
await log('
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
await log('
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
await log(
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
await log(
|
|
1009
|
-
await log(
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
await log('
|
|
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
|
-
|
|
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;
|