claude-git-hooks 2.18.0 → 2.19.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/CLAUDE.md +12 -8
  3. package/README.md +2 -1
  4. package/bin/claude-hooks +75 -89
  5. package/lib/cli-metadata.js +301 -0
  6. package/lib/commands/analyze-diff.js +12 -10
  7. package/lib/commands/analyze.js +9 -5
  8. package/lib/commands/bump-version.js +66 -43
  9. package/lib/commands/create-pr.js +71 -34
  10. package/lib/commands/debug.js +4 -7
  11. package/lib/commands/generate-changelog.js +11 -4
  12. package/lib/commands/help.js +47 -27
  13. package/lib/commands/helpers.js +66 -43
  14. package/lib/commands/hooks.js +15 -13
  15. package/lib/commands/install.js +546 -39
  16. package/lib/commands/migrate-config.js +8 -11
  17. package/lib/commands/presets.js +6 -13
  18. package/lib/commands/setup-github.js +12 -3
  19. package/lib/commands/telemetry-cmd.js +8 -6
  20. package/lib/commands/update.js +1 -2
  21. package/lib/config.js +36 -31
  22. package/lib/hooks/pre-commit.js +34 -54
  23. package/lib/hooks/prepare-commit-msg.js +39 -58
  24. package/lib/utils/analysis-engine.js +28 -21
  25. package/lib/utils/changelog-generator.js +162 -34
  26. package/lib/utils/claude-client.js +438 -377
  27. package/lib/utils/claude-diagnostics.js +20 -10
  28. package/lib/utils/file-operations.js +51 -79
  29. package/lib/utils/file-utils.js +46 -9
  30. package/lib/utils/git-operations.js +140 -123
  31. package/lib/utils/git-tag-manager.js +24 -23
  32. package/lib/utils/github-api.js +85 -61
  33. package/lib/utils/github-client.js +12 -14
  34. package/lib/utils/installation-diagnostics.js +4 -4
  35. package/lib/utils/interactive-ui.js +29 -17
  36. package/lib/utils/logger.js +4 -1
  37. package/lib/utils/pr-metadata-engine.js +67 -33
  38. package/lib/utils/preset-loader.js +20 -62
  39. package/lib/utils/prompt-builder.js +50 -55
  40. package/lib/utils/resolution-prompt.js +33 -44
  41. package/lib/utils/sanitize.js +20 -19
  42. package/lib/utils/task-id.js +27 -40
  43. package/lib/utils/telemetry.js +29 -17
  44. package/lib/utils/version-manager.js +173 -126
  45. package/lib/utils/which-command.js +23 -12
  46. package/package.json +69 -69
@@ -21,7 +21,12 @@ import path from 'path';
21
21
  import os from 'os';
22
22
  import logger from './logger.js';
23
23
  import config from '../config.js';
24
- import { detectClaudeError, formatClaudeError, ClaudeErrorType, isRecoverableError } from './claude-diagnostics.js';
24
+ import {
25
+ detectClaudeError,
26
+ formatClaudeError,
27
+ ClaudeErrorType,
28
+ isRecoverableError
29
+ } from './claude-diagnostics.js';
25
30
  import { which } from './which-command.js';
26
31
  import { recordJsonParseFailure, recordBatchSuccess, rotateTelemetry } from './telemetry.js';
27
32
 
@@ -79,7 +84,9 @@ const getClaudeCommand = () => {
79
84
  // Node 24: Use which() instead of execSync to get absolute path
80
85
  const nativePath = which('claude');
81
86
  if (nativePath) {
82
- logger.debug('claude-client - getClaudeCommand', 'Using native Windows Claude CLI', { path: nativePath });
87
+ logger.debug('claude-client - getClaudeCommand', 'Using native Windows Claude CLI', {
88
+ path: nativePath
89
+ });
83
90
  return { command: nativePath, args: [] };
84
91
  }
85
92
 
@@ -87,15 +94,18 @@ const getClaudeCommand = () => {
87
94
 
88
95
  // Fallback to WSL
89
96
  if (!isWSLAvailable()) {
90
- throw new ClaudeClientError('Claude CLI not found. Install Claude CLI natively on Windows or via WSL', {
91
- context: {
92
- platform: 'Windows',
93
- suggestions: [
94
- 'Native Windows: npm install -g @anthropic-ai/claude-cli',
95
- 'WSL: wsl --install, then install Claude in WSL'
96
- ]
97
+ throw new ClaudeClientError(
98
+ 'Claude CLI not found. Install Claude CLI natively on Windows or via WSL',
99
+ {
100
+ context: {
101
+ platform: 'Windows',
102
+ suggestions: [
103
+ 'Native Windows: npm install -g @anthropic-ai/claude-cli',
104
+ 'WSL: wsl --install, then install Claude in WSL'
105
+ ]
106
+ }
97
107
  }
98
- });
108
+ );
99
109
  }
100
110
 
101
111
  // Check if Claude is available in WSL
@@ -106,8 +116,13 @@ const getClaudeCommand = () => {
106
116
  // Verify Claude exists in WSL
107
117
  // Increased timeout from 5s to 15s to handle system load better
108
118
  const wslCheckTimeout = config.system.wslCheckTimeout || 15000;
109
- execSync(`"${wslPath}" claude --version`, { stdio: 'ignore', timeout: wslCheckTimeout });
110
- logger.debug('claude-client - getClaudeCommand', 'Using WSL Claude CLI', { wslPath });
119
+ execSync(`"${wslPath}" claude --version`, {
120
+ stdio: 'ignore',
121
+ timeout: wslCheckTimeout
122
+ });
123
+ logger.debug('claude-client - getClaudeCommand', 'Using WSL Claude CLI', {
124
+ wslPath
125
+ });
111
126
  return { command: wslPath, args: ['claude'] };
112
127
  } catch (wslError) {
113
128
  // Differentiate error types for accurate user feedback
@@ -116,15 +131,19 @@ const getClaudeCommand = () => {
116
131
 
117
132
  // Timeout: Transient system load issue
118
133
  if (errorMsg.includes('ETIMEDOUT')) {
119
- throw new ClaudeClientError('Timeout connecting to WSL - system under heavy load', {
120
- context: {
121
- platform: 'Windows',
122
- wslPath,
123
- error: 'ETIMEDOUT',
124
- timeoutValue: wslCheckTimeout,
125
- suggestion: 'System is busy. Wait a moment and try again, or skip analysis: git commit --no-verify'
134
+ throw new ClaudeClientError(
135
+ 'Timeout connecting to WSL - system under heavy load',
136
+ {
137
+ context: {
138
+ platform: 'Windows',
139
+ wslPath,
140
+ error: 'ETIMEDOUT',
141
+ timeoutValue: wslCheckTimeout,
142
+ suggestion:
143
+ 'System is busy. Wait a moment and try again, or skip analysis: git commit --no-verify'
144
+ }
126
145
  }
127
- });
146
+ );
128
147
  }
129
148
 
130
149
  // Not found: Claude CLI missing in WSL
@@ -134,7 +153,8 @@ const getClaudeCommand = () => {
134
153
  platform: 'Windows',
135
154
  wslPath,
136
155
  error: 'ENOENT',
137
- suggestion: 'Install Claude in WSL: wsl -e bash -c "npm install -g @anthropic-ai/claude-cli"'
156
+ suggestion:
157
+ 'Install Claude in WSL: wsl -e bash -c "npm install -g @anthropic-ai/claude-cli"'
138
158
  }
139
159
  });
140
160
  }
@@ -145,7 +165,8 @@ const getClaudeCommand = () => {
145
165
  platform: 'Windows',
146
166
  wslPath,
147
167
  error: errorMsg,
148
- suggestion: 'Check WSL is functioning: wsl --version, or skip analysis: git commit --no-verify'
168
+ suggestion:
169
+ 'Check WSL is functioning: wsl --version, or skip analysis: git commit --no-verify'
149
170
  }
150
171
  });
151
172
  }
@@ -170,7 +191,9 @@ const getClaudeCommand = () => {
170
191
  }
171
192
 
172
193
  // Fallback to 'claude' if which fails (will error later if not found)
173
- logger.debug('claude-client - getClaudeCommand', 'which() failed, using fallback', { command: 'claude' });
194
+ logger.debug('claude-client - getClaudeCommand', 'which() failed, using fallback', {
195
+ command: 'claude'
196
+ });
174
197
  return { command: 'claude', args: [] };
175
198
  };
176
199
 
@@ -185,259 +208,277 @@ const getClaudeCommand = () => {
185
208
  * @returns {Promise<string>} Claude's response
186
209
  * @throws {ClaudeClientError} If execution fails or times out
187
210
  */
188
- const executeClaude = (prompt, { timeout = 120000, allowedTools = [] } = {}) => new Promise((resolve, reject) => {
189
- // Get platform-specific command
190
- const { command, args } = getClaudeCommand();
191
-
192
- // Add allowed tools if specified (for MCP tools)
193
- const finalArgs = [...args];
194
- if (allowedTools.length > 0) {
195
- // Format: --allowedTools "mcp__github__create_pull_request,mcp__github__get_file_contents"
196
- finalArgs.push('--allowedTools', allowedTools.join(','));
197
- }
211
+ const executeClaude = (prompt, { timeout = 120000, allowedTools = [] } = {}) =>
212
+ new Promise((resolve, reject) => {
213
+ // Get platform-specific command
214
+ const { command, args } = getClaudeCommand();
215
+
216
+ // Add allowed tools if specified (for MCP tools)
217
+ const finalArgs = [...args];
218
+ if (allowedTools.length > 0) {
219
+ // Format: --allowedTools "mcp__github__create_pull_request,mcp__github__get_file_contents"
220
+ finalArgs.push('--allowedTools', allowedTools.join(','));
221
+ }
198
222
 
199
- // CRITICAL FIX: Windows .cmd/.bat file handling
200
- // Why: spawn() cannot execute .cmd/.bat files directly on Windows (ENOENT error)
201
- // Solution: Wrap with cmd.exe /c when command ends with .cmd or .bat
202
- // Impact: Only affects Windows npm-installed CLI tools, no impact on other platforms
203
- let spawnCommand = command;
204
- let spawnArgs = finalArgs;
205
-
206
- if (isWindows() && (command.endsWith('.cmd') || command.endsWith('.bat'))) {
207
- logger.debug('claude-client - executeClaude', 'Wrapping .cmd/.bat with cmd.exe', {
208
- originalCommand: command,
209
- originalArgs: finalArgs
223
+ // CRITICAL FIX: Windows .cmd/.bat file handling
224
+ // Why: spawn() cannot execute .cmd/.bat files directly on Windows (ENOENT error)
225
+ // Solution: Wrap with cmd.exe /c when command ends with .cmd or .bat
226
+ // Impact: Only affects Windows npm-installed CLI tools, no impact on other platforms
227
+ let spawnCommand = command;
228
+ let spawnArgs = finalArgs;
229
+
230
+ if (isWindows() && (command.endsWith('.cmd') || command.endsWith('.bat'))) {
231
+ logger.debug('claude-client - executeClaude', 'Wrapping .cmd/.bat with cmd.exe', {
232
+ originalCommand: command,
233
+ originalArgs: finalArgs
234
+ });
235
+ spawnCommand = 'cmd.exe';
236
+ spawnArgs = ['/c', command, ...finalArgs];
237
+ }
238
+
239
+ const fullCommand =
240
+ spawnArgs.length > 0 ? `${spawnCommand} ${spawnArgs.join(' ')}` : spawnCommand;
241
+
242
+ logger.debug('claude-client - executeClaude', 'Executing Claude CLI', {
243
+ promptLength: prompt.length,
244
+ timeout,
245
+ command: fullCommand,
246
+ isWindows: isWindows(),
247
+ allowedTools
210
248
  });
211
- spawnCommand = 'cmd.exe';
212
- spawnArgs = ['/c', command, ...finalArgs];
213
- }
214
249
 
215
- const fullCommand = spawnArgs.length > 0 ? `${spawnCommand} ${spawnArgs.join(' ')}` : spawnCommand;
250
+ const startTime = Date.now();
216
251
 
217
- logger.debug(
218
- 'claude-client - executeClaude',
219
- 'Executing Claude CLI',
220
- { promptLength: prompt.length, timeout, command: fullCommand, isWindows: isWindows(), allowedTools }
221
- );
252
+ // Why: Use spawn instead of exec to handle large prompts and responses
253
+ // spawn streams data, exec buffers everything in memory
254
+ // Node 24 Fix: Removed shell: true to avoid DEP0190 deprecation warning
255
+ // We now use absolute paths from which(), so shell is not needed
256
+ // Windows .cmd/.bat fix: Wrapped with cmd.exe /c (see above)
257
+ const claude = spawn(spawnCommand, spawnArgs, {
258
+ stdio: ['pipe', 'pipe', 'pipe'] // stdin, stdout, stderr
259
+ });
222
260
 
223
- const startTime = Date.now();
261
+ let stdout = '';
262
+ let stderr = '';
224
263
 
225
- // Why: Use spawn instead of exec to handle large prompts and responses
226
- // spawn streams data, exec buffers everything in memory
227
- // Node 24 Fix: Removed shell: true to avoid DEP0190 deprecation warning
228
- // We now use absolute paths from which(), so shell is not needed
229
- // Windows .cmd/.bat fix: Wrapped with cmd.exe /c (see above)
230
- const claude = spawn(spawnCommand, spawnArgs, {
231
- stdio: ['pipe', 'pipe', 'pipe'] // stdin, stdout, stderr
232
- });
264
+ // Collect stdout
265
+ claude.stdout.on('data', (data) => {
266
+ stdout += data.toString();
267
+ });
233
268
 
234
- let stdout = '';
235
- let stderr = '';
269
+ // Collect stderr
270
+ claude.stderr.on('data', (data) => {
271
+ stderr += data.toString();
272
+ });
236
273
 
237
- // Collect stdout
238
- claude.stdout.on('data', (data) => {
239
- stdout += data.toString();
240
- });
274
+ // Handle process completion
275
+ claude.on('close', (code) => {
276
+ clearTimeout(timeoutId);
277
+ const elapsedTime = Date.now() - startTime;
278
+
279
+ // Check for "Execution error" even when exit code is 0
280
+ // Why: Claude CLI sometimes returns "Execution error" with exit code 0
281
+ // This occurs during API rate limiting or temporary backend issues
282
+ // IMPORTANT: Only check for EXACT match to avoid false positives
283
+ if (stdout.trim() === 'Execution error') {
284
+ const errorInfo = detectClaudeError(stdout, stderr, code);
285
+
286
+ logger.error(
287
+ 'claude-client - executeClaude',
288
+ `Claude CLI returned execution error (exit ${code})`,
289
+ new ClaudeClientError(`Claude CLI error: ${errorInfo.type}`, {
290
+ output: { stdout, stderr },
291
+ context: {
292
+ exitCode: code,
293
+ elapsedTime,
294
+ timeoutValue: timeout,
295
+ errorType: errorInfo.type
296
+ }
297
+ })
298
+ );
241
299
 
242
- // Collect stderr
243
- claude.stderr.on('data', (data) => {
244
- stderr += data.toString();
245
- });
300
+ // Merge timing info into errorInfo for formatting
301
+ const errorInfoWithTiming = {
302
+ ...errorInfo,
303
+ elapsedTime,
304
+ timeoutValue: timeout
305
+ };
306
+
307
+ // Show formatted error to user
308
+ const formattedError = formatClaudeError(errorInfoWithTiming);
309
+ console.error(`\n${formattedError}\n`);
310
+
311
+ reject(
312
+ new ClaudeClientError(`Claude CLI error: ${errorInfo.type}`, {
313
+ output: { stdout, stderr },
314
+ context: {
315
+ exitCode: code,
316
+ elapsedTime,
317
+ timeoutValue: timeout,
318
+ errorInfo
319
+ }
320
+ })
321
+ );
322
+ return;
323
+ }
324
+
325
+ if (code === 0) {
326
+ logger.debug('claude-client - executeClaude', 'Claude CLI execution successful', {
327
+ elapsedTime,
328
+ outputLength: stdout.length
329
+ });
330
+ resolve(stdout);
331
+ } else {
332
+ // Detect specific error type
333
+ const errorInfo = detectClaudeError(stdout, stderr, code);
334
+
335
+ logger.error(
336
+ 'claude-client - executeClaude',
337
+ `Claude CLI failed: ${errorInfo.type}`,
338
+ new ClaudeClientError(`Claude CLI error: ${errorInfo.type}`, {
339
+ output: { stdout, stderr },
340
+ context: {
341
+ exitCode: code,
342
+ elapsedTime,
343
+ timeoutValue: timeout,
344
+ errorType: errorInfo.type
345
+ }
346
+ })
347
+ );
246
348
 
247
- // Handle process completion
248
- claude.on('close', (code) => {
249
- clearTimeout(timeoutId);
250
- const elapsedTime = Date.now() - startTime;
349
+ // Merge timing info into errorInfo for formatting
350
+ const errorInfoWithTiming = {
351
+ ...errorInfo,
352
+ elapsedTime,
353
+ timeoutValue: timeout
354
+ };
251
355
 
252
- // Check for "Execution error" even when exit code is 0
253
- // Why: Claude CLI sometimes returns "Execution error" with exit code 0
254
- // This occurs during API rate limiting or temporary backend issues
255
- // IMPORTANT: Only check for EXACT match to avoid false positives
256
- if (stdout.trim() === 'Execution error') {
257
- const errorInfo = detectClaudeError(stdout, stderr, code);
356
+ // Show formatted error to user
357
+ const formattedError = formatClaudeError(errorInfoWithTiming);
358
+ console.error(`\n${formattedError}\n`);
359
+
360
+ reject(
361
+ new ClaudeClientError(`Claude CLI error: ${errorInfo.type}`, {
362
+ output: { stdout, stderr },
363
+ context: {
364
+ exitCode: code,
365
+ elapsedTime,
366
+ timeoutValue: timeout,
367
+ errorInfo
368
+ }
369
+ })
370
+ );
371
+ }
372
+ });
373
+
374
+ // Handle errors
375
+ claude.on('error', (error) => {
376
+ clearTimeout(timeoutId);
377
+ const elapsedTime = Date.now() - startTime;
258
378
 
259
379
  logger.error(
260
380
  'claude-client - executeClaude',
261
- `Claude CLI returned execution error (exit ${code})`,
262
- new ClaudeClientError(`Claude CLI error: ${errorInfo.type}`, {
263
- output: { stdout, stderr },
381
+ 'Failed to spawn Claude CLI process',
382
+ error
383
+ );
384
+
385
+ reject(
386
+ new ClaudeClientError('Failed to spawn Claude CLI', {
387
+ cause: error,
264
388
  context: {
265
- exitCode: code,
389
+ command,
390
+ args,
266
391
  elapsedTime,
267
- timeoutValue: timeout,
268
- errorType: errorInfo.type
392
+ timeoutValue: timeout
269
393
  }
270
394
  })
271
395
  );
396
+ });
272
397
 
273
- // Merge timing info into errorInfo for formatting
274
- const errorInfoWithTiming = {
275
- ...errorInfo,
276
- elapsedTime,
277
- timeoutValue: timeout
278
- };
279
-
280
- // Show formatted error to user
281
- const formattedError = formatClaudeError(errorInfoWithTiming);
282
- console.error(`\n${ formattedError }\n`);
283
-
284
- reject(new ClaudeClientError(`Claude CLI error: ${errorInfo.type}`, {
285
- output: { stdout, stderr },
286
- context: {
287
- exitCode: code,
288
- elapsedTime,
289
- timeoutValue: timeout,
290
- errorInfo
291
- }
292
- }));
293
- return;
294
- }
398
+ // Set up timeout
399
+ const timeoutId = setTimeout(() => {
400
+ const elapsedTime = Date.now() - startTime;
401
+ claude.kill();
295
402
 
296
- if (code === 0) {
297
- logger.debug(
403
+ logger.error(
298
404
  'claude-client - executeClaude',
299
- 'Claude CLI execution successful',
300
- { elapsedTime, outputLength: stdout.length }
405
+ 'Claude CLI execution timed out',
406
+ new ClaudeClientError('Claude CLI timeout', {
407
+ context: {
408
+ elapsedTime,
409
+ timeoutValue: timeout
410
+ }
411
+ })
301
412
  );
302
- resolve(stdout);
303
- } else {
304
- // Detect specific error type
305
- const errorInfo = detectClaudeError(stdout, stderr, code);
306
413
 
307
- logger.error(
308
- 'claude-client - executeClaude',
309
- `Claude CLI failed: ${errorInfo.type}`,
310
- new ClaudeClientError(`Claude CLI error: ${errorInfo.type}`, {
311
- output: { stdout, stderr },
414
+ reject(
415
+ new ClaudeClientError('Claude CLI execution timed out', {
312
416
  context: {
313
- exitCode: code,
314
417
  elapsedTime,
315
418
  timeoutValue: timeout,
316
- errorType: errorInfo.type
419
+ errorInfo: {
420
+ type: ClaudeErrorType.TIMEOUT,
421
+ elapsedTime,
422
+ timeoutValue: timeout
423
+ }
317
424
  }
318
425
  })
319
426
  );
427
+ }, timeout);
320
428
 
321
- // Merge timing info into errorInfo for formatting
322
- const errorInfoWithTiming = {
323
- ...errorInfo,
324
- elapsedTime,
325
- timeoutValue: timeout
326
- };
327
-
328
- // Show formatted error to user
329
- const formattedError = formatClaudeError(errorInfoWithTiming);
330
- console.error(`\n${ formattedError }\n`);
331
-
332
- reject(new ClaudeClientError(`Claude CLI error: ${errorInfo.type}`, {
333
- output: { stdout, stderr },
334
- context: {
335
- exitCode: code,
336
- elapsedTime,
337
- timeoutValue: timeout,
338
- errorInfo
339
- }
340
- }));
341
- }
342
- });
343
-
344
- // Handle errors
345
- claude.on('error', (error) => {
346
- clearTimeout(timeoutId);
347
- const elapsedTime = Date.now() - startTime;
348
-
349
- logger.error(
350
- 'claude-client - executeClaude',
351
- 'Failed to spawn Claude CLI process',
352
- error
353
- );
354
-
355
- reject(new ClaudeClientError('Failed to spawn Claude CLI', {
356
- cause: error,
357
- context: {
358
- command,
359
- args,
360
- elapsedTime,
361
- timeoutValue: timeout
362
- }
363
- }));
364
- });
365
-
366
- // Set up timeout
367
- const timeoutId = setTimeout(() => {
368
- const elapsedTime = Date.now() - startTime;
369
- claude.kill();
429
+ // Write prompt to stdin
430
+ // Why: Claude CLI reads prompt from stdin, not command arguments
370
431
 
371
- logger.error(
372
- 'claude-client - executeClaude',
373
- 'Claude CLI execution timed out',
374
- new ClaudeClientError('Claude CLI timeout', {
375
- context: {
376
- elapsedTime,
377
- timeoutValue: timeout
378
- }
379
- })
380
- );
381
-
382
- reject(new ClaudeClientError('Claude CLI execution timed out', {
383
- context: {
384
- elapsedTime,
385
- timeoutValue: timeout,
386
- errorInfo: {
387
- type: ClaudeErrorType.TIMEOUT,
388
- elapsedTime,
389
- timeoutValue: timeout
432
+ // Handle stdin errors (e.g., EOF when process terminates unexpectedly)
433
+ // Why: write() failures can emit 'error' events asynchronously
434
+ // Common in parallel execution with large prompts
435
+ claude.stdin.on('error', (error) => {
436
+ clearTimeout(timeoutId);
437
+ logger.error(
438
+ 'claude-client - executeClaude',
439
+ 'stdin stream error (process may have terminated early)',
440
+ {
441
+ error: error.message,
442
+ code: error.code,
443
+ promptLength: prompt.length,
444
+ duration: Date.now() - startTime
390
445
  }
391
- }
392
- }));
393
- }, timeout);
394
-
395
- // Write prompt to stdin
396
- // Why: Claude CLI reads prompt from stdin, not command arguments
397
-
398
- // Handle stdin errors (e.g., EOF when process terminates unexpectedly)
399
- // Why: write() failures can emit 'error' events asynchronously
400
- // Common in parallel execution with large prompts
401
- claude.stdin.on('error', (error) => {
402
- clearTimeout(timeoutId);
403
- logger.error(
404
- 'claude-client - executeClaude',
405
- 'stdin stream error (process may have terminated early)',
406
- {
407
- error: error.message,
408
- code: error.code,
409
- promptLength: prompt.length,
410
- duration: Date.now() - startTime
411
- }
412
- );
446
+ );
413
447
 
414
- reject(new ClaudeClientError('Failed to write to Claude stdin - process terminated unexpectedly', {
415
- cause: error,
416
- context: {
417
- promptLength: prompt.length,
418
- errorCode: error.code,
419
- errorMessage: error.message,
420
- suggestion: 'Try reducing batch size or number of files per commit'
421
- }
422
- }));
423
- });
448
+ reject(
449
+ new ClaudeClientError(
450
+ 'Failed to write to Claude stdin - process terminated unexpectedly',
451
+ {
452
+ cause: error,
453
+ context: {
454
+ promptLength: prompt.length,
455
+ errorCode: error.code,
456
+ errorMessage: error.message,
457
+ suggestion: 'Try reducing batch size or number of files per commit'
458
+ }
459
+ }
460
+ )
461
+ );
462
+ });
424
463
 
425
- try {
426
- claude.stdin.write(prompt);
427
- claude.stdin.end();
428
- } catch (error) {
429
- clearTimeout(timeoutId);
430
- logger.error(
431
- 'claude-client - executeClaude',
432
- 'Failed to write prompt to Claude CLI stdin (synchronous error)',
433
- error
434
- );
464
+ try {
465
+ claude.stdin.write(prompt);
466
+ claude.stdin.end();
467
+ } catch (error) {
468
+ clearTimeout(timeoutId);
469
+ logger.error(
470
+ 'claude-client - executeClaude',
471
+ 'Failed to write prompt to Claude CLI stdin (synchronous error)',
472
+ error
473
+ );
435
474
 
436
- reject(new ClaudeClientError('Failed to write prompt', {
437
- cause: error
438
- }));
439
- }
440
- });
475
+ reject(
476
+ new ClaudeClientError('Failed to write prompt', {
477
+ cause: error
478
+ })
479
+ );
480
+ }
481
+ });
441
482
 
442
483
  /**
443
484
  * Executes Claude CLI fully interactively
@@ -448,85 +489,94 @@ const executeClaude = (prompt, { timeout = 120000, allowedTools = [] } = {}) =>
448
489
  * @returns {Promise<string>} - Returns 'interactive' since we can't capture output
449
490
  * @throws {ClaudeClientError} If execution fails
450
491
  */
451
- const executeClaudeInteractive = (prompt, { timeout = 300000 } = {}) => new Promise((resolve, reject) => {
452
- const { command, args } = getClaudeCommand();
453
- const { spawnSync } = require('child_process');
454
- const fs = require('fs');
455
- const path = require('path');
456
- const os = require('os');
492
+ const executeClaudeInteractive = (prompt, { timeout = 300000 } = {}) =>
493
+ new Promise((resolve, reject) => {
494
+ const { command, args } = getClaudeCommand();
495
+ const { spawnSync } = require('child_process');
496
+ const fs = require('fs');
497
+ const path = require('path');
498
+ const os = require('os');
499
+
500
+ // Save prompt to temp file that Claude can read
501
+ const tempDir = os.tmpdir();
502
+ const tempFile = path.join(tempDir, 'claude-pr-instructions.md');
457
503
 
458
- // Save prompt to temp file that Claude can read
459
- const tempDir = os.tmpdir();
460
- const tempFile = path.join(tempDir, 'claude-pr-instructions.md');
504
+ try {
505
+ fs.writeFileSync(tempFile, prompt);
506
+ } catch (err) {
507
+ logger.error(
508
+ 'claude-client - executeClaudeInteractive',
509
+ 'Failed to write temp file',
510
+ err
511
+ );
512
+ reject(new ClaudeClientError('Failed to write prompt file', { cause: err }));
513
+ return;
514
+ }
461
515
 
462
- try {
463
- fs.writeFileSync(tempFile, prompt);
464
- } catch (err) {
465
- logger.error('claude-client - executeClaudeInteractive', 'Failed to write temp file', err);
466
- reject(new ClaudeClientError('Failed to write prompt file', { cause: err }));
467
- return;
468
- }
516
+ logger.debug(
517
+ 'claude-client - executeClaudeInteractive',
518
+ 'Starting interactive Claude session',
519
+ { promptLength: prompt.length, tempFile, command, args }
520
+ );
469
521
 
470
- logger.debug(
471
- 'claude-client - executeClaudeInteractive',
472
- 'Starting interactive Claude session',
473
- { promptLength: prompt.length, tempFile, command, args }
474
- );
522
+ console.log('');
523
+ console.log('╔══════════════════════════════════════════════════════════════════╗');
524
+ console.log('║ 🤖 INTERACTIVE CLAUDE SESSION ║');
525
+ console.log('╠══════════════════════════════════════════════════════════════════╣');
526
+ console.log('║ ║');
527
+ console.log('║ Instructions saved to: ║');
528
+ console.log(`║ ${tempFile.padEnd(62)}║`);
529
+ console.log('║ ║');
530
+ console.log('║ When Claude starts, tell it: ║');
531
+ console.log('║ "Read and execute the instructions in the file above" ║');
532
+ console.log('║ ║');
533
+ console.log('║ • Type "y" if prompted for MCP permissions ║');
534
+ console.log('║ • Type "/exit" when done ║');
535
+ console.log('║ ║');
536
+ console.log('╚══════════════════════════════════════════════════════════════════╝');
537
+ console.log('');
538
+ console.log('Starting Claude...');
539
+ console.log('');
475
540
 
476
- console.log('');
477
- console.log('╔══════════════════════════════════════════════════════════════════╗');
478
- console.log('║ 🤖 INTERACTIVE CLAUDE SESSION ║');
479
- console.log('╠══════════════════════════════════════════════════════════════════╣');
480
- console.log('║ ║');
481
- console.log('║ Instructions saved to: ║');
482
- console.log(`║ ${tempFile.padEnd(62)}║`);
483
- console.log('║ ║');
484
- console.log('║ When Claude starts, tell it: ║');
485
- console.log('║ "Read and execute the instructions in the file above" ║');
486
- console.log('║ ║');
487
- console.log('║ • Type "y" if prompted for MCP permissions ║');
488
- console.log('║ • Type "/exit" when done ║');
489
- console.log('║ ║');
490
- console.log('╚══════════════════════════════════════════════════════════════════╝');
491
- console.log('');
492
- console.log('Starting Claude...');
493
- console.log('');
494
-
495
- // Run Claude fully interactively (no flags - pure interactive mode)
496
- const result = spawnSync(command, args, {
497
- stdio: 'inherit', // Full terminal access
498
- shell: true,
499
- timeout
500
- });
541
+ // Run Claude fully interactively (no flags - pure interactive mode)
542
+ const result = spawnSync(command, args, {
543
+ stdio: 'inherit', // Full terminal access
544
+ shell: true,
545
+ timeout
546
+ });
501
547
 
502
- // Clean up temp file
503
- try {
504
- fs.unlinkSync(tempFile);
505
- } catch (e) {
506
- logger.debug('claude-client - executeClaudeInteractive', 'Temp file cleanup', { error: e.message });
507
- }
548
+ // Clean up temp file
549
+ try {
550
+ fs.unlinkSync(tempFile);
551
+ } catch (e) {
552
+ logger.debug('claude-client - executeClaudeInteractive', 'Temp file cleanup', {
553
+ error: e.message
554
+ });
555
+ }
508
556
 
509
- if (result.error) {
510
- logger.error('claude-client - executeClaudeInteractive', 'Spawn error', result.error);
511
- reject(new ClaudeClientError('Failed to start Claude', { cause: result.error }));
512
- return;
513
- }
557
+ if (result.error) {
558
+ logger.error('claude-client - executeClaudeInteractive', 'Spawn error', result.error);
559
+ reject(new ClaudeClientError('Failed to start Claude', { cause: result.error }));
560
+ return;
561
+ }
514
562
 
515
- if (result.status === 0 || result.status === null) {
516
- console.log('');
517
- resolve('interactive-session-completed');
518
- } else if (result.signal === 'SIGTERM') {
519
- reject(new ClaudeClientError('Claude session timed out', { context: { timeout } }));
520
- } else {
521
- logger.error('claude-client - executeClaudeInteractive', 'Claude exited with error', {
522
- status: result.status,
523
- signal: result.signal
524
- });
525
- reject(new ClaudeClientError(`Claude exited with code ${result.status}`, {
526
- context: { exitCode: result.status, signal: result.signal }
527
- }));
528
- }
529
- });
563
+ if (result.status === 0 || result.status === null) {
564
+ console.log('');
565
+ resolve('interactive-session-completed');
566
+ } else if (result.signal === 'SIGTERM') {
567
+ reject(new ClaudeClientError('Claude session timed out', { context: { timeout } }));
568
+ } else {
569
+ logger.error('claude-client - executeClaudeInteractive', 'Claude exited with error', {
570
+ status: result.status,
571
+ signal: result.signal
572
+ });
573
+ reject(
574
+ new ClaudeClientError(`Claude exited with code ${result.status}`, {
575
+ context: { exitCode: result.status, signal: result.signal }
576
+ })
577
+ );
578
+ }
579
+ });
530
580
 
531
581
  /**
532
582
  * Extracts JSON from Claude's response
@@ -538,11 +588,9 @@ const executeClaudeInteractive = (prompt, { timeout = 300000 } = {}) => new Prom
538
588
  * @throws {ClaudeClientError} If no valid JSON found
539
589
  */
540
590
  const extractJSON = (response) => {
541
- logger.debug(
542
- 'claude-client - extractJSON',
543
- 'Extracting JSON from response',
544
- { responseLength: response.length }
545
- );
591
+ logger.debug('claude-client - extractJSON', 'Extracting JSON from response', {
592
+ responseLength: response.length
593
+ });
546
594
 
547
595
  // Why: Try multiple patterns to find JSON
548
596
  // Pattern 1: JSON in markdown code block
@@ -590,7 +638,10 @@ const extractJSON = (response) => {
590
638
  const jsonText = jsonLines.join('\n');
591
639
  try {
592
640
  const json = JSON.parse(jsonText);
593
- logger.debug('claude-client - extractJSON', 'JSON extracted using line matching');
641
+ logger.debug(
642
+ 'claude-client - extractJSON',
643
+ 'JSON extracted using line matching'
644
+ );
594
645
  return json;
595
646
  } catch (error) {
596
647
  // Try next occurrence
@@ -651,28 +702,21 @@ const saveDebugResponse = async (prompt, response, filename = config.output.debu
651
702
  // Display batch optimization status
652
703
  try {
653
704
  if (prompt.includes('OPTIMIZATION')) {
654
- console.log(`\n${ '='.repeat(70)}`);
705
+ console.log(`\n${'='.repeat(70)}`);
655
706
  console.log('✅ BATCH OPTIMIZATION ENABLED');
656
707
  console.log('='.repeat(70));
657
708
  console.log('Multi-file analysis organized for efficient processing');
658
709
  console.log('Check debug file for full prompt and response details');
659
- console.log(`${'='.repeat(70) }\n`);
710
+ console.log(`${'='.repeat(70)}\n`);
660
711
  }
661
712
  } catch (parseError) {
662
713
  // Ignore parsing errors, just skip the display
663
714
  }
664
715
 
665
716
  logger.info(`📝 Debug output saved to ${filename}`);
666
- logger.debug(
667
- 'claude-client - saveDebugResponse',
668
- `Debug response saved to ${filename}`
669
- );
717
+ logger.debug('claude-client - saveDebugResponse', `Debug response saved to ${filename}`);
670
718
  } catch (error) {
671
- logger.error(
672
- 'claude-client - saveDebugResponse',
673
- 'Failed to save debug response',
674
- error
675
- );
719
+ logger.error('claude-client - saveDebugResponse', 'Failed to save debug response', error);
676
720
  }
677
721
  };
678
722
 
@@ -690,7 +734,16 @@ const saveDebugResponse = async (prompt, response, filename = config.output.debu
690
734
  * @returns {Promise<any>} Result from fn
691
735
  * @throws {Error} If fn fails after all retries
692
736
  */
693
- const withRetry = async (fn, { maxRetries = 3, baseRetryDelay = 2000, retryCount = 0, operationName = 'operation', telemetryContext = null } = {}) => {
737
+ const withRetry = async (
738
+ fn,
739
+ {
740
+ maxRetries = 3,
741
+ baseRetryDelay = 2000,
742
+ retryCount = 0,
743
+ operationName = 'operation',
744
+ telemetryContext = null
745
+ } = {}
746
+ ) => {
694
747
  const retryDelay = baseRetryDelay * Math.pow(2, retryCount);
695
748
  const startTime = Date.now();
696
749
 
@@ -705,8 +758,12 @@ const withRetry = async (fn, { maxRetries = 3, baseRetryDelay = 2000, retryCount
705
758
  duration,
706
759
  retryAttempt: retryCount,
707
760
  totalRetries: maxRetries
708
- }).catch(err => {
709
- logger.debug('claude-client - withRetry', 'Failed to record success telemetry', err);
761
+ }).catch((err) => {
762
+ logger.debug(
763
+ 'claude-client - withRetry',
764
+ 'Failed to record success telemetry',
765
+ err
766
+ );
710
767
  });
711
768
  }
712
769
 
@@ -725,8 +782,12 @@ const withRetry = async (fn, { maxRetries = 3, baseRetryDelay = 2000, retryCount
725
782
  totalRetries: maxRetries,
726
783
  responseLength: error.context?.response?.length || 0,
727
784
  responsePreview: error.context?.response?.substring(0, 100) || ''
728
- }).catch(err => {
729
- logger.debug('claude-client - withRetry', 'Failed to record failure telemetry', err);
785
+ }).catch((err) => {
786
+ logger.debug(
787
+ 'claude-client - withRetry',
788
+ 'Failed to record failure telemetry',
789
+ err
790
+ );
730
791
  });
731
792
  }
732
793
 
@@ -736,20 +797,16 @@ const withRetry = async (fn, { maxRetries = 3, baseRetryDelay = 2000, retryCount
736
797
  const isRecoverable = hasErrorInfo ? isRecoverableError(error.context.errorInfo) : false;
737
798
  const canRetry = retryCount < maxRetries;
738
799
 
739
- logger.debug(
740
- 'claude-client - withRetry',
741
- `Retry check for ${operationName}`,
742
- {
743
- retryCount,
744
- maxRetries,
745
- hasContext,
746
- hasErrorInfo,
747
- isRecoverable,
748
- canRetry,
749
- errorType: error.context?.errorInfo?.type,
750
- errorName: error.name
751
- }
752
- );
800
+ logger.debug('claude-client - withRetry', `Retry check for ${operationName}`, {
801
+ retryCount,
802
+ maxRetries,
803
+ hasContext,
804
+ hasErrorInfo,
805
+ isRecoverable,
806
+ canRetry,
807
+ errorType: error.context?.errorInfo?.type,
808
+ errorName: error.name
809
+ });
753
810
 
754
811
  const shouldRetry = canRetry && hasContext && hasErrorInfo && isRecoverable;
755
812
 
@@ -760,13 +817,21 @@ const withRetry = async (fn, { maxRetries = 3, baseRetryDelay = 2000, retryCount
760
817
  { errorType: error.context.errorInfo.type }
761
818
  );
762
819
 
763
- console.log(`\n⏳ Retrying in ${retryDelay / 1000} seconds due to ${error.context.errorInfo.type}...\n`);
820
+ console.log(
821
+ `\n⏳ Retrying in ${retryDelay / 1000} seconds due to ${error.context.errorInfo.type}...\n`
822
+ );
764
823
 
765
824
  // Wait before retry
766
- await new Promise(resolve => setTimeout(resolve, retryDelay));
825
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
767
826
 
768
827
  // Retry with incremented count and same telemetry context
769
- return withRetry(fn, { maxRetries, baseRetryDelay, retryCount: retryCount + 1, operationName, telemetryContext });
828
+ return withRetry(fn, {
829
+ maxRetries,
830
+ baseRetryDelay,
831
+ retryCount: retryCount + 1,
832
+ operationName,
833
+ telemetryContext
834
+ });
770
835
  }
771
836
 
772
837
  // Add retry attempt to error context if not already present
@@ -794,13 +859,10 @@ const withRetry = async (fn, { maxRetries = 3, baseRetryDelay = 2000, retryCount
794
859
  const executeClaudeWithRetry = async (prompt, options = {}) => {
795
860
  const { telemetryContext = {}, ...executeOptions } = options;
796
861
 
797
- return withRetry(
798
- () => executeClaude(prompt, executeOptions),
799
- {
800
- operationName: 'executeClaude',
801
- telemetryContext
802
- }
803
- );
862
+ return withRetry(() => executeClaude(prompt, executeOptions), {
863
+ operationName: 'executeClaude',
864
+ telemetryContext
865
+ });
804
866
  };
805
867
 
806
868
  /**
@@ -815,17 +877,20 @@ const executeClaudeWithRetry = async (prompt, options = {}) => {
815
877
  * @returns {Promise<Object>} Parsed analysis result
816
878
  * @throws {ClaudeClientError} If analysis fails
817
879
  */
818
- const analyzeCode = async (prompt, { timeout = 120000, saveDebug = config.system.debug, telemetryContext = {} } = {}) => {
880
+ const analyzeCode = async (
881
+ prompt,
882
+ { timeout = 120000, saveDebug = config.system.debug, telemetryContext = {} } = {}
883
+ ) => {
819
884
  const startTime = Date.now();
820
885
 
821
- logger.debug(
822
- 'claude-client - analyzeCode',
823
- 'Starting code analysis',
824
- { promptLength: prompt.length, timeout, saveDebug }
825
- );
886
+ logger.debug('claude-client - analyzeCode', 'Starting code analysis', {
887
+ promptLength: prompt.length,
888
+ timeout,
889
+ saveDebug
890
+ });
826
891
 
827
892
  // Rotate telemetry files periodically
828
- rotateTelemetry().catch(err => {
893
+ rotateTelemetry().catch((err) => {
829
894
  logger.debug('claude-client - analyzeCode', 'Failed to rotate telemetry', err);
830
895
  });
831
896
 
@@ -845,16 +910,12 @@ const analyzeCode = async (prompt, { timeout = 120000, saveDebug = config.system
845
910
 
846
911
  const duration = Date.now() - startTime;
847
912
 
848
- logger.debug(
849
- 'claude-client - analyzeCode',
850
- 'Analysis complete',
851
- {
852
- hasApproved: 'approved' in result,
853
- hasQualityGate: 'QUALITY_GATE' in result,
854
- blockingIssuesCount: result.blockingIssues?.length ?? 0,
855
- duration
856
- }
857
- );
913
+ logger.debug('claude-client - analyzeCode', 'Analysis complete', {
914
+ hasApproved: 'approved' in result,
915
+ hasQualityGate: 'QUALITY_GATE' in result,
916
+ blockingIssuesCount: result.blockingIssues?.length ?? 0,
917
+ duration
918
+ });
858
919
 
859
920
  // Telemetry is now recorded by withRetry wrapper
860
921
  return result;
@@ -893,7 +954,7 @@ const chunkArray = (array, size) => {
893
954
  const analyzeCodeParallel = async (prompts, options = {}) => {
894
955
  const startTime = Date.now();
895
956
 
896
- console.log(`\n${ '='.repeat(70)}`);
957
+ console.log(`\n${'='.repeat(70)}`);
897
958
  console.log(`🚀 PARALLEL EXECUTION: ${prompts.length} Claude processes`);
898
959
  console.log('='.repeat(70));
899
960
 
@@ -923,7 +984,7 @@ const analyzeCodeParallel = async (prompts, options = {}) => {
923
984
 
924
985
  console.log('='.repeat(70));
925
986
  console.log(`✅ PARALLEL EXECUTION COMPLETE: ${results.length} results in ${duration}s`);
926
- console.log(`${'='.repeat(70) }\n`);
987
+ console.log(`${'='.repeat(70)}\n`);
927
988
 
928
989
  logger.info(`Parallel analysis complete: ${results.length} results in ${duration}s`);
929
990
  return results;