pullfrog 0.1.68 → 0.1.69

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
@@ -110773,10 +110773,9 @@ var providers = {
110773
110773
  // was the catalog's only GLM route, and Grok had no Zen route at all.
110774
110774
  glm: {
110775
110775
  displayName: "GLM",
110776
- resolve: "opencode/glm-5.2",
110777
- effort: ["high", "max"],
110778
- openRouterEffort: ["high", "xhigh"],
110779
- openRouterResolve: "openrouter/z-ai/glm-5.2"
110776
+ resolve: "opencode/glm-5.3",
110777
+ effort: ["low", "high", "max"],
110778
+ openRouterResolve: "openrouter/z-ai/glm-5.3"
110780
110779
  },
110781
110780
  grok: {
110782
110781
  displayName: "Grok",
@@ -110823,10 +110822,11 @@ var providers = {
110823
110822
  // OpenCode Go is a separate $10/mo subscription from Zen, served on its own
110824
110823
  // base URL (`https://opencode.ai/zen/go/v1`) but authenticated with the SAME
110825
110824
  // `OPENCODE_API_KEY`. it carries the open-weight coding models plus a couple
110826
- // of frontier ones, and 14 of the ids below are served ONLY here — Zen's
110827
- // `/v1/models` does not list glm-5.3*, qwen3.7/3.8-*, mimo-*, longcat-2.0,
110828
- // hy3 or muse-spark. so for a Go subscriber this provider is not a duplicate
110829
- // route to Zen, it is the only route to most of what they pay for.
110825
+ // of frontier ones, six of which are served ONLY here — Zen's `/v1/models`
110826
+ // does not list qwen3.7/3.8-*, mimo-v2.5-pro, longcat-2.0 or hy3. (it listed
110827
+ // no glm-5.3* either until 2026-09, which is why `opencode/glm` trailed on
110828
+ // 5.2.) so for a Go subscriber this provider is not a duplicate route to Zen,
110829
+ // it is the only route to a large part of what they pay for.
110830
110830
  // like `opencode` and `openrouter` this is a ROUTER, not a vendor: slugs and
110831
110831
  // display names mirror the upstream brand tier, and the picker groups them
110832
110832
  // under the upstream vendor.
@@ -110895,7 +110895,9 @@ var providers = {
110895
110895
  // `high` the Go route does not.
110896
110896
  effort: ["low", "medium", "xhigh"],
110897
110897
  openRouterEffort: ["minimal", "low", "medium", "high", "xhigh"],
110898
- openRouterResolve: "openrouter/qwen/qwen3.8-max"
110898
+ // OpenRouter retired the rolling `qwen3.8-max` for a dated snapshot, so
110899
+ // this side has to pin one; the Go route still carries the rolling id.
110900
+ openRouterResolve: "openrouter/qwen/qwen3.8-max-0902"
110899
110901
  },
110900
110902
  "qwen-plus": {
110901
110903
  displayName: "Qwen Plus",
@@ -112252,7 +112254,7 @@ var import_semver = __toESM(require_semver2(), 1);
112252
112254
  // package.json
112253
112255
  var package_default = {
112254
112256
  name: "pullfrog",
112255
- version: "0.1.68",
112257
+ version: "0.1.69",
112256
112258
  type: "module",
112257
112259
  bin: {
112258
112260
  pullfrog: "dist/cli.mjs",
@@ -113059,6 +113061,7 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
113059
113061
  - record what was done (or why nothing was done)
113060
113062
 
113061
113063
  5. Quality check:
113064
+ - re-call \`${t2("get_review_comments")}\` with \`fresh: true\` before committing: reviewers keep commenting while you work, and any thread it now returns that you have not addressed is in scope for this run.
113062
113065
  - test changes, then review the diff before committing \u2014 verify only intended changes are present, no debug artifacts remain, no fix turned out to be bloat in context (revert any that did), and the changes are clean enough that a senior engineer would approve without hesitation
113063
113066
  - ${commitStep}
113064
113067
 
@@ -113068,7 +113071,7 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
113068
113071
  - **once the fix is live on the remote**, for each thread you acted on:
113069
113072
  - reply ONCE via \`${t2("reply_to_review_comment")}\`. The \`comment_id\` parameter takes the root comment's numeric \`id=\` (from the first \`comment author=...\` tag in the \`${t2("get_review_comments")}\` output) \u2014 NOT the \`thread=\` value; that's a separate GraphQL ID used by resolve. The runtime dedupes identical bodies within a session.
113070
113073
  - **immediately** call \`${t2("resolve_review_thread")}\` with that thread's \`thread=\` value as \`thread_id\`. Resolve every thread where you (a) made the requested code change in full \u2014 partial fixes leave the thread open \u2014 OR (b) replied with a substantive answer the user explicitly asked for. Do NOT resolve threads where you pushed back on the request and the disagreement is unresolved; leave those open for the human to mediate.
113071
- - call \`${t2("report_progress")}\` with a brief summary`
113074
+ - call \`${t2("report_progress")}\` with a brief summary. If your last \`${t2("get_review_comments")}\` turned up threads you did not address, name them there \u2014 a review arriving this late is suppressed as a duplicate and nothing else will pick it up.`
113072
113075
  },
113073
113076
  // Review and IncrementalReview route the minimum reviewfrog specialists
113074
113077
  // needed to cover unresolved, disposition-changing hypotheses. Most runs
@@ -199174,7 +199177,10 @@ function extractFromFilePatches(hunks, startLine, endLine, side) {
199174
199177
  }
199175
199178
  var GetReviewComments = type({
199176
199179
  pull_number: type.number.describe("The pull request number"),
199177
- review_id: type.number.describe("The review ID to get comments for")
199180
+ review_id: type.number.describe("The review ID to get comments for"),
199181
+ "fresh?": type.boolean.describe(
199182
+ "Bypass the 60s thread cache. Use for the re-read before committing, so comments that landed while you worked are actually seen; leave unset otherwise."
199183
+ )
199178
199184
  });
199179
199185
  function hasThumbsUpFrom(comment, username) {
199180
199186
  if (!comment.reactionGroups) return false;
@@ -199208,7 +199214,7 @@ function formatReviewThreads(threadBlocks, header) {
199208
199214
  }
199209
199215
  const lines = [];
199210
199216
  lines.push(
199211
- `# Review Threads (${threadBlocks.length}) for PR #${header.pullNumber} - Review ${header.reviewId} by ${header.reviewer}`
199217
+ `# Open Threads (${threadBlocks.length}) on PR #${header.pullNumber} - dispatched by Review ${header.reviewId} from ${header.reviewer}`
199212
199218
  );
199213
199219
  lines.push("");
199214
199220
  if (threadBlocks.length > 0) {
@@ -199349,11 +199355,27 @@ async function getReviewThreads(input2) {
199349
199355
  if (!thread?.comments?.nodes) return false;
199350
199356
  return thread.comments.nodes.some((c2) => c2?.pullRequestReview?.databaseId === input2.reviewId);
199351
199357
  });
199352
- if (!input2.approvedBy) {
199353
- return threadsForReview;
199358
+ if (input2.approvedBy) {
199359
+ const username = input2.approvedBy;
199360
+ return threadsForReview.filter((thread) => threadHasThumbsUpFrom(thread, username));
199354
199361
  }
199355
- const username = input2.approvedBy;
199356
- return threadsForReview.filter((thread) => threadHasThumbsUpFrom(thread, username));
199362
+ if (!input2.addressScope) return threadsForReview;
199363
+ return [...threadsForReview, ...concurrentThreads(allThreads, threadsForReview, input2)];
199364
+ }
199365
+ function concurrentThreads(allThreads, threadsForReview, input2) {
199366
+ const alreadyIncluded = new Set(threadsForReview.map((thread) => thread.id));
199367
+ return allThreads.filter((thread) => {
199368
+ if (!thread?.comments?.nodes || alreadyIncluded.has(thread.id)) return false;
199369
+ if (thread.isResolved) return false;
199370
+ const comments = thread.comments.nodes.filter((c2) => c2 !== null);
199371
+ if (comments.length === 0) return false;
199372
+ if (comments.some((c2) => isPullfrog(c2.author?.login))) return false;
199373
+ return input2.addressScope === "all" || comments.some((c2) => mentionsPullfrog(c2.body));
199374
+ });
199375
+ }
199376
+ function mentionsPullfrog(body) {
199377
+ if (!body) return false;
199378
+ return body.split(/\r?\n/).filter((line) => !line.trim().startsWith(">")).join("\n").toLowerCase().includes("@pullfrog");
199357
199379
  }
199358
199380
  function formatReviewData(input2) {
199359
199381
  const rawReviewBody = input2.review.body;
@@ -199450,6 +199472,7 @@ function GetReviewCommentsTool(ctx) {
199450
199472
  parameters: GetReviewComments,
199451
199473
  execute: execute(async (params) => {
199452
199474
  const approvedBy = ctx.payload.event.trigger === "fix_review" && ctx.payload.event.approved_only ? ctx.payload.triggerer : void 0;
199475
+ if (params.fresh) invalidateReviewThreadCache(ctx.repo);
199453
199476
  const result = await getReviewData({
199454
199477
  octokit: ctx.octokit,
199455
199478
  owner: ctx.repo.owner,
@@ -199457,6 +199480,7 @@ function GetReviewCommentsTool(ctx) {
199457
199480
  pullNumber: params.pull_number,
199458
199481
  reviewId: params.review_id,
199459
199482
  approvedBy,
199483
+ addressScope: ctx.payload.event.address_scope,
199460
199484
  tmpdir: ctx.tmpdir,
199461
199485
  githubToken: ctx.githubInstallationToken
199462
199486
  });
@@ -199487,7 +199511,7 @@ function GetReviewCommentsTool(ctx) {
199487
199511
  threadCount: threadBlocks.length,
199488
199512
  commentsPath,
199489
199513
  toc: formatted.toc,
199490
- instructions: `the file at commentsPath contains ${threadBlocks.length} review threads with full conversation history. comments marked with * are from the target review (${params.review_id}). the TOC shows each thread's file:line and the line number where it appears in the file. to read a specific thread, use: grep -A 50 "^## <file:line>" ${commentsPath} (replace <file:line> with the path from the TOC, e.g. "^## action/utils/foo.ts:42"). address each thread in order, working through one file at a time.`
199514
+ instructions: `the file at commentsPath contains ${threadBlocks.length} review threads with full conversation history. comments marked with * are from the target review (${params.review_id}); any other thread is open feedback on this PR that is also yours to address, including comments that landed after this run started. the TOC shows each thread's file:line and the line number where it appears in the file. to read a specific thread, use: grep -A 50 "^## <file:line>" ${commentsPath} (replace <file:line> with the path from the TOC, e.g. "^## action/utils/foo.ts:42"). address each thread in order, working through one file at a time.`
199491
199515
  };
199492
199516
  })
199493
199517
  });
@@ -204932,7 +204956,7 @@ function resolveTarget(repo) {
204932
204956
  return { owner: match3[1], repo: match3[2] };
204933
204957
  }
204934
204958
  function cliVersion() {
204935
- const parts = /^(\d+)\.(\d+)\.(\d+)$/.exec("0.1.68");
204959
+ const parts = /^(\d+)\.(\d+)\.(\d+)$/.exec("0.1.69");
204936
204960
  if (!parts) return "0.0.0";
204937
204961
  return `${Number(parts[1])}.${Number(parts[2])}.${Number(parts[3])}`;
204938
204962
  }
@@ -205099,7 +205123,7 @@ async function runCli5(input2) {
205099
205123
  }
205100
205124
 
205101
205125
  // cli.ts
205102
- var VERSION10 = "0.1.68";
205126
+ var VERSION10 = "0.1.69";
205103
205127
  var bin = basename2(process.argv[1] || "");
205104
205128
  var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
205105
205129
  var rawArgs = process.argv.slice(2);
@@ -137,6 +137,15 @@ interface PullRequestReviewSubmittedEvent extends BasePayloadEvent {
137
137
  body: string | null;
138
138
  review_state: string;
139
139
  branch: string;
140
+ /**
141
+ * How far past the dispatching review this run may read: `all` on a
142
+ * Pullfrog-authored PR, `mentions` when only `@pullfrog`-mentioning threads
143
+ * are in scope. The server already computes this to decide whether to
144
+ * dispatch; forwarding it is what lets `get_review_comments` pick up the
145
+ * comments that land while the run is working. Optional so a payload from an
146
+ * older server build still parses against a newer action.
147
+ */
148
+ address_scope?: "all" | "mentions" | undefined;
140
149
  }
141
150
  interface PullRequestReviewCommentCreatedEvent extends BasePayloadEvent {
142
151
  trigger: "pull_request_review_comment_created";
package/dist/index.js CHANGED
@@ -108283,10 +108283,9 @@ var providers = {
108283
108283
  // was the catalog's only GLM route, and Grok had no Zen route at all.
108284
108284
  glm: {
108285
108285
  displayName: "GLM",
108286
- resolve: "opencode/glm-5.2",
108287
- effort: ["high", "max"],
108288
- openRouterEffort: ["high", "xhigh"],
108289
- openRouterResolve: "openrouter/z-ai/glm-5.2"
108286
+ resolve: "opencode/glm-5.3",
108287
+ effort: ["low", "high", "max"],
108288
+ openRouterResolve: "openrouter/z-ai/glm-5.3"
108290
108289
  },
108291
108290
  grok: {
108292
108291
  displayName: "Grok",
@@ -108333,10 +108332,11 @@ var providers = {
108333
108332
  // OpenCode Go is a separate $10/mo subscription from Zen, served on its own
108334
108333
  // base URL (`https://opencode.ai/zen/go/v1`) but authenticated with the SAME
108335
108334
  // `OPENCODE_API_KEY`. it carries the open-weight coding models plus a couple
108336
- // of frontier ones, and 14 of the ids below are served ONLY here — Zen's
108337
- // `/v1/models` does not list glm-5.3*, qwen3.7/3.8-*, mimo-*, longcat-2.0,
108338
- // hy3 or muse-spark. so for a Go subscriber this provider is not a duplicate
108339
- // route to Zen, it is the only route to most of what they pay for.
108335
+ // of frontier ones, six of which are served ONLY here — Zen's `/v1/models`
108336
+ // does not list qwen3.7/3.8-*, mimo-v2.5-pro, longcat-2.0 or hy3. (it listed
108337
+ // no glm-5.3* either until 2026-09, which is why `opencode/glm` trailed on
108338
+ // 5.2.) so for a Go subscriber this provider is not a duplicate route to Zen,
108339
+ // it is the only route to a large part of what they pay for.
108340
108340
  // like `opencode` and `openrouter` this is a ROUTER, not a vendor: slugs and
108341
108341
  // display names mirror the upstream brand tier, and the picker groups them
108342
108342
  // under the upstream vendor.
@@ -108405,7 +108405,9 @@ var providers = {
108405
108405
  // `high` the Go route does not.
108406
108406
  effort: ["low", "medium", "xhigh"],
108407
108407
  openRouterEffort: ["minimal", "low", "medium", "high", "xhigh"],
108408
- openRouterResolve: "openrouter/qwen/qwen3.8-max"
108408
+ // OpenRouter retired the rolling `qwen3.8-max` for a dated snapshot, so
108409
+ // this side has to pin one; the Go route still carries the rolling id.
108410
+ openRouterResolve: "openrouter/qwen/qwen3.8-max-0902"
108409
108411
  },
108410
108412
  "qwen-plus": {
108411
108413
  displayName: "Qwen Plus",
@@ -109762,7 +109764,7 @@ var import_semver = __toESM(require_semver2(), 1);
109762
109764
  // package.json
109763
109765
  var package_default = {
109764
109766
  name: "pullfrog",
109765
- version: "0.1.68",
109767
+ version: "0.1.69",
109766
109768
  type: "module",
109767
109769
  bin: {
109768
109770
  pullfrog: "dist/cli.mjs",
@@ -110569,6 +110571,7 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
110569
110571
  - record what was done (or why nothing was done)
110570
110572
 
110571
110573
  5. Quality check:
110574
+ - re-call \`${t("get_review_comments")}\` with \`fresh: true\` before committing: reviewers keep commenting while you work, and any thread it now returns that you have not addressed is in scope for this run.
110572
110575
  - test changes, then review the diff before committing \u2014 verify only intended changes are present, no debug artifacts remain, no fix turned out to be bloat in context (revert any that did), and the changes are clean enough that a senior engineer would approve without hesitation
110573
110576
  - ${commitStep}
110574
110577
 
@@ -110578,7 +110581,7 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
110578
110581
  - **once the fix is live on the remote**, for each thread you acted on:
110579
110582
  - reply ONCE via \`${t("reply_to_review_comment")}\`. The \`comment_id\` parameter takes the root comment's numeric \`id=\` (from the first \`comment author=...\` tag in the \`${t("get_review_comments")}\` output) \u2014 NOT the \`thread=\` value; that's a separate GraphQL ID used by resolve. The runtime dedupes identical bodies within a session.
110580
110583
  - **immediately** call \`${t("resolve_review_thread")}\` with that thread's \`thread=\` value as \`thread_id\`. Resolve every thread where you (a) made the requested code change in full \u2014 partial fixes leave the thread open \u2014 OR (b) replied with a substantive answer the user explicitly asked for. Do NOT resolve threads where you pushed back on the request and the disagreement is unresolved; leave those open for the human to mediate.
110581
- - call \`${t("report_progress")}\` with a brief summary`
110584
+ - call \`${t("report_progress")}\` with a brief summary. If your last \`${t("get_review_comments")}\` turned up threads you did not address, name them there \u2014 a review arriving this late is suppressed as a duplicate and nothing else will pick it up.`
110582
110585
  },
110583
110586
  // Review and IncrementalReview route the minimum reviewfrog specialists
110584
110587
  // needed to cover unresolved, disposition-changing hypotheses. Most runs
@@ -196757,7 +196760,10 @@ function extractFromFilePatches(hunks, startLine, endLine, side) {
196757
196760
  }
196758
196761
  var GetReviewComments = type({
196759
196762
  pull_number: type.number.describe("The pull request number"),
196760
- review_id: type.number.describe("The review ID to get comments for")
196763
+ review_id: type.number.describe("The review ID to get comments for"),
196764
+ "fresh?": type.boolean.describe(
196765
+ "Bypass the 60s thread cache. Use for the re-read before committing, so comments that landed while you worked are actually seen; leave unset otherwise."
196766
+ )
196761
196767
  });
196762
196768
  function hasThumbsUpFrom(comment, username) {
196763
196769
  if (!comment.reactionGroups) return false;
@@ -196791,7 +196797,7 @@ function formatReviewThreads(threadBlocks, header) {
196791
196797
  }
196792
196798
  const lines = [];
196793
196799
  lines.push(
196794
- `# Review Threads (${threadBlocks.length}) for PR #${header.pullNumber} - Review ${header.reviewId} by ${header.reviewer}`
196800
+ `# Open Threads (${threadBlocks.length}) on PR #${header.pullNumber} - dispatched by Review ${header.reviewId} from ${header.reviewer}`
196795
196801
  );
196796
196802
  lines.push("");
196797
196803
  if (threadBlocks.length > 0) {
@@ -196932,11 +196938,27 @@ async function getReviewThreads(input2) {
196932
196938
  if (!thread?.comments?.nodes) return false;
196933
196939
  return thread.comments.nodes.some((c) => c?.pullRequestReview?.databaseId === input2.reviewId);
196934
196940
  });
196935
- if (!input2.approvedBy) {
196936
- return threadsForReview;
196941
+ if (input2.approvedBy) {
196942
+ const username = input2.approvedBy;
196943
+ return threadsForReview.filter((thread) => threadHasThumbsUpFrom(thread, username));
196937
196944
  }
196938
- const username = input2.approvedBy;
196939
- return threadsForReview.filter((thread) => threadHasThumbsUpFrom(thread, username));
196945
+ if (!input2.addressScope) return threadsForReview;
196946
+ return [...threadsForReview, ...concurrentThreads(allThreads, threadsForReview, input2)];
196947
+ }
196948
+ function concurrentThreads(allThreads, threadsForReview, input2) {
196949
+ const alreadyIncluded = new Set(threadsForReview.map((thread) => thread.id));
196950
+ return allThreads.filter((thread) => {
196951
+ if (!thread?.comments?.nodes || alreadyIncluded.has(thread.id)) return false;
196952
+ if (thread.isResolved) return false;
196953
+ const comments = thread.comments.nodes.filter((c) => c !== null);
196954
+ if (comments.length === 0) return false;
196955
+ if (comments.some((c) => isPullfrog(c.author?.login))) return false;
196956
+ return input2.addressScope === "all" || comments.some((c) => mentionsPullfrog(c.body));
196957
+ });
196958
+ }
196959
+ function mentionsPullfrog(body) {
196960
+ if (!body) return false;
196961
+ return body.split(/\r?\n/).filter((line) => !line.trim().startsWith(">")).join("\n").toLowerCase().includes("@pullfrog");
196940
196962
  }
196941
196963
  function formatReviewData(input2) {
196942
196964
  const rawReviewBody = input2.review.body;
@@ -197033,6 +197055,7 @@ function GetReviewCommentsTool(ctx) {
197033
197055
  parameters: GetReviewComments,
197034
197056
  execute: execute(async (params) => {
197035
197057
  const approvedBy = ctx.payload.event.trigger === "fix_review" && ctx.payload.event.approved_only ? ctx.payload.triggerer : void 0;
197058
+ if (params.fresh) invalidateReviewThreadCache(ctx.repo);
197036
197059
  const result = await getReviewData({
197037
197060
  octokit: ctx.octokit,
197038
197061
  owner: ctx.repo.owner,
@@ -197040,6 +197063,7 @@ function GetReviewCommentsTool(ctx) {
197040
197063
  pullNumber: params.pull_number,
197041
197064
  reviewId: params.review_id,
197042
197065
  approvedBy,
197066
+ addressScope: ctx.payload.event.address_scope,
197043
197067
  tmpdir: ctx.tmpdir,
197044
197068
  githubToken: ctx.githubInstallationToken
197045
197069
  });
@@ -197070,7 +197094,7 @@ function GetReviewCommentsTool(ctx) {
197070
197094
  threadCount: threadBlocks.length,
197071
197095
  commentsPath,
197072
197096
  toc: formatted.toc,
197073
- instructions: `the file at commentsPath contains ${threadBlocks.length} review threads with full conversation history. comments marked with * are from the target review (${params.review_id}). the TOC shows each thread's file:line and the line number where it appears in the file. to read a specific thread, use: grep -A 50 "^## <file:line>" ${commentsPath} (replace <file:line> with the path from the TOC, e.g. "^## action/utils/foo.ts:42"). address each thread in order, working through one file at a time.`
197097
+ instructions: `the file at commentsPath contains ${threadBlocks.length} review threads with full conversation history. comments marked with * are from the target review (${params.review_id}); any other thread is open feedback on this PR that is also yours to address, including comments that landed after this run started. the TOC shows each thread's file:line and the line number where it appears in the file. to read a specific thread, use: grep -A 50 "^## <file:line>" ${commentsPath} (replace <file:line> with the path from the TOC, e.g. "^## action/utils/foo.ts:42"). address each thread in order, working through one file at a time.`
197074
197098
  };
197075
197099
  })
197076
197100
  });
package/dist/internal.js CHANGED
@@ -579,10 +579,9 @@ var providers = {
579
579
  // was the catalog's only GLM route, and Grok had no Zen route at all.
580
580
  glm: {
581
581
  displayName: "GLM",
582
- resolve: "opencode/glm-5.2",
583
- effort: ["high", "max"],
584
- openRouterEffort: ["high", "xhigh"],
585
- openRouterResolve: "openrouter/z-ai/glm-5.2"
582
+ resolve: "opencode/glm-5.3",
583
+ effort: ["low", "high", "max"],
584
+ openRouterResolve: "openrouter/z-ai/glm-5.3"
586
585
  },
587
586
  grok: {
588
587
  displayName: "Grok",
@@ -629,10 +628,11 @@ var providers = {
629
628
  // OpenCode Go is a separate $10/mo subscription from Zen, served on its own
630
629
  // base URL (`https://opencode.ai/zen/go/v1`) but authenticated with the SAME
631
630
  // `OPENCODE_API_KEY`. it carries the open-weight coding models plus a couple
632
- // of frontier ones, and 14 of the ids below are served ONLY here — Zen's
633
- // `/v1/models` does not list glm-5.3*, qwen3.7/3.8-*, mimo-*, longcat-2.0,
634
- // hy3 or muse-spark. so for a Go subscriber this provider is not a duplicate
635
- // route to Zen, it is the only route to most of what they pay for.
631
+ // of frontier ones, six of which are served ONLY here — Zen's `/v1/models`
632
+ // does not list qwen3.7/3.8-*, mimo-v2.5-pro, longcat-2.0 or hy3. (it listed
633
+ // no glm-5.3* either until 2026-09, which is why `opencode/glm` trailed on
634
+ // 5.2.) so for a Go subscriber this provider is not a duplicate route to Zen,
635
+ // it is the only route to a large part of what they pay for.
636
636
  // like `opencode` and `openrouter` this is a ROUTER, not a vendor: slugs and
637
637
  // display names mirror the upstream brand tier, and the picker groups them
638
638
  // under the upstream vendor.
@@ -701,7 +701,9 @@ var providers = {
701
701
  // `high` the Go route does not.
702
702
  effort: ["low", "medium", "xhigh"],
703
703
  openRouterEffort: ["minimal", "low", "medium", "high", "xhigh"],
704
- openRouterResolve: "openrouter/qwen/qwen3.8-max"
704
+ // OpenRouter retired the rolling `qwen3.8-max` for a dated snapshot, so
705
+ // this side has to pin one; the Go route still carries the rolling id.
706
+ openRouterResolve: "openrouter/qwen/qwen3.8-max-0902"
705
707
  },
706
708
  "qwen-plus": {
707
709
  displayName: "Qwen Plus",
@@ -1383,6 +1385,7 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
1383
1385
  - record what was done (or why nothing was done)
1384
1386
 
1385
1387
  5. Quality check:
1388
+ - re-call \`${t("get_review_comments")}\` with \`fresh: true\` before committing: reviewers keep commenting while you work, and any thread it now returns that you have not addressed is in scope for this run.
1386
1389
  - test changes, then review the diff before committing \u2014 verify only intended changes are present, no debug artifacts remain, no fix turned out to be bloat in context (revert any that did), and the changes are clean enough that a senior engineer would approve without hesitation
1387
1390
  - ${commitStep}
1388
1391
 
@@ -1392,7 +1395,7 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
1392
1395
  - **once the fix is live on the remote**, for each thread you acted on:
1393
1396
  - reply ONCE via \`${t("reply_to_review_comment")}\`. The \`comment_id\` parameter takes the root comment's numeric \`id=\` (from the first \`comment author=...\` tag in the \`${t("get_review_comments")}\` output) \u2014 NOT the \`thread=\` value; that's a separate GraphQL ID used by resolve. The runtime dedupes identical bodies within a session.
1394
1397
  - **immediately** call \`${t("resolve_review_thread")}\` with that thread's \`thread=\` value as \`thread_id\`. Resolve every thread where you (a) made the requested code change in full \u2014 partial fixes leave the thread open \u2014 OR (b) replied with a substantive answer the user explicitly asked for. Do NOT resolve threads where you pushed back on the request and the disagreement is unresolved; leave those open for the human to mediate.
1395
- - call \`${t("report_progress")}\` with a brief summary`
1398
+ - call \`${t("report_progress")}\` with a brief summary. If your last \`${t("get_review_comments")}\` turned up threads you did not address, name them there \u2014 a review arriving this late is suppressed as a duplicate and nothing else will pick it up.`
1396
1399
  },
1397
1400
  // Review and IncrementalReview route the minimum reviewfrog specialists
1398
1401
  // needed to cover unresolved, disposition-changing hypotheses. Most runs
@@ -67,6 +67,7 @@ export declare function parseFilePatches(patch: string): ParsedHunk[];
67
67
  export declare const GetReviewComments: import("arktype/internal/variants/object.ts").ObjectType<{
68
68
  pull_number: number;
69
69
  review_id: number;
70
+ fresh?: boolean;
70
71
  }, {}>;
71
72
  /**
72
73
  * formats thread blocks into markdown with TOC and line numbers.
@@ -94,6 +95,37 @@ export declare function buildThreadBlocks(threads: ReviewThread[], filePatchMap:
94
95
  lineRange: string;
95
96
  content: string[];
96
97
  }[];
98
+ /**
99
+ * Open threads on the PR that this run should also address, beyond the review
100
+ * that dispatched it.
101
+ *
102
+ * A reviewer working through a PR emits one `pull_request_review_submitted` per
103
+ * submission, so a second review lands seconds after the first — and until this
104
+ * existed, the run already working on the PR could never see it: the filter
105
+ * above keeps only threads carrying a comment from the *dispatched* `reviewId`,
106
+ * so a new comment on a different line was invisible no matter how often the
107
+ * agent re-read. That is what made the in-flight cap in `handleWebhook.ts` a
108
+ * silent loss rather than the coalescing it was documented as (#1103).
109
+ *
110
+ * `fetchAllReviewThreads` already pulls every thread on the PR and caches it for
111
+ * 60s, so this costs no extra round trip and a re-read mid-run picks up whatever
112
+ * landed behind the run.
113
+ *
114
+ * Three exclusions, each load-bearing:
115
+ * - resolved threads are done, and re-opening that conversation is noise.
116
+ * - a thread Pullfrog has already spoken in was handled by an earlier run.
117
+ * That deliberately includes the pushbacks AddressReviews leaves open for a
118
+ * human to mediate — re-addressing one re-litigates a settled disagreement —
119
+ * and it is also what stops this mode replying to Pullfrog's own reviews.
120
+ * - under `mentions` a concurrent thread is in scope only if it actually asked
121
+ * for Pullfrog. Deliberately asymmetric with `threadsForReview`, which is
122
+ * returned whole and always was: the dispatching review is the one the
123
+ * server already adjudicated, and the agent needs its unmentioned threads as
124
+ * context for the comment that did mention it. Which of those to ACT on stays
125
+ * the mode prompt's job. A concurrent review has had no such adjudication, so
126
+ * the mention is the only thing that can authorize it.
127
+ */
128
+ export declare function concurrentThreads(allThreads: (ReviewThread | null)[], threadsForReview: ReviewThread[], input: GetReviewDataInput): ReviewThread[];
97
129
  interface GetReviewDataInput {
98
130
  octokit: Octokit;
99
131
  owner: string;
@@ -101,6 +133,14 @@ interface GetReviewDataInput {
101
133
  pullNumber: number;
102
134
  reviewId: number;
103
135
  approvedBy?: string | undefined;
136
+ /**
137
+ * Widen the read past the dispatching review to the other open threads on the
138
+ * PR — `all` on a Pullfrog-authored PR, `mentions` when only `@pullfrog`-
139
+ * mentioning threads are in scope. Absent (console `fix_review`, and any
140
+ * payload from a server build that predates the field) keeps the read scoped
141
+ * to the dispatching review exactly as before.
142
+ */
143
+ addressScope?: "all" | "mentions" | undefined;
104
144
  tmpdir: string;
105
145
  githubToken: string;
106
146
  }
@@ -148,9 +188,11 @@ export declare function getReviewData(input: GetReviewDataInput): Promise<{
148
188
  export declare function GetReviewCommentsTool(ctx: ToolContext): import("fastmcp").Tool<any, import("@standard-schema/spec").StandardSchemaV1<{
149
189
  pull_number: number;
150
190
  review_id: number;
191
+ fresh?: boolean;
151
192
  }, {
152
193
  pull_number: number;
153
194
  review_id: number;
195
+ fresh?: boolean;
154
196
  }>> & {
155
197
  mutates?: boolean;
156
198
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pullfrog",
3
- "version": "0.1.68",
3
+ "version": "0.1.69",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "pullfrog": "dist/cli.mjs",