@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.
@@ -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 **resolved**
339
- * review thread. GitHub's REST comment endpoints omit thread-resolution state;
340
- * it is only exposed via GraphQL `reviewThreads.isResolved`. Callers use this
341
- * set to mark normalized notes resolved so resolved threads are excluded from
342
- * summary carry-over and prior-thread context. Paginates over threads.
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 listResolvedReviewCommentIds(owner, repo, pull) {
345
- const resolved = /* @__PURE__ */ new Set();
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") resolved.add(comment.databaseId);
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 resolved;
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.4 for commit ${commitSha}.`;
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
- parts.push("", `Verify each finding against this diff:\n<diff>\n${diff}\n</diff>`);
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 for the retrieval mode: each dropped file
3202
- * with its on-disk diff path and an instruction to read the highest-risk ones.
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
- return `<skipped_files>\n${files.map((f) => `- ${f.path} → ${f.diskPath}`).join("\n")}\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.`;
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) => loadNamedSkill(n, cwd, { refresh: options.refreshGitSkills })));
3498
- skills.push(...named);
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
- parts.push(`Review this diff:\n<diff>\n${diff}\n</diff>`);
3812
- if (retrievableSkipped && retrievableSkipped.length > 0) parts.push(renderRetrievableSkippedBlock(retrievableSkipped));
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
- if (!diff.trim()) throw new ReviewerError("No reviewable diff content after filtering noise files.", { hint: "Ensure the merge request introduces changes outside of generated/lock files." });
4039
- const skippedFiles = [...sizeSkippedFiles.map((f) => f.path), ...noiseSkippedFiles];
4040
- const decomposeHint = config.decomposeHintLines > 0 && reviewedChangedLines > config.decomposeHintLines ? {
4041
- lines: reviewedChangedLines,
4042
- threshold: config.decomposeHintLines
4043
- } : void 0;
4044
- const coverage = sizeSkippedFiles.length > 0 ? {
4045
- reviewedLines: reviewedChangedLines,
4046
- totalLines: reviewedChangedLines + skippedChangedLines
4047
- } : void 0;
4048
- const retrievableSkipped = config.retrieveSkipped && sizeSkippedSections.length > 0 ? await writeSkippedDiffs(cwd, sizeSkippedSections) : [];
4049
- if (retrievableSkipped.length > 0) logger.info(`Staged ${retrievableSkipped.length} dropped-file diff(s) on disk for retrieval.`);
4050
- const sizeNotice = {
4051
- sizeSkippedFiles,
4052
- decomposeHint,
4053
- coverage,
4054
- retrieved: retrievableSkipped.length > 0
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(diff, skippedFiles, options.commitLog, options.priorThreads, options.intent, coverage, retrievableSkipped);
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.4"
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.4 for commit ${commitSha}.</sub>`;
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
- * `resolvedCommentIds` carries the database ids of comments in resolved review
5532
- * threads (from the GraphQL `reviewThreads` query, since REST omits resolution),
5533
- * so each note gets a `resolved` flag mirroring GitLab's per-note field.
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, resolvedCommentIds = /* @__PURE__ */ new Set()) {
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: resolvedCommentIds.has(comment.id),
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, resolvedCommentIds] = await Promise.all([
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.listResolvedReviewCommentIds(this.owner, this.repo, this.pull)
5905
+ this.client.listSettledReviewCommentIds(this.owner, this.repo, this.pull)
5620
5906
  ]);
5621
- return normalizeGitHubDiscussions(reviewComments, issueComments, resolvedCommentIds);
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.4");
6747
+ console.log("0.8.6");
6456
6748
  return;
6457
6749
  }
6458
- process.stderr.write(`[code-review] @weareikko/code-review v0.8.4\n`);
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-FDpPZaNn.js.map
6773
+ //# sourceMappingURL=cli-BbR1oftT.js.map