@weareikko/code-review 0.8.4 → 0.8.6
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/dist/{cli-FDpPZaNn.js → cli-BbR1oftT.js} +365 -73
- package/dist/cli-BbR1oftT.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/config.d.ts +9 -1
- 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/github.d.ts +14 -6
- package/dist/github.d.ts.map +1 -1
- package/dist/gitlab-review.d.ts +29 -1
- package/dist/gitlab-review.d.ts.map +1 -1
- package/dist/platforms/github.d.ts +7 -4
- package/dist/platforms/github.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-FDpPZaNn.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";
|
|
@@ -163,6 +166,7 @@ var REVIEW_THREADS_QUERY = `
|
|
|
163
166
|
pageInfo { hasNextPage endCursor }
|
|
164
167
|
nodes {
|
|
165
168
|
isResolved
|
|
169
|
+
isOutdated
|
|
166
170
|
comments(first: 100) { nodes { databaseId } }
|
|
167
171
|
}
|
|
168
172
|
}
|
|
@@ -335,14 +339,22 @@ var GitHubClient = class {
|
|
|
335
339
|
return parsed.data;
|
|
336
340
|
}
|
|
337
341
|
/**
|
|
338
|
-
* Return the database IDs of review comments that belong to a **
|
|
339
|
-
* review thread. GitHub's REST
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
342
|
+
* Return the database IDs of review comments that belong to a **settled**
|
|
343
|
+
* review thread — one that is either resolved or outdated. GitHub's REST
|
|
344
|
+
* comment endpoints omit both states; they are only exposed via GraphQL
|
|
345
|
+
* `reviewThreads.isResolved` / `isOutdated`. Callers use this set to mark
|
|
346
|
+
* normalized notes resolved so settled threads are excluded from summary
|
|
347
|
+
* carry-over and prior-thread context. Paginates over threads.
|
|
348
|
+
*
|
|
349
|
+
* Outdated counts as settled because GitHub, unlike GitLab, does not
|
|
350
|
+
* auto-resolve a thread when the line it anchors to changes: fixing a finding
|
|
351
|
+
* flips the thread to outdated but leaves `isResolved` false until someone
|
|
352
|
+
* manually resolves it. Treating outdated as settled mirrors GitLab's
|
|
353
|
+
* "automatically resolve outdated diff discussions" behaviour, so a fixed
|
|
354
|
+
* finding stops being re-listed under "Still open from earlier reviews" (#133).
|
|
343
355
|
*/
|
|
344
|
-
async
|
|
345
|
-
const
|
|
356
|
+
async listSettledReviewCommentIds(owner, repo, pull) {
|
|
357
|
+
const settled = /* @__PURE__ */ new Set();
|
|
346
358
|
let cursor = null;
|
|
347
359
|
let hasNext = true;
|
|
348
360
|
while (hasNext) {
|
|
@@ -354,14 +366,14 @@ var GitHubClient = class {
|
|
|
354
366
|
})).repository?.pullRequest?.reviewThreads;
|
|
355
367
|
if (!threads) break;
|
|
356
368
|
for (const thread of threads.nodes ?? []) {
|
|
357
|
-
if (!thread.isResolved) continue;
|
|
358
|
-
for (const comment of thread.comments?.nodes ?? []) if (typeof comment.databaseId === "number")
|
|
369
|
+
if (!thread.isResolved && !thread.isOutdated) continue;
|
|
370
|
+
for (const comment of thread.comments?.nodes ?? []) if (typeof comment.databaseId === "number") settled.add(comment.databaseId);
|
|
359
371
|
}
|
|
360
372
|
hasNext = threads.pageInfo?.hasNextPage ?? false;
|
|
361
373
|
cursor = threads.pageInfo?.endCursor ?? null;
|
|
362
374
|
if (!cursor) hasNext = false;
|
|
363
375
|
}
|
|
364
|
-
return
|
|
376
|
+
return settled;
|
|
365
377
|
}
|
|
366
378
|
};
|
|
367
379
|
//#endregion
|
|
@@ -548,7 +560,7 @@ function buildSummaryBody(summary, costFooter, options = {}) {
|
|
|
548
560
|
return `${withFooter}\n\n${buildSummaryHistoryBlock(historyEntries)}`;
|
|
549
561
|
}
|
|
550
562
|
function buildReviewedCommitFooter(commitSha) {
|
|
551
|
-
return `Reviewed by ${PRODUCT_LINK} v0.8.
|
|
563
|
+
return `Reviewed by ${PRODUCT_LINK} v0.8.6 for commit ${commitSha}.`;
|
|
552
564
|
}
|
|
553
565
|
function extractReviewedCommitSha(body) {
|
|
554
566
|
return REVIEWED_COMMIT_FOOTER_PATTERN.exec(body)?.[1] ?? null;
|
|
@@ -800,6 +812,12 @@ var REVIEW_DEPTHS = [
|
|
|
800
812
|
"verify",
|
|
801
813
|
"full"
|
|
802
814
|
];
|
|
815
|
+
var REVIEW_INPUT_MODES = [
|
|
816
|
+
"auto",
|
|
817
|
+
"inline",
|
|
818
|
+
"disk",
|
|
819
|
+
"commits"
|
|
820
|
+
];
|
|
803
821
|
var THINKING_LEVELS = [
|
|
804
822
|
"off",
|
|
805
823
|
"minimal",
|
|
@@ -1211,6 +1229,7 @@ function resolveConfig(argv = process.argv.slice(2), env = process.env) {
|
|
|
1211
1229
|
minSeverity: normalizeChoice(args.minSeverity ?? env.CODE_REVIEW_MIN_SEVERITY ?? "info"),
|
|
1212
1230
|
thinkingLevel: normalizeChoice(args.thinking ?? env.CODE_REVIEW_THINKING_LEVEL ?? "off"),
|
|
1213
1231
|
reviewDepth: normalizeChoice(args.reviewDepth ?? env.CODE_REVIEW_DEPTH ?? "single"),
|
|
1232
|
+
inputMode: normalizeChoice(args.inputMode ?? env.CODE_REVIEW_INPUT_MODE ?? "auto"),
|
|
1214
1233
|
verifyModel: String(args.verifyModel ?? env.CODE_REVIEW_VERIFY_MODEL ?? ""),
|
|
1215
1234
|
postingMode: normalizeChoice(args.postingMode ?? env.CODE_REVIEW_POSTING_MODE ?? "direct"),
|
|
1216
1235
|
apiKey,
|
|
@@ -1268,6 +1287,7 @@ function validateConfig(config) {
|
|
|
1268
1287
|
].includes(config.minSeverity)) throw new ConfigError("--min-severity must be one of: info, warn, critical");
|
|
1269
1288
|
if (!THINKING_LEVELS.includes(config.thinkingLevel)) throw new ConfigError(`--thinking must be one of: ${THINKING_LEVELS.join(", ")}`);
|
|
1270
1289
|
if (!REVIEW_DEPTHS.includes(config.reviewDepth)) throw new ConfigError(`--review-depth must be one of: ${REVIEW_DEPTHS.join(", ")}`);
|
|
1290
|
+
if (config.inputMode !== void 0 && !REVIEW_INPUT_MODES.includes(config.inputMode)) throw new ConfigError(`--input-mode must be one of: ${REVIEW_INPUT_MODES.join(", ")}`);
|
|
1271
1291
|
if (!POSTING_MODES.includes(config.postingMode)) throw new ConfigError(`--posting-mode must be one of: ${POSTING_MODES.join(", ")}`);
|
|
1272
1292
|
}
|
|
1273
1293
|
//#endregion
|
|
@@ -1401,7 +1421,7 @@ function gitErrorMessage(error) {
|
|
|
1401
1421
|
err.stdout
|
|
1402
1422
|
].filter(Boolean).join("\n").trim();
|
|
1403
1423
|
}
|
|
1404
|
-
async function git(args, options = {}) {
|
|
1424
|
+
async function git$1(args, options = {}) {
|
|
1405
1425
|
try {
|
|
1406
1426
|
const { stdout } = await exec$1("git", args, {
|
|
1407
1427
|
cwd: options.cwd,
|
|
@@ -1443,7 +1463,7 @@ function getMergeDiffCommand(targetBranch, options = {}) {
|
|
|
1443
1463
|
];
|
|
1444
1464
|
}
|
|
1445
1465
|
async function fetchBranch(remote, branch, options) {
|
|
1446
|
-
await git([
|
|
1466
|
+
await git$1([
|
|
1447
1467
|
"fetch",
|
|
1448
1468
|
"--no-tags",
|
|
1449
1469
|
remote,
|
|
@@ -1452,7 +1472,7 @@ async function fetchBranch(remote, branch, options) {
|
|
|
1452
1472
|
}
|
|
1453
1473
|
async function isTracked(path, options) {
|
|
1454
1474
|
try {
|
|
1455
|
-
await git([
|
|
1475
|
+
await git$1([
|
|
1456
1476
|
"ls-files",
|
|
1457
1477
|
"--error-unmatch",
|
|
1458
1478
|
"--",
|
|
@@ -1479,7 +1499,7 @@ async function removeGeneratedCodeQualityArtifacts(paths = DEFAULT_CODEQUALITY_A
|
|
|
1479
1499
|
async function prepareGitHistory(sourceBranch, targetBranch, options = {}) {
|
|
1480
1500
|
const remote = options.remote ?? "origin";
|
|
1481
1501
|
await removeGeneratedCodeQualityArtifacts(options.codeQualityArtifacts, options);
|
|
1482
|
-
await git([
|
|
1502
|
+
await git$1([
|
|
1483
1503
|
"fetch",
|
|
1484
1504
|
"--unshallow",
|
|
1485
1505
|
"--no-tags",
|
|
@@ -1493,7 +1513,7 @@ async function prepareGitHistory(sourceBranch, targetBranch, options = {}) {
|
|
|
1493
1513
|
}
|
|
1494
1514
|
if (fetchErrors.length === 2) throw new GitError(`Unable to fetch MR source/target branches from ${remote}.`, { hint: fetchErrors.join("\n") });
|
|
1495
1515
|
try {
|
|
1496
|
-
await git([
|
|
1516
|
+
await git$1([
|
|
1497
1517
|
"merge-base",
|
|
1498
1518
|
remoteRef(remote, targetBranch),
|
|
1499
1519
|
"HEAD"
|
|
@@ -1507,7 +1527,7 @@ async function prepareGitHistory(sourceBranch, targetBranch, options = {}) {
|
|
|
1507
1527
|
}
|
|
1508
1528
|
}
|
|
1509
1529
|
async function getMergeDiff(targetBranch, options = {}) {
|
|
1510
|
-
return git(getMergeDiffCommand(targetBranch, options), options);
|
|
1530
|
+
return git$1(getMergeDiffCommand(targetBranch, options), options);
|
|
1511
1531
|
}
|
|
1512
1532
|
function getMergeCommitLogArguments(targetBranch, options = {}) {
|
|
1513
1533
|
return [
|
|
@@ -1518,7 +1538,7 @@ function getMergeCommitLogArguments(targetBranch, options = {}) {
|
|
|
1518
1538
|
];
|
|
1519
1539
|
}
|
|
1520
1540
|
async function getMergeCommitLog(targetBranch, options = {}) {
|
|
1521
|
-
return git(["log", ...getMergeCommitLogArguments(targetBranch, options)], options);
|
|
1541
|
+
return git$1(["log", ...getMergeCommitLogArguments(targetBranch, options)], options);
|
|
1522
1542
|
}
|
|
1523
1543
|
/**
|
|
1524
1544
|
* Summarize a unified diff into file/line counts for telemetry. Counts one file
|
|
@@ -1546,6 +1566,171 @@ function summarizeDiff(diff) {
|
|
|
1546
1566
|
};
|
|
1547
1567
|
}
|
|
1548
1568
|
//#endregion
|
|
1569
|
+
//#region src/git-tool.ts
|
|
1570
|
+
/**
|
|
1571
|
+
* Custom read-only git tools for the reviewer agent (Mode C: commit exploration).
|
|
1572
|
+
*
|
|
1573
|
+
* The reviewer's default toolbox (`createReadOnlyTools`) has no git access, and a
|
|
1574
|
+
* general bash tool is unsafe here — the reviewer processes attacker-controlled
|
|
1575
|
+
* MR content with CI credentials in the environment. These tools expose only
|
|
1576
|
+
* three read-only operations, backed by isomorphic-git (pure JS, no shell-out,
|
|
1577
|
+
* so there is no command-injection surface) and scoped to a single repo dir:
|
|
1578
|
+
*
|
|
1579
|
+
* - `git_log`: list commits (optionally the range since a base ref).
|
|
1580
|
+
* - `git_show`: a commit's message + unified diff against its first parent.
|
|
1581
|
+
* - `git_diff`: unified diff between two refs.
|
|
1582
|
+
*
|
|
1583
|
+
* Ref arguments are validated to sha/ref shapes; nothing is passed to a shell.
|
|
1584
|
+
*/
|
|
1585
|
+
/** Accept 4-40 hex sha prefixes or conservative ref names (branch/tag/HEAD~n). */
|
|
1586
|
+
var REF_RE = /^[0-9a-zA-Z._/~^-]{1,120}$/;
|
|
1587
|
+
var MAX_PATCH_BYTES = 2e5;
|
|
1588
|
+
function assertRef(ref) {
|
|
1589
|
+
if (!REF_RE.test(ref)) throw new Error(`Invalid git ref: ${JSON.stringify(ref)}`);
|
|
1590
|
+
}
|
|
1591
|
+
function text(body) {
|
|
1592
|
+
return {
|
|
1593
|
+
content: [{
|
|
1594
|
+
type: "text",
|
|
1595
|
+
text: body
|
|
1596
|
+
}],
|
|
1597
|
+
details: void 0
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1600
|
+
async function resolveOid(dir, fs, ref) {
|
|
1601
|
+
assertRef(ref);
|
|
1602
|
+
return git.resolveRef({
|
|
1603
|
+
fs,
|
|
1604
|
+
dir,
|
|
1605
|
+
ref
|
|
1606
|
+
}).catch(() => git.expandOid({
|
|
1607
|
+
fs,
|
|
1608
|
+
dir,
|
|
1609
|
+
oid: ref
|
|
1610
|
+
}));
|
|
1611
|
+
}
|
|
1612
|
+
function decode(data) {
|
|
1613
|
+
if (data.includes(0)) return null;
|
|
1614
|
+
return Buffer.from(data).toString("utf8");
|
|
1615
|
+
}
|
|
1616
|
+
async function readFileAt(dir, fs, oid, filepath) {
|
|
1617
|
+
try {
|
|
1618
|
+
const { blob } = await git.readBlob({
|
|
1619
|
+
fs,
|
|
1620
|
+
dir,
|
|
1621
|
+
oid,
|
|
1622
|
+
filepath
|
|
1623
|
+
});
|
|
1624
|
+
return decode(blob);
|
|
1625
|
+
} catch {
|
|
1626
|
+
return null;
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
/** Unified diff of every changed file between two commit oids. */
|
|
1630
|
+
async function diffCommits(dir, fs, oldOid, newOid) {
|
|
1631
|
+
const trees = oldOid ? [git.TREE({ ref: oldOid }), git.TREE({ ref: newOid })] : [git.TREE({ ref: newOid })];
|
|
1632
|
+
const changed = await git.walk({
|
|
1633
|
+
fs,
|
|
1634
|
+
dir,
|
|
1635
|
+
trees,
|
|
1636
|
+
map: async (filepath, entries) => {
|
|
1637
|
+
if (filepath === ".") return void 0;
|
|
1638
|
+
if (!oldOid) {
|
|
1639
|
+
const [b] = entries;
|
|
1640
|
+
return b && await b.type() === "blob" ? filepath : void 0;
|
|
1641
|
+
}
|
|
1642
|
+
const [a, b] = entries;
|
|
1643
|
+
if ((a ? await a.oid() : void 0) === (b ? await b.oid() : void 0)) return void 0;
|
|
1644
|
+
const aType = a ? await a.type() : void 0;
|
|
1645
|
+
const bType = b ? await b.type() : void 0;
|
|
1646
|
+
if (aType === "tree" || bType === "tree") return void 0;
|
|
1647
|
+
return filepath;
|
|
1648
|
+
}
|
|
1649
|
+
});
|
|
1650
|
+
const patches = [];
|
|
1651
|
+
let patchBytes = 0;
|
|
1652
|
+
for (const filepath of changed.sort()) {
|
|
1653
|
+
const before = oldOid ? await readFileAt(dir, fs, oldOid, filepath) ?? "" : "";
|
|
1654
|
+
const after = await readFileAt(dir, fs, newOid, filepath) ?? "";
|
|
1655
|
+
if (before === after) continue;
|
|
1656
|
+
const patch = createTwoFilesPatch(`a/${filepath}`, `b/${filepath}`, before, after);
|
|
1657
|
+
patches.push(patch);
|
|
1658
|
+
patchBytes += patch.length + 1;
|
|
1659
|
+
if (patchBytes > MAX_PATCH_BYTES) {
|
|
1660
|
+
patches.push(`\n[diff truncated at ${MAX_PATCH_BYTES} bytes — open the file directly for the rest]`);
|
|
1661
|
+
break;
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
return patches.join("\n") || "(no textual changes)";
|
|
1665
|
+
}
|
|
1666
|
+
/**
|
|
1667
|
+
* Build the read-only git tools scoped to `dir` (the repo root). Returns an empty
|
|
1668
|
+
* array when `dir` is not a git repository, so callers can wire it
|
|
1669
|
+
* unconditionally.
|
|
1670
|
+
*/
|
|
1671
|
+
function createGitTools(dir, options = {}) {
|
|
1672
|
+
const fs = options.fs ?? nodeFs;
|
|
1673
|
+
if (!fs.existsSync(join(dir, ".git"))) return [];
|
|
1674
|
+
return [
|
|
1675
|
+
{
|
|
1676
|
+
name: "git_log",
|
|
1677
|
+
label: "git log",
|
|
1678
|
+
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.",
|
|
1679
|
+
parameters: Type.Object({
|
|
1680
|
+
since: Type.Optional(Type.String({ description: "Only list commits after this ref/sha (exclusive)." })),
|
|
1681
|
+
maxCount: Type.Optional(Type.Number({ description: "Maximum number of commits to return (default 50)." }))
|
|
1682
|
+
}),
|
|
1683
|
+
async execute(_id, params) {
|
|
1684
|
+
const depth = Math.min(Math.max(params.maxCount ?? 50, 1), 500);
|
|
1685
|
+
const commits = await git.log({
|
|
1686
|
+
fs,
|
|
1687
|
+
dir,
|
|
1688
|
+
depth
|
|
1689
|
+
});
|
|
1690
|
+
let stop = -1;
|
|
1691
|
+
if (params.since) {
|
|
1692
|
+
const sinceOid = await resolveOid(dir, fs, params.since);
|
|
1693
|
+
stop = commits.findIndex((c) => c.oid === sinceOid);
|
|
1694
|
+
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)`);
|
|
1695
|
+
}
|
|
1696
|
+
const list = (stop >= 0 ? commits.slice(0, stop) : commits).map((c) => {
|
|
1697
|
+
const subject = c.commit.message.split("\n", 1)[0];
|
|
1698
|
+
return `${c.oid.slice(0, 10)} ${subject} (${c.commit.author.name})`;
|
|
1699
|
+
});
|
|
1700
|
+
return text(list.length ? list.join("\n") : "(no commits in range)");
|
|
1701
|
+
}
|
|
1702
|
+
},
|
|
1703
|
+
{
|
|
1704
|
+
name: "git_show",
|
|
1705
|
+
label: "git show",
|
|
1706
|
+
description: "Show a commit's message and its unified diff against its first parent. Pass `ref` (a sha or ref).",
|
|
1707
|
+
parameters: Type.Object({ ref: Type.String({ description: "The commit sha or ref to show." }) }),
|
|
1708
|
+
async execute(_id, params) {
|
|
1709
|
+
const oid = await resolveOid(dir, fs, params.ref);
|
|
1710
|
+
const { commit } = await git.readCommit({
|
|
1711
|
+
fs,
|
|
1712
|
+
dir,
|
|
1713
|
+
oid
|
|
1714
|
+
});
|
|
1715
|
+
const diff = await diffCommits(dir, fs, commit.parent[0] ?? null, oid);
|
|
1716
|
+
return text(`commit ${oid}\nAuthor: ${commit.author.name} <${commit.author.email}>\n\n${commit.message.trim()}\n\n${diff}`);
|
|
1717
|
+
}
|
|
1718
|
+
},
|
|
1719
|
+
{
|
|
1720
|
+
name: "git_diff",
|
|
1721
|
+
label: "git diff",
|
|
1722
|
+
description: "Unified diff between two refs/shas. Pass `from` and `to`.",
|
|
1723
|
+
parameters: Type.Object({
|
|
1724
|
+
from: Type.String({ description: "Base ref/sha." }),
|
|
1725
|
+
to: Type.String({ description: "Target ref/sha." })
|
|
1726
|
+
}),
|
|
1727
|
+
async execute(_id, params) {
|
|
1728
|
+
return text(await diffCommits(dir, fs, await resolveOid(dir, fs, params.from), await resolveOid(dir, fs, params.to)));
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
];
|
|
1732
|
+
}
|
|
1733
|
+
//#endregion
|
|
1549
1734
|
//#region src/logger.ts
|
|
1550
1735
|
var LEVELS = {
|
|
1551
1736
|
debug: 0,
|
|
@@ -2228,11 +2413,12 @@ function atEndOfBlockComment(text, i) {
|
|
|
2228
2413
|
}
|
|
2229
2414
|
//#endregion
|
|
2230
2415
|
//#region src/verify.ts
|
|
2231
|
-
function buildVerifySystemPrompt(diff, commitLog) {
|
|
2416
|
+
function buildVerifySystemPrompt(diff, commitLog, staged) {
|
|
2417
|
+
const diskMode = staged !== void 0 && staged.length > 0;
|
|
2232
2418
|
const parts = [
|
|
2233
2419
|
"You are a strict, adversarial verifier of a SINGLE code-review finding. Your job is to REFUTE the finding, not to agree with it.",
|
|
2234
2420
|
"",
|
|
2235
|
-
"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.",
|
|
2421
|
+
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.",
|
|
2236
2422
|
"",
|
|
2237
2423
|
"Apply this bar:",
|
|
2238
2424
|
"- 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.",
|
|
@@ -2248,7 +2434,10 @@ function buildVerifySystemPrompt(diff, commitLog) {
|
|
|
2248
2434
|
"- \"drop\": not a real defect — speculative, stylistic, contradicted by the code/comments, or based on external state not visible in the diff."
|
|
2249
2435
|
];
|
|
2250
2436
|
if (commitLog?.trim()) parts.push("", `Commit messages for this change (oldest first):\n<commits>\n${commitLog.trim()}\n</commits>`);
|
|
2251
|
-
|
|
2437
|
+
if (diskMode) {
|
|
2438
|
+
const list = staged.map((f) => `- ${f.path} → ${f.diskPath}`).join("\n");
|
|
2439
|
+
parts.push("", `The change is staged on disk — open the relevant file(s) to verify:\n<staged_files>\n${list}\n</staged_files>`);
|
|
2440
|
+
} else parts.push("", `Verify each finding against this diff:\n<diff>\n${diff}\n</diff>`);
|
|
2252
2441
|
return parts.join("\n");
|
|
2253
2442
|
}
|
|
2254
2443
|
function buildVerifyUserPrompt(comment) {
|
|
@@ -3037,18 +3226,18 @@ function gitSkillCacheKey(url, ref) {
|
|
|
3037
3226
|
* remote's default branch via `HEAD`.
|
|
3038
3227
|
*/
|
|
3039
3228
|
async function gitShallowClone(url, ref, dir) {
|
|
3040
|
-
await git([
|
|
3229
|
+
await git$1([
|
|
3041
3230
|
"init",
|
|
3042
3231
|
"--quiet",
|
|
3043
3232
|
dir
|
|
3044
3233
|
]);
|
|
3045
|
-
await git([
|
|
3234
|
+
await git$1([
|
|
3046
3235
|
"remote",
|
|
3047
3236
|
"add",
|
|
3048
3237
|
"origin",
|
|
3049
3238
|
url
|
|
3050
3239
|
], { cwd: dir });
|
|
3051
|
-
await git([
|
|
3240
|
+
await git$1([
|
|
3052
3241
|
"fetch",
|
|
3053
3242
|
"--depth",
|
|
3054
3243
|
"1",
|
|
@@ -3056,7 +3245,7 @@ async function gitShallowClone(url, ref, dir) {
|
|
|
3056
3245
|
"origin",
|
|
3057
3246
|
ref || "HEAD"
|
|
3058
3247
|
], { cwd: dir });
|
|
3059
|
-
await git([
|
|
3248
|
+
await git$1([
|
|
3060
3249
|
"checkout",
|
|
3061
3250
|
"--quiet",
|
|
3062
3251
|
"FETCH_HEAD"
|
|
@@ -3198,11 +3387,18 @@ async function cleanupSkippedDiffs(cwd) {
|
|
|
3198
3387
|
});
|
|
3199
3388
|
}
|
|
3200
3389
|
/**
|
|
3201
|
-
* Render the `<skipped_files>` block
|
|
3202
|
-
*
|
|
3390
|
+
* Render the `<skipped_files>` block: each file with its on-disk diff path and an
|
|
3391
|
+
* instruction to read them.
|
|
3392
|
+
*
|
|
3393
|
+
* - `partial` (retrieval mode): only budget-dropped files are staged; the rest
|
|
3394
|
+
* of the diff is inline, so the agent reads the highest-risk staged files.
|
|
3395
|
+
* - `full` (disk input mode): the ENTIRE change is staged — nothing is inline —
|
|
3396
|
+
* so reading is not optional; the review is only as good as what gets opened.
|
|
3203
3397
|
*/
|
|
3204
|
-
function renderRetrievableSkippedBlock(files) {
|
|
3205
|
-
|
|
3398
|
+
function renderRetrievableSkippedBlock(files, mode = "partial") {
|
|
3399
|
+
const list = files.map((f) => `- ${f.path} → ${f.diskPath}`).join("\n");
|
|
3400
|
+
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.`;
|
|
3401
|
+
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.`;
|
|
3206
3402
|
}
|
|
3207
3403
|
//#endregion
|
|
3208
3404
|
//#region src/triage.ts
|
|
@@ -3494,8 +3690,15 @@ async function loadReviewContext(cwd, skillNames = [], warn, options = {}) {
|
|
|
3494
3690
|
]);
|
|
3495
3691
|
const skills = [...discovered];
|
|
3496
3692
|
const discoveredNames = new Set(discovered.map((s) => s.name));
|
|
3497
|
-
const named = await Promise.all(skillNames.filter((n) => !discoveredNames.has(n)).map((n) =>
|
|
3498
|
-
|
|
3693
|
+
const named = await Promise.all(skillNames.filter((n) => !discoveredNames.has(n)).map(async (n) => {
|
|
3694
|
+
try {
|
|
3695
|
+
return await loadNamedSkill(n, cwd, { refresh: options.refreshGitSkills });
|
|
3696
|
+
} catch (error) {
|
|
3697
|
+
warn?.(`Skipping skill "${n}": ${formatError(error)}`);
|
|
3698
|
+
return null;
|
|
3699
|
+
}
|
|
3700
|
+
}));
|
|
3701
|
+
skills.push(...named.filter((s) => s !== null));
|
|
3499
3702
|
return {
|
|
3500
3703
|
conventions,
|
|
3501
3704
|
reviewRules,
|
|
@@ -3591,13 +3794,22 @@ function filterDiff(raw, maxChars = DEFAULT_MAX_DIFF_CHARS) {
|
|
|
3591
3794
|
totalChars += section.length;
|
|
3592
3795
|
reviewedChangedLines += changedLines;
|
|
3593
3796
|
}
|
|
3797
|
+
const allSections = kept.map((section) => {
|
|
3798
|
+
const filePath = parseFilePath(section.split("\n", 1)[0] ?? "");
|
|
3799
|
+
return filePath ? {
|
|
3800
|
+
path: filePath,
|
|
3801
|
+
section,
|
|
3802
|
+
changedLines: countChangedLines(section)
|
|
3803
|
+
} : null;
|
|
3804
|
+
}).filter((entry) => entry !== null);
|
|
3594
3805
|
return {
|
|
3595
3806
|
diff: included.join(""),
|
|
3596
3807
|
noiseSkippedFiles,
|
|
3597
3808
|
sizeSkippedFiles,
|
|
3598
3809
|
reviewedChangedLines,
|
|
3599
3810
|
skippedChangedLines,
|
|
3600
|
-
sizeSkippedSections
|
|
3811
|
+
sizeSkippedSections,
|
|
3812
|
+
allSections
|
|
3601
3813
|
};
|
|
3602
3814
|
}
|
|
3603
3815
|
function mergeContent(files) {
|
|
@@ -3803,13 +4015,23 @@ function buildAngleSystemPrompt(context, minSeverity, angle) {
|
|
|
3803
4015
|
"</review_angle>"
|
|
3804
4016
|
].join("\n");
|
|
3805
4017
|
}
|
|
3806
|
-
function buildUserPrompt(diff, skippedFiles = [], commitLog, priorThreads, intent, coverage, retrievableSkipped) {
|
|
4018
|
+
function buildUserPrompt(diff, skippedFiles = [], commitLog, priorThreads, intent, coverage, retrievableSkipped, omitInlineDiff = false, commitExploration) {
|
|
3807
4019
|
const parts = [];
|
|
3808
4020
|
const intentBlock = renderIntentBlock(intent);
|
|
3809
4021
|
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}`);
|
|
3810
4022
|
if (commitLog?.trim()) parts.push(`Commits in this MR (oldest first):\n<commits>\n${commitLog.trim()}\n</commits>`);
|
|
3811
|
-
|
|
3812
|
-
|
|
4023
|
+
if (commitExploration) {
|
|
4024
|
+
const since = commitExploration.sinceRef;
|
|
4025
|
+
parts.push([
|
|
4026
|
+
"This review has NO diff inline. Explore the change with the read-only git tools:",
|
|
4027
|
+
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.",
|
|
4028
|
+
"- `git_show <sha>` shows a commit's message and full diff. Open every commit in scope.",
|
|
4029
|
+
"- `git_diff <from> <to>` shows the combined diff between two points if you prefer to review the range at once.",
|
|
4030
|
+
"A commit you do not open is code you did not review. In your summary, state which commits you reviewed."
|
|
4031
|
+
].join("\n"));
|
|
4032
|
+
}
|
|
4033
|
+
if (!omitInlineDiff && !commitExploration) parts.push(`Review this diff:\n<diff>\n${diff}\n</diff>`);
|
|
4034
|
+
if (retrievableSkipped && retrievableSkipped.length > 0) parts.push(renderRetrievableSkippedBlock(retrievableSkipped, omitInlineDiff ? "full" : "partial"));
|
|
3813
4035
|
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.`);
|
|
3814
4036
|
if (coverage && coverage.totalLines > 0 && coverage.reviewedLines < coverage.totalLines) {
|
|
3815
4037
|
const pct = Math.round(coverage.reviewedLines / coverage.totalLines * 100);
|
|
@@ -4029,33 +4251,86 @@ function accumulateUsage(target, message, modelId) {
|
|
|
4029
4251
|
addUsageToBucket(bucket, message);
|
|
4030
4252
|
}
|
|
4031
4253
|
}
|
|
4254
|
+
/**
|
|
4255
|
+
* The "consider decomposing this MR" hint: present when a threshold is set
|
|
4256
|
+
* (`> 0`) and the change exceeds it. `changedLines` is the count the caller
|
|
4257
|
+
* deems reviewed — the budget-fitted subset for inline mode, the whole change
|
|
4258
|
+
* for disk/commits mode.
|
|
4259
|
+
*/
|
|
4260
|
+
function resolveDecomposeHint(threshold, reviewedLines) {
|
|
4261
|
+
return threshold > 0 && reviewedLines > threshold ? {
|
|
4262
|
+
lines: reviewedLines,
|
|
4263
|
+
threshold
|
|
4264
|
+
} : void 0;
|
|
4265
|
+
}
|
|
4032
4266
|
async function runReview(config, options) {
|
|
4033
4267
|
const cwd = options.cwd ?? config.cwd;
|
|
4034
4268
|
const minSeverity = toGitLabReviewSeverity(config.minSeverity);
|
|
4035
4269
|
const logger = options.logger ?? noopLogger;
|
|
4270
|
+
const inputMode = config.inputMode ?? "auto";
|
|
4271
|
+
const gitTools = createGitTools(cwd);
|
|
4272
|
+
let commitsMode = inputMode === "commits";
|
|
4273
|
+
if (commitsMode && gitTools.length === 0) {
|
|
4274
|
+
logger.warn("Commit-exploration input mode requires a git checkout, but none was found in cwd; falling back to auto input mode.");
|
|
4275
|
+
commitsMode = false;
|
|
4276
|
+
}
|
|
4277
|
+
const effectiveMode = commitsMode ? "commits" : inputMode === "commits" ? "auto" : inputMode;
|
|
4036
4278
|
const maxDiffChars = config.maxDiffChars > 0 ? config.maxDiffChars : DEFAULT_MAX_DIFF_CHARS;
|
|
4037
|
-
const { diff, noiseSkippedFiles, sizeSkippedFiles, reviewedChangedLines, skippedChangedLines, sizeSkippedSections } = filterDiff(options.diff, maxDiffChars);
|
|
4038
|
-
|
|
4039
|
-
const
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4279
|
+
const { diff, noiseSkippedFiles, sizeSkippedFiles, reviewedChangedLines, skippedChangedLines, sizeSkippedSections, allSections } = filterDiff(options.diff, maxDiffChars);
|
|
4280
|
+
const overflowed = sizeSkippedSections.length > 0;
|
|
4281
|
+
const diskMode = effectiveMode === "disk" || effectiveMode === "auto" && overflowed;
|
|
4282
|
+
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.");
|
|
4283
|
+
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." });
|
|
4284
|
+
const decomposeHint = resolveDecomposeHint(config.decomposeHintLines, diskMode || commitsMode ? reviewedChangedLines + skippedChangedLines : reviewedChangedLines);
|
|
4285
|
+
let promptDiff;
|
|
4286
|
+
let promptSkippedFiles;
|
|
4287
|
+
let promptCoverage;
|
|
4288
|
+
let retrievableSkipped;
|
|
4289
|
+
let sizeNotice;
|
|
4290
|
+
if (commitsMode) {
|
|
4291
|
+
retrievableSkipped = [];
|
|
4292
|
+
promptDiff = "";
|
|
4293
|
+
promptSkippedFiles = [];
|
|
4294
|
+
promptCoverage = void 0;
|
|
4295
|
+
sizeNotice = {
|
|
4296
|
+
sizeSkippedFiles: [],
|
|
4297
|
+
decomposeHint,
|
|
4298
|
+
retrieved: false
|
|
4299
|
+
};
|
|
4300
|
+
logger.info(options.sinceRef ? `Commit-exploration input mode: reviewing commits since ${options.sinceRef}.` : "Commit-exploration input mode: reviewing the full commit range.");
|
|
4301
|
+
} else if (diskMode) {
|
|
4302
|
+
retrievableSkipped = allSections.length > 0 ? await writeSkippedDiffs(cwd, allSections.map(({ path, section }) => ({
|
|
4303
|
+
path,
|
|
4304
|
+
section
|
|
4305
|
+
}))) : [];
|
|
4306
|
+
if (retrievableSkipped.length > 0) logger.info(`Disk input mode: staged ${retrievableSkipped.length} file diff(s) for on-demand review.`);
|
|
4307
|
+
promptDiff = "";
|
|
4308
|
+
promptSkippedFiles = [];
|
|
4309
|
+
promptCoverage = void 0;
|
|
4310
|
+
sizeNotice = {
|
|
4311
|
+
sizeSkippedFiles: [],
|
|
4312
|
+
decomposeHint,
|
|
4313
|
+
retrieved: false
|
|
4314
|
+
};
|
|
4315
|
+
} else {
|
|
4316
|
+
promptSkippedFiles = [...sizeSkippedFiles.map((f) => f.path), ...noiseSkippedFiles];
|
|
4317
|
+
promptCoverage = sizeSkippedFiles.length > 0 ? {
|
|
4318
|
+
reviewedLines: reviewedChangedLines,
|
|
4319
|
+
totalLines: reviewedChangedLines + skippedChangedLines
|
|
4320
|
+
} : void 0;
|
|
4321
|
+
retrievableSkipped = config.retrieveSkipped && sizeSkippedSections.length > 0 ? await writeSkippedDiffs(cwd, sizeSkippedSections) : [];
|
|
4322
|
+
if (retrievableSkipped.length > 0) logger.info(`Staged ${retrievableSkipped.length} dropped-file diff(s) on disk for retrieval.`);
|
|
4323
|
+
promptDiff = diff;
|
|
4324
|
+
sizeNotice = {
|
|
4325
|
+
sizeSkippedFiles,
|
|
4326
|
+
decomposeHint,
|
|
4327
|
+
coverage: promptCoverage,
|
|
4328
|
+
retrieved: retrievableSkipped.length > 0
|
|
4329
|
+
};
|
|
4330
|
+
}
|
|
4056
4331
|
const context = await loadReviewContext(cwd, config.skills, (msg) => logger.warn(msg), { refreshGitSkills: config.refreshGitSkills });
|
|
4057
4332
|
const systemPrompt = buildJSONSystemPrompt(context, minSeverity);
|
|
4058
|
-
const userPrompt = buildUserPrompt(
|
|
4333
|
+
const userPrompt = buildUserPrompt(promptDiff, promptSkippedFiles, options.commitLog, options.priorThreads, options.intent, promptCoverage, retrievableSkipped, diskMode, commitsMode ? { sinceRef: options.sinceRef } : void 0);
|
|
4059
4334
|
const skillNames = context.skills.map((s) => s.name);
|
|
4060
4335
|
if (skillNames.length > 0) logger.debug(`Skills loaded: ${skillNames.join(", ")}`);
|
|
4061
4336
|
if (context.conventions.length > 0) logger.debug(`Conventions: ${context.conventions.map((f) => f.path).join(", ")}`);
|
|
@@ -4063,7 +4338,7 @@ async function runReview(config, options) {
|
|
|
4063
4338
|
const pool = buildEffectivePool(config, logger);
|
|
4064
4339
|
if (pool.length > 1) logger.info(`Model pool: ${pool.map((m) => m.id).join(", ")}.`);
|
|
4065
4340
|
const primary = pool[0];
|
|
4066
|
-
const tools = createReadOnlyTools(cwd);
|
|
4341
|
+
const tools = [...createReadOnlyTools(cwd), ...gitTools];
|
|
4067
4342
|
const createAgent = options.createAgent ?? defaultCreateAgent;
|
|
4068
4343
|
const timeoutMs = options.timeoutMs ?? DEFAULT_REVIEW_TIMEOUT_MS;
|
|
4069
4344
|
const aggregated = emptyUsage();
|
|
@@ -4075,7 +4350,9 @@ async function runReview(config, options) {
|
|
|
4075
4350
|
timeoutMs,
|
|
4076
4351
|
logger,
|
|
4077
4352
|
aggregated,
|
|
4078
|
-
verifyMember: resolveVerifyMember(config, primary, logger)
|
|
4353
|
+
verifyMember: resolveVerifyMember(config, primary, logger),
|
|
4354
|
+
attachTelemetry: options.attachTelemetry,
|
|
4355
|
+
verifyStaged: diskMode ? retrievableSkipped : void 0
|
|
4079
4356
|
};
|
|
4080
4357
|
let outputText;
|
|
4081
4358
|
if (config.reviewDepth === "full") {
|
|
@@ -4241,6 +4518,7 @@ async function runMultiAngleFind(context, minSeverity, userPrompt, deps) {
|
|
|
4241
4518
|
thinkingLevel: deps.thinkingLevel,
|
|
4242
4519
|
getApiKey: member.getApiKey
|
|
4243
4520
|
});
|
|
4521
|
+
const detachTelemetry = deps.attachTelemetry?.(agent);
|
|
4244
4522
|
try {
|
|
4245
4523
|
const parsed = parseReviewMarkdownWithWarnings(await runAgentToCompletion(agent, userPrompt, {
|
|
4246
4524
|
timeoutMs: deps.timeoutMs,
|
|
@@ -4254,6 +4532,8 @@ async function runMultiAngleFind(context, minSeverity, userPrompt, deps) {
|
|
|
4254
4532
|
summaries[index] = parsed.summary;
|
|
4255
4533
|
} catch (error) {
|
|
4256
4534
|
deps.logger.warn(`Find angle "${angle.key}" failed: ${error.message}; skipping.`);
|
|
4535
|
+
} finally {
|
|
4536
|
+
detachTelemetry?.();
|
|
4257
4537
|
}
|
|
4258
4538
|
}), FIND_CONCURRENCY);
|
|
4259
4539
|
const raw = groups.reduce((total, group) => total + group.length, 0);
|
|
@@ -4279,7 +4559,7 @@ async function verifyAndSynthesize(findings, summary, diff, commitLog, deps) {
|
|
|
4279
4559
|
})).filter(({ finding }) => finding.comment.severity === "critical" || finding.comment.severity === "warn");
|
|
4280
4560
|
const verdicts = /* @__PURE__ */ new Map();
|
|
4281
4561
|
if (severe.length > 0) {
|
|
4282
|
-
const verifySystemPrompt = buildVerifySystemPrompt(diff, commitLog);
|
|
4562
|
+
const verifySystemPrompt = buildVerifySystemPrompt(diff, commitLog, deps.verifyStaged);
|
|
4283
4563
|
await runBounded(severe.map(({ finding, index }) => async () => {
|
|
4284
4564
|
const comment = finding.comment;
|
|
4285
4565
|
const verifierMember = deps.verifyMember ?? pickVerifier(deps.pool, finding.authorModel);
|
|
@@ -4290,6 +4570,7 @@ async function verifyAndSynthesize(findings, summary, diff, commitLog, deps) {
|
|
|
4290
4570
|
thinkingLevel: deps.thinkingLevel,
|
|
4291
4571
|
getApiKey: verifierMember.getApiKey
|
|
4292
4572
|
});
|
|
4573
|
+
const detachTelemetry = deps.attachTelemetry?.(verifier);
|
|
4293
4574
|
try {
|
|
4294
4575
|
const text = await runAgentToCompletion(verifier, buildVerifyUserPrompt(comment), {
|
|
4295
4576
|
timeoutMs: deps.timeoutMs,
|
|
@@ -4302,6 +4583,8 @@ async function verifyAndSynthesize(findings, summary, diff, commitLog, deps) {
|
|
|
4302
4583
|
decision: "keep",
|
|
4303
4584
|
reason: "verifier error; finding kept"
|
|
4304
4585
|
});
|
|
4586
|
+
} finally {
|
|
4587
|
+
detachTelemetry?.();
|
|
4305
4588
|
}
|
|
4306
4589
|
}), VERIFY_CONCURRENCY);
|
|
4307
4590
|
}
|
|
@@ -4932,7 +5215,7 @@ async function loadDefaultRuntime() {
|
|
|
4932
5215
|
const [sdkNode, resources, semconv] = modules;
|
|
4933
5216
|
const serviceResource = resources.resourceFromAttributes({
|
|
4934
5217
|
[semconv.ATTR_SERVICE_NAME ?? "service.name"]: SERVICE_NAME,
|
|
4935
|
-
[semconv.ATTR_SERVICE_VERSION ?? "service.version"]: "0.8.
|
|
5218
|
+
[semconv.ATTR_SERVICE_VERSION ?? "service.version"]: "0.8.6"
|
|
4936
5219
|
});
|
|
4937
5220
|
process.env.OTEL_METRICS_EXPORTER = process.env.OTEL_METRICS_EXPORTER ?? "otlp";
|
|
4938
5221
|
process.env.OTEL_LOGS_EXPORTER = process.env.OTEL_LOGS_EXPORTER ?? "otlp";
|
|
@@ -5364,7 +5647,7 @@ function boldCommentTitle(body) {
|
|
|
5364
5647
|
*/
|
|
5365
5648
|
function buildCommentBody(body, commitSha, confidence) {
|
|
5366
5649
|
const confidenceLine = `_Confidence: ${confidence}._`;
|
|
5367
|
-
const footer = `<sub>Reviewed by ${PRODUCT_LINK} v0.8.
|
|
5650
|
+
const footer = `<sub>Reviewed by ${PRODUCT_LINK} v0.8.6 for commit ${commitSha}.</sub>`;
|
|
5368
5651
|
return `${boldCommentTitle(body.trim())}\n\n${confidenceLine}\n\n---\n\n${footer}`;
|
|
5369
5652
|
}
|
|
5370
5653
|
function buildPayload(comment, body, refs, resolved) {
|
|
@@ -5528,11 +5811,14 @@ function reviewCommentPosition(comment) {
|
|
|
5528
5811
|
* comments that render identically on GitHub, so `extractExistingFingerprints`,
|
|
5529
5812
|
* `findExistingSummaryNote`, and the reviewed-commit scan all work as-is.
|
|
5530
5813
|
*
|
|
5531
|
-
* `
|
|
5532
|
-
* threads (from the GraphQL `reviewThreads` query, since
|
|
5533
|
-
*
|
|
5814
|
+
* `settledCommentIds` carries the database ids of comments in settled review
|
|
5815
|
+
* threads — resolved or outdated (from the GraphQL `reviewThreads` query, since
|
|
5816
|
+
* REST omits both). Each such note gets a `resolved` flag mirroring GitLab's
|
|
5817
|
+
* per-note field: an outdated GitHub thread maps to `resolved: true` because
|
|
5818
|
+
* GitHub, unlike GitLab, does not auto-resolve a thread when its anchored line
|
|
5819
|
+
* changes, so treating outdated as resolved matches GitLab's behaviour (#133).
|
|
5534
5820
|
*/
|
|
5535
|
-
function normalizeGitHubDiscussions(reviewComments, issueComments,
|
|
5821
|
+
function normalizeGitHubDiscussions(reviewComments, issueComments, settledCommentIds = /* @__PURE__ */ new Set()) {
|
|
5536
5822
|
const threads = /* @__PURE__ */ new Map();
|
|
5537
5823
|
const order = [];
|
|
5538
5824
|
for (const comment of reviewComments) {
|
|
@@ -5546,7 +5832,7 @@ function normalizeGitHubDiscussions(reviewComments, issueComments, resolvedComme
|
|
|
5546
5832
|
notes.push({
|
|
5547
5833
|
id: comment.id,
|
|
5548
5834
|
body: comment.body ?? "",
|
|
5549
|
-
resolved:
|
|
5835
|
+
resolved: settledCommentIds.has(comment.id),
|
|
5550
5836
|
position: reviewCommentPosition(comment)
|
|
5551
5837
|
});
|
|
5552
5838
|
}
|
|
@@ -5613,12 +5899,12 @@ var GitHubPlatform = class {
|
|
|
5613
5899
|
return refs;
|
|
5614
5900
|
}
|
|
5615
5901
|
async getDiscussions() {
|
|
5616
|
-
const [reviewComments, issueComments,
|
|
5902
|
+
const [reviewComments, issueComments, settledCommentIds] = await Promise.all([
|
|
5617
5903
|
this.client.listReviewComments(this.owner, this.repo, this.pull),
|
|
5618
5904
|
this.client.listIssueComments(this.owner, this.repo, this.pull),
|
|
5619
|
-
this.client.
|
|
5905
|
+
this.client.listSettledReviewCommentIds(this.owner, this.repo, this.pull)
|
|
5620
5906
|
]);
|
|
5621
|
-
return normalizeGitHubDiscussions(reviewComments, issueComments,
|
|
5907
|
+
return normalizeGitHubDiscussions(reviewComments, issueComments, settledCommentIds);
|
|
5622
5908
|
}
|
|
5623
5909
|
buildComments(comments, diff, refs, existingFingerprints) {
|
|
5624
5910
|
this.commitId = refs.head_sha;
|
|
@@ -6099,6 +6385,11 @@ Options:
|
|
|
6099
6385
|
--review-depth <depth> single (one pass), verify (adversarial re-check of each
|
|
6100
6386
|
severe finding), or full (multi-angle finders → triage →
|
|
6101
6387
|
verify). (default: single; env: CODE_REVIEW_DEPTH)
|
|
6388
|
+
--input-mode <mode> How the change is fed to the reviewer: auto (inline while it
|
|
6389
|
+
fits the budget, disk on overflow), inline (diff in the prompt),
|
|
6390
|
+
disk (every file diff staged on disk; the agent reads on demand),
|
|
6391
|
+
or commits (agent explores via read-only git tools).
|
|
6392
|
+
(default: auto; env: CODE_REVIEW_INPUT_MODE)
|
|
6102
6393
|
--verify-model <p/id> Model for the Verify stage (verify/full depth). Pairs a cheap
|
|
6103
6394
|
finder with a strong, high-precision verifier. Warns if it looks
|
|
6104
6395
|
cheaper than --model. Empty (default) = pool selection.
|
|
@@ -6205,7 +6496,8 @@ async function run(config, bridges) {
|
|
|
6205
6496
|
description: mr.description
|
|
6206
6497
|
},
|
|
6207
6498
|
logger,
|
|
6208
|
-
attachTelemetry: bridges?.otel?.createAgentTelemetry(runId)
|
|
6499
|
+
attachTelemetry: bridges?.otel?.createAgentTelemetry(runId),
|
|
6500
|
+
sinceRef: config.inputMode === "commits" && reviewedCommitSha && reviewedCommitSha !== refs.head_sha ? reviewedCommitSha : void 0
|
|
6209
6501
|
});
|
|
6210
6502
|
context.usage = result;
|
|
6211
6503
|
return result;
|
|
@@ -6452,10 +6744,10 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
6452
6744
|
return;
|
|
6453
6745
|
}
|
|
6454
6746
|
if (argv.includes("--version") || argv.includes("-v")) {
|
|
6455
|
-
console.log("0.8.
|
|
6747
|
+
console.log("0.8.6");
|
|
6456
6748
|
return;
|
|
6457
6749
|
}
|
|
6458
|
-
process.stderr.write(`[code-review] @weareikko/code-review v0.8.
|
|
6750
|
+
process.stderr.write(`[code-review] @weareikko/code-review v0.8.6\n`);
|
|
6459
6751
|
assertNodeVersion();
|
|
6460
6752
|
applyCodeReviewEnvPrefix();
|
|
6461
6753
|
applyDefaultCacheRetention();
|
|
@@ -6478,4 +6770,4 @@ if (isDirectRun()) main().catch((error) => {
|
|
|
6478
6770
|
//#endregion
|
|
6479
6771
|
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 };
|
|
6480
6772
|
|
|
6481
|
-
//# sourceMappingURL=cli-
|
|
6773
|
+
//# sourceMappingURL=cli-BbR1oftT.js.map
|