claude-git-hooks 2.35.3 → 2.44.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 +157 -0
- package/CLAUDE.md +24 -1389
- package/README.md +115 -2
- package/bin/claude-hooks +11 -7
- package/lib/cli-metadata.js +17 -3
- package/lib/commands/analyze-pr.js +270 -145
- package/lib/commands/analyze.js +151 -3
- package/lib/commands/create-pr.js +345 -134
- package/lib/commands/help.js +155 -81
- package/lib/commands/helpers.js +9 -4
- package/lib/commands/hooks.js +5 -5
- package/lib/commands/install.js +77 -28
- package/lib/commands/lint.js +120 -4
- package/lib/config.js +3 -0
- package/lib/hooks/pre-commit.js +26 -5
- package/lib/hooks/prepare-commit-msg.js +78 -4
- package/lib/utils/analysis-engine.js +12 -6
- package/lib/utils/claude-client.js +222 -12
- package/lib/utils/claude-diagnostics.js +5 -4
- package/lib/utils/cost-tracker.js +128 -0
- package/lib/utils/diff-analysis-orchestrator.js +2 -1
- package/lib/utils/git-operations.js +105 -2
- package/lib/utils/hooks-verified-marker.js +121 -0
- package/lib/utils/interactive-ui.js +4 -4
- package/lib/utils/judge.js +3 -2
- package/lib/utils/langfuse-tracer.js +156 -0
- package/lib/utils/logger.js +30 -5
- package/lib/utils/pr-metadata-engine.js +4 -2
- package/package.json +4 -2
- package/templates/HELP_NAVIGATE.md +41 -0
- package/templates/HELP_QUERY.md +7 -11
package/lib/commands/lint.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* claude-hooks lint src/ lib/utils/ # multiple directories
|
|
9
9
|
* claude-hooks lint file1.js file2.java # specific files
|
|
10
10
|
* claude-hooks lint src/ file3.js lib/ # mix of dirs and files
|
|
11
|
+
* claude-hooks lint --headless --format json # CI mode with JSON output
|
|
11
12
|
*
|
|
12
13
|
* Path resolution (like git add):
|
|
13
14
|
* - Directories → walk and collect files matching preset extensions
|
|
@@ -32,6 +33,28 @@ import { getStagedFiles, getRepoRoot } from '../utils/git-operations.js';
|
|
|
32
33
|
import { runLinters, displayLintResults } from '../utils/linter-runner.js';
|
|
33
34
|
import logger from '../utils/logger.js';
|
|
34
35
|
|
|
36
|
+
// ─── JSON Error Helper ────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Emit error JSON to stdout and set exit code
|
|
40
|
+
* @param {string} message - Error message
|
|
41
|
+
* @param {number} exitCode - Process exit code (default: 1)
|
|
42
|
+
* @private
|
|
43
|
+
*/
|
|
44
|
+
function _emitErrorJSON(message, exitCode = 1) {
|
|
45
|
+
process.stdout.write(`${JSON.stringify({ status: 'error', error: message })}\n`);
|
|
46
|
+
process.exitCode = exitCode;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Emit JSON result to stdout (does not exit — caller handles exit)
|
|
51
|
+
* @param {Object} payload - JSON payload
|
|
52
|
+
* @private
|
|
53
|
+
*/
|
|
54
|
+
function _emitJSON(payload) {
|
|
55
|
+
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
56
|
+
}
|
|
57
|
+
|
|
35
58
|
/**
|
|
36
59
|
* Resolve user-provided paths into a flat list of files
|
|
37
60
|
* Handles directories (walked recursively), individual files, and mixtures.
|
|
@@ -116,13 +139,34 @@ function _walkForFiles(dir, extSet, files, depth = 0) {
|
|
|
116
139
|
}
|
|
117
140
|
}
|
|
118
141
|
|
|
142
|
+
// Known flags that should not be treated as paths
|
|
143
|
+
const KNOWN_FLAGS = new Set(['--headless', '--format']);
|
|
144
|
+
|
|
119
145
|
/**
|
|
120
146
|
* Main lint command handler
|
|
121
147
|
*
|
|
122
148
|
* @param {string[]} args - CLI arguments (paths and flags)
|
|
123
149
|
*/
|
|
124
150
|
export async function runLint(args = []) {
|
|
151
|
+
// Parse flags
|
|
152
|
+
const headless = args.includes('--headless');
|
|
153
|
+
const fmtIdx = args.indexOf('--format');
|
|
154
|
+
const format = fmtIdx >= 0 ? args[fmtIdx + 1] : null;
|
|
155
|
+
const isJSON = format === 'json';
|
|
156
|
+
|
|
157
|
+
// Disallowed combo: --format json without --headless
|
|
158
|
+
if (isJSON && !headless) {
|
|
159
|
+
_emitErrorJSON('--format json requires --headless', 2);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Activate JSON mode before any output so info/warning route to stderr
|
|
164
|
+
if (isJSON) logger.setJSONMode(true);
|
|
165
|
+
|
|
166
|
+
const startTime = Date.now();
|
|
167
|
+
|
|
125
168
|
if (!checkGitRepo()) {
|
|
169
|
+
if (isJSON) { _emitErrorJSON('Not a git repository'); return; }
|
|
126
170
|
error('Not a git repository');
|
|
127
171
|
process.exit(1);
|
|
128
172
|
}
|
|
@@ -134,6 +178,19 @@ export async function runLint(args = []) {
|
|
|
134
178
|
}
|
|
135
179
|
|
|
136
180
|
if (config.linting?.enabled === false) {
|
|
181
|
+
if (isJSON) {
|
|
182
|
+
_emitJSON({
|
|
183
|
+
status: 'disabled',
|
|
184
|
+
preset: config.preset || 'default',
|
|
185
|
+
fileCount: 0,
|
|
186
|
+
totalErrors: 0,
|
|
187
|
+
totalWarnings: 0,
|
|
188
|
+
durationMs: Date.now() - startTime,
|
|
189
|
+
tools: []
|
|
190
|
+
});
|
|
191
|
+
process.exit(0);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
137
194
|
info('Linting is disabled in configuration');
|
|
138
195
|
return;
|
|
139
196
|
}
|
|
@@ -142,8 +199,14 @@ export async function runLint(args = []) {
|
|
|
142
199
|
const presetName = config.preset || 'default';
|
|
143
200
|
const { metadata } = await loadPreset(presetName);
|
|
144
201
|
|
|
145
|
-
// Separate flags from paths
|
|
146
|
-
const paths = args.filter((a) =>
|
|
202
|
+
// Separate flags from paths — filter out known flags and their values
|
|
203
|
+
const paths = args.filter((a, i) => {
|
|
204
|
+
if (KNOWN_FLAGS.has(a)) return false;
|
|
205
|
+
// Skip value immediately after a flag that takes a value
|
|
206
|
+
if (i > 0 && args[i - 1] === '--format') return false;
|
|
207
|
+
if (a.startsWith('--')) return false;
|
|
208
|
+
return true;
|
|
209
|
+
});
|
|
147
210
|
|
|
148
211
|
let filesToLint;
|
|
149
212
|
|
|
@@ -153,6 +216,19 @@ export async function runLint(args = []) {
|
|
|
153
216
|
const stagedFiles = getStagedFiles({ extensions: metadata.fileExtensions });
|
|
154
217
|
|
|
155
218
|
if (stagedFiles.length === 0) {
|
|
219
|
+
if (isJSON) {
|
|
220
|
+
_emitJSON({
|
|
221
|
+
status: 'no-files',
|
|
222
|
+
preset: presetName,
|
|
223
|
+
fileCount: 0,
|
|
224
|
+
totalErrors: 0,
|
|
225
|
+
totalWarnings: 0,
|
|
226
|
+
durationMs: Date.now() - startTime,
|
|
227
|
+
tools: []
|
|
228
|
+
});
|
|
229
|
+
process.exit(0);
|
|
230
|
+
return; // guard test-mode fallthrough
|
|
231
|
+
}
|
|
156
232
|
info('No staged files to lint');
|
|
157
233
|
return;
|
|
158
234
|
}
|
|
@@ -163,6 +239,18 @@ export async function runLint(args = []) {
|
|
|
163
239
|
filesToLint = resolvePaths(paths, metadata.fileExtensions, repoRoot);
|
|
164
240
|
|
|
165
241
|
if (filesToLint.length === 0) {
|
|
242
|
+
if (isJSON) {
|
|
243
|
+
_emitJSON({
|
|
244
|
+
status: 'no-files',
|
|
245
|
+
preset: presetName,
|
|
246
|
+
fileCount: 0,
|
|
247
|
+
totalErrors: 0,
|
|
248
|
+
totalWarnings: 0,
|
|
249
|
+
durationMs: Date.now() - startTime,
|
|
250
|
+
tools: []
|
|
251
|
+
});
|
|
252
|
+
process.exit(0);
|
|
253
|
+
}
|
|
166
254
|
info('No matching files found in specified paths');
|
|
167
255
|
return;
|
|
168
256
|
}
|
|
@@ -171,12 +259,40 @@ export async function runLint(args = []) {
|
|
|
171
259
|
info(`🎯 Linting ${filesToLint.length} file(s) with '${metadata.displayName}' preset`);
|
|
172
260
|
|
|
173
261
|
const lintResult = await runLinters(filesToLint, config, presetName);
|
|
174
|
-
displayLintResults(lintResult);
|
|
175
262
|
|
|
176
|
-
|
|
263
|
+
if (!isJSON) {
|
|
264
|
+
displayLintResults(lintResult);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Determine failure status
|
|
177
268
|
const failOnError = config.linting?.failOnError !== false;
|
|
178
269
|
const failOnWarning = config.linting?.failOnWarning === true;
|
|
270
|
+
const shouldFail =
|
|
271
|
+
(failOnError && lintResult.totalErrors > 0) ||
|
|
272
|
+
(failOnWarning && lintResult.totalWarnings > 0);
|
|
179
273
|
|
|
274
|
+
if (isJSON) {
|
|
275
|
+
_emitJSON({
|
|
276
|
+
status: shouldFail ? 'fail' : 'pass',
|
|
277
|
+
preset: presetName,
|
|
278
|
+
fileCount: filesToLint.length,
|
|
279
|
+
totalErrors: lintResult.totalErrors,
|
|
280
|
+
totalWarnings: lintResult.totalWarnings,
|
|
281
|
+
durationMs: Date.now() - startTime,
|
|
282
|
+
tools: lintResult.results.map((r) => ({
|
|
283
|
+
name: r.tool,
|
|
284
|
+
files: r.files || 0,
|
|
285
|
+
errors: r.errors.length,
|
|
286
|
+
warnings: r.warnings.length,
|
|
287
|
+
fixed: r.fixedCount || 0,
|
|
288
|
+
issues: [...r.errors, ...r.warnings]
|
|
289
|
+
}))
|
|
290
|
+
});
|
|
291
|
+
process.exit(shouldFail ? 1 : 0);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Exit with error code if linting failed
|
|
180
296
|
if (failOnError && lintResult.totalErrors > 0) {
|
|
181
297
|
process.exit(1);
|
|
182
298
|
}
|
package/lib/config.js
CHANGED
|
@@ -91,6 +91,9 @@ const HARDCODED = {
|
|
|
91
91
|
failOnError: true, // Block commit on linting errors
|
|
92
92
|
failOnWarning: false, // Do not block on warnings
|
|
93
93
|
timeout: 30000 // 30s per linter
|
|
94
|
+
},
|
|
95
|
+
claude: {
|
|
96
|
+
defaultModel: 'sonnet' // Fallback model for SDK headless mode
|
|
94
97
|
}
|
|
95
98
|
};
|
|
96
99
|
|
package/lib/hooks/pre-commit.js
CHANGED
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
* - resolution-prompt: Issue resolution generation
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import { getStagedFiles, getRepoRoot } from '../utils/git-operations.js';
|
|
22
|
+
import { getStagedFiles, getRepoRoot, getStagedTreeSha } from '../utils/git-operations.js';
|
|
23
|
+
import { writeMarker } from '../utils/hooks-verified-marker.js';
|
|
23
24
|
import { filterFiles } from '../utils/file-operations.js';
|
|
24
25
|
import {
|
|
25
26
|
buildFilesData,
|
|
@@ -53,6 +54,10 @@ import { runLinters, displayLintResults, lintIssuesToAnalysisDetails } from '../
|
|
|
53
54
|
const main = async () => {
|
|
54
55
|
const startTime = Date.now();
|
|
55
56
|
|
|
57
|
+
// Headless mode: activated by env var for CI/ECS environments (CT-805)
|
|
58
|
+
// Declared before try/catch so the catch block can show appropriate error messages
|
|
59
|
+
const isHeadless = process.env.CLAUDE_HOOKS_HEADLESS === '1';
|
|
60
|
+
|
|
56
61
|
try {
|
|
57
62
|
// Load configuration
|
|
58
63
|
const config = await getConfig();
|
|
@@ -194,7 +199,8 @@ const main = async () => {
|
|
|
194
199
|
// Step 6: Run analysis using shared engine
|
|
195
200
|
const result = await runAnalysis(filesData, config, {
|
|
196
201
|
hook: 'pre-commit',
|
|
197
|
-
saveDebug: config.system.debug
|
|
202
|
+
saveDebug: config.system.debug,
|
|
203
|
+
headless: isHeadless
|
|
198
204
|
});
|
|
199
205
|
|
|
200
206
|
// Step 7: Display results using shared function
|
|
@@ -234,7 +240,7 @@ const main = async () => {
|
|
|
234
240
|
if (judgeModule) {
|
|
235
241
|
try {
|
|
236
242
|
const { judgeAndFix } = judgeModule;
|
|
237
|
-
const judgeResult = await judgeAndFix(result, filesData, config);
|
|
243
|
+
const judgeResult = await judgeAndFix(result, filesData, config, { headless: isHeadless });
|
|
238
244
|
|
|
239
245
|
// Update result with remaining issues (fixed + false positives removed)
|
|
240
246
|
result.blockingIssues = judgeResult.remainingIssues.filter((i) =>
|
|
@@ -341,6 +347,16 @@ const main = async () => {
|
|
|
341
347
|
console.log(`\n⏱️ Analysis time: ${duration}s`);
|
|
342
348
|
logger.success('Code analysis completed. Quality gate passed.');
|
|
343
349
|
|
|
350
|
+
// Write hooks-verified marker for prepare-commit-msg trailer
|
|
351
|
+
try {
|
|
352
|
+
const treeSha = getStagedTreeSha();
|
|
353
|
+
writeMarker(repoRoot, treeSha, version);
|
|
354
|
+
logger.debug('pre-commit - main', 'Hooks-verified marker written', { treeSha });
|
|
355
|
+
} catch (markerErr) {
|
|
356
|
+
// Non-fatal: trailer won't be added but commit still succeeds
|
|
357
|
+
logger.warning(`Could not write hooks-verified marker: ${markerErr.message}`);
|
|
358
|
+
}
|
|
359
|
+
|
|
344
360
|
process.exit(0);
|
|
345
361
|
} catch (error) {
|
|
346
362
|
// Record analysis failure metric
|
|
@@ -356,8 +372,13 @@ const main = async () => {
|
|
|
356
372
|
|
|
357
373
|
logger.error('pre-commit - main', 'Pre-commit hook failed', error);
|
|
358
374
|
|
|
359
|
-
|
|
360
|
-
|
|
375
|
+
if (isHeadless) {
|
|
376
|
+
console.error('\nError executing Claude SDK');
|
|
377
|
+
console.error('Check that ANTHROPIC_API_KEY is set correctly');
|
|
378
|
+
} else {
|
|
379
|
+
console.error('\nError executing Claude CLI');
|
|
380
|
+
console.error('Check that Claude CLI is configured correctly');
|
|
381
|
+
}
|
|
361
382
|
|
|
362
383
|
if (error.output) {
|
|
363
384
|
console.error('\nClaude CLI output:');
|
|
@@ -18,7 +18,15 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import fs from 'fs/promises';
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
getStagedFiles,
|
|
23
|
+
getStagedStats,
|
|
24
|
+
getFileDiff,
|
|
25
|
+
getRepoRoot,
|
|
26
|
+
getStagedTreeSha,
|
|
27
|
+
appendTrailer
|
|
28
|
+
} from '../utils/git-operations.js';
|
|
29
|
+
import { readMarker, consumeMarker, validateMarker } from '../utils/hooks-verified-marker.js';
|
|
22
30
|
import { analyzeCode } from '../utils/claude-client.js';
|
|
23
31
|
import { loadPrompt } from '../utils/prompt-builder.js';
|
|
24
32
|
import { getVersion } from '../utils/package-info.js';
|
|
@@ -119,6 +127,9 @@ const main = async () => {
|
|
|
119
127
|
// Load configuration (includes preset + user overrides)
|
|
120
128
|
const config = await getConfig();
|
|
121
129
|
|
|
130
|
+
// Headless mode: activated by env var for CI/ECS environments (CT-805)
|
|
131
|
+
const isHeadless = process.env.CLAUDE_HOOKS_HEADLESS === '1';
|
|
132
|
+
|
|
122
133
|
// Enable debug mode from config
|
|
123
134
|
if (config.system.debug) {
|
|
124
135
|
logger.setDebugMode(true);
|
|
@@ -134,11 +145,44 @@ const main = async () => {
|
|
|
134
145
|
|
|
135
146
|
// Only process normal commits
|
|
136
147
|
// Why: Don't interfere with merge commits, amend, squash, etc.
|
|
148
|
+
// Marker is NOT consumed here — stays for the next eligible commit
|
|
137
149
|
if (commitSource && commitSource !== 'message') {
|
|
138
150
|
logger.debug('prepare-commit-msg - main', `Skipping: commit source is ${commitSource}`);
|
|
139
151
|
process.exit(0);
|
|
140
152
|
}
|
|
141
153
|
|
|
154
|
+
// Hooks-Verified trailer: validate marker now, append trailer at the very end.
|
|
155
|
+
// Why: The auto-message flow overwrites the commit file, so we can't append
|
|
156
|
+
// the trailer until the final message is written. We validate+consume the marker
|
|
157
|
+
// early (before the auto check) and remember the result for later.
|
|
158
|
+
const repoRoot = getRepoRoot();
|
|
159
|
+
const marker = readMarker(repoRoot);
|
|
160
|
+
let shouldAppendTrailer = false;
|
|
161
|
+
|
|
162
|
+
if (marker) {
|
|
163
|
+
let currentTreeSha;
|
|
164
|
+
try {
|
|
165
|
+
currentTreeSha = getStagedTreeSha();
|
|
166
|
+
} catch (err) {
|
|
167
|
+
logger.debug('prepare-commit-msg - main', 'Could not compute staged tree SHA', {
|
|
168
|
+
err: err.message
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (currentTreeSha && validateMarker(marker, currentTreeSha)) {
|
|
173
|
+
shouldAppendTrailer = true;
|
|
174
|
+
logger.debug('prepare-commit-msg - main', 'Marker valid; trailer will be appended after message is finalized');
|
|
175
|
+
} else {
|
|
176
|
+
logger.debug(
|
|
177
|
+
'prepare-commit-msg - main',
|
|
178
|
+
'Marker stale (tree mismatch); ignoring'
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Always consume marker (whether matched or not) — no stale carryover
|
|
183
|
+
consumeMarker(repoRoot);
|
|
184
|
+
}
|
|
185
|
+
|
|
142
186
|
// Read current message
|
|
143
187
|
const currentMsg = await fs.readFile(commitMsgFile, 'utf8');
|
|
144
188
|
const firstLine = currentMsg.split('\n')[0].trim();
|
|
@@ -147,6 +191,16 @@ const main = async () => {
|
|
|
147
191
|
|
|
148
192
|
// Check if message is "auto"
|
|
149
193
|
if (firstLine !== config.commitMessage.autoKeyword) {
|
|
194
|
+
// Not auto — append trailer to the manual message and exit
|
|
195
|
+
if (shouldAppendTrailer) {
|
|
196
|
+
try {
|
|
197
|
+
const withTrailer = appendTrailer(currentMsg, 'Hooks-Verified', 'true');
|
|
198
|
+
await fs.writeFile(commitMsgFile, withTrailer, 'utf8');
|
|
199
|
+
logger.debug('prepare-commit-msg - main', 'Hooks-Verified trailer appended to manual message');
|
|
200
|
+
} catch (trailerErr) {
|
|
201
|
+
logger.warning(`Could not append Hooks-Verified trailer: ${trailerErr.message}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
150
204
|
logger.debug('prepare-commit-msg - main', 'Not generating: message is not "auto"');
|
|
151
205
|
process.exit(0);
|
|
152
206
|
}
|
|
@@ -243,7 +297,8 @@ const main = async () => {
|
|
|
243
297
|
const response = await analyzeCode(prompt, {
|
|
244
298
|
timeout: config.commitMessage.timeout,
|
|
245
299
|
saveDebug: config.system.debug,
|
|
246
|
-
telemetryContext
|
|
300
|
+
telemetryContext,
|
|
301
|
+
headless: isHeadless
|
|
247
302
|
});
|
|
248
303
|
|
|
249
304
|
logger.debug('prepare-commit-msg - main', 'Response received', {
|
|
@@ -266,6 +321,20 @@ const main = async () => {
|
|
|
266
321
|
// Write to commit message file
|
|
267
322
|
await fs.writeFile(commitMsgFile, `${message}\n`, 'utf8');
|
|
268
323
|
|
|
324
|
+
// Append trailer to the auto-generated message
|
|
325
|
+
if (shouldAppendTrailer) {
|
|
326
|
+
try {
|
|
327
|
+
const generated = await fs.readFile(commitMsgFile, 'utf8');
|
|
328
|
+
const withTrailer = appendTrailer(generated, 'Hooks-Verified', 'true');
|
|
329
|
+
await fs.writeFile(commitMsgFile, withTrailer, 'utf8');
|
|
330
|
+
logger.debug('prepare-commit-msg - main', 'Hooks-Verified trailer appended to auto message');
|
|
331
|
+
} catch (trailerErr) {
|
|
332
|
+
logger.warning(
|
|
333
|
+
`Could not append Hooks-Verified trailer: ${trailerErr.message}`
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
269
338
|
// Record commit generation metric
|
|
270
339
|
recordMetric('commit.generated', {
|
|
271
340
|
fileCount: filesData.length,
|
|
@@ -292,8 +361,13 @@ const main = async () => {
|
|
|
292
361
|
|
|
293
362
|
logger.error('prepare-commit-msg - main', 'Failed to generate commit message', error);
|
|
294
363
|
|
|
295
|
-
|
|
296
|
-
|
|
364
|
+
if (isHeadless) {
|
|
365
|
+
logger.warning('Could not generate message automatically via SDK');
|
|
366
|
+
logger.warning('Check that ANTHROPIC_API_KEY is set correctly');
|
|
367
|
+
} else {
|
|
368
|
+
logger.warning('Could not generate message automatically with Claude');
|
|
369
|
+
logger.warning('Commit canceled. Run again without "auto" to write manual message');
|
|
370
|
+
}
|
|
297
371
|
|
|
298
372
|
process.exit(1);
|
|
299
373
|
}
|
|
@@ -278,7 +278,8 @@ export const createEmptyResult = () => ({
|
|
|
278
278
|
*
|
|
279
279
|
* @param {Object} result - Analysis result
|
|
280
280
|
*/
|
|
281
|
-
export const displayIssueSummary = (result) => {
|
|
281
|
+
export const displayIssueSummary = (result, { silent = false } = {}) => {
|
|
282
|
+
if (silent) return;
|
|
282
283
|
const { blocker = 0, critical = 0, major = 0, minor = 0, info = 0 } = result.issues || {};
|
|
283
284
|
const total = blocker + critical + major + minor + info;
|
|
284
285
|
|
|
@@ -296,7 +297,8 @@ export const displayIssueSummary = (result) => {
|
|
|
296
297
|
*
|
|
297
298
|
* @param {Object} result - Analysis result from Claude
|
|
298
299
|
*/
|
|
299
|
-
export const displayResults = (result) => {
|
|
300
|
+
export const displayResults = (result, { silent = false } = {}) => {
|
|
301
|
+
if (silent) return;
|
|
300
302
|
console.log();
|
|
301
303
|
console.log('╔════════════════════════════════════════════════════════════════════╗');
|
|
302
304
|
console.log('║ CODE QUALITY ANALYSIS ║');
|
|
@@ -376,7 +378,7 @@ const ORCHESTRATOR_THRESHOLD = 3;
|
|
|
376
378
|
* @returns {Promise<Object>} Analysis result
|
|
377
379
|
*/
|
|
378
380
|
export const runAnalysis = async (filesData, config, options = {}) => {
|
|
379
|
-
const { saveDebug = config.system?.debug, hook = 'analysis' } = options;
|
|
381
|
+
const { saveDebug = config.system?.debug, hook = 'analysis', headless = false, costTracker = null } = options;
|
|
380
382
|
|
|
381
383
|
if (filesData.length === 0) {
|
|
382
384
|
logger.debug('analysis-engine - runAnalysis', 'No files to analyze');
|
|
@@ -396,7 +398,7 @@ export const runAnalysis = async (filesData, config, options = {}) => {
|
|
|
396
398
|
if (useOrchestrator) {
|
|
397
399
|
// Orchestrated parallel execution: Opus groups files semantically
|
|
398
400
|
const orchestrationStart = Date.now();
|
|
399
|
-
const orchestration = await orchestrateBatches(filesData);
|
|
401
|
+
const orchestration = await orchestrateBatches(filesData, { headless });
|
|
400
402
|
const orchestrationTime = Date.now() - orchestrationStart;
|
|
401
403
|
|
|
402
404
|
const { batches, commonContext } = orchestration;
|
|
@@ -442,7 +444,9 @@ export const runAnalysis = async (filesData, config, options = {}) => {
|
|
|
442
444
|
timeout: config.analysis?.timeout,
|
|
443
445
|
model: batch.model,
|
|
444
446
|
saveDebug: false,
|
|
445
|
-
telemetryContext
|
|
447
|
+
telemetryContext,
|
|
448
|
+
headless,
|
|
449
|
+
costTracker
|
|
446
450
|
});
|
|
447
451
|
})
|
|
448
452
|
);
|
|
@@ -489,7 +493,9 @@ export const runAnalysis = async (filesData, config, options = {}) => {
|
|
|
489
493
|
result = await analyzeCode(prompt, {
|
|
490
494
|
timeout: config.analysis?.timeout,
|
|
491
495
|
saveDebug,
|
|
492
|
-
telemetryContext
|
|
496
|
+
telemetryContext,
|
|
497
|
+
headless,
|
|
498
|
+
costTracker
|
|
493
499
|
});
|
|
494
500
|
}
|
|
495
501
|
|