@link-assistant/hive-mind 1.6.2 ā 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -0
- package/package.json +1 -1
- package/src/claude.lib.mjs +16 -160
- package/src/claude.runtime-switch.lib.mjs +175 -0
- package/src/solve.auto-continue.lib.mjs +19 -2
- package/src/solve.config.lib.mjs +5 -0
- package/src/solve.mjs +3 -1
- package/src/solve.repo-setup.lib.mjs +14 -5
- package/src/solve.repository.lib.mjs +47 -3
- package/src/solve.results.lib.mjs +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 5794e2f: Add `--working-directory` / `-d` option for proper session resume
|
|
8
|
+
|
|
9
|
+
Claude Code stores sessions per-directory path, so resuming a session in a different directory fails. This change:
|
|
10
|
+
1. Adds `--working-directory` / `-d` option to solve.mjs
|
|
11
|
+
- If directory exists with git repo, uses it without cloning
|
|
12
|
+
- If directory exists but empty, clones into it
|
|
13
|
+
- If directory doesn't exist, creates it and clones
|
|
14
|
+
2. Updates `--auto-resume-on-limit-reset` to pass `--working-directory`
|
|
15
|
+
- When limit resets and session auto-resumes, it uses the same directory as the original session
|
|
16
|
+
- This ensures Claude Code can find and resume the session
|
|
17
|
+
3. Improves resume error messaging
|
|
18
|
+
- Warns when resuming without --working-directory
|
|
19
|
+
- Explains that Claude Code sessions are tied to directory paths
|
|
20
|
+
|
|
21
|
+
Example usage:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
./solve.mjs "<url>" --resume <session-id> --working-directory /tmp/gh-issue-solver-123
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 1.6.3
|
|
28
|
+
|
|
29
|
+
### Patch Changes
|
|
30
|
+
|
|
31
|
+
- Fix Anthropic cost extraction from JSON stream when session has error_during_execution
|
|
32
|
+
- Added anthropicTotalCostUSD to all failure return paths in executeClaudeCommand
|
|
33
|
+
- Changed cost capture logic to only extract from `subtype === 'success'` results
|
|
34
|
+
- This is explicit and reliable - error_during_execution results have zero cost
|
|
35
|
+
- Added case study documentation for issue #1104
|
|
36
|
+
|
|
37
|
+
Fixes #1104
|
|
38
|
+
|
|
39
|
+
Synchronize line count checks in CI/CD
|
|
40
|
+
- Add ESLint max-lines rule (1500 lines) to match CI workflow check
|
|
41
|
+
- Extract handleClaudeRuntimeSwitch to claude.runtime-switch.lib.mjs
|
|
42
|
+
- Reduce claude.lib.mjs from 1506 to 1354 lines
|
|
43
|
+
- Add case study documentation for issue #1141
|
|
44
|
+
|
|
45
|
+
Fixes #1141
|
|
46
|
+
|
|
3
47
|
## 1.6.2
|
|
4
48
|
|
|
5
49
|
### Patch Changes
|
package/package.json
CHANGED
package/src/claude.lib.mjs
CHANGED
|
@@ -8,7 +8,7 @@ const { $ } = await use('command-stream');
|
|
|
8
8
|
const fs = (await use('fs')).promises;
|
|
9
9
|
const path = (await use('path')).default;
|
|
10
10
|
// Import log from general lib
|
|
11
|
-
import { log
|
|
11
|
+
import { log } from './lib.mjs';
|
|
12
12
|
import { reportError } from './sentry.lib.mjs';
|
|
13
13
|
import { timeouts, retryLimits, claudeCode, getClaudeEnv } from './config.lib.mjs';
|
|
14
14
|
import { detectUsageLimit, formatUsageLimitMessage } from './usage-limit.lib.mjs';
|
|
@@ -16,6 +16,8 @@ import { createInteractiveHandler } from './interactive-mode.lib.mjs';
|
|
|
16
16
|
import { displayBudgetStats } from './claude.budget-stats.lib.mjs';
|
|
17
17
|
// Import Claude command builder for generating resume commands
|
|
18
18
|
import { buildClaudeResumeCommand } from './claude.command-builder.lib.mjs';
|
|
19
|
+
// Import runtime switch module (extracted to maintain file line limits, see issue #1141)
|
|
20
|
+
import { handleClaudeRuntimeSwitch } from './claude.runtime-switch.lib.mjs';
|
|
19
21
|
|
|
20
22
|
// Helper to display resume command at end of session
|
|
21
23
|
const showResumeCommand = async (sessionId, tempDir, claudePath, model, log) => {
|
|
@@ -214,163 +216,9 @@ export const validateClaudeConnection = async (model = 'haiku-3') => {
|
|
|
214
216
|
// Start the validation with retry logic
|
|
215
217
|
return await attemptValidation();
|
|
216
218
|
};
|
|
217
|
-
//
|
|
218
|
-
export
|
|
219
|
-
|
|
220
|
-
await log('\nš§ Switching Claude runtime to bun...');
|
|
221
|
-
try {
|
|
222
|
-
try {
|
|
223
|
-
await $`which bun`;
|
|
224
|
-
await log(' ā
Bun runtime found');
|
|
225
|
-
} catch (bunError) {
|
|
226
|
-
reportError(bunError, {
|
|
227
|
-
context: 'claude.lib.mjs - bun availability check',
|
|
228
|
-
level: 'error',
|
|
229
|
-
});
|
|
230
|
-
await log('ā Bun runtime not found. Please install bun first: https://bun.sh/', { level: 'error' });
|
|
231
|
-
process.exit(1);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Find Claude executable path
|
|
235
|
-
const claudePathResult = await $`which claude`;
|
|
236
|
-
const claudePath = claudePathResult.stdout.toString().trim();
|
|
237
|
-
|
|
238
|
-
if (!claudePath) {
|
|
239
|
-
await log('ā Claude executable not found', { level: 'error' });
|
|
240
|
-
process.exit(1);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
await log(` Claude path: ${claudePath}`);
|
|
244
|
-
|
|
245
|
-
try {
|
|
246
|
-
await fs.access(claudePath, fs.constants.W_OK);
|
|
247
|
-
} catch (accessError) {
|
|
248
|
-
reportError(accessError, {
|
|
249
|
-
context: 'claude.lib.mjs - Claude executable write permission check (bun)',
|
|
250
|
-
level: 'error',
|
|
251
|
-
});
|
|
252
|
-
await log('ā Cannot write to Claude executable (permission denied)', { level: 'error' });
|
|
253
|
-
await log(' Try running with sudo or changing file permissions', { level: 'error' });
|
|
254
|
-
process.exit(1);
|
|
255
|
-
}
|
|
256
|
-
// Read current shebang
|
|
257
|
-
const firstLine = await $`head -1 "${claudePath}"`;
|
|
258
|
-
const currentShebang = firstLine.stdout.toString().trim();
|
|
259
|
-
await log(` Current shebang: ${currentShebang}`);
|
|
260
|
-
if (currentShebang.includes('bun')) {
|
|
261
|
-
await log(' ā
Claude is already configured to use bun');
|
|
262
|
-
process.exit(0);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Create backup
|
|
266
|
-
const backupPath = `${claudePath}.nodejs-backup`;
|
|
267
|
-
await $`cp "${claudePath}" "${backupPath}"`;
|
|
268
|
-
await log(` š¦ Backup created: ${backupPath}`);
|
|
269
|
-
|
|
270
|
-
// Read file content and replace shebang
|
|
271
|
-
const content = await fs.readFile(claudePath, 'utf8');
|
|
272
|
-
const newContent = content.replace(/^#!.*node.*$/m, '#!/usr/bin/env bun');
|
|
273
|
-
|
|
274
|
-
if (content === newContent) {
|
|
275
|
-
await log('ā ļø No Node.js shebang found to replace', { level: 'warning' });
|
|
276
|
-
await log(` Current shebang: ${currentShebang}`, { level: 'warning' });
|
|
277
|
-
process.exit(0);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
await fs.writeFile(claudePath, newContent);
|
|
281
|
-
await log(' ā
Claude shebang updated to use bun');
|
|
282
|
-
await log(' š Claude will now run with bun runtime');
|
|
283
|
-
} catch (error) {
|
|
284
|
-
await log(`ā Failed to switch Claude to bun: ${cleanErrorMessage(error)}`, { level: 'error' });
|
|
285
|
-
process.exit(1);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// Exit after switching runtime
|
|
289
|
-
process.exit(0);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (argv['force-claude-nodejs-run']) {
|
|
293
|
-
await log('\nš§ Restoring Claude runtime to Node.js...');
|
|
294
|
-
try {
|
|
295
|
-
try {
|
|
296
|
-
await $`which node`;
|
|
297
|
-
await log(' ā
Node.js runtime found');
|
|
298
|
-
} catch (nodeError) {
|
|
299
|
-
reportError(nodeError, {
|
|
300
|
-
context: 'claude.lib.mjs - Node.js availability check',
|
|
301
|
-
level: 'error',
|
|
302
|
-
});
|
|
303
|
-
await log('ā Node.js runtime not found. Please install Node.js first', { level: 'error' });
|
|
304
|
-
process.exit(1);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// Find Claude executable path
|
|
308
|
-
const claudePathResult = await $`which claude`;
|
|
309
|
-
const claudePath = claudePathResult.stdout.toString().trim();
|
|
310
|
-
|
|
311
|
-
if (!claudePath) {
|
|
312
|
-
await log('ā Claude executable not found', { level: 'error' });
|
|
313
|
-
process.exit(1);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
await log(` Claude path: ${claudePath}`);
|
|
317
|
-
|
|
318
|
-
try {
|
|
319
|
-
await fs.access(claudePath, fs.constants.W_OK);
|
|
320
|
-
} catch (accessError) {
|
|
321
|
-
reportError(accessError, {
|
|
322
|
-
context: 'claude.lib.mjs - Claude executable write permission check (nodejs)',
|
|
323
|
-
level: 'error',
|
|
324
|
-
});
|
|
325
|
-
await log('ā Cannot write to Claude executable (permission denied)', { level: 'error' });
|
|
326
|
-
await log(' Try running with sudo or changing file permissions', { level: 'error' });
|
|
327
|
-
process.exit(1);
|
|
328
|
-
}
|
|
329
|
-
// Read current shebang
|
|
330
|
-
const firstLine = await $`head -1 "${claudePath}"`;
|
|
331
|
-
const currentShebang = firstLine.stdout.toString().trim();
|
|
332
|
-
await log(` Current shebang: ${currentShebang}`);
|
|
333
|
-
if (currentShebang.includes('node') && !currentShebang.includes('bun')) {
|
|
334
|
-
await log(' ā
Claude is already configured to use Node.js');
|
|
335
|
-
process.exit(0);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
const backupPath = `${claudePath}.nodejs-backup`;
|
|
339
|
-
try {
|
|
340
|
-
await fs.access(backupPath);
|
|
341
|
-
// Restore from backup
|
|
342
|
-
await $`cp "${backupPath}" "${claudePath}"`;
|
|
343
|
-
await log(` ā
Restored Claude from backup: ${backupPath}`);
|
|
344
|
-
} catch (backupError) {
|
|
345
|
-
reportError(backupError, {
|
|
346
|
-
context: 'claude_restore_backup',
|
|
347
|
-
level: 'info',
|
|
348
|
-
});
|
|
349
|
-
// No backup available, manually update shebang
|
|
350
|
-
await log(' š No backup found, manually updating shebang...');
|
|
351
|
-
const content = await fs.readFile(claudePath, 'utf8');
|
|
352
|
-
const newContent = content.replace(/^#!.*bun.*$/m, '#!/usr/bin/env node');
|
|
353
|
-
|
|
354
|
-
if (content === newContent) {
|
|
355
|
-
await log('ā ļø No bun shebang found to replace', { level: 'warning' });
|
|
356
|
-
await log(` Current shebang: ${currentShebang}`, { level: 'warning' });
|
|
357
|
-
process.exit(0);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
await fs.writeFile(claudePath, newContent);
|
|
361
|
-
await log(' ā
Claude shebang updated to use Node.js');
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
await log(' š Claude will now run with Node.js runtime');
|
|
365
|
-
} catch (error) {
|
|
366
|
-
await log(`ā Failed to restore Claude to Node.js: ${cleanErrorMessage(error)}`, { level: 'error' });
|
|
367
|
-
process.exit(1);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// Exit after restoring runtime
|
|
371
|
-
process.exit(0);
|
|
372
|
-
}
|
|
373
|
-
};
|
|
219
|
+
// handleClaudeRuntimeSwitch is imported from ./claude.runtime-switch.lib.mjs (see issue #1141)
|
|
220
|
+
// Re-export it for backwards compatibility
|
|
221
|
+
export { handleClaudeRuntimeSwitch };
|
|
374
222
|
/**
|
|
375
223
|
* Check if Playwright MCP is available and connected to Claude
|
|
376
224
|
* @returns {Promise<boolean>} True if Playwright MCP is available, false otherwise
|
|
@@ -1038,9 +886,13 @@ export const executeClaudeCommand = async params => {
|
|
|
1038
886
|
// Handle session result type from Claude CLI (emitted when session completes)
|
|
1039
887
|
// Subtypes: "success", "error_during_execution" (work may have been done), etc.
|
|
1040
888
|
if (data.type === 'result') {
|
|
1041
|
-
|
|
889
|
+
// Issue #1104: Only extract cost from subtype 'success' results
|
|
890
|
+
// This is explicit and reliable - error_during_execution results have zero cost
|
|
891
|
+
if (data.subtype === 'success' && data.total_cost_usd !== undefined && data.total_cost_usd !== null) {
|
|
1042
892
|
anthropicTotalCostUSD = data.total_cost_usd;
|
|
1043
|
-
await log(`š° Anthropic official cost captured: $${anthropicTotalCostUSD.toFixed(6)}`, { verbose: true });
|
|
893
|
+
await log(`š° Anthropic official cost captured from success result: $${anthropicTotalCostUSD.toFixed(6)}`, { verbose: true });
|
|
894
|
+
} else if (data.total_cost_usd !== undefined && data.total_cost_usd !== null) {
|
|
895
|
+
await log(`š° Anthropic cost from ${data.subtype || 'unknown'} result ignored: $${data.total_cost_usd.toFixed(6)}`, { verbose: true });
|
|
1044
896
|
}
|
|
1045
897
|
if (data.is_error === true) {
|
|
1046
898
|
lastMessage = data.result || JSON.stringify(data);
|
|
@@ -1170,6 +1022,7 @@ export const executeClaudeCommand = async params => {
|
|
|
1170
1022
|
limitTimezone: null,
|
|
1171
1023
|
messageCount,
|
|
1172
1024
|
toolUseCount,
|
|
1025
|
+
anthropicTotalCostUSD, // Issue #1104: Include cost even on failure
|
|
1173
1026
|
};
|
|
1174
1027
|
}
|
|
1175
1028
|
}
|
|
@@ -1218,6 +1071,7 @@ export const executeClaudeCommand = async params => {
|
|
|
1218
1071
|
messageCount,
|
|
1219
1072
|
toolUseCount,
|
|
1220
1073
|
is503Error: true,
|
|
1074
|
+
anthropicTotalCostUSD, // Issue #1104: Include cost even on failure
|
|
1221
1075
|
};
|
|
1222
1076
|
}
|
|
1223
1077
|
}
|
|
@@ -1296,6 +1150,7 @@ export const executeClaudeCommand = async params => {
|
|
|
1296
1150
|
messageCount,
|
|
1297
1151
|
toolUseCount,
|
|
1298
1152
|
errorDuringExecution,
|
|
1153
|
+
anthropicTotalCostUSD, // Issue #1104: Include cost even on failure
|
|
1299
1154
|
};
|
|
1300
1155
|
}
|
|
1301
1156
|
// Issue #1088: If error_during_execution occurred but command didn't fail,
|
|
@@ -1414,6 +1269,7 @@ export const executeClaudeCommand = async params => {
|
|
|
1414
1269
|
limitTimezone: null,
|
|
1415
1270
|
messageCount,
|
|
1416
1271
|
toolUseCount,
|
|
1272
|
+
anthropicTotalCostUSD, // Issue #1104: Include cost even on failure
|
|
1417
1273
|
};
|
|
1418
1274
|
}
|
|
1419
1275
|
}; // End of executeWithRetry function
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Claude runtime switching module
|
|
3
|
+
// Extracted from claude.lib.mjs to maintain file line limits
|
|
4
|
+
// See: docs/case-studies/issue-1141
|
|
5
|
+
|
|
6
|
+
// If not, fetch it (when running standalone)
|
|
7
|
+
if (typeof globalThis.use === 'undefined') {
|
|
8
|
+
globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
|
|
9
|
+
}
|
|
10
|
+
const { $ } = await use('command-stream');
|
|
11
|
+
const fs = (await use('fs')).promises;
|
|
12
|
+
import { log, cleanErrorMessage } from './lib.mjs';
|
|
13
|
+
import { reportError } from './sentry.lib.mjs';
|
|
14
|
+
|
|
15
|
+
// Function to handle Claude runtime switching between Node.js and Bun
|
|
16
|
+
export const handleClaudeRuntimeSwitch = async argv => {
|
|
17
|
+
if (argv['force-claude-bun-run']) {
|
|
18
|
+
await log('\nš§ Switching Claude runtime to bun...');
|
|
19
|
+
try {
|
|
20
|
+
try {
|
|
21
|
+
await $`which bun`;
|
|
22
|
+
await log(' ā
Bun runtime found');
|
|
23
|
+
} catch (bunError) {
|
|
24
|
+
reportError(bunError, {
|
|
25
|
+
context: 'claude.runtime-switch.lib.mjs - bun availability check',
|
|
26
|
+
level: 'error',
|
|
27
|
+
});
|
|
28
|
+
await log('ā Bun runtime not found. Please install bun first: https://bun.sh/', { level: 'error' });
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Find Claude executable path
|
|
33
|
+
const claudePathResult = await $`which claude`;
|
|
34
|
+
const claudePath = claudePathResult.stdout.toString().trim();
|
|
35
|
+
|
|
36
|
+
if (!claudePath) {
|
|
37
|
+
await log('ā Claude executable not found', { level: 'error' });
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
await log(` Claude path: ${claudePath}`);
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
await fs.access(claudePath, fs.constants.W_OK);
|
|
45
|
+
} catch (accessError) {
|
|
46
|
+
reportError(accessError, {
|
|
47
|
+
context: 'claude.runtime-switch.lib.mjs - Claude executable write permission check (bun)',
|
|
48
|
+
level: 'error',
|
|
49
|
+
});
|
|
50
|
+
await log('ā Cannot write to Claude executable (permission denied)', { level: 'error' });
|
|
51
|
+
await log(' Try running with sudo or changing file permissions', { level: 'error' });
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
// Read current shebang
|
|
55
|
+
const firstLine = await $`head -1 "${claudePath}"`;
|
|
56
|
+
const currentShebang = firstLine.stdout.toString().trim();
|
|
57
|
+
await log(` Current shebang: ${currentShebang}`);
|
|
58
|
+
if (currentShebang.includes('bun')) {
|
|
59
|
+
await log(' ā
Claude is already configured to use bun');
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Create backup
|
|
64
|
+
const backupPath = `${claudePath}.nodejs-backup`;
|
|
65
|
+
await $`cp "${claudePath}" "${backupPath}"`;
|
|
66
|
+
await log(` š¦ Backup created: ${backupPath}`);
|
|
67
|
+
|
|
68
|
+
// Read file content and replace shebang
|
|
69
|
+
const content = await fs.readFile(claudePath, 'utf8');
|
|
70
|
+
const newContent = content.replace(/^#!.*node.*$/m, '#!/usr/bin/env bun');
|
|
71
|
+
|
|
72
|
+
if (content === newContent) {
|
|
73
|
+
await log('ā ļø No Node.js shebang found to replace', { level: 'warning' });
|
|
74
|
+
await log(` Current shebang: ${currentShebang}`, { level: 'warning' });
|
|
75
|
+
process.exit(0);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
await fs.writeFile(claudePath, newContent);
|
|
79
|
+
await log(' ā
Claude shebang updated to use bun');
|
|
80
|
+
await log(' š Claude will now run with bun runtime');
|
|
81
|
+
} catch (error) {
|
|
82
|
+
await log(`ā Failed to switch Claude to bun: ${cleanErrorMessage(error)}`, { level: 'error' });
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Exit after switching runtime
|
|
87
|
+
process.exit(0);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (argv['force-claude-nodejs-run']) {
|
|
91
|
+
await log('\nš§ Restoring Claude runtime to Node.js...');
|
|
92
|
+
try {
|
|
93
|
+
try {
|
|
94
|
+
await $`which node`;
|
|
95
|
+
await log(' ā
Node.js runtime found');
|
|
96
|
+
} catch (nodeError) {
|
|
97
|
+
reportError(nodeError, {
|
|
98
|
+
context: 'claude.runtime-switch.lib.mjs - Node.js availability check',
|
|
99
|
+
level: 'error',
|
|
100
|
+
});
|
|
101
|
+
await log('ā Node.js runtime not found. Please install Node.js first', { level: 'error' });
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Find Claude executable path
|
|
106
|
+
const claudePathResult = await $`which claude`;
|
|
107
|
+
const claudePath = claudePathResult.stdout.toString().trim();
|
|
108
|
+
|
|
109
|
+
if (!claudePath) {
|
|
110
|
+
await log('ā Claude executable not found', { level: 'error' });
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
await log(` Claude path: ${claudePath}`);
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
await fs.access(claudePath, fs.constants.W_OK);
|
|
118
|
+
} catch (accessError) {
|
|
119
|
+
reportError(accessError, {
|
|
120
|
+
context: 'claude.runtime-switch.lib.mjs - Claude executable write permission check (nodejs)',
|
|
121
|
+
level: 'error',
|
|
122
|
+
});
|
|
123
|
+
await log('ā Cannot write to Claude executable (permission denied)', { level: 'error' });
|
|
124
|
+
await log(' Try running with sudo or changing file permissions', { level: 'error' });
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
// Read current shebang
|
|
128
|
+
const firstLine = await $`head -1 "${claudePath}"`;
|
|
129
|
+
const currentShebang = firstLine.stdout.toString().trim();
|
|
130
|
+
await log(` Current shebang: ${currentShebang}`);
|
|
131
|
+
if (currentShebang.includes('node') && !currentShebang.includes('bun')) {
|
|
132
|
+
await log(' ā
Claude is already configured to use Node.js');
|
|
133
|
+
process.exit(0);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const backupPath = `${claudePath}.nodejs-backup`;
|
|
137
|
+
try {
|
|
138
|
+
await fs.access(backupPath);
|
|
139
|
+
// Restore from backup
|
|
140
|
+
await $`cp "${backupPath}" "${claudePath}"`;
|
|
141
|
+
await log(` ā
Restored Claude from backup: ${backupPath}`);
|
|
142
|
+
} catch (backupError) {
|
|
143
|
+
reportError(backupError, {
|
|
144
|
+
context: 'claude_restore_backup',
|
|
145
|
+
level: 'info',
|
|
146
|
+
});
|
|
147
|
+
// No backup available, manually update shebang
|
|
148
|
+
await log(' š No backup found, manually updating shebang...');
|
|
149
|
+
const content = await fs.readFile(claudePath, 'utf8');
|
|
150
|
+
const newContent = content.replace(/^#!.*bun.*$/m, '#!/usr/bin/env node');
|
|
151
|
+
|
|
152
|
+
if (content === newContent) {
|
|
153
|
+
await log('ā ļø No bun shebang found to replace', { level: 'warning' });
|
|
154
|
+
await log(` Current shebang: ${currentShebang}`, { level: 'warning' });
|
|
155
|
+
process.exit(0);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
await fs.writeFile(claudePath, newContent);
|
|
159
|
+
await log(' ā
Claude shebang updated to use Node.js');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
await log(' š Claude will now run with Node.js runtime');
|
|
163
|
+
} catch (error) {
|
|
164
|
+
await log(`ā Failed to restore Claude to Node.js: ${cleanErrorMessage(error)}`, { level: 'error' });
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Exit after restoring runtime
|
|
169
|
+
process.exit(0);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export default {
|
|
174
|
+
handleClaudeRuntimeSwitch,
|
|
175
|
+
};
|
|
@@ -65,7 +65,9 @@ const formatWaitTime = ms => {
|
|
|
65
65
|
};
|
|
66
66
|
|
|
67
67
|
// Auto-continue function that waits until limit resets
|
|
68
|
-
|
|
68
|
+
// tempDir parameter is required for passing --working-directory to the resumed session
|
|
69
|
+
// (Claude Code sessions are stored per-working-directory, so resume must use same directory)
|
|
70
|
+
export const autoContinueWhenLimitResets = async (issueUrl, sessionId, argv, shouldAttachLogs, tempDir = null) => {
|
|
69
71
|
try {
|
|
70
72
|
const resetTime = global.limitResetTime;
|
|
71
73
|
const waitMs = calculateWaitTime(resetTime);
|
|
@@ -115,6 +117,17 @@ export const autoContinueWhenLimitResets = async (issueUrl, sessionId, argv, sho
|
|
|
115
117
|
if (argv.fork) resumeArgs.push('--fork');
|
|
116
118
|
if (shouldAttachLogs) resumeArgs.push('--attach-logs');
|
|
117
119
|
|
|
120
|
+
// CRITICAL: Pass --working-directory to ensure Claude Code session resume works correctly
|
|
121
|
+
// Claude Code stores sessions per working directory, so resume MUST use the same directory
|
|
122
|
+
// Without this, resume creates a NEW temp directory and session is not found
|
|
123
|
+
if (tempDir) {
|
|
124
|
+
resumeArgs.push('--working-directory', tempDir);
|
|
125
|
+
await log(`š Using working directory for session continuity: ${tempDir}`);
|
|
126
|
+
} else {
|
|
127
|
+
await log(`ā ļø Warning: No working directory specified - session resume may fail`);
|
|
128
|
+
await log(` Claude Code sessions are stored per-directory, consider using --working-directory`);
|
|
129
|
+
}
|
|
130
|
+
|
|
118
131
|
await log(`\nš Executing: ${resumeArgs.join(' ')}`);
|
|
119
132
|
|
|
120
133
|
// Execute the resume command
|
|
@@ -136,7 +149,11 @@ export const autoContinueWhenLimitResets = async (issueUrl, sessionId, argv, sho
|
|
|
136
149
|
});
|
|
137
150
|
await log(`\nā Auto-continue failed: ${cleanErrorMessage(error)}`, { level: 'error' });
|
|
138
151
|
await log('\nš Manual resume command:');
|
|
139
|
-
|
|
152
|
+
if (tempDir) {
|
|
153
|
+
await log(`./solve.mjs "${issueUrl}" --resume ${sessionId} --working-directory "${tempDir}"`);
|
|
154
|
+
} else {
|
|
155
|
+
await log(`./solve.mjs "${issueUrl}" --resume ${sessionId}`);
|
|
156
|
+
}
|
|
140
157
|
await safeExit(1, 'Auto-continue failed');
|
|
141
158
|
}
|
|
142
159
|
};
|
package/src/solve.config.lib.mjs
CHANGED
|
@@ -47,6 +47,11 @@ export const createYargsConfig = yargsInstance => {
|
|
|
47
47
|
description: 'Resume from a previous session ID (when limit was reached)',
|
|
48
48
|
alias: 'r',
|
|
49
49
|
})
|
|
50
|
+
.option('working-directory', {
|
|
51
|
+
type: 'string',
|
|
52
|
+
description: 'Use specified working directory instead of creating a new temp directory. If directory does not exist, it will be created and the repository will be cloned. Essential for --resume to work correctly with Claude Code sessions.',
|
|
53
|
+
alias: 'd',
|
|
54
|
+
})
|
|
50
55
|
.option('only-prepare-command', {
|
|
51
56
|
type: 'boolean',
|
|
52
57
|
description: 'Only prepare and print the claude command without executing it',
|
package/src/solve.mjs
CHANGED
|
@@ -513,7 +513,7 @@ if (isPrUrl) {
|
|
|
513
513
|
// Create or find temporary directory for cloning the repository
|
|
514
514
|
// Pass workspace info for --enable-workspaces mode (works with all tools)
|
|
515
515
|
const workspaceInfo = argv.enableWorkspaces ? { owner, repo, issueNumber } : null;
|
|
516
|
-
const { tempDir, workspaceTmpDir } = await setupTempDirectory(argv, workspaceInfo);
|
|
516
|
+
const { tempDir, workspaceTmpDir, needsClone } = await setupTempDirectory(argv, workspaceInfo);
|
|
517
517
|
// Populate cleanup context for signal handlers
|
|
518
518
|
cleanupContext.tempDir = tempDir;
|
|
519
519
|
cleanupContext.argv = argv;
|
|
@@ -521,6 +521,7 @@ cleanupContext.argv = argv;
|
|
|
521
521
|
let limitReached = false;
|
|
522
522
|
try {
|
|
523
523
|
// Set up repository and clone using the new module
|
|
524
|
+
// If --working-directory points to existing repo, needsClone is false and we skip cloning
|
|
524
525
|
const { forkedRepo } = await setupRepositoryAndClone({
|
|
525
526
|
argv,
|
|
526
527
|
owner,
|
|
@@ -532,6 +533,7 @@ try {
|
|
|
532
533
|
log,
|
|
533
534
|
formatAligned,
|
|
534
535
|
$,
|
|
536
|
+
needsClone,
|
|
535
537
|
});
|
|
536
538
|
|
|
537
539
|
// Verify default branch and status using the new module
|
|
@@ -3,14 +3,23 @@
|
|
|
3
3
|
* Handles repository cloning, forking, and remote setup
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
export async function setupRepositoryAndClone({ argv, owner, repo, forkOwner, tempDir, isContinueMode, issueUrl, log,
|
|
6
|
+
export async function setupRepositoryAndClone({ argv, owner, repo, forkOwner, tempDir, isContinueMode, issueUrl, log, $, needsClone = true }) {
|
|
7
7
|
// Set up repository and handle forking
|
|
8
8
|
const { repoToClone, forkedRepo, upstreamRemote, prForkOwner } = await setupRepository(argv, owner, repo, forkOwner, issueUrl);
|
|
9
9
|
|
|
10
|
-
// Clone repository and set up remotes
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
// Clone repository and set up remotes (skip if needsClone is false - directory already has repo)
|
|
11
|
+
if (needsClone) {
|
|
12
|
+
await cloneRepository(repoToClone, tempDir, argv, owner, repo);
|
|
13
|
+
// Set up upstream remote and sync fork if needed
|
|
14
|
+
await setupUpstreamAndSync(tempDir, forkedRepo, upstreamRemote, owner, repo, argv);
|
|
15
|
+
} else {
|
|
16
|
+
await log('ā¹ļø Skipping clone: Using existing repository in working directory');
|
|
17
|
+
// Still need to ensure upstream remote is set up if using fork mode
|
|
18
|
+
if (forkedRepo && upstreamRemote) {
|
|
19
|
+
await setupUpstreamAndSync(tempDir, forkedRepo, upstreamRemote, owner, repo, argv);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
14
23
|
// Set up pr-fork remote if we're continuing someone else's fork PR with --fork flag
|
|
15
24
|
const prForkRemote = await setupPrForkRemote(tempDir, argv, prForkOwner, repo, isContinueMode, owner);
|
|
16
25
|
|
|
@@ -220,16 +220,58 @@ export const buildWorkspacePath = (owner, repo, issueNumber, timestamp) => {
|
|
|
220
220
|
// When --enable-workspaces is used, creates:
|
|
221
221
|
// {workspace}/repository - for the cloned repo
|
|
222
222
|
// {workspace}/tmp - for temp files, logs, downloads
|
|
223
|
+
// When --working-directory is used, uses the specified directory (creates if needed)
|
|
223
224
|
export const setupTempDirectory = async (argv, workspaceInfo = null) => {
|
|
224
225
|
let tempDir;
|
|
225
226
|
let workspaceTmpDir = null;
|
|
226
227
|
let isResuming = argv.resume;
|
|
228
|
+
// needsClone indicates if the repository needs to be cloned into the directory
|
|
229
|
+
// This is true when: new directory is created, or existing directory is empty
|
|
230
|
+
let needsClone = true;
|
|
227
231
|
|
|
228
232
|
// Check if workspace mode should be enabled (works with all tools)
|
|
229
233
|
const useWorkspaces = argv.enableWorkspaces;
|
|
230
234
|
|
|
235
|
+
// Priority 1: --working-directory option takes precedence over all other directory selection
|
|
236
|
+
// This is essential for --resume to work correctly with Claude Code sessions,
|
|
237
|
+
// because Claude Code stores sessions by working directory path, not session ID alone
|
|
238
|
+
if (argv.workingDirectory) {
|
|
239
|
+
tempDir = path.resolve(argv.workingDirectory);
|
|
240
|
+
|
|
241
|
+
// Check if directory exists
|
|
242
|
+
try {
|
|
243
|
+
const stat = await fs.stat(tempDir);
|
|
244
|
+
if (stat.isDirectory()) {
|
|
245
|
+
// Directory exists - check if it contains a git repository
|
|
246
|
+
try {
|
|
247
|
+
await fs.access(path.join(tempDir, '.git'));
|
|
248
|
+
// Git repository exists - no need to clone
|
|
249
|
+
needsClone = false;
|
|
250
|
+
await log(`\n${formatAligned('š', 'Working directory:', tempDir)}`);
|
|
251
|
+
await log(formatAligned('', 'Status:', 'Using existing repository', 2));
|
|
252
|
+
if (isResuming) {
|
|
253
|
+
await log(formatAligned('', 'Session:', `Resuming ${argv.resume}`, 2));
|
|
254
|
+
}
|
|
255
|
+
} catch {
|
|
256
|
+
// No .git directory - directory is empty or doesn't have a repo, will clone
|
|
257
|
+
await log(`\n${formatAligned('š', 'Working directory:', tempDir)}`);
|
|
258
|
+
await log(formatAligned('', 'Status:', 'Directory exists but no repository - will clone', 2));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
} catch {
|
|
262
|
+
// Directory doesn't exist - create it
|
|
263
|
+
await fs.mkdir(tempDir, { recursive: true });
|
|
264
|
+
await log(`\n${formatAligned('š', 'Working directory:', tempDir)}`);
|
|
265
|
+
await log(formatAligned('', 'Status:', 'Created new directory - will clone repository', 2));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return { tempDir, workspaceTmpDir, isResuming, needsClone };
|
|
269
|
+
}
|
|
270
|
+
|
|
231
271
|
if (isResuming) {
|
|
232
|
-
// When resuming
|
|
272
|
+
// When resuming without --working-directory, create a new temp directory
|
|
273
|
+
// WARNING: This will NOT work correctly with Claude Code because the session
|
|
274
|
+
// is stored in a path-specific location. Use --working-directory for proper resume.
|
|
233
275
|
const scriptDir = path.dirname(process.argv[1]);
|
|
234
276
|
const sessionLogPattern = path.join(scriptDir, `${argv.resume}.log`);
|
|
235
277
|
|
|
@@ -241,7 +283,9 @@ export const setupTempDirectory = async (argv, workspaceInfo = null) => {
|
|
|
241
283
|
// For resumed sessions, create new temp directory since old one may be cleaned up
|
|
242
284
|
tempDir = path.join(os.tmpdir(), `gh-issue-solver-resume-${argv.resume}-${Date.now()}`);
|
|
243
285
|
await fs.mkdir(tempDir, { recursive: true });
|
|
244
|
-
await log(
|
|
286
|
+
await log(`ā ļø Creating new temporary directory for resumed session: ${tempDir}`);
|
|
287
|
+
await log(` Note: Claude Code sessions are tied to working directory paths.`);
|
|
288
|
+
await log(` If session resume fails, use --working-directory to specify the original directory.`);
|
|
245
289
|
} catch (err) {
|
|
246
290
|
reportError(err, {
|
|
247
291
|
context: 'resume_session_lookup',
|
|
@@ -280,7 +324,7 @@ export const setupTempDirectory = async (argv, workspaceInfo = null) => {
|
|
|
280
324
|
await log(`\nCreating temporary directory: ${tempDir}`);
|
|
281
325
|
}
|
|
282
326
|
|
|
283
|
-
return { tempDir, workspaceTmpDir, isResuming };
|
|
327
|
+
return { tempDir, workspaceTmpDir, isResuming, needsClone };
|
|
284
328
|
};
|
|
285
329
|
|
|
286
330
|
// Try to initialize an empty repository by creating a simple README.md
|
|
@@ -377,7 +377,9 @@ export const showSessionSummary = async (sessionId, limitReached, argv, issueUrl
|
|
|
377
377
|
|
|
378
378
|
if (argv.autoResumeOnLimitReset && global.limitResetTime) {
|
|
379
379
|
await log(`\nš AUTO-RESUME ON LIMIT RESET ENABLED - Will resume at ${global.limitResetTime}`);
|
|
380
|
-
|
|
380
|
+
// Pass tempDir to ensure resumed session uses the same working directory
|
|
381
|
+
// This is critical for Claude Code session resume to work correctly
|
|
382
|
+
await autoContinueWhenLimitResets(issueUrl, sessionId, argv, shouldAttachLogs, tempDir);
|
|
381
383
|
} else {
|
|
382
384
|
if (global.limitResetTime) {
|
|
383
385
|
await log(`\nā° Limit resets at: ${global.limitResetTime}`);
|