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 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.1",
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.36",
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
- return `\`${displayName}\` (free via [Pullfrog for OSS](https://pullfrog.com/for-oss))`;
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 found \u2014 nothing to post"
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
- "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."
159982
- ).optional(),
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 (!ctx.oss && ctx.payload.model && ctx.proxyModel === DEFAULT_PROXY_MODEL && resolveOpenRouterModel(ctx.payload.model) !== DEFAULT_PROXY_MODEL) {
165905
- if (isCardGatedModel(ctx.payload.model)) {
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.36";
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.1",
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.36",
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
- return `\`${displayName}\` (free via [Pullfrog for OSS](https://pullfrog.com/for-oss))`;
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 found \u2014 nothing to post"
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
- "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."
158095
- ).optional(),
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 (!ctx.oss && ctx.payload.model && ctx.proxyModel === DEFAULT_PROXY_MODEL && resolveOpenRouterModel(ctx.payload.model) !== DEFAULT_PROXY_MODEL) {
164018
- if (isCardGatedModel(ctx.payload.model)) {
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.1",
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
- return `\`${displayName}\` (free via [Pullfrog for OSS](https://pullfrog.com/for-oss))`;
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) {
@@ -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?: string;
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?: string;
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?: string;
212
+ body: string;
179
213
  approved?: boolean;
180
214
  request_changes?: boolean;
181
215
  commit_id?: string;
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pullfrog",
3
- "version": "0.1.36",
3
+ "version": "0.1.37",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "pullfrog": "dist/cli.mjs",