claude-git-hooks 2.33.1 → 2.35.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 +34 -0
- package/CLAUDE.md +56 -13
- package/README.md +37 -0
- package/bin/claude-hooks +1 -0
- package/lib/cli-metadata.js +9 -0
- package/lib/commands/install.js +24 -0
- package/lib/commands/lint.js +187 -0
- package/lib/config.js +7 -0
- package/lib/hooks/pre-commit.js +97 -31
- package/lib/utils/judge.js +11 -9
- package/lib/utils/linter-runner.js +532 -0
- package/lib/utils/tool-runner.js +418 -0
- package/package.json +2 -2
- package/templates/config.advanced.example.json +38 -0
package/lib/hooks/pre-commit.js
CHANGED
|
@@ -33,6 +33,7 @@ import { getVersion } from '../utils/package-info.js';
|
|
|
33
33
|
import logger from '../utils/logger.js';
|
|
34
34
|
import { getConfig } from '../config.js';
|
|
35
35
|
import { recordMetric } from '../utils/metrics.js';
|
|
36
|
+
import { runLinters, displayLintResults, lintIssuesToAnalysisDetails } from '../utils/linter-runner.js';
|
|
36
37
|
|
|
37
38
|
/**
|
|
38
39
|
* Configuration loaded from lib/config.js
|
|
@@ -144,11 +145,45 @@ const main = async () => {
|
|
|
144
145
|
process.exit(0);
|
|
145
146
|
}
|
|
146
147
|
|
|
147
|
-
// Step 3:
|
|
148
|
+
// Step 3: Run linters (fast, deterministic — before Claude analysis)
|
|
149
|
+
// Unfixable lint issues are forwarded to the judge for semantic resolution
|
|
150
|
+
let unfixableLintDetails = [];
|
|
151
|
+
if (config.linting?.enabled !== false) {
|
|
152
|
+
logger.info('🔍 Running linters...');
|
|
153
|
+
const lintStartTime = Date.now();
|
|
154
|
+
const filePaths = validFiles.map((f) => (typeof f === 'string' ? f : f.path));
|
|
155
|
+
const lintResult = await runLinters(filePaths, config, presetName);
|
|
156
|
+
displayLintResults(lintResult);
|
|
157
|
+
|
|
158
|
+
// Record lint metric
|
|
159
|
+
recordMetric('linting.completed', {
|
|
160
|
+
preset: presetName,
|
|
161
|
+
fileCount: filePaths.length,
|
|
162
|
+
totalErrors: lintResult.totalErrors,
|
|
163
|
+
totalWarnings: lintResult.totalWarnings,
|
|
164
|
+
totalFixed: lintResult.totalFixed,
|
|
165
|
+
toolsRun: lintResult.results.filter((r) => !r.skipped).length,
|
|
166
|
+
toolsSkipped: lintResult.results.filter((r) => r.skipped).length,
|
|
167
|
+
duration: Date.now() - lintStartTime
|
|
168
|
+
}).catch((err) => {
|
|
169
|
+
logger.debug('pre-commit - main', 'Lint metrics recording failed', err);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Forward all remaining lint issues (errors + warnings) to the judge
|
|
173
|
+
// The judge decides whether to fix, dismiss, or block — no hard exit here
|
|
174
|
+
if (lintResult.totalErrors > 0 || lintResult.totalWarnings > 0) {
|
|
175
|
+
unfixableLintDetails = lintIssuesToAnalysisDetails(lintResult);
|
|
176
|
+
logger.debug('pre-commit - main', 'Forwarding unfixable lint issues to judge', {
|
|
177
|
+
count: unfixableLintDetails.length
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Step 4: Build file data using shared engine
|
|
148
183
|
logger.debug('pre-commit - main', 'Building file data for analysis');
|
|
149
184
|
const filesData = buildFilesData(validFiles, { staged: true });
|
|
150
185
|
|
|
151
|
-
// Step
|
|
186
|
+
// Step 5: Log analysis configuration
|
|
152
187
|
logger.info(`Sending ${filesData.length} files for review...`);
|
|
153
188
|
|
|
154
189
|
// Display analysis routing hint
|
|
@@ -156,51 +191,82 @@ const main = async () => {
|
|
|
156
191
|
logger.info('⚡ Intelligent orchestration: grouping files and assigning models');
|
|
157
192
|
}
|
|
158
193
|
|
|
159
|
-
// Step
|
|
194
|
+
// Step 6: Run analysis using shared engine
|
|
160
195
|
const result = await runAnalysis(filesData, config, {
|
|
161
196
|
hook: 'pre-commit',
|
|
162
197
|
saveDebug: config.system.debug
|
|
163
198
|
});
|
|
164
199
|
|
|
165
|
-
// Step
|
|
200
|
+
// Step 7: Display results using shared function
|
|
166
201
|
displayResults(result);
|
|
167
202
|
|
|
168
|
-
// Step
|
|
203
|
+
// Step 8: Merge unfixable lint issues into analysis result for the judge
|
|
204
|
+
if (unfixableLintDetails.length > 0) {
|
|
205
|
+
if (!Array.isArray(result.details)) {
|
|
206
|
+
result.details = [];
|
|
207
|
+
}
|
|
208
|
+
result.details.push(...unfixableLintDetails);
|
|
209
|
+
|
|
210
|
+
// Update issue counts
|
|
211
|
+
for (const detail of unfixableLintDetails) {
|
|
212
|
+
const sev = (detail.severity || 'minor').toLowerCase();
|
|
213
|
+
if (result.issues[sev] !== undefined) {
|
|
214
|
+
result.issues[sev]++;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
logger.info(`📋 ${unfixableLintDetails.length} unfixable lint issue(s) forwarded to judge`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Step 9: Judge — auto-fix all issues (Claude + lint)
|
|
169
222
|
if (config.judge?.enabled !== false && hasAnyIssues(result)) {
|
|
223
|
+
let judgeModule;
|
|
170
224
|
try {
|
|
171
|
-
|
|
172
|
-
|
|
225
|
+
judgeModule = await import('../utils/judge.js');
|
|
226
|
+
} catch (importErr) {
|
|
227
|
+
logger.warning(`Judge module import failed: ${importErr.message}`);
|
|
228
|
+
logger.warning('Commit blocked — judge module unavailable');
|
|
229
|
+
result.QUALITY_GATE = 'FAILED';
|
|
230
|
+
result.approved = false;
|
|
231
|
+
judgeModule = null;
|
|
232
|
+
}
|
|
173
233
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
234
|
+
if (judgeModule) {
|
|
235
|
+
try {
|
|
236
|
+
const { judgeAndFix } = judgeModule;
|
|
237
|
+
const judgeResult = await judgeAndFix(result, filesData, config);
|
|
238
|
+
|
|
239
|
+
// Update result with remaining issues (fixed + false positives removed)
|
|
240
|
+
result.blockingIssues = judgeResult.remainingIssues.filter((i) =>
|
|
241
|
+
['blocker', 'critical'].includes((i.severity || '').toLowerCase())
|
|
242
|
+
);
|
|
243
|
+
result.details = judgeResult.remainingIssues;
|
|
244
|
+
|
|
245
|
+
// Recalculate quality gate — pass only if ALL issues resolved
|
|
246
|
+
if (judgeResult.remainingIssues.length === 0) {
|
|
247
|
+
result.issues = {
|
|
248
|
+
blocker: 0,
|
|
249
|
+
critical: 0,
|
|
250
|
+
major: 0,
|
|
251
|
+
minor: 0,
|
|
252
|
+
info: 0
|
|
253
|
+
};
|
|
254
|
+
result.QUALITY_GATE = 'PASSED';
|
|
255
|
+
result.approved = true;
|
|
256
|
+
} else {
|
|
257
|
+
result.QUALITY_GATE = 'FAILED';
|
|
258
|
+
result.approved = false;
|
|
259
|
+
}
|
|
260
|
+
} catch (err) {
|
|
261
|
+
logger.warning(`Judge execution failed: ${err.message}`);
|
|
262
|
+
logger.warning('Commit blocked — judge could not verify issues');
|
|
192
263
|
result.QUALITY_GATE = 'FAILED';
|
|
193
264
|
result.approved = false;
|
|
194
265
|
}
|
|
195
|
-
} catch (err) {
|
|
196
|
-
logger.warning(`Judge failed: ${err.message}`);
|
|
197
|
-
logger.warning('Commit blocked — judge could not verify issues');
|
|
198
|
-
result.QUALITY_GATE = 'FAILED';
|
|
199
|
-
result.approved = false;
|
|
200
266
|
}
|
|
201
267
|
}
|
|
202
268
|
|
|
203
|
-
// Step
|
|
269
|
+
// Step 10: Check quality gate
|
|
204
270
|
const qualityGatePassed = result.QUALITY_GATE === 'PASSED';
|
|
205
271
|
const approved = result.approved !== false;
|
|
206
272
|
|
package/lib/utils/judge.js
CHANGED
|
@@ -182,17 +182,19 @@ const judgeAndFix = async (analysisResult, filesData, config) => {
|
|
|
182
182
|
const resolvedIndices = new Set();
|
|
183
183
|
const verdicts = [];
|
|
184
184
|
|
|
185
|
-
|
|
185
|
+
// Cap at allIssues.length — LLM may hallucinate extra fixes beyond the input count
|
|
186
|
+
const fixCount = Math.min(fixes.length, allIssues.length);
|
|
187
|
+
|
|
188
|
+
if (fixes.length > allIssues.length) {
|
|
189
|
+
logger.debug('judge', `LLM returned ${fixes.length} fixes for ${allIssues.length} issues — ignoring ${fixes.length - allIssues.length} extra entries`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
for (let i = 0; i < fixCount; i++) {
|
|
186
193
|
const verdict = fixes[i];
|
|
187
194
|
const num = i + 1;
|
|
188
|
-
|
|
189
|
-
const
|
|
190
|
-
const
|
|
191
|
-
? (matchedIssue.severity || 'unknown').toUpperCase()
|
|
192
|
-
: 'UNKNOWN';
|
|
193
|
-
const desc = matchedIssue
|
|
194
|
-
? matchedIssue.description || matchedIssue.message || 'No description'
|
|
195
|
-
: verdict.explanation || 'Unknown issue';
|
|
195
|
+
const matchedIssue = allIssues[i];
|
|
196
|
+
const severity = (matchedIssue.severity || 'unknown').toUpperCase();
|
|
197
|
+
const desc = matchedIssue.description || matchedIssue.message || 'No description';
|
|
196
198
|
|
|
197
199
|
if (verdict.assessment === 'FALSE_POSITIVE') {
|
|
198
200
|
falsePositiveCount++;
|