pullfrog 0.1.67 → 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.
@@ -50,7 +50,7 @@ export declare function findDanglingPromptToolRefs(params: {
50
50
  toolNames: string[];
51
51
  }): string[];
52
52
  export type { EffortPosition } from "./effort.ts";
53
- export { DEFAULT_EFFORT_POSITION, EFFORT_ALIASES, isEffortPosition, offeredRungs, parseEffortPosition, resolveRung, rungLabel, rungPosition, } from "./effort.ts";
53
+ export { DEFAULT_EFFORT_POSITION, EFFORT_ALIASES, isEffortPosition, offeredRungs, parseEffortPosition, resolveRung, rungLabel, rungPosition, SUBSIDY_RUNG, } from "./effort.ts";
54
54
  export type { AutoTier, ModelAlias, ModelProvider, ProviderConfig } from "./models.ts";
55
55
  export { AUTO_EFFICIENT, AUTO_INTELLIGENT, DEFAULT_PROXY_MODEL, defaultAutoTier, getAutoSelectHintModel, getModelEffortLevels, getModelEnvVars, getModelManagedCredentials, getModelProvider, getProviderDisplayName, isAutoTier, isCardGatedModel, isOssAllowedModel, modelAliases, modelHasStoredAuth, OSS_MODEL_ALLOWLIST, OSS_RECOMMENDED_MODEL, ossFundedModelNames, parseModel, providers, resolveAutoTier, resolveCliModel, resolveDisplayAlias, resolveModelRung, resolveModelSlug, resolveOpenRouterModel, } from "./models.ts";
56
56
  export type ToolPermission = "disabled" | "enabled";
@@ -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";
@@ -257,6 +266,14 @@ export interface WriteablePayload {
257
266
  "~pullfrog": true;
258
267
  /** semantic version of the payload to ensure compatibility */
259
268
  version: string;
269
+ /**
270
+ * which kind of work this run is (`pr-created`, `issue-created`, …). the
271
+ * server's own run-type vocabulary, opaque here: the action only forwards it
272
+ * to `run-context`, which resolves the repo's per-trigger model override
273
+ * against it. absent ⇒ the repo default, which is what every run got before
274
+ * overrides existed.
275
+ */
276
+ type?: string | undefined;
260
277
  /** provider/model slug (e.g. "anthropic/claude-opus") */
261
278
  model?: string | undefined;
262
279
  /**
package/dist/index.js CHANGED
@@ -107718,6 +107718,7 @@ import { performance as performance5 } from "node:perf_hooks";
107718
107718
 
107719
107719
  // effort.ts
107720
107720
  var DEFAULT_EFFORT_POSITION = 0.5;
107721
+ var SUBSIDY_RUNG = "high";
107721
107722
  var EFFORT_ALIASES = {
107722
107723
  low: 0,
107723
107724
  medium: 0.25,
@@ -108121,6 +108122,15 @@ var providers = {
108121
108122
  // the model does cost nothing.
108122
108123
  isFree: true
108123
108124
  },
108125
+ // Zen meters Fable like any other model, so this route reaches it without
108126
+ // the Anthropic access grant `anthropic/claude-fable` still needs.
108127
+ "claude-fable": {
108128
+ displayName: "Claude Fable",
108129
+ resolve: "opencode/claude-fable-5-1",
108130
+ effort: ["low", "medium", "high", "xhigh", "max"],
108131
+ openRouterResolve: "openrouter/~anthropic/claude-fable-latest",
108132
+ subagentModel: "claude-sonnet"
108133
+ },
108124
108134
  "claude-opus": {
108125
108135
  displayName: "Claude Opus",
108126
108136
  resolve: "opencode/claude-opus-5",
@@ -108234,6 +108244,17 @@ var providers = {
108234
108244
  effort: ["minimal", "low", "medium", "high"],
108235
108245
  openRouterResolve: "openrouter/google/gemini-3.6-flash"
108236
108246
  },
108247
+ // Zen serves K3, but rule 7 can only move a mirror when its upstream moves,
108248
+ // so the K3 generation could never reach this block by bumping `kimi-k2`.
108249
+ "kimi-k3": {
108250
+ displayName: "Kimi K3",
108251
+ resolve: "opencode/kimi-k3",
108252
+ // Zen publishes a single rung for K3 where OpenRouter publishes three.
108253
+ effort: ["max"],
108254
+ openRouterEffort: ["low", "high", "max"],
108255
+ openRouterResolve: "openrouter/moonshotai/kimi-k3",
108256
+ subagentModel: "kimi-k2"
108257
+ },
108237
108258
  "kimi-k2": {
108238
108259
  displayName: "Kimi K2",
108239
108260
  // k2.7-code is UNDEPLOYED on Zen — still listed in /zen/v1/models, but
@@ -108245,25 +108266,57 @@ var providers = {
108245
108266
  resolve: "opencode/kimi-k2.6",
108246
108267
  openRouterResolve: "openrouter/moonshotai/kimi-k2.7-code"
108247
108268
  },
108269
+ // M3 is its own line beside M2, not a bump of it — same reason `kimi-k3`
108270
+ // sits beside `kimi-k2` rather than replacing it.
108271
+ "minimax-m3": {
108272
+ displayName: "MiniMax M3",
108273
+ resolve: "opencode/minimax-m3",
108274
+ openRouterResolve: "openrouter/minimax/minimax-m3"
108275
+ },
108248
108276
  // slug pins the m2 line for DB stability; resolve tracks the current m2.7.
108249
108277
  "minimax-m2.5": {
108250
108278
  displayName: "MiniMax M2",
108251
108279
  resolve: "opencode/minimax-m2.7",
108252
108280
  openRouterResolve: "openrouter/minimax/minimax-m2.7"
108253
108281
  },
108282
+ // Z.ai and xAI reach Zen subscribers only here — before this, `opencode-go`
108283
+ // was the catalog's only GLM route, and Grok had no Zen route at all.
108284
+ glm: {
108285
+ displayName: "GLM",
108286
+ resolve: "opencode/glm-5.3",
108287
+ effort: ["low", "high", "max"],
108288
+ openRouterResolve: "openrouter/z-ai/glm-5.3"
108289
+ },
108290
+ grok: {
108291
+ displayName: "Grok",
108292
+ resolve: "opencode/grok-4.6",
108293
+ effort: ["low", "medium", "high", "xhigh"],
108294
+ openRouterResolve: "openrouter/x-ai/grok-4.6"
108295
+ },
108254
108296
  "gpt-5-nano": {
108255
108297
  displayName: "GPT Nano",
108256
108298
  resolve: "opencode/gpt-5-nano",
108257
108299
  effort: ["minimal", "low", "medium", "high"],
108258
108300
  openRouterResolve: "openrouter/openai/gpt-5-nano"
108259
108301
  },
108302
+ // Zen's live free MiMo, and the second free row in a menu that big-pickle
108303
+ // was alone in since `mimo-v2-pro-free` lost its model.
108304
+ mimo: {
108305
+ displayName: "MiMo",
108306
+ resolve: "opencode/mimo-v2.5-free",
108307
+ // free to run, still gated on the provider's own OPENCODE_API_KEY —
108308
+ // see the big-pickle note above (#1077).
108309
+ isFree: true
108310
+ },
108260
108311
  "mimo-v2-pro-free": {
108261
108312
  displayName: "MiMo V2 Pro",
108262
108313
  resolve: "opencode/mimo-v2-pro-free",
108263
108314
  // free to run, still gated on the provider's own OPENCODE_API_KEY —
108264
108315
  // see the big-pickle note above (#1077).
108265
108316
  isFree: true,
108266
- fallback: "opencode/big-pickle"
108317
+ // the model id Zen dropped; land a stored MiMo pick back on MiMo rather
108318
+ // than on the unrelated model it had to settle for while none was live.
108319
+ fallback: "opencode/mimo"
108267
108320
  },
108268
108321
  "minimax-m2.5-free": {
108269
108322
  displayName: "MiniMax M2",
@@ -108279,10 +108332,11 @@ var providers = {
108279
108332
  // OpenCode Go is a separate $10/mo subscription from Zen, served on its own
108280
108333
  // base URL (`https://opencode.ai/zen/go/v1`) but authenticated with the SAME
108281
108334
  // `OPENCODE_API_KEY`. it carries the open-weight coding models plus a couple
108282
- // of frontier ones, and 14 of the ids below are served ONLY here — Zen's
108283
- // `/v1/models` does not list glm-5.3*, qwen3.7/3.8-*, mimo-*, longcat-2.0,
108284
- // hy3 or muse-spark. so for a Go subscriber this provider is not a duplicate
108285
- // 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.
108286
108340
  // like `opencode` and `openrouter` this is a ROUTER, not a vendor: slugs and
108287
108341
  // display names mirror the upstream brand tier, and the picker groups them
108288
108342
  // under the upstream vendor.
@@ -108351,13 +108405,25 @@ var providers = {
108351
108405
  // `high` the Go route does not.
108352
108406
  effort: ["low", "medium", "xhigh"],
108353
108407
  openRouterEffort: ["minimal", "low", "medium", "high", "xhigh"],
108354
- 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"
108355
108411
  },
108356
108412
  "qwen-plus": {
108357
108413
  displayName: "Qwen Plus",
108358
108414
  resolve: "opencode-go/qwen3.7-plus",
108359
108415
  openRouterResolve: "openrouter/qwen/qwen3.7-plus"
108360
108416
  },
108417
+ // the cheap rung Alibaba added under Plus (0.15/0.47 against 0.5/3), and
108418
+ // the only model either OpenCode plan has added since this block was written.
108419
+ "qwen-flash": {
108420
+ displayName: "Qwen Flash",
108421
+ resolve: "opencode-go/qwen3.8-flash",
108422
+ effort: ["low", "medium", "xhigh"],
108423
+ // the Go route publishes rungs where OpenRouter publishes none.
108424
+ openRouterEffort: [],
108425
+ openRouterResolve: "openrouter/qwen/qwen3.8-flash"
108426
+ },
108361
108427
  // MiniMax — parity with opencode/* and openrouter/*; the m2 slug pins the
108362
108428
  // line for DB stability while the resolve tracks the current m2.7.
108363
108429
  "minimax-m3": {
@@ -109482,6 +109548,7 @@ async function installFromNpmTarball(params) {
109482
109548
  // utils/providerErrors.ts
109483
109549
  var PROVIDER_BILLING_EXHAUSTED_LABEL = "provider billing exhausted";
109484
109550
  var PROVIDER_NO_ENDPOINTS_LABEL = "provider no routable endpoints";
109551
+ var PROVIDER_USAGE_LIMIT_LABEL = "provider usage limit reached";
109485
109552
  var statusKey = `\\b(?:status[_ ]?code|http[_ ]?status|status)["']?\\s*[:=]\\s*["']?`;
109486
109553
  var ANTHROPIC_SPEND_CAP_PATTERN = /reached your specified API usage limits\.?(?:\s*You will regain access on ([^"\n]+?)\.)?/i;
109487
109554
  function findAnthropicSpendCap(text) {
@@ -109527,6 +109594,14 @@ var PROVIDER_ERROR_PATTERNS = [
109527
109594
  // the wallet-shaped patterns above reach, and its 400 sits nowhere near a
109528
109595
  // `status:` key either — so the whole message was dropped on the floor (#1208).
109529
109596
  { regex: ANTHROPIC_SPEND_CAP_PATTERN, label: PROVIDER_BILLING_EXHAUSTED_LABEL },
109597
+ // OpenCode Zen / Codex-subscription cap: `AI_APICallError: The usage limit has
109598
+ // been reached`. Its noun is "usage limit" with no "rate", no status code and
109599
+ // no wallet wording, so every pattern here missed it and `lastProviderError`
109600
+ // stayed unset — which suppresses nothing, so the run was reported as "The
109601
+ // model produced no output at all … usually transient; re-running often
109602
+ // succeeds" while the same log held nine of these. Measured on a live account
109603
+ // whose ChatGPT subscription was capped.
109604
+ { regex: /\bthe usage limit has been reached\b/i, label: PROVIDER_USAGE_LIMIT_LABEL },
109530
109605
  // the OpenRouter shapes `isRouterKeylimitExhaustedError` matches. on a Router
109531
109606
  // run those mean the PULLFROG wallet is empty and `renderRunError` returns a
109532
109607
  // `BillingError` before ever reaching this list; on a BYOK run the very same
@@ -109657,7 +109732,7 @@ function ossEffortFloor(ctx) {
109657
109732
  const alias = resolveRunAlias(ctx);
109658
109733
  const published = alias?.openRouterEffort ?? alias?.effort;
109659
109734
  if (!published) return 0;
109660
- return rungPosition({ rung: "high", published }) ?? 0;
109735
+ return rungPosition({ rung: SUBSIDY_RUNG, published }) ?? 0;
109661
109736
  }
109662
109737
  function resolveRunEffort(ctx) {
109663
109738
  const configured = ctx.payload.effort !== void 0;
@@ -109689,7 +109764,7 @@ var import_semver = __toESM(require_semver2(), 1);
109689
109764
  // package.json
109690
109765
  var package_default = {
109691
109766
  name: "pullfrog",
109692
- version: "0.1.67",
109767
+ version: "0.1.69",
109693
109768
  type: "module",
109694
109769
  bin: {
109695
109770
  pullfrog: "dist/cli.mjs",
@@ -110496,6 +110571,7 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
110496
110571
  - record what was done (or why nothing was done)
110497
110572
 
110498
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.
110499
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
110500
110576
  - ${commitStep}
110501
110577
 
@@ -110505,7 +110581,7 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
110505
110581
  - **once the fix is live on the remote**, for each thread you acted on:
110506
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.
110507
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.
110508
- - 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.`
110509
110585
  },
110510
110586
  // Review and IncrementalReview route the minimum reviewfrog specialists
110511
110587
  // needed to cover unresolved, disposition-changing hypotheses. Most runs
@@ -192373,6 +192449,9 @@ function formatModelLabel(params) {
192373
192449
  if (params.fallbackFrom) {
192374
192450
  return `${base} (credentials for ${providerDisplayName(params.fallbackFrom)} were rejected by the provider)`;
192375
192451
  }
192452
+ if (params.clamped?.reason === "trial") {
192453
+ return `${base} (free trial)`;
192454
+ }
192376
192455
  if (params.clamped) {
192377
192456
  const target = isAutoTier(params.clamped.from) ? "the intelligent tier" : `\`${resolveDisplayAlias(params.clamped.from)?.displayName ?? params.clamped.from}\``;
192378
192457
  return params.clamped.reason === "card" ? `${base} (${target} needs a [card on file](https://docs.pullfrog.com/models))` : `${base} (${target} needs a [provider key](https://docs.pullfrog.com/models) \u2014 no Router support yet)`;
@@ -192382,6 +192461,15 @@ function formatModelLabel(params) {
192382
192461
  }
192383
192462
  return base;
192384
192463
  }
192464
+ function buildTrialDisclosure(owner) {
192465
+ const consoleUrl = owner ? `https://pullfrog.com/console/${owner}` : "https://pullfrog.com/console";
192466
+ return [
192467
+ "---",
192468
+ "",
192469
+ "> [!IMPORTANT]",
192470
+ `> **This run used the free trial model.** \`DeepSeek Flash\` is fast and cheap \u2014 expect lighter work than a frontier model. [Connect a subscription or API key \u2192](${consoleUrl})`
192471
+ ].join("\n");
192472
+ }
192385
192473
  function buildPullfrogFooter(params) {
192386
192474
  const parts = [];
192387
192475
  if (params.shaPinned) {
@@ -192414,10 +192502,13 @@ function buildPullfrogFooter(params) {
192414
192502
  );
192415
192503
  }
192416
192504
  const allParts = [...parts, "[\u{1D54F}](https://x.com/pullfrogai)"];
192505
+ const disclosure = params.clamped?.reason === "trial" ? `${buildTrialDisclosure(params.owner)}
192506
+
192507
+ ` : "";
192417
192508
  return `
192418
192509
 
192419
192510
  ${PULLFROG_DIVIDER}
192420
- <sup>${FROG_LOGO}&nbsp;&nbsp;\uFF5C ${allParts.join(" \uFF5C ")}</sup>`;
192511
+ ${disclosure}<sup>${FROG_LOGO}&nbsp;&nbsp;\uFF5C ${allParts.join(" \uFF5C ")}</sup>`;
192421
192512
  }
192422
192513
  function stripExistingFooter(body) {
192423
192514
  const dividerIndex = body.indexOf(PULLFROG_DIVIDER);
@@ -192571,7 +192662,8 @@ function buildCommentFooter(ctx, customParts) {
192571
192662
  clamped: ctx.toolState.modelClamped,
192572
192663
  unselectedProxyDefault: ctx.toolState.unselectedProxyDefault,
192573
192664
  shaPinned: ctx.toolState.shaPinned,
192574
- oss: ctx.oss
192665
+ oss: ctx.oss,
192666
+ owner: ctx.repo.owner
192575
192667
  });
192576
192668
  }
192577
192669
  function buildImplementPlanLink(ctx, issueNumber, commentId) {
@@ -193570,7 +193662,8 @@ async function createAndSubmitWithFooter(ctx, params, opts) {
193570
193662
  clamped: ctx.toolState.modelClamped,
193571
193663
  unselectedProxyDefault: ctx.toolState.unselectedProxyDefault,
193572
193664
  shaPinned: ctx.toolState.shaPinned,
193573
- oss: ctx.oss
193665
+ oss: ctx.oss,
193666
+ owner: ctx.repo.owner
193574
193667
  });
193575
193668
  return await ctx.octokit.rest.pulls.submitReview({
193576
193669
  owner: params.owner,
@@ -196331,7 +196424,8 @@ function buildPrBodyWithFooter(ctx, body) {
196331
196424
  clamped: ctx.toolState.modelClamped,
196332
196425
  unselectedProxyDefault: ctx.toolState.unselectedProxyDefault,
196333
196426
  shaPinned: ctx.toolState.shaPinned,
196334
- oss: ctx.oss
196427
+ oss: ctx.oss,
196428
+ owner: ctx.repo.owner
196335
196429
  });
196336
196430
  const bodyWithoutFooter = stripExistingFooter(fixDoubleEscapedString(body));
196337
196431
  return `${bodyWithoutFooter}${footer}`;
@@ -196666,7 +196760,10 @@ function extractFromFilePatches(hunks, startLine, endLine, side) {
196666
196760
  }
196667
196761
  var GetReviewComments = type({
196668
196762
  pull_number: type.number.describe("The pull request number"),
196669
- 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
+ )
196670
196767
  });
196671
196768
  function hasThumbsUpFrom(comment, username) {
196672
196769
  if (!comment.reactionGroups) return false;
@@ -196700,7 +196797,7 @@ function formatReviewThreads(threadBlocks, header) {
196700
196797
  }
196701
196798
  const lines = [];
196702
196799
  lines.push(
196703
- `# 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}`
196704
196801
  );
196705
196802
  lines.push("");
196706
196803
  if (threadBlocks.length > 0) {
@@ -196841,11 +196938,27 @@ async function getReviewThreads(input2) {
196841
196938
  if (!thread?.comments?.nodes) return false;
196842
196939
  return thread.comments.nodes.some((c) => c?.pullRequestReview?.databaseId === input2.reviewId);
196843
196940
  });
196844
- if (!input2.approvedBy) {
196845
- return threadsForReview;
196941
+ if (input2.approvedBy) {
196942
+ const username = input2.approvedBy;
196943
+ return threadsForReview.filter((thread) => threadHasThumbsUpFrom(thread, username));
196846
196944
  }
196847
- const username = input2.approvedBy;
196848
- 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");
196849
196962
  }
196850
196963
  function formatReviewData(input2) {
196851
196964
  const rawReviewBody = input2.review.body;
@@ -196942,6 +197055,7 @@ function GetReviewCommentsTool(ctx) {
196942
197055
  parameters: GetReviewComments,
196943
197056
  execute: execute(async (params) => {
196944
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);
196945
197059
  const result = await getReviewData({
196946
197060
  octokit: ctx.octokit,
196947
197061
  owner: ctx.repo.owner,
@@ -196949,6 +197063,7 @@ function GetReviewCommentsTool(ctx) {
196949
197063
  pullNumber: params.pull_number,
196950
197064
  reviewId: params.review_id,
196951
197065
  approvedBy,
197066
+ addressScope: ctx.payload.event.address_scope,
196952
197067
  tmpdir: ctx.tmpdir,
196953
197068
  githubToken: ctx.githubInstallationToken
196954
197069
  });
@@ -196979,7 +197094,7 @@ function GetReviewCommentsTool(ctx) {
196979
197094
  threadCount: threadBlocks.length,
196980
197095
  commentsPath,
196981
197096
  toc: formatted.toc,
196982
- 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.`
196983
197098
  };
196984
197099
  })
196985
197100
  });
@@ -197638,10 +197753,12 @@ function isByokSetupError(text) {
197638
197753
  var SECRETS_UNAVAILABLE_MARKER = "couldn't load your Pullfrog secrets";
197639
197754
  var ROUTER_UNFUNDED_MARKER = "your Pullfrog Router balance is empty";
197640
197755
  var CREDENTIAL_REJECTED_MARKER = "was rejected by its provider";
197641
- function buildKeyError(params) {
197642
- if (params.secretsUnavailable) return buildSecretsUnavailableError(params);
197643
- if (params.routerUnfunded) return buildRouterUnfundedError(params);
197644
- return buildMissingApiKeyError(params);
197756
+ var NoUsableCredentialError = class extends Error {
197757
+ };
197758
+ function throwKeyError(params) {
197759
+ if (params.secretsUnavailable) throw new Error(buildSecretsUnavailableError(params));
197760
+ if (params.routerUnfunded) throw new NoUsableCredentialError(buildRouterUnfundedError(params));
197761
+ throw new NoUsableCredentialError(buildMissingApiKeyError(params));
197645
197762
  }
197646
197763
  function buildRouterUnfundedError(params) {
197647
197764
  const billingUrl = `${getApiUrl()}/console/${params.owner}#billing`;
@@ -197840,57 +197957,51 @@ function validateAgentApiKey(params) {
197840
197957
  validateVertexSetup({ owner: params.owner, name: params.name });
197841
197958
  return;
197842
197959
  }
197843
- validateBedrockSetup({ owner: params.owner, name: params.name });
197844
- return;
197960
+ if (process.env[BEDROCK_MODEL_ID_ENV]?.trim() === params.model) {
197961
+ validateBedrockSetup({ owner: params.owner, name: params.name });
197962
+ return;
197963
+ }
197845
197964
  }
197846
197965
  if (params.agent.name === "opencode") {
197847
197966
  if (params.authorized.has(params.model)) return;
197848
197967
  const reason = getModelsFailure();
197849
197968
  if (reason) throw new Error(reason);
197850
197969
  if (getModelEnvVars(params.model).some(hasEnvVar2)) return;
197851
- throw new Error(
197852
- buildKeyError({
197853
- owner: params.owner,
197854
- name: params.name,
197855
- model: params.model,
197856
- secretsUnavailable: params.secretsUnavailable,
197857
- routerUnfunded: params.routerUnfunded
197858
- })
197859
- );
197860
- }
197861
- if (hasSingleProviderAuth(params.agent.name)) return;
197862
- throw new Error(
197863
- buildKeyError({
197970
+ throwKeyError({
197864
197971
  owner: params.owner,
197865
197972
  name: params.name,
197866
197973
  model: params.model,
197867
197974
  secretsUnavailable: params.secretsUnavailable,
197868
197975
  routerUnfunded: params.routerUnfunded
197869
- })
197870
- );
197976
+ });
197977
+ }
197978
+ if (hasSingleProviderAuth(params.agent.name)) return;
197979
+ throwKeyError({
197980
+ owner: params.owner,
197981
+ name: params.name,
197982
+ model: params.model,
197983
+ secretsUnavailable: params.secretsUnavailable,
197984
+ routerUnfunded: params.routerUnfunded
197985
+ });
197871
197986
  }
197872
197987
  if (params.agent.name === "opencode") {
197873
197988
  if ([...params.authorized].some(modelHasRuntimeAuth)) return;
197874
197989
  const reason = getModelsFailure();
197875
197990
  if (reason) throw new Error(reason);
197876
- throw new Error(
197877
- buildKeyError({
197878
- owner: params.owner,
197879
- name: params.name,
197880
- secretsUnavailable: params.secretsUnavailable,
197881
- routerUnfunded: params.routerUnfunded
197882
- })
197883
- );
197884
- }
197885
- if (hasSingleProviderAuth(params.agent.name)) return;
197886
- throw new Error(
197887
- buildKeyError({
197991
+ throwKeyError({
197888
197992
  owner: params.owner,
197889
197993
  name: params.name,
197890
197994
  secretsUnavailable: params.secretsUnavailable,
197891
197995
  routerUnfunded: params.routerUnfunded
197892
- })
197893
- );
197996
+ });
197997
+ }
197998
+ if (hasSingleProviderAuth(params.agent.name)) return;
197999
+ throwKeyError({
198000
+ owner: params.owner,
198001
+ name: params.name,
198002
+ secretsUnavailable: params.secretsUnavailable,
198003
+ routerUnfunded: params.routerUnfunded
198004
+ });
197894
198005
  }
197895
198006
  function isApiKeyAuthError(text) {
197896
198007
  if (!text) return false;
@@ -198015,9 +198126,9 @@ function formatApiKeyErrorSummary(params) {
198015
198126
  ].join("\n");
198016
198127
  }
198017
198128
  return [
198018
- `**Your LLM provider API key was rejected.** Rotate the key in your provider dashboard, then update the matching GitHub Actions secret.`,
198129
+ `**Your LLM provider API key was rejected.** Rotate the key in your provider dashboard, then update the copy Pullfrog uses \u2014 wherever you saved it.`,
198019
198130
  "",
198020
- `[Update repo secret \u2192](${githubSecretsUrl}) \xB7 [Model settings \u2192](${settingsUrl}) \xB7 [Setup docs \u2192](https://docs.pullfrog.com/keys) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
198131
+ `[Pullfrog secrets \u2192](${getApiUrl()}/console/${params.owner}) \xB7 [Repo secrets \u2192](${githubSecretsUrl}) \xB7 [Model settings \u2192](${settingsUrl}) \xB7 [Setup docs \u2192](https://docs.pullfrog.com/keys) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
198021
198132
  ].join("\n");
198022
198133
  }
198023
198134
 
@@ -199243,6 +199354,9 @@ var DebugInput = type.enumerated("disabled", "enabled");
199243
199354
  var JsonPayload = type({
199244
199355
  "~pullfrog": "true",
199245
199356
  version: "string",
199357
+ // the server's run type, forwarded verbatim to run-context. optional so a
199358
+ // payload from an older server build still parses against a newer action.
199359
+ "type?": "string | undefined",
199246
199360
  "model?": "string | undefined",
199247
199361
  "modelExplicit?": "boolean | undefined",
199248
199362
  "effort?": "number | string | undefined",
@@ -199728,7 +199842,8 @@ ${ctx.error}` : ctx.error;
199728
199842
  clamped: ctx.toolState.modelClamped,
199729
199843
  unselectedProxyDefault: ctx.toolState.unselectedProxyDefault,
199730
199844
  shaPinned: ctx.toolState.shaPinned,
199731
- oss: ctx.toolState.oss
199845
+ oss: ctx.toolState.oss,
199846
+ owner: repoContext.owner
199732
199847
  });
199733
199848
  const body = `${formattedError}${footer}`;
199734
199849
  const comment = ctx.toolState.progressComment;
@@ -199811,7 +199926,7 @@ async function mintProxyKey(ctx) {
199811
199926
  }
199812
199927
  }
199813
199928
  async function buildProxyTokenHeaders(ctx) {
199814
- const fundingSource = ctx.oss ? "oss" : "router";
199929
+ const fundingSource = ctx.fundingSource;
199815
199930
  if (ctx.oidcCredentials) {
199816
199931
  const creds = ctx.oidcCredentials;
199817
199932
  const oidcToken = await op(() => fetchIdTokenFromStash(creds), {
@@ -199843,7 +199958,7 @@ async function resolveProxyModel(ctx) {
199843
199958
  const key = await mintProxyKey({
199844
199959
  oidcCredentials: ctx.oidcCredentials,
199845
199960
  repo: ctx.repo,
199846
- oss: ctx.oss
199961
+ fundingSource: ctx.oss ? "oss" : "router"
199847
199962
  });
199848
199963
  if (!key) return;
199849
199964
  process.env.OPENROUTER_API_KEY = key;
@@ -199915,6 +200030,26 @@ async function runProxyResolution(ctx) {
199915
200030
  throw error62;
199916
200031
  }
199917
200032
  }
200033
+ async function resolveTrialFallback(ctx) {
200034
+ if (process.env.PULLFROG_MODEL?.trim()) return false;
200035
+ if (!ctx.oidcCredentials && !isLocalApiUrl()) return false;
200036
+ const key = await mintProxyKey({
200037
+ oidcCredentials: ctx.oidcCredentials,
200038
+ repo: ctx.repo,
200039
+ fundingSource: "trial"
200040
+ });
200041
+ if (!key) return false;
200042
+ process.env.OPENROUTER_API_KEY = key;
200043
+ core9.setSecret(key);
200044
+ ctx.payload.proxyModel = DEFAULT_PROXY_MODEL;
200045
+ ctx.toolState.model = DEFAULT_PROXY_MODEL;
200046
+ ctx.toolState.modelClamped = {
200047
+ from: ctx.configuredModel ?? DEFAULT_PROXY_MODEL,
200048
+ reason: "trial"
200049
+ };
200050
+ log.info(`\xBB proxy: trial \u2192 ${DEFAULT_PROXY_MODEL}`);
200051
+ return true;
200052
+ }
199918
200053
 
199919
200054
  // utils/prSummary.ts
199920
200055
  import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
@@ -200092,8 +200227,9 @@ async function fetchRunContext(params) {
200092
200227
  if (params.oidcToken) {
200093
200228
  headers["X-GitHub-OIDC-Token"] = params.oidcToken;
200094
200229
  }
200230
+ const query = params.runType ? `?type=${encodeURIComponent(params.runType)}` : "";
200095
200231
  const response = await apiFetch({
200096
- path: `/api/repo/${params.repoContext.owner}/${params.repoContext.name}/run-context`,
200232
+ path: `/api/repo/${params.repoContext.owner}/${params.repoContext.name}/run-context${query}`,
200097
200233
  headers,
200098
200234
  signal: controller.signal
200099
200235
  });
@@ -200130,7 +200266,8 @@ async function fetchRunContext(params) {
200130
200266
  proxyModel: data.proxyModel,
200131
200267
  dbSecrets: data.dbSecrets,
200132
200268
  secretsUnavailable: data.secretsUnavailable,
200133
- routerUnfunded: data.routerUnfunded
200269
+ routerUnfunded: data.routerUnfunded,
200270
+ trialFallback: data.trialFallback
200134
200271
  };
200135
200272
  } catch {
200136
200273
  clearTimeout(timeoutId);
@@ -200171,7 +200308,7 @@ async function resolveRunContextData(params) {
200171
200308
  retries: [100, 500],
200172
200309
  bail: (error62) => !isTransientOctokitError(error62)
200173
200310
  })(),
200174
- fetchRunContext({ token: params.token, repoContext, oidcToken })
200311
+ fetchRunContext({ token: params.token, repoContext, oidcToken, runType: params.runType })
200175
200312
  ]);
200176
200313
  return {
200177
200314
  repo: {
@@ -200189,7 +200326,8 @@ async function resolveRunContextData(params) {
200189
200326
  // a failed mint on a runner that should have been able to mint is the same
200190
200327
  // outcome as the server-side failure: the run never sees stored secrets.
200191
200328
  secretsUnavailable: runContext.secretsUnavailable || !!process.env.ACTIONS_ID_TOKEN_REQUEST_URL && oidcToken === void 0,
200192
- routerUnfunded: runContext.routerUnfunded
200329
+ routerUnfunded: runContext.routerUnfunded,
200330
+ trialFallback: runContext.trialFallback
200193
200331
  };
200194
200332
  }
200195
200333
 
@@ -201269,7 +201407,11 @@ async function main() {
201269
201407
  resolveGit();
201270
201408
  const jobToken = getJobToken();
201271
201409
  const initialOctokit = createOctokit(jobToken);
201272
- const runContext = await resolveRunContextData({ octokit: initialOctokit, token: jobToken });
201410
+ const runContext = await resolveRunContextData({
201411
+ octokit: initialOctokit,
201412
+ token: jobToken,
201413
+ runType: typeof resolvedPromptInput === "string" ? void 0 : resolvedPromptInput.type
201414
+ });
201273
201415
  timer.checkpoint("runContextData");
201274
201416
  const payload = resolvePayload(resolvedPromptInput, runContext.repoSettings);
201275
201417
  const toolState = initToolState({
@@ -201450,7 +201592,7 @@ async function main() {
201450
201592
  }
201451
201593
  const resolvedModel = credentials.kind === "fellBack" ? credentials.replacement : configuredModel;
201452
201594
  vertexCredentials = materializeVertexCredentials({ model: resolvedModel });
201453
- const agent2 = resolveAgent({
201595
+ let agent2 = resolveAgent({
201454
201596
  model: resolvedModel,
201455
201597
  proxyModel: payload.proxyModel,
201456
201598
  // the account opt-in and the canary arm are both admissions to codex, so
@@ -201462,15 +201604,32 @@ async function main() {
201462
201604
  const effectiveModel = payload.proxyModel ?? resolvedModel ?? payload.model;
201463
201605
  toolState.model = effectiveModel;
201464
201606
  if (!payload.proxyModel) {
201465
- validateAgentApiKey({
201466
- agent: agent2,
201467
- model: effectiveModel,
201468
- authorized: getAuthorizedModels(),
201469
- owner: runContext.repo.owner,
201470
- name: runContext.repo.name,
201471
- secretsUnavailable: runContext.secretsUnavailable,
201472
- routerUnfunded: runContext.routerUnfunded
201473
- });
201607
+ try {
201608
+ validateAgentApiKey({
201609
+ agent: agent2,
201610
+ model: effectiveModel,
201611
+ authorized: getAuthorizedModels(),
201612
+ owner: runContext.repo.owner,
201613
+ name: runContext.repo.name,
201614
+ secretsUnavailable: runContext.secretsUnavailable,
201615
+ routerUnfunded: runContext.routerUnfunded
201616
+ });
201617
+ } catch (missingKey) {
201618
+ const funded = runContext.trialFallback && missingKey instanceof NoUsableCredentialError ? await resolveTrialFallback({
201619
+ payload,
201620
+ configuredModel: effectiveModel,
201621
+ oidcCredentials,
201622
+ repo: runContext.repo,
201623
+ toolState
201624
+ }) : false;
201625
+ if (!funded) throw missingKey;
201626
+ agent2 = resolveAgent({
201627
+ model: resolvedModel,
201628
+ proxyModel: payload.proxyModel,
201629
+ codexAgent: runContext.repoSettings.codexAgent || payload.codexArm === true
201630
+ });
201631
+ toolState.agent = agent2.name;
201632
+ }
201474
201633
  }
201475
201634
  await setupGit({
201476
201635
  gitToken: tokenRef.gitToken,