claude-git-hooks 2.33.0 → 2.34.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.
@@ -0,0 +1,418 @@
1
+ /**
2
+ * File: tool-runner.js
3
+ * Purpose: Generic external tool executor for pre-commit pipeline
4
+ *
5
+ * Shared infrastructure for running external tools (linters, formatters)
6
+ * on staged or user-specified files. Each tool is defined as a ToolDefinition
7
+ * object with command, args builders, output parser, and install hints.
8
+ *
9
+ * Designed for extensibility:
10
+ * - Linters use this via linter-runner.js (Issue #22)
11
+ * - Formatters will use this via formatter-runner.js (Issue #26)
12
+ *
13
+ * Dependencies:
14
+ * - which-command: Cross-platform executable resolution
15
+ * - child_process: Tool execution
16
+ * - git-operations: Re-staging fixed files
17
+ * - logger: Debug and error logging
18
+ */
19
+
20
+ import { execSync } from 'child_process';
21
+ import fs from 'fs';
22
+ import path from 'path';
23
+ import { which } from './which-command.js';
24
+ import { walkDirectoryTree } from './file-utils.js';
25
+ import logger from './logger.js';
26
+
27
+ /**
28
+ * @typedef {Object} ToolDefinition
29
+ * @property {string} name - Tool display name (e.g., 'eslint')
30
+ * @property {string} command - Primary command to execute (e.g., 'npx', 'mvn')
31
+ * @property {function(string[]): string[]} args - Build args for check mode
32
+ * @property {function(string[]): string[]} [fixArgs] - Build args for auto-fix mode
33
+ * @property {string} detectCommand - Binary to check via which() (e.g., 'eslint')
34
+ * @property {string} [detectFallback] - Fallback path to check (e.g., 'node_modules/.bin/eslint')
35
+ * @property {string} installHint - Install instruction shown when tool is missing
36
+ * @property {string[]} extensions - File extensions this tool handles
37
+ * @property {function(string): Object} parseOutput - Parse stdout into structured results
38
+ * @property {number} [timeout] - Per-tool timeout in ms (default from config)
39
+ */
40
+
41
+ /**
42
+ * @typedef {Object} ToolIssue
43
+ * @property {string} file - File path
44
+ * @property {number} [line] - Line number
45
+ * @property {number} [column] - Column number
46
+ * @property {string} severity - 'error' or 'warning'
47
+ * @property {string} message - Issue description
48
+ * @property {string} [ruleId] - Linter rule identifier
49
+ */
50
+
51
+ /**
52
+ * @typedef {Object} ToolRunResult
53
+ * @property {string} tool - Tool name
54
+ * @property {boolean} skipped - Whether tool was skipped (not installed)
55
+ * @property {string} [skipReason] - Why the tool was skipped
56
+ * @property {ToolIssue[]} errors - Error-level issues
57
+ * @property {ToolIssue[]} warnings - Warning-level issues
58
+ * @property {number} fixedCount - Number of issues auto-fixed
59
+ * @property {string[]} fixedFiles - Files that were auto-fixed
60
+ */
61
+
62
+ /**
63
+ * Check if a tool is available on the system
64
+ *
65
+ * @param {ToolDefinition} toolDef - Tool definition
66
+ * @returns {{ available: boolean, path: string|null }} Availability info
67
+ */
68
+ export function isToolAvailable(toolDef) {
69
+ // Strategy 1: Check detectCommand in PATH (global install)
70
+ const resolved = which(toolDef.detectCommand);
71
+ if (resolved) {
72
+ logger.debug('tool-runner - isToolAvailable', `Found ${toolDef.name} in PATH`, { path: resolved });
73
+ return { available: true, path: resolved };
74
+ }
75
+
76
+ // Strategy 2: Check project files for the tool as a dependency
77
+ // Why: local installs (npm install --save-dev) live in node_modules/.bin,
78
+ // not in PATH. npx resolves them at runtime, so we just need to confirm
79
+ // the tool is declared in a project file (package.json deps or pom.xml plugin).
80
+ // Uses walkDirectoryTree (same walker as version-manager) to scan up to 3 levels.
81
+ if (toolDef.detectInProjectFile) {
82
+ const found = _detectInProjectFiles(toolDef);
83
+ if (found) {
84
+ // Tool is configured but the command binary is missing
85
+ if (found.configured && !found.available) {
86
+ logger.debug('tool-runner - isToolAvailable',
87
+ `${toolDef.name} configured but '${found.missingCommand}' not in PATH`
88
+ );
89
+ return {
90
+ available: false,
91
+ path: null,
92
+ installHint: `${toolDef.name} is configured in your project but '${found.missingCommand}' is not in PATH. Install it and ensure it's accessible from your terminal.`
93
+ };
94
+ }
95
+ return found;
96
+ }
97
+ }
98
+
99
+ logger.debug('tool-runner - isToolAvailable', `${toolDef.name} not found`);
100
+ return { available: false, path: null };
101
+ }
102
+
103
+ /**
104
+ * Scan project files (package.json, pom.xml) up to 3 levels deep
105
+ * to detect if a tool is declared as a dependency or plugin.
106
+ *
107
+ * @param {ToolDefinition} toolDef - Tool definition with detectInProjectFile
108
+ * @returns {{ available: boolean, path: string|null } | null} Result or null if not found
109
+ */
110
+ export function _detectInProjectFiles(toolDef) {
111
+ const { filename, check } = toolDef.detectInProjectFile;
112
+ let found = false;
113
+
114
+ walkDirectoryTree(process.cwd(), {
115
+ maxDepth: 3,
116
+ ignoreSet: new Set([
117
+ '.git', 'node_modules', 'target', 'build', 'dist',
118
+ '__pycache__', '.venv', 'vendor', 'out', 'coverage'
119
+ ]),
120
+ onFile: (entry, fullPath) => {
121
+ if (found) return; // short-circuit after first match
122
+ if (entry.name === filename) {
123
+ try {
124
+ const content = fs.readFileSync(fullPath, 'utf8');
125
+ if (check(content)) {
126
+ logger.debug('tool-runner - _detectInProjectFiles',
127
+ `Found ${toolDef.name} in ${path.relative(process.cwd(), fullPath)}`
128
+ );
129
+ found = true;
130
+ }
131
+ } catch {
132
+ // File read error — skip
133
+ }
134
+ }
135
+ }
136
+ });
137
+
138
+ if (!found) {
139
+ return null;
140
+ }
141
+
142
+ // Project file declares the tool, but can we actually run the command?
143
+ // npx-based tools (eslint, etc.) are resolved by npx at runtime — always OK.
144
+ // Non-npx tools (mvn, sqlfluff, etc.) need the command binary in PATH.
145
+ if (toolDef.command !== 'npx') {
146
+ const cmdResolved = which(toolDef.command);
147
+ if (!cmdResolved) {
148
+ logger.debug('tool-runner - _detectInProjectFiles',
149
+ `${toolDef.name} configured in project but '${toolDef.command}' not in PATH`
150
+ );
151
+ return {
152
+ available: false,
153
+ path: null,
154
+ configured: true,
155
+ missingCommand: toolDef.command
156
+ };
157
+ }
158
+ }
159
+
160
+ return { available: true, path: toolDef.command };
161
+ }
162
+
163
+ /**
164
+ * Filter files by tool's supported extensions
165
+ *
166
+ * @param {string[]} files - File paths
167
+ * @param {ToolDefinition} toolDef - Tool definition
168
+ * @returns {string[]} Files matching tool's extensions
169
+ */
170
+ export function filterFilesByTool(files, toolDef) {
171
+ return files.filter((file) => {
172
+ const ext = path.extname(file).toLowerCase();
173
+ return toolDef.extensions.includes(ext);
174
+ });
175
+ }
176
+
177
+ /**
178
+ * Execute a tool on files and parse output
179
+ *
180
+ * @param {ToolDefinition} toolDef - Tool definition
181
+ * @param {string[]} files - Files to process
182
+ * @param {Object} options - Execution options
183
+ * @param {number} [options.timeout] - Timeout in ms
184
+ * @param {string} [options.cwd] - Working directory
185
+ * @returns {ToolRunResult} Parsed result
186
+ */
187
+ export function runTool(toolDef, files, options = {}) {
188
+ const { timeout = toolDef.timeout || 30000, cwd = process.cwd() } = options;
189
+
190
+ const args = toolDef.args(files);
191
+ const fullCommand = [toolDef.command, ...args].join(' ');
192
+
193
+ logger.debug('tool-runner - runTool', `Executing ${toolDef.name}`, {
194
+ command: fullCommand,
195
+ fileCount: files.length,
196
+ timeout
197
+ });
198
+
199
+ const result = {
200
+ tool: toolDef.name,
201
+ skipped: false,
202
+ errors: [],
203
+ warnings: [],
204
+ fixedCount: 0,
205
+ fixedFiles: []
206
+ };
207
+
208
+ try {
209
+ const stdout = execSync(fullCommand, {
210
+ encoding: 'utf8',
211
+ timeout,
212
+ cwd,
213
+ stdio: ['pipe', 'pipe', 'pipe']
214
+ });
215
+
216
+ // Exit code 0 = no issues; parse anyway for warnings
217
+ if (toolDef.parseOutput && stdout.trim()) {
218
+ const parsed = toolDef.parseOutput(stdout);
219
+ result.errors = parsed.errors || [];
220
+ result.warnings = parsed.warnings || [];
221
+ }
222
+ } catch (err) {
223
+ // Many linters exit non-zero when issues are found — this is expected
224
+ if (err.stdout && toolDef.parseOutput) {
225
+ const parsed = toolDef.parseOutput(err.stdout);
226
+ result.errors = parsed.errors || [];
227
+ result.warnings = parsed.warnings || [];
228
+ } else if (err.killed) {
229
+ // Timeout
230
+ logger.warning(`${toolDef.name} timed out after ${timeout}ms`);
231
+ result.errors.push({
232
+ file: '',
233
+ severity: 'error',
234
+ message: `${toolDef.name} timed out after ${timeout / 1000}s`
235
+ });
236
+ } else {
237
+ // Genuine execution error
238
+ const stderr = err.stderr ? err.stderr.toString().trim() : '';
239
+ logger.debug('tool-runner - runTool', `${toolDef.name} execution error`, {
240
+ exitCode: err.status,
241
+ stderr
242
+ });
243
+
244
+ // If no parseable output, treat as tool error
245
+ if (!err.stdout) {
246
+ result.errors.push({
247
+ file: '',
248
+ severity: 'error',
249
+ message: `${toolDef.name} failed: ${stderr || err.message}`
250
+ });
251
+ }
252
+ }
253
+ }
254
+
255
+ logger.debug('tool-runner - runTool', `${toolDef.name} complete`, {
256
+ errors: result.errors.length,
257
+ warnings: result.warnings.length
258
+ });
259
+
260
+ return result;
261
+ }
262
+
263
+ /**
264
+ * Run a tool's auto-fix command, then re-stage fixed files
265
+ *
266
+ * @param {ToolDefinition} toolDef - Tool definition (must have fixArgs)
267
+ * @param {string[]} files - Files to fix
268
+ * @param {Object} options - Execution options
269
+ * @param {number} [options.timeout] - Timeout in ms
270
+ * @param {string} [options.cwd] - Working directory
271
+ * @param {boolean} [options.restage] - Re-stage fixed files (default: true)
272
+ * @returns {{ fixedFiles: string[], fixedCount: number }} Fix results
273
+ */
274
+ export function runToolFix(toolDef, files, options = {}) {
275
+ const { timeout = toolDef.timeout || 30000, cwd = process.cwd(), restage = true } = options;
276
+
277
+ if (!toolDef.fixArgs) {
278
+ logger.debug('tool-runner - runToolFix', `${toolDef.name} has no fixArgs, skipping`);
279
+ return { fixedFiles: [], fixedCount: 0 };
280
+ }
281
+
282
+ const args = toolDef.fixArgs(files);
283
+ const fullCommand = [toolDef.command, ...args].join(' ');
284
+
285
+ logger.debug('tool-runner - runToolFix', `Auto-fixing with ${toolDef.name}`, {
286
+ command: fullCommand,
287
+ fileCount: files.length
288
+ });
289
+
290
+ try {
291
+ execSync(fullCommand, {
292
+ encoding: 'utf8',
293
+ timeout,
294
+ cwd,
295
+ stdio: ['pipe', 'pipe', 'pipe']
296
+ });
297
+ } catch (err) {
298
+ if (err.killed) {
299
+ logger.warning(`${toolDef.name} auto-fix timed out`);
300
+ return { fixedFiles: [], fixedCount: 0 };
301
+ }
302
+ // Distinguish "command not found" from "partial fix exited non-zero"
303
+ // Exit code 1 with stderr containing "not recognized" or "not found" = command missing
304
+ const stderr = err.stderr ? err.stderr.toString() : '';
305
+ if (err.status === 1 && !err.stdout && (
306
+ stderr.includes('not recognized') ||
307
+ stderr.includes('not found') ||
308
+ stderr.includes('ENOENT')
309
+ )) {
310
+ logger.debug('tool-runner - runToolFix', `${toolDef.name} command not available`, {
311
+ stderr: stderr.trim()
312
+ });
313
+ return { fixedFiles: [], fixedCount: 0 };
314
+ }
315
+ // Genuine partial fix — proceed to restage
316
+ logger.debug('tool-runner - runToolFix', `${toolDef.name} fix exited non-zero`, {
317
+ exitCode: err.status
318
+ });
319
+ }
320
+
321
+ // Re-stage the files that were modified by the fix
322
+ const fixedFiles = [];
323
+ if (restage) {
324
+ for (const file of files) {
325
+ try {
326
+ execSync(`git add "${file}"`, { cwd, stdio: 'pipe' });
327
+ fixedFiles.push(file);
328
+ } catch {
329
+ // File may not have changed; that's fine
330
+ }
331
+ }
332
+ }
333
+
334
+ logger.debug('tool-runner - runToolFix', `${toolDef.name} fix complete`, {
335
+ restagedFiles: fixedFiles.length
336
+ });
337
+
338
+ return { fixedFiles, fixedCount: fixedFiles.length };
339
+ }
340
+
341
+ /**
342
+ * Run a tool with auto-fix: check → fix → re-check
343
+ *
344
+ * @param {ToolDefinition} toolDef - Tool definition
345
+ * @param {string[]} files - Files to process
346
+ * @param {Object} options - Execution options
347
+ * @param {boolean} [options.autoFix] - Enable auto-fix (default: false)
348
+ * @param {number} [options.timeout] - Timeout in ms
349
+ * @param {string} [options.cwd] - Working directory
350
+ * @param {boolean} [options.restage] - Re-stage after fix (default: true)
351
+ * @returns {ToolRunResult} Final result after optional fix
352
+ */
353
+ export function runToolWithAutoFix(toolDef, files, options = {}) {
354
+ const { autoFix = false } = options;
355
+
356
+ // Step 1: Initial check
357
+ const initialResult = runTool(toolDef, files, options);
358
+
359
+ // Step 2: If any issues (errors or warnings) and autoFix enabled, try to fix
360
+ const hasIssues = initialResult.errors.length > 0 || initialResult.warnings.length > 0;
361
+ if (autoFix && toolDef.fixArgs && hasIssues) {
362
+ logger.info(`🔧 Auto-fixing ${toolDef.name} issues...`);
363
+
364
+ const fixResult = runToolFix(toolDef, files, options);
365
+
366
+ if (fixResult.fixedCount > 0) {
367
+ // Step 3: Re-check after fix
368
+ const recheck = runTool(toolDef, files, options);
369
+ recheck.fixedCount = fixResult.fixedCount;
370
+ recheck.fixedFiles = fixResult.fixedFiles;
371
+ return recheck;
372
+ }
373
+ }
374
+
375
+ return initialResult;
376
+ }
377
+
378
+ /**
379
+ * Display results for a single tool run
380
+ *
381
+ * @param {ToolRunResult} result - Tool run result
382
+ */
383
+ export function displayToolResult(result) {
384
+ if (result.skipped) {
385
+ logger.warning(`⚠️ ${result.tool} — skipped`);
386
+ if (result.skipReason) {
387
+ logger.info(` 💡 ${result.skipReason}`);
388
+ }
389
+ return;
390
+ }
391
+
392
+ if (result.errors.length === 0 && result.warnings.length === 0) {
393
+ logger.info(` ✅ ${result.tool} — no issues`);
394
+ return;
395
+ }
396
+
397
+ if (result.fixedCount > 0) {
398
+ logger.info(
399
+ ` 🔧 ${result.tool} — auto-fixed ${result.fixedCount} file(s)`
400
+ );
401
+ }
402
+
403
+ for (const issue of result.errors) {
404
+ const location = issue.file
405
+ ? `${issue.file}${issue.line ? `:${issue.line}` : ''}`
406
+ : '';
407
+ const rule = issue.ruleId ? ` (${issue.ruleId})` : '';
408
+ console.log(` ❌ ${location} ${issue.message}${rule}`);
409
+ }
410
+
411
+ for (const issue of result.warnings) {
412
+ const location = issue.file
413
+ ? `${issue.file}${issue.line ? `:${issue.line}` : ''}`
414
+ : '';
415
+ const rule = issue.ruleId ? ` (${issue.ruleId})` : '';
416
+ console.log(` ⚠️ ${location} ${issue.message}${rule}`);
417
+ }
418
+ }
package/package.json CHANGED
@@ -1,69 +1,69 @@
1
- {
2
- "name": "claude-git-hooks",
3
- "version": "2.33.0",
4
- "description": "Git hooks with Claude CLI for code analysis and automatic commit messages",
5
- "type": "module",
6
- "bin": {
7
- "claude-hooks": "./bin/claude-hooks"
8
- },
9
- "scripts": {
10
- "test": "npm run test:all",
11
- "test:all": "npm run lint && npm run test:smoke && npm run test:unit && npm run test:integration",
12
- "test:smoke": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/smoke --maxWorkers=1",
13
- "test:unit": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/unit --forceExit",
14
- "test:integration": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/integration --maxWorkers=1 --testTimeout=30000 --forceExit",
15
- "test:integration:ci": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/integration/ci-safe.test.js --maxWorkers=1 --testTimeout=30000 --forceExit",
16
- "test:changed": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/unit --changedSince=main --forceExit",
17
- "test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch",
18
- "test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
19
- "lint": "eslint lib/ bin/claude-hooks",
20
- "lint:fix": "eslint lib/ bin/claude-hooks --fix",
21
- "format": "prettier --write \"lib/**/*.js\" \"bin/**\" \"test/**/*.js\"",
22
- "precommit": "npm run lint && npm run test:smoke",
23
- "prepublishOnly": "npm run test:all"
24
- },
25
- "keywords": [
26
- "git",
27
- "hooks",
28
- "claude",
29
- "ai",
30
- "code-review",
31
- "commit-messages",
32
- "pre-commit",
33
- "automation"
34
- ],
35
- "author": "Pablo Rovito",
36
- "license": "MIT",
37
- "repository": {
38
- "type": "git",
39
- "url": "https://github.com/mscope-S-L/git-hooks.git"
40
- },
41
- "engines": {
42
- "node": ">=16.9.0"
43
- },
44
- "engineStrict": false,
45
- "os": [
46
- "darwin",
47
- "linux",
48
- "win32"
49
- ],
50
- "preferGlobal": true,
51
- "files": [
52
- "bin/",
53
- "lib/",
54
- "templates/",
55
- "README.md",
56
- "CHANGELOG.md",
57
- "CLAUDE.md",
58
- "LICENSE"
59
- ],
60
- "dependencies": {
61
- "@octokit/rest": "^21.0.0"
62
- },
63
- "devDependencies": {
64
- "@types/jest": "^29.5.0",
65
- "eslint": "^8.57.0",
66
- "jest": "^29.7.0",
67
- "prettier": "^3.2.0"
68
- }
69
- }
1
+ {
2
+ "name": "claude-git-hooks",
3
+ "version": "2.34.0",
4
+ "description": "Git hooks with Claude CLI for code analysis and automatic commit messages",
5
+ "type": "module",
6
+ "bin": {
7
+ "claude-hooks": "./bin/claude-hooks"
8
+ },
9
+ "scripts": {
10
+ "test": "npm run test:all",
11
+ "test:all": "npm run lint && npm run test:smoke && npm run test:unit && npm run test:integration",
12
+ "test:smoke": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/smoke --maxWorkers=1",
13
+ "test:unit": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/unit --forceExit",
14
+ "test:integration": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/integration --maxWorkers=1 --testTimeout=30000 --forceExit",
15
+ "test:integration:ci": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/integration/ci-safe.test.js --maxWorkers=1 --testTimeout=30000 --forceExit",
16
+ "test:changed": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/unit --changedSince=main --forceExit",
17
+ "test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch",
18
+ "test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
19
+ "lint": "eslint lib/ bin/claude-hooks",
20
+ "lint:fix": "eslint lib/ bin/claude-hooks --fix",
21
+ "format": "prettier --write \"lib/**/*.js\" \"bin/**\" \"test/**/*.js\"",
22
+ "precommit": "npm run lint && npm run test:smoke",
23
+ "prepublishOnly": "npm run test:all"
24
+ },
25
+ "keywords": [
26
+ "git",
27
+ "hooks",
28
+ "claude",
29
+ "ai",
30
+ "code-review",
31
+ "commit-messages",
32
+ "pre-commit",
33
+ "automation"
34
+ ],
35
+ "author": "Pablo Rovito",
36
+ "license": "MIT",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/mscope-S-L/git-hooks.git"
40
+ },
41
+ "engines": {
42
+ "node": ">=16.9.0"
43
+ },
44
+ "engineStrict": false,
45
+ "os": [
46
+ "darwin",
47
+ "linux",
48
+ "win32"
49
+ ],
50
+ "preferGlobal": true,
51
+ "files": [
52
+ "bin/",
53
+ "lib/",
54
+ "templates/",
55
+ "README.md",
56
+ "CHANGELOG.md",
57
+ "CLAUDE.md",
58
+ "LICENSE"
59
+ ],
60
+ "dependencies": {
61
+ "@octokit/rest": "^21.0.0"
62
+ },
63
+ "devDependencies": {
64
+ "@types/jest": "^29.5.0",
65
+ "eslint": "^8.57.1",
66
+ "jest": "^29.7.0",
67
+ "prettier": "^3.2.0"
68
+ }
69
+ }
@@ -47,6 +47,14 @@
47
47
  "documentation",
48
48
  "testing"
49
49
  ]
50
+ },
51
+
52
+ "linting": {
53
+ "enabled": true,
54
+ "autoFix": true,
55
+ "failOnError": true,
56
+ "failOnWarning": false,
57
+ "timeout": 30000
50
58
  }
51
59
  },
52
60
 
@@ -105,6 +113,36 @@
105
113
  "description": "Categories for general (review-level) comments",
106
114
  "default": "[\"ticket-alignment\", \"scope\", \"style\", \"good-practice\", \"extensibility\", \"observability\", \"documentation\", \"testing\"]",
107
115
  "use_case": "Customize which observation types appear in the review body"
116
+ },
117
+
118
+ "linting.enabled": {
119
+ "description": "Enable/disable linter execution before Claude analysis in pre-commit hook and lint command",
120
+ "default": "true",
121
+ "use_case": "Set to false to skip linting entirely"
122
+ },
123
+
124
+ "linting.autoFix": {
125
+ "description": "Automatically fix linting issues and re-stage files",
126
+ "default": "true",
127
+ "use_case": "Set to false to only report issues without fixing"
128
+ },
129
+
130
+ "linting.failOnError": {
131
+ "description": "Block commit when linter reports errors",
132
+ "default": "true",
133
+ "use_case": "Set to false to allow commits with linting errors (not recommended)"
134
+ },
135
+
136
+ "linting.failOnWarning": {
137
+ "description": "Block commit when linter reports warnings",
138
+ "default": "false",
139
+ "use_case": "Set to true for strict projects that treat warnings as errors"
140
+ },
141
+
142
+ "linting.timeout": {
143
+ "description": "Timeout in milliseconds for each linter execution",
144
+ "default": "30000",
145
+ "use_case": "Increase for large projects where linters take longer"
108
146
  }
109
147
  },
110
148