@weareikko/code-review 0.8.3 → 0.8.5
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 +9 -2
- package/dist/{cli-CICuhytH.js → cli-DmzA9PxS.js} +356 -59
- package/dist/cli-DmzA9PxS.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/config.d.ts +11 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/git-tool.d.ts +29 -0
- package/dist/git-tool.d.ts.map +1 -0
- package/dist/gitlab-review.d.ts +35 -1
- package/dist/gitlab-review.d.ts.map +1 -1
- package/dist/posting.d.ts +5 -0
- package/dist/posting.d.ts.map +1 -1
- package/dist/review.js +1 -1
- package/dist/skipped-retrieval.d.ts +8 -3
- package/dist/skipped-retrieval.d.ts.map +1 -1
- package/dist/types.d.ts +16 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/verify.d.ts +2 -1
- package/dist/verify.d.ts.map +1 -1
- package/package.json +4 -1
- package/dist/cli-CICuhytH.js.map +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdir, readFile, readdir, rename, rm, unlink, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, join, relative, resolve } from "node:path";
|
|
3
3
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import nodeFs, { existsSync, readFileSync } from "node:fs";
|
|
5
5
|
import { getEnvApiKey, getModel } from "@earendil-works/pi-ai";
|
|
6
6
|
import { createHash, randomUUID } from "node:crypto";
|
|
7
7
|
import { tracingChannel } from "node:diagnostics_channel";
|
|
@@ -10,6 +10,9 @@ import { execFile } from "node:child_process";
|
|
|
10
10
|
import { promisify } from "node:util";
|
|
11
11
|
import { Agent } from "@earendil-works/pi-agent-core";
|
|
12
12
|
import { createReadOnlyTools } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import { createTwoFilesPatch } from "diff";
|
|
14
|
+
import * as git from "isomorphic-git";
|
|
15
|
+
import { Type } from "typebox";
|
|
13
16
|
import { homedir } from "node:os";
|
|
14
17
|
import { parse } from "yaml";
|
|
15
18
|
import { SpanKind, SpanStatusCode, context, metrics, trace } from "@opentelemetry/api";
|
|
@@ -512,7 +515,12 @@ function buildSizeNoticeBlock(notice) {
|
|
|
512
515
|
if (sizeSkippedFiles.length > 0) {
|
|
513
516
|
const fileList = sizeSkippedFiles.map((file) => `- \`${file.path}\` (${formatChars(file.chars)})`).join("\n");
|
|
514
517
|
const cov = notice.coverage;
|
|
515
|
-
const
|
|
518
|
+
const retrieved = notice.retrieved === true;
|
|
519
|
+
let coverageLine;
|
|
520
|
+
if (cov && cov.totalLines > 0) {
|
|
521
|
+
const pct = Math.round(cov.reviewedLines / cov.totalLines * 100);
|
|
522
|
+
coverageLine = retrieved ? `> **Large diff — ~${pct}% of changed lines fit the inline budget** (${cov.reviewedLines} of ${cov.totalLines}). The files below exceeded it and were staged for on-demand retrieval; see the review summary for which were read. Absence from the findings is not a clean bill of health.` : `> **Partial review — ~${pct}% of changed lines reviewed** (${cov.reviewedLines} of ${cov.totalLines}). The files below were NOT reviewed; their absence from the findings is not a clean bill of health.`;
|
|
523
|
+
} else coverageLine = retrieved ? `> **${sizeSkippedFiles.length} file(s) exceeded the size budget** — their diffs were staged for on-demand retrieval; see the review summary for which were read:` : `> **${sizeSkippedFiles.length} file(s) were not reviewed** — the diff exceeded the size budget, so these files were dropped from the review:`;
|
|
516
524
|
blocks.push([
|
|
517
525
|
`> [!WARNING]`,
|
|
518
526
|
coverageLine,
|
|
@@ -543,7 +551,7 @@ function buildSummaryBody(summary, costFooter, options = {}) {
|
|
|
543
551
|
return `${withFooter}\n\n${buildSummaryHistoryBlock(historyEntries)}`;
|
|
544
552
|
}
|
|
545
553
|
function buildReviewedCommitFooter(commitSha) {
|
|
546
|
-
return `Reviewed by ${PRODUCT_LINK} v0.8.
|
|
554
|
+
return `Reviewed by ${PRODUCT_LINK} v0.8.5 for commit ${commitSha}.`;
|
|
547
555
|
}
|
|
548
556
|
function extractReviewedCommitSha(body) {
|
|
549
557
|
return REVIEWED_COMMIT_FOOTER_PATTERN.exec(body)?.[1] ?? null;
|
|
@@ -795,6 +803,12 @@ var REVIEW_DEPTHS = [
|
|
|
795
803
|
"verify",
|
|
796
804
|
"full"
|
|
797
805
|
];
|
|
806
|
+
var REVIEW_INPUT_MODES = [
|
|
807
|
+
"auto",
|
|
808
|
+
"inline",
|
|
809
|
+
"disk",
|
|
810
|
+
"commits"
|
|
811
|
+
];
|
|
798
812
|
var THINKING_LEVELS = [
|
|
799
813
|
"off",
|
|
800
814
|
"minimal",
|
|
@@ -936,6 +950,7 @@ var BOOLEAN_FLAGS = new Set([
|
|
|
936
950
|
"no-summary",
|
|
937
951
|
"force-review",
|
|
938
952
|
"retrieve-skipped",
|
|
953
|
+
"no-retrieve-skipped",
|
|
939
954
|
"verbose",
|
|
940
955
|
"help",
|
|
941
956
|
"version"
|
|
@@ -994,6 +1009,22 @@ function resolvePostSummary(args, env) {
|
|
|
994
1009
|
}
|
|
995
1010
|
return true;
|
|
996
1011
|
}
|
|
1012
|
+
function resolveRetrieveSkipped(args, env) {
|
|
1013
|
+
if (args.noRetrieveSkipped === true) return false;
|
|
1014
|
+
if (args.retrieveSkipped === true) return true;
|
|
1015
|
+
const raw = env.CODE_REVIEW_RETRIEVE_SKIPPED;
|
|
1016
|
+
if (typeof raw === "string") {
|
|
1017
|
+
const normalized = raw.trim().toLowerCase();
|
|
1018
|
+
if ([
|
|
1019
|
+
"0",
|
|
1020
|
+
"false",
|
|
1021
|
+
"no",
|
|
1022
|
+
"off"
|
|
1023
|
+
].includes(normalized)) return false;
|
|
1024
|
+
if (normalized.length > 0) return true;
|
|
1025
|
+
}
|
|
1026
|
+
return true;
|
|
1027
|
+
}
|
|
997
1028
|
function normalizeChoice(value) {
|
|
998
1029
|
return String(value ?? "").trim().toLowerCase();
|
|
999
1030
|
}
|
|
@@ -1189,6 +1220,7 @@ function resolveConfig(argv = process.argv.slice(2), env = process.env) {
|
|
|
1189
1220
|
minSeverity: normalizeChoice(args.minSeverity ?? env.CODE_REVIEW_MIN_SEVERITY ?? "info"),
|
|
1190
1221
|
thinkingLevel: normalizeChoice(args.thinking ?? env.CODE_REVIEW_THINKING_LEVEL ?? "off"),
|
|
1191
1222
|
reviewDepth: normalizeChoice(args.reviewDepth ?? env.CODE_REVIEW_DEPTH ?? "single"),
|
|
1223
|
+
inputMode: normalizeChoice(args.inputMode ?? env.CODE_REVIEW_INPUT_MODE ?? "auto"),
|
|
1192
1224
|
verifyModel: String(args.verifyModel ?? env.CODE_REVIEW_VERIFY_MODEL ?? ""),
|
|
1193
1225
|
postingMode: normalizeChoice(args.postingMode ?? env.CODE_REVIEW_POSTING_MODE ?? "direct"),
|
|
1194
1226
|
apiKey,
|
|
@@ -1197,7 +1229,7 @@ function resolveConfig(argv = process.argv.slice(2), env = process.env) {
|
|
|
1197
1229
|
maxDiffChars,
|
|
1198
1230
|
decomposeHintLines,
|
|
1199
1231
|
diffContext,
|
|
1200
|
-
retrieveSkipped:
|
|
1232
|
+
retrieveSkipped: resolveRetrieveSkipped(args, env),
|
|
1201
1233
|
reviewFile: String(args.reviewFile ?? "code-review.md"),
|
|
1202
1234
|
output: String(args.output ?? "review-comments.json"),
|
|
1203
1235
|
dryRun: toBoolean(args.dryRun),
|
|
@@ -1246,6 +1278,7 @@ function validateConfig(config) {
|
|
|
1246
1278
|
].includes(config.minSeverity)) throw new ConfigError("--min-severity must be one of: info, warn, critical");
|
|
1247
1279
|
if (!THINKING_LEVELS.includes(config.thinkingLevel)) throw new ConfigError(`--thinking must be one of: ${THINKING_LEVELS.join(", ")}`);
|
|
1248
1280
|
if (!REVIEW_DEPTHS.includes(config.reviewDepth)) throw new ConfigError(`--review-depth must be one of: ${REVIEW_DEPTHS.join(", ")}`);
|
|
1281
|
+
if (config.inputMode !== void 0 && !REVIEW_INPUT_MODES.includes(config.inputMode)) throw new ConfigError(`--input-mode must be one of: ${REVIEW_INPUT_MODES.join(", ")}`);
|
|
1249
1282
|
if (!POSTING_MODES.includes(config.postingMode)) throw new ConfigError(`--posting-mode must be one of: ${POSTING_MODES.join(", ")}`);
|
|
1250
1283
|
}
|
|
1251
1284
|
//#endregion
|
|
@@ -1379,7 +1412,7 @@ function gitErrorMessage(error) {
|
|
|
1379
1412
|
err.stdout
|
|
1380
1413
|
].filter(Boolean).join("\n").trim();
|
|
1381
1414
|
}
|
|
1382
|
-
async function git(args, options = {}) {
|
|
1415
|
+
async function git$1(args, options = {}) {
|
|
1383
1416
|
try {
|
|
1384
1417
|
const { stdout } = await exec$1("git", args, {
|
|
1385
1418
|
cwd: options.cwd,
|
|
@@ -1421,7 +1454,7 @@ function getMergeDiffCommand(targetBranch, options = {}) {
|
|
|
1421
1454
|
];
|
|
1422
1455
|
}
|
|
1423
1456
|
async function fetchBranch(remote, branch, options) {
|
|
1424
|
-
await git([
|
|
1457
|
+
await git$1([
|
|
1425
1458
|
"fetch",
|
|
1426
1459
|
"--no-tags",
|
|
1427
1460
|
remote,
|
|
@@ -1430,7 +1463,7 @@ async function fetchBranch(remote, branch, options) {
|
|
|
1430
1463
|
}
|
|
1431
1464
|
async function isTracked(path, options) {
|
|
1432
1465
|
try {
|
|
1433
|
-
await git([
|
|
1466
|
+
await git$1([
|
|
1434
1467
|
"ls-files",
|
|
1435
1468
|
"--error-unmatch",
|
|
1436
1469
|
"--",
|
|
@@ -1457,7 +1490,7 @@ async function removeGeneratedCodeQualityArtifacts(paths = DEFAULT_CODEQUALITY_A
|
|
|
1457
1490
|
async function prepareGitHistory(sourceBranch, targetBranch, options = {}) {
|
|
1458
1491
|
const remote = options.remote ?? "origin";
|
|
1459
1492
|
await removeGeneratedCodeQualityArtifacts(options.codeQualityArtifacts, options);
|
|
1460
|
-
await git([
|
|
1493
|
+
await git$1([
|
|
1461
1494
|
"fetch",
|
|
1462
1495
|
"--unshallow",
|
|
1463
1496
|
"--no-tags",
|
|
@@ -1471,7 +1504,7 @@ async function prepareGitHistory(sourceBranch, targetBranch, options = {}) {
|
|
|
1471
1504
|
}
|
|
1472
1505
|
if (fetchErrors.length === 2) throw new GitError(`Unable to fetch MR source/target branches from ${remote}.`, { hint: fetchErrors.join("\n") });
|
|
1473
1506
|
try {
|
|
1474
|
-
await git([
|
|
1507
|
+
await git$1([
|
|
1475
1508
|
"merge-base",
|
|
1476
1509
|
remoteRef(remote, targetBranch),
|
|
1477
1510
|
"HEAD"
|
|
@@ -1485,7 +1518,7 @@ async function prepareGitHistory(sourceBranch, targetBranch, options = {}) {
|
|
|
1485
1518
|
}
|
|
1486
1519
|
}
|
|
1487
1520
|
async function getMergeDiff(targetBranch, options = {}) {
|
|
1488
|
-
return git(getMergeDiffCommand(targetBranch, options), options);
|
|
1521
|
+
return git$1(getMergeDiffCommand(targetBranch, options), options);
|
|
1489
1522
|
}
|
|
1490
1523
|
function getMergeCommitLogArguments(targetBranch, options = {}) {
|
|
1491
1524
|
return [
|
|
@@ -1496,7 +1529,7 @@ function getMergeCommitLogArguments(targetBranch, options = {}) {
|
|
|
1496
1529
|
];
|
|
1497
1530
|
}
|
|
1498
1531
|
async function getMergeCommitLog(targetBranch, options = {}) {
|
|
1499
|
-
return git(["log", ...getMergeCommitLogArguments(targetBranch, options)], options);
|
|
1532
|
+
return git$1(["log", ...getMergeCommitLogArguments(targetBranch, options)], options);
|
|
1500
1533
|
}
|
|
1501
1534
|
/**
|
|
1502
1535
|
* Summarize a unified diff into file/line counts for telemetry. Counts one file
|
|
@@ -1524,6 +1557,171 @@ function summarizeDiff(diff) {
|
|
|
1524
1557
|
};
|
|
1525
1558
|
}
|
|
1526
1559
|
//#endregion
|
|
1560
|
+
//#region src/git-tool.ts
|
|
1561
|
+
/**
|
|
1562
|
+
* Custom read-only git tools for the reviewer agent (Mode C: commit exploration).
|
|
1563
|
+
*
|
|
1564
|
+
* The reviewer's default toolbox (`createReadOnlyTools`) has no git access, and a
|
|
1565
|
+
* general bash tool is unsafe here — the reviewer processes attacker-controlled
|
|
1566
|
+
* MR content with CI credentials in the environment. These tools expose only
|
|
1567
|
+
* three read-only operations, backed by isomorphic-git (pure JS, no shell-out,
|
|
1568
|
+
* so there is no command-injection surface) and scoped to a single repo dir:
|
|
1569
|
+
*
|
|
1570
|
+
* - `git_log`: list commits (optionally the range since a base ref).
|
|
1571
|
+
* - `git_show`: a commit's message + unified diff against its first parent.
|
|
1572
|
+
* - `git_diff`: unified diff between two refs.
|
|
1573
|
+
*
|
|
1574
|
+
* Ref arguments are validated to sha/ref shapes; nothing is passed to a shell.
|
|
1575
|
+
*/
|
|
1576
|
+
/** Accept 4-40 hex sha prefixes or conservative ref names (branch/tag/HEAD~n). */
|
|
1577
|
+
var REF_RE = /^[0-9a-zA-Z._/~^-]{1,120}$/;
|
|
1578
|
+
var MAX_PATCH_BYTES = 2e5;
|
|
1579
|
+
function assertRef(ref) {
|
|
1580
|
+
if (!REF_RE.test(ref)) throw new Error(`Invalid git ref: ${JSON.stringify(ref)}`);
|
|
1581
|
+
}
|
|
1582
|
+
function text(body) {
|
|
1583
|
+
return {
|
|
1584
|
+
content: [{
|
|
1585
|
+
type: "text",
|
|
1586
|
+
text: body
|
|
1587
|
+
}],
|
|
1588
|
+
details: void 0
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
async function resolveOid(dir, fs, ref) {
|
|
1592
|
+
assertRef(ref);
|
|
1593
|
+
return git.resolveRef({
|
|
1594
|
+
fs,
|
|
1595
|
+
dir,
|
|
1596
|
+
ref
|
|
1597
|
+
}).catch(() => git.expandOid({
|
|
1598
|
+
fs,
|
|
1599
|
+
dir,
|
|
1600
|
+
oid: ref
|
|
1601
|
+
}));
|
|
1602
|
+
}
|
|
1603
|
+
function decode(data) {
|
|
1604
|
+
if (data.includes(0)) return null;
|
|
1605
|
+
return Buffer.from(data).toString("utf8");
|
|
1606
|
+
}
|
|
1607
|
+
async function readFileAt(dir, fs, oid, filepath) {
|
|
1608
|
+
try {
|
|
1609
|
+
const { blob } = await git.readBlob({
|
|
1610
|
+
fs,
|
|
1611
|
+
dir,
|
|
1612
|
+
oid,
|
|
1613
|
+
filepath
|
|
1614
|
+
});
|
|
1615
|
+
return decode(blob);
|
|
1616
|
+
} catch {
|
|
1617
|
+
return null;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
/** Unified diff of every changed file between two commit oids. */
|
|
1621
|
+
async function diffCommits(dir, fs, oldOid, newOid) {
|
|
1622
|
+
const trees = oldOid ? [git.TREE({ ref: oldOid }), git.TREE({ ref: newOid })] : [git.TREE({ ref: newOid })];
|
|
1623
|
+
const changed = await git.walk({
|
|
1624
|
+
fs,
|
|
1625
|
+
dir,
|
|
1626
|
+
trees,
|
|
1627
|
+
map: async (filepath, entries) => {
|
|
1628
|
+
if (filepath === ".") return void 0;
|
|
1629
|
+
if (!oldOid) {
|
|
1630
|
+
const [b] = entries;
|
|
1631
|
+
return b && await b.type() === "blob" ? filepath : void 0;
|
|
1632
|
+
}
|
|
1633
|
+
const [a, b] = entries;
|
|
1634
|
+
if ((a ? await a.oid() : void 0) === (b ? await b.oid() : void 0)) return void 0;
|
|
1635
|
+
const aType = a ? await a.type() : void 0;
|
|
1636
|
+
const bType = b ? await b.type() : void 0;
|
|
1637
|
+
if (aType === "tree" || bType === "tree") return void 0;
|
|
1638
|
+
return filepath;
|
|
1639
|
+
}
|
|
1640
|
+
});
|
|
1641
|
+
const patches = [];
|
|
1642
|
+
let patchBytes = 0;
|
|
1643
|
+
for (const filepath of changed.sort()) {
|
|
1644
|
+
const before = oldOid ? await readFileAt(dir, fs, oldOid, filepath) ?? "" : "";
|
|
1645
|
+
const after = await readFileAt(dir, fs, newOid, filepath) ?? "";
|
|
1646
|
+
if (before === after) continue;
|
|
1647
|
+
const patch = createTwoFilesPatch(`a/${filepath}`, `b/${filepath}`, before, after);
|
|
1648
|
+
patches.push(patch);
|
|
1649
|
+
patchBytes += patch.length + 1;
|
|
1650
|
+
if (patchBytes > MAX_PATCH_BYTES) {
|
|
1651
|
+
patches.push(`\n[diff truncated at ${MAX_PATCH_BYTES} bytes — open the file directly for the rest]`);
|
|
1652
|
+
break;
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
return patches.join("\n") || "(no textual changes)";
|
|
1656
|
+
}
|
|
1657
|
+
/**
|
|
1658
|
+
* Build the read-only git tools scoped to `dir` (the repo root). Returns an empty
|
|
1659
|
+
* array when `dir` is not a git repository, so callers can wire it
|
|
1660
|
+
* unconditionally.
|
|
1661
|
+
*/
|
|
1662
|
+
function createGitTools(dir, options = {}) {
|
|
1663
|
+
const fs = options.fs ?? nodeFs;
|
|
1664
|
+
if (!fs.existsSync(join(dir, ".git"))) return [];
|
|
1665
|
+
return [
|
|
1666
|
+
{
|
|
1667
|
+
name: "git_log",
|
|
1668
|
+
label: "git log",
|
|
1669
|
+
description: "List commits in this repository (newest first). Optionally pass `since` (a ref/sha) to list only commits after it — the range you have not reviewed yet.",
|
|
1670
|
+
parameters: Type.Object({
|
|
1671
|
+
since: Type.Optional(Type.String({ description: "Only list commits after this ref/sha (exclusive)." })),
|
|
1672
|
+
maxCount: Type.Optional(Type.Number({ description: "Maximum number of commits to return (default 50)." }))
|
|
1673
|
+
}),
|
|
1674
|
+
async execute(_id, params) {
|
|
1675
|
+
const depth = Math.min(Math.max(params.maxCount ?? 50, 1), 500);
|
|
1676
|
+
const commits = await git.log({
|
|
1677
|
+
fs,
|
|
1678
|
+
dir,
|
|
1679
|
+
depth
|
|
1680
|
+
});
|
|
1681
|
+
let stop = -1;
|
|
1682
|
+
if (params.since) {
|
|
1683
|
+
const sinceOid = await resolveOid(dir, fs, params.since);
|
|
1684
|
+
stop = commits.findIndex((c) => c.oid === sinceOid);
|
|
1685
|
+
if (stop < 0) return text(`(base ref ${params.since} is not within the ${depth} most recent commits — re-run git_log with a larger maxCount to cover the full unreviewed range)`);
|
|
1686
|
+
}
|
|
1687
|
+
const list = (stop >= 0 ? commits.slice(0, stop) : commits).map((c) => {
|
|
1688
|
+
const subject = c.commit.message.split("\n", 1)[0];
|
|
1689
|
+
return `${c.oid.slice(0, 10)} ${subject} (${c.commit.author.name})`;
|
|
1690
|
+
});
|
|
1691
|
+
return text(list.length ? list.join("\n") : "(no commits in range)");
|
|
1692
|
+
}
|
|
1693
|
+
},
|
|
1694
|
+
{
|
|
1695
|
+
name: "git_show",
|
|
1696
|
+
label: "git show",
|
|
1697
|
+
description: "Show a commit's message and its unified diff against its first parent. Pass `ref` (a sha or ref).",
|
|
1698
|
+
parameters: Type.Object({ ref: Type.String({ description: "The commit sha or ref to show." }) }),
|
|
1699
|
+
async execute(_id, params) {
|
|
1700
|
+
const oid = await resolveOid(dir, fs, params.ref);
|
|
1701
|
+
const { commit } = await git.readCommit({
|
|
1702
|
+
fs,
|
|
1703
|
+
dir,
|
|
1704
|
+
oid
|
|
1705
|
+
});
|
|
1706
|
+
const diff = await diffCommits(dir, fs, commit.parent[0] ?? null, oid);
|
|
1707
|
+
return text(`commit ${oid}\nAuthor: ${commit.author.name} <${commit.author.email}>\n\n${commit.message.trim()}\n\n${diff}`);
|
|
1708
|
+
}
|
|
1709
|
+
},
|
|
1710
|
+
{
|
|
1711
|
+
name: "git_diff",
|
|
1712
|
+
label: "git diff",
|
|
1713
|
+
description: "Unified diff between two refs/shas. Pass `from` and `to`.",
|
|
1714
|
+
parameters: Type.Object({
|
|
1715
|
+
from: Type.String({ description: "Base ref/sha." }),
|
|
1716
|
+
to: Type.String({ description: "Target ref/sha." })
|
|
1717
|
+
}),
|
|
1718
|
+
async execute(_id, params) {
|
|
1719
|
+
return text(await diffCommits(dir, fs, await resolveOid(dir, fs, params.from), await resolveOid(dir, fs, params.to)));
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
];
|
|
1723
|
+
}
|
|
1724
|
+
//#endregion
|
|
1527
1725
|
//#region src/logger.ts
|
|
1528
1726
|
var LEVELS = {
|
|
1529
1727
|
debug: 0,
|
|
@@ -2206,11 +2404,12 @@ function atEndOfBlockComment(text, i) {
|
|
|
2206
2404
|
}
|
|
2207
2405
|
//#endregion
|
|
2208
2406
|
//#region src/verify.ts
|
|
2209
|
-
function buildVerifySystemPrompt(diff, commitLog) {
|
|
2407
|
+
function buildVerifySystemPrompt(diff, commitLog, staged) {
|
|
2408
|
+
const diskMode = staged !== void 0 && staged.length > 0;
|
|
2210
2409
|
const parts = [
|
|
2211
2410
|
"You are a strict, adversarial verifier of a SINGLE code-review finding. Your job is to REFUTE the finding, not to agree with it.",
|
|
2212
2411
|
"",
|
|
2213
|
-
"Each request gives you one proposed finding (file, line, severity, confidence, and body) to check against the diff below. You may read referenced files to confirm reachability. Decide whether the finding survives scrutiny.",
|
|
2412
|
+
diskMode ? "Each request gives you one proposed finding (file, line, severity, confidence, and body). The change is NOT inline below — every file diff is staged on disk (see <staged_files>). Open the finding's file (and any others you need) with your file-read tool to check reachability. Decide whether the finding survives scrutiny." : "Each request gives you one proposed finding (file, line, severity, confidence, and body) to check against the diff below. You may read referenced files to confirm reachability. Decide whether the finding survives scrutiny.",
|
|
2214
2413
|
"",
|
|
2215
2414
|
"Apply this bar:",
|
|
2216
2415
|
"- The finding must point to a concrete defect demonstrable from the diff (and any file you read): a specific input, state, or execution path triggers it, and a violated contract is visible.",
|
|
@@ -2226,7 +2425,10 @@ function buildVerifySystemPrompt(diff, commitLog) {
|
|
|
2226
2425
|
"- \"drop\": not a real defect — speculative, stylistic, contradicted by the code/comments, or based on external state not visible in the diff."
|
|
2227
2426
|
];
|
|
2228
2427
|
if (commitLog?.trim()) parts.push("", `Commit messages for this change (oldest first):\n<commits>\n${commitLog.trim()}\n</commits>`);
|
|
2229
|
-
|
|
2428
|
+
if (diskMode) {
|
|
2429
|
+
const list = staged.map((f) => `- ${f.path} → ${f.diskPath}`).join("\n");
|
|
2430
|
+
parts.push("", `The change is staged on disk — open the relevant file(s) to verify:\n<staged_files>\n${list}\n</staged_files>`);
|
|
2431
|
+
} else parts.push("", `Verify each finding against this diff:\n<diff>\n${diff}\n</diff>`);
|
|
2230
2432
|
return parts.join("\n");
|
|
2231
2433
|
}
|
|
2232
2434
|
function buildVerifyUserPrompt(comment) {
|
|
@@ -3015,18 +3217,18 @@ function gitSkillCacheKey(url, ref) {
|
|
|
3015
3217
|
* remote's default branch via `HEAD`.
|
|
3016
3218
|
*/
|
|
3017
3219
|
async function gitShallowClone(url, ref, dir) {
|
|
3018
|
-
await git([
|
|
3220
|
+
await git$1([
|
|
3019
3221
|
"init",
|
|
3020
3222
|
"--quiet",
|
|
3021
3223
|
dir
|
|
3022
3224
|
]);
|
|
3023
|
-
await git([
|
|
3225
|
+
await git$1([
|
|
3024
3226
|
"remote",
|
|
3025
3227
|
"add",
|
|
3026
3228
|
"origin",
|
|
3027
3229
|
url
|
|
3028
3230
|
], { cwd: dir });
|
|
3029
|
-
await git([
|
|
3231
|
+
await git$1([
|
|
3030
3232
|
"fetch",
|
|
3031
3233
|
"--depth",
|
|
3032
3234
|
"1",
|
|
@@ -3034,7 +3236,7 @@ async function gitShallowClone(url, ref, dir) {
|
|
|
3034
3236
|
"origin",
|
|
3035
3237
|
ref || "HEAD"
|
|
3036
3238
|
], { cwd: dir });
|
|
3037
|
-
await git([
|
|
3239
|
+
await git$1([
|
|
3038
3240
|
"checkout",
|
|
3039
3241
|
"--quiet",
|
|
3040
3242
|
"FETCH_HEAD"
|
|
@@ -3176,11 +3378,18 @@ async function cleanupSkippedDiffs(cwd) {
|
|
|
3176
3378
|
});
|
|
3177
3379
|
}
|
|
3178
3380
|
/**
|
|
3179
|
-
* Render the `<skipped_files>` block
|
|
3180
|
-
*
|
|
3381
|
+
* Render the `<skipped_files>` block: each file with its on-disk diff path and an
|
|
3382
|
+
* instruction to read them.
|
|
3383
|
+
*
|
|
3384
|
+
* - `partial` (retrieval mode): only budget-dropped files are staged; the rest
|
|
3385
|
+
* of the diff is inline, so the agent reads the highest-risk staged files.
|
|
3386
|
+
* - `full` (disk input mode): the ENTIRE change is staged — nothing is inline —
|
|
3387
|
+
* so reading is not optional; the review is only as good as what gets opened.
|
|
3181
3388
|
*/
|
|
3182
|
-
function renderRetrievableSkippedBlock(files) {
|
|
3183
|
-
|
|
3389
|
+
function renderRetrievableSkippedBlock(files, mode = "partial") {
|
|
3390
|
+
const list = files.map((f) => `- ${f.path} → ${f.diskPath}`).join("\n");
|
|
3391
|
+
if (mode === "full") return `<skipped_files>\n${list}\n</skipped_files>\nThis review has NO diff inline — every changed file's diff is staged on disk at the path shown, and the list above is the complete change. Use your file-read tool to open and review them; a file you do not open is a file you did not review. Prioritise source files over config/tests/generated, but aim to cover the whole change. In your summary, state explicitly which files you reviewed and which you did not open.`;
|
|
3392
|
+
return `<skipped_files>\n${list}\n</skipped_files>\nThese files exceeded the inline size budget, so their diffs are NOT in the prompt above — but each is staged on disk at the path shown. Use your file-read tool to open the diffs most likely to contain defects (start with source files over config/tests) and review them as if they were inline. You may not have budget to read them all; say in your summary which you reviewed and which you did not.`;
|
|
3184
3393
|
}
|
|
3185
3394
|
//#endregion
|
|
3186
3395
|
//#region src/triage.ts
|
|
@@ -3569,13 +3778,22 @@ function filterDiff(raw, maxChars = DEFAULT_MAX_DIFF_CHARS) {
|
|
|
3569
3778
|
totalChars += section.length;
|
|
3570
3779
|
reviewedChangedLines += changedLines;
|
|
3571
3780
|
}
|
|
3781
|
+
const allSections = kept.map((section) => {
|
|
3782
|
+
const filePath = parseFilePath(section.split("\n", 1)[0] ?? "");
|
|
3783
|
+
return filePath ? {
|
|
3784
|
+
path: filePath,
|
|
3785
|
+
section,
|
|
3786
|
+
changedLines: countChangedLines(section)
|
|
3787
|
+
} : null;
|
|
3788
|
+
}).filter((entry) => entry !== null);
|
|
3572
3789
|
return {
|
|
3573
3790
|
diff: included.join(""),
|
|
3574
3791
|
noiseSkippedFiles,
|
|
3575
3792
|
sizeSkippedFiles,
|
|
3576
3793
|
reviewedChangedLines,
|
|
3577
3794
|
skippedChangedLines,
|
|
3578
|
-
sizeSkippedSections
|
|
3795
|
+
sizeSkippedSections,
|
|
3796
|
+
allSections
|
|
3579
3797
|
};
|
|
3580
3798
|
}
|
|
3581
3799
|
function mergeContent(files) {
|
|
@@ -3781,17 +3999,27 @@ function buildAngleSystemPrompt(context, minSeverity, angle) {
|
|
|
3781
3999
|
"</review_angle>"
|
|
3782
4000
|
].join("\n");
|
|
3783
4001
|
}
|
|
3784
|
-
function buildUserPrompt(diff, skippedFiles = [], commitLog, priorThreads, intent, coverage, retrievableSkipped) {
|
|
4002
|
+
function buildUserPrompt(diff, skippedFiles = [], commitLog, priorThreads, intent, coverage, retrievableSkipped, omitInlineDiff = false, commitExploration) {
|
|
3785
4003
|
const parts = [];
|
|
3786
4004
|
const intentBlock = renderIntentBlock(intent);
|
|
3787
4005
|
if (intentBlock) parts.push(`The author described the purpose of this change below. Use it as context for reading the diff. If the code omits something promised or adds something never claimed, note it in one line of the summary — do not raise inline findings on the description text itself:\n${intentBlock}`);
|
|
3788
4006
|
if (commitLog?.trim()) parts.push(`Commits in this MR (oldest first):\n<commits>\n${commitLog.trim()}\n</commits>`);
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
4007
|
+
if (commitExploration) {
|
|
4008
|
+
const since = commitExploration.sinceRef;
|
|
4009
|
+
parts.push([
|
|
4010
|
+
"This review has NO diff inline. Explore the change with the read-only git tools:",
|
|
4011
|
+
since ? `- \`git_log\` with since="${since}" lists the commits you have NOT reviewed yet (everything after ${since}). Review exactly those commits.` : "- `git_log` lists the commits in this change. Review all of them.",
|
|
4012
|
+
"- `git_show <sha>` shows a commit's message and full diff. Open every commit in scope.",
|
|
4013
|
+
"- `git_diff <from> <to>` shows the combined diff between two points if you prefer to review the range at once.",
|
|
4014
|
+
"A commit you do not open is code you did not review. In your summary, state which commits you reviewed."
|
|
4015
|
+
].join("\n"));
|
|
4016
|
+
}
|
|
4017
|
+
if (!omitInlineDiff && !commitExploration) parts.push(`Review this diff:\n<diff>\n${diff}\n</diff>`);
|
|
4018
|
+
if (retrievableSkipped && retrievableSkipped.length > 0) parts.push(renderRetrievableSkippedBlock(retrievableSkipped, omitInlineDiff ? "full" : "partial"));
|
|
4019
|
+
else if (skippedFiles.length > 0) parts.push(`<skipped_files>\n${skippedFiles.map((file) => `- ${file}`).join("\n")}\n</skipped_files>\nThe above files were not included because the diff exceeded the size limit. They are already surfaced to the reader in the MR summary, so do not re-list them; just do not assume they are clean, since you did not see them.`);
|
|
3792
4020
|
if (coverage && coverage.totalLines > 0 && coverage.reviewedLines < coverage.totalLines) {
|
|
3793
4021
|
const pct = Math.round(coverage.reviewedLines / coverage.totalLines * 100);
|
|
3794
|
-
parts.push(`<coverage>You reviewed ${coverage.reviewedLines} of ${coverage.totalLines} changed lines (~${pct}%). The rest were dropped for the size budget and you did NOT see them.
|
|
4022
|
+
parts.push(`<coverage>You reviewed ${coverage.reviewedLines} of ${coverage.totalLines} changed lines (~${pct}%). The rest were dropped for the size budget and you did NOT see them. The MR summary already reports this partial coverage to the reader, so do not restate it; just do not imply the unreviewed files are clean — their absence from your findings is not a clearance.</coverage>`);
|
|
3795
4023
|
}
|
|
3796
4024
|
if (priorThreads && priorThreads.length > 0) {
|
|
3797
4025
|
const block = renderPriorThreadsBlock(priorThreads);
|
|
@@ -4007,32 +4235,86 @@ function accumulateUsage(target, message, modelId) {
|
|
|
4007
4235
|
addUsageToBucket(bucket, message);
|
|
4008
4236
|
}
|
|
4009
4237
|
}
|
|
4238
|
+
/**
|
|
4239
|
+
* The "consider decomposing this MR" hint: present when a threshold is set
|
|
4240
|
+
* (`> 0`) and the change exceeds it. `changedLines` is the count the caller
|
|
4241
|
+
* deems reviewed — the budget-fitted subset for inline mode, the whole change
|
|
4242
|
+
* for disk/commits mode.
|
|
4243
|
+
*/
|
|
4244
|
+
function resolveDecomposeHint(threshold, reviewedLines) {
|
|
4245
|
+
return threshold > 0 && reviewedLines > threshold ? {
|
|
4246
|
+
lines: reviewedLines,
|
|
4247
|
+
threshold
|
|
4248
|
+
} : void 0;
|
|
4249
|
+
}
|
|
4010
4250
|
async function runReview(config, options) {
|
|
4011
4251
|
const cwd = options.cwd ?? config.cwd;
|
|
4012
4252
|
const minSeverity = toGitLabReviewSeverity(config.minSeverity);
|
|
4013
4253
|
const logger = options.logger ?? noopLogger;
|
|
4254
|
+
const inputMode = config.inputMode ?? "auto";
|
|
4255
|
+
const gitTools = createGitTools(cwd);
|
|
4256
|
+
let commitsMode = inputMode === "commits";
|
|
4257
|
+
if (commitsMode && gitTools.length === 0) {
|
|
4258
|
+
logger.warn("Commit-exploration input mode requires a git checkout, but none was found in cwd; falling back to auto input mode.");
|
|
4259
|
+
commitsMode = false;
|
|
4260
|
+
}
|
|
4261
|
+
const effectiveMode = commitsMode ? "commits" : inputMode === "commits" ? "auto" : inputMode;
|
|
4014
4262
|
const maxDiffChars = config.maxDiffChars > 0 ? config.maxDiffChars : DEFAULT_MAX_DIFF_CHARS;
|
|
4015
|
-
const { diff, noiseSkippedFiles, sizeSkippedFiles, reviewedChangedLines, skippedChangedLines, sizeSkippedSections } = filterDiff(options.diff, maxDiffChars);
|
|
4016
|
-
|
|
4017
|
-
const
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4263
|
+
const { diff, noiseSkippedFiles, sizeSkippedFiles, reviewedChangedLines, skippedChangedLines, sizeSkippedSections, allSections } = filterDiff(options.diff, maxDiffChars);
|
|
4264
|
+
const overflowed = sizeSkippedSections.length > 0;
|
|
4265
|
+
const diskMode = effectiveMode === "disk" || effectiveMode === "auto" && overflowed;
|
|
4266
|
+
if (effectiveMode === "auto") logger.info(diskMode ? `Auto input mode: diff exceeds the ${maxDiffChars}-char budget — staging all files on disk for the agent to read.` : "Auto input mode: diff fits the budget — reviewing inline.");
|
|
4267
|
+
if (!(diskMode || commitsMode ? allSections.length > 0 : diff.trim().length > 0)) throw new ReviewerError("No reviewable diff content after filtering noise files.", { hint: "Ensure the merge request introduces changes outside of generated/lock files." });
|
|
4268
|
+
const decomposeHint = resolveDecomposeHint(config.decomposeHintLines, diskMode || commitsMode ? reviewedChangedLines + skippedChangedLines : reviewedChangedLines);
|
|
4269
|
+
let promptDiff;
|
|
4270
|
+
let promptSkippedFiles;
|
|
4271
|
+
let promptCoverage;
|
|
4272
|
+
let retrievableSkipped;
|
|
4273
|
+
let sizeNotice;
|
|
4274
|
+
if (commitsMode) {
|
|
4275
|
+
retrievableSkipped = [];
|
|
4276
|
+
promptDiff = "";
|
|
4277
|
+
promptSkippedFiles = [];
|
|
4278
|
+
promptCoverage = void 0;
|
|
4279
|
+
sizeNotice = {
|
|
4280
|
+
sizeSkippedFiles: [],
|
|
4281
|
+
decomposeHint,
|
|
4282
|
+
retrieved: false
|
|
4283
|
+
};
|
|
4284
|
+
logger.info(options.sinceRef ? `Commit-exploration input mode: reviewing commits since ${options.sinceRef}.` : "Commit-exploration input mode: reviewing the full commit range.");
|
|
4285
|
+
} else if (diskMode) {
|
|
4286
|
+
retrievableSkipped = allSections.length > 0 ? await writeSkippedDiffs(cwd, allSections.map(({ path, section }) => ({
|
|
4287
|
+
path,
|
|
4288
|
+
section
|
|
4289
|
+
}))) : [];
|
|
4290
|
+
if (retrievableSkipped.length > 0) logger.info(`Disk input mode: staged ${retrievableSkipped.length} file diff(s) for on-demand review.`);
|
|
4291
|
+
promptDiff = "";
|
|
4292
|
+
promptSkippedFiles = [];
|
|
4293
|
+
promptCoverage = void 0;
|
|
4294
|
+
sizeNotice = {
|
|
4295
|
+
sizeSkippedFiles: [],
|
|
4296
|
+
decomposeHint,
|
|
4297
|
+
retrieved: false
|
|
4298
|
+
};
|
|
4299
|
+
} else {
|
|
4300
|
+
promptSkippedFiles = [...sizeSkippedFiles.map((f) => f.path), ...noiseSkippedFiles];
|
|
4301
|
+
promptCoverage = sizeSkippedFiles.length > 0 ? {
|
|
4302
|
+
reviewedLines: reviewedChangedLines,
|
|
4303
|
+
totalLines: reviewedChangedLines + skippedChangedLines
|
|
4304
|
+
} : void 0;
|
|
4305
|
+
retrievableSkipped = config.retrieveSkipped && sizeSkippedSections.length > 0 ? await writeSkippedDiffs(cwd, sizeSkippedSections) : [];
|
|
4306
|
+
if (retrievableSkipped.length > 0) logger.info(`Staged ${retrievableSkipped.length} dropped-file diff(s) on disk for retrieval.`);
|
|
4307
|
+
promptDiff = diff;
|
|
4308
|
+
sizeNotice = {
|
|
4309
|
+
sizeSkippedFiles,
|
|
4310
|
+
decomposeHint,
|
|
4311
|
+
coverage: promptCoverage,
|
|
4312
|
+
retrieved: retrievableSkipped.length > 0
|
|
4313
|
+
};
|
|
4314
|
+
}
|
|
4033
4315
|
const context = await loadReviewContext(cwd, config.skills, (msg) => logger.warn(msg), { refreshGitSkills: config.refreshGitSkills });
|
|
4034
4316
|
const systemPrompt = buildJSONSystemPrompt(context, minSeverity);
|
|
4035
|
-
const userPrompt = buildUserPrompt(
|
|
4317
|
+
const userPrompt = buildUserPrompt(promptDiff, promptSkippedFiles, options.commitLog, options.priorThreads, options.intent, promptCoverage, retrievableSkipped, diskMode, commitsMode ? { sinceRef: options.sinceRef } : void 0);
|
|
4036
4318
|
const skillNames = context.skills.map((s) => s.name);
|
|
4037
4319
|
if (skillNames.length > 0) logger.debug(`Skills loaded: ${skillNames.join(", ")}`);
|
|
4038
4320
|
if (context.conventions.length > 0) logger.debug(`Conventions: ${context.conventions.map((f) => f.path).join(", ")}`);
|
|
@@ -4040,7 +4322,7 @@ async function runReview(config, options) {
|
|
|
4040
4322
|
const pool = buildEffectivePool(config, logger);
|
|
4041
4323
|
if (pool.length > 1) logger.info(`Model pool: ${pool.map((m) => m.id).join(", ")}.`);
|
|
4042
4324
|
const primary = pool[0];
|
|
4043
|
-
const tools = createReadOnlyTools(cwd);
|
|
4325
|
+
const tools = [...createReadOnlyTools(cwd), ...gitTools];
|
|
4044
4326
|
const createAgent = options.createAgent ?? defaultCreateAgent;
|
|
4045
4327
|
const timeoutMs = options.timeoutMs ?? DEFAULT_REVIEW_TIMEOUT_MS;
|
|
4046
4328
|
const aggregated = emptyUsage();
|
|
@@ -4052,7 +4334,9 @@ async function runReview(config, options) {
|
|
|
4052
4334
|
timeoutMs,
|
|
4053
4335
|
logger,
|
|
4054
4336
|
aggregated,
|
|
4055
|
-
verifyMember: resolveVerifyMember(config, primary, logger)
|
|
4337
|
+
verifyMember: resolveVerifyMember(config, primary, logger),
|
|
4338
|
+
attachTelemetry: options.attachTelemetry,
|
|
4339
|
+
verifyStaged: diskMode ? retrievableSkipped : void 0
|
|
4056
4340
|
};
|
|
4057
4341
|
let outputText;
|
|
4058
4342
|
if (config.reviewDepth === "full") {
|
|
@@ -4218,6 +4502,7 @@ async function runMultiAngleFind(context, minSeverity, userPrompt, deps) {
|
|
|
4218
4502
|
thinkingLevel: deps.thinkingLevel,
|
|
4219
4503
|
getApiKey: member.getApiKey
|
|
4220
4504
|
});
|
|
4505
|
+
const detachTelemetry = deps.attachTelemetry?.(agent);
|
|
4221
4506
|
try {
|
|
4222
4507
|
const parsed = parseReviewMarkdownWithWarnings(await runAgentToCompletion(agent, userPrompt, {
|
|
4223
4508
|
timeoutMs: deps.timeoutMs,
|
|
@@ -4231,6 +4516,8 @@ async function runMultiAngleFind(context, minSeverity, userPrompt, deps) {
|
|
|
4231
4516
|
summaries[index] = parsed.summary;
|
|
4232
4517
|
} catch (error) {
|
|
4233
4518
|
deps.logger.warn(`Find angle "${angle.key}" failed: ${error.message}; skipping.`);
|
|
4519
|
+
} finally {
|
|
4520
|
+
detachTelemetry?.();
|
|
4234
4521
|
}
|
|
4235
4522
|
}), FIND_CONCURRENCY);
|
|
4236
4523
|
const raw = groups.reduce((total, group) => total + group.length, 0);
|
|
@@ -4256,7 +4543,7 @@ async function verifyAndSynthesize(findings, summary, diff, commitLog, deps) {
|
|
|
4256
4543
|
})).filter(({ finding }) => finding.comment.severity === "critical" || finding.comment.severity === "warn");
|
|
4257
4544
|
const verdicts = /* @__PURE__ */ new Map();
|
|
4258
4545
|
if (severe.length > 0) {
|
|
4259
|
-
const verifySystemPrompt = buildVerifySystemPrompt(diff, commitLog);
|
|
4546
|
+
const verifySystemPrompt = buildVerifySystemPrompt(diff, commitLog, deps.verifyStaged);
|
|
4260
4547
|
await runBounded(severe.map(({ finding, index }) => async () => {
|
|
4261
4548
|
const comment = finding.comment;
|
|
4262
4549
|
const verifierMember = deps.verifyMember ?? pickVerifier(deps.pool, finding.authorModel);
|
|
@@ -4267,6 +4554,7 @@ async function verifyAndSynthesize(findings, summary, diff, commitLog, deps) {
|
|
|
4267
4554
|
thinkingLevel: deps.thinkingLevel,
|
|
4268
4555
|
getApiKey: verifierMember.getApiKey
|
|
4269
4556
|
});
|
|
4557
|
+
const detachTelemetry = deps.attachTelemetry?.(verifier);
|
|
4270
4558
|
try {
|
|
4271
4559
|
const text = await runAgentToCompletion(verifier, buildVerifyUserPrompt(comment), {
|
|
4272
4560
|
timeoutMs: deps.timeoutMs,
|
|
@@ -4279,6 +4567,8 @@ async function verifyAndSynthesize(findings, summary, diff, commitLog, deps) {
|
|
|
4279
4567
|
decision: "keep",
|
|
4280
4568
|
reason: "verifier error; finding kept"
|
|
4281
4569
|
});
|
|
4570
|
+
} finally {
|
|
4571
|
+
detachTelemetry?.();
|
|
4282
4572
|
}
|
|
4283
4573
|
}), VERIFY_CONCURRENCY);
|
|
4284
4574
|
}
|
|
@@ -4909,7 +5199,7 @@ async function loadDefaultRuntime() {
|
|
|
4909
5199
|
const [sdkNode, resources, semconv] = modules;
|
|
4910
5200
|
const serviceResource = resources.resourceFromAttributes({
|
|
4911
5201
|
[semconv.ATTR_SERVICE_NAME ?? "service.name"]: SERVICE_NAME,
|
|
4912
|
-
[semconv.ATTR_SERVICE_VERSION ?? "service.version"]: "0.8.
|
|
5202
|
+
[semconv.ATTR_SERVICE_VERSION ?? "service.version"]: "0.8.5"
|
|
4913
5203
|
});
|
|
4914
5204
|
process.env.OTEL_METRICS_EXPORTER = process.env.OTEL_METRICS_EXPORTER ?? "otlp";
|
|
4915
5205
|
process.env.OTEL_LOGS_EXPORTER = process.env.OTEL_LOGS_EXPORTER ?? "otlp";
|
|
@@ -5341,7 +5631,7 @@ function boldCommentTitle(body) {
|
|
|
5341
5631
|
*/
|
|
5342
5632
|
function buildCommentBody(body, commitSha, confidence) {
|
|
5343
5633
|
const confidenceLine = `_Confidence: ${confidence}._`;
|
|
5344
|
-
const footer = `<sub>Reviewed by ${PRODUCT_LINK} v0.8.
|
|
5634
|
+
const footer = `<sub>Reviewed by ${PRODUCT_LINK} v0.8.5 for commit ${commitSha}.</sub>`;
|
|
5345
5635
|
return `${boldCommentTitle(body.trim())}\n\n${confidenceLine}\n\n---\n\n${footer}`;
|
|
5346
5636
|
}
|
|
5347
5637
|
function buildPayload(comment, body, refs, resolved) {
|
|
@@ -6066,15 +6356,21 @@ Options:
|
|
|
6066
6356
|
context aids reasoning but inflates tokens and fits fewer files in
|
|
6067
6357
|
the budget; less fits more. 0 = built-in default (20).
|
|
6068
6358
|
(env: CODE_REVIEW_DIFF_CONTEXT)
|
|
6069
|
-
--retrieve-skipped
|
|
6070
|
-
|
|
6071
|
-
|
|
6359
|
+
--no-retrieve-skipped Disable staging diffs for files dropped by the size budget on disk.
|
|
6360
|
+
Retrieval is on by default: dropped diffs are staged so the reviewer
|
|
6361
|
+
can read them on demand instead of losing them.
|
|
6362
|
+
(env: CODE_REVIEW_RETRIEVE_SKIPPED=0)
|
|
6072
6363
|
--min-severity <level> info, warn, or critical (default: info)
|
|
6073
6364
|
--thinking <level> off, minimal, low, medium, high, or xhigh (default: off).
|
|
6074
6365
|
Higher levels add billable thinking tokens at the model output rate.
|
|
6075
6366
|
--review-depth <depth> single (one pass), verify (adversarial re-check of each
|
|
6076
6367
|
severe finding), or full (multi-angle finders → triage →
|
|
6077
6368
|
verify). (default: single; env: CODE_REVIEW_DEPTH)
|
|
6369
|
+
--input-mode <mode> How the change is fed to the reviewer: auto (inline while it
|
|
6370
|
+
fits the budget, disk on overflow), inline (diff in the prompt),
|
|
6371
|
+
disk (every file diff staged on disk; the agent reads on demand),
|
|
6372
|
+
or commits (agent explores via read-only git tools).
|
|
6373
|
+
(default: auto; env: CODE_REVIEW_INPUT_MODE)
|
|
6078
6374
|
--verify-model <p/id> Model for the Verify stage (verify/full depth). Pairs a cheap
|
|
6079
6375
|
finder with a strong, high-precision verifier. Warns if it looks
|
|
6080
6376
|
cheaper than --model. Empty (default) = pool selection.
|
|
@@ -6181,7 +6477,8 @@ async function run(config, bridges) {
|
|
|
6181
6477
|
description: mr.description
|
|
6182
6478
|
},
|
|
6183
6479
|
logger,
|
|
6184
|
-
attachTelemetry: bridges?.otel?.createAgentTelemetry(runId)
|
|
6480
|
+
attachTelemetry: bridges?.otel?.createAgentTelemetry(runId),
|
|
6481
|
+
sinceRef: config.inputMode === "commits" && reviewedCommitSha && reviewedCommitSha !== refs.head_sha ? reviewedCommitSha : void 0
|
|
6185
6482
|
});
|
|
6186
6483
|
context.usage = result;
|
|
6187
6484
|
return result;
|
|
@@ -6428,10 +6725,10 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
6428
6725
|
return;
|
|
6429
6726
|
}
|
|
6430
6727
|
if (argv.includes("--version") || argv.includes("-v")) {
|
|
6431
|
-
console.log("0.8.
|
|
6728
|
+
console.log("0.8.5");
|
|
6432
6729
|
return;
|
|
6433
6730
|
}
|
|
6434
|
-
process.stderr.write(`[code-review] @weareikko/code-review v0.8.
|
|
6731
|
+
process.stderr.write(`[code-review] @weareikko/code-review v0.8.5\n`);
|
|
6435
6732
|
assertNodeVersion();
|
|
6436
6733
|
applyCodeReviewEnvPrefix();
|
|
6437
6734
|
applyDefaultCacheRetention();
|
|
@@ -6454,4 +6751,4 @@ if (isDirectRun()) main().catch((error) => {
|
|
|
6454
6751
|
//#endregion
|
|
6455
6752
|
export { normalizeBody as $, SUMMARY_HISTORY_END as A, buildSummaryHistoryEntries as B, createDiagnosticContext as C, traceDiagnosticPhase as D, traceDiagnostic as E, SUMMARY_MARKER as F, findExistingSummaryNoteId as G, extractSummaryHistoryEntries as H, buildArchivedSummaryEntry as I, upsertSummaryNote as J, stripSummaryHistory as K, buildReviewedCommitFooter as L, SUMMARY_HISTORY_ENTRY_START as M, SUMMARY_HISTORY_LIMIT as N, normalizeSeverity as O, SUMMARY_HISTORY_START as P, fingerprints as Q, buildSizeNoticeBlock as R, DIAGNOSTIC_CHANNEL_PREFIX as S, diagnosticChannels as T, findExistingReviewedCommitSha as U, extractReviewedCommitSha as V, findExistingSummaryNote as W, extractDiffHunkContext as X, appendFingerprintMarkers as Y, extractExistingFingerprints as Z, resolveNpmSkillDir as _, main as a, parseReviewMarkdownWithWarnings as b, buildGeneratedComments as c, startOtelBridge as d, sha256 as et, filterDiff as f, parseSkillSpec as g, loadNamedSkill as h, formatUsageLine as i, SUMMARY_HISTORY_ENTRY_END as j, toGitLabReviewSeverity as k, buildPayload as l, gitSkillCacheKey as m, formatPerModelUsage as n, run as o, runReview as p, stripSummaryMarker as q, formatSkillsFooter as r, withHttpStamping as s, countPostedBySeverity as t, isOtelEnabled as u, resolveSkillCacheDir as v, createDiagnosticRunId as w, DIAGNOSTIC_CHANNEL_NAMES as x, parseReviewMarkdown as y, buildSummaryBody as z };
|
|
6456
6753
|
|
|
6457
|
-
//# sourceMappingURL=cli-
|
|
6754
|
+
//# sourceMappingURL=cli-DmzA9PxS.js.map
|