claude-git-hooks 2.1.0 → 2.3.1

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.
Files changed (52) hide show
  1. package/CHANGELOG.md +240 -0
  2. package/README.md +280 -78
  3. package/bin/claude-hooks +295 -119
  4. package/lib/config.js +164 -0
  5. package/lib/hooks/pre-commit.js +180 -67
  6. package/lib/hooks/prepare-commit-msg.js +47 -41
  7. package/lib/utils/claude-client.js +107 -16
  8. package/lib/utils/claude-diagnostics.js +266 -0
  9. package/lib/utils/file-operations.js +1 -65
  10. package/lib/utils/file-utils.js +65 -0
  11. package/lib/utils/installation-diagnostics.js +145 -0
  12. package/lib/utils/package-info.js +75 -0
  13. package/lib/utils/preset-loader.js +214 -0
  14. package/lib/utils/prompt-builder.js +83 -67
  15. package/lib/utils/resolution-prompt.js +12 -2
  16. package/package.json +49 -50
  17. package/templates/ANALYZE_DIFF.md +33 -0
  18. package/templates/COMMIT_MESSAGE.md +24 -0
  19. package/templates/CUSTOMIZATION_GUIDE.md +656 -0
  20. package/templates/SUBAGENT_INSTRUCTION.md +1 -0
  21. package/templates/config.example.json +41 -0
  22. package/templates/pre-commit +40 -2
  23. package/templates/prepare-commit-msg +40 -2
  24. package/templates/presets/ai/ANALYSIS_PROMPT.md +133 -0
  25. package/templates/presets/ai/PRE_COMMIT_GUIDELINES.md +176 -0
  26. package/templates/presets/ai/config.json +12 -0
  27. package/templates/presets/ai/preset.json +42 -0
  28. package/templates/presets/backend/ANALYSIS_PROMPT.md +85 -0
  29. package/templates/presets/backend/PRE_COMMIT_GUIDELINES.md +87 -0
  30. package/templates/presets/backend/config.json +12 -0
  31. package/templates/presets/backend/preset.json +49 -0
  32. package/templates/presets/database/ANALYSIS_PROMPT.md +114 -0
  33. package/templates/presets/database/PRE_COMMIT_GUIDELINES.md +143 -0
  34. package/templates/presets/database/config.json +12 -0
  35. package/templates/presets/database/preset.json +38 -0
  36. package/templates/presets/default/config.json +12 -0
  37. package/templates/presets/default/preset.json +53 -0
  38. package/templates/presets/frontend/ANALYSIS_PROMPT.md +99 -0
  39. package/templates/presets/frontend/PRE_COMMIT_GUIDELINES.md +95 -0
  40. package/templates/presets/frontend/config.json +12 -0
  41. package/templates/presets/frontend/preset.json +50 -0
  42. package/templates/presets/fullstack/ANALYSIS_PROMPT.md +107 -0
  43. package/templates/presets/fullstack/CONSISTENCY_CHECKS.md +147 -0
  44. package/templates/presets/fullstack/PRE_COMMIT_GUIDELINES.md +125 -0
  45. package/templates/presets/fullstack/config.json +12 -0
  46. package/templates/presets/fullstack/preset.json +55 -0
  47. package/templates/shared/ANALYSIS_PROMPT.md +103 -0
  48. package/templates/shared/ANALYZE_DIFF.md +33 -0
  49. package/templates/shared/COMMIT_MESSAGE.md +24 -0
  50. package/templates/shared/PRE_COMMIT_GUIDELINES.md +145 -0
  51. package/templates/shared/RESOLUTION_PROMPT.md +32 -0
  52. package/templates/check-version.sh +0 -266
package/lib/config.js ADDED
@@ -0,0 +1,164 @@
1
+ /**
2
+ * File: config.js
3
+ * Purpose: Centralized configuration management
4
+ *
5
+ * Priority: preset config > .claude/config.json > defaults
6
+ *
7
+ * Key features:
8
+ * - Single source of truth for all configurable values
9
+ * - No environment variables (except OS for platform detection)
10
+ * - Preset-aware (allowedExtensions come from preset templates)
11
+ * - User config (.claude/config.json) provides general preferences per project
12
+ * - Preset config provides tech-stack-specific overrides (highest priority)
13
+ */
14
+
15
+ import fs from 'fs';
16
+ import path from 'path';
17
+ import { fileURLToPath } from 'url';
18
+
19
+ const __filename = fileURLToPath(import.meta.url);
20
+ const __dirname = path.dirname(__filename);
21
+
22
+ /**
23
+ * Default configuration
24
+ * All values can be overridden via .claude/config.json
25
+ */
26
+ const defaults = {
27
+ // Analysis configuration
28
+ analysis: {
29
+ maxFileSize: 100000, // 100KB - max file size to analyze
30
+ maxFiles: 10, // Max number of files per commit
31
+ timeout: 120000, // 2 minutes - Claude analysis timeout
32
+ contextLines: 3, // Git diff context lines
33
+ ignoreExtensions: [], // Extensions to exclude (e.g., ['.min.js', '.lock'])
34
+ // NOTE: allowedExtensions comes from preset/template, not here
35
+ },
36
+
37
+ // Commit message generation
38
+ commitMessage: {
39
+ autoKeyword: 'auto', // Keyword to trigger auto-generation
40
+ timeout: 180000,
41
+ },
42
+
43
+ // Subagent configuration (parallel analysis)
44
+ subagents: {
45
+ enabled: true, // Enable parallel file analysis
46
+ model: 'haiku', // haiku (fast), sonnet (balanced), opus (thorough)
47
+ batchSize: 3, // Parallel subagents per batch
48
+ },
49
+
50
+ // Template paths
51
+ templates: {
52
+ baseDir: '.claude', // Base directory for all templates
53
+ analysis: 'CLAUDE_ANALYSIS_PROMPT_SONAR.md', // Pre-commit analysis prompt
54
+ guidelines: 'CLAUDE_PRE_COMMIT_SONAR.md', // Analysis guidelines
55
+ commitMessage: 'COMMIT_MESSAGE.md', // Commit message prompt
56
+ analyzeDiff: 'ANALYZE_DIFF.md', // PR analysis prompt
57
+ resolution: 'CLAUDE_RESOLUTION_PROMPT.md', // Issue resolution prompt
58
+ subagentInstruction: 'SUBAGENT_INSTRUCTION.md', // Parallel analysis instruction
59
+ },
60
+
61
+ // Output file paths (relative to repo root)
62
+ output: {
63
+ outputDir: '.claude/out', // Output directory for all generated files
64
+ debugFile: '.claude/out/debug-claude-response.json', // Debug response dump
65
+ resolutionFile: '.claude/out/claude_resolution_prompt.md', // Generated resolution prompt
66
+ prAnalysisFile: '.claude/out/pr-analysis.json', // PR analysis result
67
+ },
68
+
69
+ // System behavior
70
+ system: {
71
+ debug: false, // Enable debug logging and file dumps
72
+ wslCheckTimeout: 3000, // 3 seconds - quick WSL availability check
73
+ },
74
+
75
+ // Git operations
76
+ git: {
77
+ diffFilter: 'ACM', // Added, Copied, Modified (excludes Deleted)
78
+ },
79
+ };
80
+
81
+ /**
82
+ * Loads user configuration from .claude/config.json
83
+ * Merges with defaults and preset config (preset takes highest priority)
84
+ *
85
+ * Priority: defaults < user config < preset config
86
+ *
87
+ * @param {string} baseDir - Base directory to search for config (default: cwd)
88
+ * @returns {Promise<Object>} Merged configuration
89
+ */
90
+ const loadUserConfig = async (baseDir = process.cwd()) => {
91
+ const configPath = path.join(baseDir, '.claude', 'config.json');
92
+
93
+ let userConfig = {};
94
+ try {
95
+ if (fs.existsSync(configPath)) {
96
+ userConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
97
+ }
98
+ } catch (error) {
99
+ console.warn(`āš ļø Warning: Could not load .claude/config.json: ${error.message}`);
100
+ console.warn('Using default configuration');
101
+ }
102
+
103
+ // Load preset if specified
104
+ let presetConfig = {};
105
+ if (userConfig.preset) {
106
+ try {
107
+ // Dynamic import to avoid circular dependency
108
+ const { loadPreset } = await import('./utils/preset-loader.js');
109
+ const { config } = await loadPreset(userConfig.preset);
110
+ presetConfig = config;
111
+ } catch (error) {
112
+ console.warn(`āš ļø Warning: Preset "${userConfig.preset}" not found, using defaults`);
113
+ }
114
+ }
115
+
116
+ // Merge: defaults < user < preset
117
+ return deepMerge(deepMerge(defaults, userConfig), presetConfig);
118
+ };
119
+
120
+ /**
121
+ * Deep merge two objects (user config overrides defaults)
122
+ *
123
+ * @param {Object} target - Target object (defaults)
124
+ * @param {Object} source - Source object (user overrides)
125
+ * @returns {Object} Merged object
126
+ */
127
+ const deepMerge = (target, source) => {
128
+ const result = { ...target };
129
+
130
+ for (const key in source) {
131
+ if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
132
+ result[key] = deepMerge(target[key] || {}, source[key]);
133
+ } else {
134
+ result[key] = source[key];
135
+ }
136
+ }
137
+
138
+ return result;
139
+ };
140
+
141
+ /**
142
+ * Get configuration instance
143
+ * Loads once, caches result
144
+ *
145
+ * Usage:
146
+ * import { getConfig } from './lib/config.js';
147
+ * const config = await getConfig();
148
+ * const maxFiles = config.analysis.maxFiles;
149
+ */
150
+ let configInstance = null;
151
+
152
+ const getConfig = async () => {
153
+ if (!configInstance) {
154
+ configInstance = await loadUserConfig();
155
+ }
156
+ return configInstance;
157
+ };
158
+
159
+ // Export async loader
160
+ export { getConfig, loadUserConfig, defaults };
161
+
162
+ // Export a sync default for backwards compatibility (loads without preset)
163
+ // NOTE: This will be deprecated - prefer using getConfig() directly
164
+ export default defaults;
@@ -30,57 +30,28 @@ import {
30
30
  filterSkipAnalysis,
31
31
  readFile
32
32
  } from '../utils/file-operations.js';
33
- import { analyzeCode } from '../utils/claude-client.js';
33
+ import { analyzeCode, analyzeCodeParallel, chunkArray } from '../utils/claude-client.js';
34
34
  import { buildAnalysisPrompt } from '../utils/prompt-builder.js';
35
35
  import {
36
36
  generateResolutionPrompt,
37
37
  shouldGeneratePrompt
38
38
  } from '../utils/resolution-prompt.js';
39
+ import { loadPreset } from '../utils/preset-loader.js';
40
+ import { getVersion, calculateBatches } from '../utils/package-info.js';
39
41
  import logger from '../utils/logger.js';
42
+ import { getConfig } from '../config.js';
40
43
 
41
- // Configuration constants
42
44
  /**
43
- * TOKEN USAGE ANALYSIS (200k token context window)
45
+ * Configuration loaded from lib/config.js
46
+ * Override via .claude/config.json
44
47
  *
45
- * Conversión aproximada: 1 token ā‰ˆ 4 caracteres, 1 KB ā‰ˆ 256 tokens
46
- *
47
- * Overhead fijo por anƔlisis:
48
- * - Template anĆ”lisis: 2KB ā‰ˆ 512 tokens
49
- * - Guidelines: 2KB ā‰ˆ 512 tokens
50
- * - Metadata: 0.2KB ā‰ˆ 50 tokens
51
- * - Total overhead: 4.2KB ā‰ˆ 1,074 tokens
52
- *
53
- * Escenarios con lĆ­mites actuales:
54
- *
55
- * 1) 10 archivos modificados (solo diffs):
56
- * - Diff promedio: 4KB/archivo Ɨ 10 = 40KB ā‰ˆ 10,240 tokens
57
- * - Total: 44.2KB ā‰ˆ 11,314 tokens (5.6% de 200k) āœ…
58
- *
59
- * 2) 10 archivos nuevos (contenido completo):
60
- * - Archivo promedio: 30KB Ɨ 10 = 300KB ā‰ˆ 76,800 tokens
61
- * - Total: 304.2KB ā‰ˆ 77,874 tokens (38.9% de 200k) āœ…
62
- *
63
- * 3) MĆ”ximo permitido (10 Ɨ 100KB):
64
- * - Total: 1,004KB ā‰ˆ 257,074 tokens (128.5% de 200k) āš ļø EXCEDE
65
- *
66
- * Con SUBAGENTES (anƔlisis paralelo):
67
- * - Cada archivo analizado independientemente con su propio overhead
68
- * - Batch size default: 3 archivos en paralelo
69
- * - Uso mĆ”ximo por subagente: ~26,265 tokens (13% de 200k) āœ…
70
- *
71
- * Recomendaciones:
72
- * - LĆ­mites actuales seguros para archivos modificados (diffs)
73
- * - Con subagentes: escala bien hasta 15-20 archivos
74
- * - Sin subagentes: limitar a 5-7 archivos grandes
75
- * - Considerar MAX_NEW_FILE_SIZE menor si se analizan muchos archivos nuevos
48
+ * TOKEN USAGE ANALYSIS (200k context window):
49
+ * - 1 token ā‰ˆ 4 chars, 1KB ā‰ˆ 256 tokens
50
+ * - Overhead: template(512) + guidelines(512) + metadata(50) ā‰ˆ 1,074 tokens
51
+ * - 10 modified files (diffs): ~11,314 tokens (5.6%) āœ…
52
+ * - 10 new files (100KB each): ~257,074 tokens (128%) āš ļø EXCEEDS
53
+ * - With subagents (batch=3): ~26,265 tokens per batch (13%) āœ…
76
54
  */
77
- const CONFIG = {
78
- MAX_FILE_SIZE: 100000, // 100KB
79
- MAX_FILES: 10,
80
- ALLOWED_EXTENSIONS: ['.java', '.xml', '.properties', '.yml', '.yaml'],
81
- TEMPLATE_NAME: 'CLAUDE_ANALYSIS_PROMPT_SONAR.md',
82
- GUIDELINES_NAME: 'CLAUDE_PRE_COMMIT_SONAR.md'
83
- };
84
55
 
85
56
  /**
86
57
  * Displays SonarQube-style analysis results
@@ -150,6 +121,56 @@ const displayResults = (result) => {
150
121
  }
151
122
  };
152
123
 
124
+ /**
125
+ * Consolidates multiple analysis results into one
126
+ * @param {Array<Object>} results - Array of analysis results
127
+ * @returns {Object} - Consolidated result
128
+ */
129
+ const consolidateResults = (results) => {
130
+ const consolidated = {
131
+ QUALITY_GATE: 'PASSED',
132
+ approved: true,
133
+ score: 10,
134
+ metrics: { reliability: 'A', security: 'A', maintainability: 'A', coverage: 100, duplications: 0, complexity: 0 },
135
+ issues: { blocker: 0, critical: 0, major: 0, minor: 0, info: 0 },
136
+ details: [],
137
+ blockingIssues: [],
138
+ securityHotspots: 0
139
+ };
140
+
141
+ for (const result of results) {
142
+ // Worst-case quality gate
143
+ if (result.QUALITY_GATE === 'FAILED') consolidated.QUALITY_GATE = 'FAILED';
144
+ if (result.approved === false) consolidated.approved = false;
145
+ if (result.score < consolidated.score) consolidated.score = result.score;
146
+
147
+ // Worst-case metrics
148
+ if (result.metrics) {
149
+ const metricOrder = { 'A': 5, 'B': 4, 'C': 3, 'D': 2, 'E': 1 };
150
+ ['reliability', 'security', 'maintainability'].forEach(m => {
151
+ const current = metricOrder[consolidated.metrics[m]] || 5;
152
+ const incoming = metricOrder[result.metrics[m]] || 5;
153
+ if (incoming < current) consolidated.metrics[m] = result.metrics[m];
154
+ });
155
+ if (result.metrics.coverage !== undefined) consolidated.metrics.coverage = Math.min(consolidated.metrics.coverage, result.metrics.coverage);
156
+ if (result.metrics.duplications !== undefined) consolidated.metrics.duplications = Math.max(consolidated.metrics.duplications, result.metrics.duplications);
157
+ if (result.metrics.complexity !== undefined) consolidated.metrics.complexity = Math.max(consolidated.metrics.complexity, result.metrics.complexity);
158
+ }
159
+
160
+ // Sum issue counts
161
+ if (result.issues) {
162
+ Object.keys(consolidated.issues).forEach(s => consolidated.issues[s] += (result.issues[s] || 0));
163
+ }
164
+
165
+ // Merge arrays
166
+ if (Array.isArray(result.details)) consolidated.details.push(...result.details);
167
+ if (Array.isArray(result.blockingIssues)) consolidated.blockingIssues.push(...result.blockingIssues);
168
+ if (result.securityHotspots) consolidated.securityHotspots += result.securityHotspots;
169
+ }
170
+
171
+ return consolidated;
172
+ };
173
+
153
174
  /**
154
175
  * Main pre-commit hook execution
155
176
  */
@@ -157,18 +178,43 @@ const main = async () => {
157
178
  const startTime = Date.now();
158
179
 
159
180
  try {
181
+ // Load configuration
182
+ const config = await getConfig();
183
+
184
+ // Display configuration info
185
+ const version = await getVersion();
186
+ console.log(`\nšŸ¤– claude-git-hooks v${version}`);
187
+
160
188
  logger.info('Starting code quality analysis...');
161
189
 
162
190
  logger.debug(
163
191
  'pre-commit - main',
164
192
  'Configuration',
165
- { ...CONFIG }
193
+ { ...config }
166
194
  );
167
195
 
168
- // Step 1: Get staged files
196
+ // Load active preset
197
+ const presetName = config.preset || 'default';
198
+ const { metadata } = await loadPreset(presetName);
199
+
200
+ logger.info(`šŸŽÆ Analyzing with '${metadata.displayName}' preset`);
201
+ logger.debug(
202
+ 'pre-commit - main',
203
+ 'Preset loaded',
204
+ {
205
+ preset: presetName,
206
+ fileExtensions: metadata.fileExtensions,
207
+ techStack: metadata.techStack
208
+ }
209
+ );
210
+
211
+ // Use preset's file extensions
212
+ const allowedExtensions = metadata.fileExtensions;
213
+
214
+ // Step 1: Get staged files with preset extensions
169
215
  logger.debug('pre-commit - main', 'Getting staged files');
170
216
  const stagedFiles = getStagedFiles({
171
- extensions: CONFIG.ALLOWED_EXTENSIONS
217
+ extensions: allowedExtensions
172
218
  });
173
219
 
174
220
  if (stagedFiles.length === 0) {
@@ -177,12 +223,17 @@ const main = async () => {
177
223
  }
178
224
 
179
225
  logger.info(`Files to review: ${stagedFiles.length}`);
226
+ logger.debug(
227
+ 'pre-commit - main',
228
+ 'Files matched by preset',
229
+ { count: stagedFiles.length, extensions: allowedExtensions }
230
+ );
180
231
 
181
232
  // Step 2: Filter files by size
182
233
  logger.debug('pre-commit - main', 'Filtering files by size');
183
234
  const filteredFiles = await filterFiles(stagedFiles, {
184
- maxSize: CONFIG.MAX_FILE_SIZE,
185
- extensions: CONFIG.ALLOWED_EXTENSIONS
235
+ maxSize: config.analysis.maxFileSize,
236
+ extensions: allowedExtensions
186
237
  });
187
238
 
188
239
  const validFiles = filteredFiles.filter(f => f.valid);
@@ -192,7 +243,7 @@ const main = async () => {
192
243
  process.exit(0);
193
244
  }
194
245
 
195
- if (validFiles.length > CONFIG.MAX_FILES) {
246
+ if (validFiles.length > config.analysis.maxFiles) {
196
247
  logger.warning(`Too many files to review (${validFiles.length})`);
197
248
  logger.warning('Consider splitting the commit into smaller parts');
198
249
  process.exit(0);
@@ -245,29 +296,91 @@ const main = async () => {
245
296
 
246
297
  // Step 4: Build analysis prompt
247
298
  logger.info(`Sending ${filesData.length} files for review...`);
248
- logger.debug('pre-commit - main', 'Building analysis prompt');
249
-
250
- const prompt = await buildAnalysisPrompt({
251
- templateName: CONFIG.TEMPLATE_NAME,
252
- guidelinesName: CONFIG.GUIDELINES_NAME,
253
- files: filesData,
254
- metadata: {
255
- REPO_NAME: getRepoName(),
256
- BRANCH_NAME: getCurrentBranch()
299
+
300
+ // Display subagent configuration
301
+ const subagentsEnabled = config.subagents?.enabled || false;
302
+ const subagentModel = config.subagents?.model || 'haiku';
303
+ const batchSize = config.subagents?.batchSize || 3;
304
+
305
+ if (subagentsEnabled && filesData.length >= 3) {
306
+ const { numBatches, shouldShowBatches } = calculateBatches(filesData.length, batchSize);
307
+ console.log(`⚔ Batch optimization: ${subagentModel} model, ${batchSize} files per batch`);
308
+ if (shouldShowBatches) {
309
+ console.log(`šŸ“Š Analyzing ${filesData.length} files in ${numBatches} batch${numBatches > 1 ? 'es' : ''}`);
257
310
  }
258
- });
311
+ }
259
312
 
260
- // Step 5: Analyze with Claude
261
- logger.debug(
262
- 'pre-commit - main',
263
- 'Sending prompt to Claude',
264
- { promptLength: prompt.length }
265
- );
313
+ // Step 5: Analyze with Claude (parallel or single)
314
+ let result;
315
+ // TODO: This can be refactored so no conditional is needed.
316
+ // Lists can have 0...N items, e.g. iterating a list of 1 element is akin to single execution.
317
+ if (subagentsEnabled && filesData.length >= 3) {
318
+ // Parallel execution: split files into batches
319
+ logger.info(`Using parallel execution with batch size ${batchSize}`);
320
+
321
+ const fileBatches = chunkArray(filesData, batchSize);
322
+ logger.debug('pre-commit - main', `Split into ${fileBatches.length} batches`);
323
+
324
+ // Build one prompt per batch
325
+ const prompts = await Promise.all(
326
+ fileBatches.map(async (batch) => {
327
+ return await buildAnalysisPrompt({
328
+ templateName: config.templates.analysis,
329
+ guidelinesName: config.templates.guidelines,
330
+ files: batch,
331
+ metadata: {
332
+ REPO_NAME: getRepoName(),
333
+ BRANCH_NAME: getCurrentBranch()
334
+ },
335
+ subagentConfig: null // Don't add subagent instruction for parallel
336
+ });
337
+ })
338
+ );
339
+
340
+ // Execute in parallel
341
+ const results = await analyzeCodeParallel(prompts, {
342
+ timeout: config.analysis.timeout,
343
+ saveDebug: false // Don't save debug for individual batches
344
+ });
345
+
346
+ // Simple consolidation: merge all results
347
+ result = consolidateResults(results);
348
+
349
+ // Save consolidated debug if enabled
350
+ if (config.system.debug) {
351
+ const { saveDebugResponse } = await import('../utils/claude-client.js');
352
+ await saveDebugResponse(
353
+ `PARALLEL ANALYSIS: ${fileBatches.length} batches`,
354
+ JSON.stringify(result, null, 2)
355
+ );
356
+ }
266
357
 
267
- const result = await analyzeCode(prompt, {
268
- timeout: 120000,
269
- saveDebug: process.env.DEBUG === 'true'
270
- });
358
+ } else {
359
+ // Single execution: original behavior
360
+ logger.debug('pre-commit - main', 'Building analysis prompt');
361
+
362
+ const prompt = await buildAnalysisPrompt({
363
+ templateName: config.templates.analysis,
364
+ guidelinesName: config.templates.guidelines,
365
+ files: filesData,
366
+ metadata: {
367
+ REPO_NAME: getRepoName(),
368
+ BRANCH_NAME: getCurrentBranch()
369
+ },
370
+ subagentConfig: config.subagents
371
+ });
372
+
373
+ logger.debug(
374
+ 'pre-commit - main',
375
+ 'Sending prompt to Claude',
376
+ { promptLength: prompt.length }
377
+ );
378
+
379
+ result = await analyzeCode(prompt, {
380
+ timeout: config.analysis.timeout,
381
+ saveDebug: config.system.debug
382
+ });
383
+ }
271
384
 
272
385
  // Step 6: Display results
273
386
  displayResults(result);
@@ -21,13 +21,10 @@ import fs from 'fs/promises';
21
21
  import { getStagedFiles, getStagedStats, getFileDiff } from '../utils/git-operations.js';
22
22
  import { analyzeCode } from '../utils/claude-client.js';
23
23
  import { filterSkipAnalysis } from '../utils/file-operations.js';
24
+ import { loadPrompt } from '../utils/prompt-builder.js';
25
+ import { getVersion, calculateBatches } from '../utils/package-info.js';
24
26
  import logger from '../utils/logger.js';
25
-
26
- // Configuration
27
- const CONFIG = {
28
- MAX_FILE_SIZE: 100000, // 100KB
29
- AUTO_KEYWORD: 'auto'
30
- };
27
+ import { getConfig } from '../config.js';
31
28
 
32
29
  /**
33
30
  * Builds commit message generation prompt
@@ -35,49 +32,36 @@ const CONFIG = {
35
32
  *
36
33
  * @param {Array<Object>} filesData - Array of file change data
37
34
  * @param {Object} stats - Staging statistics
38
- * @returns {string} Complete prompt
35
+ * @returns {Promise<string>} Complete prompt
39
36
  */
40
- const buildCommitPrompt = (filesData, stats) => {
37
+ const buildCommitPrompt = async (filesData, stats) => {
41
38
  logger.debug(
42
39
  'prepare-commit-msg - buildCommitPrompt',
43
40
  'Building commit message prompt',
44
41
  { fileCount: filesData.length }
45
42
  );
46
43
 
47
- let prompt = `Analyze the following changes and generate a commit message following the Conventional Commits format.
48
-
49
- Respond ONLY with a valid JSON:
50
-
51
- {
52
- "type": "feat|fix|docs|style|refactor|test|chore|ci|perf",
53
- "scope": "optional scope (e.g.: api, frontend, db)",
54
- "title": "short description in present tense (max 50 chars)",
55
- "body": "optional detailed description"
56
- }
57
-
58
- CHANGES TO ANALYZE:
59
-
60
- `;
61
-
62
- // Add file list
63
- prompt += '\nModified files:\n';
64
- filesData.forEach(({ path }) => {
65
- prompt += `${path}\n`;
66
- });
67
-
68
- // Add statistics
69
- prompt += `\nSummary of changes:\n`;
70
- prompt += `Files changed: ${stats.totalFiles || filesData.length}\n`;
71
- prompt += `Insertions: ${stats.insertions || 0}, Deletions: ${stats.deletions || 0}\n`;
44
+ // Build file list
45
+ const fileList = filesData.map(({ path }) => path).join('\n');
72
46
 
73
- // Add diffs
47
+ // Build file diffs
48
+ let fileDiffs = '';
74
49
  filesData.forEach(({ path, diff }) => {
75
50
  if (diff) {
76
- prompt += `\n--- Diff of ${path} ---\n`;
77
- prompt += diff;
51
+ fileDiffs += `\n--- Diff of ${path} ---\n`;
52
+ fileDiffs += diff;
78
53
  }
79
54
  });
80
55
 
56
+ // Load prompt from template
57
+ const prompt = await loadPrompt('COMMIT_MESSAGE.md', {
58
+ FILE_LIST: fileList,
59
+ FILE_COUNT: stats.totalFiles || filesData.length,
60
+ INSERTIONS: stats.insertions || 0,
61
+ DELETIONS: stats.deletions || 0,
62
+ FILE_DIFFS: fileDiffs
63
+ });
64
+
81
65
  return prompt;
82
66
  };
83
67
 
@@ -133,6 +117,9 @@ const formatCommitMessage = (json) => {
133
117
  const main = async () => {
134
118
  const startTime = Date.now();
135
119
 
120
+ // Load configuration (includes preset + user overrides)
121
+ const config = await getConfig();
122
+
136
123
  try {
137
124
  // Get hook arguments
138
125
  const args = process.argv.slice(2);
@@ -166,7 +153,7 @@ const main = async () => {
166
153
  );
167
154
 
168
155
  // Check if message is "auto"
169
- if (firstLine !== CONFIG.AUTO_KEYWORD) {
156
+ if (firstLine !== config.commitMessage.autoKeyword) {
170
157
  logger.debug(
171
158
  'prepare-commit-msg - main',
172
159
  'Not generating: message is not "auto"'
@@ -174,6 +161,17 @@ const main = async () => {
174
161
  process.exit(0);
175
162
  }
176
163
 
164
+ // Display configuration info
165
+ const version = await getVersion();
166
+ const subagentsEnabled = config.subagents?.enabled || false;
167
+ const subagentModel = config.subagents?.model || 'haiku';
168
+ const batchSize = config.subagents?.batchSize || 3;
169
+
170
+ console.log(`\nšŸ¤– claude-git-hooks v${version}`);
171
+ if (subagentsEnabled) {
172
+ console.log(`⚔ Parallel analysis: ${subagentModel} model, batch size ${batchSize}`);
173
+ }
174
+
177
175
  logger.info('Generating commit message automatically...');
178
176
 
179
177
  // Get staged files
@@ -203,7 +201,7 @@ const main = async () => {
203
201
 
204
202
  // Only include diff for small files
205
203
  let diff = null;
206
- if (fileStats.size < CONFIG.MAX_FILE_SIZE) {
204
+ if (fileStats.size < config.analysis.maxFileSize) {
207
205
  diff = getFileDiff(filePath);
208
206
  diff = filterSkipAnalysis(diff);
209
207
  }
@@ -231,7 +229,7 @@ const main = async () => {
231
229
  );
232
230
 
233
231
  // Build prompt
234
- const prompt = buildCommitPrompt(filesData, stats);
232
+ const prompt = await buildCommitPrompt(filesData, stats);
235
233
 
236
234
  logger.debug(
237
235
  'prepare-commit-msg - main',
@@ -239,12 +237,20 @@ const main = async () => {
239
237
  { promptLength: prompt.length }
240
238
  );
241
239
 
240
+ // Calculate batches if subagents enabled and applicable
241
+ if (subagentsEnabled && filesData.length >= 3) {
242
+ const { numBatches, shouldShowBatches } = calculateBatches(filesData.length, batchSize);
243
+ if (shouldShowBatches) {
244
+ console.log(`šŸ“Š Analyzing ${filesData.length} files in ${numBatches} batch${numBatches > 1 ? 'es' : ''}`);
245
+ }
246
+ }
247
+
242
248
  // Generate message with Claude
243
249
  logger.info('Sending to Claude...');
244
250
 
245
251
  const response = await analyzeCode(prompt, {
246
- timeout: 60000, // 1 minute timeout
247
- saveDebug: false
252
+ timeout: config.commitMessage.timeout,
253
+ saveDebug: config.system.debug
248
254
  });
249
255
 
250
256
  logger.debug(