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 +150 -36
- package/dist/index.js +148 -34
- package/dist/internal/index.d.ts +1 -0
- package/dist/internal.js +13 -8
- package/dist/mcp/review.d.ts +1 -0
- package/dist/utils/diffCoverage.d.ts +9 -1
- package/dist/utils/generatedFiles.d.ts +10 -0
- package/dist/utils/patchWorkflowRunFields.d.ts +4 -2
- package/dist/utils/rangeDiff.d.ts +5 -1
- package/package.json +1 -1
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.
|
|
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,
|
|
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
|
|
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
|
|
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(
|
|
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:
|
|
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
|
-
'
|
|
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
|
-
|
|
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
|
-
|
|
193676
|
-
|
|
193677
|
-
|
|
193678
|
-
|
|
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 (
|
|
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 (
|
|
193705
|
-
|
|
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, {
|
|
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
|
-
|
|
201532
|
-
|
|
201533
|
-
|
|
201534
|
-
|
|
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=${
|
|
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)
|
|
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,
|
|
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.
|
|
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.
|
|
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.
|
|
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,
|
|
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
|
|
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
|
|
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(
|
|
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:
|
|
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
|
-
'
|
|
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
|
-
|
|
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
|
-
|
|
190158
|
-
|
|
190159
|
-
|
|
190160
|
-
|
|
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 (
|
|
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 (
|
|
190187
|
-
|
|
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, {
|
|
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
|
-
|
|
198014
|
-
|
|
198015
|
-
|
|
198016
|
-
|
|
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=${
|
|
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)
|
|
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,
|
|
204746
|
+
await bestEffort(() => reportReviewNodeId(ctx, review), "reportReviewNodeId");
|
|
204633
204747
|
if (review.reviewedSha) {
|
|
204634
204748
|
await bestEffort(
|
|
204635
204749
|
() => dispatchFollowUpReReview(ctx, review.reviewedSha),
|
package/dist/internal/index.d.ts
CHANGED
|
@@ -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.
|
|
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,
|
|
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
|
|
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
|
|
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,
|
package/dist/mcp/review.d.ts
CHANGED
|
@@ -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
|
-
|
|
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 {};
|