pullfrog 0.1.102 → 0.1.104

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.mjs CHANGED
@@ -112919,7 +112919,7 @@ var import_picocolors = __toESM(require_picocolors(), 1);
112919
112919
  // package.json
112920
112920
  var package_default = {
112921
112921
  name: "pullfrog",
112922
- version: "0.1.102",
112922
+ version: "0.1.104",
112923
112923
  cliContractVersion: "1",
112924
112924
  type: "module",
112925
112925
  bin: {
@@ -117052,7 +117052,7 @@ var providers2 = {
117052
117052
  },
117053
117053
  // Zen's FREE open-weights preview (listed 2026-10-05), the eighth promo.
117054
117054
  // Zen's docs: no training, 30-day retention for safety. hidden until a
117055
- // published release carries the alias.
117055
+ // published release carries the alias; 0.1.102 is the first.
117056
117056
  beam: {
117057
117057
  displayName: "Beam",
117058
117058
  description: "Preview, free for a limited time",
@@ -117060,8 +117060,7 @@ var providers2 = {
117060
117060
  effort: ["low", "medium", "high", "xhigh", "max"],
117061
117061
  // free to run, still gated on the provider's own OPENCODE_API_KEY —
117062
117062
  // see the big-pickle note above (#1077).
117063
- isFree: true,
117064
- hidden: true
117063
+ isFree: true
117065
117064
  },
117066
117065
  // Zen's live free MiMo, and the second free row in a menu that big-pickle
117067
117066
  // was alone in since `mimo-v2-pro-free` lost its model.
@@ -117434,10 +117433,12 @@ var providers2 = {
117434
117433
  openRouterResolve: "openrouter/openai/gpt-5.4",
117435
117434
  fallback: "openrouter/gpt-sol"
117436
117435
  },
117436
+ // OpenRouter dropped `openai/o4-mini` on 2026-10-28 (only `:batch` remains) — folds stored pins to Luna.
117437
117437
  "o4-mini": {
117438
117438
  displayName: "O4 Mini",
117439
117439
  resolve: "openrouter/openai/o4-mini",
117440
- openRouterResolve: "openrouter/openai/o4-mini"
117440
+ openRouterResolve: "openrouter/openai/o4-mini",
117441
+ fallback: "openrouter/gpt-luna"
117441
117442
  },
117442
117443
  "gemini-pro": {
117443
117444
  displayName: "Gemini Pro",
@@ -119564,7 +119565,7 @@ ${PR_SUMMARY_FORMAT}`
119564
119565
 
119565
119566
  2. **checkout**: call \`${t2("checkout_pr")}\` \u2014 this returns PR metadata, \`diffPath\` (full diff), \`incrementalDiffPath\` (changes since last reviewed version, if available), and a supplemental \`impactPath\` when change-impact extraction is enabled.
119566
119567
 
119567
- 3. **incremental scope**: if \`incrementalDiffPath\` is present, read it FIRST to see what changed since the last review. this is a range-diff that isolates the net changes, filtering out base branch noise. then read the authoritative full diff end-to-end, beginning with the TOC and using its line ranges as your coverage checklist. if no incremental diff is present, start with the full-diff TOC, determine what changed since Pullfrog's most recent review, and complete the raw-diff read. only after establishing that authoritative scope and completing raw-diff coverage, use \`impactPath\` as an explicitly incomplete list of reference leads; it never replaces raw-diff reading or establishes coverage.
119568
+ 3. **incremental scope**: if \`incrementalDiffPath\` is present, it is your coverage checklist: read it FIRST and in full. it is a range-diff of the net changes since the last review, free of base-branch noise. read only the \`diffPath\` ranges the delta needs for context \u2014 do not re-read the whole PR. if no incremental diff is present, start with the full-diff TOC, determine what changed since Pullfrog's most recent review, and complete the raw-diff read. only after that, use \`impactPath\` as an explicitly incomplete list of reference leads; it never replaces raw-diff reading or establishes coverage.
119568
119569
 
119569
119570
  4. **prior feedback \u2014 read AND retire it**: fetch previous reviews via \`${t2("list_pull_request_reviews")}\`, then call \`${t2("get_review_comments")}\` on each prior Pullfrog review. Each thread renders as a section whose first line is a fenced tag \`comment author=<login> id=<fullDatabaseId> review=<reviewId> thread=<graphqlId>\`; section headers carry \`[RESOLVED]\` / \`[OUTDATED]\` when relevant. For every **open, Pullfrog-originated** thread, decide and act:
119570
119571
 
@@ -119580,13 +119581,13 @@ ${PR_SUMMARY_FORMAT}`
119580
119581
 
119581
119582
  skip the deeper pass and jump to step 10's non-substantive path (do NOT submit a review) only when the incremental changes have **no behavioral surface at all** \u2014 formatting, comment tweaks, import reordering, lockfile regen, a mechanical rename of import paths. line count is not the signal: a one-line change to auth, money, SQL, a comparison operator, a redirect, or a config default is not trivial.
119582
119583
 
119583
- 6. **specialist decision**: after covering the incremental and full diffs, name the questions about the new changes that you still cannot answer confidently yourself, and dispatch one \`${REVIEWER_AGENT_NAME}\` specialist per question. a question qualifies only when a specialist could return evidence that **changes your disposition** on the PR \u2014 generic requests for another look, extra confidence, or polish do not. most incremental reviews need zero or one, especially thread-reply re-reviews; some need several.
119584
+ 6. **specialist decision**: after covering the delta, name the questions about the new changes that you still cannot answer confidently yourself, and dispatch one \`${REVIEWER_AGENT_NAME}\` specialist per question. a question qualifies only when a specialist could return evidence that **changes your disposition** on the PR \u2014 generic requests for another look, extra confidence, or polish do not. most incremental reviews need zero or one, especially thread-reply re-reviews; some need several.
119584
119585
 
119585
119586
  **There is NO one-specialist cap or fixed maximum.** cover every orthogonal question that remains; do not collapse several real questions into one broad prompt just to reduce the count. there is no file-count, line-count, or budget threshold either \u2014 diff size is not a proxy for review uncertainty.
119586
119587
 
119587
119588
  frame each question through the lens that primes the right failure modes. for high-stakes subsystems, lead with the **domain** ("the billing lens", "the auth lens", "the schema-migration lens") rather than the generic equivalent \u2014 the domain framing makes the subagent recall failure modes a generic lens misses.
119588
119589
 
119589
- you remain the synthesizer: reading the complete raw full diff plus the incremental diff, investigating surrounding code, validating every returned finding, and writing the review are yours. specialist reads supplement that work; they never satisfy your own coverage obligation.
119590
+ you remain the synthesizer: reading the delta and the context it needs, investigating surrounding code, validating every returned finding, and writing the review are yours. specialist reads supplement that work; they never satisfy your own coverage obligation.
119590
119591
 
119591
119592
  7. **dispatch specialists (only if step 6 found unresolved questions)**: for 2+ questions, emit every Task tool_use block **IN A SINGLE ASSISTANT TURN** before reading any result, so the investigations run in parallel rather than serially. your own \`read\` / \`grep\` / \`webfetch\` calls can ride in that same turn.
119592
119593
 
@@ -192058,7 +192059,9 @@ function parseDiffTocEntries(params) {
192058
192059
  const lines = params.toc.split("\n");
192059
192060
  const entries = [];
192060
192061
  for (const line of lines) {
192061
- const match3 = line.match(/^- (.+) (?:→|->) lines (\d+)-(\d+)(?: · diff-[0-9a-f]+)?$/);
192062
+ const match3 = line.match(
192063
+ /^- (.+) (?:→|->) lines (\d+)-(\d+)(?: · diff-[0-9a-f]+)?(?: · generated)?$/
192064
+ );
192062
192065
  if (!match3) continue;
192063
192066
  const startLine = Number.parseInt(match3[2], 10);
192064
192067
  const endLine = Number.parseInt(match3[3], 10);
@@ -192067,11 +192070,24 @@ function parseDiffTocEntries(params) {
192067
192070
  }
192068
192071
  return entries;
192069
192072
  }
192073
+ function parseIncrementalDiffEntries(params) {
192074
+ const lines = params.content.split("\n");
192075
+ const entries = [];
192076
+ for (const [index, line] of lines.entries()) {
192077
+ const match3 = line.match(/^@@ (.+)$|^[ +-]## (.+) ##$/);
192078
+ if (!match3) continue;
192079
+ const previous = entries.at(-1);
192080
+ if (previous) previous.endLine = index;
192081
+ const filename = (match3[1] ?? match3[2]).replace(/(: | \().*$/, "");
192082
+ entries.push({ filename, startLine: index + 1, endLine: lines.length });
192083
+ }
192084
+ return entries;
192085
+ }
192070
192086
  function createDiffCoverageState(params) {
192071
192087
  return {
192072
192088
  diffPath: params.diffPath,
192073
192089
  totalLines: params.totalLines,
192074
- tocEntries: parseDiffTocEntries({ toc: params.toc }),
192090
+ tocEntries: params.tocEntries,
192075
192091
  coveredRanges: [],
192076
192092
  // carry forward across checkout_pr refreshes so the nudge stays "once per
192077
192093
  // review session". coveredRanges are intentionally not carried because
@@ -192286,6 +192302,9 @@ function readNumber(params) {
192286
192302
  return void 0;
192287
192303
  }
192288
192304
 
192305
+ // utils/generatedFiles.ts
192306
+ var GENERATED = /(^|\/)(package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb?|Cargo\.lock|go\.sum|poetry\.lock|Pipfile\.lock|Gemfile\.lock|composer\.lock|.*\.snap|.*\.min\.(js|css)|.*\.map|.*\.lock|.*\.(pb\.go|_pb2\.py|g\.dart)|.*\.(generated|gen)\.[^/]+|.*\.(svg|png|jpe?g|gif|ico|webp|woff2?|ttf|pdf))$|(^|\/)(dist|generated|__generated__|node_modules|\.yarn|__snapshots__|coverage|\.next|\.turbo|drizzle\/meta)\//i;
192307
+
192289
192308
  // utils/gitAuth.ts
192290
192309
  import { execSync } from "node:child_process";
192291
192310
  import { createHash as createHash2 } from "node:crypto";
@@ -193628,14 +193647,18 @@ async function runGitLines(args2) {
193628
193647
  // utils/rangeDiff.ts
193629
193648
  function computeIncrementalDiff(params) {
193630
193649
  try {
193650
+ const tips = [`origin/${params.baseBranch}`, ...params.formerBaseTips];
193651
+ const oldBase = versionBase({ sha: params.beforeSha, tips });
193652
+ const newBase = versionBase({ sha: params.headSha, tips });
193631
193653
  const raw2 = $(
193632
193654
  "sh",
193633
193655
  [
193634
193656
  "-c",
193635
- 'old_base=$(git merge-base "$1" "origin/$2") && new_base=$(git merge-base "$3" "origin/$2") && git range-diff --no-color "$old_base..$(git commit-tree "$1^{tree}" -p "$old_base" -m x)" "$new_base..$(git commit-tree "$3^{tree}" -p "$new_base" -m x)"',
193657
+ 'git range-diff --no-color --creation-factor=1000 "$1..$(git commit-tree "$2^{tree}" -p "$1" -m x)" "$3..$(git commit-tree "$4^{tree}" -p "$3" -m x)"',
193636
193658
  "--",
193659
+ oldBase,
193637
193660
  params.beforeSha,
193638
- params.baseBranch,
193661
+ newBase,
193639
193662
  params.headSha
193640
193663
  ],
193641
193664
  { log: false }
@@ -193648,6 +193671,27 @@ function computeIncrementalDiff(params) {
193648
193671
  return { status: "unavailable", reason };
193649
193672
  }
193650
193673
  }
193674
+ function git(args2) {
193675
+ return $("git", args2, { log: false }).trim();
193676
+ }
193677
+ function versionBase(params) {
193678
+ let best;
193679
+ for (const tip of params.tips) {
193680
+ let sha;
193681
+ try {
193682
+ sha = git(["merge-base", params.sha, tip]);
193683
+ } catch {
193684
+ continue;
193685
+ }
193686
+ const commits = Number(git(["rev-list", "--count", `${sha}..${params.sha}`]));
193687
+ if (!best || commits < best.commits) best = { tip, sha, commits };
193688
+ }
193689
+ if (!best) throw new Error(`no merge base between ${params.sha} and the base branch`);
193690
+ if (best.tip !== params.tips[0]) {
193691
+ log2.info(`\xBB incremental diff: ${params.sha.slice(0, 7)} sits on former base tip ${best.tip}`);
193692
+ }
193693
+ return best.sha;
193694
+ }
193651
193695
  function isDiffPrefix(ch) {
193652
193696
  return ch === " " || ch === "+" || ch === "-";
193653
193697
  }
@@ -193655,6 +193699,8 @@ function postProcessRangeDiff(raw2, contextLines = 3) {
193655
193699
  if (!raw2.trim()) return null;
193656
193700
  if (/^\d+:\s+\w+\s+=\s+\d+:/m.test(raw2)) return null;
193657
193701
  const beforeBuf = [];
193702
+ let lastOuterHdr = null;
193703
+ let outerHdrEmitted = true;
193658
193704
  let lastFileHdr = null;
193659
193705
  let lastHunkHdr = null;
193660
193706
  let fileHdrEmitted = true;
@@ -193668,17 +193714,33 @@ function postProcessRangeDiff(raw2, contextLines = 3) {
193668
193714
  if (lastEmittedSeq >= 0 && line.seq > lastEmittedSeq + 1) out += (out ? "\n" : "") + "...";
193669
193715
  out += (out ? "\n" : "") + line.prefix + raw2.slice(line.from, line.to);
193670
193716
  lastEmittedSeq = line.seq;
193717
+ if (lastOuterHdr?.seq === line.seq) outerHdrEmitted = true;
193671
193718
  if (lastFileHdr?.seq === line.seq) fileHdrEmitted = true;
193672
193719
  if (lastHunkHdr?.seq === line.seq) hunkHdrEmitted = true;
193673
193720
  }
193674
193721
  function flushBefore() {
193675
- if (lastFileHdr && !fileHdrEmitted) emit2(lastFileHdr);
193676
- if (lastHunkHdr && !hunkHdrEmitted) emit2(lastHunkHdr);
193677
- for (const line of beforeBuf) {
193678
- if (line.seq > lastEmittedSeq) emit2(line);
193722
+ const headers = [
193723
+ outerHdrEmitted ? null : lastOuterHdr,
193724
+ fileHdrEmitted ? null : lastFileHdr,
193725
+ hunkHdrEmitted ? null : lastHunkHdr
193726
+ ].filter((line) => line !== null);
193727
+ const pending = [...headers, ...beforeBuf.filter((line) => line.seq > lastEmittedSeq)].sort(
193728
+ (a2, b) => a2.seq - b.seq
193729
+ );
193730
+ for (const [i2, line] of pending.entries()) {
193731
+ if (pending[i2 - 1]?.seq !== line.seq) emit2(line);
193679
193732
  }
193680
193733
  beforeBuf.length = 0;
193681
193734
  }
193735
+ function isFileHeader(pos) {
193736
+ return raw2.startsWith("## ", pos) && !raw2.startsWith("## Commit message", pos);
193737
+ }
193738
+ function markFileHeader(line, emitted) {
193739
+ lastFileHdr = line;
193740
+ fileHdrEmitted = emitted;
193741
+ lastHunkHdr = null;
193742
+ hunkHdrEmitted = true;
193743
+ }
193682
193744
  let cursor3 = 0;
193683
193745
  while (cursor3 < raw2.length) {
193684
193746
  const eol = raw2.indexOf("\n", cursor3);
@@ -193687,7 +193749,18 @@ function postProcessRangeDiff(raw2, contextLines = 3) {
193687
193749
  cursor3 = lineEnd + 1;
193688
193750
  continue;
193689
193751
  }
193690
- if (lineEnd - cursor3 >= 5 && raw2.startsWith(" ", cursor3)) {
193752
+ if (raw2.startsWith(" @@", cursor3)) {
193753
+ const label = raw2.slice(cursor3 + 4, lineEnd);
193754
+ lastOuterHdr = label === "@@ Metadata" || label === "@@ Commit message" ? null : { prefix: "", from: cursor3 + 4, to: lineEnd, seq };
193755
+ outerHdrEmitted = false;
193756
+ lastFileHdr = null;
193757
+ fileHdrEmitted = true;
193758
+ lastHunkHdr = null;
193759
+ hunkHdrEmitted = true;
193760
+ beforeBuf.length = 0;
193761
+ afterRemaining = 0;
193762
+ seq++;
193763
+ } else if (lineEnd - cursor3 >= 5 && raw2.startsWith(" ", cursor3)) {
193691
193764
  const prefix = raw2[cursor3 + 4];
193692
193765
  if (isDiffPrefix(prefix)) {
193693
193766
  const contentPos = cursor3 + 5;
@@ -193699,13 +193772,13 @@ function postProcessRangeDiff(raw2, contextLines = 3) {
193699
193772
  } else if (isDiffPrefix(raw2[contentPos])) {
193700
193773
  isChange = isOuterChange;
193701
193774
  line = { prefix, from: contentPos + 1, to: lineEnd, seq };
193775
+ if (raw2[contentPos] === " " && isFileHeader(contentPos + 1)) {
193776
+ markFileHeader(line, isChange);
193777
+ }
193702
193778
  } else {
193703
193779
  line = { prefix, from: contentPos, to: lineEnd, seq };
193704
- if (raw2.startsWith("## ", contentPos) && !raw2.startsWith("## Commit message", contentPos)) {
193705
- lastFileHdr = line;
193706
- fileHdrEmitted = false;
193707
- lastHunkHdr = null;
193708
- hunkHdrEmitted = true;
193780
+ if (isFileHeader(contentPos)) {
193781
+ markFileHeader(line, false);
193709
193782
  } else if (raw2.startsWith("@@", contentPos) && !raw2.startsWith("@@ Metadata", contentPos)) {
193710
193783
  lastHunkHdr = line;
193711
193784
  hunkHdrEmitted = false;
@@ -199698,6 +199771,7 @@ var STRING_KEYS = [
199698
199771
  "prNodeId",
199699
199772
  "issueNodeId",
199700
199773
  "reviewNodeId",
199774
+ "reviewedCommitSha",
199701
199775
  "planCommentNodeId",
199702
199776
  "summarySnapshot",
199703
199777
  "model",
@@ -200862,7 +200936,10 @@ async function createAndSubmitWithFooter(ctx, params, opts) {
200862
200936
  }
200863
200937
  }
200864
200938
  async function reportReviewNodeId(ctx, params) {
200865
- await patchWorkflowRunFields(ctx, { reviewNodeId: params.nodeId });
200939
+ await patchWorkflowRunFields(ctx, {
200940
+ reviewNodeId: params.nodeId,
200941
+ reviewedCommitSha: params.reviewedSha
200942
+ });
200866
200943
  }
200867
200944
 
200868
200945
  // utils/setup.ts
@@ -201132,8 +201209,9 @@ function formatFilesWithLineNumbers(files) {
201132
201209
  const tocLines = [`## Files (${files.length})`];
201133
201210
  for (const entry of tocEntries) {
201134
201211
  const anchor2 = createHash3("sha256").update(entry.filename).digest("hex");
201212
+ const generated = GENERATED.test(entry.filename) ? " \xB7 generated" : "";
201135
201213
  tocLines.push(
201136
- `- ${entry.filename} \u2192 lines ${entry.startLine}-${entry.endLine} \xB7 diff-${anchor2}`
201214
+ `- ${entry.filename} \u2192 lines ${entry.startLine}-${entry.endLine} \xB7 diff-${anchor2}${generated}`
201137
201215
  );
201138
201216
  }
201139
201217
  tocLines.push("");
@@ -201455,6 +201533,34 @@ function describeHead(h2) {
201455
201533
  if (h2.kind === "branch") return `branch \`${h2.name}\``;
201456
201534
  return `detached HEAD \`${h2.sha}\``;
201457
201535
  }
201536
+ var FORMER_BASE_TIPS_QUERY = `
201537
+ query ($owner: String!, $repo: String!, $number: Int!) {
201538
+ repository(owner: $owner, name: $repo) {
201539
+ pullRequest(number: $number) {
201540
+ timelineItems(itemTypes: [BASE_REF_FORCE_PUSHED_EVENT], last: 100) {
201541
+ nodes { ... on BaseRefForcePushedEvent { beforeCommit { oid } } }
201542
+ }
201543
+ }
201544
+ }
201545
+ }
201546
+ `;
201547
+ async function getFormerBaseTips(ctx, pullNumber) {
201548
+ try {
201549
+ const response = await ctx.octokit.graphql(FORMER_BASE_TIPS_QUERY, {
201550
+ owner: ctx.repo.owner,
201551
+ repo: ctx.repo.name,
201552
+ number: pullNumber
201553
+ });
201554
+ return response.repository.pullRequest.timelineItems.nodes.flatMap(
201555
+ (node3) => node3.beforeCommit ? [node3.beforeCommit.oid] : []
201556
+ );
201557
+ } catch (error63) {
201558
+ log2.warning(
201559
+ `\xBB former base tips unavailable: ${error63 instanceof Error ? error63.message : String(error63)}`
201560
+ );
201561
+ return [];
201562
+ }
201563
+ }
201458
201564
  function CheckoutPrTool(ctx) {
201459
201565
  const runCheckout = async (pull_number, signal) => {
201460
201566
  const prResponse = await ctx.octokit.rest.pulls.get({
@@ -201498,6 +201604,8 @@ function CheckoutPrTool(ctx) {
201498
201604
  if (!checkoutSha) throw new Error("checkout completed without a resolved HEAD SHA");
201499
201605
  const headShort = checkoutSha.slice(0, 7);
201500
201606
  let incrementalDiffPath;
201607
+ let incrementalDiff;
201608
+ let incrementalEmpty = false;
201501
201609
  let incrementalUnavailable = false;
201502
201610
  let incrementalUnavailableReason;
201503
201611
  if (primary.beforeSha) {
@@ -201505,7 +201613,8 @@ function CheckoutPrTool(ctx) {
201505
201613
  const incremental = computeIncrementalDiff({
201506
201614
  baseBranch: pr.baseRef,
201507
201615
  beforeSha: primary.beforeSha,
201508
- headSha: checkoutSha
201616
+ headSha: checkoutSha,
201617
+ formerBaseTips: await getFormerBaseTips(ctx, pull_number)
201509
201618
  });
201510
201619
  if (incremental.status === "ok") {
201511
201620
  incrementalDiffPath = join17(
@@ -201513,9 +201622,12 @@ function CheckoutPrTool(ctx) {
201513
201622
  `pr-${pull_number}-${beforeShort}-${headShort}-incremental.diff`
201514
201623
  );
201515
201624
  writeFileSync11(incrementalDiffPath, incremental.diff);
201625
+ incrementalDiff = incremental.diff;
201516
201626
  log2.info(
201517
201627
  `\xBB incremental diff computed (${incremental.diff.length} bytes) \u2192 ${incrementalDiffPath}`
201518
201628
  );
201629
+ } else if (incremental.status === "empty") {
201630
+ incrementalEmpty = true;
201519
201631
  } else if (incremental.status === "unavailable") {
201520
201632
  incrementalUnavailable = true;
201521
201633
  incrementalUnavailableReason = incremental.reason;
@@ -201528,14 +201640,16 @@ ${diffPreview}`);
201528
201640
  const diffPath = join17(tempDir, `pr-${pull_number}-${headShort}.diff`);
201529
201641
  writeFileSync11(diffPath, formatResult.content);
201530
201642
  log2.debug(`wrote diff to ${diffPath} (${formatResult.content.length} bytes)`);
201531
- primary.diffCoverage = createDiffCoverageState({
201532
- diffPath,
201533
- totalLines: countLines({ content: formatResult.content }),
201534
- toc: formatResult.toc,
201643
+ const coverageEntries = incrementalDiff ? parseIncrementalDiffEntries({ content: incrementalDiff }) : parseDiffTocEntries({ toc: formatResult.toc });
201644
+ primary.diffCoverage = incrementalEmpty ? void 0 : createDiffCoverageState({
201645
+ diffPath: incrementalDiffPath ?? diffPath,
201646
+ totalLines: countLines({ content: incrementalDiff ?? formatResult.content }),
201647
+ tocEntries: coverageEntries.filter((entry) => !GENERATED.test(entry.filename)),
201535
201648
  previous: primary.diffCoverage
201536
201649
  });
201650
+ const coverage = primary.diffCoverage;
201537
201651
  log2.debug(
201538
- `\xBB diff coverage initialized: diffPath=${diffPath}, totalLines=${primary.diffCoverage.totalLines}, tocEntries=${primary.diffCoverage.tocEntries.length}`
201652
+ coverage ? `\xBB diff coverage initialized: diffPath=${coverage.diffPath}, totalLines=${coverage.totalLines}, tocEntries=${coverage.tocEntries.length}` : "\xBB diff coverage skipped: no changes since the last reviewed version"
201539
201653
  );
201540
201654
  let impactPath;
201541
201655
  if (process.env.PULLFROG_DISABLE_CHANGE_IMPACT === "1") {
@@ -201586,7 +201700,7 @@ ${diffPreview}`);
201586
201700
  primary.commentableLinesByFile = cached4;
201587
201701
  primary.commentableLinesPullNumber = pull_number;
201588
201702
  primary.commentableLinesCheckoutSha = checkoutSha;
201589
- const incrementalInstructions = incrementalDiffPath ? `IMPORTANT: incrementalDiffPath contains ONLY the changes since the last reviewed version (computed via range-diff). you MUST read incrementalDiffPath FIRST to understand what changed, then use diffPath for full PR context. do NOT skip the incremental diff. ` : incrementalUnavailable ? (
201703
+ const incrementalInstructions = incrementalDiffPath ? `IMPORTANT: incrementalDiffPath contains ONLY the changes since the last reviewed version (computed via range-diff), and it \u2014 not diffPath's TOC \u2014 is this review's coverage checklist. read it FIRST and in full, then read only the diffPath ranges the delta needs for context. ` : incrementalEmpty ? `NOTE: the PR's net changes are identical to the last reviewed version (e.g. a rebase with no content change), so there is no new diff to cover. ` : incrementalUnavailable ? (
201590
201704
  // say it FAILED rather than leaving the agent to infer "nothing changed"
201591
201705
  // from an absent field. only suggest diff-tree when the previous
201592
201706
  // revision is actually PRESENT locally — if the fetch never landed it,
@@ -201637,7 +201751,7 @@ ${diffPreview}`);
201637
201751
  commitLogTruncated,
201638
201752
  commitLogUnavailable,
201639
201753
  hookWarning: checkoutResult.hookWarning,
201640
- instructions: incrementalInstructions + `the diff file at diffPath contains a table of contents (TOC) at the top listing every changed file with its line range. use the TOC line ranges as your checklist and read specific files from the diff instead of reading the entire file. for example, if the TOC says "src/foo.ts \u2192 lines 5-42", read lines 5-42 from diffPath to see that file's changes. review files selectively based on relevance rather than reading everything sequentially. to inspect the PR's changed files, use diffPath \u2014 do NOT run \`git diff\` to re-derive what's already in diffPath. the formatted diff with line numbers is authoritative. if you ever do need a branch-vs-base diff via the git tool, use \`git diff --merge-base <base>\` (single call, includes uncommitted edits) or three-dot \`git diff <base>...HEAD\` (committed-only). bare \`<base>\` and two-dot \`<base>..HEAD\` are symmetric and pull in the inverse of every commit landed on \`<base>\` since the branch forked \u2014 the git tool will reject those forms when the divergence is detected. \`$(...)\` subshells are NOT expanded by the git tool. \`git log\` and \`git diff --stat\` are fine for commit-range overview, and \`git diff\` / \`git diff --cached\` are fine for inspecting *your own* uncommitted changes \u2014 but PR review content MUST come from diffPath. before your review is submitted, a one-time coverage pre-flight may error listing unread TOC regions. retry the same create_pull_request_review call to proceed \u2014 optionally after reading the listed ranges. the pre-flight will not block again this session. the local branch is 'localBranch' (pr-{number}), not the remote branch name. when pushing, omit branchName to use the current branch. do not use remoteBranch as a local branch name.` + impactInstructions + hookWarningInstructions + commitLogInstructions
201754
+ instructions: incrementalInstructions + `the diff file at diffPath contains a table of contents (TOC) at the top listing every changed file with its line range. absent incrementalDiffPath, use the TOC line ranges as your checklist (entries marked \`generated\` are off it) and read specific files from the diff instead of reading the entire file. for example, if the TOC says "src/foo.ts \u2192 lines 5-42", read lines 5-42 from diffPath to see that file's changes. review files selectively based on relevance rather than reading everything sequentially. to inspect the PR's changed files, use diffPath \u2014 do NOT run \`git diff\` to re-derive what's already in diffPath. the formatted diff with line numbers is authoritative. if you ever do need a branch-vs-base diff via the git tool, use \`git diff --merge-base <base>\` (single call, includes uncommitted edits) or three-dot \`git diff <base>...HEAD\` (committed-only). bare \`<base>\` and two-dot \`<base>..HEAD\` are symmetric and pull in the inverse of every commit landed on \`<base>\` since the branch forked \u2014 the git tool will reject those forms when the divergence is detected. \`$(...)\` subshells are NOT expanded by the git tool. \`git log\` and \`git diff --stat\` are fine for commit-range overview, and \`git diff\` / \`git diff --cached\` are fine for inspecting *your own* uncommitted changes \u2014 but PR review content MUST come from diffPath. before your review is submitted, a one-time coverage pre-flight may error listing unread TOC regions. retry the same create_pull_request_review call to proceed \u2014 optionally after reading the listed ranges. the pre-flight will not block again this session. the local branch is 'localBranch' (pr-{number}), not the remote branch name. when pushing, omit branchName to use the current branch. do not use remoteBranch as a local branch name.` + impactInstructions + hookWarningInstructions + commitLogInstructions
201641
201755
  };
201642
201756
  };
201643
201757
  return tool({
@@ -208095,7 +208209,7 @@ async function postReviewCleanup(ctx) {
208095
208209
  if (!review) {
208096
208210
  const checkoutSha = primaryRepoState(ctx.toolState).checkoutSha;
208097
208211
  if (ctx.toolState.selectedMode === "IncrementalReview" && checkoutSha) {
208098
- await patchWorkflowRunFields(ctx, { reviewSkipped: true });
208212
+ await patchWorkflowRunFields(ctx, { reviewSkipped: true, reviewedCommitSha: checkoutSha });
208099
208213
  await bestEffort(
208100
208214
  () => dispatchFollowUpReReview(ctx, checkoutSha),
208101
208215
  "follow-up re-review dispatch"
@@ -208104,7 +208218,7 @@ async function postReviewCleanup(ctx) {
208104
208218
  return;
208105
208219
  }
208106
208220
  delete ctx.toolState.review;
208107
- await bestEffort(() => reportReviewNodeId(ctx, { nodeId: review.nodeId }), "reportReviewNodeId");
208221
+ await bestEffort(() => reportReviewNodeId(ctx, review), "reportReviewNodeId");
208108
208222
  if (review.reviewedSha) {
208109
208223
  await bestEffort(
208110
208224
  () => dispatchFollowUpReReview(ctx, review.reviewedSha),
@@ -209874,7 +209988,7 @@ function resolveTarget2(repo) {
209874
209988
  return { owner: match3[1], repo: match3[2] };
209875
209989
  }
209876
209990
  function cliVersion() {
209877
- const parts = /^(\d+)\.(\d+)\.(\d+)$/.exec("0.1.102");
209991
+ const parts = /^(\d+)\.(\d+)\.(\d+)$/.exec("0.1.104");
209878
209992
  if (!parts) return "0.0.0";
209879
209993
  return `${Number(parts[1])}.${Number(parts[2])}.${Number(parts[3])}`;
209880
209994
  }
@@ -210041,7 +210155,7 @@ async function runCli8(input2) {
210041
210155
  }
210042
210156
 
210043
210157
  // cli.ts
210044
- var VERSION10 = "0.1.102";
210158
+ var VERSION10 = "0.1.104";
210045
210159
  var bin = basename2(process.argv[1] || "");
210046
210160
  var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
210047
210161
  var rawArgs = process.argv.slice(2);
package/dist/index.js CHANGED
@@ -109128,7 +109128,7 @@ var providers = {
109128
109128
  },
109129
109129
  // Zen's FREE open-weights preview (listed 2026-10-05), the eighth promo.
109130
109130
  // Zen's docs: no training, 30-day retention for safety. hidden until a
109131
- // published release carries the alias.
109131
+ // published release carries the alias; 0.1.102 is the first.
109132
109132
  beam: {
109133
109133
  displayName: "Beam",
109134
109134
  description: "Preview, free for a limited time",
@@ -109136,8 +109136,7 @@ var providers = {
109136
109136
  effort: ["low", "medium", "high", "xhigh", "max"],
109137
109137
  // free to run, still gated on the provider's own OPENCODE_API_KEY —
109138
109138
  // see the big-pickle note above (#1077).
109139
- isFree: true,
109140
- hidden: true
109139
+ isFree: true
109141
109140
  },
109142
109141
  // Zen's live free MiMo, and the second free row in a menu that big-pickle
109143
109142
  // was alone in since `mimo-v2-pro-free` lost its model.
@@ -109510,10 +109509,12 @@ var providers = {
109510
109509
  openRouterResolve: "openrouter/openai/gpt-5.4",
109511
109510
  fallback: "openrouter/gpt-sol"
109512
109511
  },
109512
+ // OpenRouter dropped `openai/o4-mini` on 2026-10-28 (only `:batch` remains) — folds stored pins to Luna.
109513
109513
  "o4-mini": {
109514
109514
  displayName: "O4 Mini",
109515
109515
  resolve: "openrouter/openai/o4-mini",
109516
- openRouterResolve: "openrouter/openai/o4-mini"
109516
+ openRouterResolve: "openrouter/openai/o4-mini",
109517
+ fallback: "openrouter/gpt-luna"
109517
109518
  },
109518
109519
  "gemini-pro": {
109519
109520
  displayName: "Gemini Pro",
@@ -112140,7 +112141,7 @@ var import_semver = __toESM(require_semver2(), 1);
112140
112141
  // package.json
112141
112142
  var package_default = {
112142
112143
  name: "pullfrog",
112143
- version: "0.1.102",
112144
+ version: "0.1.104",
112144
112145
  cliContractVersion: "1",
112145
112146
  type: "module",
112146
112147
  bin: {
@@ -113089,7 +113090,7 @@ ${PR_SUMMARY_FORMAT}`
113089
113090
 
113090
113091
  2. **checkout**: call \`${t("checkout_pr")}\` \u2014 this returns PR metadata, \`diffPath\` (full diff), \`incrementalDiffPath\` (changes since last reviewed version, if available), and a supplemental \`impactPath\` when change-impact extraction is enabled.
113091
113092
 
113092
- 3. **incremental scope**: if \`incrementalDiffPath\` is present, read it FIRST to see what changed since the last review. this is a range-diff that isolates the net changes, filtering out base branch noise. then read the authoritative full diff end-to-end, beginning with the TOC and using its line ranges as your coverage checklist. if no incremental diff is present, start with the full-diff TOC, determine what changed since Pullfrog's most recent review, and complete the raw-diff read. only after establishing that authoritative scope and completing raw-diff coverage, use \`impactPath\` as an explicitly incomplete list of reference leads; it never replaces raw-diff reading or establishes coverage.
113093
+ 3. **incremental scope**: if \`incrementalDiffPath\` is present, it is your coverage checklist: read it FIRST and in full. it is a range-diff of the net changes since the last review, free of base-branch noise. read only the \`diffPath\` ranges the delta needs for context \u2014 do not re-read the whole PR. if no incremental diff is present, start with the full-diff TOC, determine what changed since Pullfrog's most recent review, and complete the raw-diff read. only after that, use \`impactPath\` as an explicitly incomplete list of reference leads; it never replaces raw-diff reading or establishes coverage.
113093
113094
 
113094
113095
  4. **prior feedback \u2014 read AND retire it**: fetch previous reviews via \`${t("list_pull_request_reviews")}\`, then call \`${t("get_review_comments")}\` on each prior Pullfrog review. Each thread renders as a section whose first line is a fenced tag \`comment author=<login> id=<fullDatabaseId> review=<reviewId> thread=<graphqlId>\`; section headers carry \`[RESOLVED]\` / \`[OUTDATED]\` when relevant. For every **open, Pullfrog-originated** thread, decide and act:
113095
113096
 
@@ -113105,13 +113106,13 @@ ${PR_SUMMARY_FORMAT}`
113105
113106
 
113106
113107
  skip the deeper pass and jump to step 10's non-substantive path (do NOT submit a review) only when the incremental changes have **no behavioral surface at all** \u2014 formatting, comment tweaks, import reordering, lockfile regen, a mechanical rename of import paths. line count is not the signal: a one-line change to auth, money, SQL, a comparison operator, a redirect, or a config default is not trivial.
113107
113108
 
113108
- 6. **specialist decision**: after covering the incremental and full diffs, name the questions about the new changes that you still cannot answer confidently yourself, and dispatch one \`${REVIEWER_AGENT_NAME}\` specialist per question. a question qualifies only when a specialist could return evidence that **changes your disposition** on the PR \u2014 generic requests for another look, extra confidence, or polish do not. most incremental reviews need zero or one, especially thread-reply re-reviews; some need several.
113109
+ 6. **specialist decision**: after covering the delta, name the questions about the new changes that you still cannot answer confidently yourself, and dispatch one \`${REVIEWER_AGENT_NAME}\` specialist per question. a question qualifies only when a specialist could return evidence that **changes your disposition** on the PR \u2014 generic requests for another look, extra confidence, or polish do not. most incremental reviews need zero or one, especially thread-reply re-reviews; some need several.
113109
113110
 
113110
113111
  **There is NO one-specialist cap or fixed maximum.** cover every orthogonal question that remains; do not collapse several real questions into one broad prompt just to reduce the count. there is no file-count, line-count, or budget threshold either \u2014 diff size is not a proxy for review uncertainty.
113111
113112
 
113112
113113
  frame each question through the lens that primes the right failure modes. for high-stakes subsystems, lead with the **domain** ("the billing lens", "the auth lens", "the schema-migration lens") rather than the generic equivalent \u2014 the domain framing makes the subagent recall failure modes a generic lens misses.
113113
113114
 
113114
- you remain the synthesizer: reading the complete raw full diff plus the incremental diff, investigating surrounding code, validating every returned finding, and writing the review are yours. specialist reads supplement that work; they never satisfy your own coverage obligation.
113115
+ you remain the synthesizer: reading the delta and the context it needs, investigating surrounding code, validating every returned finding, and writing the review are yours. specialist reads supplement that work; they never satisfy your own coverage obligation.
113115
113116
 
113116
113117
  7. **dispatch specialists (only if step 6 found unresolved questions)**: for 2+ questions, emit every Task tool_use block **IN A SINGLE ASSISTANT TURN** before reading any result, so the investigations run in parallel rather than serially. your own \`read\` / \`grep\` / \`webfetch\` calls can ride in that same turn.
113117
113118
 
@@ -188540,7 +188541,9 @@ function parseDiffTocEntries(params) {
188540
188541
  const lines = params.toc.split("\n");
188541
188542
  const entries = [];
188542
188543
  for (const line of lines) {
188543
- const match3 = line.match(/^- (.+) (?:→|->) lines (\d+)-(\d+)(?: · diff-[0-9a-f]+)?$/);
188544
+ const match3 = line.match(
188545
+ /^- (.+) (?:→|->) lines (\d+)-(\d+)(?: · diff-[0-9a-f]+)?(?: · generated)?$/
188546
+ );
188544
188547
  if (!match3) continue;
188545
188548
  const startLine = Number.parseInt(match3[2], 10);
188546
188549
  const endLine = Number.parseInt(match3[3], 10);
@@ -188549,11 +188552,24 @@ function parseDiffTocEntries(params) {
188549
188552
  }
188550
188553
  return entries;
188551
188554
  }
188555
+ function parseIncrementalDiffEntries(params) {
188556
+ const lines = params.content.split("\n");
188557
+ const entries = [];
188558
+ for (const [index, line] of lines.entries()) {
188559
+ const match3 = line.match(/^@@ (.+)$|^[ +-]## (.+) ##$/);
188560
+ if (!match3) continue;
188561
+ const previous = entries.at(-1);
188562
+ if (previous) previous.endLine = index;
188563
+ const filename = (match3[1] ?? match3[2]).replace(/(: | \().*$/, "");
188564
+ entries.push({ filename, startLine: index + 1, endLine: lines.length });
188565
+ }
188566
+ return entries;
188567
+ }
188552
188568
  function createDiffCoverageState(params) {
188553
188569
  return {
188554
188570
  diffPath: params.diffPath,
188555
188571
  totalLines: params.totalLines,
188556
- tocEntries: parseDiffTocEntries({ toc: params.toc }),
188572
+ tocEntries: params.tocEntries,
188557
188573
  coveredRanges: [],
188558
188574
  // carry forward across checkout_pr refreshes so the nudge stays "once per
188559
188575
  // review session". coveredRanges are intentionally not carried because
@@ -188768,6 +188784,9 @@ function readNumber(params) {
188768
188784
  return void 0;
188769
188785
  }
188770
188786
 
188787
+ // utils/generatedFiles.ts
188788
+ var GENERATED = /(^|\/)(package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb?|Cargo\.lock|go\.sum|poetry\.lock|Pipfile\.lock|Gemfile\.lock|composer\.lock|.*\.snap|.*\.min\.(js|css)|.*\.map|.*\.lock|.*\.(pb\.go|_pb2\.py|g\.dart)|.*\.(generated|gen)\.[^/]+|.*\.(svg|png|jpe?g|gif|ico|webp|woff2?|ttf|pdf))$|(^|\/)(dist|generated|__generated__|node_modules|\.yarn|__snapshots__|coverage|\.next|\.turbo|drizzle\/meta)\//i;
188789
+
188771
188790
  // utils/gitAuth.ts
188772
188791
  import { execSync } from "node:child_process";
188773
188792
  import { createHash } from "node:crypto";
@@ -190110,14 +190129,18 @@ async function runGitLines(args2) {
190110
190129
  // utils/rangeDiff.ts
190111
190130
  function computeIncrementalDiff(params) {
190112
190131
  try {
190132
+ const tips = [`origin/${params.baseBranch}`, ...params.formerBaseTips];
190133
+ const oldBase = versionBase({ sha: params.beforeSha, tips });
190134
+ const newBase = versionBase({ sha: params.headSha, tips });
190113
190135
  const raw2 = $(
190114
190136
  "sh",
190115
190137
  [
190116
190138
  "-c",
190117
- 'old_base=$(git merge-base "$1" "origin/$2") && new_base=$(git merge-base "$3" "origin/$2") && git range-diff --no-color "$old_base..$(git commit-tree "$1^{tree}" -p "$old_base" -m x)" "$new_base..$(git commit-tree "$3^{tree}" -p "$new_base" -m x)"',
190139
+ 'git range-diff --no-color --creation-factor=1000 "$1..$(git commit-tree "$2^{tree}" -p "$1" -m x)" "$3..$(git commit-tree "$4^{tree}" -p "$3" -m x)"',
190118
190140
  "--",
190141
+ oldBase,
190119
190142
  params.beforeSha,
190120
- params.baseBranch,
190143
+ newBase,
190121
190144
  params.headSha
190122
190145
  ],
190123
190146
  { log: false }
@@ -190130,6 +190153,27 @@ function computeIncrementalDiff(params) {
190130
190153
  return { status: "unavailable", reason };
190131
190154
  }
190132
190155
  }
190156
+ function git(args2) {
190157
+ return $("git", args2, { log: false }).trim();
190158
+ }
190159
+ function versionBase(params) {
190160
+ let best;
190161
+ for (const tip of params.tips) {
190162
+ let sha;
190163
+ try {
190164
+ sha = git(["merge-base", params.sha, tip]);
190165
+ } catch {
190166
+ continue;
190167
+ }
190168
+ const commits = Number(git(["rev-list", "--count", `${sha}..${params.sha}`]));
190169
+ if (!best || commits < best.commits) best = { tip, sha, commits };
190170
+ }
190171
+ if (!best) throw new Error(`no merge base between ${params.sha} and the base branch`);
190172
+ if (best.tip !== params.tips[0]) {
190173
+ log.info(`\xBB incremental diff: ${params.sha.slice(0, 7)} sits on former base tip ${best.tip}`);
190174
+ }
190175
+ return best.sha;
190176
+ }
190133
190177
  function isDiffPrefix(ch) {
190134
190178
  return ch === " " || ch === "+" || ch === "-";
190135
190179
  }
@@ -190137,6 +190181,8 @@ function postProcessRangeDiff(raw2, contextLines = 3) {
190137
190181
  if (!raw2.trim()) return null;
190138
190182
  if (/^\d+:\s+\w+\s+=\s+\d+:/m.test(raw2)) return null;
190139
190183
  const beforeBuf = [];
190184
+ let lastOuterHdr = null;
190185
+ let outerHdrEmitted = true;
190140
190186
  let lastFileHdr = null;
190141
190187
  let lastHunkHdr = null;
190142
190188
  let fileHdrEmitted = true;
@@ -190150,17 +190196,33 @@ function postProcessRangeDiff(raw2, contextLines = 3) {
190150
190196
  if (lastEmittedSeq >= 0 && line.seq > lastEmittedSeq + 1) out += (out ? "\n" : "") + "...";
190151
190197
  out += (out ? "\n" : "") + line.prefix + raw2.slice(line.from, line.to);
190152
190198
  lastEmittedSeq = line.seq;
190199
+ if (lastOuterHdr?.seq === line.seq) outerHdrEmitted = true;
190153
190200
  if (lastFileHdr?.seq === line.seq) fileHdrEmitted = true;
190154
190201
  if (lastHunkHdr?.seq === line.seq) hunkHdrEmitted = true;
190155
190202
  }
190156
190203
  function flushBefore() {
190157
- if (lastFileHdr && !fileHdrEmitted) emit(lastFileHdr);
190158
- if (lastHunkHdr && !hunkHdrEmitted) emit(lastHunkHdr);
190159
- for (const line of beforeBuf) {
190160
- if (line.seq > lastEmittedSeq) emit(line);
190204
+ const headers = [
190205
+ outerHdrEmitted ? null : lastOuterHdr,
190206
+ fileHdrEmitted ? null : lastFileHdr,
190207
+ hunkHdrEmitted ? null : lastHunkHdr
190208
+ ].filter((line) => line !== null);
190209
+ const pending = [...headers, ...beforeBuf.filter((line) => line.seq > lastEmittedSeq)].sort(
190210
+ (a, b) => a.seq - b.seq
190211
+ );
190212
+ for (const [i, line] of pending.entries()) {
190213
+ if (pending[i - 1]?.seq !== line.seq) emit(line);
190161
190214
  }
190162
190215
  beforeBuf.length = 0;
190163
190216
  }
190217
+ function isFileHeader(pos) {
190218
+ return raw2.startsWith("## ", pos) && !raw2.startsWith("## Commit message", pos);
190219
+ }
190220
+ function markFileHeader(line, emitted) {
190221
+ lastFileHdr = line;
190222
+ fileHdrEmitted = emitted;
190223
+ lastHunkHdr = null;
190224
+ hunkHdrEmitted = true;
190225
+ }
190164
190226
  let cursor = 0;
190165
190227
  while (cursor < raw2.length) {
190166
190228
  const eol = raw2.indexOf("\n", cursor);
@@ -190169,7 +190231,18 @@ function postProcessRangeDiff(raw2, contextLines = 3) {
190169
190231
  cursor = lineEnd + 1;
190170
190232
  continue;
190171
190233
  }
190172
- if (lineEnd - cursor >= 5 && raw2.startsWith(" ", cursor)) {
190234
+ if (raw2.startsWith(" @@", cursor)) {
190235
+ const label = raw2.slice(cursor + 4, lineEnd);
190236
+ lastOuterHdr = label === "@@ Metadata" || label === "@@ Commit message" ? null : { prefix: "", from: cursor + 4, to: lineEnd, seq };
190237
+ outerHdrEmitted = false;
190238
+ lastFileHdr = null;
190239
+ fileHdrEmitted = true;
190240
+ lastHunkHdr = null;
190241
+ hunkHdrEmitted = true;
190242
+ beforeBuf.length = 0;
190243
+ afterRemaining = 0;
190244
+ seq++;
190245
+ } else if (lineEnd - cursor >= 5 && raw2.startsWith(" ", cursor)) {
190173
190246
  const prefix = raw2[cursor + 4];
190174
190247
  if (isDiffPrefix(prefix)) {
190175
190248
  const contentPos = cursor + 5;
@@ -190181,13 +190254,13 @@ function postProcessRangeDiff(raw2, contextLines = 3) {
190181
190254
  } else if (isDiffPrefix(raw2[contentPos])) {
190182
190255
  isChange = isOuterChange;
190183
190256
  line = { prefix, from: contentPos + 1, to: lineEnd, seq };
190257
+ if (raw2[contentPos] === " " && isFileHeader(contentPos + 1)) {
190258
+ markFileHeader(line, isChange);
190259
+ }
190184
190260
  } else {
190185
190261
  line = { prefix, from: contentPos, to: lineEnd, seq };
190186
- if (raw2.startsWith("## ", contentPos) && !raw2.startsWith("## Commit message", contentPos)) {
190187
- lastFileHdr = line;
190188
- fileHdrEmitted = false;
190189
- lastHunkHdr = null;
190190
- hunkHdrEmitted = true;
190262
+ if (isFileHeader(contentPos)) {
190263
+ markFileHeader(line, false);
190191
190264
  } else if (raw2.startsWith("@@", contentPos) && !raw2.startsWith("@@ Metadata", contentPos)) {
190192
190265
  lastHunkHdr = line;
190193
190266
  hunkHdrEmitted = false;
@@ -196180,6 +196253,7 @@ var STRING_KEYS = [
196180
196253
  "prNodeId",
196181
196254
  "issueNodeId",
196182
196255
  "reviewNodeId",
196256
+ "reviewedCommitSha",
196183
196257
  "planCommentNodeId",
196184
196258
  "summarySnapshot",
196185
196259
  "model",
@@ -197344,7 +197418,10 @@ async function createAndSubmitWithFooter(ctx, params, opts) {
197344
197418
  }
197345
197419
  }
197346
197420
  async function reportReviewNodeId(ctx, params) {
197347
- await patchWorkflowRunFields(ctx, { reviewNodeId: params.nodeId });
197421
+ await patchWorkflowRunFields(ctx, {
197422
+ reviewNodeId: params.nodeId,
197423
+ reviewedCommitSha: params.reviewedSha
197424
+ });
197348
197425
  }
197349
197426
 
197350
197427
  // utils/setup.ts
@@ -197614,8 +197691,9 @@ function formatFilesWithLineNumbers(files) {
197614
197691
  const tocLines = [`## Files (${files.length})`];
197615
197692
  for (const entry of tocEntries) {
197616
197693
  const anchor2 = createHash2("sha256").update(entry.filename).digest("hex");
197694
+ const generated = GENERATED.test(entry.filename) ? " \xB7 generated" : "";
197617
197695
  tocLines.push(
197618
- `- ${entry.filename} \u2192 lines ${entry.startLine}-${entry.endLine} \xB7 diff-${anchor2}`
197696
+ `- ${entry.filename} \u2192 lines ${entry.startLine}-${entry.endLine} \xB7 diff-${anchor2}${generated}`
197619
197697
  );
197620
197698
  }
197621
197699
  tocLines.push("");
@@ -197937,6 +198015,34 @@ function describeHead(h) {
197937
198015
  if (h.kind === "branch") return `branch \`${h.name}\``;
197938
198016
  return `detached HEAD \`${h.sha}\``;
197939
198017
  }
198018
+ var FORMER_BASE_TIPS_QUERY = `
198019
+ query ($owner: String!, $repo: String!, $number: Int!) {
198020
+ repository(owner: $owner, name: $repo) {
198021
+ pullRequest(number: $number) {
198022
+ timelineItems(itemTypes: [BASE_REF_FORCE_PUSHED_EVENT], last: 100) {
198023
+ nodes { ... on BaseRefForcePushedEvent { beforeCommit { oid } } }
198024
+ }
198025
+ }
198026
+ }
198027
+ }
198028
+ `;
198029
+ async function getFormerBaseTips(ctx, pullNumber) {
198030
+ try {
198031
+ const response = await ctx.octokit.graphql(FORMER_BASE_TIPS_QUERY, {
198032
+ owner: ctx.repo.owner,
198033
+ repo: ctx.repo.name,
198034
+ number: pullNumber
198035
+ });
198036
+ return response.repository.pullRequest.timelineItems.nodes.flatMap(
198037
+ (node3) => node3.beforeCommit ? [node3.beforeCommit.oid] : []
198038
+ );
198039
+ } catch (error63) {
198040
+ log.warning(
198041
+ `\xBB former base tips unavailable: ${error63 instanceof Error ? error63.message : String(error63)}`
198042
+ );
198043
+ return [];
198044
+ }
198045
+ }
197940
198046
  function CheckoutPrTool(ctx) {
197941
198047
  const runCheckout = async (pull_number, signal) => {
197942
198048
  const prResponse = await ctx.octokit.rest.pulls.get({
@@ -197980,6 +198086,8 @@ function CheckoutPrTool(ctx) {
197980
198086
  if (!checkoutSha) throw new Error("checkout completed without a resolved HEAD SHA");
197981
198087
  const headShort = checkoutSha.slice(0, 7);
197982
198088
  let incrementalDiffPath;
198089
+ let incrementalDiff;
198090
+ let incrementalEmpty = false;
197983
198091
  let incrementalUnavailable = false;
197984
198092
  let incrementalUnavailableReason;
197985
198093
  if (primary.beforeSha) {
@@ -197987,7 +198095,8 @@ function CheckoutPrTool(ctx) {
197987
198095
  const incremental = computeIncrementalDiff({
197988
198096
  baseBranch: pr.baseRef,
197989
198097
  beforeSha: primary.beforeSha,
197990
- headSha: checkoutSha
198098
+ headSha: checkoutSha,
198099
+ formerBaseTips: await getFormerBaseTips(ctx, pull_number)
197991
198100
  });
197992
198101
  if (incremental.status === "ok") {
197993
198102
  incrementalDiffPath = join16(
@@ -197995,9 +198104,12 @@ function CheckoutPrTool(ctx) {
197995
198104
  `pr-${pull_number}-${beforeShort}-${headShort}-incremental.diff`
197996
198105
  );
197997
198106
  writeFileSync10(incrementalDiffPath, incremental.diff);
198107
+ incrementalDiff = incremental.diff;
197998
198108
  log.info(
197999
198109
  `\xBB incremental diff computed (${incremental.diff.length} bytes) \u2192 ${incrementalDiffPath}`
198000
198110
  );
198111
+ } else if (incremental.status === "empty") {
198112
+ incrementalEmpty = true;
198001
198113
  } else if (incremental.status === "unavailable") {
198002
198114
  incrementalUnavailable = true;
198003
198115
  incrementalUnavailableReason = incremental.reason;
@@ -198010,14 +198122,16 @@ ${diffPreview}`);
198010
198122
  const diffPath = join16(tempDir, `pr-${pull_number}-${headShort}.diff`);
198011
198123
  writeFileSync10(diffPath, formatResult.content);
198012
198124
  log.debug(`wrote diff to ${diffPath} (${formatResult.content.length} bytes)`);
198013
- primary.diffCoverage = createDiffCoverageState({
198014
- diffPath,
198015
- totalLines: countLines({ content: formatResult.content }),
198016
- toc: formatResult.toc,
198125
+ const coverageEntries = incrementalDiff ? parseIncrementalDiffEntries({ content: incrementalDiff }) : parseDiffTocEntries({ toc: formatResult.toc });
198126
+ primary.diffCoverage = incrementalEmpty ? void 0 : createDiffCoverageState({
198127
+ diffPath: incrementalDiffPath ?? diffPath,
198128
+ totalLines: countLines({ content: incrementalDiff ?? formatResult.content }),
198129
+ tocEntries: coverageEntries.filter((entry) => !GENERATED.test(entry.filename)),
198017
198130
  previous: primary.diffCoverage
198018
198131
  });
198132
+ const coverage = primary.diffCoverage;
198019
198133
  log.debug(
198020
- `\xBB diff coverage initialized: diffPath=${diffPath}, totalLines=${primary.diffCoverage.totalLines}, tocEntries=${primary.diffCoverage.tocEntries.length}`
198134
+ coverage ? `\xBB diff coverage initialized: diffPath=${coverage.diffPath}, totalLines=${coverage.totalLines}, tocEntries=${coverage.tocEntries.length}` : "\xBB diff coverage skipped: no changes since the last reviewed version"
198021
198135
  );
198022
198136
  let impactPath;
198023
198137
  if (process.env.PULLFROG_DISABLE_CHANGE_IMPACT === "1") {
@@ -198068,7 +198182,7 @@ ${diffPreview}`);
198068
198182
  primary.commentableLinesByFile = cached4;
198069
198183
  primary.commentableLinesPullNumber = pull_number;
198070
198184
  primary.commentableLinesCheckoutSha = checkoutSha;
198071
- const incrementalInstructions = incrementalDiffPath ? `IMPORTANT: incrementalDiffPath contains ONLY the changes since the last reviewed version (computed via range-diff). you MUST read incrementalDiffPath FIRST to understand what changed, then use diffPath for full PR context. do NOT skip the incremental diff. ` : incrementalUnavailable ? (
198185
+ const incrementalInstructions = incrementalDiffPath ? `IMPORTANT: incrementalDiffPath contains ONLY the changes since the last reviewed version (computed via range-diff), and it \u2014 not diffPath's TOC \u2014 is this review's coverage checklist. read it FIRST and in full, then read only the diffPath ranges the delta needs for context. ` : incrementalEmpty ? `NOTE: the PR's net changes are identical to the last reviewed version (e.g. a rebase with no content change), so there is no new diff to cover. ` : incrementalUnavailable ? (
198072
198186
  // say it FAILED rather than leaving the agent to infer "nothing changed"
198073
198187
  // from an absent field. only suggest diff-tree when the previous
198074
198188
  // revision is actually PRESENT locally — if the fetch never landed it,
@@ -198119,7 +198233,7 @@ ${diffPreview}`);
198119
198233
  commitLogTruncated,
198120
198234
  commitLogUnavailable,
198121
198235
  hookWarning: checkoutResult.hookWarning,
198122
- instructions: incrementalInstructions + `the diff file at diffPath contains a table of contents (TOC) at the top listing every changed file with its line range. use the TOC line ranges as your checklist and read specific files from the diff instead of reading the entire file. for example, if the TOC says "src/foo.ts \u2192 lines 5-42", read lines 5-42 from diffPath to see that file's changes. review files selectively based on relevance rather than reading everything sequentially. to inspect the PR's changed files, use diffPath \u2014 do NOT run \`git diff\` to re-derive what's already in diffPath. the formatted diff with line numbers is authoritative. if you ever do need a branch-vs-base diff via the git tool, use \`git diff --merge-base <base>\` (single call, includes uncommitted edits) or three-dot \`git diff <base>...HEAD\` (committed-only). bare \`<base>\` and two-dot \`<base>..HEAD\` are symmetric and pull in the inverse of every commit landed on \`<base>\` since the branch forked \u2014 the git tool will reject those forms when the divergence is detected. \`$(...)\` subshells are NOT expanded by the git tool. \`git log\` and \`git diff --stat\` are fine for commit-range overview, and \`git diff\` / \`git diff --cached\` are fine for inspecting *your own* uncommitted changes \u2014 but PR review content MUST come from diffPath. before your review is submitted, a one-time coverage pre-flight may error listing unread TOC regions. retry the same create_pull_request_review call to proceed \u2014 optionally after reading the listed ranges. the pre-flight will not block again this session. the local branch is 'localBranch' (pr-{number}), not the remote branch name. when pushing, omit branchName to use the current branch. do not use remoteBranch as a local branch name.` + impactInstructions + hookWarningInstructions + commitLogInstructions
198236
+ instructions: incrementalInstructions + `the diff file at diffPath contains a table of contents (TOC) at the top listing every changed file with its line range. absent incrementalDiffPath, use the TOC line ranges as your checklist (entries marked \`generated\` are off it) and read specific files from the diff instead of reading the entire file. for example, if the TOC says "src/foo.ts \u2192 lines 5-42", read lines 5-42 from diffPath to see that file's changes. review files selectively based on relevance rather than reading everything sequentially. to inspect the PR's changed files, use diffPath \u2014 do NOT run \`git diff\` to re-derive what's already in diffPath. the formatted diff with line numbers is authoritative. if you ever do need a branch-vs-base diff via the git tool, use \`git diff --merge-base <base>\` (single call, includes uncommitted edits) or three-dot \`git diff <base>...HEAD\` (committed-only). bare \`<base>\` and two-dot \`<base>..HEAD\` are symmetric and pull in the inverse of every commit landed on \`<base>\` since the branch forked \u2014 the git tool will reject those forms when the divergence is detected. \`$(...)\` subshells are NOT expanded by the git tool. \`git log\` and \`git diff --stat\` are fine for commit-range overview, and \`git diff\` / \`git diff --cached\` are fine for inspecting *your own* uncommitted changes \u2014 but PR review content MUST come from diffPath. before your review is submitted, a one-time coverage pre-flight may error listing unread TOC regions. retry the same create_pull_request_review call to proceed \u2014 optionally after reading the listed ranges. the pre-flight will not block again this session. the local branch is 'localBranch' (pr-{number}), not the remote branch name. when pushing, omit branchName to use the current branch. do not use remoteBranch as a local branch name.` + impactInstructions + hookWarningInstructions + commitLogInstructions
198123
198237
  };
198124
198238
  };
198125
198239
  return tool({
@@ -204620,7 +204734,7 @@ async function postReviewCleanup(ctx) {
204620
204734
  if (!review) {
204621
204735
  const checkoutSha = primaryRepoState(ctx.toolState).checkoutSha;
204622
204736
  if (ctx.toolState.selectedMode === "IncrementalReview" && checkoutSha) {
204623
- await patchWorkflowRunFields(ctx, { reviewSkipped: true });
204737
+ await patchWorkflowRunFields(ctx, { reviewSkipped: true, reviewedCommitSha: checkoutSha });
204624
204738
  await bestEffort(
204625
204739
  () => dispatchFollowUpReReview(ctx, checkoutSha),
204626
204740
  "follow-up re-review dispatch"
@@ -204629,7 +204743,7 @@ async function postReviewCleanup(ctx) {
204629
204743
  return;
204630
204744
  }
204631
204745
  delete ctx.toolState.review;
204632
- await bestEffort(() => reportReviewNodeId(ctx, { nodeId: review.nodeId }), "reportReviewNodeId");
204746
+ await bestEffort(() => reportReviewNodeId(ctx, review), "reportReviewNodeId");
204633
204747
  if (review.reviewedSha) {
204634
204748
  await bestEffort(
204635
204749
  () => dispatchFollowUpReReview(ctx, review.reviewedSha),
@@ -17,6 +17,7 @@ export { decodeJwtExpMs, OAuthInvalidGrantError, parseCodexAuthBody, refreshCode
17
17
  export type { CredentialVerdict } from "../utils/credentialCheck.ts";
18
18
  export { isCredentialProbeable, verifyCredential } from "../utils/credentialCheck.ts";
19
19
  export { renderFailure } from "../utils/failureBody.ts";
20
+ export { GENERATED } from "../utils/generatedFiles.ts";
20
21
  export type { ResourceUsage, UsageSummary } from "../utils/github.ts";
21
22
  export { isPullfrog } from "../utils/isPullfrog.ts";
22
23
  export { isLeapingIntoActionCommentBody, isLiveChecklistCommentBody, LEAPING_INTO_ACTION_PREFIX, } from "../utils/leapingComment.ts";
package/dist/internal.js CHANGED
@@ -8,7 +8,7 @@ var __export = (target, all) => {
8
8
  // package.json
9
9
  var package_default = {
10
10
  name: "pullfrog",
11
- version: "0.1.102",
11
+ version: "0.1.104",
12
12
  cliContractVersion: "1",
13
13
  type: "module",
14
14
  bin: {
@@ -21026,7 +21026,7 @@ var providers = {
21026
21026
  },
21027
21027
  // Zen's FREE open-weights preview (listed 2026-10-05), the eighth promo.
21028
21028
  // Zen's docs: no training, 30-day retention for safety. hidden until a
21029
- // published release carries the alias.
21029
+ // published release carries the alias; 0.1.102 is the first.
21030
21030
  beam: {
21031
21031
  displayName: "Beam",
21032
21032
  description: "Preview, free for a limited time",
@@ -21034,8 +21034,7 @@ var providers = {
21034
21034
  effort: ["low", "medium", "high", "xhigh", "max"],
21035
21035
  // free to run, still gated on the provider's own OPENCODE_API_KEY —
21036
21036
  // see the big-pickle note above (#1077).
21037
- isFree: true,
21038
- hidden: true
21037
+ isFree: true
21039
21038
  },
21040
21039
  // Zen's live free MiMo, and the second free row in a menu that big-pickle
21041
21040
  // was alone in since `mimo-v2-pro-free` lost its model.
@@ -21408,10 +21407,12 @@ var providers = {
21408
21407
  openRouterResolve: "openrouter/openai/gpt-5.4",
21409
21408
  fallback: "openrouter/gpt-sol"
21410
21409
  },
21410
+ // OpenRouter dropped `openai/o4-mini` on 2026-10-28 (only `:batch` remains) — folds stored pins to Luna.
21411
21411
  "o4-mini": {
21412
21412
  displayName: "O4 Mini",
21413
21413
  resolve: "openrouter/openai/o4-mini",
21414
- openRouterResolve: "openrouter/openai/o4-mini"
21414
+ openRouterResolve: "openrouter/openai/o4-mini",
21415
+ fallback: "openrouter/gpt-luna"
21415
21416
  },
21416
21417
  "gemini-pro": {
21417
21418
  displayName: "Gemini Pro",
@@ -22133,7 +22134,7 @@ ${PR_SUMMARY_FORMAT}`
22133
22134
 
22134
22135
  2. **checkout**: call \`${t("checkout_pr")}\` \u2014 this returns PR metadata, \`diffPath\` (full diff), \`incrementalDiffPath\` (changes since last reviewed version, if available), and a supplemental \`impactPath\` when change-impact extraction is enabled.
22135
22136
 
22136
- 3. **incremental scope**: if \`incrementalDiffPath\` is present, read it FIRST to see what changed since the last review. this is a range-diff that isolates the net changes, filtering out base branch noise. then read the authoritative full diff end-to-end, beginning with the TOC and using its line ranges as your coverage checklist. if no incremental diff is present, start with the full-diff TOC, determine what changed since Pullfrog's most recent review, and complete the raw-diff read. only after establishing that authoritative scope and completing raw-diff coverage, use \`impactPath\` as an explicitly incomplete list of reference leads; it never replaces raw-diff reading or establishes coverage.
22137
+ 3. **incremental scope**: if \`incrementalDiffPath\` is present, it is your coverage checklist: read it FIRST and in full. it is a range-diff of the net changes since the last review, free of base-branch noise. read only the \`diffPath\` ranges the delta needs for context \u2014 do not re-read the whole PR. if no incremental diff is present, start with the full-diff TOC, determine what changed since Pullfrog's most recent review, and complete the raw-diff read. only after that, use \`impactPath\` as an explicitly incomplete list of reference leads; it never replaces raw-diff reading or establishes coverage.
22137
22138
 
22138
22139
  4. **prior feedback \u2014 read AND retire it**: fetch previous reviews via \`${t("list_pull_request_reviews")}\`, then call \`${t("get_review_comments")}\` on each prior Pullfrog review. Each thread renders as a section whose first line is a fenced tag \`comment author=<login> id=<fullDatabaseId> review=<reviewId> thread=<graphqlId>\`; section headers carry \`[RESOLVED]\` / \`[OUTDATED]\` when relevant. For every **open, Pullfrog-originated** thread, decide and act:
22139
22140
 
@@ -22149,13 +22150,13 @@ ${PR_SUMMARY_FORMAT}`
22149
22150
 
22150
22151
  skip the deeper pass and jump to step 10's non-substantive path (do NOT submit a review) only when the incremental changes have **no behavioral surface at all** \u2014 formatting, comment tweaks, import reordering, lockfile regen, a mechanical rename of import paths. line count is not the signal: a one-line change to auth, money, SQL, a comparison operator, a redirect, or a config default is not trivial.
22151
22152
 
22152
- 6. **specialist decision**: after covering the incremental and full diffs, name the questions about the new changes that you still cannot answer confidently yourself, and dispatch one \`${REVIEWER_AGENT_NAME}\` specialist per question. a question qualifies only when a specialist could return evidence that **changes your disposition** on the PR \u2014 generic requests for another look, extra confidence, or polish do not. most incremental reviews need zero or one, especially thread-reply re-reviews; some need several.
22153
+ 6. **specialist decision**: after covering the delta, name the questions about the new changes that you still cannot answer confidently yourself, and dispatch one \`${REVIEWER_AGENT_NAME}\` specialist per question. a question qualifies only when a specialist could return evidence that **changes your disposition** on the PR \u2014 generic requests for another look, extra confidence, or polish do not. most incremental reviews need zero or one, especially thread-reply re-reviews; some need several.
22153
22154
 
22154
22155
  **There is NO one-specialist cap or fixed maximum.** cover every orthogonal question that remains; do not collapse several real questions into one broad prompt just to reduce the count. there is no file-count, line-count, or budget threshold either \u2014 diff size is not a proxy for review uncertainty.
22155
22156
 
22156
22157
  frame each question through the lens that primes the right failure modes. for high-stakes subsystems, lead with the **domain** ("the billing lens", "the auth lens", "the schema-migration lens") rather than the generic equivalent \u2014 the domain framing makes the subagent recall failure modes a generic lens misses.
22157
22158
 
22158
- you remain the synthesizer: reading the complete raw full diff plus the incremental diff, investigating surrounding code, validating every returned finding, and writing the review are yours. specialist reads supplement that work; they never satisfy your own coverage obligation.
22159
+ you remain the synthesizer: reading the delta and the context it needs, investigating surrounding code, validating every returned finding, and writing the review are yours. specialist reads supplement that work; they never satisfy your own coverage obligation.
22159
22160
 
22160
22161
  7. **dispatch specialists (only if step 6 found unresolved questions)**: for 2+ questions, emit every Task tool_use block **IN A SINGLE ASSISTANT TURN** before reading any result, so the investigations run in parallel rather than serially. your own \`read\` / \`grep\` / \`webfetch\` calls can ride in that same turn.
22161
22162
 
@@ -22633,6 +22634,9 @@ async function verifyCredential(params) {
22633
22634
  }
22634
22635
  }
22635
22636
 
22637
+ // utils/generatedFiles.ts
22638
+ var GENERATED = /(^|\/)(package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb?|Cargo\.lock|go\.sum|poetry\.lock|Pipfile\.lock|Gemfile\.lock|composer\.lock|.*\.snap|.*\.min\.(js|css)|.*\.map|.*\.lock|.*\.(pb\.go|_pb2\.py|g\.dart)|.*\.(generated|gen)\.[^/]+|.*\.(svg|png|jpe?g|gif|ico|webp|woff2?|ttf|pdf))$|(^|\/)(dist|generated|__generated__|node_modules|\.yarn|__snapshots__|coverage|\.next|\.turbo|drizzle\/meta)\//i;
22639
+
22636
22640
  // utils/isPullfrog.ts
22637
22641
  function isPullfrog(actor) {
22638
22642
  actor = actor?.toLowerCase().replace("[bot]", "");
@@ -23480,6 +23484,7 @@ export {
23480
23484
  DEFAULT_EFFORT_POSITION,
23481
23485
  DEFAULT_PROXY_MODEL,
23482
23486
  EFFORT_ALIASES,
23487
+ GENERATED,
23483
23488
  LEAPING_INTO_ACTION_PREFIX,
23484
23489
  MAX_LEARNINGS_LENGTH,
23485
23490
  OAuthInvalidGrantError,
@@ -263,4 +263,5 @@ export declare function createAndSubmitWithFooter(ctx: ToolContext, params: Rest
263
263
  */
264
264
  export declare function reportReviewNodeId(ctx: ToolContext, params: {
265
265
  nodeId: string;
266
+ reviewedSha: string | undefined;
266
267
  }): Promise<void>;
@@ -38,10 +38,18 @@ export declare function countLines(params: {
38
38
  export declare function parseDiffTocEntries(params: {
39
39
  toc: string;
40
40
  }): DiffTocEntry[];
41
+ /**
42
+ * one entry per section of an incremental (range-diff) diff, which opens on an
43
+ * `@@ <file>[: <function>]` hunk header or a `## <file>[ (new)] ##` file header,
44
+ * either of which may be the only line naming its file.
45
+ */
46
+ export declare function parseIncrementalDiffEntries(params: {
47
+ content: string;
48
+ }): DiffTocEntry[];
41
49
  export declare function createDiffCoverageState(params: {
42
50
  diffPath: string;
43
51
  totalLines: number;
44
- toc: string;
52
+ tocEntries: DiffTocEntry[];
45
53
  previous?: DiffCoverageState | undefined;
46
54
  }): DiffCoverageState;
47
55
  export declare function recordDiffReadFromToolUse(params: {
@@ -0,0 +1,10 @@
1
+ /**
2
+ * files whose hunks carry nothing a reviewer weighs — lockfiles, minified and
3
+ * generated output, snapshots, images and fonts, built trees, migration
4
+ * metadata. the router's scorer reads a one-line stub in their place and its
5
+ * heuristic skips their lines; a review's diff keeps them but leaves them off
6
+ * its coverage checklist. `build/` and `vendor/` are deliberately not here: the
7
+ * router eval set has source under both (`src/build/expo-brand.ts`, a vendored
8
+ * crate patched in the PR), and hiding a patch is worse than reading a copy.
9
+ */
10
+ export declare const GENERATED: RegExp;
@@ -4,6 +4,8 @@ import type { ToolContext } from "../mcp/server.ts";
4
4
  * String-valued PATCH fields (all serialized identically on the wire):
5
5
  * - artifact node IDs (`*NodeId`, `summarySnapshot`) — PATCHed incrementally
6
6
  * by MCP tools as GitHub entities are created during the run.
7
+ * - `reviewedCommitSha` — the PR head a review covered, sent with `reviewNodeId`
8
+ * or `reviewSkipped`; the next re-review's delta starts there.
7
9
  * - `model` — the resolved/effective model the run actually ran on (proxy spec
8
10
  * for router/oss, post-fallback slug otherwise; NOT the configured
9
11
  * `Repo.model` slug), PATCHed once at end-of-run so per-model cost analytics
@@ -13,7 +15,7 @@ import type { ToolContext } from "../mcp/server.ts";
13
15
  * workflow can start the action several times in one run, and the server keeps
14
16
  * one row per start, keyed by `START_KEY`, and sums them onto the run.
15
17
  */
16
- declare const STRING_KEYS: readonly ["prNodeId", "issueNodeId", "reviewNodeId", "planCommentNodeId", "summarySnapshot", "model", "agent", "credential"];
18
+ declare const STRING_KEYS: readonly ["prNodeId", "issueNodeId", "reviewNodeId", "reviewedCommitSha", "planCommentNodeId", "summarySnapshot", "model", "agent", "credential"];
17
19
  /**
18
20
  * Number-valued usage fields — aggregated across all agent calls and PATCHed
19
21
  * once at end-of-run. Token counts are Int4 on the DB side (ample for any
@@ -23,7 +25,7 @@ declare const STRING_KEYS: readonly ["prNodeId", "issueNodeId", "reviewNodeId",
23
25
  declare const NUMBER_KEYS: readonly ["inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens", "costUsd"];
24
26
  /** one-way flags, only ever sent as `true`. Keep in sync with `BOOLEAN_FIELDS` in the server route. */
25
27
  declare const FLAG_KEYS: readonly ["reviewSkipped"];
26
- export type WorkflowRunPatch = Partial<Record<(typeof STRING_KEYS)[number], string>> & Partial<Record<(typeof NUMBER_KEYS)[number], number>> & Partial<Record<(typeof FLAG_KEYS)[number], true>> & {
28
+ export type WorkflowRunPatch = Partial<Record<(typeof STRING_KEYS)[number], string | undefined>> & Partial<Record<(typeof NUMBER_KEYS)[number], number>> & Partial<Record<(typeof FLAG_KEYS)[number], true>> & {
27
29
  succeeded?: boolean;
28
30
  };
29
31
  /** PATCH workflow-run fields (Pullfrog JWT, not GitHub). */
@@ -2,6 +2,8 @@ type ComputeIncrementalDiffParams = {
2
2
  baseBranch: string;
3
3
  beforeSha: string;
4
4
  headSha: string;
5
+ /** tips the base branch was force-pushed away from, which either version may still be built on */
6
+ formerBaseTips: string[];
5
7
  };
6
8
  /**
7
9
  * computes the incremental diff between two versions of a PR using range-diff
@@ -64,7 +66,9 @@ export declare function computeIncrementalDiff(params: ComputeIncrementalDiffPar
64
66
  *
65
67
  * uses a streaming approach: a ring buffer of before-context lines is flushed when
66
68
  * a change is hit, then afterCount lines of after-context are emitted directly.
67
- * nearest preceding ## / @@ headers are force-included when outside the context window.
69
+ * nearest preceding ## / @@ headers are force-included when outside the context window,
70
+ * and so is the outer `@@ <file>[: <function>]` hunk header, which is often the only
71
+ * line naming the file a change belongs to.
68
72
  */
69
73
  export declare function postProcessRangeDiff(raw: string, contextLines?: number): string | null;
70
74
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pullfrog",
3
- "version": "0.1.102",
3
+ "version": "0.1.104",
4
4
  "cliContractVersion": "1",
5
5
  "type": "module",
6
6
  "bin": {