@contextrail/code-review-agent 0.1.1 → 0.1.2-alpha.1

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/dist/index.js CHANGED
@@ -1,603 +1,65 @@
1
1
  #!/usr/bin/env node
2
2
  import path from 'node:path';
3
- import { loadConfig, validateConfig } from './config/index.js';
4
- import { DEFAULT_ORCHESTRATOR_MODEL, DEFAULT_REVIEWER_MODEL } from './config/defaults.js';
3
+ import { loadConfig, applyCliArgs, validateConfig } from './config/index.js';
5
4
  import { createLogger, parseLogLevel } from './logging/logger.js';
6
- import { McpClient } from './mcp/client.js';
7
- import { buildReviewInputs, triagePr } from './review-inputs/index.js';
8
- import { runOrchestrator } from './orchestrator/agentic-orchestrator.js';
9
- import { runReviewerLoop } from './reviewers/executor.js';
10
- import { aggregateResults, writeResult, writeTokenBudgetMetrics } from './output/writer.js';
11
- import { metadataSchema } from './output/schema.js';
12
- import { generateReviewDecision, normalizeDecisionWithSynthesis, synthesizeFindings } from './output/aggregator.js';
13
- const parseArgs = (args) => {
14
- const parsed = {};
15
- let i = 0;
16
- while (i < args.length) {
17
- const arg = args[i];
18
- if (!arg) {
19
- i += 1;
20
- continue;
21
- }
22
- if (arg === '-h' || arg === '--help') {
23
- parsed.help = true;
24
- i += 1;
25
- }
26
- else if (arg === '--repo' && i + 1 < args.length) {
27
- parsed.repo = args[i + 1];
28
- i += 2;
29
- }
30
- else if (arg === '--from' && i + 1 < args.length) {
31
- parsed.from = args[i + 1];
32
- i += 2;
33
- }
34
- else if (arg === '--to' && i + 1 < args.length) {
35
- parsed.to = args[i + 1];
36
- i += 2;
37
- }
38
- else if (arg === '--output' && i + 1 < args.length) {
39
- parsed.output = args[i + 1];
40
- i += 2;
41
- }
42
- else if (arg === '--files' && i + 1 < args.length) {
43
- const raw = args[i + 1] ?? '';
44
- const files = raw
45
- .split(',')
46
- .map((file) => file.trim())
47
- .filter(Boolean);
48
- if (!parsed.files) {
49
- parsed.files = [];
50
- }
51
- parsed.files.push(...files);
52
- i += 2;
53
- }
54
- else if (arg === '--file' && i + 1 < args.length) {
55
- const file = args[i + 1];
56
- if (file) {
57
- if (!parsed.files) {
58
- parsed.files = [];
59
- }
60
- parsed.files.push(file);
61
- }
62
- i += 2;
63
- }
64
- else if (arg === '--log-level' && i + 1 < args.length) {
65
- parsed.logLevel = args[i + 1];
66
- i += 2;
67
- }
68
- else if (arg === '--pr-description' && i + 1 < args.length) {
69
- parsed.prDescription = args[i + 1];
70
- i += 2;
71
- }
72
- else if (arg === '--domains' && i + 1 < args.length) {
73
- const raw = args[i + 1] ?? '';
74
- const domains = raw
75
- .split(',')
76
- .map((domain) => domain.trim())
77
- .filter(Boolean);
78
- if (!parsed.domains) {
79
- parsed.domains = [];
80
- }
81
- parsed.domains.push(...domains);
82
- i += 2;
83
- }
84
- else if (arg === '--max-steps' && i + 1 < args.length) {
85
- const value = parseInt(args[i + 1] ?? '10', 10);
86
- parsed.maxSteps = Number.isNaN(value) ? undefined : value;
87
- i += 2;
88
- }
89
- else if (arg === '--max-iterations' && i + 1 < args.length) {
90
- const value = parseInt(args[i + 1] ?? '2', 10);
91
- parsed.maxIterations = Number.isNaN(value) ? undefined : value;
92
- i += 2;
93
- }
94
- else if (arg === '--aggregation-max-steps' && i + 1 < args.length) {
95
- const value = parseInt(args[i + 1] ?? '5', 10);
96
- parsed.aggregationMaxSteps = Number.isNaN(value) ? undefined : value;
97
- i += 2;
98
- }
99
- else if (arg === '--max-tokens-per-file' && i + 1 < args.length) {
100
- const value = parseInt(args[i + 1] ?? '20000', 10);
101
- parsed.maxTokensPerFile = Number.isNaN(value) ? undefined : value;
102
- i += 2;
103
- }
104
- else if (arg === '--context-lines' && i + 1 < args.length) {
105
- const value = parseInt(args[i + 1] ?? '10', 10);
106
- parsed.contextLines = Number.isNaN(value) ? undefined : value;
107
- i += 2;
108
- }
109
- else if (!parsed.command && !arg.startsWith('-')) {
110
- parsed.command = arg;
111
- i += 1;
112
- }
113
- else {
114
- i += 1;
115
- }
116
- }
117
- return parsed;
118
- };
119
- const printHelp = (log) => {
120
- log.info(`code-review-agent
121
-
122
- Usage:
123
- code-review-agent review --repo <path> --from <sha> --to <sha> [--output <dir>]
124
- code-review-agent review --repo <path> --files <file1,file2> [--output <dir>]
125
- code-review-agent review --repo <path> --file <file> [--file <file> ...] [--output <dir>]
126
-
127
- Commands:
128
- review Run code review on git diff
129
-
130
- Options:
131
- --repo <path> Repository path (default: current directory)
132
- --from <sha> Base commit SHA (required)
133
- --to <sha> Head commit SHA (required)
134
- --files <list> Comma-separated list of files to review
135
- --file <path> File to review (repeatable)
136
- --output <dir> Output directory (default: ./review)
137
- --log-level <lv> Log level: debug|info|warn|error|silent (overrides DEBUG)
138
- --max-steps <n> Maximum LLM steps per call (default: 10)
139
- --max-iterations <n> Maximum reviewer validation iterations (default: 2)
140
- --aggregation-max-steps <n> Maximum steps for aggregation (default: 5)
141
- --max-tokens-per-file <n> Token budget for surrounding context per file (default: 20000)
142
- --context-lines <n> Lines of context around changes (default: 10)
143
- --pr-description <text> PR description to include as context (optional)
144
- --domains <list> Comma-separated review focus domains (optional)
145
- -h, --help Show this help message
146
-
147
- Environment Variables:
148
- CONTEXTRAIL_MCP_SERVER_URL ContextRail MCP server URL (required)
149
- CONTEXTRAIL_MCP_JWT_TOKEN ContextRail MCP authentication token (required)
150
- OPENROUTER_API_KEY OpenRouter API key (required)
151
- LLM_MODEL_ORCHESTRATOR Model for orchestrator (default: anthropic/claude-haiku-4.5)
152
- LLM_MODEL_REVIEWER Model for reviewers (default: anthropic/claude-haiku-4.5)
153
- MAX_STEPS Maximum LLM steps per call (default: 10)
154
- MAX_ITERATIONS Maximum reviewer validation iterations (default: 2)
155
- AGGREGATION_MAX_STEPS Maximum steps for aggregation (default: 5)
156
- MAX_TOKENS_PER_FILE Token budget for surrounding context per file (default: 20000)
157
- CONTEXT_LINES Lines of context around changes (default: 10)
158
- REVIEW_DOMAINS Comma-separated review focus domains (optional)
159
-
160
- Examples:
161
- code-review-agent review --repo . --from HEAD^ --to HEAD
162
- code-review-agent review --repo . --files src/a.ts,src/b.ts
163
- code-review-agent review --repo . --file src/a.ts --file src/b.ts
164
- code-review-agent review --repo /path/to/repo --from abc123 --to def456 --output ./review-results
165
- `);
166
- };
167
- const validateArgs = (args) => {
168
- if (args.help) {
169
- return;
170
- }
171
- if (args.command !== 'review') {
172
- throw new Error(`Unknown command: ${args.command ?? 'none'}. Use 'review' or see --help.`);
173
- }
174
- const hasFileList = Array.isArray(args.files) && args.files.length > 0;
175
- if (!hasFileList) {
176
- if (!args.from) {
177
- throw new Error('Missing required argument: --from');
178
- }
179
- if (!args.to) {
180
- throw new Error('Missing required argument: --to');
181
- }
182
- }
183
- };
5
+ import { parseArgs, validateArgs, printHelp } from './cli/index.js';
6
+ import { runReview } from './pipeline.js';
7
+ import { gracefulShutdown, setMcpClient, setupLifecycleHandlers } from './lifecycle.js';
8
+ import { serializeError, toError } from './errors/error-utils.js';
184
9
  const main = async () => {
10
+ setupLifecycleHandlers();
185
11
  try {
186
12
  const args = parseArgs(process.argv.slice(2));
13
+ const config = loadConfig();
187
14
  if (args.help) {
188
- // Keep --help available even when required env vars are not set.
189
- const helpLog = createLogger(parseLogLevel(args.logLevel ?? process.env.DEBUG));
190
- printHelp(helpLog);
15
+ printHelp(createLogger(parseLogLevel(args.logLevel ?? config.logLevel)));
191
16
  process.exit(0);
192
17
  }
193
- const config = loadConfig();
194
18
  const log = createLogger(parseLogLevel(args.logLevel ?? config.logLevel));
195
- log.debug(`Log level set to ${args.logLevel ?? config.logLevel}`);
196
- // Validate arguments
197
19
  validateArgs(args);
198
- // Override config with CLI args
199
- if (args.maxSteps !== undefined) {
200
- config.maxSteps = args.maxSteps;
201
- }
202
- if (args.maxIterations !== undefined) {
203
- config.maxIterations = args.maxIterations;
204
- }
205
- if (args.aggregationMaxSteps !== undefined) {
206
- config.aggregationMaxSteps = args.aggregationMaxSteps;
207
- }
208
- if (args.maxTokensPerFile !== undefined) {
209
- config.maxTokensPerFile = args.maxTokensPerFile;
210
- }
211
- if (args.contextLines !== undefined) {
212
- config.contextLines = args.contextLines;
213
- }
214
- if (args.prDescription !== undefined) {
215
- config.prDescription = args.prDescription;
216
- }
217
- if (args.domains && args.domains.length > 0) {
218
- config.reviewDomains = args.domains;
20
+ applyCliArgs(config, args);
21
+ const result = await runReview({
22
+ config: validateConfig(config),
23
+ repoPath: args.repo ?? process.cwd(),
24
+ outputDir: args.output ?? path.join(process.cwd(), 'review'),
25
+ files: args.files,
26
+ from: args.from,
27
+ to: args.to,
28
+ prDescription: config.prDescription,
29
+ log,
30
+ onMcpClientReady: setMcpClient,
31
+ });
32
+ if (result.hasValidationFailures) {
33
+ log.error('❌ Review failed: Some reviewers did not validate');
34
+ process.exit(1);
219
35
  }
220
- // Load and validate config
221
- const validatedConfig = validateConfig(config);
222
- // Set defaults
223
- const repoPath = args.repo ?? process.cwd();
224
- const outputDir = args.output ?? path.join(process.cwd(), 'review');
225
- log.info('Starting code review...');
226
- log.info(`Repository: ${repoPath}`);
227
- log.info(`Output: ${outputDir}`);
228
- if (args.files && args.files.length > 0) {
229
- log.info(`Files: ${args.files.join(', ')}`);
36
+ else if (result.decision === 'request-changes') {
37
+ log.error('❌ Review decision: REQUEST CHANGES');
38
+ process.exit(1);
230
39
  }
231
40
  else {
232
- log.info(`From: ${args.from}`);
233
- log.info(`To: ${args.to}`);
234
- }
235
- // Connect MCP client
236
- const mcpClient = new McpClient({
237
- serverUrl: validatedConfig.mcpServerUrl,
238
- authToken: validatedConfig.mcpAuthToken,
239
- clientName: 'code-review-agent',
240
- clientVersion: '0.1.0',
241
- logger: log,
242
- });
243
- mcpClientInstance = mcpClient; // Store for graceful shutdown
244
- await mcpClient.connect();
245
- log.info('Connected to MCP server');
246
- try {
247
- // Build review inputs
248
- log.info('Building review inputs...');
249
- const inputs = args.files && args.files.length > 0
250
- ? await buildReviewInputs({
251
- mode: 'file-list',
252
- files: args.files,
253
- basePath: repoPath,
254
- surroundingContext: {
255
- enabled: true,
256
- maxTokensPerFile: validatedConfig.maxTokensPerFile,
257
- contextLines: validatedConfig.contextLines,
258
- },
259
- })
260
- : await buildReviewInputs({
261
- mode: 'diff',
262
- repoPath,
263
- from: args.from,
264
- to: args.to,
265
- surroundingContext: {
266
- enabled: true,
267
- maxTokensPerFile: validatedConfig.maxTokensPerFile,
268
- contextLines: validatedConfig.contextLines,
269
- },
270
- });
271
- log.info(`Found ${inputs.files.length} files to review`);
272
- if (inputs.files.length > 0) {
273
- log.debug(` Files: ${inputs.files.slice(0, 10).join(', ')}${inputs.files.length > 10 ? ` ... and ${inputs.files.length - 10} more` : ''}`);
274
- }
275
- // Triage PR to determine if it's trivial
276
- const triageResult = triagePr(inputs);
277
- log.info(`PR triage: ${triageResult.reason}`);
278
- if (triageResult.isTrivial) {
279
- log.info(`Trivial PR detected (${triageResult.isDocsOnly ? 'docs-only' : 'small'}). Skipping full reviewer flow.`);
280
- // For trivial PRs, we still run orchestrator but with awareness
281
- // The orchestrator can use this information to select fewer reviewers or skip entirely
282
- }
283
- // Run orchestrator
284
- log.info('Running orchestrator...');
285
- const prDescription = config.prDescription;
286
- if (prDescription) {
287
- log.debug('PR description provided, will be included in prompts');
288
- }
289
- if (config.reviewDomains && config.reviewDomains.length > 0) {
290
- log.debug(`Review domains provided: ${config.reviewDomains.join(', ')}`);
291
- }
292
- const orchestratorOutput = await runOrchestrator(inputs, outputDir, {
293
- mcpClient,
294
- config: {
295
- openRouterApiKey: validatedConfig.openRouterApiKey,
296
- orchestratorModel: validatedConfig.orchestratorModel ?? DEFAULT_ORCHESTRATOR_MODEL,
297
- maxSteps: validatedConfig.maxSteps,
298
- prDescription,
299
- reviewDomains: validatedConfig.reviewDomains,
300
- },
301
- logger: log,
302
- });
303
- log.info(`Selected reviewers: ${orchestratorOutput.reviewers.join(', ')}`);
304
- log.debug(`Orchestrator understanding:\n${orchestratorOutput.understanding}`);
305
- // Run reviewers in parallel with progress logging
306
- log.info(`Running ${orchestratorOutput.reviewers.length} reviewer(s) in parallel...`);
307
- const reviewerFailures = [];
308
- const reviewerResults = await Promise.all(orchestratorOutput.reviewers.map(async (reviewer) => {
309
- const startTime = Date.now();
310
- log.info(`[${reviewer}] Starting review...`);
311
- try {
312
- const findings = await runReviewerLoop(reviewer, inputs, orchestratorOutput.understanding, outputDir, {
313
- mcpClient,
314
- config: {
315
- openRouterApiKey: validatedConfig.openRouterApiKey,
316
- reviewerModel: validatedConfig.reviewerModel ?? DEFAULT_REVIEWER_MODEL,
317
- criticModel: validatedConfig.criticModel,
318
- maxSteps: validatedConfig.maxSteps,
319
- maxIterations: validatedConfig.maxIterations,
320
- prDescription,
321
- reviewDomains: validatedConfig.reviewDomains,
322
- },
323
- logger: log,
324
- });
325
- const result = {
326
- reviewer,
327
- findings: findings.findings,
328
- validated: findings.validated,
329
- notes: findings.notes ?? undefined,
330
- };
331
- const duration = ((Date.now() - startTime) / 1000).toFixed(1);
332
- log.info(`[${reviewer}] Completed in ${duration}s`);
333
- // Log reviewer results
334
- const issueCount = findings.findings.filter((f) => f.severity !== 'pass').length;
335
- const passCount = findings.findings.filter((f) => f.severity === 'pass').length;
336
- if (issueCount === 0) {
337
- log.info(` ✓ ${reviewer}: Clean pass (pass signals: ${passCount}, validated: ${findings.validated})`);
338
- }
339
- else {
340
- log.info(` ✓ ${reviewer}: ${issueCount} issue(s) (pass signals: ${passCount}, validated: ${findings.validated})`);
341
- }
342
- log.debug(` Findings: ${JSON.stringify(findings.findings, null, 2)}`);
343
- if (findings.notes) {
344
- log.debug(` Notes: ${findings.notes}`);
345
- }
346
- return result;
347
- }
348
- catch (error) {
349
- const duration = ((Date.now() - startTime) / 1000).toFixed(1);
350
- const message = error instanceof Error ? error.message : 'Unknown error';
351
- log.error(`[${reviewer}] Failed after ${duration}s: ${message}`);
352
- // Provide actionable error guidance
353
- if (message.includes('No object generated') || message.includes('No output generated')) {
354
- log.error(`[${reviewer}] TROUBLESHOOTING: The model may not support structured output well.`);
355
- log.error(`[${reviewer}] Try: 1) Use a different model (e.g., anthropic/claude-haiku-4.5)`);
356
- log.error(`[${reviewer}] 2) Check model compatibility with structured output`);
357
- log.error(`[${reviewer}] 3) Review the model's response format requirements`);
358
- }
359
- reviewerFailures.push({ reviewer, message });
360
- return {
361
- reviewer,
362
- findings: [],
363
- validated: false,
364
- notes: `Error: ${message}`,
365
- };
366
- }
367
- }));
368
- // Aggregate results
369
- log.info('Aggregating results...');
370
- log.debug(` Total issue findings to aggregate: ${reviewerResults.reduce((sum, rr) => sum + rr.findings.filter((f) => f.severity !== 'pass').length, 0)}`);
371
- log.debug(` Total pass signals to aggregate: ${reviewerResults.reduce((sum, rr) => sum + rr.findings.filter((f) => f.severity === 'pass').length, 0)}`);
372
- log.debug(` Reviewers validated: ${reviewerResults.filter((rr) => rr.validated).length}/${reviewerResults.length}`);
373
- const metadata = metadataSchema.parse({
374
- timestamp: new Date().toISOString(),
375
- mode: inputs.mode,
376
- fileCount: inputs.files.length,
377
- });
378
- // Synthesize findings across reviewers (deduplication, contradictions, compound risks)
379
- log.info(`Synthesis pass starting (model: ${validatedConfig.orchestratorModel ?? DEFAULT_ORCHESTRATOR_MODEL}, reviewers: ${reviewerResults.length})`);
380
- const synthesisResult = await synthesizeFindings(reviewerResults, {
381
- openRouterApiKey: validatedConfig.openRouterApiKey,
382
- model: validatedConfig.orchestratorModel ?? DEFAULT_ORCHESTRATOR_MODEL,
383
- maxSteps: validatedConfig.aggregationMaxSteps,
384
- logger: log,
385
- });
386
- log.info(`Synthesis pass complete: ${synthesisResult.synthesis.findings.length} deduped findings, ${synthesisResult.synthesis.contradictions.length} contradictions, ${synthesisResult.synthesis.compoundRisks.length} compound risks`);
387
- log.debug(` Synthesis: ${synthesisResult.synthesis.findings.length} deduplicated findings`);
388
- log.debug(` Contradictions: ${synthesisResult.synthesis.contradictions.length}`);
389
- log.debug(` Compound risks: ${synthesisResult.synthesis.compoundRisks.length}`);
390
- const synthesisSeverityCounts = synthesisResult.synthesis.findings.reduce((acc, finding) => {
391
- acc[finding.severity] += 1;
392
- return acc;
393
- }, { critical: 0, major: 0, minor: 0, info: 0, pass: 0 });
394
- log.debug(` Synthesis severity: ${JSON.stringify(synthesisSeverityCounts)}`);
395
- // Generate review decision based on synthesized findings
396
- log.info(`Decision pass starting (model: ${validatedConfig.orchestratorModel ?? DEFAULT_ORCHESTRATOR_MODEL}, synthesis findings: ${synthesisResult.synthesis.findings.length})`);
397
- const decisionResult = await generateReviewDecision(orchestratorOutput.understanding, synthesisResult.synthesis, {
398
- openRouterApiKey: validatedConfig.openRouterApiKey,
399
- model: validatedConfig.orchestratorModel ?? DEFAULT_ORCHESTRATOR_MODEL,
400
- maxSteps: validatedConfig.aggregationMaxSteps,
401
- logger: log,
402
- });
403
- const normalizedDecision = normalizeDecisionWithSynthesis(decisionResult.decision, synthesisResult.synthesis);
404
- const decisionEvidence = {
405
- findings: synthesisResult.synthesis.findings.length,
406
- blockingFindings: synthesisResult.synthesis.findings.filter((f) => f.severity === 'critical' || f.severity === 'major').length,
407
- contradictions: synthesisResult.synthesis.contradictions.length,
408
- compoundRisks: synthesisResult.synthesis.compoundRisks.length,
409
- modelDecision: decisionResult.decision.decision,
410
- normalizedDecision: normalizedDecision.decision,
411
- };
412
- log.debug(` Decision evidence: ${JSON.stringify(decisionEvidence)}`);
413
- if (normalizedDecision.decision !== decisionResult.decision.decision) {
414
- log.warn(`Decision normalized from ${decisionResult.decision.decision} to ${normalizedDecision.decision} to match synthesized findings.`);
415
- }
416
- log.info(`Decision pass complete: ${normalizedDecision.decision}`);
417
- log.debug(` Aggregation decision: ${normalizedDecision.decision}`);
418
- log.debug(` Decision summary: ${normalizedDecision.summary}`);
419
- log.debug(` Decision rationale: ${normalizedDecision.rationale}`);
420
- const result = aggregateResults(metadata, orchestratorOutput.understanding, normalizedDecision, reviewerResults, reviewerFailures, synthesisResult.synthesis);
421
- // Write result.json
422
- await writeResult(outputDir, result);
423
- const resultPath = path.join(outputDir, 'result.json');
424
- log.info(`Results written to ${resultPath}`);
425
- log.debug(` Full review results available at: ${resultPath}`);
426
- // Write token budget metrics
427
- const aggregationUsage = [];
428
- if (synthesisResult.usage) {
429
- aggregationUsage.push(synthesisResult.usage);
430
- }
431
- if (decisionResult.usage) {
432
- aggregationUsage.push(decisionResult.usage);
433
- }
434
- await writeTokenBudgetMetrics(outputDir, orchestratorOutput.reviewers, aggregationUsage.length > 0 ? aggregationUsage : undefined);
435
- log.info(`Token budget metrics written to ${path.join(outputDir, 'token-budget.json')}`);
436
- // Print summary
437
- log.info('\n═══════════════════════════════════════════════════════════');
438
- log.info('Review Summary');
439
- log.info('═══════════════════════════════════════════════════════════');
440
- log.info(` Files reviewed: ${result.metadata.fileCount}`);
441
- log.info(` Reviewers executed: ${reviewerResults.length}`);
442
- log.info(` Total issue findings: ${result.summary.totalFindings}`);
443
- log.info(` Critical: ${result.summary.bySeverity.critical}`);
444
- log.info(` Major: ${result.summary.bySeverity.major}`);
445
- log.info(` Minor: ${result.summary.bySeverity.minor}`);
446
- log.info(` Info: ${result.summary.bySeverity.info}`);
447
- log.info(` Pass signals: ${result.summary.bySeverity.pass}`);
448
- // Show per-reviewer breakdown
449
- if (result.summary.totalFindings > 0) {
450
- log.info('\n Findings by reviewer:');
451
- for (const [reviewer, count] of Object.entries(result.summary.byReviewer)) {
452
- log.info(` ${reviewer}: ${count} finding(s)`);
453
- }
454
- }
455
- // Show decision details
456
- log.info(`\n Decision: ${result.decision.decision.toUpperCase()}`);
457
- log.info(` Summary: ${result.decision.summary}`);
458
- log.info(` Rationale: ${result.decision.rationale}`);
459
- // Show reviewer pass summaries (positive signal)
460
- const passReviewers = reviewerResults
461
- .map((rr) => ({
462
- reviewer: rr.reviewer,
463
- passFinding: rr.findings.find((f) => f.severity === 'pass'),
464
- notes: rr.notes,
465
- validated: rr.validated,
466
- }))
467
- .filter((rr) => rr.validated && (rr.passFinding || rr.notes));
468
- if (passReviewers.length > 0) {
469
- log.info('\n Reviewer pass summaries:');
470
- for (const rr of passReviewers) {
471
- const passLine = rr.notes?.trim() ?? rr.passFinding?.title ?? 'PASS';
472
- log.info(` ${rr.reviewer}: ${passLine.split('\n')[0] ?? passLine}`);
473
- }
474
- }
475
- // Debug: Show synthesized (deduplicated) findings when available.
476
- if (result.synthesis && result.synthesis.findings.length > 0) {
477
- log.debug('\n Deduplicated findings (synthesis):');
478
- for (const finding of result.synthesis.findings) {
479
- log.debug(` [${finding.severity.toUpperCase()}] ${finding.title}`);
480
- if (finding.file) {
481
- log.debug(` File: ${finding.file}${finding.line ? `:${finding.line}` : ''}`);
482
- }
483
- if (finding.sourceReviewers && finding.sourceReviewers.length > 0) {
484
- log.debug(` Source reviewers: ${finding.sourceReviewers.join(', ')}`);
485
- }
486
- if (finding.contextTitles && finding.contextTitles.length > 0) {
487
- log.debug(` ContextRail Standards: ${finding.contextTitles.join(', ')}`);
488
- }
489
- log.debug(` ${finding.description}`);
490
- }
491
- }
492
- else {
493
- // Fallback: Show raw reviewer findings when synthesis is unavailable.
494
- const hasAnyEntries = reviewerResults.some((rr) => rr.findings.length > 0);
495
- if (hasAnyEntries) {
496
- log.debug('\n All findings:');
497
- for (const rr of reviewerResults) {
498
- if (rr.findings.length > 0) {
499
- log.debug(`\n ${rr.reviewer}:`);
500
- for (const finding of rr.findings) {
501
- log.debug(` [${finding.severity.toUpperCase()}] ${finding.title}`);
502
- if (finding.file) {
503
- log.debug(` File: ${finding.file}${finding.line ? `:${finding.line}` : ''}`);
504
- }
505
- if (finding.contextTitles && finding.contextTitles.length > 0) {
506
- log.debug(` ContextRail Standards: ${finding.contextTitles.join(', ')}`);
507
- }
508
- log.debug(` ${finding.description}`);
509
- }
510
- }
511
- }
512
- }
513
- }
514
- // Show failures if any
515
- if (result.failures && result.failures.length > 0) {
516
- log.warn('\n Reviewer failures:');
517
- for (const failure of result.failures) {
518
- log.warn(` ${failure.reviewer}: ${failure.message}`);
519
- }
520
- }
521
- log.info('═══════════════════════════════════════════════════════════\n');
522
- // Set exit code based on reviewer statuses
523
- const hasFailures = reviewerResults.some((rr) => !rr.validated);
524
- if (hasFailures) {
525
- log.error('❌ Review failed: Some reviewers did not validate');
526
- process.exit(1);
527
- }
528
- else if (result.decision.decision === 'request-changes') {
529
- log.error('❌ Review decision: REQUEST CHANGES');
530
- log.error(` Reason: ${result.decision.rationale}`);
531
- process.exit(1);
532
- }
533
- else {
534
- if (result.summary.totalFindings === 0) {
535
- log.info('✅ Review passed: No findings - code is clean!');
536
- }
537
- else {
538
- log.info('✅ Review passed: Findings present but decision is APPROVE');
539
- log.info(` ${result.summary.totalFindings} finding(s) were reviewed and deemed acceptable`);
540
- }
541
- log.info(` Decision rationale: ${result.decision.rationale}`);
542
- process.exit(0);
543
- }
544
- }
545
- finally {
546
- await mcpClient.close();
41
+ result.totalFindings === 0
42
+ ? log.info('✅ Review passed: No findings - code is clean!')
43
+ : log.info(`✅ Review passed: ${result.totalFindings} finding(s) deemed acceptable`);
44
+ process.exit(0);
547
45
  }
548
46
  }
549
47
  catch (error) {
48
+ const normalizedError = toError(error);
550
49
  const log = createLogger(parseLogLevel(process.env.DEBUG));
551
- const normalizedError = error instanceof Error ? error : new Error('Unknown error');
552
- const message = normalizedError.message;
553
- const stack = normalizedError.stack;
554
- log.error({ msg: 'Fatal error', error: message, stack });
555
- if (stack && process.env.NODE_ENV === 'development') {
556
- log.debug({ msg: 'Error stack trace', stack });
557
- }
50
+ log.error({
51
+ msg: 'Fatal error in main execution',
52
+ error: serializeError(normalizedError),
53
+ });
558
54
  await gracefulShutdown('SIGTERM', normalizedError);
559
- return;
560
55
  }
561
56
  };
562
- // Set up process-level error handlers with structured logging
563
- let mcpClientInstance = null;
564
- const gracefulShutdown = async (signal, error) => {
565
- const log = createLogger(parseLogLevel(process.env.DEBUG));
566
- log.warn({ msg: `Received ${signal}. Shutting down...`, signal });
567
- try {
568
- if (mcpClientInstance) {
569
- await mcpClientInstance.close();
570
- }
571
- process.exit(error ? 1 : 0);
572
- }
573
- catch (shutdownError) {
574
- const message = shutdownError instanceof Error ? shutdownError.message : 'Unknown error';
575
- log.error({ msg: 'Error during shutdown', error: message });
576
- process.exit(1);
577
- }
578
- };
579
- process.on('uncaughtException', async (error) => {
580
- const log = createLogger(parseLogLevel(process.env.DEBUG));
581
- log.error({
582
- msg: 'Uncaught exception',
583
- error: error.message,
584
- stack: error.stack,
585
- name: error.name,
586
- });
587
- await gracefulShutdown('SIGTERM', error);
588
- });
589
- process.on('unhandledRejection', async (reason) => {
57
+ void main().catch(async (error) => {
58
+ const normalizedError = toError(error);
590
59
  const log = createLogger(parseLogLevel(process.env.DEBUG));
591
- const message = reason instanceof Error ? reason.message : String(reason);
592
- const stack = reason instanceof Error ? reason.stack : undefined;
593
60
  log.error({
594
- msg: 'Unhandled rejection',
595
- error: message,
596
- stack,
61
+ msg: 'Unhandled top-level main() rejection',
62
+ error: serializeError(normalizedError),
597
63
  });
598
- await gracefulShutdown('SIGTERM', reason instanceof Error ? reason : new Error(message));
64
+ await gracefulShutdown('SIGTERM', normalizedError);
599
65
  });
600
- // Capture SIGTERM and SIGINT for graceful shutdown
601
- process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
602
- process.on('SIGINT', () => gracefulShutdown('SIGINT'));
603
- main();
@@ -0,0 +1,4 @@
1
+ import type { McpClient } from './mcp/client.js';
2
+ export declare const setMcpClient: (client: McpClient) => void;
3
+ export declare const gracefulShutdown: (signal: string, error?: Error) => Promise<void>;
4
+ export declare const setupLifecycleHandlers: () => void;