pullfrog 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -19718,10 +19718,10 @@ var require_core = __commonJS({
19718
19718
  (0, command_1.issueCommand)("set-env", { name }, convertedVal);
19719
19719
  }
19720
19720
  exports.exportVariable = exportVariable;
19721
- function setSecret3(secret) {
19721
+ function setSecret4(secret) {
19722
19722
  (0, command_1.issueCommand)("add-mask", {}, secret);
19723
19723
  }
19724
- exports.setSecret = setSecret3;
19724
+ exports.setSecret = setSecret4;
19725
19725
  function addPath(inputPath) {
19726
19726
  const filePath = process.env["GITHUB_PATH"] || "";
19727
19727
  if (filePath) {
@@ -47737,7 +47737,7 @@ var require_core3 = __commonJS({
47737
47737
  Object.defineProperty(exports, "__esModule", { value: true });
47738
47738
  var id_1 = require_id();
47739
47739
  var ref_1 = require_ref();
47740
- var core7 = [
47740
+ var core8 = [
47741
47741
  "$schema",
47742
47742
  "$id",
47743
47743
  "$defs",
@@ -47747,7 +47747,7 @@ var require_core3 = __commonJS({
47747
47747
  id_1.default,
47748
47748
  ref_1.default
47749
47749
  ];
47750
- exports.default = core7;
47750
+ exports.default = core8;
47751
47751
  }
47752
47752
  });
47753
47753
 
@@ -98924,7 +98924,7 @@ var require_fast_content_type_parse = __commonJS({
98924
98924
  });
98925
98925
 
98926
98926
  // main.ts
98927
- var core6 = __toESM(require_core(), 1);
98927
+ var core7 = __toESM(require_core(), 1);
98928
98928
  import { existsSync as existsSync7, readdirSync } from "node:fs";
98929
98929
  import { readFile as readFile4 } from "node:fs/promises";
98930
98930
  import { join as join17 } from "node:path";
@@ -107955,8 +107955,7 @@ var providers = {
107955
107955
  "gpt-5-nano": {
107956
107956
  displayName: "GPT Nano",
107957
107957
  resolve: "opencode/gpt-5-nano",
107958
- envVars: [],
107959
- isFree: true
107958
+ openRouterResolve: "openrouter/openai/gpt-5-nano"
107960
107959
  },
107961
107960
  "mimo-v2-pro-free": {
107962
107961
  displayName: "MiMo V2 Pro",
@@ -108185,6 +108184,11 @@ async function apiFetch(options) {
108185
108184
  if (bypassSecret) {
108186
108185
  headers["x-vercel-protection-bypass"] = bypassSecret;
108187
108186
  }
108187
+ if (!options.body) {
108188
+ for (const key of Object.keys(headers)) {
108189
+ if (key.toLowerCase() === "content-type") delete headers[key];
108190
+ }
108191
+ }
108188
108192
  log.debug(`api fetch: ${options.method ?? "GET"} ${url4.pathname}`);
108189
108193
  const init = {
108190
108194
  method: options.method ?? "GET",
@@ -108873,8 +108877,11 @@ function sanitizeToolForGemini(tool2) {
108873
108877
  }
108874
108878
  function isGeminiRouted(ctx) {
108875
108879
  const effective = ctx.payload.proxyModel ?? ctx.resolvedModel ?? ctx.payload.model;
108876
- if (!effective) return false;
108877
- return effective.toLowerCase().includes("gemini");
108880
+ if (!effective) return true;
108881
+ const normalized = effective.toLowerCase();
108882
+ if (normalized.includes("gemini")) return true;
108883
+ if (!normalized.includes("/")) return true;
108884
+ return false;
108878
108885
  }
108879
108886
 
108880
108887
  // mcp/shared.ts
@@ -109735,12 +109742,41 @@ function installSignalHandler() {
109735
109742
  killTrackedChildren();
109736
109743
  });
109737
109744
  }
109745
+ var DEFAULT_MAX_RETAINED_BYTES = 8 * 1024 * 1024;
109746
+ var TailBuffer = class {
109747
+ // explicit field declarations rather than constructor parameter properties:
109748
+ // node's strip-only TS loader (used by action/test/run.ts in CI) rejects
109749
+ // `constructor(private readonly cap: number)` with ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX.
109750
+ cap;
109751
+ buffer = "";
109752
+ truncatedBytes = 0;
109753
+ constructor(cap) {
109754
+ this.cap = cap;
109755
+ }
109756
+ append(chunk) {
109757
+ if (this.cap <= 0) return;
109758
+ this.buffer += chunk;
109759
+ if (this.buffer.length > this.cap) {
109760
+ const drop = this.buffer.length - this.cap;
109761
+ this.truncatedBytes += drop;
109762
+ this.buffer = this.buffer.slice(drop);
109763
+ }
109764
+ }
109765
+ toString() {
109766
+ if (this.truncatedBytes === 0) return this.buffer;
109767
+ const mib = (this.truncatedBytes / 1024 / 1024).toFixed(1);
109768
+ return `... [${mib} MiB truncated by retain:tail cap] ...
109769
+ ${this.buffer}`;
109770
+ }
109771
+ };
109738
109772
  async function spawn(options) {
109739
109773
  const activityTimeoutMs = options.activityTimeout ?? DEFAULT_ACTIVITY_TIMEOUT_MS;
109740
109774
  installSignalHandler();
109741
109775
  const startTime = performance3.now();
109742
- let stdoutBuffer = "";
109743
- let stderrBuffer = "";
109776
+ const retain = options.retain ?? "tail";
109777
+ const cap = options.maxRetainedBytes ?? DEFAULT_MAX_RETAINED_BYTES;
109778
+ const stdoutBuffer = retain === "none" ? null : new TailBuffer(cap);
109779
+ const stderrBuffer = retain === "none" ? null : new TailBuffer(cap);
109744
109780
  const killGroup = options.killGroup ?? false;
109745
109781
  return new Promise((resolve3, reject) => {
109746
109782
  const child = nodeSpawn(options.cmd, options.args, {
@@ -109814,17 +109850,29 @@ async function spawn(options) {
109814
109850
  }
109815
109851
  if (child.stdout) {
109816
109852
  child.stdout.on("data", (data) => {
109817
- updateActivity();
109818
- const chunk = data.toString();
109819
- stdoutBuffer += chunk;
109820
- options.onStdout?.(chunk);
109853
+ try {
109854
+ updateActivity();
109855
+ const chunk = data.toString();
109856
+ stdoutBuffer?.append(chunk);
109857
+ options.onStdout?.(chunk);
109858
+ } catch (err) {
109859
+ log.debug(
109860
+ `spawn stdout handler threw: ${err instanceof Error ? err.message : String(err)}`
109861
+ );
109862
+ }
109821
109863
  });
109822
109864
  }
109823
109865
  if (child.stderr) {
109824
109866
  child.stderr.on("data", (data) => {
109825
- const chunk = data.toString();
109826
- stderrBuffer += chunk;
109827
- options.onStderr?.(chunk);
109867
+ try {
109868
+ const chunk = data.toString();
109869
+ stderrBuffer?.append(chunk);
109870
+ options.onStderr?.(chunk);
109871
+ } catch (err) {
109872
+ log.debug(
109873
+ `spawn stderr handler threw: ${err instanceof Error ? err.message : String(err)}`
109874
+ );
109875
+ }
109828
109876
  });
109829
109877
  }
109830
109878
  child.on("close", (exitCode, signal) => {
@@ -109851,7 +109899,7 @@ async function spawn(options) {
109851
109899
  return;
109852
109900
  }
109853
109901
  let resolvedExitCode = exitCode ?? 0;
109854
- let resolvedStderr = stderrBuffer;
109902
+ let resolvedStderr = stderrBuffer?.toString() ?? "";
109855
109903
  if (exitCode === null && signal) {
109856
109904
  const killMsg = `[spawn] ${options.cmd}: killed by signal ${signal}`;
109857
109905
  resolvedStderr = resolvedStderr ? `${resolvedStderr}
@@ -109859,7 +109907,7 @@ ${killMsg}` : killMsg;
109859
109907
  resolvedExitCode = 1;
109860
109908
  }
109861
109909
  resolve3({
109862
- stdout: stdoutBuffer,
109910
+ stdout: stdoutBuffer?.toString() ?? "",
109863
109911
  stderr: resolvedStderr,
109864
109912
  exitCode: resolvedExitCode,
109865
109913
  durationMs
@@ -109873,11 +109921,12 @@ ${killMsg}` : killMsg;
109873
109921
  if (activityCheckIntervalId) clearInterval(activityCheckIntervalId);
109874
109922
  const errMsg = `[spawn] ${options.cmd}: ${error49.message}`;
109875
109923
  console.error(errMsg);
109876
- stderrBuffer = stderrBuffer ? `${stderrBuffer}
109924
+ const existingStderr = stderrBuffer?.toString() ?? "";
109925
+ const finalStderr = existingStderr ? `${existingStderr}
109877
109926
  ${errMsg}` : errMsg;
109878
109927
  resolve3({
109879
- stdout: stdoutBuffer,
109880
- stderr: stderrBuffer,
109928
+ stdout: stdoutBuffer?.toString() ?? "",
109929
+ stderr: finalStderr,
109881
109930
  exitCode: 1,
109882
109931
  durationMs
109883
109932
  });
@@ -137786,7 +137835,7 @@ var require_core4 = /* @__PURE__ */ __commonJSMin(((exports) => {
137786
137835
  Object.defineProperty(exports, "__esModule", { value: true });
137787
137836
  const id_1 = require_id2();
137788
137837
  const ref_1 = require_ref2();
137789
- const core7 = [
137838
+ const core8 = [
137790
137839
  "$schema",
137791
137840
  "$id",
137792
137841
  "$defs",
@@ -137796,7 +137845,7 @@ var require_core4 = /* @__PURE__ */ __commonJSMin(((exports) => {
137796
137845
  id_1.default,
137797
137846
  ref_1.default
137798
137847
  ];
137799
- exports.default = core7;
137848
+ exports.default = core8;
137800
137849
  }));
137801
137850
  var require_limitNumber2 = /* @__PURE__ */ __commonJSMin(((exports) => {
137802
137851
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -142306,7 +142355,7 @@ var import_semver = __toESM(require_semver2(), 1);
142306
142355
  // package.json
142307
142356
  var package_default = {
142308
142357
  name: "pullfrog",
142309
- version: "0.1.4",
142358
+ version: "0.1.6",
142310
142359
  type: "module",
142311
142360
  bin: {
142312
142361
  pullfrog: "dist/cli.mjs",
@@ -146383,6 +146432,8 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
146383
146432
  - **4\u20135 lenses (high-stakes subsystem touches)** \u2014 any billing/payments change (billing-subsystem + correctness + security + operational-readiness); new auth flow (auth-subsystem + correctness + security + test-integrity); schema migration (schema-migration-subsystem + correctness + operational-readiness + impact); cross-subsystem PR that touches billing AND auth AND schema (one subsystem lens per domain + correctness)
146384
146433
  - **6+ lenses** \u2014 almost always a smell; you're either covering overlapping ground or this PR should have been split. push back via the review body rather than expanding lens count.
146385
146434
 
146435
+ **lens-add discipline.** Each lens needs to clear a specific bar before you dispatch it: name the concrete failure mode this lens would catch *that the diff plausibly introduces*, in one sentence. "Could apply", "good to have", "for completeness" do not qualify. If you can't name what the lens is going to find, drop it. The "when unsure, treat as non-trivial" rule above is for the trivial-vs-non-trivial gate at step 3 \u2014 it does not license expanding lens count without articulated risk. Every extra lens adds wall-time, log noise, and pulls subagent attention onto speculative angles, which biases the final review toward bloat-shaped findings.
146436
+
146386
146437
  lenses come in two flavors, and you can mix them:
146387
146438
  - **themed lenses** \u2014 a perspective applied across the whole diff (correctness, security, user-journey, performance, etc.).
146388
146439
  - **subsystem lenses** \u2014 a domain-scoped frame for high-stakes subsystems the PR touches (e.g. "the auth lens", "the billing lens", "the schema-migration lens"). a subsystem lens is "review the PR specifically for what could go wrong in this subsystem" and naturally combines theme + scope. **for high-stakes domains, lead with the subsystem lens rather than the generic themed equivalent** \u2014 "billing-subsystem" outperforms "correctness on billing code" because the framing primes the subagent to remember domain-specific failure modes (double-charges, refund races, currency rounding, dispute flows) the generic lens misses.
@@ -146390,7 +146441,7 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
146390
146441
  starter menu (combine, omit, or invent your own):
146391
146442
  - **correctness & invariants** \u2014 bugs, races, error handling, edge cases, state-machine boundaries
146392
146443
  - **impact** \u2014 when the PR removes features, deletes exports, renames identifiers, or changes architectural patterns: stale references in code, tests, docs (\`docs/\`, \`wiki/\`), comments, configs, UI
146393
- - **research-validated assumptions** \u2014 third-party API contracts, SDK semantics, framework directives, version-gated behavior. the subagent must verify load-bearing claims via web search and quote source URLs.
146444
+ - **research-validated assumptions** \u2014 third-party API contracts, SDK semantics, framework directives, version-gated behavior. **only pick when the PR's correctness depends on the contract behaving a specific way** \u2014 not when the API is merely used. An idempotency key as a backstop, a timeout as a hint, a retry as belt-and-suspenders: not load-bearing, skip this lens. The bar is "if the third-party contract differs from what the diff assumes, the PR is incorrect." When dispatched, the subagent must verify load-bearing claims via web search and quote source URLs.
146394
146445
  - **security** \u2014 new endpoints, authZ, input validation, secrets handling, replay/CSRF/injection, cross-tenant isolation
146395
146446
  - **user-journey** \u2014 UX-touching flows: walk through happy path and failure modes as a user
146396
146447
  - **operational readiness** \u2014 observability, alerting, migrations (forward + rollback), feature flags, on-call burden
@@ -146479,7 +146530,7 @@ ${PR_SUMMARY_FORMAT}`
146479
146530
  "Looks trivial but isn't" (do NOT skip \u2014 same anti-patterns as Review mode): 1-line changes to SQL/regex/auth/billing/permissions/signature-verification code; flipping feature-flag defaults or retry/timeout constants; money/tax/HTTP-method/redirect changes; tightening or loosening a comparison operator; mixed diffs with a semantic line buried in formatting.
146480
146531
  When unsure, treat as non-trivial.
146481
146532
 
146482
- otherwise pick lenses by where the new commits concentrate risk \u2014 **there's no fixed count**, same calibration as Review mode (1 lens for pure refactor / isolated fix; 2\u20133 for typical features; 4\u20135 for high-stakes subsystem touches; 6+ is a smell). lens framing follows Review mode: themed lenses (correctness & invariants, impact when new commits remove/rename/deprecate things, research-validated assumptions, security, user-journey, operational readiness, integration & cross-cutting, test integrity, performance, holistic) and subsystem lenses (auth, billing, schema migration, etc.) \u2014 for high-stakes domains lead with the subsystem lens rather than the generic themed equivalent.
146533
+ otherwise pick lenses by where the new commits concentrate risk \u2014 **there's no fixed count**, same calibration as Review mode (1 lens for pure refactor / isolated fix; 2\u20133 for typical features; 4\u20135 for high-stakes subsystem touches; 6+ is a smell). same **lens-add discipline** as Review mode applies: each lens needs to name the concrete failure mode it would catch *that the new commits plausibly introduce* \u2014 "could apply" doesn't qualify, drop it. **research-validated assumptions** specifically: only pick when the new commits' correctness depends on a third-party contract behaving a specific way; merely using an API doesn't qualify. lens framing follows Review mode: themed lenses (correctness & invariants, impact when new commits remove/rename/deprecate things, research-validated assumptions, security, user-journey, operational readiness, integration & cross-cutting, test integrity, performance, holistic) and subsystem lenses (auth, billing, schema migration, etc.) \u2014 for high-stakes domains lead with the subsystem lens rather than the generic themed equivalent.
146483
146534
 
146484
146535
  dispatch one \`${REVIEWER_AGENT_NAME}\` subagent per lens \u2014 its baked-in system prompt enforces the non-mutative + non-recursive contract (read-only file/search/web tools and read-only MCP queries; no writes, shell side effects, state-changing MCP calls, or nested subagent dispatch). dispatch them in a **single assistant turn with multiple parallel subagent calls** (serial dispatch collapses the fan-out). if a subagent errors out, times out, or returns nothing usable, retry once with the same lens; if it still fails, proceed with partial coverage and note the missing lens in the review body \u2014 do not skip step 5 entirely on a single subagent failure. each subagent gets:
146485
146536
  - the diff scope (incremental diff path if available, full diff otherwise). do NOT tell them to skip pre-existing issues \u2014 that suppresses regressions the new commits amplified; the "issues must be NEW" filter lives at aggregation time (step 6), not in the subagent prompt
@@ -146849,20 +146900,30 @@ var ThinkingTimer = class {
146849
146900
  maximumFractionDigits: 1
146850
146901
  });
146851
146902
  lastToolResultTimestamp = null;
146903
+ formatLine;
146904
+ // node's native TS strip-only mode does not support parameter properties,
146905
+ // so the formatter is declared as a field and assigned in the body.
146906
+ constructor(formatLine = (l) => l) {
146907
+ this.formatLine = formatLine;
146908
+ }
146852
146909
  markToolResult() {
146853
146910
  this.lastToolResultTimestamp = performance5.now();
146854
- log.debug(`\xBB thinking timer: markToolResult at ${this.lastToolResultTimestamp}`);
146911
+ log.debug(
146912
+ this.formatLine(`\xBB thinking timer: markToolResult at ${this.lastToolResultTimestamp}`)
146913
+ );
146855
146914
  }
146856
146915
  markToolCall() {
146857
146916
  const now = performance5.now();
146858
146917
  log.debug(
146859
- `\xBB thinking timer: markToolCall at ${now}, lastToolResult=${this.lastToolResultTimestamp}`
146918
+ this.formatLine(
146919
+ `\xBB thinking timer: markToolCall at ${now}, lastToolResult=${this.lastToolResultTimestamp}`
146920
+ )
146860
146921
  );
146861
146922
  if (this.lastToolResultTimestamp === null) return;
146862
146923
  const elapsed = now - this.lastToolResultTimestamp;
146863
146924
  if (elapsed < THINKING_THRESHOLD) return;
146864
146925
  const seconds = elapsed / 1e3;
146865
- log.info(`\xBB thought for ${this.durationFormatter.format(seconds)}`);
146926
+ log.info(this.formatLine(`\xBB thought for ${this.durationFormatter.format(seconds)}`));
146866
146927
  }
146867
146928
  };
146868
146929
 
@@ -147111,19 +147172,39 @@ function deriveLabelFromTaskInput(input) {
147111
147172
  }
147112
147173
  var SessionLabeler = class {
147113
147174
  labels = /* @__PURE__ */ new Map();
147175
+ labelsByToolUseId = /* @__PURE__ */ new Map();
147114
147176
  pendingLabels = [];
147115
147177
  fallbackCounter = 0;
147116
- recordTaskDispatch(input) {
147178
+ /**
147179
+ * Record a Task/Agent tool dispatch.
147180
+ *
147181
+ * @param input Task tool input — used to derive the lens label.
147182
+ * @param toolUseId Optional Agent tool_use id. When provided, future events
147183
+ * carrying `parent_tool_use_id === toolUseId` resolve
147184
+ * directly to this label without consuming the FIFO queue
147185
+ * (Claude path). Always also pushed to the FIFO queue so
147186
+ * the OpenCode path still works when toolUseId is absent.
147187
+ */
147188
+ recordTaskDispatch(input, toolUseId) {
147117
147189
  const label = deriveLabelFromTaskInput(input);
147118
147190
  this.pendingLabels.push(label);
147191
+ if (toolUseId) this.labelsByToolUseId.set(toolUseId, label);
147119
147192
  return label;
147120
147193
  }
147121
147194
  /**
147122
- * Return a label for the given sessionID. Binds on first call.
147123
- * Pass undefined/empty for events that lack a session id — the caller
147124
- * gets ORCHESTRATOR_LABEL so the line is still attributable.
147195
+ * Return a label for the given event.
147196
+ *
147197
+ * @param sessionID Session id from the event (OpenCode: per-session;
147198
+ * Claude: shared across orchestrator + subagents).
147199
+ * @param parentToolUseId Claude's `parent_tool_use_id` — non-null on
147200
+ * subagent messages. When set and known, takes
147201
+ * priority over the FIFO/sessionID path.
147125
147202
  */
147126
- labelFor(sessionID) {
147203
+ labelFor(sessionID, parentToolUseId) {
147204
+ if (parentToolUseId) {
147205
+ const direct = this.labelsByToolUseId.get(parentToolUseId);
147206
+ if (direct) return direct;
147207
+ }
147127
147208
  if (!sessionID) return ORCHESTRATOR_LABEL;
147128
147209
  const existing = this.labels.get(sessionID);
147129
147210
  if (existing) return existing;
@@ -147195,8 +147276,7 @@ function stripProviderPrefix(specifier) {
147195
147276
  const slashIndex = specifier.indexOf("/");
147196
147277
  return slashIndex > 0 ? specifier.slice(slashIndex + 1) : specifier;
147197
147278
  }
147198
- function resolveEffort(model) {
147199
- if (model?.includes("opus")) return "max";
147279
+ function resolveEffort(_model) {
147200
147280
  return "high";
147201
147281
  }
147202
147282
  function tailLines(text, maxCodeUnits) {
@@ -147208,7 +147288,23 @@ function tailLines(text, maxCodeUnits) {
147208
147288
  async function runClaude(params) {
147209
147289
  const startTime = performance6.now();
147210
147290
  let eventCount = 0;
147211
- const thinkingTimer = new ThinkingTimer();
147291
+ const labeler = new SessionLabeler();
147292
+ function eventLabel(event) {
147293
+ return labeler.labelFor(event.session_id ?? null, event.parent_tool_use_id ?? null);
147294
+ }
147295
+ function withLabel(label, message) {
147296
+ return label === ORCHESTRATOR_LABEL ? message : formatWithLabel(label, message);
147297
+ }
147298
+ const thinkingTimers = /* @__PURE__ */ new Map();
147299
+ function timerFor(label) {
147300
+ let t = thinkingTimers.get(label);
147301
+ if (!t) {
147302
+ const formatLine = (line) => label === ORCHESTRATOR_LABEL ? line : formatWithLabel(label, line);
147303
+ t = new ThinkingTimer(formatLine);
147304
+ thinkingTimers.set(label, t);
147305
+ }
147306
+ return t;
147307
+ }
147212
147308
  let finalOutput = "";
147213
147309
  let sessionId;
147214
147310
  let resultErrorSubtype = null;
@@ -147229,17 +147325,22 @@ async function runClaude(params) {
147229
147325
  } : void 0;
147230
147326
  }
147231
147327
  const handlers2 = {
147232
- system: (_event) => {
147233
- log.debug(`\xBB ${params.label} system event`);
147328
+ system: (event) => {
147329
+ const label = eventLabel(event);
147330
+ log.debug(withLabel(label, `\xBB ${params.label} system event`));
147234
147331
  },
147235
147332
  assistant: (event) => {
147236
147333
  const content = event.message?.content;
147237
147334
  if (!content) return;
147335
+ const label = eventLabel(event);
147336
+ const boxTitle = label === ORCHESTRATOR_LABEL ? params.label : `${params.label} [${label}]`;
147238
147337
  for (const block of content) {
147239
147338
  if (block.type === "text" && block.text?.trim()) {
147240
147339
  const message = block.text.trim();
147241
- log.box(message, { title: params.label });
147242
- finalOutput = message;
147340
+ log.box(message, { title: boxTitle });
147341
+ if (label === ORCHESTRATOR_LABEL) {
147342
+ finalOutput = message;
147343
+ }
147243
147344
  } else if (block.type === "tool_use") {
147244
147345
  const toolName = block.name || "unknown";
147245
147346
  if (params.onToolUse) {
@@ -147248,20 +147349,25 @@ async function runClaude(params) {
147248
147349
  input: block.input
147249
147350
  });
147250
147351
  }
147251
- thinkingTimer.markToolCall();
147252
- log.toolCall({ toolName, input: block.input || {} });
147253
- if (toolName === "Task" && block.input && typeof block.input === "object") {
147352
+ timerFor(label).markToolCall();
147353
+ const inputFormatted = formatJsonValue(block.input || {});
147354
+ const toolCallLine = inputFormatted !== "{}" ? `\xBB ${toolName}(${inputFormatted})` : `\xBB ${toolName}()`;
147355
+ log.info(withLabel(label, toolCallLine));
147356
+ if ((toolName === "Task" || toolName === "Agent") && block.input && typeof block.input === "object") {
147254
147357
  const taskInput = block.input;
147255
- const label = deriveLabelFromTaskInput(taskInput);
147358
+ const dispatchedLabel = labeler.recordTaskDispatch(taskInput, block.id ?? null);
147256
147359
  log.info(
147257
- `\xBB dispatching subagent: ${label}` + (taskInput.subagent_type ? ` (subagent_type=${taskInput.subagent_type})` : "")
147360
+ withLabel(
147361
+ label,
147362
+ `\xBB dispatching subagent: ${dispatchedLabel}` + (taskInput.subagent_type ? ` (subagent_type=${taskInput.subagent_type})` : "")
147363
+ )
147258
147364
  );
147259
147365
  }
147260
147366
  if (toolName.includes("report_progress") && params.todoTracker) {
147261
147367
  log.debug("\xBB report_progress detected, disabling todo tracking");
147262
147368
  params.todoTracker.cancel();
147263
147369
  }
147264
- if (toolName === "TodoWrite" && params.todoTracker?.enabled) {
147370
+ if (toolName === "TodoWrite" && params.todoTracker?.enabled && label === ORCHESTRATOR_LABEL) {
147265
147371
  params.todoTracker.update(block.input);
147266
147372
  }
147267
147373
  }
@@ -147277,17 +147383,18 @@ async function runClaude(params) {
147277
147383
  user: (event) => {
147278
147384
  const content = event.message?.content;
147279
147385
  if (!content) return;
147386
+ const label = eventLabel(event);
147280
147387
  for (const block of content) {
147281
147388
  if (typeof block === "string") continue;
147282
147389
  if (block.type === "tool_result") {
147283
- thinkingTimer.markToolResult();
147390
+ timerFor(label).markToolResult();
147284
147391
  const outputContent = typeof block.content === "string" ? block.content : Array.isArray(block.content) ? block.content.map(
147285
147392
  (entry) => typeof entry === "string" ? entry : typeof entry === "object" && entry !== null && "text" in entry ? String(entry.text) : JSON.stringify(entry)
147286
147393
  ).join("\n") : String(block.content);
147287
147394
  if (block.is_error) {
147288
- log.info(`\xBB tool error: ${outputContent}`);
147395
+ log.info(withLabel(label, `\xBB tool error: ${outputContent}`));
147289
147396
  } else {
147290
- log.debug(`\xBB tool output: ${outputContent}`);
147397
+ log.debug(withLabel(label, `\xBB tool output: ${outputContent}`));
147291
147398
  }
147292
147399
  }
147293
147400
  }
@@ -147357,7 +147464,7 @@ async function runClaude(params) {
147357
147464
  };
147358
147465
  const recentStderr = [];
147359
147466
  let lastProviderError = null;
147360
- let output = "";
147467
+ const output = new TailBuffer(DEFAULT_MAX_RETAINED_BYTES);
147361
147468
  let stdoutBuffer = "";
147362
147469
  try {
147363
147470
  const result = await spawn({
@@ -147374,9 +147481,14 @@ async function runClaude(params) {
147374
147481
  // there's no shim-orphan issue like opencode-ai/bin/opencode, but
147375
147482
  // detached + killGroup is the right default for any agent runtime.
147376
147483
  killGroup: true,
147484
+ // claude already drains every chunk via onStdout (NDJSON parsing) and
147485
+ // onStderr (recentStderr ring buffer). retaining a second copy in the
147486
+ // spawn wrapper would grow unbounded for long sessions and previously
147487
+ // crashed the wrapper with RangeError. see issue #680.
147488
+ retain: "none",
147377
147489
  onStdout: async (chunk) => {
147378
147490
  const text = chunk.toString();
147379
- output += text;
147491
+ output.append(text);
147380
147492
  markActivity();
147381
147493
  stdoutBuffer += text;
147382
147494
  const lines = stdoutBuffer.split("\n");
@@ -147451,16 +147563,18 @@ ${stderrContext}`);
147451
147563
  const usage = buildUsage();
147452
147564
  if (result.exitCode !== 0) {
147453
147565
  const errorContext = lastProviderError ? ` (${lastProviderError})` : "";
147454
- const truncatedStdout = result.stdout ? tailLines(result.stdout, 2048) : "";
147455
- const errorMessage = lastResultError || result.stderr || truncatedStdout || `unknown error - no output from Claude CLI${errorContext}`;
147566
+ const stdoutSnapshot = output.toString();
147567
+ const stderrSnapshot = recentStderr.join("\n");
147568
+ const truncatedStdout = stdoutSnapshot ? tailLines(stdoutSnapshot, 2048) : "";
147569
+ const errorMessage = lastResultError || stderrSnapshot || truncatedStdout || `unknown error - no output from Claude CLI${errorContext}`;
147456
147570
  log.error(
147457
147571
  `${params.label} exited with code ${result.exitCode}${errorContext}: ${errorMessage}`
147458
147572
  );
147459
- log.debug(`stdout: ${result.stdout?.substring(0, 500)}`);
147460
- log.debug(`stderr: ${result.stderr?.substring(0, 500)}`);
147573
+ log.debug(`stdout: ${stdoutSnapshot.substring(0, 500)}`);
147574
+ log.debug(`stderr: ${stderrSnapshot.substring(0, 500)}`);
147461
147575
  return {
147462
147576
  success: false,
147463
- output: finalOutput || output,
147577
+ output: finalOutput || stdoutSnapshot,
147464
147578
  error: errorMessage,
147465
147579
  usage,
147466
147580
  sessionId
@@ -147469,7 +147583,7 @@ ${stderrContext}`);
147469
147583
  if (eventCount === 0 && lastProviderError) {
147470
147584
  return {
147471
147585
  success: false,
147472
- output: finalOutput || output,
147586
+ output: finalOutput || output.toString(),
147473
147587
  error: `provider error: ${lastProviderError}`,
147474
147588
  usage,
147475
147589
  sessionId
@@ -147478,13 +147592,13 @@ ${stderrContext}`);
147478
147592
  if (resultErrorSubtype) {
147479
147593
  return {
147480
147594
  success: false,
147481
- output: finalOutput || output,
147595
+ output: finalOutput || output.toString(),
147482
147596
  error: lastResultError || `result subtype: ${resultErrorSubtype}`,
147483
147597
  usage,
147484
147598
  sessionId
147485
147599
  };
147486
147600
  }
147487
- return { success: true, output: finalOutput || output, usage, sessionId };
147601
+ return { success: true, output: finalOutput || output.toString(), usage, sessionId };
147488
147602
  } catch (error49) {
147489
147603
  params.todoTracker?.cancel();
147490
147604
  const duration4 = performance6.now() - startTime;
@@ -147503,7 +147617,7 @@ ${stderrContext}`
147503
147617
  );
147504
147618
  return {
147505
147619
  success: false,
147506
- output: finalOutput || output,
147620
+ output: finalOutput || output.toString(),
147507
147621
  error: `${errorMessage} [${diagnosis}]`,
147508
147622
  usage: buildUsage(),
147509
147623
  sessionId
@@ -147739,6 +147853,15 @@ function buildSecurityConfig(ctx, model) {
147739
147853
  [pullfrogMcpName]: { type: "remote", url: ctx.mcpServerUrl }
147740
147854
  },
147741
147855
  agent: buildReviewerAgentConfig(),
147856
+ // opt into opencode's experimental `batch` tool (added in
147857
+ // anomalyco/opencode PR #2983, opt-in via `experimental.batch_tool`). it
147858
+ // exposes a single `batch` tool that runs 1-25 independent tool calls
147859
+ // (read/grep/glob/bash/etc.) concurrently in one assistant turn, which
147860
+ // collapses the dominant grep→20×read pattern into a single round trip.
147861
+ // edits are explicitly disallowed inside the batch upstream. paired with
147862
+ // the "Parallel tool execution" guidance in utils/instructions.ts so the
147863
+ // model actually reaches for it. see wiki/prompt.md.
147864
+ experimental: { batch_tool: true },
147742
147865
  provider: {
147743
147866
  google: {
147744
147867
  models: Object.fromEntries(
@@ -147811,7 +147934,6 @@ function autoSelectModel(cliPath) {
147811
147934
  async function runOpenCode(params) {
147812
147935
  const startTime = performance7.now();
147813
147936
  let eventCount = 0;
147814
- const thinkingTimer = new ThinkingTimer();
147815
147937
  let finalOutput = "";
147816
147938
  let accumulatedTokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
147817
147939
  let accumulatedCostUsd = 0;
@@ -147828,6 +147950,16 @@ async function runOpenCode(params) {
147828
147950
  function withLabel(label, message) {
147829
147951
  return label === ORCHESTRATOR_LABEL ? message : formatWithLabel(label, message);
147830
147952
  }
147953
+ const thinkingTimers = /* @__PURE__ */ new Map();
147954
+ function timerFor(label) {
147955
+ let t = thinkingTimers.get(label);
147956
+ if (!t) {
147957
+ const formatLine = (line) => label === ORCHESTRATOR_LABEL ? line : formatWithLabel(label, line);
147958
+ t = new ThinkingTimer(formatLine);
147959
+ thinkingTimers.set(label, t);
147960
+ }
147961
+ return t;
147962
+ }
147831
147963
  const taskDispatchByCallID = /* @__PURE__ */ new Map();
147832
147964
  const pendingTaskDispatches = [];
147833
147965
  const knownNonTaskCallIDs = /* @__PURE__ */ new Set();
@@ -147976,7 +148108,7 @@ async function runOpenCode(params) {
147976
148108
  input: event.part?.state?.input
147977
148109
  });
147978
148110
  }
147979
- thinkingTimer.markToolCall();
148111
+ timerFor(label).markToolCall();
147980
148112
  const inputFormatted = formatJsonValue(event.part?.state?.input || {});
147981
148113
  const toolCallLine = inputFormatted !== "{}" ? `\xBB ${toolName}(${inputFormatted})` : `\xBB ${toolName}()`;
147982
148114
  log.info(withLabel(label, toolCallLine));
@@ -148000,7 +148132,7 @@ async function runOpenCode(params) {
148000
148132
  const status = event.part?.state?.status || event.status || "unknown";
148001
148133
  const output2 = event.part?.state?.output || event.output;
148002
148134
  const label = eventLabel(event);
148003
- thinkingTimer.markToolResult();
148135
+ timerFor(label).markToolResult();
148004
148136
  if (taskDispatchByCallID.size > 0 || pendingTaskDispatches.length > 0) {
148005
148137
  if (toolId && taskDispatchByCallID.has(toolId)) {
148006
148138
  const dispatch = taskDispatchByCallID.get(toolId);
@@ -148125,7 +148257,7 @@ async function runOpenCode(params) {
148125
148257
  const recentStderr = [];
148126
148258
  let lastProviderError = null;
148127
148259
  let agentErrorEvent = null;
148128
- let output = "";
148260
+ const output = new TailBuffer(DEFAULT_MAX_RETAINED_BYTES);
148129
148261
  let stdoutBuffer = "";
148130
148262
  try {
148131
148263
  const result = await spawn({
@@ -148143,6 +148275,11 @@ async function runOpenCode(params) {
148143
148275
  // never fires — producing zombie runs. detached + killGroup nukes the
148144
148276
  // whole tree.
148145
148277
  killGroup: true,
148278
+ // we already drain every chunk via onStdout/onStderr (NDJSON parsing
148279
+ // + recentStderr ring buffer). retaining a second copy in the spawn
148280
+ // wrapper would grow unbounded for multi-lens Reviews and previously
148281
+ // crashed the wrapper with RangeError at ~1 GiB. see issue #680.
148282
+ retain: "none",
148146
148283
  // NB: we used to pass `isPausedExternally: isSubagentInFlight` to suspend
148147
148284
  // the activity timer during subagent dispatches. unnecessary now that
148148
148285
  // our injected plugin (action/agents/opencodePlugin.ts) re-emits
@@ -148152,7 +148289,7 @@ async function runOpenCode(params) {
148152
148289
  // (~3.3 plugin events/sec during a typical subagent run).
148153
148290
  onStdout: async (chunk) => {
148154
148291
  const text = chunk.toString();
148155
- output += text;
148292
+ output.append(text);
148156
148293
  markActivity();
148157
148294
  stdoutBuffer += text;
148158
148295
  const lines = stdoutBuffer.split("\n");
@@ -148241,18 +148378,25 @@ ${stderrContext}`);
148241
148378
  const usage = buildUsage();
148242
148379
  if (result.exitCode !== 0) {
148243
148380
  const errorContext = lastProviderError ? ` (${lastProviderError})` : "";
148244
- const errorMessage = result.stderr || result.stdout || `unknown error - no output from OpenCode CLI${errorContext}`;
148381
+ const stdoutSnapshot = output.toString();
148382
+ const stderrSnapshot = recentStderr.join("\n");
148383
+ const errorMessage = stderrSnapshot || stdoutSnapshot || `unknown error - no output from OpenCode CLI${errorContext}`;
148245
148384
  log.error(
148246
148385
  `${params.label} exited with code ${result.exitCode}${errorContext}: ${errorMessage}`
148247
148386
  );
148248
- log.debug(`stdout: ${result.stdout?.substring(0, 500)}`);
148249
- log.debug(`stderr: ${result.stderr?.substring(0, 500)}`);
148250
- return { success: false, output: finalOutput || output, error: errorMessage, usage };
148387
+ log.debug(`stdout: ${stdoutSnapshot.substring(0, 500)}`);
148388
+ log.debug(`stderr: ${stderrSnapshot.substring(0, 500)}`);
148389
+ return {
148390
+ success: false,
148391
+ output: finalOutput || stdoutSnapshot,
148392
+ error: errorMessage,
148393
+ usage
148394
+ };
148251
148395
  }
148252
148396
  if (eventCount === 0 && lastProviderError) {
148253
148397
  return {
148254
148398
  success: false,
148255
- output: finalOutput || output,
148399
+ output: finalOutput || output.toString(),
148256
148400
  error: `provider error: ${lastProviderError}`,
148257
148401
  usage
148258
148402
  };
@@ -148263,12 +148407,12 @@ ${stderrContext}`);
148263
148407
  const errorMessage = errorEvent.error?.data?.message || errorEvent.error?.name || JSON.stringify(errorEvent);
148264
148408
  return {
148265
148409
  success: false,
148266
- output: finalOutput || output,
148410
+ output: finalOutput || output.toString(),
148267
148411
  error: `${errorName}: ${errorMessage}`,
148268
148412
  usage
148269
148413
  };
148270
148414
  }
148271
- return { success: true, output: finalOutput || output, usage };
148415
+ return { success: true, output: finalOutput || output.toString(), usage };
148272
148416
  } catch (error49) {
148273
148417
  params.todoTracker?.cancel();
148274
148418
  const duration4 = performance7.now() - startTime;
@@ -148287,7 +148431,7 @@ ${stderrContext}`
148287
148431
  );
148288
148432
  return {
148289
148433
  success: false,
148290
- output: finalOutput || output,
148434
+ output: finalOutput || output.toString(),
148291
148435
  error: `${errorMessage} [${diagnosis}]`,
148292
148436
  usage: buildUsage()
148293
148437
  };
@@ -152193,6 +152337,14 @@ function isOIDCAvailable() {
152193
152337
  process.env.ACTIONS_ID_TOKEN_REQUEST_URL && process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN
152194
152338
  );
152195
152339
  }
152340
+ var TokenExchangeError = class extends Error {
152341
+ status;
152342
+ constructor(status, message) {
152343
+ super(message);
152344
+ this.name = "TokenExchangeError";
152345
+ this.status = status;
152346
+ }
152347
+ };
152196
152348
  async function acquireTokenViaOIDC(opts) {
152197
152349
  const oidcToken = await core2.getIDToken("pullfrog-api");
152198
152350
  const repos = [...opts?.repos ?? []];
@@ -152217,7 +152369,16 @@ async function acquireTokenViaOIDC(opts) {
152217
152369
  });
152218
152370
  clearTimeout(timeoutId);
152219
152371
  if (!tokenResponse.ok) {
152220
- throw new Error(`Token exchange failed: ${tokenResponse.status} ${tokenResponse.statusText}`);
152372
+ let serverMessage;
152373
+ try {
152374
+ const body = await tokenResponse.json();
152375
+ if (typeof body.error === "string") serverMessage = body.error;
152376
+ } catch {
152377
+ }
152378
+ throw new TokenExchangeError(
152379
+ tokenResponse.status,
152380
+ serverMessage ?? `Token exchange failed: ${tokenResponse.status} ${tokenResponse.statusText}`
152381
+ );
152221
152382
  }
152222
152383
  const tokenData = await tokenResponse.json();
152223
152384
  return tokenData.token;
@@ -152338,7 +152499,10 @@ async function acquireNewToken(opts) {
152338
152499
  if (isOIDCAvailable()) {
152339
152500
  return await retry(() => acquireTokenViaOIDC(opts), {
152340
152501
  label: "token exchange",
152341
- shouldRetry: (error49) => error49 instanceof Error && (error49.name === "AbortError" || error49.message.includes("fetch failed") || error49.message.includes("ECONNRESET") || error49.message.includes("ETIMEDOUT") || error49.message.includes("Token exchange failed"))
152502
+ shouldRetry: (error49) => {
152503
+ if (error49 instanceof TokenExchangeError) return error49.status >= 500 || error49.status === 429;
152504
+ return error49 instanceof Error && (error49.message.includes("timed out") || error49.message.includes("fetch failed") || error49.message.includes("ECONNRESET") || error49.message.includes("ETIMEDOUT"));
152505
+ }
152342
152506
  });
152343
152507
  } else {
152344
152508
  return await acquireTokenViaGitHubApp(opts);
@@ -152885,6 +153049,21 @@ ${getStandaloneModeInstructions(ctx.payload.event.trigger, t, ctx.outputSchema)}
152885
153049
 
152886
153050
  Trust the tools \u2014 do not repeatedly verify file contents or git status after operations. If a tool reports success, proceed to the next step. Only verify if you encounter an actual error. Exception: right before \`${t("push_branch")}\`, ensure the working tree is clean \u2014 that tool rejects dirty trees, and tests you ran earlier often leave untracked output.
152887
153051
 
153052
+ ### Parallel tool execution
153053
+
153054
+ For maximum efficiency, whenever you need to perform multiple independent operations, invoke all relevant tools simultaneously in a single assistant turn rather than sequentially. The dominant failure mode is grep \u2192 read \u2192 read \u2192 read \u2192 read across separate turns when one round trip would do. Always parallelize when calls are independent:
153055
+ - reading multiple files (especially after a grep returns candidates)
153056
+ - multiple greps with different patterns
153057
+ - glob + grep + read combos
153058
+ - listing multiple directories
153059
+ - inspecting multiple MCP tools or resources
153060
+
153061
+ Do NOT parallelize operations that depend on prior output (e.g. create a file then read it), or ordered stateful mutations. Edits are not parallelizable \u2014 sequence those normally.${ctx.agentId === "opencode" ? `
153062
+
153063
+ On OpenCode you also have a \`batch\` tool that bundles 1-25 independent calls into one wrapper call. Reach for it whenever you have >=2 independent calls. Native parallel tool_use and \`batch\` both achieve one round trip instead of N \u2014 use whichever your provider supports best.` : `
153064
+
153065
+ Emit multiple \`tool_use\` blocks in the same assistant message for independent calls \u2014 the runtime executes them concurrently. Do not wait for one tool result before issuing the next independent call.`}
153066
+
152888
153067
  ### Command execution
152889
153068
 
152890
153069
  Never use \`sleep\` to wait for commands to complete. Commands run synchronously \u2014 when the shell tool returns, the command has finished.
@@ -153011,10 +153190,22 @@ async function readLearningsFile(path3) {
153011
153190
  }
153012
153191
 
153013
153192
  // utils/normalizeEnv.ts
153014
- function maskValue(value2) {
153015
- if (value2 && typeof value2 === "string" && value2.trim().length > 0) {
153016
- console.log(`::add-mask::${value2}`);
153193
+ var core4 = __toESM(require_core(), 1);
153194
+ function sanitizeSecret(key, value2) {
153195
+ const trimmed = value2.trim();
153196
+ if (trimmed.length === 0) {
153197
+ log.warning(
153198
+ `\xBB ${key} is whitespace-only \u2014 leaving env var unchanged. check your secret value.`
153199
+ );
153200
+ return null;
153201
+ }
153202
+ if (trimmed !== value2) {
153203
+ log.warning(
153204
+ `\xBB stripped whitespace from ${key} (whitespace in secret values breaks GitHub Actions log masking)`
153205
+ );
153017
153206
  }
153207
+ core4.setSecret(trimmed);
153208
+ return trimmed;
153018
153209
  }
153019
153210
  function normalizeEnv() {
153020
153211
  const upperKeys = /* @__PURE__ */ new Map();
@@ -153025,11 +153216,6 @@ function normalizeEnv() {
153025
153216
  upperKeys.set(upper2, existing);
153026
153217
  }
153027
153218
  for (const [upperKey, keys] of upperKeys) {
153028
- if (isSensitiveEnvName(upperKey)) {
153029
- for (const key of keys) {
153030
- maskValue(process.env[key]);
153031
- }
153032
- }
153033
153219
  if (keys.length === 1) {
153034
153220
  const key = keys[0];
153035
153221
  if (key !== upperKey) {
@@ -153052,10 +153238,17 @@ function normalizeEnv() {
153052
153238
  }
153053
153239
  process.env[upperKey] = preferredValue;
153054
153240
  }
153241
+ for (const key of Object.keys(process.env)) {
153242
+ if (!isSensitiveEnvName(key)) continue;
153243
+ const value2 = process.env[key];
153244
+ if (typeof value2 !== "string" || value2.length === 0) continue;
153245
+ const sanitized = sanitizeSecret(key, value2);
153246
+ if (sanitized !== null) process.env[key] = sanitized;
153247
+ }
153055
153248
  }
153056
153249
 
153057
153250
  // utils/payload.ts
153058
- var core4 = __toESM(require_core(), 1);
153251
+ var core5 = __toESM(require_core(), 1);
153059
153252
  import { isAbsolute as isAbsolute2, resolve as resolve2 } from "node:path";
153060
153253
 
153061
153254
  // utils/versioning.ts
@@ -153119,7 +153312,7 @@ function resolveCwd(cwd) {
153119
153312
  return workspace ? resolve2(workspace, cwd) : cwd;
153120
153313
  }
153121
153314
  function resolvePromptInput() {
153122
- const prompt = core4.getInput("prompt", { required: true });
153315
+ const prompt = core5.getInput("prompt", { required: true });
153123
153316
  let parsed2;
153124
153317
  try {
153125
153318
  parsed2 = JSON.parse(prompt);
@@ -153135,11 +153328,11 @@ function resolvePromptInput() {
153135
153328
  }
153136
153329
  function resolveNonPromptInputs() {
153137
153330
  return Inputs.omit("prompt").assert({
153138
- model: core4.getInput("model") || void 0,
153139
- timeout: core4.getInput("timeout") || void 0,
153140
- cwd: core4.getInput("cwd") || void 0,
153141
- push: core4.getInput("push") || void 0,
153142
- shell: core4.getInput("shell") || void 0
153331
+ model: core5.getInput("model") || void 0,
153332
+ timeout: core5.getInput("timeout") || void 0,
153333
+ cwd: core5.getInput("cwd") || void 0,
153334
+ push: core5.getInput("push") || void 0,
153335
+ shell: core5.getInput("shell") || void 0
153143
153336
  });
153144
153337
  }
153145
153338
  var isPullfrog = (actor) => {
@@ -153354,8 +153547,7 @@ async function fetchRunContext(params) {
153354
153547
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
153355
153548
  try {
153356
153549
  const headers = {
153357
- Authorization: `Bearer ${params.token}`,
153358
- "Content-Type": "application/json"
153550
+ Authorization: `Bearer ${params.token}`
153359
153551
  };
153360
153552
  if (params.oidcToken) {
153361
153553
  headers["X-GitHub-OIDC-Token"] = params.oidcToken;
@@ -153396,13 +153588,13 @@ async function fetchRunContext(params) {
153396
153588
  }
153397
153589
 
153398
153590
  // utils/runContextData.ts
153399
- var core5 = __toESM(require_core(), 1);
153591
+ var core6 = __toESM(require_core(), 1);
153400
153592
  async function resolveRunContextData(params) {
153401
153593
  log.info(`\xBB running Pullfrog v${package_default.version}...`);
153402
153594
  const repoContext = parseRepoContext();
153403
153595
  let oidcToken;
153404
153596
  try {
153405
- oidcToken = await core5.getIDToken("pullfrog-api");
153597
+ oidcToken = await core6.getIDToken("pullfrog-api");
153406
153598
  } catch {
153407
153599
  }
153408
153600
  const [repoResponse, runContext] = await Promise.all([
@@ -153705,7 +153897,7 @@ async function resolveRun(params) {
153705
153897
 
153706
153898
  // main.ts
153707
153899
  function resolveOutputSchema() {
153708
- const raw2 = core6.getInput("output_schema");
153900
+ const raw2 = core7.getInput("output_schema");
153709
153901
  if (!raw2) return void 0;
153710
153902
  let parsed2;
153711
153903
  try {
@@ -153771,7 +153963,7 @@ function formatBillingErrorSummary(error49, owner) {
153771
153963
  return [
153772
153964
  "**Add a card to start using Pullfrog Router.**",
153773
153965
  "",
153774
- "Router proxies OpenRouter at raw cost \u2014 no platform markup, and your first $20 of usage is on us.",
153966
+ "Router proxies OpenRouter at raw cost \u2014 no platform markup. Add a card and we'll auto-reload your wallet so runs keep flowing.",
153775
153967
  "",
153776
153968
  `[Add a card \u2192](${billingConsoleUrl(owner, "model-access")})`
153777
153969
  ].join("\n");
@@ -153873,7 +154065,7 @@ async function buildProxyTokenHeaders(ctx) {
153873
154065
  if (ctx.oidcCredentials) {
153874
154066
  process.env.ACTIONS_ID_TOKEN_REQUEST_URL = ctx.oidcCredentials.requestUrl;
153875
154067
  process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = ctx.oidcCredentials.requestToken;
153876
- const oidcToken = await core6.getIDToken("pullfrog-api");
154068
+ const oidcToken = await core7.getIDToken("pullfrog-api");
153877
154069
  delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
153878
154070
  delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
153879
154071
  return { Authorization: `Bearer ${oidcToken}` };
@@ -153895,7 +154087,7 @@ async function resolveProxyModel(ctx) {
153895
154087
  const key = await mintProxyKey({ oidcCredentials: ctx.oidcCredentials, repo: ctx.repo });
153896
154088
  if (!key) return;
153897
154089
  process.env.OPENROUTER_API_KEY = key;
153898
- core6.setSecret(key);
154090
+ core7.setSecret(key);
153899
154091
  ctx.payload.proxyModel = ctx.proxyModel;
153900
154092
  const label = ctx.oss ? "oss" : "router";
153901
154093
  log.info(`\xBB proxy: ${label} \u2192 ${ctx.proxyModel}`);
@@ -153947,12 +154139,12 @@ async function persistLearnings(ctx) {
153947
154139
  });
153948
154140
  if (!response.ok) {
153949
154141
  const error49 = await response.text().catch(() => "(no body)");
153950
- log.debug(`learnings persist failed (${response.status}): ${error49}`);
154142
+ log.warning(`learnings persist failed (${response.status}): ${error49}`);
153951
154143
  return;
153952
154144
  }
153953
154145
  log.info("\xBB learnings updated");
153954
154146
  } catch (err) {
153955
- log.debug(`learnings persist failed: ${err instanceof Error ? err.message : String(err)}`);
154147
+ log.warning(`learnings persist failed: ${err instanceof Error ? err.message : String(err)}`);
153956
154148
  }
153957
154149
  }
153958
154150
  async function persistSummary(ctx) {
@@ -154007,8 +154199,8 @@ async function main() {
154007
154199
  if (runContext.dbSecrets) {
154008
154200
  for (const [key, value2] of Object.entries(runContext.dbSecrets)) {
154009
154201
  if (!process.env[key]) {
154010
- process.env[key] = value2;
154011
- core6.setSecret(value2);
154202
+ const sanitized = sanitizeSecret(key, value2);
154203
+ if (sanitized !== null) process.env[key] = sanitized;
154012
154204
  }
154013
154205
  }
154014
154206
  const count = Object.keys(runContext.dbSecrets).length;
@@ -154346,7 +154538,7 @@ ${instructions.user}` : null,
154346
154538
  }
154347
154539
  if (toolState.output) {
154348
154540
  log.info(`::pullfrog-output::${Buffer.from(toolState.output).toString("base64")}`);
154349
- core6.setOutput("result", toolState.output);
154541
+ core7.setOutput("result", toolState.output);
154350
154542
  }
154351
154543
  return await handleAgentResult({
154352
154544
  result,