@krotovm/gitlab-ai-review 1.0.33 → 1.0.34
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 +67 -8
- 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) {
|
|
@@ -592,9 +637,10 @@ async function runVerificationWithTools(params) {
|
|
|
592
637
|
});
|
|
593
638
|
}
|
|
594
639
|
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;
|
|
640
|
+
const { openaiInstance, aiModel, promptLimits, triageDiffChars, changes, refs, gitLabProjectApiUrl, projectId, headers, maxFindings, reviewConcurrency, forceTools, promptProfile = DEFAULT_PROMPT_PROFILE, loggers, debugDumpFile, debugRecordWriter, } = params;
|
|
596
641
|
const { logStep } = loggers;
|
|
597
|
-
logStep(`Pass 1/4: triaging ${changes.length} file(s)
|
|
642
|
+
logStep(`Pass 1/4: triaging ${changes.length} file(s) ` +
|
|
643
|
+
`(prompt profile=${promptProfile}, triage_diff_chars=${triageDiffChars})`);
|
|
598
644
|
const triageInputs = changes.map((c) => ({
|
|
599
645
|
path: c.new_path,
|
|
600
646
|
new_file: c.new_file,
|
|
@@ -602,7 +648,7 @@ export async function reviewMergeRequestMultiPass(params) {
|
|
|
602
648
|
renamed_file: c.renamed_file,
|
|
603
649
|
diff: c.diff,
|
|
604
650
|
}));
|
|
605
|
-
const triageMessages = buildTriagePrompt(triageInputs, promptProfile);
|
|
651
|
+
const triageMessages = buildTriagePrompt(triageInputs, promptProfile, triageDiffChars);
|
|
606
652
|
let triageResult = null;
|
|
607
653
|
let triageText = null;
|
|
608
654
|
let triageParseReason = null;
|
|
@@ -662,16 +708,29 @@ export async function reviewMergeRequestMultiPass(params) {
|
|
|
662
708
|
debugRecordWriter,
|
|
663
709
|
});
|
|
664
710
|
}
|
|
665
|
-
const
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
711
|
+
const { decisions, skipAllOverride, reviewFiles } = buildTriageDecisions({
|
|
712
|
+
changes,
|
|
713
|
+
triageResult,
|
|
714
|
+
});
|
|
715
|
+
const skippedCount = decisions.filter((d) => d.review_action === "skipped").length;
|
|
716
|
+
await appendDebugDump(debugDumpFile, debugRecordWriter, {
|
|
717
|
+
kind: "triage_decision",
|
|
718
|
+
summary: triageResult.summary,
|
|
719
|
+
skip_all_override: skipAllOverride,
|
|
720
|
+
triage_diff_chars: triageDiffChars,
|
|
721
|
+
files: decisions,
|
|
722
|
+
});
|
|
723
|
+
if (skipAllOverride) {
|
|
669
724
|
logStep(`Triage wanted to skip all ${changes.length} file(s) — overriding to review all. Summary: ${triageResult.summary.slice(0, 120)}...`);
|
|
670
|
-
reviewFiles = changes;
|
|
671
725
|
}
|
|
672
726
|
else {
|
|
673
727
|
logStep(`Triage: ${reviewFiles.length} file(s) to review, ${skippedCount} skipped. Summary: ${triageResult.summary.slice(0, 120)}...`);
|
|
674
728
|
}
|
|
729
|
+
for (const decision of decisions) {
|
|
730
|
+
if (decision.review_action === "review")
|
|
731
|
+
continue;
|
|
732
|
+
logStep(formatTriageDecisionLine(decision));
|
|
733
|
+
}
|
|
675
734
|
logStep(`Pass 2/4: reviewing ${reviewFiles.length} file(s) (concurrency=${reviewConcurrency})`);
|
|
676
735
|
const allChangedPaths = changes.map((c) => c.new_path);
|
|
677
736
|
const perFileFindings = await mapWithConcurrency(reviewFiles, reviewConcurrency, async (change) => {
|
|
@@ -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
|
}
|