@krotovm/gitlab-ai-review 1.0.42 → 1.0.43
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 +2 -2
- package/dist/cli/ci-review.js +34 -23
- package/dist/cli/tooling.js +6 -0
- package/dist/cli.js +11 -12
- package/dist/gitlab/services.js +5 -68
- package/dist/gitlab/types.js +0 -2
- package/dist/prompt/index.js +0 -2
- package/dist/prompt/messages.js +0 -4
- package/dist/prompt/templates/review-system.js +0 -4
- package/dist/prompt/templates/triage-system.js +0 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -76,7 +76,7 @@ Set these in your project/group CI settings:
|
|
|
76
76
|
- `OPENAI_BASE_URL` (optional, for OpenAI-compatible providers/proxies)
|
|
77
77
|
- `AI_MODEL` (optional, default: `gpt-4o-mini`; example: `gpt-4o`)
|
|
78
78
|
- `AI_PROMPT_PROFILE` (optional, default: `default`; one of `default` \| `weak`). Use `weak` for small or heavily-quantized models (≤ ~7B, local Ollama, etc.). The `weak` profile uses shorter system prompts, positive rules instead of negations, and inline few-shot examples for triage / per-file review / consolidation / verification, which dramatically improves output-format adherence on weak models. When the variable is not set, the profile is auto-detected: if the triage pass returns unparseable JSON with `default` prompts, triage is retried once with `weak` and, on success, the whole run continues with it. Set the variable explicitly to pin a profile and disable auto-detection.
|
|
79
|
-
- `PROJECT_ACCESS_TOKEN` (
|
|
79
|
+
- `PROJECT_ACCESS_TOKEN` (required; project or personal access token with `api` scope — `CI_JOB_TOKEN` can't post MR comments or use the search API)
|
|
80
80
|
- `GITLAB_TOKEN` (optional alias for `PROJECT_ACCESS_TOKEN`)
|
|
81
81
|
- `AI_REVIEW_CONCURRENCY` (optional) — same as `--max-review-concurrency`, handy as a CI/CD variable; the flag wins when both are set.
|
|
82
82
|
- `AI_REVIEW_ARTIFACT_HTML_FILE` (optional, default: `ai-review-report.html`; used with `--include-artifacts`)
|
|
@@ -88,7 +88,7 @@ GitLab provides these automatically in Merge Request pipelines:
|
|
|
88
88
|
- `CI_API_V4_URL`
|
|
89
89
|
- `CI_PROJECT_ID`
|
|
90
90
|
- `CI_MERGE_REQUEST_IID`
|
|
91
|
-
- `
|
|
91
|
+
- `CI_MERGE_REQUEST_PROJECT_ID` (the MR's target project; used instead of `CI_PROJECT_ID`, so MRs from forks work)
|
|
92
92
|
|
|
93
93
|
## Flags
|
|
94
94
|
|
package/dist/cli/ci-review.js
CHANGED
|
@@ -3,7 +3,7 @@ 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
5
|
import { parseReviewFindings, truncateWithMarker } from "../prompt/utils.js";
|
|
6
|
-
import { logToolUsageMinimal, MAX_FILE_TOOL_ROUNDS, MAX_TOOL_ROUNDS, MAX_VERIFICATION_TOOL_ROUNDS, TOOL_NAME_GET_FILE, TOOL_NAME_GREP, } from "./tooling.js";
|
|
6
|
+
import { logToolFailure, 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. */
|
|
9
9
|
const COMPLETION_TIMEOUT_MS = 180_000;
|
|
@@ -14,6 +14,7 @@ let streamUsageSupported = true;
|
|
|
14
14
|
* round, so oversized results multiply prefill time on self-hosted models. */
|
|
15
15
|
const MAX_TOOL_FILE_CHARS = 12_000;
|
|
16
16
|
const MAX_TOOL_GREP_CHARS = 800;
|
|
17
|
+
const MAX_TOOL_GREP_RESULTS = 10;
|
|
17
18
|
function buildTriageDecisions(params) {
|
|
18
19
|
const { changes, triageResult } = params;
|
|
19
20
|
const triageMap = new Map(triageResult.files.map((f) => [f.path, f]));
|
|
@@ -149,7 +150,7 @@ function buildReviewMetadata(changes, refs) {
|
|
|
149
150
|
],
|
|
150
151
|
}, null, 2);
|
|
151
152
|
}
|
|
152
|
-
async function handleGetFileTool(argsRaw, gitLabProjectApiUrl, headers) {
|
|
153
|
+
async function handleGetFileTool(argsRaw, gitLabProjectApiUrl, headers, logStep) {
|
|
153
154
|
try {
|
|
154
155
|
const parsed = JSON.parse(argsRaw);
|
|
155
156
|
const path = parsed.path?.trim();
|
|
@@ -167,10 +168,12 @@ async function handleGetFileTool(argsRaw, gitLabProjectApiUrl, headers) {
|
|
|
167
168
|
ref,
|
|
168
169
|
});
|
|
169
170
|
if (fileText instanceof Error) {
|
|
171
|
+
logToolFailure(logStep, TOOL_NAME_GET_FILE, fileText);
|
|
170
172
|
return JSON.stringify({
|
|
171
173
|
ok: false,
|
|
172
174
|
path,
|
|
173
175
|
ref,
|
|
176
|
+
status: fileText.statusCode,
|
|
174
177
|
error: fileText.message,
|
|
175
178
|
});
|
|
176
179
|
}
|
|
@@ -183,46 +186,57 @@ async function handleGetFileTool(argsRaw, gitLabProjectApiUrl, headers) {
|
|
|
183
186
|
});
|
|
184
187
|
}
|
|
185
188
|
catch (error) {
|
|
189
|
+
logToolFailure(logStep, TOOL_NAME_GET_FILE, error);
|
|
186
190
|
return JSON.stringify({
|
|
187
191
|
ok: false,
|
|
188
|
-
error: `
|
|
192
|
+
error: `Tool call failed: ${String(error?.message ?? error)}`,
|
|
189
193
|
raw: argsRaw,
|
|
190
194
|
});
|
|
191
195
|
}
|
|
192
196
|
}
|
|
193
|
-
async function handleGrepTool(argsRaw,
|
|
197
|
+
async function handleGrepTool(argsRaw, refs, gitLabProjectApiUrl, headers, logStep) {
|
|
194
198
|
try {
|
|
195
199
|
const parsed = JSON.parse(argsRaw);
|
|
196
200
|
const query = parsed.query?.trim();
|
|
197
201
|
if (!query)
|
|
198
202
|
return JSON.stringify({ ok: false, error: "query is required." });
|
|
199
|
-
|
|
203
|
+
// GitLab search answers 200 [] for unknown refs, so only the MR's own refs are searched.
|
|
204
|
+
const ref = parsed.ref?.trim() === refs.base ? refs.base : refs.head;
|
|
200
205
|
const results = await searchRepository({
|
|
201
206
|
gitLabProjectApiUrl: gitLabProjectApiUrl,
|
|
202
207
|
headers,
|
|
203
208
|
query,
|
|
204
209
|
ref,
|
|
205
|
-
projectId,
|
|
206
210
|
});
|
|
207
211
|
if (results instanceof Error) {
|
|
212
|
+
logToolFailure(logStep, TOOL_NAME_GREP, results);
|
|
208
213
|
return JSON.stringify({
|
|
209
214
|
ok: false,
|
|
210
215
|
query,
|
|
211
216
|
ref,
|
|
217
|
+
status: results.statusCode,
|
|
212
218
|
error: results.message,
|
|
213
219
|
});
|
|
214
220
|
}
|
|
215
|
-
|
|
221
|
+
// GitLab lists file-name matches (file head, startline 1) before content matches;
|
|
222
|
+
// rank hits that contain the query first so they aren't crowded out.
|
|
223
|
+
const needle = query.toLowerCase();
|
|
224
|
+
const hasNeedle = (r) => (r.data ?? "").toLowerCase().includes(needle);
|
|
225
|
+
const trimmed = results
|
|
226
|
+
.sort((a, b) => Number(hasNeedle(b)) - Number(hasNeedle(a)))
|
|
227
|
+
.slice(0, MAX_TOOL_GREP_RESULTS)
|
|
228
|
+
.map((r) => ({
|
|
216
229
|
path: r.path,
|
|
217
230
|
startline: r.startline,
|
|
218
|
-
data: r.data.slice(0, MAX_TOOL_GREP_CHARS),
|
|
231
|
+
data: (r.data ?? "").slice(0, MAX_TOOL_GREP_CHARS),
|
|
219
232
|
}));
|
|
220
233
|
return JSON.stringify({ ok: true, query, ref, matches: trimmed });
|
|
221
234
|
}
|
|
222
235
|
catch (error) {
|
|
236
|
+
logToolFailure(logStep, TOOL_NAME_GREP, error);
|
|
223
237
|
return JSON.stringify({
|
|
224
238
|
ok: false,
|
|
225
|
-
error: `
|
|
239
|
+
error: `Tool call failed: ${String(error?.message ?? error)}`,
|
|
226
240
|
raw: argsRaw,
|
|
227
241
|
});
|
|
228
242
|
}
|
|
@@ -241,8 +255,8 @@ async function mapWithConcurrency(items, concurrency, fn) {
|
|
|
241
255
|
await Promise.all(workers);
|
|
242
256
|
return results;
|
|
243
257
|
}
|
|
244
|
-
|
|
245
|
-
const { openaiInstance, aiModel, promptLimits, changes, refs, gitLabProjectApiUrl,
|
|
258
|
+
async function reviewMergeRequestWithTools(params) {
|
|
259
|
+
const { openaiInstance, aiModel, promptLimits, changes, refs, gitLabProjectApiUrl, headers, forceTools, promptProfile = DEFAULT_PROMPT_PROFILE, loggers, debugDumpFile, debugRecordWriter, } = params;
|
|
246
260
|
const { logDebug, logStep } = loggers;
|
|
247
261
|
logStep(`Single-pass review started for ${changes.length} file(s) (fallback mode).`);
|
|
248
262
|
const messages = buildPrompt({
|
|
@@ -343,10 +357,10 @@ export async function reviewMergeRequestWithTools(params) {
|
|
|
343
357
|
logToolUsageMinimal(logStep, toolName, argsRaw);
|
|
344
358
|
let toolContent;
|
|
345
359
|
if (toolName === TOOL_NAME_GET_FILE) {
|
|
346
|
-
toolContent = await handleGetFileTool(argsRaw, gitLabProjectApiUrl, headers);
|
|
360
|
+
toolContent = await handleGetFileTool(argsRaw, gitLabProjectApiUrl, headers, logStep);
|
|
347
361
|
}
|
|
348
362
|
else if (toolName === TOOL_NAME_GREP) {
|
|
349
|
-
toolContent = await handleGrepTool(argsRaw, refs
|
|
363
|
+
toolContent = await handleGrepTool(argsRaw, refs, gitLabProjectApiUrl, headers, logStep);
|
|
350
364
|
}
|
|
351
365
|
else {
|
|
352
366
|
toolContent = JSON.stringify({
|
|
@@ -390,7 +404,7 @@ export async function reviewMergeRequestWithTools(params) {
|
|
|
390
404
|
return buildAnswer(finalCompletion);
|
|
391
405
|
}
|
|
392
406
|
async function runFileReviewWithTools(params) {
|
|
393
|
-
const { openaiInstance, aiModel, filePath, fileDiff, summary, otherChangedFiles, refs, gitLabProjectApiUrl,
|
|
407
|
+
const { openaiInstance, aiModel, filePath, fileDiff, summary, otherChangedFiles, refs, gitLabProjectApiUrl, headers, forceTools, promptProfile, loggers, debugDumpFile, debugRecordWriter, } = params;
|
|
394
408
|
const { logDebug, logStep } = loggers;
|
|
395
409
|
const messages = buildFileReviewPrompt({
|
|
396
410
|
filePath,
|
|
@@ -487,10 +501,10 @@ async function runFileReviewWithTools(params) {
|
|
|
487
501
|
logToolUsageMinimal(logStep, toolName, argsRaw, filePath);
|
|
488
502
|
let toolContent;
|
|
489
503
|
if (toolName === TOOL_NAME_GET_FILE) {
|
|
490
|
-
toolContent = await handleGetFileTool(argsRaw, gitLabProjectApiUrl, headers);
|
|
504
|
+
toolContent = await handleGetFileTool(argsRaw, gitLabProjectApiUrl, headers, logStep);
|
|
491
505
|
}
|
|
492
506
|
else if (toolName === TOOL_NAME_GREP) {
|
|
493
|
-
toolContent = await handleGrepTool(argsRaw, refs
|
|
507
|
+
toolContent = await handleGrepTool(argsRaw, refs, gitLabProjectApiUrl, headers, logStep);
|
|
494
508
|
}
|
|
495
509
|
else {
|
|
496
510
|
toolContent = JSON.stringify({
|
|
@@ -537,7 +551,7 @@ function draftHasStructuredFindings(consolidatedText) {
|
|
|
537
551
|
return /-\s*\[(?:high|medium)\]/i.test(consolidatedText);
|
|
538
552
|
}
|
|
539
553
|
async function runVerificationWithTools(params) {
|
|
540
|
-
const { openaiInstance, aiModel, baseMessages, refs, gitLabProjectApiUrl,
|
|
554
|
+
const { openaiInstance, aiModel, baseMessages, refs, gitLabProjectApiUrl, headers, forceTools, consolidatedDraft, loggers, debugDumpFile, debugRecordWriter, } = params;
|
|
541
555
|
const { logDebug, logStep } = loggers;
|
|
542
556
|
const messages = [...baseMessages];
|
|
543
557
|
const tools = [
|
|
@@ -627,10 +641,10 @@ async function runVerificationWithTools(params) {
|
|
|
627
641
|
logToolUsageMinimal(logStep, toolName, argsRaw, "(verify)");
|
|
628
642
|
let toolContent;
|
|
629
643
|
if (toolName === TOOL_NAME_GET_FILE) {
|
|
630
|
-
toolContent = await handleGetFileTool(argsRaw, gitLabProjectApiUrl, headers);
|
|
644
|
+
toolContent = await handleGetFileTool(argsRaw, gitLabProjectApiUrl, headers, logStep);
|
|
631
645
|
}
|
|
632
646
|
else if (toolName === TOOL_NAME_GREP) {
|
|
633
|
-
toolContent = await handleGrepTool(argsRaw, refs
|
|
647
|
+
toolContent = await handleGrepTool(argsRaw, refs, gitLabProjectApiUrl, headers, logStep);
|
|
634
648
|
}
|
|
635
649
|
else {
|
|
636
650
|
toolContent = JSON.stringify({
|
|
@@ -672,7 +686,7 @@ async function runVerificationWithTools(params) {
|
|
|
672
686
|
});
|
|
673
687
|
}
|
|
674
688
|
export async function reviewMergeRequestMultiPass(params) {
|
|
675
|
-
const { openaiInstance, aiModel, promptLimits, triageDiffChars, changes, refs, gitLabProjectApiUrl,
|
|
689
|
+
const { openaiInstance, aiModel, promptLimits, triageDiffChars, changes, refs, gitLabProjectApiUrl, headers, maxFindings, reviewConcurrency, forceTools, promptProfile = DEFAULT_PROMPT_PROFILE, allowProfileAutoDemote = false, loggers, debugDumpFile, debugRecordWriter, } = params;
|
|
676
690
|
const { logStep } = loggers;
|
|
677
691
|
let effectiveProfile = promptProfile;
|
|
678
692
|
logStep(`Pass 1/4: triaging ${changes.length} file(s) ` +
|
|
@@ -773,7 +787,6 @@ export async function reviewMergeRequestMultiPass(params) {
|
|
|
773
787
|
changes,
|
|
774
788
|
refs,
|
|
775
789
|
gitLabProjectApiUrl,
|
|
776
|
-
projectId,
|
|
777
790
|
headers,
|
|
778
791
|
forceTools,
|
|
779
792
|
promptProfile: effectiveProfile,
|
|
@@ -821,7 +834,6 @@ export async function reviewMergeRequestMultiPass(params) {
|
|
|
821
834
|
otherChangedFiles: otherFiles,
|
|
822
835
|
refs,
|
|
823
836
|
gitLabProjectApiUrl,
|
|
824
|
-
projectId,
|
|
825
837
|
headers,
|
|
826
838
|
forceTools,
|
|
827
839
|
promptProfile: effectiveProfile,
|
|
@@ -909,7 +921,6 @@ export async function reviewMergeRequestMultiPass(params) {
|
|
|
909
921
|
baseMessages: verificationMessages,
|
|
910
922
|
refs,
|
|
911
923
|
gitLabProjectApiUrl,
|
|
912
|
-
projectId,
|
|
913
924
|
headers,
|
|
914
925
|
forceTools,
|
|
915
926
|
consolidatedDraft: consolidatedText,
|
package/dist/cli/tooling.js
CHANGED
|
@@ -27,3 +27,9 @@ export function logToolUsageMinimal(logStep, toolName, argsRaw, contextFile) {
|
|
|
27
27
|
const suffix = contextFile ? ` review_file=${contextFile}` : "";
|
|
28
28
|
logStep(`[tools] ${toolName} args=${argsRaw.slice(0, 120)}${suffix}`);
|
|
29
29
|
}
|
|
30
|
+
/** Operator-facing line for a failed tool call: GitLab status and request URL when known. */
|
|
31
|
+
export function logToolFailure(logStep, toolName, error) {
|
|
32
|
+
const status = error?.statusCode != null ? ` status=${error.statusCode}` : "";
|
|
33
|
+
const url = typeof error?.cause?.url === "string" ? ` url=${error.cause.url}` : "";
|
|
34
|
+
logStep(`[tools] ${toolName} failed: ${String(error?.message ?? error)}${status}${url}`);
|
|
35
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -38,10 +38,10 @@ Env vars:
|
|
|
38
38
|
positive rules, and few-shot examples.
|
|
39
39
|
When unset, an unparseable triage response auto-retries
|
|
40
40
|
with "weak" and keeps it for the whole run.
|
|
41
|
-
PROJECT_ACCESS_TOKEN (
|
|
41
|
+
PROJECT_ACCESS_TOKEN (required) GitLab project/personal access token with api scope (alias: GITLAB_TOKEN). CI_JOB_TOKEN can't post MR comments.
|
|
42
42
|
|
|
43
43
|
CI-only env vars (provided by GitLab):
|
|
44
|
-
CI_API_V4_URL, CI_PROJECT_ID,
|
|
44
|
+
CI_API_V4_URL, CI_PROJECT_ID, CI_MERGE_REQUEST_PROJECT_ID, CI_MERGE_REQUEST_IID
|
|
45
45
|
`);
|
|
46
46
|
}
|
|
47
47
|
const DEBUG_MODE = hasDebugFlag(process.argv);
|
|
@@ -107,25 +107,24 @@ async function main() {
|
|
|
107
107
|
"CI_PROJECT_ID",
|
|
108
108
|
"CI_MERGE_REQUEST_IID",
|
|
109
109
|
];
|
|
110
|
+
// CI_JOB_TOKEN can't post MR notes or call the search API, so a real token is required.
|
|
110
111
|
if (projectAccessToken == null)
|
|
111
|
-
gitlabRequired.push("
|
|
112
|
+
gitlabRequired.push("PROJECT_ACCESS_TOKEN");
|
|
112
113
|
const envs = requireEnvs(gitlabRequired);
|
|
113
114
|
const openaiApiKey = envs["OPENAI_API_KEY"];
|
|
114
115
|
const ciApiV4Url = envs["CI_API_V4_URL"];
|
|
115
|
-
|
|
116
|
+
// MR endpoints live in the MR's target project; in fork pipelines CI_PROJECT_ID is the fork.
|
|
117
|
+
const projectId = envOrUndefined("CI_MERGE_REQUEST_PROJECT_ID") ?? envs["CI_PROJECT_ID"];
|
|
116
118
|
const mergeRequestIid = envs["CI_MERGE_REQUEST_IID"];
|
|
117
119
|
const gitLabProjectApiUrl = new URL(`${ciApiV4Url}/projects/${projectId}`);
|
|
118
|
-
const headers = {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
else
|
|
122
|
-
headers["JOB-TOKEN"] = envs["CI_JOB_TOKEN"];
|
|
120
|
+
const headers = {
|
|
121
|
+
"PRIVATE-TOKEN": projectAccessToken,
|
|
122
|
+
};
|
|
123
123
|
try {
|
|
124
124
|
logStep("Fetching merge request changes");
|
|
125
125
|
const mrChanges = await fetchMergeRequestChanges({
|
|
126
126
|
gitLabProjectApiUrl: gitLabProjectApiUrl,
|
|
127
127
|
headers,
|
|
128
|
-
projectId,
|
|
129
128
|
mergeRequestIid,
|
|
130
129
|
});
|
|
131
130
|
if (mrChanges instanceof Error)
|
|
@@ -149,7 +148,6 @@ async function main() {
|
|
|
149
148
|
head: mrChanges.diff_refs?.head_sha ?? "HEAD",
|
|
150
149
|
},
|
|
151
150
|
gitLabProjectApiUrl: gitLabProjectApiUrl,
|
|
152
|
-
projectId,
|
|
153
151
|
headers,
|
|
154
152
|
maxFindings,
|
|
155
153
|
reviewConcurrency,
|
|
@@ -164,7 +162,8 @@ async function main() {
|
|
|
164
162
|
gitLabProjectApiUrl: gitLabProjectApiUrl,
|
|
165
163
|
headers,
|
|
166
164
|
mergeRequestIid,
|
|
167
|
-
|
|
165
|
+
note: answer,
|
|
166
|
+
});
|
|
168
167
|
if (noteRes instanceof Error)
|
|
169
168
|
throw noteRes;
|
|
170
169
|
process.stdout.write("Posted AI review comment to merge request.\n");
|
package/dist/gitlab/services.js
CHANGED
|
@@ -1,47 +1,5 @@
|
|
|
1
1
|
/** @format */
|
|
2
|
-
import
|
|
3
|
-
import { AI_MODEL_TEMPERATURE } from "../prompt/index.js";
|
|
4
|
-
import { GitLabError, OpenAIError, } from "./types.js";
|
|
5
|
-
export const fetchPreEditFiles = async ({ gitLabProjectApiUrl, headers, changesOldPaths, ref }) => {
|
|
6
|
-
const oldFilesRequestUrls = changesOldPaths.map((filePath) => {
|
|
7
|
-
const url = new URL(`${gitLabProjectApiUrl}/repository/files/${encodeURIComponent(filePath)}/raw`);
|
|
8
|
-
url.searchParams.set("ref", ref);
|
|
9
|
-
return url;
|
|
10
|
-
});
|
|
11
|
-
let oldFiles;
|
|
12
|
-
try {
|
|
13
|
-
oldFiles = await Promise.allSettled(oldFilesRequestUrls.map(async (url) => {
|
|
14
|
-
const res = await fetch(url, { headers: { ...headers } });
|
|
15
|
-
if (!res.ok) {
|
|
16
|
-
const bodyText = await res.text().catch(() => "");
|
|
17
|
-
throw new Error(`Failed to fetch old file: ${url.toString()} (status ${res.status} ${res.statusText}, body: ${bodyText.slice(0, 500)})`);
|
|
18
|
-
}
|
|
19
|
-
return await res.text();
|
|
20
|
-
}));
|
|
21
|
-
}
|
|
22
|
-
catch (error) {
|
|
23
|
-
oldFiles = error;
|
|
24
|
-
}
|
|
25
|
-
if (oldFiles instanceof Error) {
|
|
26
|
-
return new GitLabError({
|
|
27
|
-
name: "MISSING_OLD_FILES",
|
|
28
|
-
message: "Failed to fetch old files",
|
|
29
|
-
cause: {
|
|
30
|
-
message: oldFiles.message,
|
|
31
|
-
stack: oldFiles.stack,
|
|
32
|
-
},
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
return oldFiles.reduce((acc, file, index) => {
|
|
36
|
-
if (file.status === "fulfilled") {
|
|
37
|
-
acc.push({
|
|
38
|
-
fileName: changesOldPaths[index],
|
|
39
|
-
fileContent: file.value,
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
return acc;
|
|
43
|
-
}, []);
|
|
44
|
-
};
|
|
2
|
+
import { GitLabError, } from "./types.js";
|
|
45
3
|
export const fetchFileAtRef = async ({ gitLabProjectApiUrl, headers, filePath, ref }) => {
|
|
46
4
|
const url = new URL(`${gitLabProjectApiUrl}/repository/files/${encodeURIComponent(filePath)}/raw`);
|
|
47
5
|
url.searchParams.set("ref", ref);
|
|
@@ -81,29 +39,7 @@ export const fetchFileAtRef = async ({ gitLabProjectApiUrl, headers, filePath, r
|
|
|
81
39
|
}
|
|
82
40
|
return await res.text();
|
|
83
41
|
};
|
|
84
|
-
export async
|
|
85
|
-
let completion;
|
|
86
|
-
try {
|
|
87
|
-
completion = await openaiInstance.chat.completions.create({
|
|
88
|
-
model: aiModel,
|
|
89
|
-
temperature: AI_MODEL_TEMPERATURE,
|
|
90
|
-
stream: false,
|
|
91
|
-
messages,
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
catch (error) {
|
|
95
|
-
completion = error;
|
|
96
|
-
}
|
|
97
|
-
if (completion instanceof Error) {
|
|
98
|
-
return new OpenAIError({
|
|
99
|
-
name: "MISSING_AI_COMPLETION",
|
|
100
|
-
message: "Failed to generate AI completion",
|
|
101
|
-
cause: completion,
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
return completion;
|
|
105
|
-
}
|
|
106
|
-
export const postMergeRequestNote = async ({ gitLabProjectApiUrl, headers, mergeRequestIid }, commentPayload) => {
|
|
42
|
+
export const postMergeRequestNote = async ({ gitLabProjectApiUrl, headers, mergeRequestIid, note }) => {
|
|
107
43
|
const commentUrl = new URL(`${gitLabProjectApiUrl}/merge_requests/${mergeRequestIid}/notes`);
|
|
108
44
|
let aiComment;
|
|
109
45
|
try {
|
|
@@ -113,7 +49,7 @@ export const postMergeRequestNote = async ({ gitLabProjectApiUrl, headers, merge
|
|
|
113
49
|
...headers,
|
|
114
50
|
"Content-Type": "application/json",
|
|
115
51
|
},
|
|
116
|
-
body: JSON.stringify(
|
|
52
|
+
body: JSON.stringify({ body: note }),
|
|
117
53
|
});
|
|
118
54
|
}
|
|
119
55
|
catch (error) {
|
|
@@ -152,7 +88,8 @@ export const searchRepository = async ({ gitLabProjectApiUrl, headers, query, re
|
|
|
152
88
|
url.searchParams.set("scope", "blobs");
|
|
153
89
|
url.searchParams.set("search", query);
|
|
154
90
|
url.searchParams.set("ref", ref);
|
|
155
|
-
|
|
91
|
+
// Name matches come before content matches; the grep tool re-ranks and keeps 10.
|
|
92
|
+
url.searchParams.set("per_page", "50");
|
|
156
93
|
let res;
|
|
157
94
|
try {
|
|
158
95
|
res = await fetch(url, { headers: { ...headers } });
|
package/dist/gitlab/types.js
CHANGED
package/dist/prompt/index.js
CHANGED
|
@@ -12,8 +12,6 @@ export const DEFAULT_PROMPT_LIMITS = {
|
|
|
12
12
|
};
|
|
13
13
|
/** Max chars of each file diff sent to the triage pass (Pass 1). */
|
|
14
14
|
export const DEFAULT_TRIAGE_DIFF_CHARS = 2000;
|
|
15
|
-
export const AI_MODEL_TEMPERATURE = 0.2;
|
|
16
|
-
export const AI_MAX_OUTPUT_TOKENS = 600;
|
|
17
15
|
export const buildPrompt = ({ changes, limits, allowTools = false, profile = DEFAULT_PROMPT_PROFILE, }) => {
|
|
18
16
|
const effectiveLimits = {
|
|
19
17
|
...DEFAULT_PROMPT_LIMITS,
|
package/dist/prompt/messages.js
CHANGED
|
@@ -16,7 +16,3 @@ export const buildFileReviewSystemMessage = (profile = DEFAULT_PROMPT_PROFILE) =
|
|
|
16
16
|
role: "system",
|
|
17
17
|
content: getFileReviewSystemPrompt(profile),
|
|
18
18
|
});
|
|
19
|
-
/** @deprecated kept for backward compatibility — use buildTriageSystemMessage(). */
|
|
20
|
-
export const TRIAGE_SYSTEM = buildTriageSystemMessage();
|
|
21
|
-
/** @deprecated kept for backward compatibility — use buildFileReviewSystemMessage(). */
|
|
22
|
-
export const FILE_REVIEW_SYSTEM = buildFileReviewSystemMessage();
|
|
@@ -106,7 +106,3 @@ export function getMainSystemPrompt(profile) {
|
|
|
106
106
|
export function getFileReviewSystemPrompt(profile) {
|
|
107
107
|
return profile === "weak" ? FILE_WEAK : FILE_DEFAULT;
|
|
108
108
|
}
|
|
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");
|
|
@@ -42,5 +42,3 @@ Example output (copy this exactly — one line, one JSON object, nothing else):
|
|
|
42
42
|
export function getTriageSystemPrompt(profile) {
|
|
43
43
|
return profile === "weak" ? WEAK : DEFAULT;
|
|
44
44
|
}
|
|
45
|
-
/** @deprecated kept for backward compatibility — use getTriageSystemPrompt("default"). */
|
|
46
|
-
export const TRIAGE_SYSTEM_LINES = DEFAULT.split("\n");
|