pullfrog 0.1.75 → 0.1.77

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.
@@ -42,6 +42,26 @@ interface AzureProviderEntry {
42
42
  * guaranteed present by `validateOpenAICompatibleSetup`.
43
43
  */
44
44
  export declare function openAICompatibleProvider(model: string | undefined): Record<string, OpenAICompatibleProviderEntry>;
45
+ /**
46
+ * Customer-gateway block for OPENCODE_CONFIG_CONTENT. opencode resolves a
47
+ * provider's endpoint as `options.baseURL ?? model.api.url` (`getSDK`), and
48
+ * models.dev declares no base-URL env var for any provider we catalog — so a
49
+ * config override is the ONLY lever that re-points one. Unlike
50
+ * `openAICompatibleProvider` this declares no `npm` and no models: the native
51
+ * provider and its catalog metadata (including token limits) stay exactly as
52
+ * they are, and only the host changes. `{}` when unset so the caller can spread
53
+ * it unconditionally.
54
+ *
55
+ * `anthropic/*` normally never reaches here — `resolveAgent` routes it to
56
+ * claude-code, which reads ANTHROPIC_BASE_URL natively — but keying off the
57
+ * resolved specifier rather than the harness means it still holds on the paths
58
+ * that do send a Claude model through opencode (`PULLFROG_AGENT=opencode`).
59
+ */
60
+ export declare function providerGatewayOverride(model: string | undefined): Record<string, {
61
+ options: {
62
+ baseURL: string;
63
+ };
64
+ }>;
45
65
  /**
46
66
  * Azure model block for OPENCODE_CONFIG_CONTENT. Unlike openai-compatible this
47
67
  * does NOT declare a provider — opencode has a native `azure` one and the env
package/dist/cli.mjs CHANGED
@@ -123000,7 +123000,7 @@ var import_picocolors = __toESM(require_picocolors(), 1);
123000
123000
  // package.json
123001
123001
  var package_default = {
123002
123002
  name: "pullfrog",
123003
- version: "0.1.75",
123003
+ version: "0.1.77",
123004
123004
  cliContractVersion: "1",
123005
123005
  type: "module",
123006
123006
  bin: {
@@ -124108,6 +124108,20 @@ var repoConfigFields = [
124108
124108
  nullable: true,
124109
124109
  description: "additional instructions for planning"
124110
124110
  },
124111
+ {
124112
+ key: "prompts.address-reviews",
124113
+ field: "AddressReviews",
124114
+ kind: "prompt",
124115
+ nullable: true,
124116
+ description: "additional instructions for addressing review feedback"
124117
+ },
124118
+ {
124119
+ key: "prompts.fix-ci",
124120
+ field: "Fix",
124121
+ kind: "prompt",
124122
+ nullable: true,
124123
+ description: "additional instructions for fixing CI failures"
124124
+ },
124111
124125
  { key: "mention.enabled", field: "mention", kind: "toggle", description: "respond to mentions" },
124112
124126
  {
124113
124127
  key: "mention.instructions",
@@ -126058,6 +126072,71 @@ var AUTO_TIER_TARGET = {
126058
126072
  function isAutoTier(slug2) {
126059
126073
  return slug2 === AUTO_EFFICIENT || slug2 === AUTO_INTELLIGENT;
126060
126074
  }
126075
+ var PROVIDER_LADDERS = {
126076
+ anthropic: {
126077
+ minimal: "anthropic/claude-haiku",
126078
+ light: "anthropic/claude-sonnet",
126079
+ standard: "anthropic/claude-opus",
126080
+ deep: "anthropic/claude-opus"
126081
+ },
126082
+ openai: {
126083
+ minimal: "openai/gpt-luna",
126084
+ light: "openai/gpt-terra",
126085
+ standard: "openai/gpt-sol",
126086
+ deep: "openai/gpt-astra"
126087
+ },
126088
+ google: {
126089
+ minimal: "google/gemini-flash",
126090
+ light: "google/gemini-flash",
126091
+ standard: "google/gemini-pro",
126092
+ deep: "google/gemini-pro"
126093
+ },
126094
+ deepseek: {
126095
+ minimal: "deepseek/deepseek-flash",
126096
+ light: "deepseek/deepseek-flash",
126097
+ standard: "deepseek/deepseek-pro",
126098
+ deep: "deepseek/deepseek-pro"
126099
+ },
126100
+ xai: {
126101
+ minimal: "xai/grok-fast",
126102
+ light: "xai/grok-fast",
126103
+ standard: "xai/grok",
126104
+ deep: "xai/grok"
126105
+ },
126106
+ moonshotai: {
126107
+ minimal: "moonshotai/kimi-k2",
126108
+ light: "moonshotai/kimi-k2",
126109
+ standard: "moonshotai/kimi-k3",
126110
+ deep: "moonshotai/kimi-k3"
126111
+ },
126112
+ openrouter: {
126113
+ minimal: "openrouter/deepseek-flash",
126114
+ light: "openrouter/gpt-luna",
126115
+ standard: "openrouter/gpt-sol",
126116
+ deep: "openrouter/claude-opus"
126117
+ },
126118
+ opencode: {
126119
+ minimal: "opencode/gpt-luna",
126120
+ light: "opencode/gpt-luna",
126121
+ standard: "opencode/gpt-sol",
126122
+ deep: "opencode/claude-opus"
126123
+ }
126124
+ };
126125
+ var ROUTER_LADDER = {
126126
+ minimal: "deepseek/deepseek-flash",
126127
+ light: "openai/gpt-luna",
126128
+ standard: "openai/gpt-sol",
126129
+ deep: "anthropic/claude-opus"
126130
+ };
126131
+ var AUTO_PREFIX = "auto/";
126132
+ function autoLadder(slug2) {
126133
+ if (!slug2 || !slug2.startsWith(AUTO_PREFIX) || isAutoTier(slug2)) return void 0;
126134
+ const target = slug2.slice(AUTO_PREFIX.length);
126135
+ return target === "router" ? ROUTER_LADDER : PROVIDER_LADDERS[target];
126136
+ }
126137
+ var ROUTED_PROVIDERS = Object.keys(
126138
+ PROVIDER_LADDERS
126139
+ ).map((key) => ({ key, displayName: providers[key].displayName }));
126061
126140
  function isCardGatedModel(slug2) {
126062
126141
  return resolveOpenRouterModel(slug2) !== void 0;
126063
126142
  }
@@ -126066,7 +126145,7 @@ function resolveModelSlug(slug2) {
126066
126145
  }
126067
126146
  var MAX_FALLBACK_DEPTH = 10;
126068
126147
  function resolveDisplayAlias(slug2) {
126069
- let current = isAutoTier(slug2) ? AUTO_TIER_TARGET[slug2] : slug2;
126148
+ let current = isAutoTier(slug2) ? AUTO_TIER_TARGET[slug2] : autoLadder(slug2)?.standard ?? slug2;
126070
126149
  const visited = /* @__PURE__ */ new Set();
126071
126150
  for (let i2 = 0; i2 < MAX_FALLBACK_DEPTH; i2++) {
126072
126151
  if (visited.has(current)) return void 0;
@@ -126101,6 +126180,11 @@ if (!defaultProxyAlias?.openRouterResolve) {
126101
126180
  }
126102
126181
  var DEFAULT_PROXY_MODEL = defaultProxyAlias.openRouterResolve;
126103
126182
  var defaultProxyDisplayName = defaultProxyAlias.displayName;
126183
+ for (const [name, ladder] of Object.entries({ ...PROVIDER_LADDERS, router: ROUTER_LADDER })) {
126184
+ for (const slug2 of Object.values(ladder)) {
126185
+ if (!resolveDisplayAlias(slug2)) throw new Error(`${name} ladder names unknown alias ${slug2}`);
126186
+ }
126187
+ }
126104
126188
  var OSS_MODEL_ALLOWLIST = [
126105
126189
  "deepseek/deepseek-flash",
126106
126190
  "deepseek/deepseek-pro",
@@ -126138,6 +126222,17 @@ var OPENAI_COMPATIBLE_API_KEY_ENV = "OPENAI_COMPATIBLE_API_KEY";
126138
126222
  var OPENAI_COMPATIBLE_MODEL_ENV = "OPENAI_COMPATIBLE_MODEL";
126139
126223
  var OPENAI_COMPATIBLE_CONTEXT_ENV = "OPENAI_COMPATIBLE_CONTEXT";
126140
126224
  var OPENAI_COMPATIBLE_MAX_OUTPUT_ENV = "OPENAI_COMPATIBLE_MAX_OUTPUT";
126225
+ var PROVIDER_GATEWAY_URL_ENV = {
126226
+ anthropic: "ANTHROPIC_BASE_URL",
126227
+ openai: "OPENAI_BASE_URL"
126228
+ };
126229
+ function getProviderGatewayUrl(specifier) {
126230
+ const slashIndex = specifier?.indexOf("/") ?? -1;
126231
+ if (!specifier || slashIndex <= 0) return void 0;
126232
+ const envVar = PROVIDER_GATEWAY_URL_ENV[specifier.slice(0, slashIndex)];
126233
+ if (!envVar) return void 0;
126234
+ return process.env[envVar]?.trim().replace(/\/$/, "") || void 0;
126235
+ }
126141
126236
  function isBedrockAnthropicId(bedrockModelId) {
126142
126237
  return bedrockModelId.toLowerCase().split(/[./:]/).includes("anthropic");
126143
126238
  }
@@ -127644,26 +127739,24 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
127644
127739
 
127645
127740
  2. Checkout the PR branch via \`${t2("checkout_pr")}\`.
127646
127741
 
127647
- 3. Fetch review comments via \`${t2("get_review_comments")}\`.
127742
+ 3. Fetch review comments via \`${t2("get_review_comments")}\` and read the full conversation in each relevant thread.
127648
127743
 
127649
- 4. For each comment:
127650
- - understand the feedback
127744
+ 4. For each thread:
127745
+ - **check intent first**: questions, explanations, and disagreement call for a reply, not a code change. Change code only when explicitly requested. Automatic review feedback on Pullfrog-authored PRs (\`address_scope: all\`) remains actionable without a separate request; later replies can narrow or withdraw that permission.
127746
+ - under \`address_scope: mentions\`, unmentioned threads are context only unless the user's review-body request explicitly includes them
127651
127747
  - **verify the finding yourself** against the actual code before deciding whether to apply \u2014 every comment (human or agent) is a hypothesis, not a directive. agent reviewers especially are fallible.
127652
- - you are searching for a solution that is **complete, minimal, and elegant** \u2014 you may need to think hard to find it. do not over-engineer, do not be over-defensive, **do not write AI slop**. reviewers bias toward *recommending additions*, and that bias has a recognizable slop texture: defensive checks for impossible cases, extra abstractions used once, comments restating obvious code, tests asserting tautologies, "just-in-case" guards, error handlers for cases the type system already rules out. reject those. evaluate whether applying the finding would leave the code more **sound, correct, AND elegant**; two-out-of-three is a signal to look harder for a fix that gets all three. if a request would add bloat \u2014 ceremony without commensurate correctness benefit \u2014 push back in your reply rather than mechanically applying it.
127653
- - if the request stands, make the code change using your native tools; otherwise reply explaining why
127748
+ - keep requested changes **sound, minimal, and elegant**. Reject defensive checks for impossible cases, single-use abstractions, redundant comments, and tautological tests rather than mechanically applying reviewer suggestions.
127749
+ - if intent permits a code change and the finding stands, implement it using your native tools; otherwise reply without editing
127654
127750
  - record what was done (or why nothing was done)
127655
127751
 
127656
127752
  5. Quality check:
127657
- - 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.
127658
- - 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
127659
- - ${commitStep}
127753
+ - re-call \`${t2("get_review_comments")}\` with \`fresh: true\` before finishing, even when only replying. Apply the same scope and intent checks to new comments; respect requests to stop or change direction before committing.
127754
+ - if code changed, test and review the diff, remove unintended changes or bloat, then ${commitStep}
127660
127755
 
127661
- 6. Finalize. Reply + resolve are paired write actions: do BOTH or NEITHER for each thread.
127662
- - ${finalizeStep} (same push/prepush guidance as Build mode in *SYSTEM*)
127663
- - **if the push/commit fails**, call \`${t2("report_progress")}\` with the exact error and STOP \u2014 do NOT reply or resolve any thread until the fix is live on the remote. Resolving a thread without the fix landing misleads the reviewer.
127664
- - **once the fix is live on the remote**, for each thread you acted on:
127665
- - 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.
127666
- - **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.
127756
+ 6. Finalize:
127757
+ - **only if code changed**, ${finalizeStep} (same push/prepush guidance as Build mode in *SYSTEM*). If push/commit fails, report the exact error and STOP without claiming a fix or resolving its threads.
127758
+ - reply ONCE per relevant thread via \`${t2("reply_to_review_comment")}\`, including when no code changed. The \`comment_id\` is the root comment's numeric \`id=\`, NOT the \`thread=\` value from \`${t2("get_review_comments")}\`. The runtime dedupes identical bodies within a session.
127759
+ - resolve a thread via \`${t2("resolve_review_thread")}\` (using \`thread=\` as \`thread_id\`) only after its requested fix is fully on the remote, or the user explicitly asks to resolve it. Leave discussion-only replies, partial fixes, and disagreements open.
127667
127760
  - 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.`
127668
127761
  },
127669
127762
  // Review and IncrementalReview route the minimum reviewfrog specialists
@@ -129640,6 +129733,19 @@ function warnIfNativeEditsUnavailable(ctx) {
129640
129733
  "\xBB this run cannot change files: codex's native write tool depends on bubblewrap, which Ubuntu 24.04 blocks (https://github.com/openai/codex/issues/29908), and the MCP shell is disabled for this repo. read-only work is unaffected \u2014 set `shell: restricted` if the agent needs to edit anything."
129641
129734
  );
129642
129735
  }
129736
+ var CODEX_GATEWAY_PROVIDER = "pullfrog-gateway";
129737
+ function gatewayProvider(gatewayUrl) {
129738
+ if (!gatewayUrl) return [];
129739
+ return [
129740
+ `[model_providers.${CODEX_GATEWAY_PROVIDER}]`,
129741
+ 'name = "OpenAI gateway"',
129742
+ `base_url = ${JSON.stringify(gatewayUrl)}`,
129743
+ 'wire_api = "responses"',
129744
+ // where the harness puts the run's OPENAI_API_KEY
129745
+ 'env_key = "CODEX_API_KEY"',
129746
+ ""
129747
+ ];
129748
+ }
129643
129749
  function writeCodexConfig(params) {
129644
129750
  const shellEnabled = params.ctx.payload.shell === "enabled";
129645
129751
  const sandboxMode = params.ctx.payload.push === "disabled" ? "read-only" : "workspace-write";
@@ -129656,6 +129762,7 @@ function writeCodexConfig(params) {
129656
129762
  'web_search = "live"',
129657
129763
  ...params.model ? [`model = ${JSON.stringify(params.model)}`] : [],
129658
129764
  ...params.effortRung ? [`model_reasoning_effort = ${JSON.stringify(params.effortRung)}`] : [],
129765
+ ...params.gatewayUrl ? [`model_provider = ${JSON.stringify(CODEX_GATEWAY_PROVIDER)}`] : [],
129659
129766
  "",
129660
129767
  "[features]",
129661
129768
  tomlBool(CODEX_DISABLED_FEATURES, false),
@@ -129685,6 +129792,7 @@ function writeCodexConfig(params) {
129685
129792
  "tool_timeout_sec = 660",
129686
129793
  "",
129687
129794
  ...repoMcpApprovals(repoDir),
129795
+ ...gatewayProvider(params.gatewayUrl),
129688
129796
  // UNTRUSTED, deliberately, and this one line is the whole config-precedence
129689
129797
  // boundary. Codex loads `<repo>/.codex/config.toml` as a layer that
129690
129798
  // OUTRANKS ours (project 25 vs user 20), and its denylist strips none of
@@ -129953,7 +130061,11 @@ var codex = agent({
129953
130061
  ctx,
129954
130062
  codexHome,
129955
130063
  model: resolveCodexModel(ctx),
129956
- effortRung: effort.rung && CODEX_EFFORTS.includes(effort.rung) ? effort.rung : void 0
130064
+ effortRung: effort.rung && CODEX_EFFORTS.includes(effort.rung) ? effort.rung : void 0,
130065
+ // keyed off OpenAI, not the resolved model: codex serves nothing else, an
130066
+ // auto-selected run has no model, and a ChatGPT-subscription token is
130067
+ // valid only at ChatGPT's own backend
130068
+ gatewayUrl: codexHomeAuth ? void 0 : getProviderGatewayUrl("openai/")
129957
130069
  });
129958
130070
  if (codexHomeAuth) {
129959
130071
  core3.saveState(
@@ -135720,6 +135832,14 @@ function openAICompatibleProvider(model) {
135720
135832
  }
135721
135833
  };
135722
135834
  }
135835
+ var OPENCODE_GATEWAY_PATH = { anthropic: "/v1" };
135836
+ function providerGatewayOverride(model) {
135837
+ const gatewayUrl = getProviderGatewayUrl(model);
135838
+ if (!model || !gatewayUrl) return {};
135839
+ const provider2 = model.slice(0, model.indexOf("/"));
135840
+ const baseURL = `${gatewayUrl}${OPENCODE_GATEWAY_PATH[provider2] ?? ""}`;
135841
+ return { [provider2]: { options: { baseURL } } };
135842
+ }
135723
135843
  function azureProvider(model) {
135724
135844
  const prefix = `${AZURE_PROVIDER}/`;
135725
135845
  if (!model?.startsWith(prefix)) return {};
@@ -135835,7 +135955,8 @@ function buildSecurityConfig(ctx, model) {
135835
135955
  provider: {
135836
135956
  openrouter: { models: kimiOpenRouterProviderOverrides() },
135837
135957
  ...openAICompatibleProvider(model),
135838
- ...azureProvider(model)
135958
+ ...azureProvider(model),
135959
+ ...providerGatewayOverride(model)
135839
135960
  }
135840
135961
  };
135841
135962
  if (model) {
@@ -214306,7 +214427,7 @@ function GetReviewCommentsTool(ctx) {
214306
214427
  threadCount: threadBlocks.length,
214307
214428
  commentsPath,
214308
214429
  toc: formatted.toc,
214309
- 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.`
214430
+ 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, including comments that landed after this run started. Being returned does not authorize a code change; follow the task's scope and each thread's latest request. 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"). read each full thread before deciding whether it calls for a reply or a code change.`
214310
214431
  };
214311
214432
  })
214312
214433
  });
@@ -214429,8 +214550,7 @@ An existing plan comment was found for this issue. Update that comment with the
214429
214550
  };
214430
214551
  }
214431
214552
  var modeInstructionParent = {
214432
- IncrementalReview: "Review",
214433
- Fix: "Build"
214553
+ IncrementalReview: "Review"
214434
214554
  };
214435
214555
  function buildOrchestratorGuidance(ctx, mode, overrideGuidance) {
214436
214556
  const hardcoded = overrideGuidance ?? mode.prompt ?? "";
@@ -215642,18 +215762,13 @@ function hasEnvVar4(name) {
215642
215762
  const value2 = process.env[name];
215643
215763
  return typeof value2 === "string" && value2.length > 0;
215644
215764
  }
215645
- var HOST_OVERRIDES = {
215646
- ANTHROPIC_API_KEY: "ANTHROPIC_BASE_URL",
215647
- CLAUDE_CODE_OAUTH_TOKEN: "ANTHROPIC_BASE_URL"
215648
- };
215649
215765
  function envVarsFor(model) {
215650
215766
  return model.includes("/") ? getModelEnvVars(model) : [];
215651
215767
  }
215652
215768
  async function checkOne(params) {
215653
215769
  const value2 = process.env[params.envVar];
215654
215770
  if (!value2) return null;
215655
- const hostOverride = HOST_OVERRIDES[params.envVar];
215656
- if (hostOverride && hasEnvVar4(hostOverride)) return null;
215771
+ if (getProviderGatewayUrl(params.model)) return null;
215657
215772
  if (params.envVar === "CLAUDE_CODE_OAUTH_TOKEN") {
215658
215773
  const preflight = await preflightClaudeSubscription({
215659
215774
  token: value2,
@@ -216571,6 +216686,15 @@ var JsonPayload = type({
216571
216686
  "type?": "string | undefined",
216572
216687
  "model?": "string | undefined",
216573
216688
  "modelExplicit?": "boolean | undefined",
216689
+ // optional so a payload from a pre-router server build still parses against a
216690
+ // newer action across a rolling deploy.
216691
+ "routing?": type({
216692
+ tier: "'minimal' | 'light' | 'standard' | 'deep'",
216693
+ stakes: "number",
216694
+ workload: "number",
216695
+ source: "'scorer' | 'heuristic' | 'fixed'",
216696
+ rationale: "string"
216697
+ }).or("undefined"),
216574
216698
  "effort?": "number | string | undefined",
216575
216699
  "debug?": "boolean | undefined",
216576
216700
  prompt: "string",
@@ -216752,6 +216876,7 @@ function resolvePayload(resolvedPromptInput, repoSettings) {
216752
216876
  // explicit only when the model came from a per-run override flag (carried on
216753
216877
  // the JSON payload). a GHA `model` input or the repo default is not explicit.
216754
216878
  modelExplicit: jsonPayload?.modelExplicit ?? false,
216879
+ routing: jsonPayload?.routing,
216755
216880
  effort,
216756
216881
  debug: debug4 || void 0,
216757
216882
  prompt,
@@ -217438,9 +217563,12 @@ async function fetchRunContext(params) {
217438
217563
  if (params.oidcToken) {
217439
217564
  headers["X-GitHub-OIDC-Token"] = params.oidcToken;
217440
217565
  }
217441
- const query = params.runType ? `?type=${encodeURIComponent(params.runType)}` : "";
217566
+ const query = new URLSearchParams();
217567
+ if (params.runType) query.set("type", params.runType);
217568
+ if (params.routedTier) query.set("tier", params.routedTier);
217569
+ const search2 = query.toString();
217442
217570
  const response = await apiFetch({
217443
- path: `/api/repo/${params.repoContext.owner}/${params.repoContext.name}/run-context${query}`,
217571
+ path: `/api/repo/${params.repoContext.owner}/${params.repoContext.name}/run-context${search2 ? `?${search2}` : ""}`,
217444
217572
  headers,
217445
217573
  signal: controller.signal
217446
217574
  });
@@ -217507,7 +217635,13 @@ async function resolveRunContextData(params) {
217507
217635
  retries: [100, 500],
217508
217636
  bail: (error109) => !isTransientOctokitError(error109)
217509
217637
  })(),
217510
- fetchRunContext({ token: params.token, repoContext, oidcToken, runType: params.runType })
217638
+ fetchRunContext({
217639
+ token: params.token,
217640
+ repoContext,
217641
+ oidcToken,
217642
+ runType: params.runType,
217643
+ routedTier: params.routedTier
217644
+ })
217511
217645
  ]);
217512
217646
  return {
217513
217647
  repo: {
@@ -218415,6 +218549,12 @@ function logRunStartup(ctx) {
218415
218549
  log2.info(
218416
218550
  `\xBB effort: ${resolveEffortForLog({ payload: ctx.payload, resolvedModel: ctx.resolvedModel })}`
218417
218551
  );
218552
+ const routing = ctx.payload.routing;
218553
+ if (routing) {
218554
+ log2.info(
218555
+ `\xBB routed: ${routing.tier} (${routing.source}: stakes ${routing.stakes}, workload ${routing.workload}) \u2014 ${routing.rationale}`
218556
+ );
218557
+ }
218418
218558
  log2.info(
218419
218559
  `\xBB agent: ${resolveAgentForLog({ agentName: ctx.agentName, resolvedModel: ctx.resolvedModel })}`
218420
218560
  );
@@ -218609,7 +218749,8 @@ async function main() {
218609
218749
  const runContext = await resolveRunContextData({
218610
218750
  octokit: initialOctokit,
218611
218751
  token: jobToken,
218612
- runType: typeof resolvedPromptInput === "string" ? void 0 : resolvedPromptInput.type
218752
+ runType: typeof resolvedPromptInput === "string" ? void 0 : resolvedPromptInput.type,
218753
+ routedTier: typeof resolvedPromptInput === "string" ? void 0 : resolvedPromptInput.routing?.tier
218613
218754
  });
218614
218755
  timer.checkpoint("runContextData");
218615
218756
  const payload = resolvePayload(resolvedPromptInput, runContext.repoSettings);
@@ -219730,7 +219871,7 @@ function resolveTarget2(repo) {
219730
219871
  return { owner: match3[1], repo: match3[2] };
219731
219872
  }
219732
219873
  function cliVersion() {
219733
- const parts = /^(\d+)\.(\d+)\.(\d+)$/.exec("0.1.75");
219874
+ const parts = /^(\d+)\.(\d+)\.(\d+)$/.exec("0.1.77");
219734
219875
  if (!parts) return "0.0.0";
219735
219876
  return `${Number(parts[1])}.${Number(parts[2])}.${Number(parts[3])}`;
219736
219877
  }
@@ -219897,7 +220038,7 @@ async function runCli8(input2) {
219897
220038
  }
219898
220039
 
219899
220040
  // cli.ts
219900
- var VERSION10 = "0.1.75";
220041
+ var VERSION10 = "0.1.77";
219901
220042
  var bin = basename2(process.argv[1] || "");
219902
220043
  var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
219903
220044
  var rawArgs = process.argv.slice(2);
@@ -4,6 +4,7 @@
4
4
  * Other files in action/ re-export from this file for backward compatibility.
5
5
  */
6
6
  import type { EffortPosition } from "./effort.ts";
7
+ import type { RouterTier } from "./models.ts";
7
8
  export declare const pullfrogMcpName = "pullfrog";
8
9
  /** @see {@link file://./agents/shared.ts} Agent interface that uses this type */
9
10
  export type AgentId = "claude" | "codex" | "opencode";
@@ -51,8 +52,8 @@ export declare function findDanglingPromptToolRefs(params: {
51
52
  }): string[];
52
53
  export type { EffortPosition } from "./effort.ts";
53
54
  export { DEFAULT_EFFORT_POSITION, EFFORT_ALIASES, isEffortPosition, offeredRungs, parseEffortPosition, resolveRung, rungLabel, rungPosition, SUBSIDY_RUNG, } from "./effort.ts";
54
- export type { AutoTier, ModelAlias, ModelProvider, ProviderConfig } from "./models.ts";
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";
55
+ export type { AutoTier, ModelAlias, ModelProvider, ProviderConfig, RouterTier, } from "./models.ts";
56
+ export { AUTO_EFFICIENT, AUTO_INTELLIGENT, AUTO_ROUTER, autoLadder, autoRoutedSlug, DEFAULT_PROXY_MODEL, defaultAutoTier, getAutoSelectHintModel, getModelEffortLevels, getModelEnvVars, getModelManagedCredentials, getModelProvider, getProviderDisplayName, isAutoRouted, isAutoSlug, isAutoTier, isCardGatedModel, isOssAllowedModel, isRouterTier, ladderRungs, modelAliases, modelHasStoredAuth, OSS_MODEL_ALLOWLIST, OSS_RECOMMENDED_MODEL, ossFundedModelNames, PROVIDER_GATEWAY_URL_ENV, parseModel, providers, ROUTED_PROVIDERS, ROUTER_LADDER, ROUTER_TIERS, resolveAutoTier, resolveCliModel, resolveDisplayAlias, resolveModelRung, resolveModelSlug, resolveOpenRouterModel, resolveRoutedModel, } from "./models.ts";
56
57
  export type ToolPermission = "disabled" | "enabled";
57
58
  export type ShellPermission = "disabled" | "restricted" | "enabled";
58
59
  export type PushPermission = "disabled" | "restricted" | "enabled";
@@ -262,6 +263,21 @@ export interface XrepoConfig {
262
263
  */
263
264
  unavailable?: string[] | undefined;
264
265
  }
266
+ /**
267
+ * the model router's decision, carried so the runner can log it and forward the
268
+ * tier to run-context as `?tier=` — the Router proxy mint happens there and
269
+ * never sees `model` below. see wiki/router.md.
270
+ */
271
+ export interface PayloadRouting {
272
+ tier: RouterTier;
273
+ stakes: number;
274
+ workload: number;
275
+ /** who decided: the scorer, the heuristic it falls back to, or the fixed tier a non-review kind of work carries. mirrors the `RouterSource` enum. */
276
+ source: RouterSource;
277
+ /** the scorer's own sentence or two on what a reviewer must reconstruct */
278
+ rationale: string;
279
+ }
280
+ export type RouterSource = "scorer" | "heuristic" | "fixed";
265
281
  export interface WriteablePayload {
266
282
  "~pullfrog": true;
267
283
  /** semantic version of the payload to ensure compatibility */
@@ -283,6 +299,8 @@ export interface WriteablePayload {
283
299
  * baseInstructions flag) keeps the soft-fallback safety net. see modelAccess.ts.
284
300
  */
285
301
  modelExplicit?: boolean | undefined;
302
+ /** the model router's decision, when it made one; `model` above already reflects it */
303
+ routing?: PayloadRouting | undefined;
286
304
  /** reasoning-effort position on [0,1]; 0 is the model's cheapest rung, 1 its priciest */
287
305
  effort?: EffortPosition | undefined;
288
306
  /**