@krotovm/gitlab-ai-review 1.0.19 → 1.0.21

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/README.md CHANGED
@@ -32,12 +32,12 @@ ai_review:
32
32
 
33
33
  ## Env variables
34
34
 
35
- Set these in your project/group CI settings (or locally in your shell):
35
+ Set these in your project/group CI settings:
36
36
 
37
37
  - `OPENAI_API_KEY` (required)
38
38
  - `OPENAI_BASE_URL` (optional, for OpenAI-compatible providers/proxies)
39
39
  - `AI_MODEL` (optional, default: `gpt-4o-mini`; example: `gpt-4o`)
40
- - `PROJECT_ACCESS_TOKEN` (optional but recommended for private projects; token with `api` scope)
40
+ - `PROJECT_ACCESS_TOKEN` (optional for public projects, but required for most private projects; token with `api` scope)
41
41
  - `GITLAB_TOKEN` (optional alias for `PROJECT_ACCESS_TOKEN`)
42
42
 
43
43
  `OPENAI_BASE_URL` is passed through to the `openai` SDK client, so you can use any OpenAI-compatible gateway/provider endpoint.
@@ -62,12 +62,10 @@ GitLab provides these automatically in Merge Request pipelines:
62
62
 
63
63
  ## Architecture
64
64
 
65
- The reviewer uses a three-pass pipeline optimised for large merge requests:
65
+ The reviewer uses a three-pass pipeline optimized for large merge requests:
66
66
 
67
- 1. **Triage** a fast LLM call classifies each changed file as `NEEDS_REVIEW` or `SKIP` (cosmetic-only changes, docs, config) and produces a short MR summary.
68
- 2. **Per-file review** only `NEEDS_REVIEW` files are reviewed, each in a dedicated LLM call running in parallel (with tool access to fetch full files or grep the repository). Each file gets full model attention instead of competing for it across a single giant prompt.
69
- 3. **Consolidate** all per-file findings are merged, deduplicated, ranked by severity, and trimmed to the top N (default 5).
67
+ 1. **Triage** - A fast LLM pass classifies each changed file as `NEEDS_REVIEW` or `SKIP` and generates a short MR summary.
68
+ 2. **Per-file review** - Only `NEEDS_REVIEW` files are reviewed, each in a dedicated LLM call running in parallel (with tools to fetch full files or grep the repository).
69
+ 3. **Consolidate** - Per-file findings are merged, deduplicated, ranked by severity, and trimmed to top N (default 5).
70
70
 
71
71
  If the triage pass fails (API error, unparseable response), the pipeline falls back to the original single-pass approach automatically.
72
-
73
- The CI pipeline falls back to a simpler single-pass review automatically when triage cannot be used.
@@ -1,6 +1,6 @@
1
1
  /** @format */
2
2
  import OpenAI from "openai";
3
- import { AI_MAX_OUTPUT_TOKENS, buildAnswer, buildConsolidatePrompt, buildFileReviewPrompt, buildPrompt, buildTriagePrompt, extractCompletionText, parseTriageResponse, } from "../prompt/index.js";
3
+ import { AI_MAX_OUTPUT_TOKENS, buildAnswer, buildConsolidatePrompt, buildFileReviewPrompt, buildPrompt, buildTriagePrompt, buildVerificationPrompt, extractCompletionText, parseTriageResponse, } from "../prompt/index.js";
4
4
  import { fetchFileAtRef, searchRepository, } from "../gitlab/services.js";
5
5
  import { logToolUsageMinimal, MAX_FILE_TOOL_ROUNDS, MAX_TOOL_ROUNDS, TOOL_NAME_GET_FILE, TOOL_NAME_GREP, } from "./tooling.js";
6
6
  function buildReviewMetadata(changes, refs) {
@@ -339,7 +339,7 @@ async function runFileReviewWithTools(params) {
339
339
  export async function reviewMergeRequestMultiPass(params) {
340
340
  const { openaiInstance, aiModel, promptLimits, changes, refs, gitLabProjectApiUrl, projectId, headers, maxFindings, reviewConcurrency, forceTools, loggers, } = params;
341
341
  const { logStep } = loggers;
342
- logStep(`Pass 1/3: triaging ${changes.length} file(s)`);
342
+ logStep(`Pass 1/4: triaging ${changes.length} file(s)`);
343
343
  const triageInputs = changes.map((c) => ({
344
344
  path: c.new_path,
345
345
  new_file: c.new_file,
@@ -387,7 +387,7 @@ export async function reviewMergeRequestMultiPass(params) {
387
387
  const DISCLAIMER = "This comment was generated by an artificial intelligence duck.";
388
388
  return `No confirmed bugs or high-value optimizations found.\n\n---\n_${DISCLAIMER}_`;
389
389
  }
390
- logStep(`Pass 2/3: reviewing ${reviewFiles.length} file(s) (concurrency=${reviewConcurrency})`);
390
+ logStep(`Pass 2/4: reviewing ${reviewFiles.length} file(s) (concurrency=${reviewConcurrency})`);
391
391
  const allChangedPaths = changes.map((c) => c.new_path);
392
392
  const perFileFindings = await mapWithConcurrency(reviewFiles, reviewConcurrency, async (change) => {
393
393
  const otherFiles = allChangedPaths.filter((p) => p !== change.new_path);
@@ -407,7 +407,7 @@ export async function reviewMergeRequestMultiPass(params) {
407
407
  });
408
408
  return { path: change.new_path, findings };
409
409
  });
410
- logStep("Pass 3/3: consolidating findings");
410
+ logStep("Pass 3/4: consolidating findings");
411
411
  const consolidateMessages = buildConsolidatePrompt({
412
412
  perFileFindings,
413
413
  summary: triageResult.summary,
@@ -425,7 +425,31 @@ export async function reviewMergeRequestMultiPass(params) {
425
425
  stream: false,
426
426
  messages: consolidateMessages,
427
427
  });
428
- return buildAnswer(consolidateCompletion);
428
+ const consolidatedText = extractCompletionText(consolidateCompletion);
429
+ if (consolidatedText == null || consolidatedText.trim() === "") {
430
+ return buildAnswer(consolidateCompletion);
431
+ }
432
+ logStep("Pass 4/4: verifying consolidated findings");
433
+ const verificationMessages = buildVerificationPrompt({
434
+ perFileFindings,
435
+ summary: triageResult.summary,
436
+ consolidatedFindings: consolidatedText,
437
+ maxFindings,
438
+ });
439
+ try {
440
+ const verificationCompletion = await openaiInstance.chat.completions.create({
441
+ model: aiModel,
442
+ temperature: 0.0,
443
+ max_tokens: AI_MAX_OUTPUT_TOKENS,
444
+ stream: false,
445
+ messages: verificationMessages,
446
+ });
447
+ return buildAnswer(verificationCompletion);
448
+ }
449
+ catch (error) {
450
+ logStep(`Verification failed: ${error?.message ?? error}. Returning consolidated findings.`);
451
+ return buildAnswer(consolidateCompletion);
452
+ }
429
453
  }
430
454
  catch (error) {
431
455
  logStep(`Consolidation failed: ${error?.message ?? error}. Returning raw per-file findings.`);
package/dist/cli.js CHANGED
@@ -30,7 +30,7 @@ function printHelp() {
30
30
  " OPENAI_API_KEY (required) OpenAI API key.",
31
31
  " OPENAI_BASE_URL (optional) Custom OpenAI-compatible API base URL.",
32
32
  " AI_MODEL (optional) OpenAI chat model, e.g. gpt-4o. Default: gpt-4o-mini.",
33
- " PROJECT_ACCESS_TOKEN (optional) GitLab Project/Personal Access Token for API calls (recommended for private projects).",
33
+ " PROJECT_ACCESS_TOKEN (optional) GitLab Project/Personal Access Token for API calls (required for most private repos; should have api scope).",
34
34
  "",
35
35
  "CI-only env vars (provided by GitLab):",
36
36
  " CI_API_V4_URL, CI_PROJECT_ID, CI_MERGE_REQUEST_IID, CI_JOB_TOKEN (only if PROJECT_ACCESS_TOKEN is not set)",
@@ -9,7 +9,7 @@ export const DEFAULT_PROMPT_LIMITS = {
9
9
  const MESSAGES = buildMainSystemMessages();
10
10
  export const AI_MODEL_TEMPERATURE = 0.2;
11
11
  export const AI_MAX_OUTPUT_TOKENS = 600;
12
- export const buildPrompt = ({ changes, limits, allowTools = false, additionalContext, }) => {
12
+ export const buildPrompt = ({ changes, limits, allowTools = false, }) => {
13
13
  const effectiveLimits = {
14
14
  ...DEFAULT_PROMPT_LIMITS,
15
15
  ...(limits ?? {}),
@@ -36,9 +36,6 @@ export const buildPrompt = ({ changes, limits, allowTools = false, additionalCon
36
36
  const userContent = [
37
37
  `Review the following code changes (git diff format). ${stats}`,
38
38
  toolNote,
39
- additionalContext?.trim()
40
- ? `\nAdditional local context (best-effort snippets):\n${additionalContext}`
41
- : "",
42
39
  "",
43
40
  "Changes:",
44
41
  changesText || "(no changes provided)",
@@ -169,6 +166,47 @@ export function buildConsolidatePrompt(params) {
169
166
  },
170
167
  ];
171
168
  }
169
+ export function buildVerificationPrompt(params) {
170
+ const { perFileFindings, summary, consolidatedFindings, maxFindings } = params;
171
+ const findingsText = perFileFindings
172
+ .map((f) => `### ${f.path}\n${f.findings}`)
173
+ .join("\n\n");
174
+ return [
175
+ {
176
+ role: "system",
177
+ content: [
178
+ "You are a skeptical verifier of a merge request review.",
179
+ "Your job is to remove weak, speculative, or unsupported findings from the draft list.",
180
+ "Do not add new findings. Keep, rewrite for clarity, or remove existing findings only.",
181
+ "A finding can stay only if it is directly supported by evidence from the per-file findings.",
182
+ "If confidence is not high, drop the finding.",
183
+ "Preserve this exact per-finding markdown block:",
184
+ "`- [high|medium] <title>`",
185
+ "` File: <path>`",
186
+ "` Line: ~<N>`",
187
+ "` Why: <one concise sentence with key evidence>`",
188
+ "Do not add headings, summaries, or extra commentary.",
189
+ `Return at most ${maxFindings} findings.`,
190
+ 'If no findings survive verification, return exactly: "No confirmed bugs or high-value optimizations found."',
191
+ "GitLab-flavoured markdown.",
192
+ ].join("\n"),
193
+ },
194
+ {
195
+ role: "user",
196
+ content: [
197
+ `MR Summary: ${summary}`,
198
+ "",
199
+ "Per-file findings (evidence pool):",
200
+ findingsText,
201
+ "",
202
+ "Draft consolidated findings to verify:",
203
+ consolidatedFindings,
204
+ "",
205
+ "Return only the verified final findings.",
206
+ ].join("\n"),
207
+ },
208
+ ];
209
+ }
172
210
  export function extractCompletionText(completion) {
173
211
  if (completion instanceof Error || completion == null)
174
212
  return null;
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@krotovm/gitlab-ai-review",
4
- "version": "1.0.19",
5
- "description": "CLI tool to generate AI code reviews for GitLab merge requests and local diffs.",
4
+ "version": "1.0.21",
5
+ "description": "CLI tool to generate AI code reviews for GitLab merge requests.",
6
6
  "main": "dist/cli.js",
7
7
  "bin": {
8
8
  "gitlab-ai-review": "dist/cli.js"