claude-git-hooks 1.5.3 → 1.5.5

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 CHANGED
@@ -5,6 +5,60 @@ 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
+ ## [1.5.5] - 2025-10-29
9
+
10
+ ### Added
11
+
12
+ - 🚀 **Parallel Analysis with Subagents**
13
+ - New `CLAUDE_USE_SUBAGENTS` environment variable to enable parallel file analysis
14
+ - Each file analyzed by dedicated Claude subagent for faster processing
15
+ - Works across all analysis functions: pre-commit, message generation, and analyze-diff
16
+ - Significantly faster for commits with 3+ files
17
+
18
+ - ⚙️ **Subagent Configuration Options**
19
+ - `CLAUDE_SUBAGENT_MODEL` - Choose model: haiku (fast), sonnet (balanced), opus (thorough)
20
+ - `CLAUDE_SUBAGENT_BATCH_SIZE` - Control parallel processing (default: 3 files at once)
21
+ - Automatic result consolidation across all subagents
22
+
23
+ - ⏱️ **Execution Time Measurement**
24
+ - All operations now display execution time in seconds and milliseconds
25
+ - Pre-commit analysis: Shows time on success and failure
26
+ - Message generation: Shows generation time
27
+ - Analyze-diff: Shows analysis time
28
+ - Helps identify performance improvements and bottlenecks
29
+
30
+ - 🔧 **Git Worktree Support**
31
+ - `claude-hooks` now recognizes worktrees created in PowerShell from WSL
32
+ - Automatically converts Windows paths (C:\) to WSL paths (/mnt/c/)
33
+ - No more "not a git repository" errors when working in cross-platform worktrees
34
+
35
+ ### Changed
36
+
37
+ - 📝 Subagent instructions include explicit consolidation logic for consistent results
38
+ - 🎨 Help command updated with batching examples and clarifications
39
+ - 📋 Post-install text now mentions parallel analysis feature with examples
40
+ - ✅ **Batch size validation** - `CLAUDE_SUBAGENT_BATCH_SIZE` now validated (minimum: 1)
41
+ - 📚 Batching behavior explicitly documented with concrete examples
42
+
43
+ ## [1.5.4] - 2025-09-15
44
+
45
+ ### Fixed
46
+
47
+ - 🔧 **Generación de prompt de resolución en modo SonarQube**
48
+ - Ahora se genera correctamente `claude_resolution_prompt.md` cuando el Quality Gate falla
49
+ - El archivo se genera cuando hay `blockingIssues`, independiente del tipo de fallo
50
+ - Corregido el flujo para que siempre muestre los issues críticos antes de generar el prompt
51
+
52
+ - 🎯 **Unificación de lógica de análisis**
53
+ - Eliminada lógica duplicada entre modo "clásico" y SonarQube
54
+ - Ahora siempre se usa modo SonarQube (como se especificó en v1.4.1)
55
+ - Simplificación del flujo de decisión en el pre-commit hook
56
+
57
+ ### Changed
58
+
59
+ - 📝 Refactorización del hook pre-commit para mayor claridad y mantenibilidad
60
+ - 🔄 La función `generate_resolution_prompt` ahora se llama consistentemente cuando hay issues bloqueantes
61
+
8
62
  ## [1.5.3] - 2025-09-12
9
63
 
10
64
  ### Fixed
package/LICENSE CHANGED
File without changes
package/README.md CHANGED
@@ -101,6 +101,11 @@ public void methodToIgnore() {
101
101
  # Variables de entorno
102
102
  export CLAUDE_ANALYSIS_MODE=sonarqube # Modo de análisis
103
103
  export CLAUDE_DEBUG=true # Debug detallado
104
+
105
+ # Subagent configuration (parallel analysis for multiple files)
106
+ export CLAUDE_USE_SUBAGENTS=true # Enable subagent parallel analysis
107
+ export CLAUDE_SUBAGENT_MODEL=haiku # Model: haiku (fast), sonnet (balanced), opus (thorough)
108
+ export CLAUDE_SUBAGENT_BATCH_SIZE=3 # Parallel subagents per batch (default: 3)
104
109
  ```
105
110
 
106
111
  ### 🎯 Casos de Uso Específicos
@@ -134,6 +139,41 @@ git commit -m "fix: resolver issues"
134
139
  5. **Archivos grandes**: Se omiten automáticamente archivos > 100KB
135
140
  6. **Límite de archivos**: Máximo 10 archivos por commit
136
141
 
142
+ ### 🚀 Parallel Analysis with Subagents (NEW in v1.5.5)
143
+
144
+ **When analyzing 3+ files**, enable subagents for faster parallel processing:
145
+
146
+ ```bash
147
+ # Enable subagent parallel analysis
148
+ export CLAUDE_USE_SUBAGENTS=true
149
+
150
+ # Optional: Choose model (default: haiku for speed)
151
+ export CLAUDE_SUBAGENT_MODEL=haiku # Fast & cheap
152
+ export CLAUDE_SUBAGENT_MODEL=sonnet # Balanced quality/speed
153
+ export CLAUDE_SUBAGENT_MODEL=opus # Maximum quality
154
+
155
+ # Optional: Adjust batch size (default: 3 files at a time)
156
+ export CLAUDE_SUBAGENT_BATCH_SIZE=3
157
+
158
+ # Now commits will use parallel analysis automatically
159
+ git commit -m "feat: implement multiple features"
160
+ ```
161
+
162
+ **How batching works:**
163
+ - `BATCH_SIZE=1` with 4 files → 4 sequential batches (1 subagent each)
164
+ - `BATCH_SIZE=3` with 4 files → 2 batches (3 parallel, then 1)
165
+ - `BATCH_SIZE=4` with 4 files → 1 batch (all 4 in parallel)
166
+ - Batch size is validated automatically (minimum: 1)
167
+
168
+ **How it works:**
169
+ - Each file is analyzed by a dedicated Claude subagent
170
+ - Files are processed in batches for optimal performance
171
+ - Results are consolidated automatically into single response
172
+ - Shows execution time for all operations
173
+ - Uses one-line prompt injection - no complex architecture changes
174
+
175
+ **Best for:** Large commits (3+ files), refactoring tasks, feature branches
176
+
137
177
  ## 🔧 Configuración Previa Importante
138
178
 
139
179
  ### Credenciales Git en WSL
package/bin/claude-hooks CHANGED
@@ -42,24 +42,24 @@ async function checkVersionAndPromptUpdate() {
42
42
  try {
43
43
  const currentVersion = require('../package.json').version;
44
44
  const latestVersion = await getLatestVersion('claude-git-hooks');
45
-
45
+
46
46
  if (currentVersion === latestVersion) {
47
47
  return true; // Already updated
48
48
  }
49
-
49
+
50
50
  console.log('');
51
51
  warning(`New version available: ${latestVersion} (current: ${currentVersion})`);
52
-
52
+
53
53
  // Interactive prompt compatible with all consoles
54
54
  const rl = readline.createInterface({
55
55
  input: process.stdin,
56
56
  output: process.stdout
57
57
  });
58
-
58
+
59
59
  return new Promise((resolve) => {
60
60
  rl.question('Do you want to update now? (y/n): ', (answer) => {
61
61
  rl.close();
62
-
62
+
63
63
  if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
64
64
  info('Updating claude-git-hooks...');
65
65
  try {
@@ -124,7 +124,7 @@ function readPassword(prompt) {
124
124
  input: process.stdin,
125
125
  output: process.stdout
126
126
  });
127
-
127
+
128
128
  // Disable echo
129
129
  rl.stdoutMuted = true;
130
130
  rl._writeToOutput = function _writeToOutput(stringToWrite) {
@@ -133,7 +133,7 @@ function readPassword(prompt) {
133
133
  else
134
134
  rl.output.write(stringToWrite);
135
135
  };
136
-
136
+
137
137
  rl.question(prompt, (password) => {
138
138
  rl.close();
139
139
  console.log(); // New line
@@ -145,7 +145,7 @@ function readPassword(prompt) {
145
145
  // Check if sudo password is correct
146
146
  function testSudoPassword(password) {
147
147
  try {
148
- execSync('echo "' + password + '" | sudo -S true', {
148
+ execSync('echo "' + password + '" | sudo -S true', {
149
149
  stdio: 'ignore',
150
150
  timeout: 5000
151
151
  });
@@ -231,26 +231,26 @@ class Entertainment {
231
231
  // Get first joke from API without blocking
232
232
  this.getJoke().then(joke => {
233
233
  if (!isFinished) currentJoke = joke;
234
- }).catch(() => {}); // If it fails, keep the local one
234
+ }).catch(() => { }); // If it fails, keep the local one
235
235
 
236
236
  // Hide cursor
237
237
  process.stdout.write('\x1B[?25l');
238
-
238
+
239
239
  // Reserve space for the 3 lines
240
240
  process.stdout.write('\n\n\n');
241
-
241
+
242
242
  const interval = setInterval(() => {
243
243
  if (isFinished) {
244
244
  clearInterval(interval);
245
245
  return;
246
246
  }
247
-
247
+
248
248
  spinnerIndex++;
249
-
249
+
250
250
  // Update countdown every second (10 iterations of 100ms)
251
251
  if (spinnerIndex % 10 === 0) {
252
252
  jokeCountdown--;
253
-
253
+
254
254
  // Refresh joke every 10 seconds
255
255
  if (jokeCountdown <= 0) {
256
256
  this.getJoke().then(joke => {
@@ -263,19 +263,19 @@ class Entertainment {
263
263
  jokeCountdown = 10;
264
264
  }
265
265
  }
266
-
266
+
267
267
  // Always go back exactly 3 lines up
268
268
  process.stdout.write('\x1B[3A');
269
-
269
+
270
270
  // Render the 3 lines from the beginning
271
271
  const spinner = spinners[spinnerIndex % spinners.length];
272
-
272
+
273
273
  // Line 1: Spinner
274
274
  process.stdout.write('\r\x1B[2K' + `${colors.yellow}${spinner} ${message}${colors.reset}\n`);
275
-
275
+
276
276
  // Line 2: Joke
277
277
  process.stdout.write('\r\x1B[2K' + `${colors.green}🎭 ${currentJoke}${colors.reset}\n`);
278
-
278
+
279
279
  // Line 3: Countdown
280
280
  process.stdout.write('\r\x1B[2K' + `${colors.yellow}⏱️ Next joke in: ${jokeCountdown}s${colors.reset}\n`);
281
281
  }, 100);
@@ -284,7 +284,7 @@ class Entertainment {
284
284
  const result = await promise;
285
285
  isFinished = true;
286
286
  clearInterval(interval);
287
-
287
+
288
288
  // Clean exactly 3 lines completely
289
289
  process.stdout.write('\x1B[3A'); // Go up 3 lines
290
290
  process.stdout.write('\r\x1B[2K'); // Clean line 1
@@ -292,15 +292,15 @@ class Entertainment {
292
292
  process.stdout.write('\n\r\x1B[2K'); // Go down and clean line 3
293
293
  process.stdout.write('\x1B[2A'); // Go up 2 lines to end up on the first
294
294
  process.stdout.write('\r'); // Go to beginning of line
295
-
295
+
296
296
  // Show cursor
297
297
  process.stdout.write('\x1B[?25h');
298
-
298
+
299
299
  return result;
300
300
  } catch (error) {
301
301
  isFinished = true;
302
302
  clearInterval(interval);
303
-
303
+
304
304
  // Clean exactly 3 lines completely
305
305
  process.stdout.write('\x1B[3A'); // Go up 3 lines
306
306
  process.stdout.write('\r\x1B[2K'); // Clean line 1
@@ -308,21 +308,42 @@ class Entertainment {
308
308
  process.stdout.write('\n\r\x1B[2K'); // Go down and clean line 3
309
309
  process.stdout.write('\x1B[2A'); // Go up 2 lines to end up on the first
310
310
  process.stdout.write('\r'); // Go to beginning of line
311
-
311
+
312
312
  // Show cursor
313
313
  process.stdout.write('\x1B[?25h');
314
-
314
+
315
315
  throw error;
316
316
  }
317
317
  }
318
318
  }
319
319
 
320
- // Check if we are in a git repository
320
+ // Check if we are in a git repository (including worktrees created in PowerShell)
321
321
  function checkGitRepo() {
322
322
  try {
323
323
  execSync('git rev-parse --git-dir', { stdio: 'ignore' });
324
324
  return true;
325
325
  } catch (e) {
326
+ // Try to detect worktree created in PowerShell
327
+ try {
328
+ if (fs.existsSync('.git')) {
329
+ const gitContent = fs.readFileSync('.git', 'utf8').trim();
330
+ // Check if it's a worktree pointer (gitdir: ...)
331
+ if (gitContent.startsWith('gitdir:')) {
332
+ let gitdir = gitContent.substring(8).trim();
333
+ // Convert Windows path to WSL if needed (C:\ -> /mnt/c/)
334
+ if (/^[A-Za-z]:/.test(gitdir)) {
335
+ gitdir = gitdir.replace(/^([A-Za-z]):/, (_, drive) => `/mnt/${drive.toLowerCase()}`);
336
+ gitdir = gitdir.replace(/\\/g, '/');
337
+ }
338
+ // Verify the gitdir exists
339
+ if (fs.existsSync(gitdir)) {
340
+ return true;
341
+ }
342
+ }
343
+ }
344
+ } catch (worktreeError) {
345
+ // Ignore worktree detection errors
346
+ }
326
347
  return false;
327
348
  }
328
349
  }
@@ -340,7 +361,7 @@ async function install(args) {
340
361
 
341
362
  const isForce = args.includes('--force');
342
363
  const skipAuth = args.includes('--skip-auth');
343
-
364
+
344
365
  if (isForce) {
345
366
  info('Installing Claude Git Hooks (force mode)...');
346
367
  } else {
@@ -354,7 +375,7 @@ async function install(args) {
354
375
  if (needsInstall) {
355
376
  info('Sudo access is needed for automatic dependency installation, please enter password');
356
377
  sudoPassword = await readPassword('Enter your Ubuntu password for sudo: ');
357
-
378
+
358
379
  if (sudoPassword && !testSudoPassword(sudoPassword)) {
359
380
  warning('Incorrect password. Continuing without automatic installation.');
360
381
  sudoPassword = null;
@@ -377,18 +398,18 @@ async function install(args) {
377
398
 
378
399
  // Hooks to install
379
400
  const hooks = ['pre-commit', 'prepare-commit-msg'];
380
-
401
+
381
402
  hooks.forEach(hook => {
382
403
  const sourcePath = path.join(templatesPath, hook);
383
404
  const destPath = path.join(hooksPath, hook);
384
-
405
+
385
406
  // Make backup if it exists
386
407
  if (fs.existsSync(destPath)) {
387
408
  const backupPath = `${destPath}.backup.${Date.now()}`;
388
409
  fs.copyFileSync(destPath, backupPath);
389
410
  info(`Backup created: ${backupPath}`);
390
411
  }
391
-
412
+
392
413
  // Copy hook
393
414
  fs.copyFileSync(sourcePath, destPath);
394
415
  fs.chmodSync(destPath, '755');
@@ -398,7 +419,7 @@ async function install(args) {
398
419
  // Copy version verification script
399
420
  const checkVersionSource = path.join(templatesPath, 'check-version.sh');
400
421
  const checkVersionDest = path.join(hooksPath, 'check-version.sh');
401
-
422
+
402
423
  if (fs.existsSync(checkVersionSource)) {
403
424
  fs.copyFileSync(checkVersionSource, checkVersionDest);
404
425
  fs.chmodSync(checkVersionDest, '755');
@@ -418,11 +439,11 @@ async function install(args) {
418
439
  'CLAUDE_ANALYSIS_PROMPT_SONAR.md',
419
440
  'CLAUDE_RESOLUTION_PROMPT.md'
420
441
  ];
421
-
442
+
422
443
  claudeFiles.forEach(file => {
423
444
  const destPath = path.join(claudeDir, file);
424
445
  const sourcePath = path.join(templatesPath, file);
425
-
446
+
426
447
  // In force mode or if it doesn't exist, copy the file
427
448
  if (isForce || !fs.existsSync(destPath)) {
428
449
  if (fs.existsSync(sourcePath)) {
@@ -450,13 +471,19 @@ async function install(args) {
450
471
  console.log(' // SKIP_ANALYSIS_BLOCK # Exclude block until finding another equal one');
451
472
  console.log(' ...excluded code...');
452
473
  console.log(' // SKIP_ANALYSIS_BLOCK');
474
+ console.log('\nNEW: Parallel analysis for faster multi-file commits:');
475
+ console.log(' export CLAUDE_USE_SUBAGENTS=true # Enable subagents');
476
+ console.log(' export CLAUDE_SUBAGENT_MODEL=haiku # haiku/sonnet/opus');
477
+ console.log(' export CLAUDE_SUBAGENT_BATCH_SIZE=3 # Parallel per batch (default: 3)');
478
+ console.log(' # Example: 4 files, BATCH_SIZE=1 → 4 sequential batches');
479
+ console.log(' # Example: 4 files, BATCH_SIZE=3 → 2 batches (3 parallel + 1)');
453
480
  console.log('\nFor more options: claude-hooks --help');
454
481
  }
455
482
 
456
483
  // Check complete dependencies (like setup-wsl.sh)
457
484
  async function checkAndInstallDependencies(sudoPassword = null, skipAuth = false) {
458
485
  info('Checking system dependencies...');
459
-
486
+
460
487
  // Check Node.js
461
488
  try {
462
489
  const nodeVersion = execSync('node --version', { encoding: 'utf8' }).trim();
@@ -464,7 +491,7 @@ async function checkAndInstallDependencies(sudoPassword = null, skipAuth = false
464
491
  } catch (e) {
465
492
  error('Node.js is not installed. Install Node.js and try again.');
466
493
  }
467
-
494
+
468
495
  // Check npm
469
496
  try {
470
497
  const npmVersion = execSync('npm --version', { encoding: 'utf8' }).trim();
@@ -472,7 +499,7 @@ async function checkAndInstallDependencies(sudoPassword = null, skipAuth = false
472
499
  } catch (e) {
473
500
  error('npm is not installed.');
474
501
  }
475
-
502
+
476
503
  // Check and install jq
477
504
  try {
478
505
  const jqVersion = execSync('jq --version', { encoding: 'utf8' }).trim();
@@ -490,7 +517,7 @@ async function checkAndInstallDependencies(sudoPassword = null, skipAuth = false
490
517
  }
491
518
  }
492
519
  }
493
-
520
+
494
521
  // Check and install curl
495
522
  try {
496
523
  const curlVersion = execSync('curl --version', { encoding: 'utf8' }).split('\n')[0];
@@ -508,7 +535,7 @@ async function checkAndInstallDependencies(sudoPassword = null, skipAuth = false
508
535
  }
509
536
  }
510
537
  }
511
-
538
+
512
539
  // Check Git
513
540
  try {
514
541
  const gitVersion = execSync('git --version', { encoding: 'utf8' }).trim();
@@ -516,11 +543,11 @@ async function checkAndInstallDependencies(sudoPassword = null, skipAuth = false
516
543
  } catch (e) {
517
544
  error('Git is not installed. Install Git and try again.');
518
545
  }
519
-
546
+
520
547
  // Check standard Unix tools
521
548
  const unixTools = ['sed', 'awk', 'grep', 'head', 'tail', 'stat', 'tput'];
522
549
  const missingTools = [];
523
-
550
+
524
551
  unixTools.forEach(tool => {
525
552
  try {
526
553
  execSync(`which ${tool}`, { stdio: 'ignore' });
@@ -528,23 +555,23 @@ async function checkAndInstallDependencies(sudoPassword = null, skipAuth = false
528
555
  missingTools.push(tool);
529
556
  }
530
557
  });
531
-
558
+
532
559
  if (missingTools.length === 0) {
533
560
  success('Standard Unix tools verified');
534
561
  } else {
535
562
  error(`Missing standard Unix tools: ${missingTools.join(', ')}. Retry installation in an Ubuntu console`);
536
563
  }
537
-
564
+
538
565
  // Check and install Claude CLI
539
566
  await checkAndInstallClaude();
540
-
567
+
541
568
  // Check Claude authentication (if not skipped)
542
569
  if (!skipAuth) {
543
570
  await checkClaudeAuth();
544
571
  } else {
545
572
  warning('Skipping Claude authentication verification (--skip-auth)');
546
573
  }
547
-
574
+
548
575
  // Clear password from memory
549
576
  sudoPassword = null;
550
577
  }
@@ -552,7 +579,7 @@ async function checkAndInstallDependencies(sudoPassword = null, skipAuth = false
552
579
  // Check if we need to install dependencies
553
580
  async function checkIfInstallationNeeded() {
554
581
  const dependencies = ['jq', 'curl'];
555
-
582
+
556
583
  for (const dep of dependencies) {
557
584
  try {
558
585
  execSync(`which ${dep}`, { stdio: 'ignore' });
@@ -560,14 +587,14 @@ async function checkIfInstallationNeeded() {
560
587
  return true; // Needs installation
561
588
  }
562
589
  }
563
-
590
+
564
591
  // Verificar Claude CLI
565
592
  try {
566
593
  execSync('claude --version', { stdio: 'ignore' });
567
594
  } catch (e) {
568
595
  return true; // Needs Claude installation
569
596
  }
570
-
597
+
571
598
  return false;
572
599
  }
573
600
 
@@ -590,7 +617,7 @@ async function checkAndInstallClaude() {
590
617
  // Check Claude authentication with entertainment
591
618
  async function checkClaudeAuth() {
592
619
  info('Checking Claude authentication...');
593
-
620
+
594
621
  // Use spawn to not block, but with stdio: 'ignore' like the original
595
622
  const authPromise = new Promise((resolve, reject) => {
596
623
  const child = spawn('claude', ['auth', 'status'], {
@@ -598,13 +625,13 @@ async function checkClaudeAuth() {
598
625
  detached: false,
599
626
  windowsHide: true
600
627
  });
601
-
628
+
602
629
  // Manual timeout since spawn doesn't have native timeout
603
630
  const timeout = setTimeout(() => {
604
631
  child.kill();
605
632
  reject(new Error('timeout'));
606
633
  }, 120000); // 2 minutos
607
-
634
+
608
635
  child.on('exit', (code) => {
609
636
  clearTimeout(timeout);
610
637
  if (code === 0) {
@@ -613,7 +640,7 @@ async function checkClaudeAuth() {
613
640
  reject(new Error('not_authenticated'));
614
641
  }
615
642
  });
616
-
643
+
617
644
  child.on('error', (err) => {
618
645
  clearTimeout(timeout);
619
646
  reject(err);
@@ -633,7 +660,7 @@ async function checkClaudeAuth() {
633
660
  // Update .gitignore with Claude entries
634
661
  function updateGitignore() {
635
662
  info('Updating .gitignore...');
636
-
663
+
637
664
  const gitignorePath = '.gitignore';
638
665
  const claudeEntries = [
639
666
  '# Claude Git Hooks',
@@ -642,16 +669,16 @@ function updateGitignore() {
642
669
  'claude_resolution_prompt.md',
643
670
  '.claude-pr-analysis.json',
644
671
  ];
645
-
672
+
646
673
  let gitignoreContent = '';
647
674
  let fileExists = false;
648
-
675
+
649
676
  // Read existing .gitignore if it exists
650
677
  if (fs.existsSync(gitignorePath)) {
651
678
  gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
652
679
  fileExists = true;
653
680
  }
654
-
681
+
655
682
  // Check which entries are missing
656
683
  const missingEntries = [];
657
684
  claudeEntries.forEach(entry => {
@@ -668,31 +695,31 @@ function updateGitignore() {
668
695
  }
669
696
  }
670
697
  });
671
-
698
+
672
699
  // If there are missing entries, add them
673
700
  if (missingEntries.length > 0) {
674
701
  // Ensure there's a newline at the end if the file exists and is not empty
675
702
  if (fileExists && gitignoreContent.length > 0 && !gitignoreContent.endsWith('\n')) {
676
703
  gitignoreContent += '\n';
677
704
  }
678
-
705
+
679
706
  // If the file is not empty, add a blank line before
680
707
  if (gitignoreContent.length > 0) {
681
708
  gitignoreContent += '\n';
682
709
  }
683
-
710
+
684
711
  // Add the missing entries
685
712
  gitignoreContent += missingEntries.join('\n') + '\n';
686
-
713
+
687
714
  // Write the updated file
688
715
  fs.writeFileSync(gitignorePath, gitignoreContent);
689
-
716
+
690
717
  if (fileExists) {
691
718
  success('.gitignore updated with Claude entries');
692
719
  } else {
693
720
  success('.gitignore created with Claude entries');
694
721
  }
695
-
722
+
696
723
  // Show what was added
697
724
  missingEntries.forEach(entry => {
698
725
  if (!entry.startsWith('#')) {
@@ -707,12 +734,12 @@ function updateGitignore() {
707
734
  // Configure Git (line endings, etc.)
708
735
  function configureGit() {
709
736
  info('Configuring Git...');
710
-
737
+
711
738
  try {
712
739
  // Configure line endings for WSL
713
740
  execSync('git config core.autocrlf input', { stdio: 'ignore' });
714
741
  success('Line endings configured for WSL (core.autocrlf = input)');
715
-
742
+
716
743
  // Try to configure on Windows through PowerShell
717
744
  try {
718
745
  execSync('powershell.exe -Command "git config core.autocrlf true"', { stdio: 'ignore' });
@@ -720,7 +747,7 @@ function configureGit() {
720
747
  } catch (psError) {
721
748
  info('Could not configure automatically on Windows');
722
749
  }
723
-
750
+
724
751
  } catch (e) {
725
752
  warning('Error configuring Git');
726
753
  }
@@ -736,7 +763,7 @@ function uninstall() {
736
763
 
737
764
  const hooksPath = '.git/hooks';
738
765
  const hooks = ['pre-commit', 'prepare-commit-msg'];
739
-
766
+
740
767
  hooks.forEach(hook => {
741
768
  const hookPath = path.join(hooksPath, hook);
742
769
  if (fs.existsSync(hookPath)) {
@@ -755,11 +782,11 @@ function enable(hookName) {
755
782
  }
756
783
 
757
784
  const hooks = hookName ? [hookName] : ['pre-commit', 'prepare-commit-msg'];
758
-
785
+
759
786
  hooks.forEach(hook => {
760
787
  const disabledPath = `.git/hooks/${hook}.disabled`;
761
788
  const enabledPath = `.git/hooks/${hook}`;
762
-
789
+
763
790
  if (fs.existsSync(disabledPath)) {
764
791
  fs.renameSync(disabledPath, enabledPath);
765
792
  success(`${hook} enabled`);
@@ -778,11 +805,11 @@ function disable(hookName) {
778
805
  }
779
806
 
780
807
  const hooks = hookName ? [hookName] : ['pre-commit', 'prepare-commit-msg'];
781
-
808
+
782
809
  hooks.forEach(hook => {
783
810
  const enabledPath = `.git/hooks/${hook}`;
784
811
  const disabledPath = `.git/hooks/${hook}.disabled`;
785
-
812
+
786
813
  if (fs.existsSync(enabledPath)) {
787
814
  fs.renameSync(enabledPath, disabledPath);
788
815
  success(`${hook} disabled`);
@@ -802,7 +829,7 @@ function analyzeDiff(args) {
802
829
  }
803
830
 
804
831
  const currentBranch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();
805
-
832
+
806
833
  if (!currentBranch) {
807
834
  error('You are not in a valid branch.');
808
835
  return;
@@ -819,7 +846,7 @@ function analyzeDiff(args) {
819
846
  baseBranch = `origin/${targetBranch}`;
820
847
  compareWith = `${baseBranch}...HEAD`;
821
848
  contextDescription = `${currentBranch} vs ${baseBranch}`;
822
-
849
+
823
850
  // Check that the origin branch exists
824
851
  try {
825
852
  execSync(`git rev-parse --verify ${baseBranch}`, { stdio: 'ignore' });
@@ -832,7 +859,7 @@ function analyzeDiff(args) {
832
859
  baseBranch = `origin/${currentBranch}`;
833
860
  compareWith = `${baseBranch}...HEAD`;
834
861
  contextDescription = `${currentBranch} vs ${baseBranch}`;
835
-
862
+
836
863
  // Check that the origin branch exists
837
864
  try {
838
865
  execSync(`git rev-parse --verify ${baseBranch}`, { stdio: 'ignore' });
@@ -841,7 +868,7 @@ function analyzeDiff(args) {
841
868
  baseBranch = 'origin/develop';
842
869
  compareWith = `${baseBranch}...HEAD`;
843
870
  contextDescription = `${currentBranch} vs ${baseBranch} (fallback)`;
844
-
871
+
845
872
  try {
846
873
  execSync(`git rev-parse --verify ${baseBranch}`, { stdio: 'ignore' });
847
874
  warning(`Branch origin/${currentBranch} does not exist. Using ${baseBranch} as fallback.`);
@@ -850,7 +877,7 @@ function analyzeDiff(args) {
850
877
  baseBranch = 'origin/main';
851
878
  compareWith = `${baseBranch}...HEAD`;
852
879
  contextDescription = `${currentBranch} vs ${baseBranch} (fallback)`;
853
-
880
+
854
881
  try {
855
882
  execSync(`git rev-parse --verify ${baseBranch}`, { stdio: 'ignore' });
856
883
  warning(`No origin/develop branch. Using ${baseBranch} as fallback.`);
@@ -868,12 +895,12 @@ function analyzeDiff(args) {
868
895
  let diffFiles;
869
896
  try {
870
897
  diffFiles = execSync(`git diff ${compareWith} --name-only`, { encoding: 'utf8' }).trim();
871
-
898
+
872
899
  if (!diffFiles) {
873
900
  // Check if there are staged or unstaged changes
874
901
  const stagedFiles = execSync('git diff --cached --name-only', { encoding: 'utf8' }).trim();
875
902
  const unstagedFiles = execSync('git diff --name-only', { encoding: 'utf8' }).trim();
876
-
903
+
877
904
  if (stagedFiles || unstagedFiles) {
878
905
  warning('No differences with remote, but you have uncommitted local changes.');
879
906
  console.log('Staged changes:', stagedFiles || 'none');
@@ -902,14 +929,26 @@ function analyzeDiff(args) {
902
929
  error('Error getting diff or commits: ' + e.message);
903
930
  return;
904
931
  }
905
-
932
+
906
933
  // Create the prompt for Claude
907
934
  const tempDir = `/tmp/claude-analyze-${Date.now()}`;
908
935
  fs.mkdirSync(tempDir, { recursive: true });
909
-
936
+
937
+ // Check if subagents should be used
938
+ const useSubagents = process.env.CLAUDE_USE_SUBAGENTS === 'true';
939
+ const subagentModel = process.env.CLAUDE_SUBAGENT_MODEL || 'haiku';
940
+ let subagentBatchSize = parseInt(process.env.CLAUDE_SUBAGENT_BATCH_SIZE || '3');
941
+ // Validate batch size (must be >= 1)
942
+ if (subagentBatchSize < 1) {
943
+ subagentBatchSize = 1;
944
+ }
945
+ const subagentInstruction = useSubagents
946
+ ? `\n\nIMPORTANT PARALLEL PROCESSING: If analyzing 3+ files, process them in batches of ${subagentBatchSize}. For EACH batch, create that many subagents in parallel using Task tool (send single message with multiple Task calls). Each subagent analyzes one file and provides insights. After ALL batches complete, consolidate into SINGLE JSON with ONE cohesive PR title/description. Model: ${subagentModel}. Example: 4 files with BATCH_SIZE=1 → 4 sequential batches of 1 subagent each. Example: 4 files with BATCH_SIZE=3 → batch 1 has 3 parallel subagents (files 1-3), batch 2 has 1 subagent (file 4).\n`
947
+ : '';
948
+
910
949
  const promptFile = path.join(tempDir, 'prompt.txt');
911
950
  const prompt = `Analyze the following changes. CONTEXT: ${contextDescription}
912
-
951
+ ${subagentInstruction}
913
952
  Please generate:
914
953
  1. A concise and descriptive PR title (maximum 72 characters)
915
954
  2. A detailed PR description that includes:
@@ -943,10 +982,11 @@ ${fullDiff.substring(0, 50000)} ${fullDiff.length > 50000 ? '\n... (truncated di
943
982
  fs.writeFileSync(promptFile, prompt);
944
983
 
945
984
  info('Sending to Claude for analysis...');
946
-
985
+ const startTime = Date.now();
986
+
947
987
  try {
948
988
  const response = execSync(`claude < "${promptFile}"`, { encoding: 'utf8', maxBuffer: 1024 * 1024 * 10 });
949
-
989
+
950
990
  // Extraer el JSON de la respuesta
951
991
  const jsonMatch = response.match(/\{[\s\S]*\}/);
952
992
  if (!jsonMatch) {
@@ -954,7 +994,7 @@ ${fullDiff.substring(0, 50000)} ${fullDiff.length > 50000 ? '\n... (truncated di
954
994
  console.log('Complete response:', response);
955
995
  return;
956
996
  }
957
-
997
+
958
998
  let result;
959
999
  try {
960
1000
  result = JSON.parse(jsonMatch[0]);
@@ -963,44 +1003,44 @@ ${fullDiff.substring(0, 50000)} ${fullDiff.length > 50000 ? '\n... (truncated di
963
1003
  console.log('JSON received:', jsonMatch[0]);
964
1004
  return;
965
1005
  }
966
-
1006
+
967
1007
  // Show the results
968
1008
  console.log('');
969
1009
  console.log('════════════════════════════════════════════════════════════════');
970
1010
  console.log(' DIFFERENCES ANALYSIS ');
971
1011
  console.log('════════════════════════════════════════════════════════════════');
972
1012
  console.log('');
973
-
1013
+
974
1014
  console.log(`🔍 ${colors.blue}Context:${colors.reset} ${contextDescription}`);
975
1015
  console.log(`📊 ${colors.blue}Changed Files:${colors.reset} ${diffFiles.split('\n').length}`);
976
1016
  console.log('');
977
-
1017
+
978
1018
  console.log(`📝 ${colors.green}Pull Request Title:${colors.reset}`);
979
1019
  console.log(` ${result.prTitle}`);
980
1020
  console.log('');
981
-
1021
+
982
1022
  console.log(`🌿 ${colors.green}Suggested branch name:${colors.reset}`);
983
1023
  console.log(` ${result.suggestedBranchName}`);
984
1024
  console.log('');
985
-
1025
+
986
1026
  console.log(`📋 ${colors.green}Type of change:${colors.reset} ${result.changeType}`);
987
-
1027
+
988
1028
  if (result.breakingChanges) {
989
1029
  console.log(`⚠️ ${colors.yellow}Breaking Changes: SÍ${colors.reset}`);
990
1030
  }
991
-
1031
+
992
1032
  console.log('');
993
1033
  console.log(`📄 ${colors.green}Pull Request Description:${colors.reset}`);
994
1034
  console.log('───────────────────────────────────────────────────────────────');
995
1035
  console.log(result.prDescription);
996
1036
  console.log('───────────────────────────────────────────────────────────────');
997
-
1037
+
998
1038
  if (result.testingNotes) {
999
1039
  console.log('');
1000
1040
  console.log(`🧪 ${colors.green}Testing notes:${colors.reset}`);
1001
1041
  console.log(result.testingNotes);
1002
1042
  }
1003
-
1043
+
1004
1044
  // Guardar los resultados en un archivo con contexto
1005
1045
  const outputData = {
1006
1046
  ...result,
@@ -1012,12 +1052,17 @@ ${fullDiff.substring(0, 50000)} ${fullDiff.length > 50000 ? '\n... (truncated di
1012
1052
  timestamp: new Date().toISOString()
1013
1053
  }
1014
1054
  };
1015
-
1055
+
1016
1056
  const outputFile = '.claude-pr-analysis.json';
1017
1057
  fs.writeFileSync(outputFile, JSON.stringify(outputData, null, 2));
1058
+
1059
+ const elapsed = Date.now() - startTime;
1060
+ const seconds = Math.floor(elapsed / 1000);
1061
+ const ms = elapsed % 1000;
1018
1062
  console.log('');
1063
+ console.log(`${colors.blue}⏱️ Analysis completed in ${seconds}.${ms}s${colors.reset}`);
1019
1064
  info(`Results saved in ${outputFile}`);
1020
-
1065
+
1021
1066
  // Sugerencias contextuales
1022
1067
  console.log('');
1023
1068
  if (!args[0] && contextDescription.includes('local changes without push')) {
@@ -1030,9 +1075,9 @@ ${fullDiff.substring(0, 50000)} ${fullDiff.length > 50000 ? '\n... (truncated di
1030
1075
  console.log(`💡 ${colors.yellow}For renaming your current branch:${colors.reset}`);
1031
1076
  console.log(` git branch -m ${result.suggestedBranchName}`);
1032
1077
  }
1033
-
1078
+
1034
1079
  console.log(`💡 ${colors.yellow}Tip:${colors.reset} Use this information to create your PR on GitHub.`);
1035
-
1080
+
1036
1081
  } catch (e) {
1037
1082
  error('Error executing Claude: ' + e.message);
1038
1083
  } finally {
@@ -1053,7 +1098,7 @@ function status() {
1053
1098
  hooks.forEach(hook => {
1054
1099
  const enabledPath = `.git/hooks/${hook}`;
1055
1100
  const disabledPath = `.git/hooks/${hook}.disabled`;
1056
-
1101
+
1057
1102
  if (fs.existsSync(enabledPath)) {
1058
1103
  success(`${hook}: enabled`);
1059
1104
  } else if (fs.existsSync(disabledPath)) {
@@ -1084,7 +1129,7 @@ function status() {
1084
1129
  const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
1085
1130
  const claudeIgnores = ['.claude/', 'debug-claude-response.json', '.claude-pr-analysis.json'];
1086
1131
  let allPresent = true;
1087
-
1132
+
1088
1133
  claudeIgnores.forEach(entry => {
1089
1134
  const regex = new RegExp(`^${entry.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'm');
1090
1135
  if (regex.test(gitignoreContent)) {
@@ -1094,7 +1139,7 @@ function status() {
1094
1139
  allPresent = false;
1095
1140
  }
1096
1141
  });
1097
-
1142
+
1098
1143
  if (!allPresent) {
1099
1144
  info('\nRun "claude-hooks install" to update .gitignore');
1100
1145
  }
@@ -1116,12 +1161,12 @@ function compareVersions(v1, v2) {
1116
1161
  // Usar el script compartido para mantener consistencia
1117
1162
  const result = execSync(`bash -c 'source "${getTemplatesPath()}/check-version.sh" && compare_versions "${v1}" "${v2}"; echo $?'`, { encoding: 'utf8' }).trim();
1118
1163
  const exitCode = parseInt(result);
1119
-
1164
+
1120
1165
  // Convertir los códigos de retorno del script bash a valores JS
1121
1166
  if (exitCode === 0) return 0; // iguales
1122
1167
  if (exitCode === 1) return 1; // v1 > v2
1123
1168
  if (exitCode === 2) return -1; // v1 < v2
1124
-
1169
+
1125
1170
  // Fallback: comparación simple si el script falla
1126
1171
  if (v1 === v2) return 0;
1127
1172
  const sorted = [v1, v2].sort((a, b) => {
@@ -1145,13 +1190,13 @@ function compareVersions(v1, v2) {
1145
1190
  // Update command - update to the latest version
1146
1191
  async function update() {
1147
1192
  info('Checking latest available version...');
1148
-
1193
+
1149
1194
  try {
1150
1195
  const currentVersion = require('../package.json').version;
1151
1196
  const latestVersion = await getLatestVersion('claude-git-hooks');
1152
-
1197
+
1153
1198
  const comparison = compareVersions(currentVersion, latestVersion);
1154
-
1199
+
1155
1200
  if (comparison === 0) {
1156
1201
  success(`You already have the latest version installed (${currentVersion})`);
1157
1202
  return;
@@ -1161,20 +1206,20 @@ async function update() {
1161
1206
  success(`You already have the latest version installed (${currentVersion})`);
1162
1207
  return;
1163
1208
  }
1164
-
1209
+
1165
1210
  info(`Current version: ${currentVersion}`);
1166
1211
  info(`Available version: ${latestVersion}`);
1167
-
1212
+
1168
1213
  // Actualizar el paquete
1169
1214
  info('Updating claude-git-hooks...');
1170
1215
  try {
1171
1216
  execSync('npm install -g claude-git-hooks@latest', { stdio: 'inherit' });
1172
1217
  success(`Successfully updated to version ${latestVersion}`);
1173
-
1218
+
1174
1219
  // Reinstall hooks with the new version
1175
1220
  info('Reinstalling hooks with the new version...');
1176
1221
  await install(['--force']);
1177
-
1222
+
1178
1223
  } catch (updateError) {
1179
1224
  error('Error updating. Try running: npm install -g claude-git-hooks@latest');
1180
1225
  }
@@ -1241,6 +1286,18 @@ Exclude code from analysis:
1241
1286
  ...excluded code...
1242
1287
  // SKIP_ANALYSIS_BLOCK
1243
1288
 
1289
+ Performance optimization (NEW in v1.5.5):
1290
+ export CLAUDE_USE_SUBAGENTS=true # Enable parallel analysis for 3+ files
1291
+ export CLAUDE_SUBAGENT_MODEL=haiku # Model: haiku (fast), sonnet, opus
1292
+ export CLAUDE_SUBAGENT_BATCH_SIZE=3 # Parallel subagents per batch (default: 3)
1293
+
1294
+ # Batching examples:
1295
+ # BATCH_SIZE=1 with 4 files → 4 sequential batches (1 subagent each)
1296
+ # BATCH_SIZE=3 with 4 files → 2 batches (3 parallel, then 1)
1297
+ # BATCH_SIZE=4 with 4 files → 1 batch (4 parallel subagents)
1298
+
1299
+ # Benefits: Faster for multi-file commits, shows execution time
1300
+
1244
1301
  More information: https://github.com/pablorovito/claude-git-hooks
1245
1302
  `);
1246
1303
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-git-hooks",
3
- "version": "1.5.3",
3
+ "version": "1.5.5",
4
4
  "description": "Git hooks con Claude CLI para análisis de código y generación automática de mensajes de commit",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
File without changes
File without changes
@@ -5,7 +5,6 @@ repo:{{REPO_NAME}}
5
5
  branch:{{BRANCH_NAME}}
6
6
  commit:{{COMMIT_SHA}}
7
7
  files:{{FILE_COUNT}}
8
- mode:{{ANALYSIS_MODE}}
9
8
 
10
9
  CRITICAL_ISSUES:
11
10
  {{BLOCKING_ISSUES}}
File without changes
@@ -9,6 +9,14 @@ set -e
9
9
  CLAUDE_CLI="claude"
10
10
  TEMP_DIR="/tmp/code-review-$$"
11
11
  MAX_FILE_SIZE=100000 # 100KB maximum per file
12
+ USE_SUBAGENTS="${CLAUDE_USE_SUBAGENTS:-false}" # Use subagents for parallel analysis
13
+ SUBAGENT_MODEL="${CLAUDE_SUBAGENT_MODEL:-haiku}" # Model for subagents (haiku/sonnet/opus)
14
+ SUBAGENT_BATCH_SIZE="${CLAUDE_SUBAGENT_BATCH_SIZE:-3}" # Number of parallel subagents per batch
15
+
16
+ # Validate batch size (must be >= 1)
17
+ if [ "$SUBAGENT_BATCH_SIZE" -le 0 ] 2>/dev/null; then
18
+ SUBAGENT_BATCH_SIZE=1
19
+ fi
12
20
 
13
21
  # Colors for output
14
22
  RED='\033[0;31m'
@@ -30,6 +38,16 @@ warning() {
30
38
  echo -e "${YELLOW}[WARNING]${NC} $1"
31
39
  }
32
40
 
41
+ # Function to show elapsed time
42
+ show_elapsed_time() {
43
+ local start_ms=$1
44
+ local end_ms=$(date +%s%3N 2>/dev/null || echo $(($(date +%s) * 1000)))
45
+ local elapsed=$((end_ms - start_ms))
46
+ local seconds=$((elapsed / 1000))
47
+ local ms=$((elapsed % 1000))
48
+ echo -e "${BLUE}⏱️ Analysis completed in ${seconds}.${ms}s${NC}"
49
+ }
50
+
33
51
  # Check version at start (before any analysis)
34
52
  # Try to find the check-version.sh script
35
53
  CHECK_VERSION_SCRIPT=""
@@ -166,6 +184,15 @@ cleanup() {
166
184
  rm -rf "$TEMP_DIR"
167
185
  }
168
186
 
187
+ # Function to inject subagent instruction for parallel analysis
188
+ inject_subagent_instruction() {
189
+ if [ "$USE_SUBAGENTS" = "true" ]; then
190
+ echo ""
191
+ echo "IMPORTANT PARALLEL PROCESSING: If analyzing 3+ files, process them in batches of ${SUBAGENT_BATCH_SIZE}. For EACH batch, create that many subagents in parallel using Task tool (send single message with multiple Task calls). Each subagent analyzes one assigned file following OUTPUT_SCHEMA. After ALL batches complete, consolidate results into SINGLE JSON: (1) merge blockingIssues arrays, (2) merge details arrays, (3) sum issue counts, (4) worst-case metrics (lowest rating), (5) QUALITY_GATE=FAILED if ANY subagent found blockers/criticals, (6) approved=false if any disapproved. Model: ${SUBAGENT_MODEL}. Example: 4 files with BATCH_SIZE=1 → 4 sequential batches of 1 subagent each. Example: 4 files with BATCH_SIZE=3 → batch 1 has 3 parallel subagents (files 1-3), batch 2 has 1 subagent (file 4)."
192
+ echo ""
193
+ fi
194
+ }
195
+
169
196
  # Configure cleanup on exit
170
197
  trap cleanup EXIT
171
198
 
@@ -250,6 +277,10 @@ cat "$PROMPT_TEMPLATE" > "$PROMPT_FILE"
250
277
  # Add the guidelines
251
278
  echo "=== EVALUATION GUIDELINES ===" >> "$PROMPT_FILE"
252
279
  cat "$GUIDELINES_FILE" >> "$PROMPT_FILE"
280
+
281
+ # Inject subagent instruction if enabled
282
+ inject_subagent_instruction >> "$PROMPT_FILE"
283
+
253
284
  echo -e "\n\n=== CHANGES TO REVIEW ===\n" >> "$PROMPT_FILE"
254
285
 
255
286
  # Process each Java file
@@ -300,6 +331,7 @@ log "Sending $FILE_COUNT files for review with Claude..."
300
331
 
301
332
  # Send to Claude and capture response
302
333
  RESPONSE_FILE="$TEMP_DIR/code_review_response.txt"
334
+ START_TIME=$(date +%s%3N 2>/dev/null || echo $(($(date +%s) * 1000)))
303
335
 
304
336
  # Execute Claude CLI and capture the response
305
337
  if $CLAUDE_CLI < "$PROMPT_FILE" > "$RESPONSE_FILE" 2>&1; then
@@ -332,126 +364,98 @@ if $CLAUDE_CLI < "$PROMPT_FILE" > "$RESPONSE_FILE" 2>&1; then
332
364
  BLOCKING_ISSUES=$(echo "$JSON_RESPONSE" | jq -r '.blockingIssues[].description' 2>/dev/null | sed '/^$/d')
333
365
  fi
334
366
 
367
+ # Always use SonarQube mode (as per v1.4.1)
335
368
  QUALITY_GATE=$(echo "$JSON_RESPONSE" | jq -r '.QUALITY_GATE // ""' 2>/dev/null)
336
-
337
- if [ -n "$QUALITY_GATE" ] && [ "$QUALITY_GATE" != "null" ]; then
338
- # Show SonarQube style results
339
- echo
340
- echo "╔════════════════════════════════════════════════════════════════════╗"
341
- echo "║ CODE QUALITY ANALYSIS ║"
342
- echo "╚════════════════════════════════════════════════════════════════════╝"
369
+
370
+ # Show SonarQube style results
371
+ echo
372
+ echo "╔════════════════════════════════════════════════════════════════════╗"
373
+ echo "║ CODE QUALITY ANALYSIS ║"
374
+ echo "╚════════════════════════════════════════════════════════════════════╝"
375
+ echo
376
+
377
+ # Quality Gate Status
378
+ if [ "$QUALITY_GATE" = "PASSED" ]; then
379
+ echo -e "${GREEN}✓ Quality Gate: PASSED${NC}"
380
+ else
381
+ echo -e "${RED}✗ Quality Gate: FAILED${NC}"
382
+ fi
383
+ echo
384
+
385
+ # Metrics
386
+ METRICS=$(echo "$JSON_RESPONSE" | jq -r '.metrics // {}' 2>/dev/null)
387
+ if [ "$METRICS" != "{}" ] && [ "$METRICS" != "null" ]; then
388
+ echo "📊 METRICS"
389
+ echo "├─ Reliability: $(echo "$METRICS" | jq -r '.reliability // "?"' 2>/dev/null)"
390
+ echo "├─ Security: $(echo "$METRICS" | jq -r '.security // "?"' 2>/dev/null)"
391
+ echo "├─ Maintainability: $(echo "$METRICS" | jq -r '.maintainability // "?"' 2>/dev/null)"
392
+ echo "├─ Coverage: $(echo "$METRICS" | jq -r '.coverage // "?"' 2>/dev/null)%"
393
+ echo "├─ Duplications: $(echo "$METRICS" | jq -r '.duplications // "?"' 2>/dev/null)%"
394
+ echo "└─ Complexity: $(echo "$METRICS" | jq -r '.complexity // "?"' 2>/dev/null)"
343
395
  echo
344
-
345
- # Quality Gate Status
346
- if [ "$QUALITY_GATE" = "PASSED" ]; then
347
- echo -e "${GREEN}✓ Quality Gate: PASSED${NC}"
348
- else
349
- echo -e "${RED}✗ Quality Gate: FAILED${NC}"
350
- fi
396
+ fi
397
+
398
+ # Issues Summary
399
+ ISSUES=$(echo "$JSON_RESPONSE" | jq -r '.issues // {}' 2>/dev/null)
400
+ if [ "$ISSUES" != "{}" ] && [ "$ISSUES" != "null" ]; then
401
+ echo "📋 ISSUES SUMMARY"
402
+ BLOCKER=$(echo "$ISSUES" | jq -r '.blocker // 0' 2>/dev/null)
403
+ CRITICAL=$(echo "$ISSUES" | jq -r '.critical // 0' 2>/dev/null)
404
+ MAJOR=$(echo "$ISSUES" | jq -r '.major // 0' 2>/dev/null)
405
+ MINOR=$(echo "$ISSUES" | jq -r '.minor // 0' 2>/dev/null)
406
+ INFO=$(echo "$ISSUES" | jq -r '.info // 0' 2>/dev/null)
407
+ TOTAL=$((BLOCKER + CRITICAL + MAJOR + MINOR + INFO))
408
+
409
+ echo "Total: $TOTAL issues found"
410
+ [ "$BLOCKER" -gt 0 ] && echo -e " ${RED}🔴 Blocker: $BLOCKER${NC}"
411
+ [ "$CRITICAL" -gt 0 ] && echo -e " ${RED}🟠 Critical: $CRITICAL${NC}"
412
+ [ "$MAJOR" -gt 0 ] && echo -e " ${YELLOW}🟡 Major: $MAJOR${NC}"
413
+ [ "$MINOR" -gt 0 ] && echo " 🔵 Minor: $MINOR"
414
+ [ "$INFO" -gt 0 ] && echo " ⚪ Info: $INFO"
351
415
  echo
352
-
353
- # Metrics
354
- METRICS=$(echo "$JSON_RESPONSE" | jq -r '.metrics // {}' 2>/dev/null)
355
- if [ "$METRICS" != "{}" ] && [ "$METRICS" != "null" ]; then
356
- echo "📊 METRICS"
357
- echo "├─ Reliability: $(echo "$METRICS" | jq -r '.reliability // "?"' 2>/dev/null)"
358
- echo "├─ Security: $(echo "$METRICS" | jq -r '.security // "?"' 2>/dev/null)"
359
- echo "├─ Maintainability: $(echo "$METRICS" | jq -r '.maintainability // "?"' 2>/dev/null)"
360
- echo "├─ Coverage: $(echo "$METRICS" | jq -r '.coverage // "?"' 2>/dev/null)%"
361
- echo "├─ Duplications: $(echo "$METRICS" | jq -r '.duplications // "?"' 2>/dev/null)%"
362
- echo "└─ Complexity: $(echo "$METRICS" | jq -r '.complexity // "?"' 2>/dev/null)"
363
- echo
364
- fi
365
-
366
- # Issues Summary
367
- ISSUES=$(echo "$JSON_RESPONSE" | jq -r '.issues // {}' 2>/dev/null)
368
- if [ "$ISSUES" != "{}" ] && [ "$ISSUES" != "null" ]; then
369
- echo "📋 ISSUES SUMMARY"
370
- BLOCKER=$(echo "$ISSUES" | jq -r '.blocker // 0' 2>/dev/null)
371
- CRITICAL=$(echo "$ISSUES" | jq -r '.critical // 0' 2>/dev/null)
372
- MAJOR=$(echo "$ISSUES" | jq -r '.major // 0' 2>/dev/null)
373
- MINOR=$(echo "$ISSUES" | jq -r '.minor // 0' 2>/dev/null)
374
- INFO=$(echo "$ISSUES" | jq -r '.info // 0' 2>/dev/null)
375
- TOTAL=$((BLOCKER + CRITICAL + MAJOR + MINOR + INFO))
376
-
377
- echo "Total: $TOTAL issues found"
378
- [ "$BLOCKER" -gt 0 ] && echo -e " ${RED}🔴 Blocker: $BLOCKER${NC}"
379
- [ "$CRITICAL" -gt 0 ] && echo -e " ${RED}🟠 Critical: $CRITICAL${NC}"
380
- [ "$MAJOR" -gt 0 ] && echo -e " ${YELLOW}🟡 Major: $MAJOR${NC}"
381
- [ "$MINOR" -gt 0 ] && echo " 🔵 Minor: $MINOR"
382
- [ "$INFO" -gt 0 ] && echo " ⚪ Info: $INFO"
383
- echo
384
- fi
385
-
386
- # Detailed Issues
387
- DETAILS_COUNT=$(echo "$JSON_RESPONSE" | jq -r '.details | length' 2>/dev/null)
388
- if [ "$DETAILS_COUNT" -gt 0 ] 2>/dev/null; then
389
- echo "🔍 DETAILED ISSUES"
390
- echo "$JSON_RESPONSE" | jq -r '.details[]? |
391
- "[\(.severity)] \(.type) in \(.file):\(.line // "?")\n \(.message)\n"' 2>/dev/null
392
- fi
393
-
394
- # Security Hotspots
395
- HOTSPOTS=$(echo "$JSON_RESPONSE" | jq -r '.securityHotspots // 0' 2>/dev/null)
396
- if [ "$HOTSPOTS" -gt 0 ] 2>/dev/null; then
397
- echo "🔥 SECURITY HOTSPOTS: $HOTSPOTS found"
398
- echo " Review security-sensitive code carefully"
416
+ fi
399
417
 
400
- echo
401
- fi
402
-
403
- # Check if commit should be blocked
404
- if [ "$QUALITY_GATE" = "FAILED" ] || [ "$APPROVED" = "false" ]; then
405
- echo
406
- error "❌ Commit blocked due to quality gate failure"
407
- exit 1
408
- fi
409
-
418
+ # Detailed Issues
419
+ DETAILS_COUNT=$(echo "$JSON_RESPONSE" | jq -r '.details | length' 2>/dev/null)
420
+ if [ "$DETAILS_COUNT" -gt 0 ] 2>/dev/null; then
421
+ echo "🔍 DETAILED ISSUES"
422
+ echo "$JSON_RESPONSE" | jq -r '.details[]? |
423
+ "[\(.severity)] \(.type) in \(.file):\(.line // "?")\n \(.message)\n"' 2>/dev/null
424
+ fi
425
+
426
+ # Security Hotspots
427
+ HOTSPOTS=$(echo "$JSON_RESPONSE" | jq -r '.securityHotspots // 0' 2>/dev/null)
428
+ if [ "$HOTSPOTS" -gt 0 ] 2>/dev/null; then
429
+ echo "🔥 SECURITY HOTSPOTS: $HOTSPOTS found"
430
+ echo " Review security-sensitive code carefully"
410
431
  echo
411
- log "✅ Code analysis completed. Quality gate passed."
412
- else
413
- # Show results in classic format
432
+ fi
433
+
434
+ # Check if commit should be blocked
435
+ if [ "$QUALITY_GATE" = "FAILED" ] || [ "$APPROVED" = "false" ]; then
414
436
  echo
415
- echo "=== REVIEW RESULTS ==="
416
- echo "Score: $SCORE/10"
417
-
418
- if [ -n "$RECOMMENDATIONS" ] && [ "$RECOMMENDATIONS" != "null" ]; then
437
+ show_elapsed_time "$START_TIME"
438
+ error " Commit blocked due to quality gate failure"
439
+
440
+ # Show blocking issues if they exist
441
+ if [ -n "$BLOCKING_ISSUES" ] && [ "$BLOCKING_ISSUES" != "null" ]; then
419
442
  echo
420
- echo "=== RECOMMENDATIONS ==="
421
- echo "$RECOMMENDATIONS" | sed 's/^/- /'
422
- fi
423
-
424
- # Check if the commit should be blocked
425
- if [ "$APPROVED" = "false" ]; then
426
- error "❌ Commit rejected due to critical issues"
427
- if [ -n "$BLOCKING_ISSUES" ] && [ "$BLOCKING_ISSUES" != "null" ]; then
428
- echo
429
- echo "=== CRITICAL ISSUES ==="
430
- echo "$BLOCKING_ISSUES" | sed 's/^/- /'
431
-
432
- # Generate AI-friendly resolution prompt
433
- generate_resolution_prompt
434
- fi
435
- exit 1
443
+ echo "=== CRITICAL ISSUES ==="
444
+ echo "$BLOCKING_ISSUES" | sed 's/^/- /'
436
445
  fi
437
446
 
438
- # Show additional details if they exist
439
- DETAILS=$(echo "$JSON_RESPONSE" | jq -r '.details // null')
440
- if [ -n "$DETAILS" ] && [ "$DETAILS" != "null" ]; then
441
- echo
442
- echo "=== ADDITIONAL DETAILS ==="
443
- # If details is a string, print it directly
444
- if echo "$DETAILS" | jq -e 'type == "string"' >/dev/null 2>&1; then
445
- echo "$DETAILS" | jq -r '.'
446
- # If it's an object or array, format it
447
- else
448
- echo "$DETAILS" | jq '.'
449
- fi
447
+ # Generate AI-friendly resolution prompt if there are blocking issues
448
+ if [ "$BLOCKING_COUNT" -gt 0 ]; then
449
+ generate_resolution_prompt
450
450
  fi
451
-
452
- log "✅ Review completed. Commit approved (Score: $SCORE/10)"
451
+
452
+ exit 1
453
453
  fi
454
-
454
+
455
+ echo
456
+ show_elapsed_time "$START_TIME"
457
+ log "✅ Code analysis completed. Quality gate passed."
458
+
455
459
  else
456
460
  error "Error executing Claude CLI"
457
461
  error "Check that Claude CLI is configured correctly"
@@ -10,11 +10,20 @@ CLAUDE_CLI="claude"
10
10
  TEMP_DIR="/tmp/commit-msg-$$"
11
11
  MAX_FILE_SIZE=100000
12
12
  AUTO_COMMIT_ENABLED=true
13
+ USE_SUBAGENTS="${CLAUDE_USE_SUBAGENTS:-false}" # Use subagents for parallel analysis
14
+ SUBAGENT_MODEL="${CLAUDE_SUBAGENT_MODEL:-haiku}" # Model for subagents (haiku/sonnet/opus)
15
+ SUBAGENT_BATCH_SIZE="${CLAUDE_SUBAGENT_BATCH_SIZE:-3}" # Number of parallel subagents per batch
16
+
17
+ # Validate batch size (must be >= 1)
18
+ if [ "$SUBAGENT_BATCH_SIZE" -le 0 ] 2>/dev/null; then
19
+ SUBAGENT_BATCH_SIZE=1
20
+ fi
13
21
 
14
22
  # Colors for output
15
23
  RED='\033[0;31m'
16
24
  GREEN='\033[0;32m'
17
25
  YELLOW='\033[1;33m'
26
+ BLUE='\033[0;34m'
18
27
  NC='\033[0m'
19
28
 
20
29
  # Function for logging
@@ -26,10 +35,30 @@ warning() {
26
35
  printf "${YELLOW}[WARNING]${NC} %s\n" "$1" >&2
27
36
  }
28
37
 
38
+ # Function to show elapsed time
39
+ show_elapsed_time() {
40
+ local start_ms=$1
41
+ local end_ms=$(date +%s%3N 2>/dev/null || echo $(($(date +%s) * 1000)))
42
+ local elapsed=$((end_ms - start_ms))
43
+ local seconds=$((elapsed / 1000))
44
+ local ms=$((elapsed % 1000))
45
+ printf "${BLUE}⏱️ Message generation completed in ${seconds}.${ms}s${NC}\n" >&2
46
+ }
47
+
29
48
  # Function to clean temporary files
30
49
  cleanup() {
31
50
  rm -rf "$TEMP_DIR"
32
51
  }
52
+
53
+ # Function to inject subagent instruction for parallel analysis
54
+ inject_subagent_instruction() {
55
+ if [ "$USE_SUBAGENTS" = "true" ]; then
56
+ echo ""
57
+ echo "IMPORTANT PARALLEL PROCESSING: If analyzing 3+ files, process them in batches of ${SUBAGENT_BATCH_SIZE}. For EACH batch, create that many subagents in parallel using Task tool (send single message with multiple Task calls). Each subagent analyzes one file and provides summary. After ALL batches complete, consolidate into SINGLE JSON with ONE commit message describing all changes. Determine type based on predominant change. Model: ${SUBAGENT_MODEL}. Example: 4 files with BATCH_SIZE=1 → 4 sequential batches of 1 subagent each. Example: 4 files with BATCH_SIZE=3 → batch 1 has 3 parallel subagents (files 1-3), batch 2 has 1 subagent (file 4)."
58
+ echo ""
59
+ fi
60
+ }
61
+
33
62
  trap cleanup EXIT
34
63
 
35
64
  # Hook arguments
@@ -76,6 +105,9 @@ Respond ONLY with a valid JSON:
76
105
  CHANGES TO ANALYZE:
77
106
  EOF
78
107
 
108
+ # Inject subagent instruction if enabled
109
+ inject_subagent_instruction >> "$PROMPT_FILE"
110
+
79
111
  # Get staged files
80
112
  ALL_STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null || echo "")
81
113
 
@@ -105,6 +137,7 @@ done
105
137
 
106
138
  RESPONSE_FILE="$TEMP_DIR/commit_msg_response.txt"
107
139
  log "Generating commit message with Claude..."
140
+ START_TIME=$(date +%s%3N 2>/dev/null || echo $(($(date +%s) * 1000)))
108
141
 
109
142
  if $CLAUDE_CLI < "$PROMPT_FILE" > "$RESPONSE_FILE" 2>&1; then
110
143
  JSON_MSG=$(sed -n '/^{/,/^}/p' "$RESPONSE_FILE" | head -n 1000)
@@ -127,6 +160,7 @@ if $CLAUDE_CLI < "$PROMPT_FILE" > "$RESPONSE_FILE" 2>&1; then
127
160
  fi
128
161
 
129
162
  printf "%s\n" "$FULL_MESSAGE" > "$COMMIT_MSG_FILE"
163
+ show_elapsed_time "$START_TIME"
130
164
  log "📝 Message generated: $(echo "$FULL_MESSAGE" | head -n 1)"
131
165
  exit 0
132
166
  fi