pullfrog 0.1.95 → 0.1.97

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.
@@ -6,7 +6,7 @@ export { CLI_CONTRACT_HEADER, CLI_CONTRACT_VERSION, CLI_UPGRADE_MESSAGE } from "
6
6
  export type { ConfigField, ConfigScope, ConfigValue } from "../configuration.ts";
7
7
  export { configEntrySchema, configFields, configResponseSchema, configValueSchema, } from "../configuration.ts";
8
8
  export type { AuthorPermission, AutoTier, EffortPosition, ModelAlias, ModelProvider, Payload, PayloadEvent, PayloadRouting, ProviderConfig, PushPermission, RouterSource, RouterTier, ShellPermission, ToolPermission, WriteablePayload, XrepoConfig, } from "../external.ts";
9
- export { AUTO_EFFICIENT, AUTO_INTELLIGENT, AUTO_ROUTER, autoLadder, autoRoutedSlug, DEFAULT_EFFORT_POSITION, DEFAULT_PROXY_MODEL, defaultAutoTier, EFFORT_ALIASES, getAutoSelectHintModel, getModelEffortLevels, getModelEnvVars, getModelManagedCredentials, getModelProvider, getProviderDisplayName, isAutoRouted, isAutoSlug, isAutoTier, isCardGatedModel, isEffortPosition, isOssAllowedModel, isRouterTier, ladderRungs, modelAliases, modelHasStoredAuth, modelIdLabel, OSS_MODEL_ALLOWLIST, OSS_RECOMMENDED_MODEL, offeredRungs, ossFundedModelNames, PROVIDER_GATEWAY_URL_ENV, parseEffortPosition, parseModel, pickUsesOpenRouter, providers, pullfrogMcpName, ROUTED_PROVIDERS, ROUTER_LADDER, ROUTER_TIERS, resolveAutoTier, resolveCliModel, resolveDisplayAlias, resolveModelRung, resolveModelSlug, resolveOpenRouterModel, resolveRoutedModel, resolveRung, routerProxyOptOut, rungLabel, rungPosition, SUBSIDY_RUNG, } from "../external.ts";
9
+ export { AUTO_EFFICIENT, AUTO_INTELLIGENT, AUTO_ROUTER, autoLadder, autoRoutedSlug, DEFAULT_EFFORT_POSITION, DEFAULT_PROXY_MODEL, defaultAutoTier, EFFORT_ALIASES, getAutoSelectHintModel, getModelEffortLevels, getModelEnvVars, getModelManagedCredentials, getModelProvider, getProviderDisplayName, isAutoRouted, isAutoSlug, isAutoTier, isCardGatedModel, isEffortPosition, isOssAllowedModel, isRouterTier, ladderRungs, modelAliases, modelHasStoredAuth, modelIdLabel, OSS_MODEL_ALLOWLIST, OSS_RECOMMENDED_MODEL, offeredRungs, ossFundedModelNames, PROVIDER_GATEWAY_URL_ENV, parseEffortPosition, parseModel, providers, pullfrogMcpName, ROUTED_PROVIDERS, ROUTER_LADDER, ROUTER_TIERS, resolveAutoTier, resolveCliModel, resolveDisplayAlias, resolveModelRung, resolveModelSlug, resolveOpenRouterModel, resolvePickModel, resolveRoutedModel, resolveRung, routerProxyOptOut, rungLabel, rungPosition, SUBSIDY_RUNG, } from "../external.ts";
10
10
  export type { Mode } from "../modes.ts";
11
11
  export { modes } from "../modes.ts";
12
12
  export { type CommercialRefusal, commercialPaywallBody } from "../utils/billingErrors.ts";
package/dist/internal.js CHANGED
@@ -8,7 +8,7 @@ var __export = (target, all) => {
8
8
  // package.json
9
9
  var package_default = {
10
10
  name: "pullfrog",
11
- version: "0.1.95",
11
+ version: "0.1.97",
12
12
  cliContractVersion: "1",
13
13
  type: "module",
14
14
  bin: {
@@ -74,7 +74,7 @@ var package_default = {
74
74
  undici: "^7.22.0",
75
75
  vitest: "^4.0.17",
76
76
  yaml: "^2.8.2",
77
- yes: "0.1.0",
77
+ yes: "0.2.3",
78
78
  zod: "4.3.6"
79
79
  },
80
80
  repository: {
@@ -14932,6 +14932,15 @@ var providers = {
14932
14932
  openRouterEffort: [],
14933
14933
  openRouterResolve: "openrouter/qwen/qwen3.8-flash"
14934
14934
  },
14935
+ // Zen lists Mistral Large 4 at half its list price for launch, level with
14936
+ // the routers. hidden until a published `pullfrog` resolves it, as there.
14937
+ "mistral-large": {
14938
+ displayName: "Mistral Large",
14939
+ resolve: "opencode/mistral-large-4",
14940
+ effort: ["none", "high"],
14941
+ openRouterResolve: "openrouter/mistralai/mistral-large-4-0",
14942
+ hidden: true
14943
+ },
14935
14944
  "gpt-5-nano": {
14936
14945
  displayName: "GPT Nano",
14937
14946
  resolve: "opencode/gpt-5.4-nano",
@@ -14980,12 +14989,12 @@ var providers = {
14980
14989
  fallback: "opencode/big-pickle"
14981
14990
  },
14982
14991
  // Zen's FREE stealth preview (listed 2026-09-23), the third after
14983
- // big-pickle and union-alpha. OpenRouter's listing: prompts and
14984
- // completions may be retained by the provider, not trained on. unlike the
14985
- // first two it publishes an effort ladder, so the gate mirrors one here.
14992
+ // big-pickle and union-alpha. Zen's docs: zero retention, no training.
14993
+ // unlike the first two it publishes an effort ladder, so the gate
14994
+ // mirrors one here.
14986
14995
  "space-bunny": {
14987
14996
  displayName: "Space Bunny",
14988
- description: "Stealth preview; prompts may be retained",
14997
+ description: "Stealth preview, free for a limited time",
14989
14998
  resolve: "opencode/space-bunny-free",
14990
14999
  effort: ["low", "medium", "high", "xhigh", "max"],
14991
15000
  // free to run, still gated on the provider's own OPENCODE_API_KEY —
@@ -15003,17 +15012,30 @@ var providers = {
15003
15012
  // see the big-pickle note above (#1077).
15004
15013
  isFree: true
15005
15014
  },
15006
- // Zen's FREE stealth preview (listed 2026-10-01), the fifth promo. Zen
15007
- // has not published its data terms yet, so the description claims none.
15015
+ // Zen's FREE stealth preview (listed 2026-10-01), the fifth promo. Zen's
15016
+ // docs: data may be used to improve the model during the free period.
15008
15017
  "fledge-alpha": {
15009
15018
  displayName: "Fledge Alpha",
15010
- description: "Stealth preview, free for a limited time",
15019
+ description: "Stealth preview; data may be used to improve it",
15011
15020
  resolve: "opencode/fledge-alpha-free",
15012
15021
  effort: ["low", "high", "max"],
15013
15022
  // free to run, still gated on the provider's own OPENCODE_API_KEY —
15014
15023
  // see the big-pickle note above (#1077).
15015
15024
  isFree: true
15016
15025
  },
15026
+ // Zen's FREE preview (listed 2026-10-06), the sixth promo, on Fledge
15027
+ // Alpha's terms. hidden until a published `pullfrog` resolves it.
15028
+ exo: {
15029
+ displayName: "Exo",
15030
+ description: "Preview; data may be used to improve it",
15031
+ resolve: "opencode/exo-free",
15032
+ // Zen publishes a single rung.
15033
+ effort: ["high"],
15034
+ // free to run, still gated on the provider's own OPENCODE_API_KEY —
15035
+ // see the big-pickle note above (#1077).
15036
+ isFree: true,
15037
+ hidden: true
15038
+ },
15017
15039
  // Zen's live free MiMo, and the second free row in a menu that big-pickle
15018
15040
  // was alone in since `mimo-v2-pro-free` lost its model.
15019
15041
  mimo: {
@@ -15076,7 +15098,11 @@ var providers = {
15076
15098
  "glm-flash": {
15077
15099
  displayName: "GLM Flash",
15078
15100
  resolve: "opencode-go/glm-5.3-flash",
15079
- effort: ["low", "high", "max"],
15101
+ // the Go endpoint intermittently rejects reasoning_effort ("native reasoning
15102
+ // control reasoning_effort is not allowed") mid-run, despite models.dev
15103
+ // advertising a ladder — so this route sends none.
15104
+ effort: [],
15105
+ openRouterEffort: ["low", "high", "max"],
15080
15106
  openRouterResolve: "openrouter/z-ai/glm-5.3-flash"
15081
15107
  },
15082
15108
  // legacy alias — the slug pinned a version instead of a brand tier and
@@ -15465,6 +15491,17 @@ var providers = {
15465
15491
  resolve: "openrouter/meta/muse-spark-1.3-contributor",
15466
15492
  effort: ["minimal", "low", "medium", "high", "xhigh", "max"],
15467
15493
  openRouterResolve: "openrouter/meta/muse-spark-1.3-contributor"
15494
+ },
15495
+ // Mistral's flagship (public preview, 2026-10-06), on the routers only:
15496
+ // the catalog carries no direct Mistral provider. out of the pickers
15497
+ // until a published `pullfrog` resolves the slug; see "A new alias
15498
+ // reaches the picker at merge".
15499
+ "mistral-large": {
15500
+ displayName: "Mistral Large",
15501
+ resolve: "openrouter/mistralai/mistral-large-4-0",
15502
+ effort: ["none", "high"],
15503
+ openRouterResolve: "openrouter/mistralai/mistral-large-4-0",
15504
+ hidden: true
15468
15505
  }
15469
15506
  }
15470
15507
  }),
@@ -15532,7 +15569,7 @@ var providers = {
15532
15569
  "deepseek-pro": {
15533
15570
  displayName: "DeepSeek Pro",
15534
15571
  resolve: "vercel/deepseek/deepseek-v4-pro-0813",
15535
- effort: ["none", "high", "max"]
15572
+ effort: ["none", "low", "high", "max"]
15536
15573
  },
15537
15574
  // the gateway's bare `deepseek-v4-flash` is the April preview — the same
15538
15575
  // fork trap as OpenRouter, with `deepseek-v4-flash-0731` beside it — so
@@ -15540,7 +15577,7 @@ var providers = {
15540
15577
  "deepseek-flash": {
15541
15578
  displayName: "DeepSeek Flash",
15542
15579
  resolve: "vercel/deepseek/deepseek-v4.1-flash",
15543
- effort: ["none", "high", "max"]
15580
+ effort: ["none", "low", "high", "max"]
15544
15581
  },
15545
15582
  glm: {
15546
15583
  displayName: "GLM",
@@ -15556,6 +15593,13 @@ var providers = {
15556
15593
  displayName: "Muse Spark",
15557
15594
  resolve: "vercel/meta/muse-spark-1.3",
15558
15595
  effort: ["minimal", "low", "medium", "high", "xhigh"]
15596
+ },
15597
+ // hidden until a published `pullfrog` resolves it, as on OpenRouter.
15598
+ "mistral-large": {
15599
+ displayName: "Mistral Large",
15600
+ resolve: "vercel/mistral/mistral-large-4",
15601
+ effort: ["none", "low", "medium", "high"],
15602
+ hidden: true
15559
15603
  }
15560
15604
  }
15561
15605
  })
@@ -15611,8 +15655,8 @@ function routerProxyOptOut(ctx) {
15611
15655
  if (ctx.slug === null) return false;
15612
15656
  return resolveDisplayAlias(ctx.slug)?.isFree === true || modelHasStoredAuth({ model: ctx.slug, secretNames: ctx.secretNames });
15613
15657
  }
15614
- function pickUsesOpenRouter(params) {
15615
- return params.proxied && !routerProxyOptOut(params);
15658
+ function resolvePickModel(params) {
15659
+ return params.proxied && !routerProxyOptOut(params) ? resolveOpenRouterModel(params.slug) : resolveCliModel(params.slug);
15616
15660
  }
15617
15661
  var modelAliases = Object.entries(providers).flatMap(
15618
15662
  ([providerKey, config2]) => Object.entries(config2.models).map(([modelId, def]) => ({
@@ -15795,13 +15839,13 @@ for (const [name, ladder] of Object.entries({ ...PROVIDER_LADDERS, router: ROUTE
15795
15839
  if (terminal.size < 2) throw new Error(`${name} ladder runs every tier on ${[...terminal][0]}`);
15796
15840
  }
15797
15841
  var OSS_MODEL_ALLOWLIST = [
15798
- "deepseek/deepseek-flash",
15799
- "openai/gpt-luna",
15800
- "openrouter/minimax-m2.5",
15801
15842
  // opt-in: 0.86x the old default on the measured mix, and Meta trains on the
15802
15843
  // traffic — fine for public code on headless runs, the human decided
15803
15844
  // 2026-09-11. unmeasured on a real run; not the default until it is.
15804
- "meta/muse-spark-contributor"
15845
+ "meta/muse-spark-contributor",
15846
+ "deepseek/deepseek-flash",
15847
+ "openai/gpt-luna",
15848
+ "openrouter/minimax-m2.5"
15805
15849
  ];
15806
15850
  var OSS_RECOMMENDED_MODEL = AUTO_TIER_TARGET[AUTO_EFFICIENT];
15807
15851
  function ossFundedModelNames() {
@@ -15848,17 +15892,16 @@ var REVIEWER_AGENT_NAME = "reviewfrog";
15848
15892
  // modes.ts
15849
15893
  var PR_SUMMARY_FORMAT = `### Default format
15850
15894
 
15851
- The body has at most three parts, in this order:
15895
+ The body has at most two parts, in this order:
15852
15896
 
15853
15897
  1. **Reviewed changes preamble** \u2014 a bolded \`**Reviewed changes**\` lead-in with one sentence on what was reviewed in this run (for \`IncrementalReview\`: what changed since the prior pullfrog review), then a bullet list of the substantive changes \u2014 short bolded title, one sentence each. A reviewer should understand the full reviewed scope from this list alone. Close the preamble with the metadata comment below.
15854
15898
  2. **Cross-cutting issue sections** (zero or more) \u2014 one \`### {emoji} {what's wrong, not what to do}\` heading per concern.
15855
- 3. **\`### \u2139\uFE0F Nitpicks\`** at the very bottom, if any \u2014 a flat bullet list, no technical-details block. Only for what has no runtime effect: a stale or wrong comment or doc, a misleading name, a typo, dead code, a missing test. Never formatting or whitespace.
15856
15899
 
15857
15900
  **Inline vs. body.** Concerns that anchor to a specific line go inline (the \`comments\` parameter), even when their implications are broad. Body \`### \` sections are reserved for concerns that have **no line to anchor to** \u2014 *absence* (something the diff should have done but didn't), *sequencing* (rollout / deletion / migration order), *design decisions only the human can make*, or *scope questions the diff raises but doesn't address*. With no non-anchorable concerns, the body is just the preamble + metadata.
15858
15901
 
15859
15902
  **Severity emoji** on every \`### \` heading, and nowhere else: \u{1F6A8} critical (blocks merge \u2014 data loss, security, broken core flow) \xB7 \u26A0\uFE0F important (must address before merging) \xB7 \u2139\uFE0F informational (mergeable as-is).
15860
15903
 
15861
- **Behavior decides placement, not fix size.** A finding that changes what the code does \u2014 a wrong value or message, a crash, a race, a leak, a missed cleanup, a slow path \u2014 gets its own inline comment (or a \`### \` section if no line anchors it) at its real severity, however small the fix. Never put it in Nitpicks: before submitting, re-read each Nitpicks bullet and move any that fails this test inline.
15904
+ **Behavior decides placement, not fix size.** A finding that changes what the code does \u2014 a wrong value or message, a crash, a race, a leak, a missed cleanup, a slow path \u2014 gets its own inline comment (or a \`### \` section if no line anchors it) at its real severity, however small the fix.
15862
15905
 
15863
15906
  **Blank line between every block-level element.** GitHub's markdown parser requires one before and after HTML tags (\`<details>\`, \`<summary>\`, \`<sub>\`, \`<br/>\`) \u2014 without it GitHub treats what follows as a continuation of the HTML block and renders your markdown as literal text. This is a parser quirk, not a style preference, and it permanently breaks the posted review.
15864
15907
 
@@ -16007,8 +16050,6 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
16007
16050
 
16008
16051
  3. **triage**: orient yourself on the PR \u2014 identify *what kind of thing this is* (domain it touches, seams it crosses, external contracts it depends on, user-facing surfaces it changes). pull as much context as you need to render a confident, well-grounded review: read related files, grep for callers of changed symbols, check tests that exercise the touched paths, fetch related GitHub state. **you are the synthesizer** \u2014 never delegate understanding to subagents.
16009
16052
 
16010
- when the diff adds or changes a test, check that it can actually fail: a test that would still pass with the bug present is theatre, not coverage. the usual tell is a loose assertion standing where an exact one belongs \u2014 \`>=\` or a truthiness check over an expected value, or a snapshot that absorbs whatever it is handed. read the assertion against the behavior it claims to pin, not against whether it currently passes.
16011
-
16012
16053
  skip the deeper pass and submit a \`No new issues found.\` review per step 7 only when the diff has **no behavioral surface at all** \u2014 doc typos, whitespace/formatting, lockfile or generated-code regeneration, a mechanical rename whose only effect is import-path updates. line count is not the signal: a one-line change to auth, money, SQL, a comparison operator, a redirect, or a config default is not trivial.
16013
16054
 
16014
16055
  4. **specialist decision**: after reading the complete diff, name the questions you still cannot answer confidently yourself, and dispatch one \`${REVIEWER_AGENT_NAME}\` specialist per question. a question qualifies only when a specialist could return evidence that **changes your disposition** on the PR \u2014 generic requests for another look, extra confidence, or polish do not. most reviews need zero or one; some need several.
@@ -16032,8 +16073,6 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
16032
16073
 
16033
16074
  6. **aggregate & draft**: when specialist results land, merge findings; de-dup overlaps (two specialists catching the same issue = higher-confidence signal); trace each finding yourself before accepting it. drop praise, style preferences, speculative/unverified claims, findings about pre-existing code unrelated to the PR (heuristic: if the finding's root cause lives in lines this PR added or modified, it's in scope; otherwise drop unless the PR plausibly introduced or amplified the regression), and anything not actionable. also drop **bloat-shaped findings** \u2014 proposed fixes that would add defensive checks for cases that can't happen, abstractions used once, comments restating obvious code, tests asserting tautologies, or "just-in-case" guards. subagents are fallible and bias toward recommending changes; the bar for an actionable inline comment is sound + correct + elegant. recommending a change that improves only one of the three (or worse, degrades elegance to nominally improve correctness) makes the codebase worse, not better.
16034
16075
 
16035
- **Hunt for non-anchored concerns before drafting.** After collecting your anchored findings, deliberately scan for concerns that have no specific line to point at \u2014 typically: deletion / cleanup plans for code the diff replaces or shadows; rollout sequencing (what happens to in-flight state during deploy / revert?); scope questions that only the human can answer (e.g. is the legacy path going away or is this a long-term dual track?); architectural risks the diff opens up that aren't a single-line bug.
16036
-
16037
16076
  for surviving findings, draft inline comments with NEW line numbers from the diff \u2014 attach a \`<details>Technical details</details>\` block to any inline comment whose fix is non-trivial or has cross-file implications (see Inline technical details in the format below). every comment must be actionable, 2-3 sentences max in the visible part, one per root cause \u2014 name its other sites in that comment rather than repeating it. use GitHub permalink format for code references. for impact-analysis findings (stale references after rename/remove), report them in the review body ordered by severity (runtime breakage > incorrect docs > stale comments) rather than as inline comments unless they're anchored to a specific line.
16038
16077
 
16039
16078
  7. **submit**: ALWAYS submit exactly one review via \`${t("create_pull_request_review")}\`. Do NOT call \`report_progress\` \u2014 the review is the final record and the progress comment will be cleaned up automatically.
@@ -16055,7 +16094,7 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
16055
16094
  \`approved: false\`. Body opens with \`> [!CAUTION]\\n> This PR introduces ...\`, followed by the PR summary. Include all inline comments via \`comments\`.
16056
16095
  - **must-address non-critical findings** (real consequences if shipped \u2014 incorrect behavior in non-critical paths, missing validation on user input, regressions the author should fix before merge):
16057
16096
  \`approved: false\`. Body opens with \`> [!IMPORTANT]\\n> ...\`, followed by the PR summary. Reserve this tier for findings with concrete fallout \u2014 do NOT use \`[!IMPORTANT]\` for nits, style preferences, or "consider also" suggestions. Include all inline comments via \`comments\`.
16058
- - **minor suggestions only** (low-stakes findings, or Nitpicks only):
16097
+ - **minor suggestions only** (low-stakes findings):
16059
16098
  \`approved: false\`. Body opens with \`> \u2139\uFE0F No critical issues \u2014 minor suggestions inline.\\n\\n\` followed by the PR summary. Include all inline comments via \`comments\`. Vary the wording after the emoji to fit the review (e.g. "Minor suggestions only.", "Two rough edges worth a look."), but always keep the \u2139\uFE0F prefix and keep it short.
16060
16099
  - **informational observations** (mergeable as-is, nothing actionable \u2014 e.g. prior feedback addressed cleanly, calling out something noteworthy without recommending a change):
16061
16100
  \`approved: true\`. Body opens with \`> \u2705 No new issues found.\\n\\n\` followed by the PR summary. Do NOT include inline \`comments\` \u2014 the \u2705 signals "no action needed", which contradicts an actionable anchor; if a point is concrete enough to anchor to a line, downgrade the whole review to "minor suggestions only" (\`approved: false\`) instead.
@@ -16098,8 +16137,6 @@ ${PR_SUMMARY_FORMAT}`
16098
16137
 
16099
16138
  5. **triage**: orient on the *incremental* changes \u2014 domain, seams, external contracts, user-facing surfaces. pull as much context as you need to render a confident review: read related files, grep for callers of changed symbols, check tests that exercise the touched paths. **you are the synthesizer.**
16100
16139
 
16101
- a test added or changed in this delta must be able to fail \u2014 one that would still pass with the bug present is theatre, not coverage. the tell is a loose assertion where an exact one belongs (\`>=\` or a truthiness check over an expected value, a snapshot that absorbs whatever it is handed).
16102
-
16103
16140
  skip the deeper pass and jump to step 10's non-substantive path (do NOT submit a review) only when the incremental changes have **no behavioral surface at all** \u2014 formatting, comment tweaks, import reordering, lockfile regen, a mechanical rename of import paths. line count is not the signal: a one-line change to auth, money, SQL, a comparison operator, a redirect, or a config default is not trivial.
16104
16141
 
16105
16142
  6. **specialist decision**: after covering the incremental and full diffs, name the questions about the new changes that you still cannot answer confidently yourself, and dispatch one \`${REVIEWER_AGENT_NAME}\` specialist per question. a question qualifies only when a specialist could return evidence that **changes your disposition** on the PR \u2014 generic requests for another look, extra confidence, or polish do not. most incremental reviews need zero or one, especially thread-reply re-reviews; some need several.
@@ -16123,11 +16160,9 @@ ${PR_SUMMARY_FORMAT}`
16123
16160
 
16124
16161
  8. **aggregate, draft, self-critique**: merge findings (yours + output from every specialist you dispatched); de-dup overlaps; trace each finding yourself. drop praise, style preferences, speculative/unverified claims, findings about pre-existing code unrelated to the new commits, anything not actionable, and anything that re-states prior review feedback (heuristic: if the finding's root cause lives in lines the *new commits* added or modified, it's in scope; otherwise drop). also drop **bloat-shaped findings** \u2014 proposed fixes that would add defensive checks for cases that can't happen, abstractions used once, comments restating obvious code, tests asserting tautologies, or "just-in-case" guards. subagents are fallible and bias toward recommending changes; the bar for an actionable inline comment is sound + correct + elegant. recommending a change that improves only one of the three (or degrades elegance to nominally improve correctness) makes the codebase worse, not better. To compute "lines the new commits added or modified": if \`incrementalDiffPath\` from step 2 is present, use it directly. Otherwise, take the prior Pullfrog review's \`commit_id\` (returned alongside each entry from \`${t("list_pull_request_reviews")}\` in step 4) and run \`git diff <prior-review-sha>..HEAD\` to isolate the lines added since that review.
16125
16162
 
16126
- **Hunt for non-anchored concerns before drafting.** After collecting your anchored findings, deliberately scan for concerns that have no specific line to point at \u2014 typically: deletion / cleanup plans for code the new commits replace or shadow; rollout sequencing (what happens to in-flight state during deploy / revert?); scope questions that only the human can answer (e.g. is the legacy path going away or is this a long-term dual track?); architectural risks the new commits open up that aren't a single-line bug.
16127
-
16128
16163
  draft inline comments with NEW line numbers from the full PR diff \u2014 attach a \`<details>Technical details</details>\` block to any inline comment whose fix is non-trivial or has cross-file implications (see Inline technical details in the format below). every comment must be actionable, 2-3 sentences max in the visible part, one per root cause \u2014 name its other sites in that comment rather than repeating it.
16129
16164
 
16130
- 9. **build the review body**: use the same default format as Review mode (preamble + optional cross-cutting \`### \` sections + optional \`### \u2139\uFE0F Nitpicks\`) \u2014 scoped to the **incremental delta**, not the full PR. The "Reviewed changes" bullets describe what changed since the prior pullfrog review (each bullet starts with a past-tense verb, e.g. \`- Extracted shared CLI runtime into a single module\`). Do NOT include a separate "Prior review feedback" checklist \u2014 that's tracked in the rolling PR summary snapshot for the next agent run, and surfacing it in the user-facing body is noise (changes that addressed prior feedback are already covered by the Reviewed-changes bullets). In some cases you may receive a complete diff for the whole PR instead of an incremental one; when this happens, determine what changed since Pullfrog's most recent review yourself before drafting bullets.
16165
+ 9. **build the review body**: use the same default format as Review mode (preamble + optional cross-cutting \`### \` sections) \u2014 scoped to the **incremental delta**, not the full PR. The "Reviewed changes" bullets describe what changed since the prior pullfrog review (each bullet starts with a past-tense verb, e.g. \`- Extracted shared CLI runtime into a single module\`). Do NOT include a separate "Prior review feedback" checklist \u2014 that's tracked in the rolling PR summary snapshot for the next agent run, and surfacing it in the user-facing body is noise (changes that addressed prior feedback are already covered by the Reviewed-changes bullets). In some cases you may receive a complete diff for the whole PR instead of an incremental one; when this happens, determine what changed since Pullfrog's most recent review yourself before drafting bullets.
16131
16166
 
16132
16167
  10. Submit \u2014 every run must end with EXACTLY ONE of \`${t("create_pull_request_review")}\` (substantive review) or \`${t("report_progress")}\` (no-review acknowledgement). do NOT call \`create_issue_comment\` for review output.
16133
16168
 
@@ -16135,10 +16170,10 @@ ${PR_SUMMARY_FORMAT}`
16135
16170
 
16136
16171
  Follow these rules:
16137
16172
  - note: the first create_pull_request_review submission may error with a one-time diff-coverage nudge listing unread TOC regions. retry the same call to proceed \u2014 optionally after reading the listed ranges. the pre-flight will not block again this session.
16138
- - IF NO NEW ISSUES, NON-SUBSTANTIVE CHANGES ONLY (trivial formatting, import reordering, comment tweaks): do NOT submit a review. Instead call \`${t("report_progress")}\` with a 1-2 sentence note explaining no review was warranted (e.g. "No new issues. Changes since last review are formatting-only."). this leaves a visible signal that the run completed.
16173
+ - IF NO NEW ISSUES, NON-SUBSTANTIVE CHANGES ONLY (trivial formatting, import reordering, comment tweaks): do NOT submit a review. Instead call \`${t("report_progress")}\` with \`no_review_needed: true\` and a 1-2 sentence note explaining no review was warranted (e.g. "No new issues. Changes since last review are formatting-only."). this leaves a visible signal that the run completed.
16139
16174
  - ELSE IF NEW CRITICAL ISSUES (blocks merge \u2014 bugs, security, data loss, broken core flows): call \`${t("create_pull_request_review")}\` with \`approved: false\`, all comments, and the review body. body opens with \`> [!CAUTION]\\n> This PR introduces ...\`, followed by the PR summary using the default format below.
16140
16175
  - ELSE IF NEW MUST-ADDRESS NON-CRITICAL FINDINGS (real consequences if shipped \u2014 incorrect behavior, missing validation, regressions the author should fix before merge): call \`${t("create_pull_request_review")}\` with \`approved: false\`, all comments, and the review body. body opens with \`> [!IMPORTANT]\\n> ...\`, followed by the PR summary using the default format below. Do NOT use this tier for nits, style preferences, or "consider also" suggestions.
16141
- - ELSE IF NEW MINOR SUGGESTIONS ONLY (low-stakes findings, or Nitpicks only): call \`${t("create_pull_request_review")}\` with \`approved: false\`, all comments, and the review body. body opens with \`> \u2139\uFE0F No critical issues \u2014 minor suggestions inline.\\n\\n\` (vary the wording after \u2139\uFE0F to fit the review), followed by the PR summary using the default format below.
16176
+ - ELSE IF NEW MINOR SUGGESTIONS ONLY (low-stakes findings): call \`${t("create_pull_request_review")}\` with \`approved: false\`, all comments, and the review body. body opens with \`> \u2139\uFE0F No critical issues \u2014 minor suggestions inline.\\n\\n\` (vary the wording after \u2139\uFE0F to fit the review), followed by the PR summary using the default format below.
16142
16177
  - ELSE IF INFORMATIONAL OBSERVATIONS (mergeable as-is, but worth surfacing \u2014 e.g. prior feedback addressed cleanly, or a noteworthy positive observation): call \`${t("create_pull_request_review")}\` with \`approved: true\`, NO inline comments, and the review body. body opens with \`> \u2705 No new issues found.\\n\\n\` (or similar friendly green opener), followed by the PR summary using the default format below. If a point is concrete enough to anchor to a line, downgrade the whole review to "minor suggestions only" (\`approved: false\`) instead \u2014 the \u2705 signals "no action needed", which contradicts an actionable anchor.
16143
16178
  - ELSE IF NO NEW ISSUES, SUBSTANTIVE CHANGES (new functionality, behavior changes, or fixes to prior review feedback): call \`${t("create_pull_request_review")}\` to create a PR review. If all previous reviews have been properly addressed and no new issues were discovered, set \`approved: true\`. body opens with \`> \u2705 No new issues found.\\n\\n\`, followed by the PR summary using the default format below.
16144
16179
 
@@ -16847,7 +16882,13 @@ var PROVIDER_ERROR_PATTERNS = [
16847
16882
  // …/settings/credits`) matched neither this nor `requires more credits`, so
16848
16883
  // the one run in that episode that was a genuinely empty wallet rendered with
16849
16884
  // no CTA at all while runs 1-6 rendered correctly.
16850
- { regex: /Insufficient (?:balance|credits)/i, label: PROVIDER_BILLING_EXHAUSTED_LABEL },
16885
+ // `account funds` is Zen's (#1474).
16886
+ {
16887
+ regex: /Insufficient (?:balance|credits|account funds)/i,
16888
+ label: PROVIDER_BILLING_EXHAUSTED_LABEL
16889
+ },
16890
+ // OpenAI's `You have no credits remaining` (#1474).
16891
+ { regex: /no credits remaining/i, label: PROVIDER_BILLING_EXHAUSTED_LABEL },
16851
16892
  { regex: /credit balance is too low/i, label: PROVIDER_BILLING_EXHAUSTED_LABEL },
16852
16893
  // "spending cap" (Gemini) and "spending limit" (xAI, #1076) are the same
16853
16894
  // condition in two house styles — a configured ceiling was reached.
@@ -17019,7 +17060,12 @@ function terminalOutput(params) {
17019
17060
  const review = params.reviewUrl ? `
17020
17061
 
17021
17062
  [View the review Pullfrog posted \u2192](${params.reviewUrl})` : "";
17022
- if (params.conclusion === "success") return { title: base.title, summary: base.summary + review };
17063
+ if (params.conclusion === "success") {
17064
+ const note = params.note ? `
17065
+
17066
+ ${params.note}` : "";
17067
+ return { title: base.title, summary: base.summary + review + note };
17068
+ }
17023
17069
  return {
17024
17070
  title: base.title,
17025
17071
  summary: base.summary + logsLine(params.detailsUrl) + review + disableCheckLine(params.owner, params.repo)
@@ -17061,7 +17107,8 @@ async function finalizeRunStatusCheck(params) {
17061
17107
  owner: params.owner,
17062
17108
  repo: params.repo,
17063
17109
  detailsUrl: params.detailsUrl,
17064
- reviewUrl: params.reviewUrl
17110
+ reviewUrl: params.reviewUrl,
17111
+ note: params.note
17065
17112
  })
17066
17113
  };
17067
17114
  if (params.detailsUrl) updateParams.details_url = params.detailsUrl;
@@ -17204,7 +17251,13 @@ var codexUsageSchema = external_exports.object({
17204
17251
  limit_reached: external_exports.boolean(),
17205
17252
  primary_window: codexWindowSchema,
17206
17253
  secondary_window: codexWindowSchema
17207
- }).optional()
17254
+ }).optional(),
17255
+ credits: external_exports.object({
17256
+ has_credits: external_exports.boolean().optional(),
17257
+ unlimited: external_exports.boolean().optional(),
17258
+ overage_limit_reached: external_exports.boolean().optional()
17259
+ }).nullish(),
17260
+ spend_control: external_exports.object({ reached: external_exports.boolean().optional() }).nullish()
17208
17261
  });
17209
17262
  var unknown2 = { status: "unknown" };
17210
17263
  var SubscriptionCredentialError = class extends Error {
@@ -17272,9 +17325,12 @@ async function probeSubscription(input) {
17272
17325
  const body = await response.json().catch(() => null);
17273
17326
  if (!codex) return unknown2;
17274
17327
  const parsed = codexUsageSchema.safeParse(body);
17275
- const limit = parsed.success ? parsed.data.rate_limit : void 0;
17276
- if (!limit) return unknown2;
17328
+ if (!parsed.success || !parsed.data.rate_limit) return unknown2;
17329
+ const limit = parsed.data.rate_limit;
17277
17330
  if (limit.allowed && !limit.limit_reached) return { status: "usable" };
17331
+ const credits = parsed.data.credits;
17332
+ if ((credits?.has_credits || credits?.unlimited) && !credits.overage_limit_reached && !parsed.data.spend_control?.reached)
17333
+ return { status: "usable" };
17278
17334
  const windows = [limit.primary_window, limit.secondary_window];
17279
17335
  const full = windows.filter((window) => (window?.used_percent ?? 0) >= 100);
17280
17336
  const resets = (full.length ? full : windows).flatMap(
@@ -17322,15 +17378,24 @@ async function probeInference(input) {
17322
17378
  const error48 = external_exports.object({ error: external_exports.object({ code: external_exports.string() }) }).safeParse(await response.json().catch(() => null));
17323
17379
  return error48.success && error48.data.error.code === "insufficient_quota" ? { status: "exhausted", detail: "refused with 429: insufficient quota", resetAt: void 0 } : unknown2;
17324
17380
  }
17381
+ if (response?.status === 401 || response?.status === 403)
17382
+ return {
17383
+ status: "rejected",
17384
+ detail: `rejected with ${response.status}${await providerReason(response)}`
17385
+ };
17325
17386
  await response?.body?.cancel();
17326
17387
  if (!response) return unknown2;
17327
17388
  if (response.ok) return { status: "usable" };
17328
- if (response.status === 401 || response.status === 403)
17329
- return { status: "rejected", detail: `rejected with ${response.status}` };
17330
17389
  if (response.status === 402)
17331
17390
  return { status: "exhausted", detail: "refused with 402", resetAt: void 0 };
17332
17391
  return unknown2;
17333
17392
  }
17393
+ async function providerReason(response) {
17394
+ const body = external_exports.object({ error: external_exports.union([external_exports.string(), external_exports.object({ message: external_exports.string() })]) }).safeParse(await response.json().catch(() => null));
17395
+ if (!body.success) return "";
17396
+ const error48 = body.data.error;
17397
+ return `: ${(typeof error48 === "string" ? error48 : error48.message).slice(0, 300)}`;
17398
+ }
17334
17399
 
17335
17400
  // utils/time.ts
17336
17401
  var TIMEOUT_DISABLED = "none";
@@ -17420,7 +17485,6 @@ export {
17420
17485
  parseModel,
17421
17486
  parseTimeString,
17422
17487
  parseXaiAuthBody,
17423
- pickUsesOpenRouter,
17424
17488
  probeSubscription,
17425
17489
  providers,
17426
17490
  pullfrogMcpName,
@@ -17432,6 +17496,7 @@ export {
17432
17496
  resolveModelRung,
17433
17497
  resolveModelSlug,
17434
17498
  resolveOpenRouterModel,
17499
+ resolvePickModel,
17435
17500
  resolveRoutedModel,
17436
17501
  resolveRung,
17437
17502
  routerProxyOptOut,
@@ -61,6 +61,8 @@ export type PrData = {
61
61
  };
62
62
  type CheckoutPrBranchParams = GitContext & {
63
63
  beforeSha?: string | undefined;
64
+ /** aborts the in-flight fetch (SIGTERM, so git removes its own locks) when the tool deadline fires */
65
+ signal: AbortSignal;
64
66
  };
65
67
  /**
66
68
  * Shared helper to checkout a PR branch and configure fork remotes.
@@ -33,6 +33,7 @@ export declare function EditCommentTool(ctx: ToolContext): import("fastmcp").Too
33
33
  export declare const ReportProgress: import("arktype/internal/variants/object.ts").ObjectType<{
34
34
  body: string;
35
35
  target_plan_comment?: boolean;
36
+ no_review_needed?: boolean;
36
37
  }, {}>;
37
38
  /**
38
39
  * Report progress to a GitHub comment.
@@ -61,9 +62,11 @@ export declare function reportProgress(ctx: ToolContext, params: {
61
62
  export declare function ReportProgressTool(ctx: ToolContext): import("fastmcp").Tool<any, import("@standard-schema/spec").StandardSchemaV1<{
62
63
  body: string;
63
64
  target_plan_comment?: boolean;
65
+ no_review_needed?: boolean;
64
66
  }, {
65
67
  body: string;
66
68
  target_plan_comment?: boolean;
69
+ no_review_needed?: boolean;
67
70
  }>> & {
68
71
  mutates?: boolean;
69
72
  };
package/dist/models.d.ts CHANGED
@@ -189,7 +189,7 @@ export declare function modelHasStoredAuth(params: {
189
189
  * as-configured rather than clamping to the auto tier.
190
190
  *
191
191
  * one predicate for every reader — the console picker's `customLocked`, the
192
- * effort ladder's `resolveRunning` and `pickUsesOpenRouter`. they held two
192
+ * effort ladder's `resolveRunning` and `resolvePickModel`. they held two
193
193
  * copies and the free half was missing from both, which put the whole `opencode/*` free tier (`opencode/big-pickle`
194
194
  * is `preferred`, so the most-picked model there is) behind "add a payment
195
195
  * method to run it" on a card-less Router repo, against a server that ran it.
@@ -198,13 +198,13 @@ export declare function routerProxyOptOut(ctx: {
198
198
  slug: string | null;
199
199
  secretNames: string[];
200
200
  }): boolean;
201
- /** whether a pick runs as its OpenRouter target: a proxied account (Router, the OSS subsidy)
202
- * mints for every pick `routerProxyOptOut` does not exempt. */
203
- export declare function pickUsesOpenRouter(params: {
201
+ /** the specifier a pick runs as: its OpenRouter target on a proxied account (Router, the OSS
202
+ * subsidy), which mints for every pick `routerProxyOptOut` does not exempt, else its direct one. */
203
+ export declare function resolvePickModel(params: {
204
204
  slug: string;
205
205
  proxied: boolean;
206
206
  secretNames: string[];
207
- }): boolean;
207
+ }): string | undefined;
208
208
  export declare const modelAliases: ModelAlias[];
209
209
  /**
210
210
  * `repo.model` sentinels for the two managed auto tiers. stored verbatim in
@@ -106,6 +106,7 @@ export interface ToolState {
106
106
  sha: string | undefined;
107
107
  url?: string | undefined;
108
108
  };
109
+ noReviewNeededSha?: string | undefined;
109
110
  reviewReplies?: Map<number, {
110
111
  commentId: number;
111
112
  url: string | undefined;
@@ -147,10 +148,11 @@ export interface ToolState {
147
148
  todoTracker?: TodoTracker | undefined;
148
149
  agentDiagnostic?: AgentDiagnostic | undefined;
149
150
  /**
150
- * a `PATCH /api/workflow-run/:id` came back 404 — the reservation was never
151
- * claimed, which is permanent for the life of the run (the claim happens at
152
- * setup, strictly before any PATCH). latched so the remaining calls stop
153
- * firing: one CI run spent 28 doomed round trips on it. see #1153.
151
+ * a `PATCH /api/workflow-run/:id` came back 404 — Pullfrog neither claimed a
152
+ * reservation for the run nor recorded it as a custom run, which is permanent for
153
+ * the life of the run (both happen at setup, strictly before any PATCH). latched so
154
+ * the remaining calls stop firing: one CI run spent 28 doomed round trips on it.
155
+ * see #1153.
154
156
  */
155
157
  workflowRunUnclaimed?: boolean | undefined;
156
158
  }
@@ -1,3 +1,5 @@
1
+ import type { CredentialCandidate } from "./subscriptionCredentials.ts";
2
+ import type { ProbeVerdict } from "./subscriptionProbe.ts";
1
3
  /** shared sentence in the four `resolveSlug` routing-slug throws. */
2
4
  export declare const BYOK_SLUG_MARKER = "env var is required when the model is set to";
3
5
  /** shared sentence in the four `validate*Setup` throws. */
@@ -49,6 +51,12 @@ export declare const ROUTER_UNFUNDED_MARKER = "your Pullfrog Router balance is e
49
51
  * hand that agent the ability to suppress every other error body.
50
52
  */
51
53
  export declare const CREDENTIAL_REJECTED_MARKER = "was rejected by its provider";
54
+ /**
55
+ * marker for the credential pool's refusal: every configured credential for the
56
+ * model was rejected or exhausted. verbatim for the same reason as
57
+ * `CREDENTIAL_REJECTED_MARKER` — each bullet quotes the probe's verdict.
58
+ */
59
+ export declare const CREDENTIAL_POOL_REFUSED_MARKER = "No configured credential can serve this run";
52
60
  /**
53
61
  * Three ways to arrive at "the runner has no key", each with a different CTA:
54
62
  * we couldn't read the key you stored, your Router wallet ran dry and you have
@@ -184,6 +192,24 @@ export declare function buildRejectedCredentialError(params: {
184
192
  /** stored in Pullfrog (console / `pullfrog auth`) rather than a GitHub Actions secret. */
185
193
  inPullfrogStore: boolean;
186
194
  }): string;
195
+ export type RefusedCredential = {
196
+ name: string;
197
+ source: CredentialCandidate["source"] | "workflow";
198
+ verdict: Extract<ProbeVerdict, {
199
+ status: "rejected" | "exhausted";
200
+ }>;
201
+ };
202
+ /**
203
+ * The credential pool probed every credential configured for the model and
204
+ * none can serve it. One bullet per credential: the probe's own verdict, quoted,
205
+ * plus the remedy its kind and status call for — never inferred from the prose.
206
+ */
207
+ export declare function buildCredentialPoolRefusedError(params: {
208
+ model: string;
209
+ refused: RefusedCredential[];
210
+ owner: string;
211
+ name: string;
212
+ }): string;
187
213
  /**
188
214
  * Friendly Markdown summary for both the missing-key and invalid-key cases.
189
215
  * Used in the catch / result-failure paths in `main.ts` to overwrite the raw
@@ -6,6 +6,7 @@ type CreateChangeImpactParams = {
6
6
  pullNumber: number;
7
7
  baseSha: string;
8
8
  headSha: string;
9
+ signal: AbortSignal;
9
10
  };
10
11
  type ChangeImpactResult = {
11
12
  path: string;
@@ -1,3 +1,5 @@
1
+ /** the data dir a subscription was installed into this run, which opencode's XDG_DATA_HOME must name. */
2
+ export declare function getSubscriptionDataHome(): string | undefined;
1
3
  export declare function clearInstalledSubscription(name: "CODEX_AUTH_JSON" | "GROK_AUTH_JSON"): void;
2
4
  /** sandbox-hidden home for pullfrog-managed on-disk secrets in CI. bash via
3
5
  * MCP shell tmpfs-overlays this path; opencode's internal auth module
@@ -11,8 +13,6 @@ export interface InstalledCodexAuth {
11
13
  /** absolute path of the auth.json we wrote — caller passes this to the
12
14
  * post-hook via core.saveState for refresh-detection later. */
13
15
  authPath: string;
14
- /** value to set as XDG_DATA_HOME for the OpenCode subprocess. */
15
- xdgDataHome: string;
16
16
  /** refresh_token from the env at materialization time. post-hook
17
17
  * compares against the on-disk file after the run to detect whether
18
18
  * OpenCode refreshed during the session (only happens on long runs
@@ -31,15 +31,13 @@ export interface InstalledCodexAuth {
31
31
  * caller treats null as "no codex auth, fall through to API key flow".
32
32
  *
33
33
  * The env value is server-side guaranteed fresh by `maybeRotateCodexSecret`
34
- * in the run-context endpoint. We parse + write it here and set
35
- * `process.env.XDG_DATA_HOME` so every opencode subprocess discovers the
34
+ * in the run-context endpoint. We parse + write it here and record the dir
35
+ * (`getSubscriptionDataHome`) so every opencode subprocess discovers the
36
36
  * auth.json; no refresh, no DB interaction. */
37
37
  export declare function installCodexAuth(): InstalledCodexAuth | null;
38
38
  export interface InstalledXaiAuth {
39
39
  /** absolute path of the auth.json we wrote — the post-hook diffs it. */
40
40
  authPath: string;
41
- /** value to set as XDG_DATA_HOME for the OpenCode subprocess. */
42
- xdgDataHome: string;
43
41
  /** refresh_token at materialization time. opencode's XaiAuthPlugin rotates
44
42
  * in-process on a long run, so the post-hook compares against this to decide
45
43
  * whether anything needs writing back. */
@@ -22,6 +22,7 @@ type GitAuthOptions = {
22
22
  * when present, an auth-class failure is retried ONCE with a fresh token.
23
23
  */
24
24
  refreshGitToken?: ((stale: string) => Promise<string>) | undefined;
25
+ signal?: AbortSignal | undefined;
25
26
  };
26
27
  /**
27
28
  * installation-token 401s on git. GitHub intermittently mints a token its git
@@ -75,7 +75,7 @@ export declare function fetchIdTokenFromStash(creds: OidcCredentials): Promise<s
75
75
  * own retry still re-mints. Mid-run refreshes take the stashed-credentials path
76
76
  * (`opts.oidc`) and deliberately bypass this.
77
77
  */
78
- export declare const mintIdToken: yes.OpFunction<() => Promise<string>>;
78
+ export declare const mintIdToken: (() => Promise<string>) & yes.CacheMethods<undefined, undefined>;
79
79
  /**
80
80
  * ensure a GitHub token is available in the environment.
81
81
  *
@@ -6,6 +6,8 @@
6
6
  * post-hook's stdlib-only import graph and bring back #815. Keep the two in
7
7
  * sync by hand; `entryPost.stdlibOnly.test.ts` guards the reason they differ. */
8
8
  export declare const OAUTH_WRITEBACK_STATE = "oauth_writeback";
9
+ /** GHA state key for the run's subscription data dir (codexHome.ts); `entryPost.ts` spells it literally too. */
10
+ export declare const SUBSCRIPTION_DATA_DIR_STATE = "subscription_data_dir";
9
11
  /**
10
12
  * Persist any OAuth refresh chain the run rotated, back into Pullfrog's own
11
13
  * secret store.
@@ -22,6 +24,9 @@ export declare const OAUTH_WRITEBACK_STATE = "oauth_writeback";
22
24
  * to write them back from inside a job — so a token stashed there silently
23
25
  * goes stale on the first refresh and the next run fails. See wiki/codex-auth.md.
24
26
  *
27
+ * Then removes the run's subscription data dir (codexHome.ts), which holds the
28
+ * auth.json diffed here.
29
+ *
25
30
  * Best-effort throughout: a missed write-back costs the user one
26
31
  * `pullfrog auth codex` re-run, while a throw here would fail a workflow whose
27
32
  * agent already succeeded.