pullfrog 0.1.36 → 0.1.37
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 +47 -9
- package/dist/index.js +46 -8
- package/dist/internal.js +5 -2
- package/dist/mcp/review.d.ts +37 -3
- package/dist/toolState.d.ts +2 -1
- package/dist/utils/buildPullfrogFooter.d.ts +1 -1
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -102064,7 +102064,7 @@ var providers = {
|
|
|
102064
102064
|
envVars: ["OPENCODE_API_KEY"],
|
|
102065
102065
|
models: {
|
|
102066
102066
|
"glm-5.1": {
|
|
102067
|
-
displayName: "GLM 5.
|
|
102067
|
+
displayName: "GLM 5.2",
|
|
102068
102068
|
resolve: "opencode-go/glm-5.2",
|
|
102069
102069
|
openRouterResolve: "openrouter/z-ai/glm-5.2",
|
|
102070
102070
|
preferred: true
|
|
@@ -103001,7 +103001,7 @@ var import_semver = __toESM(require_semver2(), 1);
|
|
|
103001
103001
|
// package.json
|
|
103002
103002
|
var package_default = {
|
|
103003
103003
|
name: "pullfrog",
|
|
103004
|
-
version: "0.1.
|
|
103004
|
+
version: "0.1.37",
|
|
103005
103005
|
type: "module",
|
|
103006
103006
|
bin: {
|
|
103007
103007
|
pullfrog: "dist/cli.mjs",
|
|
@@ -151595,6 +151595,7 @@ function initToolState(params) {
|
|
|
151595
151595
|
progressComment: resolved,
|
|
151596
151596
|
hadProgressComment: !!resolved,
|
|
151597
151597
|
prepushFailureCount: 0,
|
|
151598
|
+
noopReviewSubmissions: 0,
|
|
151598
151599
|
backgroundProcesses: /* @__PURE__ */ new Map(),
|
|
151599
151600
|
usageEntries: []
|
|
151600
151601
|
};
|
|
@@ -159033,7 +159034,10 @@ function formatModelLabel(params) {
|
|
|
159033
159034
|
modelAliases.find((a) => a.resolve === params.model || a.openRouterResolve === params.model);
|
|
159034
159035
|
const displayName = alias?.displayName ?? params.model;
|
|
159035
159036
|
if (params.oss) {
|
|
159036
|
-
|
|
159037
|
+
const ossBase = `\`${displayName}\` (free via [Pullfrog for OSS](https://pullfrog.com/for-oss))`;
|
|
159038
|
+
if (params.clamped?.reason !== "oss") return ossBase;
|
|
159039
|
+
const configured = isAutoTier(params.clamped.from) ? "the intelligent tier" : `\`${resolveDisplayAlias(params.clamped.from)?.displayName ?? params.clamped.from}\``;
|
|
159040
|
+
return `${ossBase} (${configured} not used \u2014 the program covers this model; add its [provider key](https://docs.pullfrog.com/models) to run your pick)`;
|
|
159037
159041
|
}
|
|
159038
159042
|
const base = alias?.isFree ? `\`${displayName}\` (free)` : `\`${displayName}\``;
|
|
159039
159043
|
if (params.fallbackFrom) {
|
|
@@ -159944,12 +159948,23 @@ function duplicateReviewDecision(params) {
|
|
|
159944
159948
|
reason: `review ${existing.id} was already submitted in this session; ignoring duplicate call (call \`checkout_pr\` again first if new commits were pushed)`
|
|
159945
159949
|
};
|
|
159946
159950
|
}
|
|
159951
|
+
var VERDICT_MARKERS = ["> \u2705", "> \u2139\uFE0F", "> [!IMPORTANT]", "> [!CAUTION]"];
|
|
159952
|
+
var MIN_UNMARKED_BODY_LENGTH = 20;
|
|
159953
|
+
var PLACEHOLDER_PATTERN = /\b(test|testing|placeholder|temp|foo|bar|asdf|dummy|sample)\b/i;
|
|
159954
|
+
var PLACEHOLDER_SCAN_LENGTH = 50;
|
|
159955
|
+
function isDegenerateReviewBody(body) {
|
|
159956
|
+
const trimmed = body.trim();
|
|
159957
|
+
if (VERDICT_MARKERS.some((marker) => trimmed.startsWith(marker))) return false;
|
|
159958
|
+
if (trimmed.length < MIN_UNMARKED_BODY_LENGTH) return true;
|
|
159959
|
+
return trimmed.length < PLACEHOLDER_SCAN_LENGTH && PLACEHOLDER_PATTERN.test(trimmed);
|
|
159960
|
+
}
|
|
159961
|
+
var MAX_CONSECUTIVE_NOOP_SUBMISSIONS = 4;
|
|
159947
159962
|
function reviewSkipDecision(params) {
|
|
159948
159963
|
if (params.body || params.hasComments) return null;
|
|
159949
159964
|
if (!params.approved) {
|
|
159950
159965
|
return {
|
|
159951
159966
|
kind: "no-issues",
|
|
159952
|
-
reason: params.requestChanges ? "request_changes with no body or comments \u2014 nothing to block on" : "no issues
|
|
159967
|
+
reason: params.requestChanges ? "request_changes with no body or comments \u2014 nothing to block on" : "this call carried neither `body` nor `comments`, so nothing was posted. if you found no issues, you are done \u2014 do not call this tool again. if you meant to submit a review, the `body` parameter was missing from your tool call: resend it with `body` set to the full review text. if a resend drops `body` again, do not keep retrying and do not probe with placeholder text \u2014 write the review into `create_issue_comment` instead, which takes the same text under a smaller schema and is editable after posting. every successful call here posts a permanent, publicly visible review."
|
|
159953
159968
|
};
|
|
159954
159969
|
}
|
|
159955
159970
|
if (!params.prApproveEnabled) {
|
|
@@ -159977,9 +159992,15 @@ function formatDroppedCommentsNote(dropped) {
|
|
|
159977
159992
|
}
|
|
159978
159993
|
var CreatePullRequestReview = type({
|
|
159979
159994
|
pull_number: type.number.describe("The pull request number to review"),
|
|
159995
|
+
// REQUIRED on purpose, not because an empty review is invalid — pass "" for
|
|
159996
|
+
// that. models that emit tool calls without schema-constrained decoding drop
|
|
159997
|
+
// optional parameters and keep required ones; in the incident log every
|
|
159998
|
+
// required parameter survived and this one, when optional, vanished ~15 times
|
|
159999
|
+
// running. required-ness is the only pressure that empirically held.
|
|
160000
|
+
// see wiki/review-approval.md.
|
|
159980
160001
|
body: type.string.describe(
|
|
159981
|
-
|
|
159982
|
-
)
|
|
160002
|
+
`1-2 sentence high-level summary with urgency level, critical callouts, and feedback about code outside the diff. Specific feedback on diff lines goes in 'comments' array. ALWAYS pass this parameter \u2014 pass an empty string "" when approving with no commentary, never omit it.`
|
|
160003
|
+
),
|
|
159983
160004
|
approved: type.boolean.describe(
|
|
159984
160005
|
"Set to true to submit as an approval. Use for `> \u2705 No new issues found.` reviews where the PR is mergeable as-is and nothing in the body warrants code changes \u2014 approving also suppresses the Fix-button footer affordance so users don't dispatch a fix run on non-actionable feedback. Reserve approved: false for `> \u2139\uFE0F ...` (minor suggestions inline), `> [!IMPORTANT]` (recommended changes), and `> [!CAUTION]` (critical) reviews. Defaults to false (comment-only review). Mutually exclusive with request_changes. Approval is REJECTED while any unresolved Pullfrog review thread remains open on the PR (not just the latest commit's diff): resolve the threads the current code addresses (reply + resolve_review_thread) first, or submit a non-approving review if a real issue remains."
|
|
159985
160006
|
).optional(),
|
|
@@ -160024,6 +160045,11 @@ function CreatePullRequestReviewTool(ctx) {
|
|
|
160024
160045
|
);
|
|
160025
160046
|
}
|
|
160026
160047
|
if (body) body = fixDoubleEscapedString(body);
|
|
160048
|
+
if (body && isDegenerateReviewBody(body)) {
|
|
160049
|
+
throw new Error(
|
|
160050
|
+
`refusing to submit a review whose body looks like placeholder text: ${JSON.stringify(body)}. every submitted review is permanent and publicly visible on the contributor's PR \u2014 never probe this tool with test content. if the tool has been rejecting your submissions, the cause is your payload, not the tool: re-send with \`body\` set to the full review text, opening with a verdict marker (${VERDICT_MARKERS.join(", ")}).`
|
|
160051
|
+
);
|
|
160052
|
+
}
|
|
160027
160053
|
const primary = primaryRepoState(ctx.toolState);
|
|
160028
160054
|
primary.issueNumber = pull_number;
|
|
160029
160055
|
const dup = duplicateReviewDecision({
|
|
@@ -160068,9 +160094,16 @@ function CreatePullRequestReviewTool(ctx) {
|
|
|
160068
160094
|
prApproveEnabled: ctx.prApproveEnabled && !selfAuthored
|
|
160069
160095
|
});
|
|
160070
160096
|
if (skip) {
|
|
160097
|
+
ctx.toolState.noopReviewSubmissions += 1;
|
|
160071
160098
|
log.info(`skipping review submission: ${skip.reason}`);
|
|
160099
|
+
if (ctx.toolState.noopReviewSubmissions >= MAX_CONSECUTIVE_NOOP_SUBMISSIONS) {
|
|
160100
|
+
throw new Error(
|
|
160101
|
+
`${ctx.toolState.noopReviewSubmissions} consecutive review submissions posted nothing \u2014 every one carried an empty payload. stop calling this tool: the run will report the review as unsubmitted. if you have review feedback, your tool calls are dropping the \`body\` parameter \u2014 write the review into \`create_issue_comment\` instead, which takes the same text and is recoverable if it goes wrong.`
|
|
160102
|
+
);
|
|
160103
|
+
}
|
|
160072
160104
|
return { success: true, skipped: true, reason: skip.reason };
|
|
160073
160105
|
}
|
|
160106
|
+
ctx.toolState.noopReviewSubmissions = 0;
|
|
160074
160107
|
let event = approved ? "APPROVE" : request_changes ? "REQUEST_CHANGES" : "COMMENT";
|
|
160075
160108
|
if (event === "APPROVE" || event === "REQUEST_CHANGES") {
|
|
160076
160109
|
if (!ctx.prApproveEnabled) {
|
|
@@ -165901,8 +165934,13 @@ async function resolveProxyModel(ctx) {
|
|
|
165901
165934
|
`\xBB no model selected \u2014 using the cost-optimized default (${ctx.proxyModel}); pick a model in your Pullfrog repo settings for stronger reviews.`
|
|
165902
165935
|
);
|
|
165903
165936
|
}
|
|
165904
|
-
if (
|
|
165905
|
-
if (
|
|
165937
|
+
if (ctx.payload.model && ctx.proxyModel === DEFAULT_PROXY_MODEL && resolveOpenRouterModel(ctx.payload.model) !== DEFAULT_PROXY_MODEL) {
|
|
165938
|
+
if (ctx.oss) {
|
|
165939
|
+
ctx.toolState.modelClamped = { from: ctx.payload.model, reason: "oss" };
|
|
165940
|
+
log.info(
|
|
165941
|
+
`\xBB ${ctx.payload.model} overridden \u2014 Pullfrog for OSS covers ${ctx.proxyModel}; add a provider key in your Pullfrog settings to run your configured model.`
|
|
165942
|
+
);
|
|
165943
|
+
} else if (isCardGatedModel(ctx.payload.model)) {
|
|
165906
165944
|
ctx.toolState.modelClamped = { from: ctx.payload.model, reason: "card" };
|
|
165907
165945
|
log.warning(
|
|
165908
165946
|
`\xBB ${ctx.payload.model} needs a card on file \u2014 using the efficient default (${ctx.proxyModel}); add a card in your Pullfrog org billing settings.`
|
|
@@ -168423,7 +168461,7 @@ async function runCli4(input) {
|
|
|
168423
168461
|
}
|
|
168424
168462
|
|
|
168425
168463
|
// cli.ts
|
|
168426
|
-
var VERSION10 = "0.1.
|
|
168464
|
+
var VERSION10 = "0.1.37";
|
|
168427
168465
|
var bin = basename2(process.argv[1] || "");
|
|
168428
168466
|
var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
|
|
168429
168467
|
var rawArgs = process.argv.slice(2);
|
package/dist/index.js
CHANGED
|
@@ -100135,7 +100135,7 @@ var providers = {
|
|
|
100135
100135
|
envVars: ["OPENCODE_API_KEY"],
|
|
100136
100136
|
models: {
|
|
100137
100137
|
"glm-5.1": {
|
|
100138
|
-
displayName: "GLM 5.
|
|
100138
|
+
displayName: "GLM 5.2",
|
|
100139
100139
|
resolve: "opencode-go/glm-5.2",
|
|
100140
100140
|
openRouterResolve: "openrouter/z-ai/glm-5.2",
|
|
100141
100141
|
preferred: true
|
|
@@ -101072,7 +101072,7 @@ var import_semver = __toESM(require_semver2(), 1);
|
|
|
101072
101072
|
// package.json
|
|
101073
101073
|
var package_default = {
|
|
101074
101074
|
name: "pullfrog",
|
|
101075
|
-
version: "0.1.
|
|
101075
|
+
version: "0.1.37",
|
|
101076
101076
|
type: "module",
|
|
101077
101077
|
bin: {
|
|
101078
101078
|
pullfrog: "dist/cli.mjs",
|
|
@@ -149708,6 +149708,7 @@ function initToolState(params) {
|
|
|
149708
149708
|
progressComment: resolved,
|
|
149709
149709
|
hadProgressComment: !!resolved,
|
|
149710
149710
|
prepushFailureCount: 0,
|
|
149711
|
+
noopReviewSubmissions: 0,
|
|
149711
149712
|
backgroundProcesses: /* @__PURE__ */ new Map(),
|
|
149712
149713
|
usageEntries: []
|
|
149713
149714
|
};
|
|
@@ -157146,7 +157147,10 @@ function formatModelLabel(params) {
|
|
|
157146
157147
|
modelAliases.find((a) => a.resolve === params.model || a.openRouterResolve === params.model);
|
|
157147
157148
|
const displayName = alias?.displayName ?? params.model;
|
|
157148
157149
|
if (params.oss) {
|
|
157149
|
-
|
|
157150
|
+
const ossBase = `\`${displayName}\` (free via [Pullfrog for OSS](https://pullfrog.com/for-oss))`;
|
|
157151
|
+
if (params.clamped?.reason !== "oss") return ossBase;
|
|
157152
|
+
const configured = isAutoTier(params.clamped.from) ? "the intelligent tier" : `\`${resolveDisplayAlias(params.clamped.from)?.displayName ?? params.clamped.from}\``;
|
|
157153
|
+
return `${ossBase} (${configured} not used \u2014 the program covers this model; add its [provider key](https://docs.pullfrog.com/models) to run your pick)`;
|
|
157150
157154
|
}
|
|
157151
157155
|
const base = alias?.isFree ? `\`${displayName}\` (free)` : `\`${displayName}\``;
|
|
157152
157156
|
if (params.fallbackFrom) {
|
|
@@ -158057,12 +158061,23 @@ function duplicateReviewDecision(params) {
|
|
|
158057
158061
|
reason: `review ${existing.id} was already submitted in this session; ignoring duplicate call (call \`checkout_pr\` again first if new commits were pushed)`
|
|
158058
158062
|
};
|
|
158059
158063
|
}
|
|
158064
|
+
var VERDICT_MARKERS = ["> \u2705", "> \u2139\uFE0F", "> [!IMPORTANT]", "> [!CAUTION]"];
|
|
158065
|
+
var MIN_UNMARKED_BODY_LENGTH = 20;
|
|
158066
|
+
var PLACEHOLDER_PATTERN = /\b(test|testing|placeholder|temp|foo|bar|asdf|dummy|sample)\b/i;
|
|
158067
|
+
var PLACEHOLDER_SCAN_LENGTH = 50;
|
|
158068
|
+
function isDegenerateReviewBody(body) {
|
|
158069
|
+
const trimmed = body.trim();
|
|
158070
|
+
if (VERDICT_MARKERS.some((marker) => trimmed.startsWith(marker))) return false;
|
|
158071
|
+
if (trimmed.length < MIN_UNMARKED_BODY_LENGTH) return true;
|
|
158072
|
+
return trimmed.length < PLACEHOLDER_SCAN_LENGTH && PLACEHOLDER_PATTERN.test(trimmed);
|
|
158073
|
+
}
|
|
158074
|
+
var MAX_CONSECUTIVE_NOOP_SUBMISSIONS = 4;
|
|
158060
158075
|
function reviewSkipDecision(params) {
|
|
158061
158076
|
if (params.body || params.hasComments) return null;
|
|
158062
158077
|
if (!params.approved) {
|
|
158063
158078
|
return {
|
|
158064
158079
|
kind: "no-issues",
|
|
158065
|
-
reason: params.requestChanges ? "request_changes with no body or comments \u2014 nothing to block on" : "no issues
|
|
158080
|
+
reason: params.requestChanges ? "request_changes with no body or comments \u2014 nothing to block on" : "this call carried neither `body` nor `comments`, so nothing was posted. if you found no issues, you are done \u2014 do not call this tool again. if you meant to submit a review, the `body` parameter was missing from your tool call: resend it with `body` set to the full review text. if a resend drops `body` again, do not keep retrying and do not probe with placeholder text \u2014 write the review into `create_issue_comment` instead, which takes the same text under a smaller schema and is editable after posting. every successful call here posts a permanent, publicly visible review."
|
|
158066
158081
|
};
|
|
158067
158082
|
}
|
|
158068
158083
|
if (!params.prApproveEnabled) {
|
|
@@ -158090,9 +158105,15 @@ function formatDroppedCommentsNote(dropped) {
|
|
|
158090
158105
|
}
|
|
158091
158106
|
var CreatePullRequestReview = type({
|
|
158092
158107
|
pull_number: type.number.describe("The pull request number to review"),
|
|
158108
|
+
// REQUIRED on purpose, not because an empty review is invalid — pass "" for
|
|
158109
|
+
// that. models that emit tool calls without schema-constrained decoding drop
|
|
158110
|
+
// optional parameters and keep required ones; in the incident log every
|
|
158111
|
+
// required parameter survived and this one, when optional, vanished ~15 times
|
|
158112
|
+
// running. required-ness is the only pressure that empirically held.
|
|
158113
|
+
// see wiki/review-approval.md.
|
|
158093
158114
|
body: type.string.describe(
|
|
158094
|
-
|
|
158095
|
-
)
|
|
158115
|
+
`1-2 sentence high-level summary with urgency level, critical callouts, and feedback about code outside the diff. Specific feedback on diff lines goes in 'comments' array. ALWAYS pass this parameter \u2014 pass an empty string "" when approving with no commentary, never omit it.`
|
|
158116
|
+
),
|
|
158096
158117
|
approved: type.boolean.describe(
|
|
158097
158118
|
"Set to true to submit as an approval. Use for `> \u2705 No new issues found.` reviews where the PR is mergeable as-is and nothing in the body warrants code changes \u2014 approving also suppresses the Fix-button footer affordance so users don't dispatch a fix run on non-actionable feedback. Reserve approved: false for `> \u2139\uFE0F ...` (minor suggestions inline), `> [!IMPORTANT]` (recommended changes), and `> [!CAUTION]` (critical) reviews. Defaults to false (comment-only review). Mutually exclusive with request_changes. Approval is REJECTED while any unresolved Pullfrog review thread remains open on the PR (not just the latest commit's diff): resolve the threads the current code addresses (reply + resolve_review_thread) first, or submit a non-approving review if a real issue remains."
|
|
158098
158119
|
).optional(),
|
|
@@ -158137,6 +158158,11 @@ function CreatePullRequestReviewTool(ctx) {
|
|
|
158137
158158
|
);
|
|
158138
158159
|
}
|
|
158139
158160
|
if (body) body = fixDoubleEscapedString(body);
|
|
158161
|
+
if (body && isDegenerateReviewBody(body)) {
|
|
158162
|
+
throw new Error(
|
|
158163
|
+
`refusing to submit a review whose body looks like placeholder text: ${JSON.stringify(body)}. every submitted review is permanent and publicly visible on the contributor's PR \u2014 never probe this tool with test content. if the tool has been rejecting your submissions, the cause is your payload, not the tool: re-send with \`body\` set to the full review text, opening with a verdict marker (${VERDICT_MARKERS.join(", ")}).`
|
|
158164
|
+
);
|
|
158165
|
+
}
|
|
158140
158166
|
const primary = primaryRepoState(ctx.toolState);
|
|
158141
158167
|
primary.issueNumber = pull_number;
|
|
158142
158168
|
const dup = duplicateReviewDecision({
|
|
@@ -158181,9 +158207,16 @@ function CreatePullRequestReviewTool(ctx) {
|
|
|
158181
158207
|
prApproveEnabled: ctx.prApproveEnabled && !selfAuthored
|
|
158182
158208
|
});
|
|
158183
158209
|
if (skip) {
|
|
158210
|
+
ctx.toolState.noopReviewSubmissions += 1;
|
|
158184
158211
|
log.info(`skipping review submission: ${skip.reason}`);
|
|
158212
|
+
if (ctx.toolState.noopReviewSubmissions >= MAX_CONSECUTIVE_NOOP_SUBMISSIONS) {
|
|
158213
|
+
throw new Error(
|
|
158214
|
+
`${ctx.toolState.noopReviewSubmissions} consecutive review submissions posted nothing \u2014 every one carried an empty payload. stop calling this tool: the run will report the review as unsubmitted. if you have review feedback, your tool calls are dropping the \`body\` parameter \u2014 write the review into \`create_issue_comment\` instead, which takes the same text and is recoverable if it goes wrong.`
|
|
158215
|
+
);
|
|
158216
|
+
}
|
|
158185
158217
|
return { success: true, skipped: true, reason: skip.reason };
|
|
158186
158218
|
}
|
|
158219
|
+
ctx.toolState.noopReviewSubmissions = 0;
|
|
158187
158220
|
let event = approved ? "APPROVE" : request_changes ? "REQUEST_CHANGES" : "COMMENT";
|
|
158188
158221
|
if (event === "APPROVE" || event === "REQUEST_CHANGES") {
|
|
158189
158222
|
if (!ctx.prApproveEnabled) {
|
|
@@ -164014,8 +164047,13 @@ async function resolveProxyModel(ctx) {
|
|
|
164014
164047
|
`\xBB no model selected \u2014 using the cost-optimized default (${ctx.proxyModel}); pick a model in your Pullfrog repo settings for stronger reviews.`
|
|
164015
164048
|
);
|
|
164016
164049
|
}
|
|
164017
|
-
if (
|
|
164018
|
-
if (
|
|
164050
|
+
if (ctx.payload.model && ctx.proxyModel === DEFAULT_PROXY_MODEL && resolveOpenRouterModel(ctx.payload.model) !== DEFAULT_PROXY_MODEL) {
|
|
164051
|
+
if (ctx.oss) {
|
|
164052
|
+
ctx.toolState.modelClamped = { from: ctx.payload.model, reason: "oss" };
|
|
164053
|
+
log.info(
|
|
164054
|
+
`\xBB ${ctx.payload.model} overridden \u2014 Pullfrog for OSS covers ${ctx.proxyModel}; add a provider key in your Pullfrog settings to run your configured model.`
|
|
164055
|
+
);
|
|
164056
|
+
} else if (isCardGatedModel(ctx.payload.model)) {
|
|
164019
164057
|
ctx.toolState.modelClamped = { from: ctx.payload.model, reason: "card" };
|
|
164020
164058
|
log.warning(
|
|
164021
164059
|
`\xBB ${ctx.payload.model} needs a card on file \u2014 using the efficient default (${ctx.proxyModel}); add a card in your Pullfrog org billing settings.`
|
package/dist/internal.js
CHANGED
|
@@ -326,7 +326,7 @@ var providers = {
|
|
|
326
326
|
envVars: ["OPENCODE_API_KEY"],
|
|
327
327
|
models: {
|
|
328
328
|
"glm-5.1": {
|
|
329
|
-
displayName: "GLM 5.
|
|
329
|
+
displayName: "GLM 5.2",
|
|
330
330
|
resolve: "opencode-go/glm-5.2",
|
|
331
331
|
openRouterResolve: "openrouter/z-ai/glm-5.2",
|
|
332
332
|
preferred: true
|
|
@@ -1209,7 +1209,10 @@ function formatModelLabel(params) {
|
|
|
1209
1209
|
modelAliases.find((a) => a.resolve === params.model || a.openRouterResolve === params.model);
|
|
1210
1210
|
const displayName = alias?.displayName ?? params.model;
|
|
1211
1211
|
if (params.oss) {
|
|
1212
|
-
|
|
1212
|
+
const ossBase = `\`${displayName}\` (free via [Pullfrog for OSS](https://pullfrog.com/for-oss))`;
|
|
1213
|
+
if (params.clamped?.reason !== "oss") return ossBase;
|
|
1214
|
+
const configured = isAutoTier(params.clamped.from) ? "the intelligent tier" : `\`${resolveDisplayAlias(params.clamped.from)?.displayName ?? params.clamped.from}\``;
|
|
1215
|
+
return `${ossBase} (${configured} not used \u2014 the program covers this model; add its [provider key](https://docs.pullfrog.com/models) to run your pick)`;
|
|
1213
1216
|
}
|
|
1214
1217
|
const base = alias?.isFree ? `\`${displayName}\` (free)` : `\`${displayName}\``;
|
|
1215
1218
|
if (params.fallbackFrom) {
|
package/dist/mcp/review.d.ts
CHANGED
|
@@ -117,6 +117,31 @@ export declare function duplicateReviewDecision(params: {
|
|
|
117
117
|
} | undefined;
|
|
118
118
|
currentCheckoutSha: string | undefined;
|
|
119
119
|
}): DuplicateReviewDecision | null;
|
|
120
|
+
/**
|
|
121
|
+
* reject placeholder review bodies at the tool boundary.
|
|
122
|
+
*
|
|
123
|
+
* every successful create_pull_request_review call posts a permanent, publicly
|
|
124
|
+
* visible review that no Pullfrog tool can retract (`edit_issue_comment` 404s
|
|
125
|
+
* on a review id). a weak model that cannot get a real submission through
|
|
126
|
+
* eventually probes the tool with a minimal payload to test whether it works —
|
|
127
|
+
* `test`, `test body`, `placeholder`, `foo` — and that probe lands on a
|
|
128
|
+
* customer's PR. 27 such reviews reached 10 public repos between 2026-06-25 and
|
|
129
|
+
* 2026-07-18. the tool description already says "NEVER submit test or
|
|
130
|
+
* diagnostic reviews"; a prompt instruction is not a control.
|
|
131
|
+
*
|
|
132
|
+
* a body carrying a verdict marker is always accepted — `> ✅ No new issues
|
|
133
|
+
* found.` is a legitimate 24-char review. everything else must clear
|
|
134
|
+
* MIN_UNMARKED_BODY_LENGTH, which every observed placeholder fails and every
|
|
135
|
+
* observed real review passes.
|
|
136
|
+
*/
|
|
137
|
+
export declare function isDegenerateReviewBody(body: string): boolean;
|
|
138
|
+
/**
|
|
139
|
+
* consecutive no-op submissions tolerated before the tool stops explaining and
|
|
140
|
+
* starts refusing. kimi-k2.7-code ran ~15 rounds of empty submissions against a
|
|
141
|
+
* misleading skip reason before probing with a placeholder; a bounded budget
|
|
142
|
+
* converts that unbounded loop into a run-visible failure.
|
|
143
|
+
*/
|
|
144
|
+
export declare const MAX_CONSECUTIVE_NOOP_SUBMISSIONS = 4;
|
|
120
145
|
/**
|
|
121
146
|
* decide whether to skip a review submission before any network call.
|
|
122
147
|
*
|
|
@@ -126,6 +151,15 @@ export declare function duplicateReviewDecision(params: {
|
|
|
126
151
|
* 1. `!approved` + empty body/comments: agent's "no issues found" result.
|
|
127
152
|
* skipping preserves the agent's intent (nothing to post is a fine
|
|
128
153
|
* outcome for a review run) without a spurious 422.
|
|
154
|
+
* this shape is AMBIGUOUS: it is equally the signature of a weak model
|
|
155
|
+
* that meant to submit a real review but dropped `body` from the tool
|
|
156
|
+
* call. the skip reason must therefore describe the empty PAYLOAD, not
|
|
157
|
+
* assert an empty VERDICT — a reason reading "no issues found" reads as
|
|
158
|
+
* success to the agent, so it retries blind. kimi-k2.7-code looped ~15
|
|
159
|
+
* times against the old wording, then probed the tool with
|
|
160
|
+
* `body: "test"` to check whether it worked at all; that probe posted
|
|
161
|
+
* permanently to a customer PR (software-mansion/TypeGPU#2730), and
|
|
162
|
+
* 26 more like it across 10 repos. see wiki/review-approval.md.
|
|
129
163
|
* 2. `approved` + `!prApproveEnabled` + empty body/comments: the runtime
|
|
130
164
|
* downgrades APPROVE to COMMENT when prApproveEnabled is off, and the
|
|
131
165
|
* resulting empty-COMMENT is exactly the shape GitHub 422s. skipping
|
|
@@ -146,7 +180,7 @@ export declare function reviewSkipDecision(params: {
|
|
|
146
180
|
export declare function formatDroppedCommentsNote(dropped: DroppedComment[]): string;
|
|
147
181
|
export declare const CreatePullRequestReview: import("arktype/internal/variants/object.ts").ObjectType<{
|
|
148
182
|
pull_number: number;
|
|
149
|
-
body
|
|
183
|
+
body: string;
|
|
150
184
|
approved?: boolean;
|
|
151
185
|
request_changes?: boolean;
|
|
152
186
|
commit_id?: string;
|
|
@@ -161,7 +195,7 @@ export declare const CreatePullRequestReview: import("arktype/internal/variants/
|
|
|
161
195
|
}, {}>;
|
|
162
196
|
export declare function CreatePullRequestReviewTool(ctx: ToolContext): import("fastmcp").Tool<any, import("@standard-schema/spec").StandardSchemaV1<{
|
|
163
197
|
pull_number: number;
|
|
164
|
-
body
|
|
198
|
+
body: string;
|
|
165
199
|
approved?: boolean;
|
|
166
200
|
request_changes?: boolean;
|
|
167
201
|
commit_id?: string;
|
|
@@ -175,7 +209,7 @@ export declare function CreatePullRequestReviewTool(ctx: ToolContext): import("f
|
|
|
175
209
|
}[];
|
|
176
210
|
}, {
|
|
177
211
|
pull_number: number;
|
|
178
|
-
body
|
|
212
|
+
body: string;
|
|
179
213
|
approved?: boolean;
|
|
180
214
|
request_changes?: boolean;
|
|
181
215
|
commit_id?: string;
|
package/dist/toolState.d.ts
CHANGED
|
@@ -93,6 +93,7 @@ export interface ToolState {
|
|
|
93
93
|
primaryRepoKey: string;
|
|
94
94
|
selectedMode?: string;
|
|
95
95
|
prepushFailureCount: number;
|
|
96
|
+
noopReviewSubmissions: number;
|
|
96
97
|
backgroundProcesses: Map<string, BackgroundProcess>;
|
|
97
98
|
browserDaemon?: BrowserDaemon | undefined;
|
|
98
99
|
review?: {
|
|
@@ -139,7 +140,7 @@ export interface ToolState {
|
|
|
139
140
|
unselectedProxyDefault?: boolean | undefined;
|
|
140
141
|
modelClamped?: {
|
|
141
142
|
from: string;
|
|
142
|
-
reason: "card" | "noRouterPath";
|
|
143
|
+
reason: "card" | "noRouterPath" | "oss";
|
|
143
144
|
} | undefined;
|
|
144
145
|
shaPinned?: boolean | undefined;
|
|
145
146
|
oss?: boolean | undefined;
|
|
@@ -36,7 +36,7 @@ export interface BuildPullfrogFooterParams {
|
|
|
36
36
|
*/
|
|
37
37
|
clamped?: {
|
|
38
38
|
from: string;
|
|
39
|
-
reason: "card" | "noRouterPath";
|
|
39
|
+
reason: "card" | "noRouterPath" | "oss";
|
|
40
40
|
} | undefined;
|
|
41
41
|
/**
|
|
42
42
|
* true when the run used the default proxy model only because no model was
|