@krotovm/gitlab-ai-review 1.0.40 → 1.0.42
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 +24 -0
- package/dist/cli/ci-review.js +29 -13
- package/dist/cli.js +39 -40
- package/dist/gitlab/services.js +10 -10
- package/dist/prompt/index.js +3 -3
- package/dist/prompt/messages.js +5 -5
- package/dist/prompt/templates/postprocess-system.js +94 -108
- package/dist/prompt/templates/review-system.js +108 -116
- package/dist/prompt/templates/triage-system.js +44 -48
- package/dist/prompt/templates/user-prompts.js +34 -42
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -2,8 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
# AI Code Reviewer
|
|
4
4
|
|
|
5
|
+
[](https://www.npmjs.com/package/@krotovm/gitlab-ai-review)
|
|
6
|
+
[](https://www.npmjs.com/package/@krotovm/gitlab-ai-review)
|
|
7
|
+
[](https://github.com/KrotovM/gitlab-ai-mr-reviewer/blob/main/LICENSE)
|
|
8
|
+
[](https://gitlab.com/explore/catalog/KrotovM/gitlab-ai-review)
|
|
9
|
+
|
|
5
10
|
Gitlab AI Code Review is a CLI tool that leverages OpenAI models to automatically review code changes and post a Markdown review to GitLab merge requests from CI.
|
|
6
11
|
|
|
12
|
+

|
|
13
|
+
|
|
7
14
|
## Features
|
|
8
15
|
|
|
9
16
|
- Automatically reviews code changes in GitLab repositories
|
|
@@ -30,6 +37,15 @@ ai_review:
|
|
|
30
37
|
- npx -y @krotovm/gitlab-ai-review
|
|
31
38
|
```
|
|
32
39
|
|
|
40
|
+
Or include it as a [CI/CD component from the GitLab Catalog](https://gitlab.com/explore/catalog/KrotovM/gitlab-ai-review) (gitlab.com):
|
|
41
|
+
|
|
42
|
+
```yaml
|
|
43
|
+
include:
|
|
44
|
+
- component: gitlab.com/KrotovM/gitlab-ai-review/review@1.0.2
|
|
45
|
+
inputs:
|
|
46
|
+
args: "--include-artifacts"
|
|
47
|
+
```
|
|
48
|
+
|
|
33
49
|
Save debug HTML as a CI artifact:
|
|
34
50
|
|
|
35
51
|
```yaml
|
|
@@ -48,6 +64,10 @@ ai_review:
|
|
|
48
64
|
- ai-review-report.html
|
|
49
65
|
```
|
|
50
66
|
|
|
67
|
+
The HTML artifact breaks the run down per pass — tokens, durations, triage decisions, per-file findings, and errors:
|
|
68
|
+
|
|
69
|
+

|
|
70
|
+
|
|
51
71
|
## Env variables
|
|
52
72
|
|
|
53
73
|
Set these in your project/group CI settings:
|
|
@@ -110,3 +130,7 @@ The reviewer uses a three-pass pipeline optimized for large merge requests:
|
|
|
110
130
|
3. **Consolidate** - Per-file findings are merged, deduplicated, ranked by severity, and trimmed to top N (default 5).
|
|
111
131
|
|
|
112
132
|
If the triage pass fails (API error, unparseable response), the pipeline falls back to the original single-pass approach automatically.
|
|
133
|
+
|
|
134
|
+
The pipeline in a CI job log — triage, parallel tool-assisted file reviews, consolidation, verification:
|
|
135
|
+
|
|
136
|
+

|
package/dist/cli/ci-review.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import OpenAI from "openai";
|
|
3
3
|
import { buildAnswer, buildConsolidatePrompt, buildFileReviewPrompt, buildPrompt, buildTriagePrompt, buildVerificationPrompt, DEFAULT_PROMPT_PROFILE, extractCompletionText, isEmptyReviewBody, NO_FINDINGS_SENTENCE, parseTriageResponseDetailed, parseTriageResponse, } from "../prompt/index.js";
|
|
4
4
|
import { fetchFileAtRef, searchRepository, } from "../gitlab/services.js";
|
|
5
|
-
import { truncateWithMarker } from "../prompt/utils.js";
|
|
5
|
+
import { parseReviewFindings, truncateWithMarker } from "../prompt/utils.js";
|
|
6
6
|
import { logToolUsageMinimal, MAX_FILE_TOOL_ROUNDS, MAX_TOOL_ROUNDS, MAX_VERIFICATION_TOOL_ROUNDS, TOOL_NAME_GET_FILE, TOOL_NAME_GREP, } from "./tooling.js";
|
|
7
7
|
/** Hard deadline per model call, retries included. Must exceed the gateway's own
|
|
8
8
|
* proxy timeout so the SDK's 5xx retries stay the first line of defense. */
|
|
@@ -143,7 +143,7 @@ function buildReviewMetadata(changes, refs) {
|
|
|
143
143
|
changed_files: files,
|
|
144
144
|
tool_usage_guidance: [
|
|
145
145
|
"If diff context is insufficient, call get_file_at_ref to read a specific file.",
|
|
146
|
-
"Use grep_repository to
|
|
146
|
+
"Use grep_repository to find usages or definitions by literal text (no regex) across the codebase.",
|
|
147
147
|
"Use refs.base to inspect pre-change content and refs.head for current content.",
|
|
148
148
|
"Prefer targeted searches and file fetches; avoid broad context requests.",
|
|
149
149
|
],
|
|
@@ -161,7 +161,7 @@ async function handleGetFileTool(argsRaw, gitLabProjectApiUrl, headers) {
|
|
|
161
161
|
});
|
|
162
162
|
}
|
|
163
163
|
const fileText = await fetchFileAtRef({
|
|
164
|
-
|
|
164
|
+
gitLabProjectApiUrl: gitLabProjectApiUrl,
|
|
165
165
|
headers,
|
|
166
166
|
filePath: path,
|
|
167
167
|
ref,
|
|
@@ -198,7 +198,7 @@ async function handleGrepTool(argsRaw, defaultRef, gitLabProjectApiUrl, headers,
|
|
|
198
198
|
return JSON.stringify({ ok: false, error: "query is required." });
|
|
199
199
|
const ref = parsed.ref?.trim() || defaultRef;
|
|
200
200
|
const results = await searchRepository({
|
|
201
|
-
|
|
201
|
+
gitLabProjectApiUrl: gitLabProjectApiUrl,
|
|
202
202
|
headers,
|
|
203
203
|
query,
|
|
204
204
|
ref,
|
|
@@ -279,14 +279,14 @@ export async function reviewMergeRequestWithTools(params) {
|
|
|
279
279
|
type: "function",
|
|
280
280
|
function: {
|
|
281
281
|
name: TOOL_NAME_GREP,
|
|
282
|
-
description: "Search the repository for a
|
|
282
|
+
description: "Search the repository for literal text (case-insensitive substring match). Regular expressions are not supported: `a|b` or `foo.*bar` are searched as literal text, so query one identifier at a time. Returns up to 10 matching code fragments with file paths and line numbers.",
|
|
283
283
|
parameters: {
|
|
284
284
|
type: "object",
|
|
285
285
|
additionalProperties: false,
|
|
286
286
|
properties: {
|
|
287
287
|
query: {
|
|
288
288
|
type: "string",
|
|
289
|
-
description: "
|
|
289
|
+
description: "Literal text to find, e.g. a function or variable name (no regex).",
|
|
290
290
|
},
|
|
291
291
|
ref: {
|
|
292
292
|
type: "string",
|
|
@@ -424,14 +424,14 @@ async function runFileReviewWithTools(params) {
|
|
|
424
424
|
type: "function",
|
|
425
425
|
function: {
|
|
426
426
|
name: TOOL_NAME_GREP,
|
|
427
|
-
description: "Search the repository for a
|
|
427
|
+
description: "Search the repository for literal text (case-insensitive substring match). Regular expressions are not supported: `a|b` or `foo.*bar` are searched as literal text, so query one identifier at a time. Returns up to 10 matching code fragments with file paths and line numbers.",
|
|
428
428
|
parameters: {
|
|
429
429
|
type: "object",
|
|
430
430
|
additionalProperties: false,
|
|
431
431
|
properties: {
|
|
432
432
|
query: {
|
|
433
433
|
type: "string",
|
|
434
|
-
description: "
|
|
434
|
+
description: "Literal text to find, e.g. a function or variable name (no regex).",
|
|
435
435
|
},
|
|
436
436
|
ref: {
|
|
437
437
|
type: "string",
|
|
@@ -564,14 +564,14 @@ async function runVerificationWithTools(params) {
|
|
|
564
564
|
type: "function",
|
|
565
565
|
function: {
|
|
566
566
|
name: TOOL_NAME_GREP,
|
|
567
|
-
description: "Search the repository for a
|
|
567
|
+
description: "Search the repository for literal text (case-insensitive substring match). Regular expressions are not supported: `a|b` or `foo.*bar` are searched as literal text, so query one identifier at a time. Returns up to 10 matching code fragments with file paths and line numbers.",
|
|
568
568
|
parameters: {
|
|
569
569
|
type: "object",
|
|
570
570
|
additionalProperties: false,
|
|
571
571
|
properties: {
|
|
572
572
|
query: {
|
|
573
573
|
type: "string",
|
|
574
|
-
description: "
|
|
574
|
+
description: "Literal text to find, e.g. a function or variable name (no regex).",
|
|
575
575
|
},
|
|
576
576
|
ref: {
|
|
577
577
|
type: "string",
|
|
@@ -849,6 +849,22 @@ export async function reviewMergeRequestMultiPass(params) {
|
|
|
849
849
|
// every final-answer path gets it regardless of model formatting discipline.
|
|
850
850
|
const summaryText = triageResult.summary.trim().replace(/\s*\n\s*/g, " ");
|
|
851
851
|
const withHeader = (body) => `### 🤖 AI Code Review\n\n${summaryText === "" ? "" : `> ${summaryText}\n\n`}${body}`;
|
|
852
|
+
// Cap transparency: if consolidation likely trimmed the list (final count hit
|
|
853
|
+
// the cap while per-file review produced more candidates), say so in the note
|
|
854
|
+
// instead of silently dropping findings.
|
|
855
|
+
// ponytail: candidates deduped by file+title only; differently-worded dupes
|
|
856
|
+
// across files still inflate the count, which is why the note says "candidate".
|
|
857
|
+
const candidateCount = new Set(perFileFindings.flatMap((f) => parseReviewFindings(f.findings).map((x) => `${x.file}|${x.title.toLowerCase()}`))).size;
|
|
858
|
+
const withCapNote = (body) => {
|
|
859
|
+
const finalCount = parseReviewFindings(body).length;
|
|
860
|
+
if (finalCount < maxFindings || candidateCount <= finalCount)
|
|
861
|
+
return body;
|
|
862
|
+
const note = `\n\nℹ️ Showing top ${finalCount} of ${candidateCount} candidate findings (\`--max-findings=${maxFindings}\`). Run with \`--include-artifacts\` for the full list.`;
|
|
863
|
+
const disclaimerAt = body.lastIndexOf("\n\n---\n_");
|
|
864
|
+
return disclaimerAt === -1
|
|
865
|
+
? body + note
|
|
866
|
+
: body.slice(0, disclaimerAt) + note + body.slice(disclaimerAt);
|
|
867
|
+
};
|
|
852
868
|
logStep("Pass 3/4: consolidating findings");
|
|
853
869
|
const consolidateMessages = buildConsolidatePrompt({
|
|
854
870
|
perFileFindings,
|
|
@@ -875,7 +891,7 @@ export async function reviewMergeRequestMultiPass(params) {
|
|
|
875
891
|
});
|
|
876
892
|
const consolidatedText = extractCompletionText(consolidateCompletion);
|
|
877
893
|
if (consolidatedText == null || consolidatedText.trim() === "") {
|
|
878
|
-
return withHeader(buildAnswer(consolidateCompletion));
|
|
894
|
+
return withHeader(withCapNote(buildAnswer(consolidateCompletion)));
|
|
879
895
|
}
|
|
880
896
|
logStep("Pass 4/4: verifying consolidated findings (repo tools)");
|
|
881
897
|
const verificationMessages = buildVerificationPrompt({
|
|
@@ -901,11 +917,11 @@ export async function reviewMergeRequestMultiPass(params) {
|
|
|
901
917
|
debugDumpFile,
|
|
902
918
|
debugRecordWriter,
|
|
903
919
|
});
|
|
904
|
-
return withHeader(buildAnswer(verificationCompletion));
|
|
920
|
+
return withHeader(withCapNote(buildAnswer(verificationCompletion)));
|
|
905
921
|
}
|
|
906
922
|
catch (error) {
|
|
907
923
|
logStep(`Verification failed: ${error?.message ?? error}. Returning consolidated findings.`);
|
|
908
|
-
return withHeader(buildAnswer(consolidateCompletion));
|
|
924
|
+
return withHeader(withCapNote(buildAnswer(consolidateCompletion)));
|
|
909
925
|
}
|
|
910
926
|
}
|
|
911
927
|
catch (error) {
|
package/dist/cli.js
CHANGED
|
@@ -8,43 +8,41 @@ import { reviewMergeRequestMultiPass } from "./cli/ci-review.js";
|
|
|
8
8
|
import { fetchMergeRequestChanges, postMergeRequestNote, } from "./gitlab/services.js";
|
|
9
9
|
import { renderDebugArtifactsHtml } from "./cli/debug-artifacts-html.js";
|
|
10
10
|
function printHelp() {
|
|
11
|
-
process.stdout.write(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"",
|
|
47
|
-
].join("\n"));
|
|
11
|
+
process.stdout.write(`gitlab-ai-review
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
gitlab-ai-review
|
|
15
|
+
gitlab-ai-review --help
|
|
16
|
+
gitlab-ai-review --debug
|
|
17
|
+
gitlab-ai-review --ignore-ext=md,lock
|
|
18
|
+
|
|
19
|
+
Debug:
|
|
20
|
+
--debug Print full error details (stack, API error fields).
|
|
21
|
+
--include-artifacts Generate local HTML artifact without printing payloads to console.
|
|
22
|
+
--force-tools Force at least one tool-call round in tool-enabled review paths.
|
|
23
|
+
--ignore-ext Ignore file extensions (comma-separated only). Example: --ignore-ext=md,lock
|
|
24
|
+
--max-diffs=50
|
|
25
|
+
--max-diff-chars=16000
|
|
26
|
+
--max-total-prompt-chars=220000
|
|
27
|
+
--triage-diff-chars=2000 Max chars per file diff in triage pass (Pass 1).
|
|
28
|
+
--max-findings=5 Max findings in final review (CI multi-pass only).
|
|
29
|
+
--max-review-concurrency=2 Parallel per-file review calls (CI multi-pass only).
|
|
30
|
+
|
|
31
|
+
Env vars:
|
|
32
|
+
OPENAI_API_KEY (required) OpenAI API key.
|
|
33
|
+
OPENAI_BASE_URL (optional) Custom OpenAI-compatible API base URL.
|
|
34
|
+
AI_MODEL (optional) OpenAI chat model, e.g. gpt-4o. Default: gpt-4o-mini.
|
|
35
|
+
AI_REVIEW_CONCURRENCY (optional) Same as --max-review-concurrency; the flag wins.
|
|
36
|
+
AI_PROMPT_PROFILE (optional) Prompt style: "default" | "weak". Default: default.
|
|
37
|
+
Use "weak" for small/quantized models — shorter prompts,
|
|
38
|
+
positive rules, and few-shot examples.
|
|
39
|
+
When unset, an unparseable triage response auto-retries
|
|
40
|
+
with "weak" and keeps it for the whole run.
|
|
41
|
+
PROJECT_ACCESS_TOKEN (optional) GitLab Project/Personal Access Token for API calls (required for most private repos; should have api scope).
|
|
42
|
+
|
|
43
|
+
CI-only env vars (provided by GitLab):
|
|
44
|
+
CI_API_V4_URL, CI_PROJECT_ID, CI_MERGE_REQUEST_IID, CI_JOB_TOKEN (only if PROJECT_ACCESS_TOKEN is not set)
|
|
45
|
+
`);
|
|
48
46
|
}
|
|
49
47
|
const DEBUG_MODE = hasDebugFlag(process.argv);
|
|
50
48
|
const FORCE_TOOLS = hasForceToolsFlag(process.argv);
|
|
@@ -116,6 +114,7 @@ async function main() {
|
|
|
116
114
|
const ciApiV4Url = envs["CI_API_V4_URL"];
|
|
117
115
|
const projectId = envs["CI_PROJECT_ID"];
|
|
118
116
|
const mergeRequestIid = envs["CI_MERGE_REQUEST_IID"];
|
|
117
|
+
const gitLabProjectApiUrl = new URL(`${ciApiV4Url}/projects/${projectId}`);
|
|
119
118
|
const headers = {};
|
|
120
119
|
if (projectAccessToken != null)
|
|
121
120
|
headers["PRIVATE-TOKEN"] = projectAccessToken;
|
|
@@ -124,7 +123,7 @@ async function main() {
|
|
|
124
123
|
try {
|
|
125
124
|
logStep("Fetching merge request changes");
|
|
126
125
|
const mrChanges = await fetchMergeRequestChanges({
|
|
127
|
-
|
|
126
|
+
gitLabProjectApiUrl: gitLabProjectApiUrl,
|
|
128
127
|
headers,
|
|
129
128
|
projectId,
|
|
130
129
|
mergeRequestIid,
|
|
@@ -149,7 +148,7 @@ async function main() {
|
|
|
149
148
|
base: mrChanges.diff_refs?.base_sha ?? "HEAD",
|
|
150
149
|
head: mrChanges.diff_refs?.head_sha ?? "HEAD",
|
|
151
150
|
},
|
|
152
|
-
gitLabProjectApiUrl:
|
|
151
|
+
gitLabProjectApiUrl: gitLabProjectApiUrl,
|
|
153
152
|
projectId,
|
|
154
153
|
headers,
|
|
155
154
|
maxFindings,
|
|
@@ -162,7 +161,7 @@ async function main() {
|
|
|
162
161
|
});
|
|
163
162
|
logStep("Posting AI review note to merge request");
|
|
164
163
|
const noteRes = await postMergeRequestNote({
|
|
165
|
-
|
|
164
|
+
gitLabProjectApiUrl: gitLabProjectApiUrl,
|
|
166
165
|
headers,
|
|
167
166
|
mergeRequestIid,
|
|
168
167
|
}, { body: answer });
|
package/dist/gitlab/services.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
import OpenAI from "openai";
|
|
3
3
|
import { AI_MODEL_TEMPERATURE } from "../prompt/index.js";
|
|
4
4
|
import { GitLabError, OpenAIError, } from "./types.js";
|
|
5
|
-
export const fetchPreEditFiles = async ({
|
|
5
|
+
export const fetchPreEditFiles = async ({ gitLabProjectApiUrl, headers, changesOldPaths, ref }) => {
|
|
6
6
|
const oldFilesRequestUrls = changesOldPaths.map((filePath) => {
|
|
7
|
-
const url = new URL(`${
|
|
7
|
+
const url = new URL(`${gitLabProjectApiUrl}/repository/files/${encodeURIComponent(filePath)}/raw`);
|
|
8
8
|
url.searchParams.set("ref", ref);
|
|
9
9
|
return url;
|
|
10
10
|
});
|
|
@@ -42,8 +42,8 @@ export const fetchPreEditFiles = async ({ gitLabBaseUrl, headers, changesOldPath
|
|
|
42
42
|
return acc;
|
|
43
43
|
}, []);
|
|
44
44
|
};
|
|
45
|
-
export const fetchFileAtRef = async ({
|
|
46
|
-
const url = new URL(`${
|
|
45
|
+
export const fetchFileAtRef = async ({ gitLabProjectApiUrl, headers, filePath, ref }) => {
|
|
46
|
+
const url = new URL(`${gitLabProjectApiUrl}/repository/files/${encodeURIComponent(filePath)}/raw`);
|
|
47
47
|
url.searchParams.set("ref", ref);
|
|
48
48
|
let res;
|
|
49
49
|
try {
|
|
@@ -103,8 +103,8 @@ export async function generateAICompletion(messages, openaiInstance, aiModel) {
|
|
|
103
103
|
}
|
|
104
104
|
return completion;
|
|
105
105
|
}
|
|
106
|
-
export const postMergeRequestNote = async ({
|
|
107
|
-
const commentUrl = new URL(`${
|
|
106
|
+
export const postMergeRequestNote = async ({ gitLabProjectApiUrl, headers, mergeRequestIid }, commentPayload) => {
|
|
107
|
+
const commentUrl = new URL(`${gitLabProjectApiUrl}/merge_requests/${mergeRequestIid}/notes`);
|
|
108
108
|
let aiComment;
|
|
109
109
|
try {
|
|
110
110
|
aiComment = await fetch(commentUrl, {
|
|
@@ -147,8 +147,8 @@ export const postMergeRequestNote = async ({ gitLabBaseUrl, headers, mergeReques
|
|
|
147
147
|
});
|
|
148
148
|
}
|
|
149
149
|
};
|
|
150
|
-
export const searchRepository = async ({
|
|
151
|
-
const url = new URL(`${
|
|
150
|
+
export const searchRepository = async ({ gitLabProjectApiUrl, headers, query, ref }) => {
|
|
151
|
+
const url = new URL(`${gitLabProjectApiUrl}/search`);
|
|
152
152
|
url.searchParams.set("scope", "blobs");
|
|
153
153
|
url.searchParams.set("search", query);
|
|
154
154
|
url.searchParams.set("ref", ref);
|
|
@@ -185,8 +185,8 @@ export const searchRepository = async ({ gitLabBaseUrl, headers, query, ref, pro
|
|
|
185
185
|
}
|
|
186
186
|
return (await res.json());
|
|
187
187
|
};
|
|
188
|
-
export const fetchMergeRequestChanges = async ({
|
|
189
|
-
const url = new URL(`${
|
|
188
|
+
export const fetchMergeRequestChanges = async ({ gitLabProjectApiUrl, headers, mergeRequestIid }) => {
|
|
189
|
+
const url = new URL(`${gitLabProjectApiUrl}/merge_requests/${mergeRequestIid}/changes`);
|
|
190
190
|
// Ask GitLab for raw diffs to reduce truncation on larger merge requests.
|
|
191
191
|
url.searchParams.set("access_raw_diffs", "true");
|
|
192
192
|
let res;
|
package/dist/prompt/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** @format */
|
|
2
2
|
import { buildFileReviewSystemMessage, buildMainSystemMessages, buildTriageSystemMessage, } from "./messages.js";
|
|
3
3
|
import { extractFirstJsonObject, normalizeReviewFindingsMarkdown, sanitizeGitLabMarkdown, truncateWithMarker, } from "./utils.js";
|
|
4
|
-
import {
|
|
4
|
+
import { getConsolidateSystemPrompt, getVerificationSystemPrompt, } from "./templates/postprocess-system.js";
|
|
5
5
|
import { buildConsolidateUserContent, buildFileReviewUserContent, buildMainReviewUserContent, buildTriageUserContent, buildVerificationUserContent, } from "./templates/user-prompts.js";
|
|
6
6
|
import { DEFAULT_PROMPT_PROFILE, isEmptyReviewBody } from "./profile.js";
|
|
7
7
|
export { DEFAULT_PROMPT_PROFILE, NO_FINDINGS_SENTENCE, isEmptyReviewBody, parsePromptProfile, } from "./profile.js";
|
|
@@ -160,7 +160,7 @@ export function buildConsolidatePrompt(params) {
|
|
|
160
160
|
return [
|
|
161
161
|
{
|
|
162
162
|
role: "system",
|
|
163
|
-
content:
|
|
163
|
+
content: getConsolidateSystemPrompt(profile, maxFindings),
|
|
164
164
|
},
|
|
165
165
|
{
|
|
166
166
|
role: "user",
|
|
@@ -180,7 +180,7 @@ export function buildVerificationPrompt(params) {
|
|
|
180
180
|
return [
|
|
181
181
|
{
|
|
182
182
|
role: "system",
|
|
183
|
-
content:
|
|
183
|
+
content: getVerificationSystemPrompt(profile, maxFindings),
|
|
184
184
|
},
|
|
185
185
|
{
|
|
186
186
|
role: "user",
|
package/dist/prompt/messages.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
/** @format */
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { getFileReviewSystemPrompt, getMainSystemPrompt, } from "./templates/review-system.js";
|
|
3
|
+
import { getTriageSystemPrompt } from "./templates/triage-system.js";
|
|
4
4
|
import { DEFAULT_PROMPT_PROFILE } from "./profile.js";
|
|
5
5
|
export const buildMainSystemMessages = (profile = DEFAULT_PROMPT_PROFILE) => [
|
|
6
6
|
{
|
|
7
7
|
role: "system",
|
|
8
|
-
content:
|
|
8
|
+
content: getMainSystemPrompt(profile),
|
|
9
9
|
},
|
|
10
10
|
];
|
|
11
11
|
export const buildTriageSystemMessage = (profile = DEFAULT_PROMPT_PROFILE) => ({
|
|
12
12
|
role: "system",
|
|
13
|
-
content:
|
|
13
|
+
content: getTriageSystemPrompt(profile),
|
|
14
14
|
});
|
|
15
15
|
export const buildFileReviewSystemMessage = (profile = DEFAULT_PROMPT_PROFILE) => ({
|
|
16
16
|
role: "system",
|
|
17
|
-
content:
|
|
17
|
+
content: getFileReviewSystemPrompt(profile),
|
|
18
18
|
});
|
|
19
19
|
/** @deprecated kept for backward compatibility — use buildTriageSystemMessage(). */
|
|
20
20
|
export const TRIAGE_SYSTEM = buildTriageSystemMessage();
|
|
@@ -1,118 +1,104 @@
|
|
|
1
1
|
/** @format */
|
|
2
2
|
import { NO_FINDINGS_SENTENCE } from "../profile.js";
|
|
3
|
-
function
|
|
4
|
-
return
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"GitLab-flavoured markdown.",
|
|
18
|
-
];
|
|
3
|
+
function buildConsolidateDefaultPrompt(maxFindings) {
|
|
4
|
+
return `You consolidate per-file code review findings into a final ranked list.
|
|
5
|
+
Select the top ${maxFindings} most important findings. Deduplicate overlapping issues.
|
|
6
|
+
Preserve this exact per-finding markdown block:
|
|
7
|
+
\`- [high|medium] <title>\`
|
|
8
|
+
\` File: <path>\`
|
|
9
|
+
\` Line: ~<N>\`
|
|
10
|
+
\` Why: <one concise sentence with key evidence>\`
|
|
11
|
+
Rank by: (1) [high] before [medium], (2) correctness > security > perf.
|
|
12
|
+
Do not add new findings. Do not add headings, summaries, or commentary.
|
|
13
|
+
Drop any finding that: claims a syntax error without quoting the invalid token, claims a function/variable is missing without concrete proof, or flags a refactoring rename as a bug.
|
|
14
|
+
If fewer than ${maxFindings} findings exist, return all of them.
|
|
15
|
+
If all findings are low quality or dubious after dedup, return exactly: "${NO_FINDINGS_SENTENCE}"
|
|
16
|
+
GitLab-flavoured markdown.`;
|
|
19
17
|
}
|
|
20
|
-
function
|
|
21
|
-
return
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
`If after dedup nothing survives, output exactly this single line and nothing else: ${NO_FINDINGS_SENTENCE}`,
|
|
50
|
-
];
|
|
18
|
+
function buildConsolidateWeakPrompt(maxFindings) {
|
|
19
|
+
return `You merge per-file review findings into one ranked list.
|
|
20
|
+
Output bullets only — no preamble, no headings, no code fences.
|
|
21
|
+
Keep at most ${maxFindings} findings. Do not invent new ones.
|
|
22
|
+
|
|
23
|
+
Steps:
|
|
24
|
+
1. Drop duplicates and findings that are reworded versions of each other (keep the clearest).
|
|
25
|
+
2. Drop a finding if it has no concrete evidence (no file/line, vague reasoning, or just "this might be wrong").
|
|
26
|
+
3. Sort the remainder: all [high] before any [medium]; within a tier, correctness bugs before security before perf.
|
|
27
|
+
4. Take the top items up to the limit.
|
|
28
|
+
|
|
29
|
+
Output format — copy spacing and case exactly, do not wrap bullets in backticks:
|
|
30
|
+
- [high|medium] <short title>
|
|
31
|
+
File: <path>
|
|
32
|
+
Line: ~<N>
|
|
33
|
+
Why: <one sentence pointing at the evidence>
|
|
34
|
+
|
|
35
|
+
Example output (two findings, ranked):
|
|
36
|
+
- [high] Null pointer dereference in user lookup
|
|
37
|
+
File: src/user/service.ts
|
|
38
|
+
Line: ~42
|
|
39
|
+
Why: getUser() can return null but the next line calls user.id without a guard.
|
|
40
|
+
|
|
41
|
+
- [medium] Off-by-one in pagination loop
|
|
42
|
+
File: src/api/list.ts
|
|
43
|
+
Line: ~88
|
|
44
|
+
Why: loop runs while i <= total but indices are 0-based, so the last item is read past the array end.
|
|
45
|
+
|
|
46
|
+
If after dedup nothing survives, output exactly this single line and nothing else: ${NO_FINDINGS_SENTENCE}`;
|
|
51
47
|
}
|
|
52
|
-
function
|
|
53
|
-
return
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
"GitLab-flavoured markdown.",
|
|
70
|
-
];
|
|
48
|
+
function buildVerificationDefaultPrompt(maxFindings) {
|
|
49
|
+
return `You are a skeptical verifier of a merge request review.
|
|
50
|
+
Your job is to remove weak, speculative, or unsupported findings from the draft list.
|
|
51
|
+
Tools get_file_at_ref and grep_repository are available. Use them to check claims about current code against the repository at the MR head ref.
|
|
52
|
+
Drop a finding if file contents at refs.head contradict it, or if it cannot be verified after reasonable tool use.
|
|
53
|
+
Do not add new findings. Keep, rewrite for clarity, or remove existing findings only.
|
|
54
|
+
A finding can stay only if supported by the per-file evidence pool and not contradicted by tools when the claim is about code that exists at refs.head.
|
|
55
|
+
If confidence is not high, drop the finding.
|
|
56
|
+
Preserve this exact per-finding markdown block:
|
|
57
|
+
\`- [high|medium] <title>\`
|
|
58
|
+
\` File: <path>\`
|
|
59
|
+
\` Line: ~<N>\`
|
|
60
|
+
\` Why: <one concise sentence with key evidence>\`
|
|
61
|
+
Do not add headings, summaries, or extra commentary.
|
|
62
|
+
Return at most ${maxFindings} findings.
|
|
63
|
+
If no findings survive verification, return exactly: "${NO_FINDINGS_SENTENCE}"
|
|
64
|
+
GitLab-flavoured markdown.`;
|
|
71
65
|
}
|
|
72
|
-
function
|
|
73
|
-
return
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
`If after verification nothing survives, output exactly this single line and nothing else: ${NO_FINDINGS_SENTENCE}`,
|
|
101
|
-
];
|
|
66
|
+
function buildVerificationWeakPrompt(maxFindings) {
|
|
67
|
+
return `You verify a draft list of merge-request review findings against the actual repository.
|
|
68
|
+
Output bullets only — no preamble, no headings, no code fences. Do not invent new findings.
|
|
69
|
+
Return at most ${maxFindings} findings.
|
|
70
|
+
|
|
71
|
+
For each draft finding, follow these steps in order:
|
|
72
|
+
1. Look it up in the per-file evidence pool. If it is not in the evidence pool, drop it.
|
|
73
|
+
2. If the finding makes a claim about code that exists at refs.head, call get_file_at_ref(path, refs.head) and confirm the offending text really sits at the cited line (±10 lines is fine).
|
|
74
|
+
3. If the file contents at refs.head do not contain the offending text, or contain a corrected version of it, drop the finding.
|
|
75
|
+
4. If you cannot confirm the claim with one or two tool calls, drop the finding.
|
|
76
|
+
|
|
77
|
+
Keep a finding only after step 3 succeeds. Otherwise drop it.
|
|
78
|
+
|
|
79
|
+
Output format — copy spacing and case exactly, do not wrap bullets in backticks:
|
|
80
|
+
- [high|medium] <short title>
|
|
81
|
+
File: <path>
|
|
82
|
+
Line: ~<N>
|
|
83
|
+
Why: <one sentence pointing at the evidence you confirmed>
|
|
84
|
+
|
|
85
|
+
Example — KEEP this finding because get_file_at_ref shows the cited code at refs.head:
|
|
86
|
+
- [high] Null pointer dereference in user lookup
|
|
87
|
+
File: src/user/service.ts
|
|
88
|
+
Line: ~42
|
|
89
|
+
Why: getUser() returns null on miss and the next line calls user.id without a guard (confirmed in src/user/service.ts at refs.head).
|
|
90
|
+
|
|
91
|
+
Example — DROP a finding when refs.head shows the bug is already gone (do not output it).
|
|
92
|
+
|
|
93
|
+
If after verification nothing survives, output exactly this single line and nothing else: ${NO_FINDINGS_SENTENCE}`;
|
|
102
94
|
}
|
|
103
|
-
export function
|
|
104
|
-
return buildConsolidateDefaultLines(maxFindings);
|
|
105
|
-
}
|
|
106
|
-
export function buildVerificationSystemLines(maxFindings) {
|
|
107
|
-
return buildVerificationDefaultLines(maxFindings);
|
|
108
|
-
}
|
|
109
|
-
export function getConsolidateSystemLines(profile, maxFindings) {
|
|
95
|
+
export function getConsolidateSystemPrompt(profile, maxFindings) {
|
|
110
96
|
return profile === "weak"
|
|
111
|
-
?
|
|
112
|
-
:
|
|
97
|
+
? buildConsolidateWeakPrompt(maxFindings)
|
|
98
|
+
: buildConsolidateDefaultPrompt(maxFindings);
|
|
113
99
|
}
|
|
114
|
-
export function
|
|
100
|
+
export function getVerificationSystemPrompt(profile, maxFindings) {
|
|
115
101
|
return profile === "weak"
|
|
116
|
-
?
|
|
117
|
-
:
|
|
102
|
+
? buildVerificationWeakPrompt(maxFindings)
|
|
103
|
+
: buildVerificationDefaultPrompt(maxFindings);
|
|
118
104
|
}
|
|
@@ -1,120 +1,112 @@
|
|
|
1
1
|
/** @format */
|
|
2
2
|
import { NO_FINDINGS_SENTENCE } from "../profile.js";
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
"
|
|
105
|
-
" File: src/api/list.ts",
|
|
106
|
-
" Line: ~88",
|
|
107
|
-
" Why: loop runs while i <= total but indices are 0-based, so the last item is read past the array end.",
|
|
108
|
-
"",
|
|
109
|
-
`If you cannot meet all three conditions for any finding, output exactly this single line and nothing else: ${NO_FINDINGS_SENTENCE}`,
|
|
110
|
-
];
|
|
111
|
-
export function getMainSystemLines(profile) {
|
|
112
|
-
return profile === "weak" ? MAIN_WEAK_LINES : MAIN_DEFAULT_LINES;
|
|
3
|
+
const MAIN_DEFAULT = `You are an AI code reviewer for pull requests.
|
|
4
|
+
Find only real bugs introduced by the diff.
|
|
5
|
+
Return at most 3 findings. Prefer no finding over a weak one.
|
|
6
|
+
|
|
7
|
+
Rules:
|
|
8
|
+
- Focus on changed lines.
|
|
9
|
+
- Ignore style, refactoring suggestions, and general best practices.
|
|
10
|
+
- If uncertain, use tools: get_file_at_ref and grep_repository.
|
|
11
|
+
- Report only issues clearly visible in diff or verified by tools.
|
|
12
|
+
- If uncertain after checking, return exactly: "${NO_FINDINGS_SENTENCE}"
|
|
13
|
+
|
|
14
|
+
Severity:
|
|
15
|
+
- [high]: deterministic runtime/security breakage with clear path.
|
|
16
|
+
- [medium]: likely bug with strong evidence.
|
|
17
|
+
|
|
18
|
+
Output format (strict):
|
|
19
|
+
- \`- [high|medium] <title>\`
|
|
20
|
+
- \` File: <path>\`
|
|
21
|
+
- \` Line: ~<N>\`
|
|
22
|
+
- \` Why: <one concise sentence with evidence>\`
|
|
23
|
+
- If no issues: exactly "${NO_FINDINGS_SENTENCE}"`;
|
|
24
|
+
const MAIN_WEAK = `You are an AI bug-finder for a merge request.
|
|
25
|
+
Output bullets only — no preamble, no headings, no code fences, no closing remarks.
|
|
26
|
+
Return at most 3 findings.
|
|
27
|
+
|
|
28
|
+
Report a finding ONLY when ALL of these are true:
|
|
29
|
+
1. The bug is on a line added or modified in the diff (or a direct consequence of one).
|
|
30
|
+
2. You can quote the offending text from the diff or from get_file_at_ref output.
|
|
31
|
+
3. The bug causes wrong behavior, a crash, a security issue, or data loss — not a style nit.
|
|
32
|
+
If any of those is not true, do not include the finding.
|
|
33
|
+
|
|
34
|
+
When the diff is unclear, call the tools: get_file_at_ref(path, ref) or grep_repository(query, ref). Prefer the head ref.
|
|
35
|
+
|
|
36
|
+
Severity tags:
|
|
37
|
+
- [high] = will break at runtime or leak data.
|
|
38
|
+
- [medium] = strong evidence of a bug, but conditions matter.
|
|
39
|
+
|
|
40
|
+
Output format — copy spacing and case exactly, do not wrap in backticks or fences:
|
|
41
|
+
- [high|medium] <short title>
|
|
42
|
+
File: <path>
|
|
43
|
+
Line: ~<N>
|
|
44
|
+
Why: <one sentence pointing at the evidence>
|
|
45
|
+
|
|
46
|
+
Example of a valid finding:
|
|
47
|
+
- [high] Null pointer dereference in user lookup
|
|
48
|
+
File: src/user/service.ts
|
|
49
|
+
Line: ~42
|
|
50
|
+
Why: getUser() can return null but the next line calls user.id without a guard.
|
|
51
|
+
|
|
52
|
+
If you cannot meet all three conditions for any finding, output exactly this single line and nothing else: ${NO_FINDINGS_SENTENCE}`;
|
|
53
|
+
const FILE_DEFAULT = `You are an AI reviewer for a single-file diff.
|
|
54
|
+
Find only real bugs introduced by changed lines.
|
|
55
|
+
Return at most 2 findings. Prefer no finding over a weak one.
|
|
56
|
+
|
|
57
|
+
Rules:
|
|
58
|
+
- Focus on changed lines only.
|
|
59
|
+
- Ignore style/refactor/general improvement comments.
|
|
60
|
+
- If uncertain, use tools: get_file_at_ref and grep_repository.
|
|
61
|
+
- Report only issues clearly visible in diff or verified by tools.
|
|
62
|
+
- If uncertain after checking, return exactly: "${NO_FINDINGS_SENTENCE}"
|
|
63
|
+
|
|
64
|
+
Severity:
|
|
65
|
+
- [high]: deterministic runtime/security breakage with clear path.
|
|
66
|
+
- [medium]: likely bug with strong evidence.
|
|
67
|
+
|
|
68
|
+
Output format (strict):
|
|
69
|
+
- \`- [high|medium] <title>\`
|
|
70
|
+
- \` File: <path>\`
|
|
71
|
+
- \` Line: ~<N>\`
|
|
72
|
+
- \` Why: <one concise sentence with evidence>\`
|
|
73
|
+
- If no issues: exactly "${NO_FINDINGS_SENTENCE}"`;
|
|
74
|
+
const FILE_WEAK = `You are an AI bug-finder reviewing one file's diff.
|
|
75
|
+
Output bullets only — no preamble, no headings, no code fences.
|
|
76
|
+
Return at most 2 findings.
|
|
77
|
+
|
|
78
|
+
Report a finding ONLY when ALL of these are true:
|
|
79
|
+
1. The bug is on a line added or modified in this file's diff.
|
|
80
|
+
2. You can quote the offending text from the diff or from get_file_at_ref output.
|
|
81
|
+
3. The bug causes wrong behavior, a crash, a security issue, or data loss.
|
|
82
|
+
If any of those is not true, do not include the finding.
|
|
83
|
+
|
|
84
|
+
When the diff is unclear, call the tools: get_file_at_ref(path, ref) or grep_repository(query, ref). Prefer the head ref.
|
|
85
|
+
|
|
86
|
+
Severity tags:
|
|
87
|
+
- [high] = will break at runtime or leak data.
|
|
88
|
+
- [medium] = strong evidence of a bug, but conditions matter.
|
|
89
|
+
|
|
90
|
+
Output format — copy spacing and case exactly, do not wrap in backticks or fences:
|
|
91
|
+
- [high|medium] <short title>
|
|
92
|
+
File: <path>
|
|
93
|
+
Line: ~<N>
|
|
94
|
+
Why: <one sentence pointing at the evidence>
|
|
95
|
+
|
|
96
|
+
Example of a valid finding:
|
|
97
|
+
- [medium] Off-by-one in pagination loop
|
|
98
|
+
File: src/api/list.ts
|
|
99
|
+
Line: ~88
|
|
100
|
+
Why: loop runs while i <= total but indices are 0-based, so the last item is read past the array end.
|
|
101
|
+
|
|
102
|
+
If you cannot meet all three conditions for any finding, output exactly this single line and nothing else: ${NO_FINDINGS_SENTENCE}`;
|
|
103
|
+
export function getMainSystemPrompt(profile) {
|
|
104
|
+
return profile === "weak" ? MAIN_WEAK : MAIN_DEFAULT;
|
|
113
105
|
}
|
|
114
|
-
export function
|
|
115
|
-
return profile === "weak" ?
|
|
106
|
+
export function getFileReviewSystemPrompt(profile) {
|
|
107
|
+
return profile === "weak" ? FILE_WEAK : FILE_DEFAULT;
|
|
116
108
|
}
|
|
117
|
-
/** @deprecated kept for backward compatibility — use
|
|
118
|
-
export const MAIN_SYSTEM_LINES =
|
|
119
|
-
/** @deprecated kept for backward compatibility — use
|
|
120
|
-
export const FILE_REVIEW_SYSTEM_LINES =
|
|
109
|
+
/** @deprecated kept for backward compatibility — use getMainSystemPrompt("default"). */
|
|
110
|
+
export const MAIN_SYSTEM_LINES = MAIN_DEFAULT.split("\n");
|
|
111
|
+
/** @deprecated kept for backward compatibility — use getFileReviewSystemPrompt("default"). */
|
|
112
|
+
export const FILE_REVIEW_SYSTEM_LINES = FILE_DEFAULT.split("\n");
|
|
@@ -1,50 +1,46 @@
|
|
|
1
1
|
/** @format */
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
'{"summary":"Adds retry-with-backoff to the OpenAI client and updates the README badge.","files":[{"path":"src/openai/client.ts","verdict":"NEEDS_REVIEW","reason":"adds retry loop in API client"},{"path":"README.md","verdict":"SKIP","reason":"badge URL only"}]}',
|
|
45
|
-
];
|
|
46
|
-
export function getTriageSystemLines(profile) {
|
|
47
|
-
return profile === "weak" ? WEAK_LINES : DEFAULT_LINES;
|
|
2
|
+
const DEFAULT = `You are a senior developer triaging files in a merge request.
|
|
3
|
+
For each file, decide whether it NEEDS_REVIEW (modifies logic, functionality, security, or performance) or can be SKIPPED (cosmetic-only: formatting, comments, renaming for clarity, docs, auto-generated files).
|
|
4
|
+
|
|
5
|
+
Also produce a concise summary (2-4 sentences) of the entire merge request: what it does and which areas it touches.
|
|
6
|
+
|
|
7
|
+
Respond with a JSON object (no markdown fences, no prose before or after) in this exact schema:
|
|
8
|
+
{ "summary": "<MR summary>", "files": [{ "path": "<file path>", "verdict": "NEEDS_REVIEW" | "SKIP", "reason": "<one short sentence explaining the verdict>" }] }
|
|
9
|
+
|
|
10
|
+
The reason field is required for every file — cite what you saw in the diff (e.g. "import reordering only" or "changes Input props and validation").
|
|
11
|
+
|
|
12
|
+
Rules:
|
|
13
|
+
- When in doubt, verdict is NEEDS_REVIEW.
|
|
14
|
+
- Look beyond comments and JSDoc: if the diff changes function signatures, return types, control flow, variable assignments, or adds/removes code lines (not just comments), it is a logic change → NEEDS_REVIEW.
|
|
15
|
+
- If a file mixes doc/comment edits with actual code changes, verdict is NEEDS_REVIEW.
|
|
16
|
+
- Refactoring (splitting/merging functions, changing data structures, renaming with signature changes) is NEEDS_REVIEW.
|
|
17
|
+
- Deleted files are SKIP unless the deletion could break dependents.
|
|
18
|
+
- New files containing logic are NEEDS_REVIEW.
|
|
19
|
+
- Test files that add, remove, or change assertions or expected values are NEEDS_REVIEW.
|
|
20
|
+
- Test-only files are SKIP only if changes are purely cosmetic (formatting, comments).
|
|
21
|
+
- Config/CI/docs files are SKIP unless they modify build targets, env vars, or secrets.
|
|
22
|
+
|
|
23
|
+
Example output (copy this style exactly — single JSON object, no fences, no preamble):
|
|
24
|
+
{"summary":"Adds retry-with-backoff to the OpenAI client and updates the README badge.","files":[{"path":"src/openai/client.ts","verdict":"NEEDS_REVIEW","reason":"adds retry loop and error handling in API client"},{"path":"README.md","verdict":"SKIP","reason":"badge URL change only"}]}`;
|
|
25
|
+
const WEAK = `You triage merge-request files. Output one JSON object only — no prose, no markdown, no code fences, no preamble.
|
|
26
|
+
|
|
27
|
+
Schema (use exactly these keys):
|
|
28
|
+
{"summary":"<2-3 sentences about the MR>","files":[{"path":"<file path>","verdict":"NEEDS_REVIEW" | "SKIP","reason":"<one short sentence>"}]}
|
|
29
|
+
|
|
30
|
+
Mark a file NEEDS_REVIEW when ANY of these is true:
|
|
31
|
+
- the diff adds or removes non-comment lines,
|
|
32
|
+
- the diff changes a function signature, return type, control flow, or variable value,
|
|
33
|
+
- the file is new and contains code,
|
|
34
|
+
- a test changes an assertion or expected value,
|
|
35
|
+
- a config/CI file changes build targets, env vars, or secrets.
|
|
36
|
+
|
|
37
|
+
Mark a file SKIP only when the diff is purely cosmetic: whitespace, comments, JSDoc, plain docs, auto-generated files, or deletions that cannot affect callers.
|
|
38
|
+
When in doubt, choose NEEDS_REVIEW.
|
|
39
|
+
|
|
40
|
+
Example output (copy this exactly — one line, one JSON object, nothing else):
|
|
41
|
+
{"summary":"Adds retry-with-backoff to the OpenAI client and updates the README badge.","files":[{"path":"src/openai/client.ts","verdict":"NEEDS_REVIEW","reason":"adds retry loop in API client"},{"path":"README.md","verdict":"SKIP","reason":"badge URL only"}]}`;
|
|
42
|
+
export function getTriageSystemPrompt(profile) {
|
|
43
|
+
return profile === "weak" ? WEAK : DEFAULT;
|
|
48
44
|
}
|
|
49
|
-
/** @deprecated kept for backward compatibility — use
|
|
50
|
-
export const TRIAGE_SYSTEM_LINES =
|
|
45
|
+
/** @deprecated kept for backward compatibility — use getTriageSystemPrompt("default"). */
|
|
46
|
+
export const TRIAGE_SYSTEM_LINES = DEFAULT.split("\n");
|
|
@@ -2,15 +2,13 @@
|
|
|
2
2
|
import { truncateWithMarker } from "../utils.js";
|
|
3
3
|
export function buildMainReviewUserContent(params) {
|
|
4
4
|
const { stats, toolNote, changesText } = params;
|
|
5
|
-
return
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"Produce your review now. Follow the system instructions strictly.",
|
|
13
|
-
].join("\n");
|
|
5
|
+
return `Review the following code changes (git diff format). ${stats}
|
|
6
|
+
${toolNote}
|
|
7
|
+
|
|
8
|
+
Changes:
|
|
9
|
+
${changesText || "(no changes provided)"}
|
|
10
|
+
|
|
11
|
+
Produce your review now. Follow the system instructions strictly.`;
|
|
14
12
|
}
|
|
15
13
|
export function buildTriageUserContent(changes, maxDiffChars) {
|
|
16
14
|
const fileEntries = changes.map((c) => {
|
|
@@ -32,43 +30,37 @@ export function buildFileReviewUserContent(params) {
|
|
|
32
30
|
const otherFilesNote = otherChangedFiles.length > 0
|
|
33
31
|
? `\nOther files changed in this MR: ${otherChangedFiles.join(", ")}`
|
|
34
32
|
: "";
|
|
35
|
-
return
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"Review this file now.",
|
|
46
|
-
].join("\n");
|
|
33
|
+
return `MR Summary: ${summary}
|
|
34
|
+
${otherFilesNote}
|
|
35
|
+
|
|
36
|
+
${toolNote}
|
|
37
|
+
|
|
38
|
+
File: ${filePath}
|
|
39
|
+
Diff:
|
|
40
|
+
${fileDiff}
|
|
41
|
+
|
|
42
|
+
Review this file now.`;
|
|
47
43
|
}
|
|
48
44
|
export function buildConsolidateUserContent(params) {
|
|
49
45
|
const { summary, findingsText, maxFindings } = params;
|
|
50
|
-
return
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
`Return the top ${maxFindings} findings in the required bullet format.`,
|
|
57
|
-
].join("\n");
|
|
46
|
+
return `MR Summary: ${summary}
|
|
47
|
+
|
|
48
|
+
Per-file findings:
|
|
49
|
+
${findingsText}
|
|
50
|
+
|
|
51
|
+
Return the top ${maxFindings} findings in the required bullet format.`;
|
|
58
52
|
}
|
|
59
53
|
export function buildVerificationUserContent(params) {
|
|
60
54
|
const { summary, findingsText, consolidatedFindings, refs } = params;
|
|
61
|
-
return
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
"Return only the verified final findings.",
|
|
73
|
-
].join("\n");
|
|
55
|
+
return `MR Summary: ${summary}
|
|
56
|
+
|
|
57
|
+
Refs for tools: head (post-change)="${refs.head}", base="${refs.base}". Prefer head when checking whether the issue exists in the MR.
|
|
58
|
+
|
|
59
|
+
Per-file findings (evidence pool):
|
|
60
|
+
${findingsText}
|
|
61
|
+
|
|
62
|
+
Draft consolidated findings to verify:
|
|
63
|
+
${consolidatedFindings}
|
|
64
|
+
|
|
65
|
+
Return only the verified final findings.`;
|
|
74
66
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@krotovm/gitlab-ai-review",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.42",
|
|
5
5
|
"description": "CLI tool to generate AI code reviews for GitLab merge requests.",
|
|
6
6
|
"main": "dist/cli.js",
|
|
7
7
|
"bin": {
|
|
@@ -16,8 +16,7 @@
|
|
|
16
16
|
"LICENSE"
|
|
17
17
|
],
|
|
18
18
|
"scripts": {
|
|
19
|
-
"
|
|
20
|
-
"prepublishOnly": "pnpm build:ts"
|
|
19
|
+
"prepare": "tsc"
|
|
21
20
|
},
|
|
22
21
|
"keywords": [
|
|
23
22
|
"gitlab",
|