claude-git-hooks 2.18.1 → 2.20.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 (54) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/CLAUDE.md +85 -38
  3. package/README.md +52 -18
  4. package/bin/claude-hooks +75 -89
  5. package/lib/cli-metadata.js +306 -0
  6. package/lib/commands/analyze-diff.js +12 -10
  7. package/lib/commands/analyze.js +9 -5
  8. package/lib/commands/bump-version.js +56 -40
  9. package/lib/commands/create-pr.js +71 -34
  10. package/lib/commands/debug.js +4 -7
  11. package/lib/commands/diff-batch-info.js +105 -0
  12. package/lib/commands/generate-changelog.js +3 -2
  13. package/lib/commands/help.js +47 -27
  14. package/lib/commands/helpers.js +66 -43
  15. package/lib/commands/hooks.js +15 -13
  16. package/lib/commands/install.js +546 -49
  17. package/lib/commands/migrate-config.js +8 -11
  18. package/lib/commands/presets.js +6 -13
  19. package/lib/commands/setup-github.js +12 -3
  20. package/lib/commands/telemetry-cmd.js +8 -6
  21. package/lib/commands/update.js +1 -2
  22. package/lib/config.js +36 -52
  23. package/lib/hooks/pre-commit.js +70 -64
  24. package/lib/hooks/prepare-commit-msg.js +35 -75
  25. package/lib/utils/analysis-engine.js +77 -54
  26. package/lib/utils/changelog-generator.js +63 -37
  27. package/lib/utils/claude-client.js +447 -438
  28. package/lib/utils/claude-diagnostics.js +20 -10
  29. package/lib/utils/diff-analysis-orchestrator.js +332 -0
  30. package/lib/utils/file-operations.js +51 -79
  31. package/lib/utils/file-utils.js +6 -7
  32. package/lib/utils/git-operations.js +140 -123
  33. package/lib/utils/git-tag-manager.js +24 -23
  34. package/lib/utils/github-api.js +85 -61
  35. package/lib/utils/github-client.js +12 -14
  36. package/lib/utils/installation-diagnostics.js +4 -4
  37. package/lib/utils/interactive-ui.js +29 -17
  38. package/lib/utils/judge.js +195 -0
  39. package/lib/utils/logger.js +4 -1
  40. package/lib/utils/package-info.js +0 -11
  41. package/lib/utils/pr-metadata-engine.js +67 -33
  42. package/lib/utils/preset-loader.js +20 -62
  43. package/lib/utils/prompt-builder.js +57 -68
  44. package/lib/utils/resolution-prompt.js +34 -52
  45. package/lib/utils/sanitize.js +20 -19
  46. package/lib/utils/task-id.js +27 -40
  47. package/lib/utils/telemetry.js +73 -25
  48. package/lib/utils/version-manager.js +147 -70
  49. package/lib/utils/which-command.js +23 -12
  50. package/package.json +1 -1
  51. package/templates/CLAUDE_RESOLUTION_PROMPT.md +17 -9
  52. package/templates/DIFF_ANALYSIS_ORCHESTRATION_PROMPT.md +70 -0
  53. package/templates/config.advanced.example.json +15 -31
  54. package/templates/config.example.json +0 -11
@@ -21,7 +21,7 @@ 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 { loadPrompt } from '../utils/prompt-builder.js';
24
- import { getVersion, calculateBatches } from '../utils/package-info.js';
24
+ import { getVersion } from '../utils/package-info.js';
25
25
  import logger from '../utils/logger.js';
26
26
  import { getConfig } from '../config.js';
27
27
  import { getOrPromptTaskId, formatWithTaskId } from '../utils/task-id.js';
@@ -35,11 +35,9 @@ import { getOrPromptTaskId, formatWithTaskId } from '../utils/task-id.js';
35
35
  * @returns {Promise<string>} Complete prompt
36
36
  */
37
37
  const buildCommitPrompt = async (filesData, stats) => {
38
- logger.debug(
39
- 'prepare-commit-msg - buildCommitPrompt',
40
- 'Building commit message prompt',
41
- { fileCount: filesData.length }
42
- );
38
+ logger.debug('prepare-commit-msg - buildCommitPrompt', 'Building commit message prompt', {
39
+ fileCount: filesData.length
40
+ });
43
41
 
44
42
  // Build file list
45
43
  const fileList = filesData.map(({ path }) => path).join('\n');
@@ -81,11 +79,11 @@ const buildCommitPrompt = async (filesData, stats) => {
81
79
  * }
82
80
  */
83
81
  const formatCommitMessage = (json) => {
84
- logger.debug(
85
- 'prepare-commit-msg - formatCommitMessage',
86
- 'Formatting commit message',
87
- { type: json.type, hasScope: !!json.scope, hasBody: !!json.body }
88
- );
82
+ logger.debug('prepare-commit-msg - formatCommitMessage', 'Formatting commit message', {
83
+ type: json.type,
84
+ hasScope: !!json.scope,
85
+ hasBody: !!json.body
86
+ });
89
87
 
90
88
  const type = json.type || 'feat';
91
89
  const scope = json.scope && json.scope !== 'null' ? json.scope : null;
@@ -131,19 +129,12 @@ const main = async () => {
131
129
  const commitMsgFile = args[0];
132
130
  const commitSource = args[1];
133
131
 
134
- logger.debug(
135
- 'prepare-commit-msg - main',
136
- 'Hook started',
137
- { commitMsgFile, commitSource }
138
- );
132
+ logger.debug('prepare-commit-msg - main', 'Hook started', { commitMsgFile, commitSource });
139
133
 
140
134
  // Only process normal commits
141
135
  // Why: Don't interfere with merge commits, amend, squash, etc.
142
136
  if (commitSource && commitSource !== 'message') {
143
- logger.debug(
144
- 'prepare-commit-msg - main',
145
- `Skipping: commit source is ${commitSource}`
146
- );
137
+ logger.debug('prepare-commit-msg - main', `Skipping: commit source is ${commitSource}`);
147
138
  process.exit(0);
148
139
  }
149
140
 
@@ -151,45 +142,34 @@ const main = async () => {
151
142
  const currentMsg = await fs.readFile(commitMsgFile, 'utf8');
152
143
  const firstLine = currentMsg.split('\n')[0].trim();
153
144
 
154
- logger.debug(
155
- 'prepare-commit-msg - main',
156
- 'Current commit message',
157
- { firstLine }
158
- );
145
+ logger.debug('prepare-commit-msg - main', 'Current commit message', { firstLine });
159
146
 
160
147
  // Check if message is "auto"
161
148
  if (firstLine !== config.commitMessage.autoKeyword) {
162
- logger.debug(
163
- 'prepare-commit-msg - main',
164
- 'Not generating: message is not "auto"'
165
- );
149
+ logger.debug('prepare-commit-msg - main', 'Not generating: message is not "auto"');
166
150
  process.exit(0);
167
151
  }
168
152
 
169
153
  // Display configuration info
170
154
  const version = await getVersion();
171
- const subagentsEnabled = config.subagents?.enabled || false;
172
- const subagentModel = config.subagents?.model || 'haiku';
173
- const batchSize = config.subagents?.batchSize || 3;
174
-
175
155
  console.log(`\n🤖 claude-git-hooks v${version}`);
176
- if (subagentsEnabled) {
177
- console.log(`⚡ Parallel analysis: ${subagentModel} model, batch size ${batchSize}`);
178
- }
179
156
 
180
157
  logger.info('Generating commit message automatically...');
181
158
 
182
159
  // Step 1: Auto-detect task ID from branch (no prompt)
183
160
  logger.debug('prepare-commit-msg - main', 'Detecting task ID from branch');
184
161
  const taskId = await getOrPromptTaskId({
185
- prompt: false, // Don't prompt - just detect from branch
162
+ prompt: false, // Don't prompt - just detect from branch
186
163
  required: false,
187
- config // Pass config for custom pattern
164
+ config // Pass config for custom pattern
188
165
  });
189
166
 
190
167
  // Note: getOrPromptTaskId() already logs the task ID if found
191
168
  if (!taskId) {
192
- logger.debug('prepare-commit-msg - main', 'No task ID found in branch, continuing without it');
169
+ logger.debug(
170
+ 'prepare-commit-msg - main',
171
+ 'No task ID found in branch, continuing without it'
172
+ );
193
173
  }
194
174
 
195
175
  // Step 2: Get staged files
@@ -204,11 +184,10 @@ const main = async () => {
204
184
  // Get statistics
205
185
  const stats = getStagedStats();
206
186
 
207
- logger.debug(
208
- 'prepare-commit-msg - main',
209
- 'Staged files',
210
- { fileCount: stagedFiles.length, stats }
211
- );
187
+ logger.debug('prepare-commit-msg - main', 'Staged files', {
188
+ fileCount: stagedFiles.length,
189
+ stats
190
+ });
212
191
 
213
192
  // Build file data with diffs
214
193
  const filesData = await Promise.all(
@@ -228,7 +207,6 @@ const main = async () => {
228
207
  diff,
229
208
  size: fileStats.size
230
209
  };
231
-
232
210
  } catch (error) {
233
211
  logger.error(
234
212
  'prepare-commit-msg - main',
@@ -248,19 +226,7 @@ const main = async () => {
248
226
  // Build prompt
249
227
  const prompt = await buildCommitPrompt(filesData, stats);
250
228
 
251
- logger.debug(
252
- 'prepare-commit-msg - main',
253
- 'Prompt built',
254
- { promptLength: prompt.length }
255
- );
256
-
257
- // Calculate batches if subagents enabled and applicable
258
- if (subagentsEnabled && filesData.length >= 3) {
259
- const { numBatches, shouldShowBatches } = calculateBatches(filesData.length, batchSize);
260
- if (shouldShowBatches) {
261
- console.log(`📊 Analyzing ${filesData.length} files in ${numBatches} batch${numBatches > 1 ? 'es' : ''}`);
262
- }
263
- }
229
+ logger.debug('prepare-commit-msg - main', 'Prompt built', { promptLength: prompt.length });
264
230
 
265
231
  // Generate message with Claude
266
232
  logger.info('Sending to Claude...');
@@ -268,11 +234,8 @@ const main = async () => {
268
234
  // Build telemetry context
269
235
  const telemetryContext = {
270
236
  fileCount: filesData.length,
271
- batchSize: config.subagents?.batchSize || 3,
272
- totalBatches: subagentsEnabled && filesData.length >= 3
273
- ? Math.ceil(filesData.length / (config.subagents?.batchSize || 3))
274
- : 1,
275
- model: subagentModel,
237
+ batchSize: filesData.length,
238
+ totalBatches: 1,
276
239
  hook: 'prepare-commit-msg'
277
240
  };
278
241
 
@@ -282,11 +245,10 @@ const main = async () => {
282
245
  telemetryContext
283
246
  });
284
247
 
285
- logger.debug(
286
- 'prepare-commit-msg - main',
287
- 'Response received',
288
- { hasType: !!response.type, hasTitle: !!response.title }
289
- );
248
+ logger.debug('prepare-commit-msg - main', 'Response received', {
249
+ hasType: !!response.type,
250
+ hasTitle: !!response.title
251
+ });
290
252
 
291
253
  // Format message
292
254
  let message = formatCommitMessage(response);
@@ -294,15 +256,14 @@ const main = async () => {
294
256
  // Add task ID prefix if available
295
257
  if (taskId) {
296
258
  message = formatWithTaskId(message, taskId);
297
- logger.debug(
298
- 'prepare-commit-msg - main',
299
- 'Task ID added to message',
300
- { taskId, message: message.split('\n')[0] }
301
- );
259
+ logger.debug('prepare-commit-msg - main', 'Task ID added to message', {
260
+ taskId,
261
+ message: message.split('\n')[0]
262
+ });
302
263
  }
303
264
 
304
265
  // Write to commit message file
305
- await fs.writeFile(commitMsgFile, `${message }\n`, 'utf8');
266
+ await fs.writeFile(commitMsgFile, `${message}\n`, 'utf8');
306
267
 
307
268
  const duration = ((Date.now() - startTime) / 1000).toFixed(2);
308
269
  console.error(`⏱️ Message generation completed in ${duration}s`);
@@ -310,7 +271,6 @@ const main = async () => {
310
271
  logger.success(`Message generated: ${message.split('\n')[0]}`);
311
272
 
312
273
  process.exit(0);
313
-
314
274
  } catch (error) {
315
275
  const duration = ((Date.now() - startTime) / 1000).toFixed(2);
316
276
  console.error(`⏱️ Message generation failed after ${duration}s`);
@@ -30,7 +30,8 @@ import {
30
30
  getRepoName,
31
31
  getCurrentBranch
32
32
  } from './git-operations.js';
33
- import { analyzeCode, analyzeCodeParallel, chunkArray } from './claude-client.js';
33
+ import { analyzeCode } from './claude-client.js';
34
+ import { orchestrateBatches } from './diff-analysis-orchestrator.js';
34
35
  import { buildAnalysisPrompt } from './prompt-builder.js';
35
36
  import logger from './logger.js';
36
37
 
@@ -82,9 +83,12 @@ export const buildFileData = (filePath, options = {}) => {
82
83
  content: null,
83
84
  isNew: false
84
85
  };
85
-
86
86
  } catch (err) {
87
- logger.error('analysis-engine - buildFileData', `Failed to build file data: ${filePath}`, err);
87
+ logger.error(
88
+ 'analysis-engine - buildFileData',
89
+ `Failed to build file data: ${filePath}`,
90
+ err
91
+ );
88
92
  return null;
89
93
  }
90
94
  };
@@ -159,8 +163,8 @@ export const consolidateResults = (results) => {
159
163
 
160
164
  // Worst-case metrics
161
165
  if (result.metrics) {
162
- const metricOrder = { 'A': 5, 'B': 4, 'C': 3, 'D': 2, 'E': 1 };
163
- ['reliability', 'security', 'maintainability'].forEach(m => {
166
+ const metricOrder = { A: 5, B: 4, C: 3, D: 2, E: 1 };
167
+ ['reliability', 'security', 'maintainability'].forEach((m) => {
164
168
  const current = metricOrder[consolidated.metrics[m]] || 5;
165
169
  const incoming = metricOrder[result.metrics[m]] || 5;
166
170
  if (incoming < current) {
@@ -190,8 +194,8 @@ export const consolidateResults = (results) => {
190
194
 
191
195
  // Sum issue counts
192
196
  if (result.issues) {
193
- Object.keys(consolidated.issues).forEach(severity => {
194
- consolidated.issues[severity] += (result.issues[severity] || 0);
197
+ Object.keys(consolidated.issues).forEach((severity) => {
198
+ consolidated.issues[severity] += result.issues[severity] || 0;
195
199
  });
196
200
  }
197
201
 
@@ -309,7 +313,7 @@ export const displayResults = (result) => {
309
313
 
310
314
  // Issues Summary - Simple count
311
315
  if (Array.isArray(result.details) && result.details.length > 0) {
312
- const fileCount = new Set(result.details.map(i => i.file)).size;
316
+ const fileCount = new Set(result.details.map((i) => i.file)).size;
313
317
  console.log(`📊 ${result.details.length} issue(s) found across ${fileCount} file(s)`);
314
318
  } else {
315
319
  console.log('✅ No issues found!');
@@ -335,8 +339,10 @@ export const displayResults = (result) => {
335
339
  // Detailed Issues
336
340
  if (Array.isArray(result.details) && result.details.length > 0) {
337
341
  console.log('🔍 DETAILED ISSUES');
338
- result.details.forEach(detail => {
339
- console.log(`[${detail.severity}] ${detail.type} in ${detail.file}:${detail.line || '?'}`);
342
+ result.details.forEach((detail) => {
343
+ console.log(
344
+ `[${detail.severity}] ${detail.type} in ${detail.file}:${detail.line || '?'}`
345
+ );
340
346
  console.log(` ${detail.message}`);
341
347
  console.log();
342
348
  });
@@ -350,10 +356,17 @@ export const displayResults = (result) => {
350
356
  }
351
357
  };
352
358
 
359
+ // Minimum file count to trigger Opus orchestration instead of sequential analysis
360
+ const ORCHESTRATOR_THRESHOLD = 3;
361
+
353
362
  /**
354
363
  * Runs code analysis on files
355
364
  * Why: Unified analysis orchestration for both pre-commit and analyze command
356
365
  *
366
+ * For N >= ORCHESTRATOR_THRESHOLD (3+): Opus orchestrator groups files semantically,
367
+ * assigns per-batch models, and builds a shared commit context for better analysis.
368
+ * For N < ORCHESTRATOR_THRESHOLD (1-2): simple sequential analysis.
369
+ *
357
370
  * @param {Array<FileData>} filesData - Array of file data objects
358
371
  * @param {Object} config - Configuration object
359
372
  * @param {Object} options - Analysis options
@@ -369,68 +382,81 @@ export const runAnalysis = async (filesData, config, options = {}) => {
369
382
  return createEmptyResult();
370
383
  }
371
384
 
372
- // Determine if parallel execution should be used
373
- const subagentsEnabled = config.subagents?.enabled !== false;
374
- const batchSize = config.subagents?.batchSize || 3;
375
- const useParallel = subagentsEnabled && filesData.length >= 3;
385
+ const useOrchestrator = filesData.length >= ORCHESTRATOR_THRESHOLD;
376
386
 
377
387
  logger.debug('analysis-engine - runAnalysis', 'Starting analysis', {
378
388
  fileCount: filesData.length,
379
- useParallel,
380
- batchSize
389
+ useOrchestrator,
390
+ threshold: ORCHESTRATOR_THRESHOLD
381
391
  });
382
392
 
383
393
  let result;
384
394
 
385
- if (useParallel) {
386
- // Parallel execution: split files into batches
387
- const fileBatches = chunkArray(filesData, batchSize);
395
+ if (useOrchestrator) {
396
+ // Orchestrated parallel execution: Opus groups files semantically
397
+ const orchestrationStart = Date.now();
398
+ const orchestration = await orchestrateBatches(filesData);
399
+ const orchestrationTime = Date.now() - orchestrationStart;
388
400
 
389
- logger.debug('analysis-engine - runAnalysis', `Split into ${fileBatches.length} batches`);
401
+ const { batches, commonContext } = orchestration;
390
402
 
391
- // Build one prompt per batch
392
- const prompts = await Promise.all(
393
- fileBatches.map(async (batch) => await buildAnalysisPrompt({
394
- templateName: config.templates?.analysis,
395
- guidelinesName: config.templates?.guidelines,
396
- files: batch,
397
- metadata: {
398
- REPO_NAME: getRepoName(),
399
- BRANCH_NAME: getCurrentBranch()
400
- },
401
- subagentConfig: null // Don't add subagent instruction for parallel
402
- }))
403
+ logger.debug(
404
+ 'analysis-engine - runAnalysis',
405
+ `Orchestrated: ${filesData.length} files ${batches.length} logical batches`,
406
+ { orchestrationTime, models: batches.map((b) => b.model) }
403
407
  );
404
408
 
405
- // Build telemetry context
406
- const telemetryContext = {
407
- fileCount: filesData.length,
408
- batchSize,
409
- model: config.subagents?.model || 'haiku',
410
- hook
411
- };
409
+ // Build one prompt per batch with shared common context
410
+ const prompts = await Promise.all(
411
+ batches.map((batch) =>
412
+ buildAnalysisPrompt({
413
+ templateName: config.templates?.analysis,
414
+ guidelinesName: config.templates?.guidelines,
415
+ files: batch.files,
416
+ metadata: {
417
+ REPO_NAME: getRepoName(),
418
+ BRANCH_NAME: getCurrentBranch()
419
+ },
420
+ commonContext,
421
+ batchRationale: batch.rationale
422
+ })
423
+ )
424
+ );
412
425
 
413
- // Execute in parallel
414
- const results = await analyzeCodeParallel(prompts, {
415
- timeout: config.analysis?.timeout,
416
- saveDebug: false, // Don't save debug for individual batches
417
- telemetryContext
418
- });
426
+ // Execute all batches in parallel, each with its orchestrator-assigned model
427
+ const results = await Promise.all(
428
+ prompts.map((prompt, i) => {
429
+ const batch = batches[i];
430
+ const telemetryContext = {
431
+ fileCount: batch.files.length,
432
+ batchSize: batch.files.length,
433
+ batchIndex: i,
434
+ totalBatches: batches.length,
435
+ model: batch.model,
436
+ batchModel: batch.model,
437
+ orchestrationTime: i === 0 ? orchestrationTime : 0,
438
+ hook
439
+ };
440
+ return analyzeCode(prompt, {
441
+ timeout: config.analysis?.timeout,
442
+ model: batch.model,
443
+ saveDebug: false,
444
+ telemetryContext
445
+ });
446
+ })
447
+ );
419
448
 
420
- // Consolidate results
421
449
  result = consolidateResults(results);
422
450
 
423
- // Save consolidated debug if enabled
424
451
  if (saveDebug) {
425
452
  const { saveDebugResponse } = await import('./claude-client.js');
426
453
  await saveDebugResponse(
427
- `PARALLEL ANALYSIS: ${fileBatches.length} batches`,
454
+ `ORCHESTRATED ANALYSIS: ${batches.length} batches, commonContext length=${commonContext.length}`,
428
455
  JSON.stringify(result, null, 2)
429
456
  );
430
457
  }
431
-
432
458
  } else {
433
- // Single/sequential execution
459
+ // Single/sequential execution for 1-2 files
434
460
  logger.debug('analysis-engine - runAnalysis', 'Using sequential analysis');
435
461
 
436
462
  const prompt = await buildAnalysisPrompt({
@@ -440,16 +466,13 @@ export const runAnalysis = async (filesData, config, options = {}) => {
440
466
  metadata: {
441
467
  REPO_NAME: getRepoName(),
442
468
  BRANCH_NAME: getCurrentBranch()
443
- },
444
- subagentConfig: config.subagents
469
+ }
445
470
  });
446
471
 
447
- // Build telemetry context
448
472
  const telemetryContext = {
449
473
  fileCount: filesData.length,
450
474
  batchSize: filesData.length,
451
475
  totalBatches: 1,
452
- model: config.subagents?.model || 'haiku',
453
476
  hook
454
477
  };
455
478
 
@@ -37,21 +37,24 @@ export function getLastFinalVersionTag() {
37
37
  return null;
38
38
  }
39
39
 
40
- const tags = output.split(/\r?\n/).filter(t => t.length > 0);
40
+ const tags = output.split(/\r?\n/).filter((t) => t.length > 0);
41
41
 
42
42
  // Find first tag without suffix (not -SNAPSHOT, -RC, -ALPHA, etc.)
43
43
  for (const tag of tags) {
44
44
  const version = tag.replace(/^v/, '');
45
45
  // Check if version has suffix (contains hyphen after version number)
46
46
  if (!/\d+-/.test(version)) {
47
- logger.debug('changelog-generator - getLastFinalVersionTag', 'Final version tag found', { tag });
47
+ logger.debug(
48
+ 'changelog-generator - getLastFinalVersionTag',
49
+ 'Final version tag found',
50
+ { tag }
51
+ );
48
52
  return tag;
49
53
  }
50
54
  }
51
55
 
52
56
  logger.debug('changelog-generator - getLastFinalVersionTag', 'No final version tags found');
53
57
  return null;
54
-
55
58
  } catch (error) {
56
59
  logger.error('changelog-generator - getLastFinalVersionTag', 'Failed to get tags', error);
57
60
  return null;
@@ -84,7 +87,7 @@ export function getCommitsSinceTag(fromTag, toRef = 'HEAD') {
84
87
  return [];
85
88
  }
86
89
 
87
- const commits = output.split(/\r?\n/).map(line => {
90
+ const commits = output.split(/\r?\n/).map((line) => {
88
91
  const [sha, author, date, ...messageParts] = line.split('|');
89
92
  return {
90
93
  sha: sha.trim(),
@@ -99,7 +102,6 @@ export function getCommitsSinceTag(fromTag, toRef = 'HEAD') {
99
102
  });
100
103
 
101
104
  return commits;
102
-
103
105
  } catch (error) {
104
106
  logger.error('changelog-generator - getCommitsSinceTag', 'Failed to get commits', error);
105
107
  return [];
@@ -176,9 +178,7 @@ export function formatChangelogSection(version, date, categories, isUnreleased =
176
178
  isUnreleased
177
179
  });
178
180
 
179
- const header = isUnreleased
180
- ? '## [Unreleased]\n'
181
- : `## [${version}] - ${date}\n`;
181
+ const header = isUnreleased ? '## [Unreleased]\n' : `## [${version}] - ${date}\n`;
182
182
 
183
183
  const sections = [];
184
184
 
@@ -202,7 +202,7 @@ export function formatChangelogSection(version, date, categories, isUnreleased =
202
202
  }
203
203
  }
204
204
 
205
- const content = `${header }\n${ sections.join('')}`;
205
+ const content = `${header}\n${sections.join('')}`;
206
206
 
207
207
  logger.debug('changelog-generator - formatChangelogSection', 'Section formatted');
208
208
 
@@ -220,11 +220,7 @@ export function formatChangelogSection(version, date, categories, isUnreleased =
220
220
  * @returns {Promise<Object>} Generated entry: { content, categories }
221
221
  */
222
222
  export async function generateChangelogEntry(options) {
223
- const {
224
- version,
225
- isReleaseVersion = false,
226
- baseBranch = 'main',
227
- } = options;
223
+ const { version, isReleaseVersion = false, baseBranch = 'main' } = options;
228
224
 
229
225
  logger.debug('changelog-generator - generateChangelogEntry', 'Generating CHANGELOG entry', {
230
226
  version,
@@ -246,7 +242,10 @@ export async function generateChangelogEntry(options) {
246
242
  const commits = getCommitsSinceTag(fromTag);
247
243
 
248
244
  if (commits.length === 0) {
249
- logger.warning('changelog-generator - generateChangelogEntry', 'No commits found for CHANGELOG');
245
+ logger.warning(
246
+ 'changelog-generator - generateChangelogEntry',
247
+ 'No commits found for CHANGELOG'
248
+ );
250
249
  return {
251
250
  content: '',
252
251
  categories: {}
@@ -255,7 +254,7 @@ export async function generateChangelogEntry(options) {
255
254
 
256
255
  // Format commits for prompt
257
256
  const commitsText = commits
258
- .map(c => `${c.sha.substring(0, 7)} - ${c.message} (${c.author}, ${c.date})`)
257
+ .map((c) => `${c.sha.substring(0, 7)} - ${c.message} (${c.author}, ${c.date})`)
259
258
  .join('\n');
260
259
 
261
260
  if (!fromTag) {
@@ -273,7 +272,11 @@ export async function generateChangelogEntry(options) {
273
272
  } catch (error) {
274
273
  if (fromTag) {
275
274
  // Unexpected: a tag exists but diff failed
276
- logger.warning('changelog-generator - generateChangelogEntry', 'Could not get diff', error);
275
+ logger.warning(
276
+ 'changelog-generator - generateChangelogEntry',
277
+ 'Could not get diff',
278
+ error
279
+ );
277
280
  } else {
278
281
  // Expected: no remote or remote branch not yet pushed — commit messages are primary context
279
282
  logger.debug(
@@ -286,9 +289,8 @@ export async function generateChangelogEntry(options) {
286
289
  }
287
290
 
288
291
  // Truncate diff if too large
289
- const truncatedDiff = fullDiff.length > 30000
290
- ? `${fullDiff.substring(0, 30000) }\n... (truncated)`
291
- : fullDiff;
292
+ const truncatedDiff =
293
+ fullDiff.length > 30000 ? `${fullDiff.substring(0, 30000)}\n... (truncated)` : fullDiff;
292
294
 
293
295
  // Load prompt template
294
296
  const prompt = await loadPrompt('GENERATE_CHANGELOG.md', {
@@ -298,7 +300,10 @@ export async function generateChangelogEntry(options) {
298
300
  COMMIT_COUNT: commits.length
299
301
  });
300
302
 
301
- logger.debug('changelog-generator - generateChangelogEntry', 'Sending to Claude for analysis');
303
+ logger.debug(
304
+ 'changelog-generator - generateChangelogEntry',
305
+ 'Sending to Claude for analysis'
306
+ );
302
307
 
303
308
  // Execute Claude
304
309
  const response = await executeClaudeWithRetry(prompt, {
@@ -329,9 +334,12 @@ export async function generateChangelogEntry(options) {
329
334
  content,
330
335
  categories: result
331
336
  };
332
-
333
337
  } catch (error) {
334
- logger.error('changelog-generator - generateChangelogEntry', 'Failed to generate CHANGELOG', error);
338
+ logger.error(
339
+ 'changelog-generator - generateChangelogEntry',
340
+ 'Failed to generate CHANGELOG',
341
+ error
342
+ );
335
343
  throw new Error(`Failed to generate CHANGELOG: ${error.message}`);
336
344
  }
337
345
  }
@@ -378,24 +386,30 @@ y este proyecto adhiere a [Semantic Versioning](https://semver.org/spec/v2.0.0.h
378
386
  if (headerEnd !== -1) {
379
387
  const before = existingContent.substring(0, match.index + headerEnd);
380
388
  const after = existingContent.substring(match.index + headerEnd);
381
- updatedContent = `${before + newContent }\n${ after}`;
389
+ updatedContent = `${before + newContent}\n${after}`;
382
390
  } else {
383
391
  // No existing versions, append after header
384
- updatedContent = `${existingContent }\n${ newContent}`;
392
+ updatedContent = `${existingContent}\n${newContent}`;
385
393
  }
386
394
  } else {
387
395
  // No header found, prepend content
388
- updatedContent = `${newContent }\n${ existingContent}`;
396
+ updatedContent = `${newContent}\n${existingContent}`;
389
397
  }
390
398
 
391
399
  fs.writeFileSync(targetPath, updatedContent, 'utf8');
392
400
 
393
- logger.debug('changelog-generator - updateChangelogFile', 'CHANGELOG.md updated successfully');
401
+ logger.debug(
402
+ 'changelog-generator - updateChangelogFile',
403
+ 'CHANGELOG.md updated successfully'
404
+ );
394
405
 
395
406
  return true;
396
-
397
407
  } catch (error) {
398
- logger.error('changelog-generator - updateChangelogFile', 'Failed to update CHANGELOG.md', error);
408
+ logger.error(
409
+ 'changelog-generator - updateChangelogFile',
410
+ 'Failed to update CHANGELOG.md',
411
+ error
412
+ );
399
413
  return false;
400
414
  }
401
415
  }
@@ -415,13 +429,21 @@ y este proyecto adhiere a [Semantic Versioning](https://semver.org/spec/v2.0.0.h
415
429
  export function discoverChangelogFiles(options = {}) {
416
430
  const { maxDepth = 3, ignoreDirs = [] } = options;
417
431
 
418
- logger.debug('changelog-generator - discoverChangelogFiles', 'Starting discovery', { maxDepth });
432
+ logger.debug('changelog-generator - discoverChangelogFiles', 'Starting discovery', {
433
+ maxDepth
434
+ });
419
435
 
420
436
  const repoRoot = getRepoRoot();
421
437
 
422
438
  const defaultIgnore = [
423
- 'node_modules', 'dist', 'build', 'target', 'vendor',
424
- '.next', '.nuxt', 'coverage'
439
+ 'node_modules',
440
+ 'dist',
441
+ 'build',
442
+ 'target',
443
+ 'vendor',
444
+ '.next',
445
+ '.nuxt',
446
+ 'coverage'
425
447
  ];
426
448
  const ignoreSet = new Set([...defaultIgnore, ...ignoreDirs]);
427
449
 
@@ -434,10 +456,14 @@ export function discoverChangelogFiles(options = {}) {
434
456
  if (entry.name.toLowerCase() === 'changelog.md') found.push(fullPath);
435
457
  },
436
458
  onError: (dir, error) => {
437
- logger.debug('changelog-generator - discoverChangelogFiles', 'Error reading directory', {
438
- dir,
439
- error: error.message
440
- });
459
+ logger.debug(
460
+ 'changelog-generator - discoverChangelogFiles',
461
+ 'Error reading directory',
462
+ {
463
+ dir,
464
+ error: error.message
465
+ }
466
+ );
441
467
  }
442
468
  });
443
469
 
@@ -451,7 +477,7 @@ export function discoverChangelogFiles(options = {}) {
451
477
 
452
478
  logger.debug('changelog-generator - discoverChangelogFiles', 'Discovery complete', {
453
479
  count: found.length,
454
- files: found.map(f => path.relative(repoRoot, f))
480
+ files: found.map((f) => path.relative(repoRoot, f))
455
481
  });
456
482
 
457
483
  return found;