claude-git-hooks 2.6.3 → 2.8.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 +173 -0
- package/README.md +77 -58
- package/bin/claude-hooks +322 -27
- package/lib/config.js +176 -83
- package/lib/hooks/pre-commit.js +11 -14
- package/lib/hooks/prepare-commit-msg.js +2 -3
- package/lib/utils/claude-client.js +199 -34
- package/lib/utils/claude-diagnostics.js +102 -11
- package/lib/utils/prompt-builder.js +2 -2
- package/package.json +1 -1
- package/templates/{CLAUDE_ANALYSIS_PROMPT_SONAR.md → CLAUDE_ANALYSIS_PROMPT.md} +2 -1
- package/templates/{CLAUDE_PRE_COMMIT_SONAR.md → CLAUDE_PRE_COMMIT.md} +10 -1
- package/templates/CUSTOMIZATION_GUIDE.md +3 -3
- package/templates/config.advanced.example.json +113 -0
- package/templates/config.example.json +53 -36
- package/templates/presets/ai/ANALYSIS_PROMPT.md +1 -1
- package/templates/presets/ai/config.json +5 -12
- package/templates/presets/backend/ANALYSIS_PROMPT.md +1 -1
- package/templates/presets/backend/config.json +5 -12
- package/templates/presets/database/ANALYSIS_PROMPT.md +1 -1
- package/templates/presets/database/config.json +5 -12
- package/templates/presets/default/config.json +5 -12
- package/templates/presets/frontend/ANALYSIS_PROMPT.md +1 -1
- package/templates/presets/frontend/config.json +5 -12
- package/templates/presets/fullstack/ANALYSIS_PROMPT.md +1 -1
- package/templates/presets/fullstack/config.json +5 -12
- package/templates/shared/ANALYSIS_PROMPT.md +1 -1
|
@@ -21,7 +21,7 @@ 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 } from './claude-diagnostics.js';
|
|
24
|
+
import { detectClaudeError, formatClaudeError, ClaudeErrorType, isRecoverableError } from './claude-diagnostics.js';
|
|
25
25
|
import { which } from './which-command.js';
|
|
26
26
|
|
|
27
27
|
/**
|
|
@@ -103,12 +103,15 @@ const getClaudeCommand = () => {
|
|
|
103
103
|
if (wslPath) {
|
|
104
104
|
try {
|
|
105
105
|
// Verify Claude exists in WSL
|
|
106
|
-
|
|
106
|
+
// Increased timeout from 5s to 15s to handle system load better
|
|
107
|
+
const wslCheckTimeout = config.system.wslCheckTimeout || 15000;
|
|
108
|
+
execSync(`"${wslPath}" claude --version`, { stdio: 'ignore', timeout: wslCheckTimeout });
|
|
107
109
|
logger.debug('claude-client - getClaudeCommand', 'Using WSL Claude CLI', { wslPath });
|
|
108
110
|
return { command: wslPath, args: ['claude'] };
|
|
109
111
|
} catch (wslError) {
|
|
110
112
|
// Differentiate error types for accurate user feedback
|
|
111
113
|
const errorMsg = wslError.message || '';
|
|
114
|
+
const wslCheckTimeout = config.system.wslCheckTimeout || 15000;
|
|
112
115
|
|
|
113
116
|
// Timeout: Transient system load issue
|
|
114
117
|
if (errorMsg.includes('ETIMEDOUT')) {
|
|
@@ -117,6 +120,7 @@ const getClaudeCommand = () => {
|
|
|
117
120
|
platform: 'Windows',
|
|
118
121
|
wslPath,
|
|
119
122
|
error: 'ETIMEDOUT',
|
|
123
|
+
timeoutValue: wslCheckTimeout,
|
|
120
124
|
suggestion: 'System is busy. Wait a moment and try again, or skip analysis: git commit --no-verify'
|
|
121
125
|
}
|
|
122
126
|
});
|
|
@@ -241,13 +245,57 @@ const executeClaude = (prompt, { timeout = 120000, allowedTools = [] } = {}) =>
|
|
|
241
245
|
|
|
242
246
|
// Handle process completion
|
|
243
247
|
claude.on('close', (code) => {
|
|
244
|
-
const
|
|
248
|
+
const elapsedTime = Date.now() - startTime;
|
|
249
|
+
|
|
250
|
+
// Check for "Execution error" even when exit code is 0
|
|
251
|
+
// Why: Claude CLI sometimes returns "Execution error" with exit code 0
|
|
252
|
+
// This occurs during API rate limiting or temporary backend issues
|
|
253
|
+
// IMPORTANT: Only check for EXACT match to avoid false positives
|
|
254
|
+
if (stdout.trim() === 'Execution error') {
|
|
255
|
+
const errorInfo = detectClaudeError(stdout, stderr, code);
|
|
256
|
+
|
|
257
|
+
logger.error(
|
|
258
|
+
'claude-client - executeClaude',
|
|
259
|
+
`Claude CLI returned execution error (exit ${code})`,
|
|
260
|
+
new ClaudeClientError(`Claude CLI error: ${errorInfo.type}`, {
|
|
261
|
+
output: { stdout, stderr },
|
|
262
|
+
context: {
|
|
263
|
+
exitCode: code,
|
|
264
|
+
elapsedTime,
|
|
265
|
+
timeoutValue: timeout,
|
|
266
|
+
errorType: errorInfo.type
|
|
267
|
+
}
|
|
268
|
+
})
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
// Merge timing info into errorInfo for formatting
|
|
272
|
+
const errorInfoWithTiming = {
|
|
273
|
+
...errorInfo,
|
|
274
|
+
elapsedTime,
|
|
275
|
+
timeoutValue: timeout
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
// Show formatted error to user
|
|
279
|
+
const formattedError = formatClaudeError(errorInfoWithTiming);
|
|
280
|
+
console.error(`\n${ formattedError }\n`);
|
|
281
|
+
|
|
282
|
+
reject(new ClaudeClientError(`Claude CLI error: ${errorInfo.type}`, {
|
|
283
|
+
output: { stdout, stderr },
|
|
284
|
+
context: {
|
|
285
|
+
exitCode: code,
|
|
286
|
+
elapsedTime,
|
|
287
|
+
timeoutValue: timeout,
|
|
288
|
+
errorInfo
|
|
289
|
+
}
|
|
290
|
+
}));
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
245
293
|
|
|
246
294
|
if (code === 0) {
|
|
247
295
|
logger.debug(
|
|
248
296
|
'claude-client - executeClaude',
|
|
249
297
|
'Claude CLI execution successful',
|
|
250
|
-
{
|
|
298
|
+
{ elapsedTime, outputLength: stdout.length }
|
|
251
299
|
);
|
|
252
300
|
resolve(stdout);
|
|
253
301
|
} else {
|
|
@@ -259,23 +307,42 @@ const executeClaude = (prompt, { timeout = 120000, allowedTools = [] } = {}) =>
|
|
|
259
307
|
`Claude CLI failed: ${errorInfo.type}`,
|
|
260
308
|
new ClaudeClientError(`Claude CLI error: ${errorInfo.type}`, {
|
|
261
309
|
output: { stdout, stderr },
|
|
262
|
-
context: {
|
|
310
|
+
context: {
|
|
311
|
+
exitCode: code,
|
|
312
|
+
elapsedTime,
|
|
313
|
+
timeoutValue: timeout,
|
|
314
|
+
errorType: errorInfo.type
|
|
315
|
+
}
|
|
263
316
|
})
|
|
264
317
|
);
|
|
265
318
|
|
|
319
|
+
// Merge timing info into errorInfo for formatting
|
|
320
|
+
const errorInfoWithTiming = {
|
|
321
|
+
...errorInfo,
|
|
322
|
+
elapsedTime,
|
|
323
|
+
timeoutValue: timeout
|
|
324
|
+
};
|
|
325
|
+
|
|
266
326
|
// Show formatted error to user
|
|
267
|
-
const formattedError = formatClaudeError(
|
|
327
|
+
const formattedError = formatClaudeError(errorInfoWithTiming);
|
|
268
328
|
console.error(`\n${ formattedError }\n`);
|
|
269
329
|
|
|
270
330
|
reject(new ClaudeClientError(`Claude CLI error: ${errorInfo.type}`, {
|
|
271
331
|
output: { stdout, stderr },
|
|
272
|
-
context: {
|
|
332
|
+
context: {
|
|
333
|
+
exitCode: code,
|
|
334
|
+
elapsedTime,
|
|
335
|
+
timeoutValue: timeout,
|
|
336
|
+
errorInfo
|
|
337
|
+
}
|
|
273
338
|
}));
|
|
274
339
|
}
|
|
275
340
|
});
|
|
276
341
|
|
|
277
342
|
// Handle errors
|
|
278
343
|
claude.on('error', (error) => {
|
|
344
|
+
const elapsedTime = Date.now() - startTime;
|
|
345
|
+
|
|
279
346
|
logger.error(
|
|
280
347
|
'claude-client - executeClaude',
|
|
281
348
|
'Failed to spawn Claude CLI process',
|
|
@@ -284,23 +351,36 @@ const executeClaude = (prompt, { timeout = 120000, allowedTools = [] } = {}) =>
|
|
|
284
351
|
|
|
285
352
|
reject(new ClaudeClientError('Failed to spawn Claude CLI', {
|
|
286
353
|
cause: error,
|
|
287
|
-
context: {
|
|
354
|
+
context: {
|
|
355
|
+
command,
|
|
356
|
+
args,
|
|
357
|
+
elapsedTime,
|
|
358
|
+
timeoutValue: timeout
|
|
359
|
+
}
|
|
288
360
|
}));
|
|
289
361
|
});
|
|
290
362
|
|
|
291
363
|
// Set up timeout
|
|
292
364
|
const timeoutId = setTimeout(() => {
|
|
365
|
+
const elapsedTime = Date.now() - startTime;
|
|
293
366
|
claude.kill();
|
|
367
|
+
|
|
294
368
|
logger.error(
|
|
295
369
|
'claude-client - executeClaude',
|
|
296
370
|
'Claude CLI execution timed out',
|
|
297
371
|
new ClaudeClientError('Claude CLI timeout', {
|
|
298
|
-
context: {
|
|
372
|
+
context: {
|
|
373
|
+
elapsedTime,
|
|
374
|
+
timeoutValue: timeout
|
|
375
|
+
}
|
|
299
376
|
})
|
|
300
377
|
);
|
|
301
378
|
|
|
302
379
|
reject(new ClaudeClientError('Claude CLI execution timed out', {
|
|
303
|
-
context: {
|
|
380
|
+
context: {
|
|
381
|
+
elapsedTime,
|
|
382
|
+
timeoutValue: timeout
|
|
383
|
+
}
|
|
304
384
|
}));
|
|
305
385
|
}, timeout);
|
|
306
386
|
|
|
@@ -580,6 +660,90 @@ const saveDebugResponse = async (prompt, response, filename = config.output.debu
|
|
|
580
660
|
}
|
|
581
661
|
};
|
|
582
662
|
|
|
663
|
+
/**
|
|
664
|
+
* Wraps an async function with retry logic for recoverable errors
|
|
665
|
+
* Why: Reusable retry logic for Claude CLI operations
|
|
666
|
+
*
|
|
667
|
+
* @param {Function} fn - Async function to execute with retry
|
|
668
|
+
* @param {Object} options - Retry options
|
|
669
|
+
* @param {number} options.maxRetries - Maximum retry attempts (default: 3)
|
|
670
|
+
* @param {number} options.baseRetryDelay - Base delay in ms (default: 2000)
|
|
671
|
+
* @param {number} options.retryCount - Current retry attempt (internal, default: 0)
|
|
672
|
+
* @param {string} options.operationName - Name for logging (default: 'operation')
|
|
673
|
+
* @returns {Promise<any>} Result from fn
|
|
674
|
+
* @throws {Error} If fn fails after all retries
|
|
675
|
+
*/
|
|
676
|
+
const withRetry = async (fn, { maxRetries = 3, baseRetryDelay = 2000, retryCount = 0, operationName = 'operation' } = {}) => {
|
|
677
|
+
const retryDelay = baseRetryDelay * Math.pow(2, retryCount);
|
|
678
|
+
|
|
679
|
+
try {
|
|
680
|
+
return await fn();
|
|
681
|
+
} catch (error) {
|
|
682
|
+
// Check if error is recoverable and we haven't exceeded retry limit
|
|
683
|
+
const hasContext = !!error.context;
|
|
684
|
+
const hasErrorInfo = !!error.context?.errorInfo;
|
|
685
|
+
const isRecoverable = hasErrorInfo ? isRecoverableError(error.context.errorInfo) : false;
|
|
686
|
+
const canRetry = retryCount < maxRetries;
|
|
687
|
+
|
|
688
|
+
logger.debug(
|
|
689
|
+
'claude-client - withRetry',
|
|
690
|
+
`Retry check for ${operationName}`,
|
|
691
|
+
{
|
|
692
|
+
retryCount,
|
|
693
|
+
maxRetries,
|
|
694
|
+
hasContext,
|
|
695
|
+
hasErrorInfo,
|
|
696
|
+
isRecoverable,
|
|
697
|
+
canRetry,
|
|
698
|
+
errorType: error.context?.errorInfo?.type,
|
|
699
|
+
errorName: error.name
|
|
700
|
+
}
|
|
701
|
+
);
|
|
702
|
+
|
|
703
|
+
const shouldRetry = canRetry && hasContext && hasErrorInfo && isRecoverable;
|
|
704
|
+
|
|
705
|
+
if (shouldRetry) {
|
|
706
|
+
logger.debug(
|
|
707
|
+
'claude-client - withRetry',
|
|
708
|
+
`Recoverable error detected, retrying ${operationName} in ${retryDelay}ms (attempt ${retryCount + 1}/${maxRetries})`,
|
|
709
|
+
{ errorType: error.context.errorInfo.type }
|
|
710
|
+
);
|
|
711
|
+
|
|
712
|
+
console.log(`\n⏳ Retrying in ${retryDelay / 1000} seconds due to ${error.context.errorInfo.type}...\n`);
|
|
713
|
+
|
|
714
|
+
// Wait before retry
|
|
715
|
+
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
|
716
|
+
|
|
717
|
+
// Retry with incremented count
|
|
718
|
+
return withRetry(fn, { maxRetries, baseRetryDelay, retryCount: retryCount + 1, operationName });
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Add retry attempt to error context if not already present
|
|
722
|
+
if (error.context && !error.context.retryAttempt) {
|
|
723
|
+
error.context.retryAttempt = retryCount;
|
|
724
|
+
error.context.maxRetries = maxRetries;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
logger.error('claude-client - withRetry', `${operationName} failed after retries`, error);
|
|
728
|
+
throw error;
|
|
729
|
+
}
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Executes Claude CLI with retry logic
|
|
734
|
+
* Why: Provides retry capability for executeClaude calls
|
|
735
|
+
*
|
|
736
|
+
* @param {string} prompt - Prompt text
|
|
737
|
+
* @param {Object} options - Execution options (timeout, allowedTools)
|
|
738
|
+
* @returns {Promise<string>} Claude's response
|
|
739
|
+
*/
|
|
740
|
+
const executeClaudeWithRetry = async (prompt, options = {}) => {
|
|
741
|
+
return withRetry(
|
|
742
|
+
() => executeClaude(prompt, options),
|
|
743
|
+
{ operationName: 'executeClaude' }
|
|
744
|
+
);
|
|
745
|
+
};
|
|
746
|
+
|
|
583
747
|
/**
|
|
584
748
|
* Analyzes code using Claude CLI
|
|
585
749
|
* Why: High-level interface that handles execution, parsing, and debug
|
|
@@ -598,34 +762,34 @@ const analyzeCode = async (prompt, { timeout = 120000, saveDebug = config.system
|
|
|
598
762
|
{ promptLength: prompt.length, timeout, saveDebug }
|
|
599
763
|
);
|
|
600
764
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
if (saveDebug) {
|
|
607
|
-
await saveDebugResponse(prompt, response);
|
|
608
|
-
}
|
|
765
|
+
// Use withRetry to wrap the entire analysis flow
|
|
766
|
+
return withRetry(
|
|
767
|
+
async () => {
|
|
768
|
+
// Execute Claude CLI
|
|
769
|
+
const response = await executeClaude(prompt, { timeout });
|
|
609
770
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
logger.debug(
|
|
614
|
-
'claude-client - analyzeCode',
|
|
615
|
-
'Analysis complete',
|
|
616
|
-
{
|
|
617
|
-
hasApproved: 'approved' in result,
|
|
618
|
-
hasQualityGate: 'QUALITY_GATE' in result,
|
|
619
|
-
blockingIssuesCount: result.blockingIssues?.length ?? 0
|
|
771
|
+
// Save debug if requested
|
|
772
|
+
if (saveDebug) {
|
|
773
|
+
await saveDebugResponse(prompt, response);
|
|
620
774
|
}
|
|
621
|
-
);
|
|
622
775
|
|
|
623
|
-
|
|
776
|
+
// Extract and parse JSON
|
|
777
|
+
const result = extractJSON(response);
|
|
624
778
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
779
|
+
logger.debug(
|
|
780
|
+
'claude-client - analyzeCode',
|
|
781
|
+
'Analysis complete',
|
|
782
|
+
{
|
|
783
|
+
hasApproved: 'approved' in result,
|
|
784
|
+
hasQualityGate: 'QUALITY_GATE' in result,
|
|
785
|
+
blockingIssuesCount: result.blockingIssues?.length ?? 0
|
|
786
|
+
}
|
|
787
|
+
);
|
|
788
|
+
|
|
789
|
+
return result;
|
|
790
|
+
},
|
|
791
|
+
{ operationName: 'analyzeCode' }
|
|
792
|
+
);
|
|
629
793
|
};
|
|
630
794
|
|
|
631
795
|
/**
|
|
@@ -679,6 +843,7 @@ const analyzeCodeParallel = async (prompts, options = {}) => {
|
|
|
679
843
|
export {
|
|
680
844
|
ClaudeClientError,
|
|
681
845
|
executeClaude,
|
|
846
|
+
executeClaudeWithRetry,
|
|
682
847
|
executeClaudeInteractive,
|
|
683
848
|
extractJSON,
|
|
684
849
|
saveDebugResponse,
|
|
@@ -25,6 +25,7 @@ export const ClaudeErrorType = {
|
|
|
25
25
|
TIMEOUT: 'TIMEOUT',
|
|
26
26
|
NETWORK: 'NETWORK',
|
|
27
27
|
INVALID_RESPONSE: 'INVALID_RESPONSE',
|
|
28
|
+
EXECUTION_ERROR: 'EXECUTION_ERROR',
|
|
28
29
|
GENERIC: 'GENERIC'
|
|
29
30
|
};
|
|
30
31
|
|
|
@@ -44,7 +45,20 @@ export const ClaudeErrorType = {
|
|
|
44
45
|
* @returns {Object|null} Error information or null if no specific error detected
|
|
45
46
|
*/
|
|
46
47
|
export const detectClaudeError = (stdout = '', stderr = '', exitCode = 1) => {
|
|
47
|
-
// 1.
|
|
48
|
+
// 1. Execution error detection (Claude CLI internal error)
|
|
49
|
+
// This occurs when Claude API returns an error instead of valid response
|
|
50
|
+
// Often caused by rate limiting, API errors, or temporary backend issues
|
|
51
|
+
// IMPORTANT: Only match EXACT "Execution error" response, not partial matches
|
|
52
|
+
const trimmedStdout = stdout.trim();
|
|
53
|
+
if (trimmedStdout === 'Execution error') {
|
|
54
|
+
return {
|
|
55
|
+
type: ClaudeErrorType.EXECUTION_ERROR,
|
|
56
|
+
exitCode,
|
|
57
|
+
stdout: stdout.substring(0, 100)
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 2. Rate limit detection
|
|
48
62
|
const rateLimitMatch = stdout.match(/Claude AI usage limit reached\|(\d+)/);
|
|
49
63
|
if (rateLimitMatch) {
|
|
50
64
|
const resetTimestamp = parseInt(rateLimitMatch[1], 10);
|
|
@@ -61,7 +75,7 @@ export const detectClaudeError = (stdout = '', stderr = '', exitCode = 1) => {
|
|
|
61
75
|
};
|
|
62
76
|
}
|
|
63
77
|
|
|
64
|
-
//
|
|
78
|
+
// 3. Authentication failure detection
|
|
65
79
|
if (stdout.includes('not authenticated') || stderr.includes('not authenticated') ||
|
|
66
80
|
stdout.includes('authentication failed') || stderr.includes('authentication failed')) {
|
|
67
81
|
return {
|
|
@@ -70,7 +84,7 @@ export const detectClaudeError = (stdout = '', stderr = '', exitCode = 1) => {
|
|
|
70
84
|
};
|
|
71
85
|
}
|
|
72
86
|
|
|
73
|
-
//
|
|
87
|
+
// 4. Network errors
|
|
74
88
|
if (stderr.includes('ENOTFOUND') || stderr.includes('ECONNREFUSED') ||
|
|
75
89
|
stderr.includes('network error') || stderr.includes('connection refused')) {
|
|
76
90
|
return {
|
|
@@ -79,7 +93,7 @@ export const detectClaudeError = (stdout = '', stderr = '', exitCode = 1) => {
|
|
|
79
93
|
};
|
|
80
94
|
}
|
|
81
95
|
|
|
82
|
-
//
|
|
96
|
+
// 5. Invalid response (JSON parsing errors)
|
|
83
97
|
if (stdout.includes('SyntaxError') || stdout.includes('Unexpected token')) {
|
|
84
98
|
return {
|
|
85
99
|
type: ClaudeErrorType.INVALID_RESPONSE,
|
|
@@ -87,7 +101,7 @@ export const detectClaudeError = (stdout = '', stderr = '', exitCode = 1) => {
|
|
|
87
101
|
};
|
|
88
102
|
}
|
|
89
103
|
|
|
90
|
-
//
|
|
104
|
+
// 6. Generic error
|
|
91
105
|
return {
|
|
92
106
|
type: ClaudeErrorType.GENERIC,
|
|
93
107
|
exitCode,
|
|
@@ -96,6 +110,38 @@ export const detectClaudeError = (stdout = '', stderr = '', exitCode = 1) => {
|
|
|
96
110
|
};
|
|
97
111
|
};
|
|
98
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Formats timing information for error messages
|
|
115
|
+
* @param {Object} errorInfo - Error information with optional timing data
|
|
116
|
+
* @returns {Array<string>} Array of formatted timing lines
|
|
117
|
+
*/
|
|
118
|
+
const formatTimingInfo = (errorInfo) => {
|
|
119
|
+
const lines = [];
|
|
120
|
+
const { elapsedTime, timeoutValue, retryAttempt, maxRetries } = errorInfo;
|
|
121
|
+
|
|
122
|
+
if (elapsedTime !== undefined || timeoutValue !== undefined || retryAttempt !== undefined) {
|
|
123
|
+
lines.push('');
|
|
124
|
+
lines.push('Timing information:');
|
|
125
|
+
|
|
126
|
+
if (elapsedTime !== undefined) {
|
|
127
|
+
const seconds = (elapsedTime / 1000).toFixed(2);
|
|
128
|
+
lines.push(` Elapsed time: ${seconds}s`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (timeoutValue !== undefined) {
|
|
132
|
+
const timeoutSeconds = (timeoutValue / 1000).toFixed(0);
|
|
133
|
+
lines.push(` Timeout limit: ${timeoutSeconds}s`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (retryAttempt !== undefined) {
|
|
137
|
+
const maxRetriesText = maxRetries !== undefined ? `/${maxRetries}` : '';
|
|
138
|
+
lines.push(` Retry attempt: ${retryAttempt}${maxRetriesText}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return lines;
|
|
143
|
+
};
|
|
144
|
+
|
|
99
145
|
/**
|
|
100
146
|
* Formats Claude error message with diagnostics and remediation steps
|
|
101
147
|
* Why: Provides consistent, actionable error messages
|
|
@@ -107,6 +153,9 @@ export const formatClaudeError = (errorInfo) => {
|
|
|
107
153
|
const lines = [];
|
|
108
154
|
|
|
109
155
|
switch (errorInfo.type) {
|
|
156
|
+
case ClaudeErrorType.EXECUTION_ERROR:
|
|
157
|
+
return formatExecutionError(errorInfo);
|
|
158
|
+
|
|
110
159
|
case ClaudeErrorType.RATE_LIMIT:
|
|
111
160
|
return formatRateLimitError(errorInfo);
|
|
112
161
|
|
|
@@ -125,10 +174,42 @@ export const formatClaudeError = (errorInfo) => {
|
|
|
125
174
|
}
|
|
126
175
|
};
|
|
127
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Format execution error
|
|
179
|
+
*/
|
|
180
|
+
const formatExecutionError = (errorInfo) => {
|
|
181
|
+
const { exitCode, stdout } = errorInfo;
|
|
182
|
+
const lines = [];
|
|
183
|
+
|
|
184
|
+
lines.push('❌ Claude CLI execution error');
|
|
185
|
+
lines.push('');
|
|
186
|
+
lines.push('Claude returned "Execution error" instead of valid response.');
|
|
187
|
+
lines.push(...formatTimingInfo(errorInfo));
|
|
188
|
+
lines.push('');
|
|
189
|
+
lines.push('Common causes:');
|
|
190
|
+
lines.push(' 1. API rate limiting (burst limits)');
|
|
191
|
+
lines.push(' 2. Temporary Claude API backend issues');
|
|
192
|
+
lines.push(' 3. Request/response size limits exceeded');
|
|
193
|
+
lines.push('');
|
|
194
|
+
lines.push('Solutions:');
|
|
195
|
+
lines.push(' 1. Wait 10-30 seconds and try again');
|
|
196
|
+
lines.push(' 2. Reduce prompt size by committing fewer files at once');
|
|
197
|
+
lines.push(' 3. Switch to haiku model (faster, higher rate limits):');
|
|
198
|
+
lines.push(' Edit .claude/config.json:');
|
|
199
|
+
lines.push(' { "subagents": { "model": "haiku" } }');
|
|
200
|
+
lines.push(' 4. Skip analysis for now:');
|
|
201
|
+
lines.push(' git commit --no-verify -m "your message"');
|
|
202
|
+
lines.push('');
|
|
203
|
+
lines.push('The hook will automatically retry once after 2 seconds.');
|
|
204
|
+
|
|
205
|
+
return lines.join('\n');
|
|
206
|
+
};
|
|
207
|
+
|
|
128
208
|
/**
|
|
129
209
|
* Format rate limit error
|
|
130
210
|
*/
|
|
131
|
-
const formatRateLimitError = (
|
|
211
|
+
const formatRateLimitError = (errorInfo) => {
|
|
212
|
+
const { resetDate, minutesUntilReset } = errorInfo;
|
|
132
213
|
const lines = [];
|
|
133
214
|
|
|
134
215
|
lines.push('❌ Claude API usage limit reached');
|
|
@@ -145,6 +226,7 @@ const formatRateLimitError = ({ resetDate, minutesUntilReset }) => {
|
|
|
145
226
|
lines.push(' Limit should be reset now');
|
|
146
227
|
}
|
|
147
228
|
|
|
229
|
+
lines.push(...formatTimingInfo(errorInfo));
|
|
148
230
|
lines.push('');
|
|
149
231
|
lines.push('Options:');
|
|
150
232
|
lines.push(' 1. Wait for rate limit to reset');
|
|
@@ -160,10 +242,12 @@ const formatRateLimitError = ({ resetDate, minutesUntilReset }) => {
|
|
|
160
242
|
/**
|
|
161
243
|
* Format authentication error
|
|
162
244
|
*/
|
|
163
|
-
const formatAuthError = (
|
|
245
|
+
const formatAuthError = (errorInfo) => {
|
|
246
|
+
const { exitCode } = errorInfo;
|
|
164
247
|
const lines = [];
|
|
165
248
|
|
|
166
249
|
lines.push('❌ Claude CLI authentication failed');
|
|
250
|
+
lines.push(...formatTimingInfo(errorInfo));
|
|
167
251
|
lines.push('');
|
|
168
252
|
lines.push('Possible causes:');
|
|
169
253
|
lines.push(' 1. Not logged in to Claude CLI');
|
|
@@ -181,10 +265,12 @@ const formatAuthError = ({ exitCode }) => {
|
|
|
181
265
|
/**
|
|
182
266
|
* Format network error
|
|
183
267
|
*/
|
|
184
|
-
const formatNetworkError = (
|
|
268
|
+
const formatNetworkError = (errorInfo) => {
|
|
269
|
+
const { exitCode } = errorInfo;
|
|
185
270
|
const lines = [];
|
|
186
271
|
|
|
187
272
|
lines.push('❌ Network error connecting to Claude API');
|
|
273
|
+
lines.push(...formatTimingInfo(errorInfo));
|
|
188
274
|
lines.push('');
|
|
189
275
|
lines.push('Possible causes:');
|
|
190
276
|
lines.push(' 1. No internet connection');
|
|
@@ -203,10 +289,12 @@ const formatNetworkError = ({ exitCode }) => {
|
|
|
203
289
|
/**
|
|
204
290
|
* Format invalid response error
|
|
205
291
|
*/
|
|
206
|
-
const formatInvalidResponseError = (
|
|
292
|
+
const formatInvalidResponseError = (errorInfo) => {
|
|
293
|
+
const { exitCode } = errorInfo;
|
|
207
294
|
const lines = [];
|
|
208
295
|
|
|
209
296
|
lines.push('❌ Claude returned invalid response');
|
|
297
|
+
lines.push(...formatTimingInfo(errorInfo));
|
|
210
298
|
lines.push('');
|
|
211
299
|
lines.push('This usually means:');
|
|
212
300
|
lines.push(' - Claude did not return valid JSON');
|
|
@@ -223,12 +311,14 @@ const formatInvalidResponseError = ({ exitCode }) => {
|
|
|
223
311
|
/**
|
|
224
312
|
* Format generic error
|
|
225
313
|
*/
|
|
226
|
-
const formatGenericError = (
|
|
314
|
+
const formatGenericError = (errorInfo) => {
|
|
315
|
+
const { exitCode, stdout, stderr } = errorInfo;
|
|
227
316
|
const lines = [];
|
|
228
317
|
|
|
229
318
|
lines.push('❌ Claude CLI execution failed');
|
|
230
319
|
lines.push('');
|
|
231
320
|
lines.push(`Exit code: ${exitCode}`);
|
|
321
|
+
lines.push(...formatTimingInfo(errorInfo));
|
|
232
322
|
|
|
233
323
|
if (stdout && stdout.trim()) {
|
|
234
324
|
lines.push('');
|
|
@@ -261,6 +351,7 @@ const formatGenericError = ({ exitCode, stdout, stderr }) => {
|
|
|
261
351
|
* @returns {boolean} True if error might resolve with retry
|
|
262
352
|
*/
|
|
263
353
|
export const isRecoverableError = (errorInfo) => {
|
|
264
|
-
return errorInfo.type === ClaudeErrorType.
|
|
354
|
+
return errorInfo.type === ClaudeErrorType.EXECUTION_ERROR ||
|
|
355
|
+
errorInfo.type === ClaudeErrorType.RATE_LIMIT ||
|
|
265
356
|
errorInfo.type === ClaudeErrorType.NETWORK;
|
|
266
357
|
};
|
|
@@ -215,8 +215,8 @@ const formatFileSection = ({ path, diff, content, isNew }) => {
|
|
|
215
215
|
* }
|
|
216
216
|
*/
|
|
217
217
|
const buildAnalysisPrompt = async ({
|
|
218
|
-
templateName = '
|
|
219
|
-
guidelinesName = '
|
|
218
|
+
templateName = 'CLAUDE_ANALYSIS_PROMPT.md',
|
|
219
|
+
guidelinesName = 'CLAUDE_PRE_COMMIT.md',
|
|
220
220
|
files = [],
|
|
221
221
|
metadata = {},
|
|
222
222
|
subagentConfig = null
|
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
ROLE:SeniorCodeReviewer,
|
|
1
|
+
ROLE:SeniorCodeReviewer,CodeQualityExpert
|
|
2
2
|
TASK:Analyze code->JSON only
|
|
3
3
|
CRITICAL:NoTextOutsideJSON,ValidJSON
|
|
4
4
|
|
|
@@ -44,6 +44,7 @@ OUTPUT_SCHEMA:
|
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
RULES:
|
|
47
|
+
- **FALSE_POSITIVE_PREVENTION**: Only report actual problems. Do not report improvements, positive changes, or routine maintenance as issues. Zero issues is a valid and positive outcome.
|
|
47
48
|
- blockingIssues=ALWAYS_ARRAY_OF_OBJECTS(never_strings)
|
|
48
49
|
- blockingIssues=ALL(details.severity∈{BLOCKER,CRITICAL})
|
|
49
50
|
- Each_blockingIssue_MUST_have:description,file,line,method,severity
|
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
#
|
|
1
|
+
# CODE_QUALITY_EVALUATION_CRITERIA
|
|
2
|
+
|
|
3
|
+
## FALSE_POSITIVE_PREVENTION
|
|
4
|
+
|
|
5
|
+
**IMPORTANT**: Only report actual problems, bugs, security issues, or code quality concerns.
|
|
6
|
+
|
|
7
|
+
- Do NOT report improvements, positive changes, or routine maintenance as issues
|
|
8
|
+
- It is perfectly acceptable to find ZERO issues - this is a positive outcome
|
|
9
|
+
- When in doubt about whether something is an issue, err on the side of NOT reporting it
|
|
10
|
+
- Focus on what could break, harm security, or degrade quality - not what could be "better"
|
|
2
11
|
|
|
3
12
|
## EXCEPTIONS_AND_ALLOWED_PATTERNS
|
|
4
13
|
### Spring Framework Patterns (NOT blocking issues)
|
|
@@ -149,7 +149,7 @@ A preset is a **self-contained package** that customizes claude-hooks for a spec
|
|
|
149
149
|
│
|
|
150
150
|
┌────────────────────────▼────────────────────────────────────┐
|
|
151
151
|
│ 8. RESPONSE PROCESSED │
|
|
152
|
-
│ - JSON format
|
|
152
|
+
│ - JSON format with structured results │
|
|
153
153
|
│ - Quality gate: PASSED/FAILED │
|
|
154
154
|
│ - Issues by severity: blocker, critical, major, minor │
|
|
155
155
|
│ - Commit proceeds or blocked │
|
|
@@ -382,7 +382,7 @@ Perform a comprehensive code quality analysis focusing on these areas:
|
|
|
382
382
|
**Template**:
|
|
383
383
|
|
|
384
384
|
````markdown
|
|
385
|
-
#
|
|
385
|
+
# Code Quality Evaluation Criteria
|
|
386
386
|
|
|
387
387
|
## Severity Levels
|
|
388
388
|
|
|
@@ -599,7 +599,7 @@ Please generate the following files:
|
|
|
599
599
|
- This preset will be installed at: `templates/presets/{preset-name}/`
|
|
600
600
|
- It should follow the same structure as existing presets in `templates/presets/backend/`
|
|
601
601
|
- Users will select it with: `claude-hooks --set-preset {preset-name}`
|
|
602
|
-
- The preset must output JSON
|
|
602
|
+
- The preset must output JSON with structured analysis results
|
|
603
603
|
|
|
604
604
|
**Format**: Return each file's content as a separate code block with clear file headers.
|
|
605
605
|
```
|