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.
- package/CHANGELOG.md +52 -0
- package/CLAUDE.md +85 -38
- package/README.md +52 -18
- package/bin/claude-hooks +75 -89
- package/lib/cli-metadata.js +306 -0
- package/lib/commands/analyze-diff.js +12 -10
- package/lib/commands/analyze.js +9 -5
- package/lib/commands/bump-version.js +56 -40
- package/lib/commands/create-pr.js +71 -34
- package/lib/commands/debug.js +4 -7
- package/lib/commands/diff-batch-info.js +105 -0
- package/lib/commands/generate-changelog.js +3 -2
- package/lib/commands/help.js +47 -27
- package/lib/commands/helpers.js +66 -43
- package/lib/commands/hooks.js +15 -13
- package/lib/commands/install.js +546 -49
- package/lib/commands/migrate-config.js +8 -11
- package/lib/commands/presets.js +6 -13
- package/lib/commands/setup-github.js +12 -3
- package/lib/commands/telemetry-cmd.js +8 -6
- package/lib/commands/update.js +1 -2
- package/lib/config.js +36 -52
- package/lib/hooks/pre-commit.js +70 -64
- package/lib/hooks/prepare-commit-msg.js +35 -75
- package/lib/utils/analysis-engine.js +77 -54
- package/lib/utils/changelog-generator.js +63 -37
- package/lib/utils/claude-client.js +447 -438
- package/lib/utils/claude-diagnostics.js +20 -10
- package/lib/utils/diff-analysis-orchestrator.js +332 -0
- package/lib/utils/file-operations.js +51 -79
- package/lib/utils/file-utils.js +6 -7
- package/lib/utils/git-operations.js +140 -123
- package/lib/utils/git-tag-manager.js +24 -23
- package/lib/utils/github-api.js +85 -61
- package/lib/utils/github-client.js +12 -14
- package/lib/utils/installation-diagnostics.js +4 -4
- package/lib/utils/interactive-ui.js +29 -17
- package/lib/utils/judge.js +195 -0
- package/lib/utils/logger.js +4 -1
- package/lib/utils/package-info.js +0 -11
- package/lib/utils/pr-metadata-engine.js +67 -33
- package/lib/utils/preset-loader.js +20 -62
- package/lib/utils/prompt-builder.js +57 -68
- package/lib/utils/resolution-prompt.js +34 -52
- package/lib/utils/sanitize.js +20 -19
- package/lib/utils/task-id.js +27 -40
- package/lib/utils/telemetry.js +73 -25
- package/lib/utils/version-manager.js +147 -70
- package/lib/utils/which-command.js +23 -12
- package/package.json +1 -1
- package/templates/CLAUDE_RESOLUTION_PROMPT.md +17 -9
- package/templates/DIFF_ANALYSIS_ORCHESTRATION_PROMPT.md +70 -0
- package/templates/config.advanced.example.json +15 -31
- 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
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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,
|
|
162
|
+
prompt: false, // Don't prompt - just detect from branch
|
|
186
163
|
required: false,
|
|
187
|
-
config
|
|
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(
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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:
|
|
272
|
-
totalBatches:
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
|
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
|
|
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(
|
|
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 = {
|
|
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] +=
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
380
|
-
|
|
389
|
+
useOrchestrator,
|
|
390
|
+
threshold: ORCHESTRATOR_THRESHOLD
|
|
381
391
|
});
|
|
382
392
|
|
|
383
393
|
let result;
|
|
384
394
|
|
|
385
|
-
if (
|
|
386
|
-
//
|
|
387
|
-
const
|
|
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
|
-
|
|
401
|
+
const { batches, commonContext } = orchestration;
|
|
390
402
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
`
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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 =
|
|
290
|
-
? `${fullDiff.substring(0, 30000)
|
|
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(
|
|
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(
|
|
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
|
|
389
|
+
updatedContent = `${before + newContent}\n${after}`;
|
|
382
390
|
} else {
|
|
383
391
|
// No existing versions, append after header
|
|
384
|
-
updatedContent = `${existingContent
|
|
392
|
+
updatedContent = `${existingContent}\n${newContent}`;
|
|
385
393
|
}
|
|
386
394
|
} else {
|
|
387
395
|
// No header found, prepend content
|
|
388
|
-
updatedContent = `${newContent
|
|
396
|
+
updatedContent = `${newContent}\n${existingContent}`;
|
|
389
397
|
}
|
|
390
398
|
|
|
391
399
|
fs.writeFileSync(targetPath, updatedContent, 'utf8');
|
|
392
400
|
|
|
393
|
-
logger.debug(
|
|
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(
|
|
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', {
|
|
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',
|
|
424
|
-
'
|
|
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(
|
|
438
|
-
|
|
439
|
-
|
|
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;
|