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.
@@ -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: Build file data using shared engine
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 4: Log analysis configuration
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 5: Run analysis using shared engine
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 6: Display results using shared function
200
+ // Step 7: Display results using shared function
166
201
  displayResults(result);
167
202
 
168
- // Step 6.5: Judge auto-fix all issues
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
- const { judgeAndFix } = await import('../utils/judge.js');
172
- const judgeResult = await judgeAndFix(result, filesData, config);
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
- // Update result with remaining issues (fixed + false positives removed)
175
- result.blockingIssues = judgeResult.remainingIssues.filter((i) =>
176
- ['blocker', 'critical'].includes((i.severity || '').toLowerCase())
177
- );
178
- result.details = judgeResult.remainingIssues;
179
-
180
- // Recalculate quality gate — pass only if ALL issues resolved
181
- if (judgeResult.remainingIssues.length === 0) {
182
- result.issues = {
183
- blocker: 0,
184
- critical: 0,
185
- major: 0,
186
- minor: 0,
187
- info: 0
188
- };
189
- result.QUALITY_GATE = 'PASSED';
190
- result.approved = true;
191
- } else {
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 7: Check quality gate
269
+ // Step 10: Check quality gate
204
270
  const qualityGatePassed = result.QUALITY_GATE === 'PASSED';
205
271
  const approved = result.approved !== false;
206
272
 
@@ -182,17 +182,19 @@ const judgeAndFix = async (analysisResult, filesData, config) => {
182
182
  const resolvedIndices = new Set();
183
183
  const verdicts = [];
184
184
 
185
- for (let i = 0; i < fixes.length; i++) {
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
- // Find matching issue by index or use verdict data if index exceeds allIssues length
189
- const matchedIssue = i < allIssues.length ? allIssues[i] : null;
190
- const severity = matchedIssue
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++;