claude-git-hooks 2.33.1 → 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.
- package/CHANGELOG.md +19 -0
- package/CLAUDE.md +53 -12
- 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 +443 -0
- package/lib/utils/tool-runner.js +418 -0
- package/package.json +69 -69
- package/templates/config.advanced.example.json +38 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,25 @@ Todos los cambios notables en este proyecto se documentarán en este archivo.
|
|
|
5
5
|
El formato está basado en [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
y este proyecto adhiere a [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.34.0] - 2026-03-27
|
|
9
|
+
|
|
10
|
+
### ✨ Added
|
|
11
|
+
- Pre-commit linting with ESLint, Spotless, and sqlfluff - runs before Claude analysis with auto-fix and re-stage
|
|
12
|
+
- `lint` command - run linters on staged files, directories, or specific files (`claude-hooks lint [paths...]`)
|
|
13
|
+
- Linter availability check during installation - verifies linter presence per preset and shows install instructions
|
|
14
|
+
- Generic tool executor infrastructure (`tool-runner.js`) for linters and future formatters
|
|
15
|
+
- Linter orchestration system (`linter-runner.js`) with preset-to-linter mapping
|
|
16
|
+
- Maven availability check during installation with install instructions for backend/fullstack presets
|
|
17
|
+
|
|
18
|
+
### 🔧 Changed
|
|
19
|
+
- Pre-commit flow now runs linting before Claude analysis - fast, deterministic checks first
|
|
20
|
+
- Unfixable lint issues are forwarded to the judge for semantic resolution
|
|
21
|
+
- Updated documentation to reflect new linting workflow and command usage
|
|
22
|
+
|
|
23
|
+
### 🐛 Fixed
|
|
24
|
+
- WSL Claude CLI detection in non-default distros (#120, #121)
|
|
25
|
+
|
|
26
|
+
|
|
8
27
|
## [2.33.1] - 2026-03-26
|
|
9
28
|
|
|
10
29
|
### 🔧 Changed
|
package/CLAUDE.md
CHANGED
|
@@ -6,17 +6,19 @@
|
|
|
6
6
|
|
|
7
7
|
**Main use cases:**
|
|
8
8
|
|
|
9
|
-
1. **Pre-commit
|
|
10
|
-
2. **
|
|
11
|
-
3. **
|
|
12
|
-
4. **
|
|
13
|
-
5. **PR
|
|
14
|
-
6. **PR
|
|
15
|
-
7. **
|
|
16
|
-
8. **
|
|
17
|
-
9. **
|
|
18
|
-
10. **
|
|
19
|
-
11. **Release
|
|
9
|
+
1. **Pre-commit linting**: Runs linters (ESLint, Spotless, sqlfluff) on staged files before Claude analysis — fast, deterministic, auto-fix enabled by default
|
|
10
|
+
2. **Pre-commit analysis**: Detects security issues, bugs, and code smells before each commit (blocks on CRITICAL/BLOCKER only)
|
|
11
|
+
3. **Interactive analysis**: `claude-hooks analyze` - review all issues (INFO to BLOCKER) interactively before committing
|
|
12
|
+
4. **Automatic messages**: Write `git commit -m "auto"` and Claude generates the message in Conventional Commits format with task-id extracted from branch
|
|
13
|
+
5. **PR analysis**: `claude-hooks analyze-diff [branch]` generates title, description, and test plan for PRs
|
|
14
|
+
6. **PR review**: `claude-hooks analyze-pr <url>` analyzes a GitHub PR with preset guidelines, Linear ticket enrichment, and posts review comments
|
|
15
|
+
7. **PR creation**: `claude-hooks create-pr [branch]` creates the PR on GitHub with automatic metadata (reviewers from CODEOWNERS, labels by preset, merge strategy auto-detected from branch naming)
|
|
16
|
+
8. **Linting**: `claude-hooks lint [paths...]` runs linters on staged files, directories, or specific files — supports ESLint, Spotless, sqlfluff per preset
|
|
17
|
+
9. **Coupling detection**: `claude-hooks check-coupling` scans open PRs targeting a base branch, computes file overlap, and reports which features are coupled (share modified files) — helps TL make informed decisions before cutting a release
|
|
18
|
+
10. **Shadow management**: `claude-hooks shadow <analyze|reset|sync>` manages the shadow branch lifecycle — analyze divergence vs main and active RC, reset shadow to a clean copy of main, or sync shadow with a source branch (RC, develop, feature)
|
|
19
|
+
11. **Release creation**: `claude-hooks create-release <major|minor|patch>` creates a release-candidate branch from develop, bumps version files, commits, pushes, and deploys to shadow — replaces the 8 manual steps executed every Tuesday by the Tech Lead
|
|
20
|
+
12. **Feature revert**: `claude-hooks revert-feature <task-id>` finds a squash-merged feature commit by task ID in the current release-candidate, checks coupling with other RC features, reverts it, pushes, and optionally re-deploys shadow
|
|
21
|
+
13. **Release closure**: `claude-hooks close-release [description]` finalizes the active release-candidate — soft-resets onto main, creates a single clean commit, force-pushes, and creates a PR to main with the merge-commit strategy
|
|
20
22
|
|
|
21
23
|
## Architecture
|
|
22
24
|
|
|
@@ -57,7 +59,7 @@ claude-git-hooks/
|
|
|
57
59
|
│ ├── config.js # Config system - load/merge with priority
|
|
58
60
|
│ ├── commands/ # Command modules - one file per CLI command
|
|
59
61
|
│ │ ├── helpers.js # Shared CLI utilities - colors, output, platform
|
|
60
|
-
│ │ ├── install.js # Install command - dependencies, hooks, templates
|
|
62
|
+
│ │ ├── install.js # Install command - dependencies, hooks, templates, linter check
|
|
61
63
|
│ │ ├── hooks.js # Hook management - enable, disable, status, uninstall
|
|
62
64
|
│ │ ├── analyze-diff.js # Diff analysis - generate PR metadata from git diff
|
|
63
65
|
│ │ ├── analyze-pr.js # PR analysis - analyze GitHub PR with team guidelines
|
|
@@ -77,11 +79,14 @@ claude-git-hooks/
|
|
|
77
79
|
│ │ ├── bump-version.js # Version management - bump with commit, CHANGELOG and tags
|
|
78
80
|
│ │ ├── generate-changelog.js # CHANGELOG generation - standalone command
|
|
79
81
|
│ │ ├── diff-batch-info.js # Batch info - orchestration config + speed telemetry (v2.20.0)
|
|
82
|
+
│ │ ├── lint.js # Lint command - run linters on staged files, dirs, or files
|
|
80
83
|
│ │ └── help.js # Help, AI help, and report-issue commands
|
|
81
84
|
│ ├── hooks/ # Git hooks - Node.js implementations
|
|
82
85
|
│ │ ├── pre-commit.js # Pre-commit analysis - code quality gate
|
|
83
86
|
│ │ └── prepare-commit-msg.js # Message generation - auto commit messages
|
|
84
87
|
│ └── utils/ # Reusable modules - shared logic
|
|
88
|
+
│ ├── tool-runner.js # Generic tool executor - resolve, spawn, parse, auto-fix (v2.34.0)
|
|
89
|
+
│ ├── linter-runner.js # Linter orchestration - preset mapping, ESLint/Spotless/sqlfluff (v2.34.0)
|
|
85
90
|
│ ├── analysis-engine.js # Shared analysis logic - file data, 3-tier routing, results (v2.13.0+)
|
|
86
91
|
│ ├── diff-analysis-orchestrator.js # Intelligent batch orchestration via Opus (v2.20.0)
|
|
87
92
|
│ ├── claude-client.js # Claude CLI wrapper - spawn, retry, model override
|
|
@@ -218,6 +223,11 @@ preset config (.claude/presets/{name}/config.json) ← HIGHEST PRIORITY
|
|
|
218
223
|
| Judge timeout | 120s | Per-judge call timeout |
|
|
219
224
|
| PR analysis model | sonnet | Default, configurable via `config.prAnalysis.model` |
|
|
220
225
|
| PR analysis timeout | 300s | Per-analysis Claude call |
|
|
226
|
+
| Linting enabled | true | Runs linters before Claude analysis |
|
|
227
|
+
| Linting auto-fix | true | Auto-fix and re-stage files |
|
|
228
|
+
| Linting fail on error | true | Block commit on linting errors |
|
|
229
|
+
| Linting fail on warn | false | Do not block on warnings |
|
|
230
|
+
| Linting timeout | 30s | Per-linter timeout |
|
|
221
231
|
|
|
222
232
|
**Judge behavior (v2.20.0):**
|
|
223
233
|
|
|
@@ -299,6 +309,7 @@ consolidateResults()
|
|
|
299
309
|
| `debug.js` | Debug toggle | `runSetDebug()` |
|
|
300
310
|
| `telemetry-cmd.js` | Telemetry commands | `runShowTelemetry()`, `runClearTelemetry()` |
|
|
301
311
|
| `diff-batch-info.js` | Batch info display | `runDiffBatchInfo()` |
|
|
312
|
+
| `lint.js` | Lint command | `runLint()`, `resolvePaths()` |
|
|
302
313
|
| `help.js` | Help, AI help, report-issue | `runShowHelp()`, `showStaticHelp()`, `runShowVersion()` |
|
|
303
314
|
|
|
304
315
|
**Utility Modules (`lib/utils/`):**
|
|
@@ -307,6 +318,8 @@ consolidateResults()
|
|
|
307
318
|
| ------------------------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
308
319
|
| `lib/cli-metadata.js` | Command registry | `commands`, `buildCommandMap()`, `generateCompletionData()`, `PRESET_NAMES`, `HOOK_NAMES`, `BUMP_TYPES` |
|
|
309
320
|
| `lib/config.js` | Config system | `getConfig()` |
|
|
321
|
+
| `tool-runner.js` | Generic tool executor | `isToolAvailable()`, `filterFilesByTool()`, `runTool()`, `runToolFix()`, `runToolWithAutoFix()`, `displayToolResult()` (v2.34.0) |
|
|
322
|
+
| `linter-runner.js` | Linter orchestration | `runLinters()`, `displayLintResults()`, `checkLinterAvailability()`, `getLinterToolsForPreset()`, `LINTER_TOOLS`, `PRESET_LINTERS`, `parseEslintOutput()`, `parseSpotlessOutput()`, `parseSqlfluffOutput()`, `filesToSpotlessRegex()` (v2.34.0) |
|
|
310
323
|
| `analysis-engine.js` | Shared analysis logic | `buildFileData()`, `buildFilesData()`, `runAnalysis()`, `consolidateResults()`, `hasBlockingIssues()`, `hasAnyIssues()`, `displayResults()`, `displayIssueSummary()` (v2.13.0+) |
|
|
311
324
|
| `diff-analysis-orchestrator.js` | Intelligent batch orchestration | `orchestrateBatches()`, `buildFileOverview()`, `detectDependencies()` (v2.20.0) |
|
|
312
325
|
| `claude-client.js` | Claude CLI wrapper | `analyzeCode()`, `executeClaudeWithRetry()`, `extractJSON()` — spawn, retry, model override |
|
|
@@ -347,15 +360,36 @@ consolidateResults()
|
|
|
347
360
|
8. **Singleton Pattern**: `config.js` loads configuration once per execution
|
|
348
361
|
9. **Guard Pattern**: `authorization.js` — fail-closed gate in `bin/claude-hooks` before command dispatch; static `PROTECTED_COMMANDS` set avoids API calls for unprotected commands; permissions sourced from `mscope-S-L/git-hooks-config/permissions.json`
|
|
349
362
|
10. **Remote Config Pattern**: `remote-config.js` — fetches JSON from `mscope-S-L/git-hooks-config`, caches per-process (including nulls), graceful degradation (warn + return null); `label-resolver.js` — callers receive config via dependency injection and decide fallback
|
|
363
|
+
11. **Pipeline Pattern**: `tool-runner.js` + `linter-runner.js` — generic tool execution infrastructure; linters (and future formatters) share the same resolve → spawn → parse → fix → re-stage pipeline. Tool definitions are data objects, not classes.
|
|
350
364
|
|
|
351
365
|
### Key Data Flows
|
|
352
366
|
|
|
367
|
+
**Flow 0: Pre-commit linting (v2.34.0)**
|
|
368
|
+
|
|
369
|
+
```
|
|
370
|
+
git commit
|
|
371
|
+
→ hook reads staged files
|
|
372
|
+
→ filters by preset extensions + size
|
|
373
|
+
↓
|
|
374
|
+
→ LINTING STEP (fast, deterministic)
|
|
375
|
+
→ getLinterToolsForPreset(presetName) → applicable linters
|
|
376
|
+
→ for each linter:
|
|
377
|
+
isToolAvailable() → not found? warn + install hint → skip
|
|
378
|
+
filterFilesByTool() → matching files
|
|
379
|
+
runToolWithAutoFix() → check → auto-fix → re-stage → re-check
|
|
380
|
+
→ if failOnError && errors remain → exit 1 (COMMIT BLOCKED)
|
|
381
|
+
→ if failOnWarning && warnings → exit 1 (COMMIT BLOCKED)
|
|
382
|
+
↓
|
|
383
|
+
→ continues to Claude analysis
|
|
384
|
+
```
|
|
385
|
+
|
|
353
386
|
**Flow 1: Pre-commit with blocking**
|
|
354
387
|
|
|
355
388
|
```
|
|
356
389
|
git commit
|
|
357
390
|
→ hook reads staged files
|
|
358
391
|
→ filters by preset extensions
|
|
392
|
+
→ [linting step — see Flow 0]
|
|
359
393
|
→ builds prompt with diff
|
|
360
394
|
→ Claude analyzes → detects issues
|
|
361
395
|
→ judge evaluates ALL issues (any severity):
|
|
@@ -1107,6 +1141,13 @@ claude-hooks presets # List available presets
|
|
|
1107
1141
|
claude-hooks --set-preset backend # Change preset
|
|
1108
1142
|
claude-hooks preset current # View current preset
|
|
1109
1143
|
|
|
1144
|
+
# Linting
|
|
1145
|
+
claude-hooks lint # Lint staged files
|
|
1146
|
+
claude-hooks lint src/ # Lint all files in directory
|
|
1147
|
+
claude-hooks lint src/ lib/utils/ # Multiple directories
|
|
1148
|
+
claude-hooks lint file1.js file2.js # Specific files
|
|
1149
|
+
claude-hooks lint src/ file.js lib/ # Mix of dirs and files
|
|
1150
|
+
|
|
1110
1151
|
# Analysis and PRs
|
|
1111
1152
|
claude-hooks analyze-diff [branch] # Analyze diff for PR
|
|
1112
1153
|
claude-hooks analyze-pr <pr-url> # Analyze GitHub PR with team guidelines
|
package/bin/claude-hooks
CHANGED
package/lib/cli-metadata.js
CHANGED
|
@@ -97,6 +97,15 @@ export const commands = [
|
|
|
97
97
|
description: 'Show hook status',
|
|
98
98
|
handler: async () => (await import('./commands/hooks.js')).runStatus
|
|
99
99
|
},
|
|
100
|
+
{
|
|
101
|
+
name: 'lint',
|
|
102
|
+
description: 'Run linters on staged files, directories, or specific files',
|
|
103
|
+
handler: async () => (await import('./commands/lint.js')).runLint,
|
|
104
|
+
args: {
|
|
105
|
+
name: 'paths',
|
|
106
|
+
completion: "find . -maxdepth 3 -type d -not -path '*/\\.*' -not -path '*/node_modules/*'"
|
|
107
|
+
}
|
|
108
|
+
},
|
|
100
109
|
{
|
|
101
110
|
name: 'analyze',
|
|
102
111
|
description: 'Analyze code interactively before committing',
|
package/lib/commands/install.js
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
} from './helpers.js';
|
|
33
33
|
import { runSetupGitHub } from './setup-github.js';
|
|
34
34
|
import { generateCompletionData } from '../cli-metadata.js';
|
|
35
|
+
import { getConfig } from '../config.js';
|
|
35
36
|
|
|
36
37
|
/**
|
|
37
38
|
* Function to check version (used by hooks)
|
|
@@ -186,6 +187,17 @@ async function checkAndInstallDependencies(skipAuth = false) {
|
|
|
186
187
|
error('npm is not installed.');
|
|
187
188
|
}
|
|
188
189
|
|
|
190
|
+
// Check Maven (optional — needed for backend/fullstack presets with Spotless)
|
|
191
|
+
try {
|
|
192
|
+
const mvnVersion = execSync('mvn --version', { encoding: 'utf8', timeout: 5000 })
|
|
193
|
+
.split('\n')[0].trim();
|
|
194
|
+
success(`${mvnVersion}`);
|
|
195
|
+
} catch {
|
|
196
|
+
warning('Maven (mvn) not found — required for Spotless linting in backend/fullstack presets');
|
|
197
|
+
info(' Install: https://maven.apache.org/install.html');
|
|
198
|
+
info(' Or via package manager: brew install maven / choco install maven / apt install maven');
|
|
199
|
+
}
|
|
200
|
+
|
|
189
201
|
// v2.0.0+: jq and curl are no longer needed (pure Node.js implementation)
|
|
190
202
|
|
|
191
203
|
// Check Git
|
|
@@ -655,6 +667,18 @@ export async function runInstall(args) {
|
|
|
655
667
|
// Install shell completions
|
|
656
668
|
installCompletions();
|
|
657
669
|
|
|
670
|
+
// Check linter toolchain availability for the selected preset
|
|
671
|
+
try {
|
|
672
|
+
const config = await getConfig();
|
|
673
|
+
const presetName = config.preset || 'default';
|
|
674
|
+
if (config.linting?.enabled !== false) {
|
|
675
|
+
const { checkLinterAvailability } = await import('../utils/linter-runner.js');
|
|
676
|
+
checkLinterAvailability(presetName);
|
|
677
|
+
}
|
|
678
|
+
} catch {
|
|
679
|
+
// Non-fatal — linter check failure should not block installation
|
|
680
|
+
}
|
|
681
|
+
|
|
658
682
|
success('Claude Git Hooks installed successfully! 🎉');
|
|
659
683
|
console.log('\nRun claude-hooks --help to see all available commands.');
|
|
660
684
|
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: lint.js
|
|
3
|
+
* Purpose: CLI command to run linters on files or directories
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* claude-hooks lint # lint staged files
|
|
7
|
+
* claude-hooks lint src/ # lint all files in src/
|
|
8
|
+
* claude-hooks lint src/ lib/utils/ # multiple directories
|
|
9
|
+
* claude-hooks lint file1.js file2.java # specific files
|
|
10
|
+
* claude-hooks lint src/ file3.js lib/ # mix of dirs and files
|
|
11
|
+
*
|
|
12
|
+
* Path resolution (like git add):
|
|
13
|
+
* - Directories → walk and collect files matching preset extensions
|
|
14
|
+
* - Files → use directly
|
|
15
|
+
* - No args → fall back to staged files
|
|
16
|
+
*
|
|
17
|
+
* Dependencies:
|
|
18
|
+
* - linter-runner: Linter orchestration
|
|
19
|
+
* - git-operations: Staged files, repo root
|
|
20
|
+
* - file-utils: Directory walking
|
|
21
|
+
* - preset-loader: Extension filtering
|
|
22
|
+
* - config: Linting configuration
|
|
23
|
+
* - helpers: checkGitRepo, colors
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import fs from 'fs';
|
|
27
|
+
import path from 'path';
|
|
28
|
+
import { checkGitRepo, error, info } from './helpers.js';
|
|
29
|
+
import { getConfig } from '../config.js';
|
|
30
|
+
import { loadPreset } from '../utils/preset-loader.js';
|
|
31
|
+
import { getStagedFiles, getRepoRoot } from '../utils/git-operations.js';
|
|
32
|
+
import { runLinters, displayLintResults } from '../utils/linter-runner.js';
|
|
33
|
+
import logger from '../utils/logger.js';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Resolve user-provided paths into a flat list of files
|
|
37
|
+
* Handles directories (walked recursively), individual files, and mixtures.
|
|
38
|
+
*
|
|
39
|
+
* @param {string[]} paths - User-provided paths (files and/or directories)
|
|
40
|
+
* @param {string[]} extensions - Allowed file extensions from preset
|
|
41
|
+
* @param {string} repoRoot - Repository root for relative path resolution
|
|
42
|
+
* @returns {string[]} Resolved file paths (relative to cwd)
|
|
43
|
+
*/
|
|
44
|
+
export function resolvePaths(paths, extensions, _repoRoot) {
|
|
45
|
+
const files = new Set();
|
|
46
|
+
const extSet = new Set(extensions.map((e) => e.toLowerCase()));
|
|
47
|
+
|
|
48
|
+
for (const userPath of paths) {
|
|
49
|
+
const resolved = path.resolve(userPath);
|
|
50
|
+
|
|
51
|
+
if (!fs.existsSync(resolved)) {
|
|
52
|
+
logger.warning(`Path not found: ${userPath}`);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const stat = fs.statSync(resolved);
|
|
57
|
+
|
|
58
|
+
if (stat.isFile()) {
|
|
59
|
+
// Accept file if it matches preset extensions, or if no extensions filter
|
|
60
|
+
const ext = path.extname(resolved).toLowerCase();
|
|
61
|
+
if (extSet.size === 0 || extSet.has(ext)) {
|
|
62
|
+
files.add(path.relative(process.cwd(), resolved));
|
|
63
|
+
} else {
|
|
64
|
+
logger.debug('lint - resolvePaths', `Skipping ${userPath}: extension ${ext} not in preset`);
|
|
65
|
+
}
|
|
66
|
+
} else if (stat.isDirectory()) {
|
|
67
|
+
// Walk directory and collect matching files
|
|
68
|
+
_walkForFiles(resolved, extSet, files);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return Array.from(files);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Recursively walk a directory collecting files that match extensions
|
|
77
|
+
*
|
|
78
|
+
* @param {string} dir - Directory to walk
|
|
79
|
+
* @param {Set<string>} extSet - Allowed extensions
|
|
80
|
+
* @param {Set<string>} files - Accumulator set of relative file paths
|
|
81
|
+
* @param {number} [depth] - Current depth (max 10)
|
|
82
|
+
*/
|
|
83
|
+
function _walkForFiles(dir, extSet, files, depth = 0) {
|
|
84
|
+
if (depth > 10) return;
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
88
|
+
|
|
89
|
+
for (const entry of entries) {
|
|
90
|
+
// Skip hidden dirs, node_modules, target, build
|
|
91
|
+
if (
|
|
92
|
+
entry.name.startsWith('.') ||
|
|
93
|
+
entry.name === 'node_modules' ||
|
|
94
|
+
entry.name === 'target' ||
|
|
95
|
+
entry.name === 'build' ||
|
|
96
|
+
entry.name === 'dist'
|
|
97
|
+
) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const fullPath = path.join(dir, entry.name);
|
|
102
|
+
|
|
103
|
+
if (entry.isDirectory()) {
|
|
104
|
+
_walkForFiles(fullPath, extSet, files, depth + 1);
|
|
105
|
+
} else if (entry.isFile()) {
|
|
106
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
107
|
+
if (extSet.size === 0 || extSet.has(ext)) {
|
|
108
|
+
files.add(path.relative(process.cwd(), fullPath));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
} catch (err) {
|
|
113
|
+
logger.debug('lint - _walkForFiles', `Cannot read directory: ${dir}`, {
|
|
114
|
+
error: err.message
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Main lint command handler
|
|
121
|
+
*
|
|
122
|
+
* @param {string[]} args - CLI arguments (paths and flags)
|
|
123
|
+
*/
|
|
124
|
+
export async function runLint(args = []) {
|
|
125
|
+
if (!checkGitRepo()) {
|
|
126
|
+
error('Not a git repository');
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const config = await getConfig();
|
|
131
|
+
|
|
132
|
+
if (config.system?.debug) {
|
|
133
|
+
logger.setDebugMode(true);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (config.linting?.enabled === false) {
|
|
137
|
+
info('Linting is disabled in configuration');
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const repoRoot = getRepoRoot();
|
|
142
|
+
const presetName = config.preset || 'default';
|
|
143
|
+
const { metadata } = await loadPreset(presetName);
|
|
144
|
+
|
|
145
|
+
// Separate flags from paths
|
|
146
|
+
const paths = args.filter((a) => !a.startsWith('--'));
|
|
147
|
+
|
|
148
|
+
let filesToLint;
|
|
149
|
+
|
|
150
|
+
if (paths.length === 0) {
|
|
151
|
+
// No paths → lint staged files
|
|
152
|
+
info('No paths specified — linting staged files');
|
|
153
|
+
const stagedFiles = getStagedFiles({ extensions: metadata.fileExtensions });
|
|
154
|
+
|
|
155
|
+
if (stagedFiles.length === 0) {
|
|
156
|
+
info('No staged files to lint');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
filesToLint = stagedFiles;
|
|
161
|
+
} else {
|
|
162
|
+
// Resolve user-provided paths
|
|
163
|
+
filesToLint = resolvePaths(paths, metadata.fileExtensions, repoRoot);
|
|
164
|
+
|
|
165
|
+
if (filesToLint.length === 0) {
|
|
166
|
+
info('No matching files found in specified paths');
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
info(`🎯 Linting ${filesToLint.length} file(s) with '${metadata.displayName}' preset`);
|
|
172
|
+
|
|
173
|
+
const lintResult = runLinters(filesToLint, config, presetName);
|
|
174
|
+
displayLintResults(lintResult);
|
|
175
|
+
|
|
176
|
+
// Exit with error code if linting failed
|
|
177
|
+
const failOnError = config.linting?.failOnError !== false;
|
|
178
|
+
const failOnWarning = config.linting?.failOnWarning === true;
|
|
179
|
+
|
|
180
|
+
if (failOnError && lintResult.totalErrors > 0) {
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (failOnWarning && lintResult.totalWarnings > 0) {
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
}
|
package/lib/config.js
CHANGED
|
@@ -84,6 +84,13 @@ const HARDCODED = {
|
|
|
84
84
|
'documentation',
|
|
85
85
|
'testing'
|
|
86
86
|
]
|
|
87
|
+
},
|
|
88
|
+
linting: {
|
|
89
|
+
enabled: true, // Run linters before Claude analysis
|
|
90
|
+
autoFix: true, // Auto-fix and re-stage
|
|
91
|
+
failOnError: true, // Block commit on linting errors
|
|
92
|
+
failOnWarning: false, // Do not block on warnings
|
|
93
|
+
timeout: 30000 // 30s per linter
|
|
87
94
|
}
|
|
88
95
|
};
|
|
89
96
|
|
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 = 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++;
|