@krotovm/gitlab-ai-review 1.0.33 → 1.0.35
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 +3 -2
- package/dist/cli/args.js +4 -1
- package/dist/cli/ci-review.js +107 -26
- package/dist/cli/debug-artifacts-html.js +58 -1
- package/dist/cli.js +4 -1
- package/dist/prompt/index.js +7 -2
- package/dist/prompt/templates/triage-system.js +6 -4
- package/dist/prompt/templates/user-prompts.js +4 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -75,6 +75,7 @@ GitLab provides these automatically in Merge Request pipelines:
|
|
|
75
75
|
- `--max-diffs=50` - Max number of diffs included in the prompt.
|
|
76
76
|
- `--max-diff-chars=16000` - Max chars per diff chunk (single-pass fallback only).
|
|
77
77
|
- `--max-total-prompt-chars=220000` - Final hard cap for prompt size (single-pass fallback only).
|
|
78
|
+
- `--triage-diff-chars=2000` - Max chars per file diff sent to the triage pass (Pass 1). Increase for large diffs where the first hunks are mostly git headers.
|
|
78
79
|
- `--max-findings=5` - Max findings in the final review (CI multi-pass only).
|
|
79
80
|
- `--max-review-concurrency=5` - Parallel per-file review API calls (CI multi-pass only).
|
|
80
81
|
- `--debug` - Print full error details (stack and API error fields).
|
|
@@ -85,8 +86,8 @@ GitLab provides these automatically in Merge Request pipelines:
|
|
|
85
86
|
|
|
86
87
|
The reviewer uses a three-pass pipeline optimized for large merge requests:
|
|
87
88
|
|
|
88
|
-
1. **Triage** - A fast LLM pass classifies each changed file as `NEEDS_REVIEW` or `SKIP` and generates a short MR summary.
|
|
89
|
-
2. **Per-file review** - Only `NEEDS_REVIEW` files are reviewed
|
|
89
|
+
1. **Triage** - A fast LLM pass classifies each changed file as `NEEDS_REVIEW` or `SKIP`, with a per-file `reason`, and generates a short MR summary. Each file diff is truncated to `--triage-diff-chars` (default 2000).
|
|
90
|
+
2. **Per-file review** - Only `NEEDS_REVIEW` files are reviewed (if triage marks every file `SKIP`, all files are reviewed anyway). Each reviewed file gets a dedicated LLM call running in parallel (with tools to fetch full files or grep the repository).
|
|
90
91
|
3. **Consolidate** - Per-file findings are merged, deduplicated, ranked by severity, and trimmed to top N (default 5).
|
|
91
92
|
|
|
92
93
|
If the triage pass fails (API error, unparseable response), the pipeline falls back to the original single-pass approach automatically.
|
package/dist/cli/args.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** @format */
|
|
2
|
-
import { DEFAULT_PROMPT_LIMITS, parsePromptProfile as parsePromptProfileValue, } from "../prompt/index.js";
|
|
2
|
+
import { DEFAULT_PROMPT_LIMITS, DEFAULT_TRIAGE_DIFF_CHARS, parsePromptProfile as parsePromptProfileValue, } from "../prompt/index.js";
|
|
3
3
|
export function requireEnvs(names) {
|
|
4
4
|
const missing = names.filter((name) => {
|
|
5
5
|
const value = process.env[name];
|
|
@@ -87,6 +87,9 @@ export function parsePromptLimits(argv) {
|
|
|
87
87
|
maxTotalPromptChars: parseNumberFlag(argv, "max-total-prompt-chars", DEFAULT_PROMPT_LIMITS.maxTotalPromptChars, 1),
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
|
+
export function parseTriageDiffChars(argv) {
|
|
91
|
+
return parseNumberFlag(argv, "triage-diff-chars", DEFAULT_TRIAGE_DIFF_CHARS, 1);
|
|
92
|
+
}
|
|
90
93
|
export function hasIgnoredExtension(filePath, ignoredExtensions) {
|
|
91
94
|
const lowerPath = filePath.toLowerCase();
|
|
92
95
|
return ignoredExtensions.some((ext) => lowerPath.endsWith(ext));
|
package/dist/cli/ci-review.js
CHANGED
|
@@ -3,6 +3,51 @@ 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 { logToolUsageMinimal, MAX_FILE_TOOL_ROUNDS, MAX_TOOL_ROUNDS, MAX_VERIFICATION_TOOL_ROUNDS, TOOL_NAME_GET_FILE, TOOL_NAME_GREP, } from "./tooling.js";
|
|
6
|
+
function buildTriageDecisions(params) {
|
|
7
|
+
const { changes, triageResult } = params;
|
|
8
|
+
const triageMap = new Map(triageResult.files.map((f) => [f.path, f]));
|
|
9
|
+
let reviewFiles = changes.filter((c) => triageMap.get(c.new_path)?.verdict !== "SKIP");
|
|
10
|
+
const skipAllOverride = reviewFiles.length === 0;
|
|
11
|
+
if (skipAllOverride)
|
|
12
|
+
reviewFiles = changes;
|
|
13
|
+
const decisions = changes.map((change) => {
|
|
14
|
+
const path = change.new_path;
|
|
15
|
+
const triageFile = triageMap.get(path);
|
|
16
|
+
const verdict = triageFile?.verdict ?? "NEEDS_REVIEW";
|
|
17
|
+
const reason = triageFile?.reason ??
|
|
18
|
+
(triageFile == null ? "not listed in triage response" : undefined);
|
|
19
|
+
let review_action;
|
|
20
|
+
if (verdict === "NEEDS_REVIEW" || triageFile == null) {
|
|
21
|
+
review_action = "review";
|
|
22
|
+
}
|
|
23
|
+
else if (skipAllOverride) {
|
|
24
|
+
review_action = "reviewed_via_override";
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
review_action = "skipped";
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
path,
|
|
31
|
+
verdict,
|
|
32
|
+
...(reason != null ? { reason } : {}),
|
|
33
|
+
review_action,
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
return { decisions, skipAllOverride, reviewFiles };
|
|
37
|
+
}
|
|
38
|
+
function formatTriageDecisionLine(decision) {
|
|
39
|
+
const reasonSuffix = decision.reason != null && decision.reason !== ""
|
|
40
|
+
? ` — ${decision.reason}`
|
|
41
|
+
: "";
|
|
42
|
+
switch (decision.review_action) {
|
|
43
|
+
case "skipped":
|
|
44
|
+
return ` Skipped: ${decision.path} (${decision.verdict})${reasonSuffix}`;
|
|
45
|
+
case "reviewed_via_override":
|
|
46
|
+
return ` Reviewed via override: ${decision.path} (triage=${decision.verdict})${reasonSuffix}`;
|
|
47
|
+
default:
|
|
48
|
+
return ` Review: ${decision.path} (${decision.verdict})${reasonSuffix}`;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
6
51
|
async function appendDebugDump(_debugDumpFile, debugRecordWriter, record) {
|
|
7
52
|
const withTs = { ts: new Date().toISOString(), ...record };
|
|
8
53
|
if (debugRecordWriter != null) {
|
|
@@ -16,8 +61,13 @@ async function createCompletionWithDebug(params) {
|
|
|
16
61
|
label: requestLabel,
|
|
17
62
|
request,
|
|
18
63
|
});
|
|
64
|
+
// Stream and reassemble: reverse proxies (nginx proxy_read_timeout) return 504 on
|
|
65
|
+
// long silent non-streamed completions; streamed chunks keep the connection alive.
|
|
66
|
+
const { stream: _stream, ...createParams } = request;
|
|
19
67
|
try {
|
|
20
|
-
const completion = await openaiInstance.chat.completions
|
|
68
|
+
const completion = await openaiInstance.chat.completions
|
|
69
|
+
.stream(createParams)
|
|
70
|
+
.finalChatCompletion();
|
|
21
71
|
await appendDebugDump(debugDumpFile, debugRecordWriter, {
|
|
22
72
|
kind: "openai_response",
|
|
23
73
|
label: requestLabel,
|
|
@@ -592,9 +642,10 @@ async function runVerificationWithTools(params) {
|
|
|
592
642
|
});
|
|
593
643
|
}
|
|
594
644
|
export async function reviewMergeRequestMultiPass(params) {
|
|
595
|
-
const { openaiInstance, aiModel, promptLimits, changes, refs, gitLabProjectApiUrl, projectId, headers, maxFindings, reviewConcurrency, forceTools, promptProfile = DEFAULT_PROMPT_PROFILE, loggers, debugDumpFile, debugRecordWriter, } = params;
|
|
645
|
+
const { openaiInstance, aiModel, promptLimits, triageDiffChars, changes, refs, gitLabProjectApiUrl, projectId, headers, maxFindings, reviewConcurrency, forceTools, promptProfile = DEFAULT_PROMPT_PROFILE, loggers, debugDumpFile, debugRecordWriter, } = params;
|
|
596
646
|
const { logStep } = loggers;
|
|
597
|
-
logStep(`Pass 1/4: triaging ${changes.length} file(s)
|
|
647
|
+
logStep(`Pass 1/4: triaging ${changes.length} file(s) ` +
|
|
648
|
+
`(prompt profile=${promptProfile}, triage_diff_chars=${triageDiffChars})`);
|
|
598
649
|
const triageInputs = changes.map((c) => ({
|
|
599
650
|
path: c.new_path,
|
|
600
651
|
new_file: c.new_file,
|
|
@@ -602,7 +653,7 @@ export async function reviewMergeRequestMultiPass(params) {
|
|
|
602
653
|
renamed_file: c.renamed_file,
|
|
603
654
|
diff: c.diff,
|
|
604
655
|
}));
|
|
605
|
-
const triageMessages = buildTriagePrompt(triageInputs, promptProfile);
|
|
656
|
+
const triageMessages = buildTriagePrompt(triageInputs, promptProfile, triageDiffChars);
|
|
606
657
|
let triageResult = null;
|
|
607
658
|
let triageText = null;
|
|
608
659
|
let triageParseReason = null;
|
|
@@ -662,39 +713,69 @@ export async function reviewMergeRequestMultiPass(params) {
|
|
|
662
713
|
debugRecordWriter,
|
|
663
714
|
});
|
|
664
715
|
}
|
|
665
|
-
const
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
716
|
+
const { decisions, skipAllOverride, reviewFiles } = buildTriageDecisions({
|
|
717
|
+
changes,
|
|
718
|
+
triageResult,
|
|
719
|
+
});
|
|
720
|
+
const skippedCount = decisions.filter((d) => d.review_action === "skipped").length;
|
|
721
|
+
await appendDebugDump(debugDumpFile, debugRecordWriter, {
|
|
722
|
+
kind: "triage_decision",
|
|
723
|
+
summary: triageResult.summary,
|
|
724
|
+
skip_all_override: skipAllOverride,
|
|
725
|
+
triage_diff_chars: triageDiffChars,
|
|
726
|
+
files: decisions,
|
|
727
|
+
});
|
|
728
|
+
if (skipAllOverride) {
|
|
669
729
|
logStep(`Triage wanted to skip all ${changes.length} file(s) — overriding to review all. Summary: ${triageResult.summary.slice(0, 120)}...`);
|
|
670
|
-
reviewFiles = changes;
|
|
671
730
|
}
|
|
672
731
|
else {
|
|
673
732
|
logStep(`Triage: ${reviewFiles.length} file(s) to review, ${skippedCount} skipped. Summary: ${triageResult.summary.slice(0, 120)}...`);
|
|
674
733
|
}
|
|
734
|
+
for (const decision of decisions) {
|
|
735
|
+
if (decision.review_action === "review")
|
|
736
|
+
continue;
|
|
737
|
+
logStep(formatTriageDecisionLine(decision));
|
|
738
|
+
}
|
|
675
739
|
logStep(`Pass 2/4: reviewing ${reviewFiles.length} file(s) (concurrency=${reviewConcurrency})`);
|
|
676
740
|
const allChangedPaths = changes.map((c) => c.new_path);
|
|
741
|
+
const failedPaths = [];
|
|
677
742
|
const perFileFindings = await mapWithConcurrency(reviewFiles, reviewConcurrency, async (change) => {
|
|
678
743
|
const otherFiles = allChangedPaths.filter((p) => p !== change.new_path);
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
744
|
+
let findings;
|
|
745
|
+
try {
|
|
746
|
+
findings = await runFileReviewWithTools({
|
|
747
|
+
openaiInstance,
|
|
748
|
+
aiModel,
|
|
749
|
+
filePath: change.new_path,
|
|
750
|
+
fileDiff: change.diff,
|
|
751
|
+
summary: triageResult.summary,
|
|
752
|
+
otherChangedFiles: otherFiles,
|
|
753
|
+
refs,
|
|
754
|
+
gitLabProjectApiUrl,
|
|
755
|
+
projectId,
|
|
756
|
+
headers,
|
|
757
|
+
forceTools,
|
|
758
|
+
promptProfile,
|
|
759
|
+
loggers,
|
|
760
|
+
debugDumpFile,
|
|
761
|
+
debugRecordWriter,
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
catch (error) {
|
|
765
|
+
const preview = String(error?.message ?? error)
|
|
766
|
+
.replace(/\s+/g, " ")
|
|
767
|
+
.slice(0, 200);
|
|
768
|
+
logStep(`Pass 2: review failed for ${change.new_path}: ${preview}. Continuing with remaining files.`);
|
|
769
|
+
failedPaths.push(change.new_path);
|
|
770
|
+
// ponytail: failed file surfaces as "no findings" in the MR comment;
|
|
771
|
+
// thread failedPaths into the consolidate prompt if that ever matters.
|
|
772
|
+
findings = NO_FINDINGS_SENTENCE;
|
|
773
|
+
}
|
|
696
774
|
return { path: change.new_path, findings };
|
|
697
775
|
});
|
|
776
|
+
if (failedPaths.length === reviewFiles.length) {
|
|
777
|
+
throw new Error(`All ${reviewFiles.length} per-file reviews failed (e.g. ${failedPaths[0]}). See log above.`);
|
|
778
|
+
}
|
|
698
779
|
logStep("Pass 3/4: consolidating findings");
|
|
699
780
|
const consolidateMessages = buildConsolidatePrompt({
|
|
700
781
|
perFileFindings,
|
|
@@ -50,6 +50,58 @@ export async function renderDebugArtifactsHtml(params) {
|
|
|
50
50
|
return value;
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
+
function renderTriageDecisions() {
|
|
54
|
+
const decisionRecord = records.find((r) => r.kind === "triage_decision");
|
|
55
|
+
if (decisionRecord == null)
|
|
56
|
+
return "<pre>No triage decision record</pre>";
|
|
57
|
+
const summary = typeof decisionRecord.summary === "string" ? decisionRecord.summary : "";
|
|
58
|
+
const skipAllOverride = decisionRecord.skip_all_override === true;
|
|
59
|
+
const triageDiffChars = Number(decisionRecord.triage_diff_chars ?? 0);
|
|
60
|
+
const files = Array.isArray(decisionRecord.files) ? decisionRecord.files : [];
|
|
61
|
+
const actionLabel = (action) => {
|
|
62
|
+
switch (action) {
|
|
63
|
+
case "skipped":
|
|
64
|
+
return "Skipped";
|
|
65
|
+
case "reviewed_via_override":
|
|
66
|
+
return "Reviewed (override)";
|
|
67
|
+
case "review":
|
|
68
|
+
return "Review";
|
|
69
|
+
default:
|
|
70
|
+
return String(action ?? "unknown");
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const rows = files
|
|
74
|
+
.map((file) => {
|
|
75
|
+
const path = typeof file.path === "string" ? file.path : "?";
|
|
76
|
+
const verdict = typeof file.verdict === "string" ? file.verdict : "?";
|
|
77
|
+
const reason = typeof file.reason === "string" ? file.reason : "";
|
|
78
|
+
const action = actionLabel(file.review_action);
|
|
79
|
+
return `<tr>
|
|
80
|
+
<td><code>${escapeHtml(path)}</code></td>
|
|
81
|
+
<td>${escapeHtml(verdict)}</td>
|
|
82
|
+
<td>${escapeHtml(action)}</td>
|
|
83
|
+
<td>${escapeHtml(reason)}</td>
|
|
84
|
+
</tr>`;
|
|
85
|
+
})
|
|
86
|
+
.join("\n");
|
|
87
|
+
const overrideNote = skipAllOverride
|
|
88
|
+
? `<p class="tokens" style="margin:0 0 10px;color:var(--med);">All files were marked SKIP — pipeline overrode and reviewed every file.</p>`
|
|
89
|
+
: "";
|
|
90
|
+
return `${overrideNote}
|
|
91
|
+
<p class="tokens" style="margin:0 0 10px;">triage_diff_chars=${escapeHtml(String(triageDiffChars))}</p>
|
|
92
|
+
<p style="margin:0 0 12px;">${escapeHtml(summary)}</p>
|
|
93
|
+
<table style="width:100%;border-collapse:collapse;font-size:13px;">
|
|
94
|
+
<thead>
|
|
95
|
+
<tr style="text-align:left;color:var(--muted);">
|
|
96
|
+
<th style="padding:6px 8px;border-bottom:1px solid var(--line);">File</th>
|
|
97
|
+
<th style="padding:6px 8px;border-bottom:1px solid var(--line);">Verdict</th>
|
|
98
|
+
<th style="padding:6px 8px;border-bottom:1px solid var(--line);">Action</th>
|
|
99
|
+
<th style="padding:6px 8px;border-bottom:1px solid var(--line);">Reason</th>
|
|
100
|
+
</tr>
|
|
101
|
+
</thead>
|
|
102
|
+
<tbody>${rows}</tbody>
|
|
103
|
+
</table>`;
|
|
104
|
+
}
|
|
53
105
|
function renderFindings(markdown) {
|
|
54
106
|
const trimmed = markdown.trim();
|
|
55
107
|
if (trimmed === "")
|
|
@@ -144,7 +196,12 @@ export async function renderDebugArtifactsHtml(params) {
|
|
|
144
196
|
<div class="tokens" style="margin-top:6px;"><strong>Total:</strong> ${escapeHtml(totalTokens.toLocaleString())} tokens</div>
|
|
145
197
|
</div>
|
|
146
198
|
|
|
147
|
-
<h2>Pass 1 — Triage</h2>
|
|
199
|
+
<h2>Pass 1 — Triage Decisions</h2>
|
|
200
|
+
<div class="section">
|
|
201
|
+
${renderTriageDecisions()}
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<h2>Pass 1 — Triage (raw model output)</h2>
|
|
148
205
|
<div class="section">
|
|
149
206
|
<div class="row"><span class="badge">label: triage_pass</span><span class="tokens">${escapeHtml(findTs("triage_pass"))}</span></div>
|
|
150
207
|
<pre>${escapeHtml(triageContentPretty)}</pre>
|
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import OpenAI from "openai";
|
|
4
4
|
import { readFile } from "node:fs/promises";
|
|
5
5
|
import { DEFAULT_MAX_FINDINGS, DEFAULT_REVIEW_CONCURRENCY, } from "./prompt/index.js";
|
|
6
|
-
import { envOrDefault, envOrUndefined, hasDebugFlag, hasForceToolsFlag, hasIncludeArtifactsFlag, hasIgnoredExtension, parseIgnoreExtensions, parseNumberFlag, parsePromptLimits, readPromptProfileFromEnv, requireEnvs, } from "./cli/args.js";
|
|
6
|
+
import { envOrDefault, envOrUndefined, hasDebugFlag, hasForceToolsFlag, hasIncludeArtifactsFlag, hasIgnoredExtension, parseIgnoreExtensions, parseNumberFlag, parsePromptLimits, parseTriageDiffChars, readPromptProfileFromEnv, requireEnvs, } from "./cli/args.js";
|
|
7
7
|
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";
|
|
@@ -25,6 +25,7 @@ function printHelp() {
|
|
|
25
25
|
" --max-diffs=50",
|
|
26
26
|
" --max-diff-chars=16000",
|
|
27
27
|
" --max-total-prompt-chars=220000",
|
|
28
|
+
" --triage-diff-chars=2000 Max chars per file diff in triage pass (Pass 1).",
|
|
28
29
|
" --max-findings=5 Max findings in final review (CI multi-pass only).",
|
|
29
30
|
" --max-review-concurrency=5 Parallel per-file review calls (CI multi-pass only).",
|
|
30
31
|
"",
|
|
@@ -77,6 +78,7 @@ async function main() {
|
|
|
77
78
|
logStep(`gitlab-ai-review v${cliVersion}`);
|
|
78
79
|
const ignoredExtensions = parseIgnoreExtensions(process.argv);
|
|
79
80
|
const promptLimits = parsePromptLimits(process.argv);
|
|
81
|
+
const triageDiffChars = parseTriageDiffChars(process.argv);
|
|
80
82
|
const maxFindings = parseNumberFlag(process.argv, "max-findings", DEFAULT_MAX_FINDINGS, 1);
|
|
81
83
|
const reviewConcurrency = parseNumberFlag(process.argv, "max-review-concurrency", DEFAULT_REVIEW_CONCURRENCY, 1);
|
|
82
84
|
const aiModel = envOrDefault("AI_MODEL", "gpt-4o-mini");
|
|
@@ -134,6 +136,7 @@ async function main() {
|
|
|
134
136
|
openaiInstance: new OpenAI({ apiKey: openaiApiKey }),
|
|
135
137
|
aiModel,
|
|
136
138
|
promptLimits,
|
|
139
|
+
triageDiffChars,
|
|
137
140
|
changes: filteredChanges,
|
|
138
141
|
refs: {
|
|
139
142
|
base: mrChanges.diff_refs?.base_sha ?? "HEAD",
|
package/dist/prompt/index.js
CHANGED
|
@@ -10,6 +10,8 @@ export const DEFAULT_PROMPT_LIMITS = {
|
|
|
10
10
|
maxDiffChars: 16000,
|
|
11
11
|
maxTotalPromptChars: 220000,
|
|
12
12
|
};
|
|
13
|
+
/** Max chars of each file diff sent to the triage pass (Pass 1). */
|
|
14
|
+
export const DEFAULT_TRIAGE_DIFF_CHARS = 2000;
|
|
13
15
|
export const AI_MODEL_TEMPERATURE = 0.2;
|
|
14
16
|
export const AI_MAX_OUTPUT_TOKENS = 600;
|
|
15
17
|
export const buildPrompt = ({ changes, limits, allowTools = false, profile = DEFAULT_PROMPT_PROFILE, }) => {
|
|
@@ -52,12 +54,12 @@ export const buildPrompt = ({ changes, limits, allowTools = false, profile = DEF
|
|
|
52
54
|
// ---------------------------------------------------------------------------
|
|
53
55
|
export const DEFAULT_MAX_FINDINGS = 5;
|
|
54
56
|
export const DEFAULT_REVIEW_CONCURRENCY = 5;
|
|
55
|
-
export function buildTriagePrompt(changes, profile = DEFAULT_PROMPT_PROFILE) {
|
|
57
|
+
export function buildTriagePrompt(changes, profile = DEFAULT_PROMPT_PROFILE, triageDiffChars = DEFAULT_TRIAGE_DIFF_CHARS) {
|
|
56
58
|
return [
|
|
57
59
|
buildTriageSystemMessage(profile),
|
|
58
60
|
{
|
|
59
61
|
role: "user",
|
|
60
|
-
content: buildTriageUserContent(changes),
|
|
62
|
+
content: buildTriageUserContent(changes, triageDiffChars),
|
|
61
63
|
},
|
|
62
64
|
];
|
|
63
65
|
}
|
|
@@ -115,6 +117,9 @@ export function parseTriageResponseDetailed(text) {
|
|
|
115
117
|
.map((f) => ({
|
|
116
118
|
path: f.path,
|
|
117
119
|
verdict: f.verdict,
|
|
120
|
+
...(typeof f.reason === "string" && f.reason.trim() !== ""
|
|
121
|
+
? { reason: f.reason.trim() }
|
|
122
|
+
: {}),
|
|
118
123
|
})),
|
|
119
124
|
},
|
|
120
125
|
reason: null,
|
|
@@ -6,7 +6,9 @@ const DEFAULT_LINES = [
|
|
|
6
6
|
"Also produce a concise summary (2-4 sentences) of the entire merge request: what it does and which areas it touches.",
|
|
7
7
|
"",
|
|
8
8
|
"Respond with a JSON object (no markdown fences, no prose before or after) in this exact schema:",
|
|
9
|
-
'{ "summary": "<MR summary>", "files": [{ "path": "<file path>", "verdict": "NEEDS_REVIEW" | "SKIP" }] }',
|
|
9
|
+
'{ "summary": "<MR summary>", "files": [{ "path": "<file path>", "verdict": "NEEDS_REVIEW" | "SKIP", "reason": "<one short sentence explaining the verdict>" }] }',
|
|
10
|
+
"",
|
|
11
|
+
"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\").",
|
|
10
12
|
"",
|
|
11
13
|
"Rules:",
|
|
12
14
|
"- When in doubt, verdict is NEEDS_REVIEW.",
|
|
@@ -20,13 +22,13 @@ const DEFAULT_LINES = [
|
|
|
20
22
|
"- Config/CI/docs files are SKIP unless they modify build targets, env vars, or secrets.",
|
|
21
23
|
"",
|
|
22
24
|
"Example output (copy this style exactly — single JSON object, no fences, no preamble):",
|
|
23
|
-
'{"summary":"Adds retry-with-backoff to the OpenAI client and updates the README badge.","files":[{"path":"src/openai/client.ts","verdict":"NEEDS_REVIEW"},{"path":"README.md","verdict":"SKIP"}]}',
|
|
25
|
+
'{"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"}]}',
|
|
24
26
|
];
|
|
25
27
|
const WEAK_LINES = [
|
|
26
28
|
"You triage merge-request files. Output one JSON object only — no prose, no markdown, no code fences, no preamble.",
|
|
27
29
|
"",
|
|
28
30
|
"Schema (use exactly these keys):",
|
|
29
|
-
'{"summary":"<2-3 sentences about the MR>","files":[{"path":"<file path>","verdict":"NEEDS_REVIEW" | "SKIP"}]}',
|
|
31
|
+
'{"summary":"<2-3 sentences about the MR>","files":[{"path":"<file path>","verdict":"NEEDS_REVIEW" | "SKIP","reason":"<one short sentence>"}]}',
|
|
30
32
|
"",
|
|
31
33
|
"Mark a file NEEDS_REVIEW when ANY of these is true:",
|
|
32
34
|
"- the diff adds or removes non-comment lines,",
|
|
@@ -39,7 +41,7 @@ const WEAK_LINES = [
|
|
|
39
41
|
"When in doubt, choose NEEDS_REVIEW.",
|
|
40
42
|
"",
|
|
41
43
|
"Example output (copy this exactly — one line, one JSON object, nothing else):",
|
|
42
|
-
'{"summary":"Adds retry-with-backoff to the OpenAI client and updates the README badge.","files":[{"path":"src/openai/client.ts","verdict":"NEEDS_REVIEW"},{"path":"README.md","verdict":"SKIP"}]}',
|
|
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"}]}',
|
|
43
45
|
];
|
|
44
46
|
export function getTriageSystemLines(profile) {
|
|
45
47
|
return profile === "weak" ? WEAK_LINES : DEFAULT_LINES;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** @format */
|
|
2
|
+
import { truncateWithMarker } from "../utils.js";
|
|
2
3
|
export function buildMainReviewUserContent(params) {
|
|
3
4
|
const { stats, toolNote, changesText } = params;
|
|
4
5
|
return [
|
|
@@ -11,7 +12,7 @@ export function buildMainReviewUserContent(params) {
|
|
|
11
12
|
"Produce your review now. Follow the system instructions strictly.",
|
|
12
13
|
].join("\n");
|
|
13
14
|
}
|
|
14
|
-
export function buildTriageUserContent(changes) {
|
|
15
|
+
export function buildTriageUserContent(changes, maxDiffChars) {
|
|
15
16
|
const fileEntries = changes.map((c) => {
|
|
16
17
|
const flags = [];
|
|
17
18
|
if (c.new_file)
|
|
@@ -21,9 +22,8 @@ export function buildTriageUserContent(changes) {
|
|
|
21
22
|
if (c.renamed_file)
|
|
22
23
|
flags.push("renamed");
|
|
23
24
|
const flagStr = flags.length > 0 ? ` [${flags.join(", ")}]` : "";
|
|
24
|
-
const snippet = c.diff
|
|
25
|
-
|
|
26
|
-
return `### ${c.path}${flagStr}\n\`\`\`\n${snippet}${truncNote}\n\`\`\``;
|
|
25
|
+
const snippet = truncateWithMarker(c.diff, maxDiffChars, c.path);
|
|
26
|
+
return `### ${c.path}${flagStr}\n\`\`\`diff\n${snippet}\n\`\`\``;
|
|
27
27
|
});
|
|
28
28
|
return `Triage these ${changes.length} file(s):\n\n${fileEntries.join("\n\n")}`;
|
|
29
29
|
}
|