claude-git-hooks 2.1.0 → 2.3.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +178 -0
  2. package/README.md +203 -79
  3. package/bin/claude-hooks +295 -119
  4. package/lib/config.js +163 -0
  5. package/lib/hooks/pre-commit.js +179 -67
  6. package/lib/hooks/prepare-commit-msg.js +47 -41
  7. package/lib/utils/claude-client.js +93 -11
  8. package/lib/utils/file-operations.js +1 -65
  9. package/lib/utils/file-utils.js +65 -0
  10. package/lib/utils/package-info.js +75 -0
  11. package/lib/utils/preset-loader.js +209 -0
  12. package/lib/utils/prompt-builder.js +83 -67
  13. package/lib/utils/resolution-prompt.js +12 -2
  14. package/package.json +49 -50
  15. package/templates/ANALYZE_DIFF.md +33 -0
  16. package/templates/COMMIT_MESSAGE.md +24 -0
  17. package/templates/SUBAGENT_INSTRUCTION.md +1 -0
  18. package/templates/config.example.json +41 -0
  19. package/templates/presets/ai/ANALYSIS_PROMPT.md +133 -0
  20. package/templates/presets/ai/PRE_COMMIT_GUIDELINES.md +176 -0
  21. package/templates/presets/ai/config.json +12 -0
  22. package/templates/presets/ai/preset.json +42 -0
  23. package/templates/presets/backend/ANALYSIS_PROMPT.md +85 -0
  24. package/templates/presets/backend/PRE_COMMIT_GUIDELINES.md +87 -0
  25. package/templates/presets/backend/config.json +12 -0
  26. package/templates/presets/backend/preset.json +49 -0
  27. package/templates/presets/database/ANALYSIS_PROMPT.md +114 -0
  28. package/templates/presets/database/PRE_COMMIT_GUIDELINES.md +143 -0
  29. package/templates/presets/database/config.json +12 -0
  30. package/templates/presets/database/preset.json +38 -0
  31. package/templates/presets/default/config.json +12 -0
  32. package/templates/presets/default/preset.json +53 -0
  33. package/templates/presets/frontend/ANALYSIS_PROMPT.md +99 -0
  34. package/templates/presets/frontend/PRE_COMMIT_GUIDELINES.md +95 -0
  35. package/templates/presets/frontend/config.json +12 -0
  36. package/templates/presets/frontend/preset.json +50 -0
  37. package/templates/presets/fullstack/ANALYSIS_PROMPT.md +107 -0
  38. package/templates/presets/fullstack/CONSISTENCY_CHECKS.md +147 -0
  39. package/templates/presets/fullstack/PRE_COMMIT_GUIDELINES.md +125 -0
  40. package/templates/presets/fullstack/config.json +12 -0
  41. package/templates/presets/fullstack/preset.json +55 -0
  42. package/templates/shared/ANALYSIS_PROMPT.md +103 -0
  43. package/templates/shared/ANALYZE_DIFF.md +33 -0
  44. package/templates/shared/COMMIT_MESSAGE.md +24 -0
  45. package/templates/shared/PRE_COMMIT_GUIDELINES.md +145 -0
  46. package/templates/shared/RESOLUTION_PROMPT.md +32 -0
  47. package/templates/check-version.sh +0 -266
package/lib/config.js ADDED
@@ -0,0 +1,163 @@
1
+ /**
2
+ * File: config.js
3
+ * Purpose: Centralized configuration management
4
+ *
5
+ * Priority: .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
+ * - Override via .claude/config.json per project
12
+ */
13
+
14
+ import fs from 'fs';
15
+ import path from 'path';
16
+ import { fileURLToPath } from 'url';
17
+
18
+ const __filename = fileURLToPath(import.meta.url);
19
+ const __dirname = path.dirname(__filename);
20
+
21
+ /**
22
+ * Default configuration
23
+ * All values can be overridden via .claude/config.json
24
+ */
25
+ const defaults = {
26
+ // Analysis configuration
27
+ analysis: {
28
+ maxFileSize: 100000, // 100KB - max file size to analyze
29
+ maxFiles: 10, // Max number of files per commit
30
+ timeout: 120000, // 2 minutes - Claude analysis timeout
31
+ contextLines: 3, // Git diff context lines
32
+ ignoreExtensions: [], // Extensions to exclude (e.g., ['.min.js', '.lock'])
33
+ // NOTE: allowedExtensions comes from preset/template, not here
34
+ },
35
+
36
+ // Commit message generation
37
+ commitMessage: {
38
+ autoKeyword: 'auto', // Keyword to trigger auto-generation
39
+ timeout: 180000,
40
+ },
41
+
42
+ // Subagent configuration (parallel analysis)
43
+ subagents: {
44
+ enabled: true, // Enable parallel file analysis
45
+ model: 'haiku', // haiku (fast), sonnet (balanced), opus (thorough)
46
+ batchSize: 3, // Parallel subagents per batch
47
+ },
48
+
49
+ // Template paths
50
+ templates: {
51
+ baseDir: '.claude', // Base directory for all templates
52
+ analysis: 'CLAUDE_ANALYSIS_PROMPT_SONAR.md', // Pre-commit analysis prompt
53
+ guidelines: 'CLAUDE_PRE_COMMIT_SONAR.md', // Analysis guidelines
54
+ commitMessage: 'COMMIT_MESSAGE.md', // Commit message prompt
55
+ analyzeDiff: 'ANALYZE_DIFF.md', // PR analysis prompt
56
+ resolution: 'CLAUDE_RESOLUTION_PROMPT.md', // Issue resolution prompt
57
+ subagentInstruction: 'SUBAGENT_INSTRUCTION.md', // Parallel analysis instruction
58
+ },
59
+
60
+ // Output file paths (relative to repo root)
61
+ output: {
62
+ outputDir: '.claude/out', // Output directory for all generated files
63
+ debugFile: '.claude/out/debug-claude-response.json', // Debug response dump
64
+ resolutionFile: '.claude/out/claude_resolution_prompt.md', // Generated resolution prompt
65
+ prAnalysisFile: '.claude/out/pr-analysis.json', // PR analysis result
66
+ },
67
+
68
+ // System behavior
69
+ system: {
70
+ debug: false, // Enable debug logging and file dumps
71
+ wslCheckTimeout: 3000, // 3 seconds - quick WSL availability check
72
+ },
73
+
74
+ // Git operations
75
+ git: {
76
+ diffFilter: 'ACM', // Added, Copied, Modified (excludes Deleted)
77
+ },
78
+ };
79
+
80
+ /**
81
+ * Loads user configuration from .claude/config.json
82
+ * Merges with defaults and preset config (preset -> user config takes priority)
83
+ *
84
+ * Priority: defaults < preset config < user config
85
+ *
86
+ * @param {string} baseDir - Base directory to search for config (default: cwd)
87
+ * @returns {Promise<Object>} Merged configuration
88
+ */
89
+ const loadUserConfig = async (baseDir = process.cwd()) => {
90
+ const configPath = path.join(baseDir, '.claude', 'config.json');
91
+
92
+ let userConfig = {};
93
+ try {
94
+ if (fs.existsSync(configPath)) {
95
+ userConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
96
+ }
97
+ } catch (error) {
98
+ console.warn(`āš ļø Warning: Could not load .claude/config.json: ${error.message}`);
99
+ console.warn('Using default configuration');
100
+ }
101
+
102
+ // Load preset if specified
103
+ let presetConfig = {};
104
+ if (userConfig.preset) {
105
+ try {
106
+ // Dynamic import to avoid circular dependency
107
+ const { loadPreset } = await import('./utils/preset-loader.js');
108
+ const { config } = await loadPreset(userConfig.preset);
109
+ presetConfig = config;
110
+ } catch (error) {
111
+ console.warn(`āš ļø Warning: Preset "${userConfig.preset}" not found, using defaults`);
112
+ }
113
+ }
114
+
115
+ // Merge: defaults < preset < user
116
+ return deepMerge(deepMerge(defaults, presetConfig), userConfig);
117
+ };
118
+
119
+ /**
120
+ * Deep merge two objects (user config overrides defaults)
121
+ *
122
+ * @param {Object} target - Target object (defaults)
123
+ * @param {Object} source - Source object (user overrides)
124
+ * @returns {Object} Merged object
125
+ */
126
+ const deepMerge = (target, source) => {
127
+ const result = { ...target };
128
+
129
+ for (const key in source) {
130
+ if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
131
+ result[key] = deepMerge(target[key] || {}, source[key]);
132
+ } else {
133
+ result[key] = source[key];
134
+ }
135
+ }
136
+
137
+ return result;
138
+ };
139
+
140
+ /**
141
+ * Get configuration instance
142
+ * Loads once, caches result
143
+ *
144
+ * Usage:
145
+ * import { getConfig } from './lib/config.js';
146
+ * const config = await getConfig();
147
+ * const maxFiles = config.analysis.maxFiles;
148
+ */
149
+ let configInstance = null;
150
+
151
+ const getConfig = async () => {
152
+ if (!configInstance) {
153
+ configInstance = await loadUserConfig();
154
+ }
155
+ return configInstance;
156
+ };
157
+
158
+ // Export async loader
159
+ export { getConfig, loadUserConfig, defaults };
160
+
161
+ // Export a sync default for backwards compatibility (loads without preset)
162
+ // NOTE: This will be deprecated - prefer using getConfig() directly
163
+ 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,90 @@ 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
+
316
+ if (subagentsEnabled && filesData.length >= 3) {
317
+ // Parallel execution: split files into batches
318
+ logger.info(`Using parallel execution with batch size ${batchSize}`);
319
+
320
+ const fileBatches = chunkArray(filesData, batchSize);
321
+ logger.debug('pre-commit - main', `Split into ${fileBatches.length} batches`);
322
+
323
+ // Build one prompt per batch
324
+ const prompts = await Promise.all(
325
+ fileBatches.map(async (batch) => {
326
+ return await buildAnalysisPrompt({
327
+ templateName: config.templates.analysis,
328
+ guidelinesName: config.templates.guidelines,
329
+ files: batch,
330
+ metadata: {
331
+ REPO_NAME: getRepoName(),
332
+ BRANCH_NAME: getCurrentBranch()
333
+ },
334
+ subagentConfig: null // Don't add subagent instruction for parallel
335
+ });
336
+ })
337
+ );
338
+
339
+ // Execute in parallel
340
+ const results = await analyzeCodeParallel(prompts, {
341
+ timeout: config.analysis.timeout,
342
+ saveDebug: false // Don't save debug for individual batches
343
+ });
344
+
345
+ // Simple consolidation: merge all results
346
+ result = consolidateResults(results);
347
+
348
+ // Save consolidated debug if enabled
349
+ if (config.system.debug) {
350
+ const { saveDebugResponse } = await import('../utils/claude-client.js');
351
+ await saveDebugResponse(
352
+ `PARALLEL ANALYSIS: ${fileBatches.length} batches`,
353
+ JSON.stringify(result, null, 2)
354
+ );
355
+ }
266
356
 
267
- const result = await analyzeCode(prompt, {
268
- timeout: 120000,
269
- saveDebug: process.env.DEBUG === 'true'
270
- });
357
+ } else {
358
+ // Single execution: original behavior
359
+ logger.debug('pre-commit - main', 'Building analysis prompt');
360
+
361
+ const prompt = await buildAnalysisPrompt({
362
+ templateName: config.templates.analysis,
363
+ guidelinesName: config.templates.guidelines,
364
+ files: filesData,
365
+ metadata: {
366
+ REPO_NAME: getRepoName(),
367
+ BRANCH_NAME: getCurrentBranch()
368
+ },
369
+ subagentConfig: config.subagents
370
+ });
371
+
372
+ logger.debug(
373
+ 'pre-commit - main',
374
+ 'Sending prompt to Claude',
375
+ { promptLength: prompt.length }
376
+ );
377
+
378
+ result = await analyzeCode(prompt, {
379
+ timeout: config.analysis.timeout,
380
+ saveDebug: config.system.debug
381
+ });
382
+ }
271
383
 
272
384
  // Step 6: Display results
273
385
  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(