pullfrog 0.1.102 → 0.1.103

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.103",
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";
@@ -193632,7 +193651,7 @@ function computeIncrementalDiff(params) {
193632
193651
  "sh",
193633
193652
  [
193634
193653
  "-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)"',
193654
+ 'old_base=$(git merge-base "$1" "origin/$2") && new_base=$(git merge-base "$3" "origin/$2") && git range-diff --no-color --creation-factor=1000 "$old_base..$(git commit-tree "$1^{tree}" -p "$old_base" -m x)" "$new_base..$(git commit-tree "$3^{tree}" -p "$new_base" -m x)"',
193636
193655
  "--",
193637
193656
  params.beforeSha,
193638
193657
  params.baseBranch,
@@ -193655,6 +193674,8 @@ function postProcessRangeDiff(raw2, contextLines = 3) {
193655
193674
  if (!raw2.trim()) return null;
193656
193675
  if (/^\d+:\s+\w+\s+=\s+\d+:/m.test(raw2)) return null;
193657
193676
  const beforeBuf = [];
193677
+ let lastOuterHdr = null;
193678
+ let outerHdrEmitted = true;
193658
193679
  let lastFileHdr = null;
193659
193680
  let lastHunkHdr = null;
193660
193681
  let fileHdrEmitted = true;
@@ -193668,10 +193689,12 @@ function postProcessRangeDiff(raw2, contextLines = 3) {
193668
193689
  if (lastEmittedSeq >= 0 && line.seq > lastEmittedSeq + 1) out += (out ? "\n" : "") + "...";
193669
193690
  out += (out ? "\n" : "") + line.prefix + raw2.slice(line.from, line.to);
193670
193691
  lastEmittedSeq = line.seq;
193692
+ if (lastOuterHdr?.seq === line.seq) outerHdrEmitted = true;
193671
193693
  if (lastFileHdr?.seq === line.seq) fileHdrEmitted = true;
193672
193694
  if (lastHunkHdr?.seq === line.seq) hunkHdrEmitted = true;
193673
193695
  }
193674
193696
  function flushBefore() {
193697
+ if (lastOuterHdr && !outerHdrEmitted) emit2(lastOuterHdr);
193675
193698
  if (lastFileHdr && !fileHdrEmitted) emit2(lastFileHdr);
193676
193699
  if (lastHunkHdr && !hunkHdrEmitted) emit2(lastHunkHdr);
193677
193700
  for (const line of beforeBuf) {
@@ -193687,7 +193710,17 @@ function postProcessRangeDiff(raw2, contextLines = 3) {
193687
193710
  cursor3 = lineEnd + 1;
193688
193711
  continue;
193689
193712
  }
193690
- if (lineEnd - cursor3 >= 5 && raw2.startsWith(" ", cursor3)) {
193713
+ if (raw2.startsWith(" @@", cursor3)) {
193714
+ lastOuterHdr = { prefix: "", from: cursor3 + 4, to: lineEnd, seq };
193715
+ outerHdrEmitted = false;
193716
+ lastFileHdr = null;
193717
+ fileHdrEmitted = true;
193718
+ lastHunkHdr = null;
193719
+ hunkHdrEmitted = true;
193720
+ beforeBuf.length = 0;
193721
+ afterRemaining = 0;
193722
+ seq++;
193723
+ } else if (lineEnd - cursor3 >= 5 && raw2.startsWith(" ", cursor3)) {
193691
193724
  const prefix = raw2[cursor3 + 4];
193692
193725
  if (isDiffPrefix(prefix)) {
193693
193726
  const contentPos = cursor3 + 5;
@@ -201132,8 +201165,9 @@ function formatFilesWithLineNumbers(files) {
201132
201165
  const tocLines = [`## Files (${files.length})`];
201133
201166
  for (const entry of tocEntries) {
201134
201167
  const anchor2 = createHash3("sha256").update(entry.filename).digest("hex");
201168
+ const generated = GENERATED.test(entry.filename) ? " \xB7 generated" : "";
201135
201169
  tocLines.push(
201136
- `- ${entry.filename} \u2192 lines ${entry.startLine}-${entry.endLine} \xB7 diff-${anchor2}`
201170
+ `- ${entry.filename} \u2192 lines ${entry.startLine}-${entry.endLine} \xB7 diff-${anchor2}${generated}`
201137
201171
  );
201138
201172
  }
201139
201173
  tocLines.push("");
@@ -201498,6 +201532,8 @@ function CheckoutPrTool(ctx) {
201498
201532
  if (!checkoutSha) throw new Error("checkout completed without a resolved HEAD SHA");
201499
201533
  const headShort = checkoutSha.slice(0, 7);
201500
201534
  let incrementalDiffPath;
201535
+ let incrementalDiff;
201536
+ let incrementalEmpty = false;
201501
201537
  let incrementalUnavailable = false;
201502
201538
  let incrementalUnavailableReason;
201503
201539
  if (primary.beforeSha) {
@@ -201513,9 +201549,12 @@ function CheckoutPrTool(ctx) {
201513
201549
  `pr-${pull_number}-${beforeShort}-${headShort}-incremental.diff`
201514
201550
  );
201515
201551
  writeFileSync11(incrementalDiffPath, incremental.diff);
201552
+ incrementalDiff = incremental.diff;
201516
201553
  log2.info(
201517
201554
  `\xBB incremental diff computed (${incremental.diff.length} bytes) \u2192 ${incrementalDiffPath}`
201518
201555
  );
201556
+ } else if (incremental.status === "empty") {
201557
+ incrementalEmpty = true;
201519
201558
  } else if (incremental.status === "unavailable") {
201520
201559
  incrementalUnavailable = true;
201521
201560
  incrementalUnavailableReason = incremental.reason;
@@ -201528,14 +201567,16 @@ ${diffPreview}`);
201528
201567
  const diffPath = join17(tempDir, `pr-${pull_number}-${headShort}.diff`);
201529
201568
  writeFileSync11(diffPath, formatResult.content);
201530
201569
  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,
201570
+ const coverageEntries = incrementalDiff ? parseIncrementalDiffEntries({ content: incrementalDiff }) : parseDiffTocEntries({ toc: formatResult.toc });
201571
+ primary.diffCoverage = incrementalEmpty ? void 0 : createDiffCoverageState({
201572
+ diffPath: incrementalDiffPath ?? diffPath,
201573
+ totalLines: countLines({ content: incrementalDiff ?? formatResult.content }),
201574
+ tocEntries: coverageEntries.filter((entry) => !GENERATED.test(entry.filename)),
201535
201575
  previous: primary.diffCoverage
201536
201576
  });
201577
+ const coverage = primary.diffCoverage;
201537
201578
  log2.debug(
201538
- `\xBB diff coverage initialized: diffPath=${diffPath}, totalLines=${primary.diffCoverage.totalLines}, tocEntries=${primary.diffCoverage.tocEntries.length}`
201579
+ 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
201580
  );
201540
201581
  let impactPath;
201541
201582
  if (process.env.PULLFROG_DISABLE_CHANGE_IMPACT === "1") {
@@ -201586,7 +201627,7 @@ ${diffPreview}`);
201586
201627
  primary.commentableLinesByFile = cached4;
201587
201628
  primary.commentableLinesPullNumber = pull_number;
201588
201629
  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 ? (
201630
+ 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
201631
  // say it FAILED rather than leaving the agent to infer "nothing changed"
201591
201632
  // from an absent field. only suggest diff-tree when the previous
201592
201633
  // revision is actually PRESENT locally — if the fetch never landed it,
@@ -201637,7 +201678,7 @@ ${diffPreview}`);
201637
201678
  commitLogTruncated,
201638
201679
  commitLogUnavailable,
201639
201680
  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
201681
+ 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
201682
  };
201642
201683
  };
201643
201684
  return tool({
@@ -209874,7 +209915,7 @@ function resolveTarget2(repo) {
209874
209915
  return { owner: match3[1], repo: match3[2] };
209875
209916
  }
209876
209917
  function cliVersion() {
209877
- const parts = /^(\d+)\.(\d+)\.(\d+)$/.exec("0.1.102");
209918
+ const parts = /^(\d+)\.(\d+)\.(\d+)$/.exec("0.1.103");
209878
209919
  if (!parts) return "0.0.0";
209879
209920
  return `${Number(parts[1])}.${Number(parts[2])}.${Number(parts[3])}`;
209880
209921
  }
@@ -210041,7 +210082,7 @@ async function runCli8(input2) {
210041
210082
  }
210042
210083
 
210043
210084
  // cli.ts
210044
- var VERSION10 = "0.1.102";
210085
+ var VERSION10 = "0.1.103";
210045
210086
  var bin = basename2(process.argv[1] || "");
210046
210087
  var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
210047
210088
  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.103",
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";
@@ -190114,7 +190133,7 @@ function computeIncrementalDiff(params) {
190114
190133
  "sh",
190115
190134
  [
190116
190135
  "-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)"',
190136
+ 'old_base=$(git merge-base "$1" "origin/$2") && new_base=$(git merge-base "$3" "origin/$2") && git range-diff --no-color --creation-factor=1000 "$old_base..$(git commit-tree "$1^{tree}" -p "$old_base" -m x)" "$new_base..$(git commit-tree "$3^{tree}" -p "$new_base" -m x)"',
190118
190137
  "--",
190119
190138
  params.beforeSha,
190120
190139
  params.baseBranch,
@@ -190137,6 +190156,8 @@ function postProcessRangeDiff(raw2, contextLines = 3) {
190137
190156
  if (!raw2.trim()) return null;
190138
190157
  if (/^\d+:\s+\w+\s+=\s+\d+:/m.test(raw2)) return null;
190139
190158
  const beforeBuf = [];
190159
+ let lastOuterHdr = null;
190160
+ let outerHdrEmitted = true;
190140
190161
  let lastFileHdr = null;
190141
190162
  let lastHunkHdr = null;
190142
190163
  let fileHdrEmitted = true;
@@ -190150,10 +190171,12 @@ function postProcessRangeDiff(raw2, contextLines = 3) {
190150
190171
  if (lastEmittedSeq >= 0 && line.seq > lastEmittedSeq + 1) out += (out ? "\n" : "") + "...";
190151
190172
  out += (out ? "\n" : "") + line.prefix + raw2.slice(line.from, line.to);
190152
190173
  lastEmittedSeq = line.seq;
190174
+ if (lastOuterHdr?.seq === line.seq) outerHdrEmitted = true;
190153
190175
  if (lastFileHdr?.seq === line.seq) fileHdrEmitted = true;
190154
190176
  if (lastHunkHdr?.seq === line.seq) hunkHdrEmitted = true;
190155
190177
  }
190156
190178
  function flushBefore() {
190179
+ if (lastOuterHdr && !outerHdrEmitted) emit(lastOuterHdr);
190157
190180
  if (lastFileHdr && !fileHdrEmitted) emit(lastFileHdr);
190158
190181
  if (lastHunkHdr && !hunkHdrEmitted) emit(lastHunkHdr);
190159
190182
  for (const line of beforeBuf) {
@@ -190169,7 +190192,17 @@ function postProcessRangeDiff(raw2, contextLines = 3) {
190169
190192
  cursor = lineEnd + 1;
190170
190193
  continue;
190171
190194
  }
190172
- if (lineEnd - cursor >= 5 && raw2.startsWith(" ", cursor)) {
190195
+ if (raw2.startsWith(" @@", cursor)) {
190196
+ lastOuterHdr = { prefix: "", from: cursor + 4, to: lineEnd, seq };
190197
+ outerHdrEmitted = false;
190198
+ lastFileHdr = null;
190199
+ fileHdrEmitted = true;
190200
+ lastHunkHdr = null;
190201
+ hunkHdrEmitted = true;
190202
+ beforeBuf.length = 0;
190203
+ afterRemaining = 0;
190204
+ seq++;
190205
+ } else if (lineEnd - cursor >= 5 && raw2.startsWith(" ", cursor)) {
190173
190206
  const prefix = raw2[cursor + 4];
190174
190207
  if (isDiffPrefix(prefix)) {
190175
190208
  const contentPos = cursor + 5;
@@ -197614,8 +197647,9 @@ function formatFilesWithLineNumbers(files) {
197614
197647
  const tocLines = [`## Files (${files.length})`];
197615
197648
  for (const entry of tocEntries) {
197616
197649
  const anchor2 = createHash2("sha256").update(entry.filename).digest("hex");
197650
+ const generated = GENERATED.test(entry.filename) ? " \xB7 generated" : "";
197617
197651
  tocLines.push(
197618
- `- ${entry.filename} \u2192 lines ${entry.startLine}-${entry.endLine} \xB7 diff-${anchor2}`
197652
+ `- ${entry.filename} \u2192 lines ${entry.startLine}-${entry.endLine} \xB7 diff-${anchor2}${generated}`
197619
197653
  );
197620
197654
  }
197621
197655
  tocLines.push("");
@@ -197980,6 +198014,8 @@ function CheckoutPrTool(ctx) {
197980
198014
  if (!checkoutSha) throw new Error("checkout completed without a resolved HEAD SHA");
197981
198015
  const headShort = checkoutSha.slice(0, 7);
197982
198016
  let incrementalDiffPath;
198017
+ let incrementalDiff;
198018
+ let incrementalEmpty = false;
197983
198019
  let incrementalUnavailable = false;
197984
198020
  let incrementalUnavailableReason;
197985
198021
  if (primary.beforeSha) {
@@ -197995,9 +198031,12 @@ function CheckoutPrTool(ctx) {
197995
198031
  `pr-${pull_number}-${beforeShort}-${headShort}-incremental.diff`
197996
198032
  );
197997
198033
  writeFileSync10(incrementalDiffPath, incremental.diff);
198034
+ incrementalDiff = incremental.diff;
197998
198035
  log.info(
197999
198036
  `\xBB incremental diff computed (${incremental.diff.length} bytes) \u2192 ${incrementalDiffPath}`
198000
198037
  );
198038
+ } else if (incremental.status === "empty") {
198039
+ incrementalEmpty = true;
198001
198040
  } else if (incremental.status === "unavailable") {
198002
198041
  incrementalUnavailable = true;
198003
198042
  incrementalUnavailableReason = incremental.reason;
@@ -198010,14 +198049,16 @@ ${diffPreview}`);
198010
198049
  const diffPath = join16(tempDir, `pr-${pull_number}-${headShort}.diff`);
198011
198050
  writeFileSync10(diffPath, formatResult.content);
198012
198051
  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,
198052
+ const coverageEntries = incrementalDiff ? parseIncrementalDiffEntries({ content: incrementalDiff }) : parseDiffTocEntries({ toc: formatResult.toc });
198053
+ primary.diffCoverage = incrementalEmpty ? void 0 : createDiffCoverageState({
198054
+ diffPath: incrementalDiffPath ?? diffPath,
198055
+ totalLines: countLines({ content: incrementalDiff ?? formatResult.content }),
198056
+ tocEntries: coverageEntries.filter((entry) => !GENERATED.test(entry.filename)),
198017
198057
  previous: primary.diffCoverage
198018
198058
  });
198059
+ const coverage = primary.diffCoverage;
198019
198060
  log.debug(
198020
- `\xBB diff coverage initialized: diffPath=${diffPath}, totalLines=${primary.diffCoverage.totalLines}, tocEntries=${primary.diffCoverage.tocEntries.length}`
198061
+ 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
198062
  );
198022
198063
  let impactPath;
198023
198064
  if (process.env.PULLFROG_DISABLE_CHANGE_IMPACT === "1") {
@@ -198068,7 +198109,7 @@ ${diffPreview}`);
198068
198109
  primary.commentableLinesByFile = cached4;
198069
198110
  primary.commentableLinesPullNumber = pull_number;
198070
198111
  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 ? (
198112
+ 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
198113
  // say it FAILED rather than leaving the agent to infer "nothing changed"
198073
198114
  // from an absent field. only suggest diff-tree when the previous
198074
198115
  // revision is actually PRESENT locally — if the fetch never landed it,
@@ -198119,7 +198160,7 @@ ${diffPreview}`);
198119
198160
  commitLogTruncated,
198120
198161
  commitLogUnavailable,
198121
198162
  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
198163
+ 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
198164
  };
198124
198165
  };
198125
198166
  return tool({
@@ -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.103",
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,
@@ -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;
@@ -64,7 +64,9 @@ export declare function computeIncrementalDiff(params: ComputeIncrementalDiffPar
64
64
  *
65
65
  * uses a streaming approach: a ring buffer of before-context lines is flushed when
66
66
  * 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.
67
+ * nearest preceding ## / @@ headers are force-included when outside the context window,
68
+ * and so is the outer `@@ <file>[: <function>]` hunk header, which is often the only
69
+ * line naming the file a change belongs to.
68
70
  */
69
71
  export declare function postProcessRangeDiff(raw: string, contextLines?: number): string | null;
70
72
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pullfrog",
3
- "version": "0.1.102",
3
+ "version": "0.1.103",
4
4
  "cliContractVersion": "1",
5
5
  "type": "module",
6
6
  "bin": {