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/cli.mjs CHANGED
@@ -19935,10 +19935,10 @@ var require_core = __commonJS({
19935
19935
  (0, command_1.issueCommand)("set-env", { name }, convertedVal);
19936
19936
  }
19937
19937
  exports.exportVariable = exportVariable;
19938
- function setSecret4(secret) {
19938
+ function setSecret5(secret) {
19939
19939
  (0, command_1.issueCommand)("add-mask", {}, secret);
19940
19940
  }
19941
- exports.setSecret = setSecret4;
19941
+ exports.setSecret = setSecret5;
19942
19942
  function addPath(inputPath) {
19943
19943
  const filePath = process.env["GITHUB_PATH"] || "";
19944
19944
  if (filePath) {
@@ -47954,7 +47954,7 @@ var require_core3 = __commonJS({
47954
47954
  Object.defineProperty(exports, "__esModule", { value: true });
47955
47955
  var id_1 = require_id();
47956
47956
  var ref_1 = require_ref();
47957
- var core8 = [
47957
+ var core9 = [
47958
47958
  "$schema",
47959
47959
  "$id",
47960
47960
  "$defs",
@@ -47964,7 +47964,7 @@ var require_core3 = __commonJS({
47964
47964
  id_1.default,
47965
47965
  ref_1.default
47966
47966
  ];
47967
- exports.default = core8;
47967
+ exports.default = core9;
47968
47968
  }
47969
47969
  });
47970
47970
 
@@ -99202,12 +99202,12 @@ var import_picocolors2 = __toESM(require_picocolors(), 1);
99202
99202
  import { basename as basename2 } from "node:path";
99203
99203
 
99204
99204
  // commands/gha.ts
99205
- var core7 = __toESM(require_core(), 1);
99205
+ var core8 = __toESM(require_core(), 1);
99206
99206
  var import_arg = __toESM(require_arg(), 1);
99207
99207
  import { dirname as dirname6 } from "node:path";
99208
99208
 
99209
99209
  // main.ts
99210
- var core6 = __toESM(require_core(), 1);
99210
+ var core7 = __toESM(require_core(), 1);
99211
99211
  import { existsSync as existsSync7, readdirSync } from "node:fs";
99212
99212
  import { readFile as readFile4 } from "node:fs/promises";
99213
99213
  import { join as join17 } from "node:path";
@@ -108238,8 +108238,7 @@ var providers = {
108238
108238
  "gpt-5-nano": {
108239
108239
  displayName: "GPT Nano",
108240
108240
  resolve: "opencode/gpt-5-nano",
108241
- envVars: [],
108242
- isFree: true
108241
+ openRouterResolve: "openrouter/openai/gpt-5-nano"
108243
108242
  },
108244
108243
  "mimo-v2-pro-free": {
108245
108244
  displayName: "MiMo V2 Pro",
@@ -108468,6 +108467,11 @@ async function apiFetch(options) {
108468
108467
  if (bypassSecret) {
108469
108468
  headers["x-vercel-protection-bypass"] = bypassSecret;
108470
108469
  }
108470
+ if (!options.body) {
108471
+ for (const key of Object.keys(headers)) {
108472
+ if (key.toLowerCase() === "content-type") delete headers[key];
108473
+ }
108474
+ }
108471
108475
  log.debug(`api fetch: ${options.method ?? "GET"} ${url4.pathname}`);
108472
108476
  const init = {
108473
108477
  method: options.method ?? "GET",
@@ -109156,8 +109160,11 @@ function sanitizeToolForGemini(tool2) {
109156
109160
  }
109157
109161
  function isGeminiRouted(ctx) {
109158
109162
  const effective = ctx.payload.proxyModel ?? ctx.resolvedModel ?? ctx.payload.model;
109159
- if (!effective) return false;
109160
- return effective.toLowerCase().includes("gemini");
109163
+ if (!effective) return true;
109164
+ const normalized = effective.toLowerCase();
109165
+ if (normalized.includes("gemini")) return true;
109166
+ if (!normalized.includes("/")) return true;
109167
+ return false;
109161
109168
  }
109162
109169
 
109163
109170
  // mcp/shared.ts
@@ -110018,12 +110025,41 @@ function installSignalHandler() {
110018
110025
  killTrackedChildren();
110019
110026
  });
110020
110027
  }
110028
+ var DEFAULT_MAX_RETAINED_BYTES = 8 * 1024 * 1024;
110029
+ var TailBuffer = class {
110030
+ // explicit field declarations rather than constructor parameter properties:
110031
+ // node's strip-only TS loader (used by action/test/run.ts in CI) rejects
110032
+ // `constructor(private readonly cap: number)` with ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX.
110033
+ cap;
110034
+ buffer = "";
110035
+ truncatedBytes = 0;
110036
+ constructor(cap) {
110037
+ this.cap = cap;
110038
+ }
110039
+ append(chunk) {
110040
+ if (this.cap <= 0) return;
110041
+ this.buffer += chunk;
110042
+ if (this.buffer.length > this.cap) {
110043
+ const drop = this.buffer.length - this.cap;
110044
+ this.truncatedBytes += drop;
110045
+ this.buffer = this.buffer.slice(drop);
110046
+ }
110047
+ }
110048
+ toString() {
110049
+ if (this.truncatedBytes === 0) return this.buffer;
110050
+ const mib = (this.truncatedBytes / 1024 / 1024).toFixed(1);
110051
+ return `... [${mib} MiB truncated by retain:tail cap] ...
110052
+ ${this.buffer}`;
110053
+ }
110054
+ };
110021
110055
  async function spawn(options) {
110022
110056
  const activityTimeoutMs = options.activityTimeout ?? DEFAULT_ACTIVITY_TIMEOUT_MS;
110023
110057
  installSignalHandler();
110024
110058
  const startTime = performance3.now();
110025
- let stdoutBuffer = "";
110026
- let stderrBuffer = "";
110059
+ const retain = options.retain ?? "tail";
110060
+ const cap = options.maxRetainedBytes ?? DEFAULT_MAX_RETAINED_BYTES;
110061
+ const stdoutBuffer = retain === "none" ? null : new TailBuffer(cap);
110062
+ const stderrBuffer = retain === "none" ? null : new TailBuffer(cap);
110027
110063
  const killGroup = options.killGroup ?? false;
110028
110064
  return new Promise((resolve3, reject) => {
110029
110065
  const child = nodeSpawn(options.cmd, options.args, {
@@ -110097,17 +110133,29 @@ async function spawn(options) {
110097
110133
  }
110098
110134
  if (child.stdout) {
110099
110135
  child.stdout.on("data", (data) => {
110100
- updateActivity();
110101
- const chunk = data.toString();
110102
- stdoutBuffer += chunk;
110103
- options.onStdout?.(chunk);
110136
+ try {
110137
+ updateActivity();
110138
+ const chunk = data.toString();
110139
+ stdoutBuffer?.append(chunk);
110140
+ options.onStdout?.(chunk);
110141
+ } catch (err) {
110142
+ log.debug(
110143
+ `spawn stdout handler threw: ${err instanceof Error ? err.message : String(err)}`
110144
+ );
110145
+ }
110104
110146
  });
110105
110147
  }
110106
110148
  if (child.stderr) {
110107
110149
  child.stderr.on("data", (data) => {
110108
- const chunk = data.toString();
110109
- stderrBuffer += chunk;
110110
- options.onStderr?.(chunk);
110150
+ try {
110151
+ const chunk = data.toString();
110152
+ stderrBuffer?.append(chunk);
110153
+ options.onStderr?.(chunk);
110154
+ } catch (err) {
110155
+ log.debug(
110156
+ `spawn stderr handler threw: ${err instanceof Error ? err.message : String(err)}`
110157
+ );
110158
+ }
110111
110159
  });
110112
110160
  }
110113
110161
  child.on("close", (exitCode, signal) => {
@@ -110134,7 +110182,7 @@ async function spawn(options) {
110134
110182
  return;
110135
110183
  }
110136
110184
  let resolvedExitCode = exitCode ?? 0;
110137
- let resolvedStderr = stderrBuffer;
110185
+ let resolvedStderr = stderrBuffer?.toString() ?? "";
110138
110186
  if (exitCode === null && signal) {
110139
110187
  const killMsg = `[spawn] ${options.cmd}: killed by signal ${signal}`;
110140
110188
  resolvedStderr = resolvedStderr ? `${resolvedStderr}
@@ -110142,7 +110190,7 @@ ${killMsg}` : killMsg;
110142
110190
  resolvedExitCode = 1;
110143
110191
  }
110144
110192
  resolve3({
110145
- stdout: stdoutBuffer,
110193
+ stdout: stdoutBuffer?.toString() ?? "",
110146
110194
  stderr: resolvedStderr,
110147
110195
  exitCode: resolvedExitCode,
110148
110196
  durationMs
@@ -110156,11 +110204,12 @@ ${killMsg}` : killMsg;
110156
110204
  if (activityCheckIntervalId) clearInterval(activityCheckIntervalId);
110157
110205
  const errMsg = `[spawn] ${options.cmd}: ${error49.message}`;
110158
110206
  console.error(errMsg);
110159
- stderrBuffer = stderrBuffer ? `${stderrBuffer}
110207
+ const existingStderr = stderrBuffer?.toString() ?? "";
110208
+ const finalStderr = existingStderr ? `${existingStderr}
110160
110209
  ${errMsg}` : errMsg;
110161
110210
  resolve3({
110162
- stdout: stdoutBuffer,
110163
- stderr: stderrBuffer,
110211
+ stdout: stdoutBuffer?.toString() ?? "",
110212
+ stderr: finalStderr,
110164
110213
  exitCode: 1,
110165
110214
  durationMs
110166
110215
  });
@@ -138069,7 +138118,7 @@ var require_core4 = /* @__PURE__ */ __commonJSMin(((exports) => {
138069
138118
  Object.defineProperty(exports, "__esModule", { value: true });
138070
138119
  const id_1 = require_id2();
138071
138120
  const ref_1 = require_ref2();
138072
- const core8 = [
138121
+ const core9 = [
138073
138122
  "$schema",
138074
138123
  "$id",
138075
138124
  "$defs",
@@ -138079,7 +138128,7 @@ var require_core4 = /* @__PURE__ */ __commonJSMin(((exports) => {
138079
138128
  id_1.default,
138080
138129
  ref_1.default
138081
138130
  ];
138082
- exports.default = core8;
138131
+ exports.default = core9;
138083
138132
  }));
138084
138133
  var require_limitNumber2 = /* @__PURE__ */ __commonJSMin(((exports) => {
138085
138134
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -142589,7 +142638,7 @@ var import_semver = __toESM(require_semver2(), 1);
142589
142638
  // package.json
142590
142639
  var package_default = {
142591
142640
  name: "pullfrog",
142592
- version: "0.1.4",
142641
+ version: "0.1.6",
142593
142642
  type: "module",
142594
142643
  bin: {
142595
142644
  pullfrog: "dist/cli.mjs",
@@ -146666,6 +146715,8 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
146666
146715
  - **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)
146667
146716
  - **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.
146668
146717
 
146718
+ **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.
146719
+
146669
146720
  lenses come in two flavors, and you can mix them:
146670
146721
  - **themed lenses** \u2014 a perspective applied across the whole diff (correctness, security, user-journey, performance, etc.).
146671
146722
  - **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.
@@ -146673,7 +146724,7 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
146673
146724
  starter menu (combine, omit, or invent your own):
146674
146725
  - **correctness & invariants** \u2014 bugs, races, error handling, edge cases, state-machine boundaries
146675
146726
  - **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
146676
- - **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.
146727
+ - **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.
146677
146728
  - **security** \u2014 new endpoints, authZ, input validation, secrets handling, replay/CSRF/injection, cross-tenant isolation
146678
146729
  - **user-journey** \u2014 UX-touching flows: walk through happy path and failure modes as a user
146679
146730
  - **operational readiness** \u2014 observability, alerting, migrations (forward + rollback), feature flags, on-call burden
@@ -146762,7 +146813,7 @@ ${PR_SUMMARY_FORMAT}`
146762
146813
  "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.
146763
146814
  When unsure, treat as non-trivial.
146764
146815
 
146765
- 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.
146816
+ 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.
146766
146817
 
146767
146818
  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:
146768
146819
  - 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
@@ -147132,20 +147183,30 @@ var ThinkingTimer = class {
147132
147183
  maximumFractionDigits: 1
147133
147184
  });
147134
147185
  lastToolResultTimestamp = null;
147186
+ formatLine;
147187
+ // node's native TS strip-only mode does not support parameter properties,
147188
+ // so the formatter is declared as a field and assigned in the body.
147189
+ constructor(formatLine = (l) => l) {
147190
+ this.formatLine = formatLine;
147191
+ }
147135
147192
  markToolResult() {
147136
147193
  this.lastToolResultTimestamp = performance5.now();
147137
- log.debug(`\xBB thinking timer: markToolResult at ${this.lastToolResultTimestamp}`);
147194
+ log.debug(
147195
+ this.formatLine(`\xBB thinking timer: markToolResult at ${this.lastToolResultTimestamp}`)
147196
+ );
147138
147197
  }
147139
147198
  markToolCall() {
147140
147199
  const now = performance5.now();
147141
147200
  log.debug(
147142
- `\xBB thinking timer: markToolCall at ${now}, lastToolResult=${this.lastToolResultTimestamp}`
147201
+ this.formatLine(
147202
+ `\xBB thinking timer: markToolCall at ${now}, lastToolResult=${this.lastToolResultTimestamp}`
147203
+ )
147143
147204
  );
147144
147205
  if (this.lastToolResultTimestamp === null) return;
147145
147206
  const elapsed = now - this.lastToolResultTimestamp;
147146
147207
  if (elapsed < THINKING_THRESHOLD) return;
147147
147208
  const seconds = elapsed / 1e3;
147148
- log.info(`\xBB thought for ${this.durationFormatter.format(seconds)}`);
147209
+ log.info(this.formatLine(`\xBB thought for ${this.durationFormatter.format(seconds)}`));
147149
147210
  }
147150
147211
  };
147151
147212
 
@@ -147394,19 +147455,39 @@ function deriveLabelFromTaskInput(input) {
147394
147455
  }
147395
147456
  var SessionLabeler = class {
147396
147457
  labels = /* @__PURE__ */ new Map();
147458
+ labelsByToolUseId = /* @__PURE__ */ new Map();
147397
147459
  pendingLabels = [];
147398
147460
  fallbackCounter = 0;
147399
- recordTaskDispatch(input) {
147461
+ /**
147462
+ * Record a Task/Agent tool dispatch.
147463
+ *
147464
+ * @param input Task tool input — used to derive the lens label.
147465
+ * @param toolUseId Optional Agent tool_use id. When provided, future events
147466
+ * carrying `parent_tool_use_id === toolUseId` resolve
147467
+ * directly to this label without consuming the FIFO queue
147468
+ * (Claude path). Always also pushed to the FIFO queue so
147469
+ * the OpenCode path still works when toolUseId is absent.
147470
+ */
147471
+ recordTaskDispatch(input, toolUseId) {
147400
147472
  const label = deriveLabelFromTaskInput(input);
147401
147473
  this.pendingLabels.push(label);
147474
+ if (toolUseId) this.labelsByToolUseId.set(toolUseId, label);
147402
147475
  return label;
147403
147476
  }
147404
147477
  /**
147405
- * Return a label for the given sessionID. Binds on first call.
147406
- * Pass undefined/empty for events that lack a session id — the caller
147407
- * gets ORCHESTRATOR_LABEL so the line is still attributable.
147478
+ * Return a label for the given event.
147479
+ *
147480
+ * @param sessionID Session id from the event (OpenCode: per-session;
147481
+ * Claude: shared across orchestrator + subagents).
147482
+ * @param parentToolUseId Claude's `parent_tool_use_id` — non-null on
147483
+ * subagent messages. When set and known, takes
147484
+ * priority over the FIFO/sessionID path.
147408
147485
  */
147409
- labelFor(sessionID) {
147486
+ labelFor(sessionID, parentToolUseId) {
147487
+ if (parentToolUseId) {
147488
+ const direct = this.labelsByToolUseId.get(parentToolUseId);
147489
+ if (direct) return direct;
147490
+ }
147410
147491
  if (!sessionID) return ORCHESTRATOR_LABEL;
147411
147492
  const existing = this.labels.get(sessionID);
147412
147493
  if (existing) return existing;
@@ -147478,8 +147559,7 @@ function stripProviderPrefix(specifier) {
147478
147559
  const slashIndex = specifier.indexOf("/");
147479
147560
  return slashIndex > 0 ? specifier.slice(slashIndex + 1) : specifier;
147480
147561
  }
147481
- function resolveEffort(model) {
147482
- if (model?.includes("opus")) return "max";
147562
+ function resolveEffort(_model) {
147483
147563
  return "high";
147484
147564
  }
147485
147565
  function tailLines(text, maxCodeUnits) {
@@ -147491,7 +147571,23 @@ function tailLines(text, maxCodeUnits) {
147491
147571
  async function runClaude(params) {
147492
147572
  const startTime = performance6.now();
147493
147573
  let eventCount = 0;
147494
- const thinkingTimer = new ThinkingTimer();
147574
+ const labeler = new SessionLabeler();
147575
+ function eventLabel(event) {
147576
+ return labeler.labelFor(event.session_id ?? null, event.parent_tool_use_id ?? null);
147577
+ }
147578
+ function withLabel(label, message) {
147579
+ return label === ORCHESTRATOR_LABEL ? message : formatWithLabel(label, message);
147580
+ }
147581
+ const thinkingTimers = /* @__PURE__ */ new Map();
147582
+ function timerFor(label) {
147583
+ let t2 = thinkingTimers.get(label);
147584
+ if (!t2) {
147585
+ const formatLine = (line) => label === ORCHESTRATOR_LABEL ? line : formatWithLabel(label, line);
147586
+ t2 = new ThinkingTimer(formatLine);
147587
+ thinkingTimers.set(label, t2);
147588
+ }
147589
+ return t2;
147590
+ }
147495
147591
  let finalOutput = "";
147496
147592
  let sessionId;
147497
147593
  let resultErrorSubtype = null;
@@ -147512,17 +147608,22 @@ async function runClaude(params) {
147512
147608
  } : void 0;
147513
147609
  }
147514
147610
  const handlers2 = {
147515
- system: (_event) => {
147516
- log.debug(`\xBB ${params.label} system event`);
147611
+ system: (event) => {
147612
+ const label = eventLabel(event);
147613
+ log.debug(withLabel(label, `\xBB ${params.label} system event`));
147517
147614
  },
147518
147615
  assistant: (event) => {
147519
147616
  const content = event.message?.content;
147520
147617
  if (!content) return;
147618
+ const label = eventLabel(event);
147619
+ const boxTitle = label === ORCHESTRATOR_LABEL ? params.label : `${params.label} [${label}]`;
147521
147620
  for (const block of content) {
147522
147621
  if (block.type === "text" && block.text?.trim()) {
147523
147622
  const message = block.text.trim();
147524
- log.box(message, { title: params.label });
147525
- finalOutput = message;
147623
+ log.box(message, { title: boxTitle });
147624
+ if (label === ORCHESTRATOR_LABEL) {
147625
+ finalOutput = message;
147626
+ }
147526
147627
  } else if (block.type === "tool_use") {
147527
147628
  const toolName = block.name || "unknown";
147528
147629
  if (params.onToolUse) {
@@ -147531,20 +147632,25 @@ async function runClaude(params) {
147531
147632
  input: block.input
147532
147633
  });
147533
147634
  }
147534
- thinkingTimer.markToolCall();
147535
- log.toolCall({ toolName, input: block.input || {} });
147536
- if (toolName === "Task" && block.input && typeof block.input === "object") {
147635
+ timerFor(label).markToolCall();
147636
+ const inputFormatted = formatJsonValue(block.input || {});
147637
+ const toolCallLine = inputFormatted !== "{}" ? `\xBB ${toolName}(${inputFormatted})` : `\xBB ${toolName}()`;
147638
+ log.info(withLabel(label, toolCallLine));
147639
+ if ((toolName === "Task" || toolName === "Agent") && block.input && typeof block.input === "object") {
147537
147640
  const taskInput = block.input;
147538
- const label = deriveLabelFromTaskInput(taskInput);
147641
+ const dispatchedLabel = labeler.recordTaskDispatch(taskInput, block.id ?? null);
147539
147642
  log.info(
147540
- `\xBB dispatching subagent: ${label}` + (taskInput.subagent_type ? ` (subagent_type=${taskInput.subagent_type})` : "")
147643
+ withLabel(
147644
+ label,
147645
+ `\xBB dispatching subagent: ${dispatchedLabel}` + (taskInput.subagent_type ? ` (subagent_type=${taskInput.subagent_type})` : "")
147646
+ )
147541
147647
  );
147542
147648
  }
147543
147649
  if (toolName.includes("report_progress") && params.todoTracker) {
147544
147650
  log.debug("\xBB report_progress detected, disabling todo tracking");
147545
147651
  params.todoTracker.cancel();
147546
147652
  }
147547
- if (toolName === "TodoWrite" && params.todoTracker?.enabled) {
147653
+ if (toolName === "TodoWrite" && params.todoTracker?.enabled && label === ORCHESTRATOR_LABEL) {
147548
147654
  params.todoTracker.update(block.input);
147549
147655
  }
147550
147656
  }
@@ -147560,17 +147666,18 @@ async function runClaude(params) {
147560
147666
  user: (event) => {
147561
147667
  const content = event.message?.content;
147562
147668
  if (!content) return;
147669
+ const label = eventLabel(event);
147563
147670
  for (const block of content) {
147564
147671
  if (typeof block === "string") continue;
147565
147672
  if (block.type === "tool_result") {
147566
- thinkingTimer.markToolResult();
147673
+ timerFor(label).markToolResult();
147567
147674
  const outputContent = typeof block.content === "string" ? block.content : Array.isArray(block.content) ? block.content.map(
147568
147675
  (entry) => typeof entry === "string" ? entry : typeof entry === "object" && entry !== null && "text" in entry ? String(entry.text) : JSON.stringify(entry)
147569
147676
  ).join("\n") : String(block.content);
147570
147677
  if (block.is_error) {
147571
- log.info(`\xBB tool error: ${outputContent}`);
147678
+ log.info(withLabel(label, `\xBB tool error: ${outputContent}`));
147572
147679
  } else {
147573
- log.debug(`\xBB tool output: ${outputContent}`);
147680
+ log.debug(withLabel(label, `\xBB tool output: ${outputContent}`));
147574
147681
  }
147575
147682
  }
147576
147683
  }
@@ -147640,7 +147747,7 @@ async function runClaude(params) {
147640
147747
  };
147641
147748
  const recentStderr = [];
147642
147749
  let lastProviderError = null;
147643
- let output = "";
147750
+ const output = new TailBuffer(DEFAULT_MAX_RETAINED_BYTES);
147644
147751
  let stdoutBuffer = "";
147645
147752
  try {
147646
147753
  const result = await spawn({
@@ -147657,9 +147764,14 @@ async function runClaude(params) {
147657
147764
  // there's no shim-orphan issue like opencode-ai/bin/opencode, but
147658
147765
  // detached + killGroup is the right default for any agent runtime.
147659
147766
  killGroup: true,
147767
+ // claude already drains every chunk via onStdout (NDJSON parsing) and
147768
+ // onStderr (recentStderr ring buffer). retaining a second copy in the
147769
+ // spawn wrapper would grow unbounded for long sessions and previously
147770
+ // crashed the wrapper with RangeError. see issue #680.
147771
+ retain: "none",
147660
147772
  onStdout: async (chunk) => {
147661
147773
  const text = chunk.toString();
147662
- output += text;
147774
+ output.append(text);
147663
147775
  markActivity();
147664
147776
  stdoutBuffer += text;
147665
147777
  const lines = stdoutBuffer.split("\n");
@@ -147734,16 +147846,18 @@ ${stderrContext}`);
147734
147846
  const usage = buildUsage();
147735
147847
  if (result.exitCode !== 0) {
147736
147848
  const errorContext = lastProviderError ? ` (${lastProviderError})` : "";
147737
- const truncatedStdout = result.stdout ? tailLines(result.stdout, 2048) : "";
147738
- const errorMessage = lastResultError || result.stderr || truncatedStdout || `unknown error - no output from Claude CLI${errorContext}`;
147849
+ const stdoutSnapshot = output.toString();
147850
+ const stderrSnapshot = recentStderr.join("\n");
147851
+ const truncatedStdout = stdoutSnapshot ? tailLines(stdoutSnapshot, 2048) : "";
147852
+ const errorMessage = lastResultError || stderrSnapshot || truncatedStdout || `unknown error - no output from Claude CLI${errorContext}`;
147739
147853
  log.error(
147740
147854
  `${params.label} exited with code ${result.exitCode}${errorContext}: ${errorMessage}`
147741
147855
  );
147742
- log.debug(`stdout: ${result.stdout?.substring(0, 500)}`);
147743
- log.debug(`stderr: ${result.stderr?.substring(0, 500)}`);
147856
+ log.debug(`stdout: ${stdoutSnapshot.substring(0, 500)}`);
147857
+ log.debug(`stderr: ${stderrSnapshot.substring(0, 500)}`);
147744
147858
  return {
147745
147859
  success: false,
147746
- output: finalOutput || output,
147860
+ output: finalOutput || stdoutSnapshot,
147747
147861
  error: errorMessage,
147748
147862
  usage,
147749
147863
  sessionId
@@ -147752,7 +147866,7 @@ ${stderrContext}`);
147752
147866
  if (eventCount === 0 && lastProviderError) {
147753
147867
  return {
147754
147868
  success: false,
147755
- output: finalOutput || output,
147869
+ output: finalOutput || output.toString(),
147756
147870
  error: `provider error: ${lastProviderError}`,
147757
147871
  usage,
147758
147872
  sessionId
@@ -147761,13 +147875,13 @@ ${stderrContext}`);
147761
147875
  if (resultErrorSubtype) {
147762
147876
  return {
147763
147877
  success: false,
147764
- output: finalOutput || output,
147878
+ output: finalOutput || output.toString(),
147765
147879
  error: lastResultError || `result subtype: ${resultErrorSubtype}`,
147766
147880
  usage,
147767
147881
  sessionId
147768
147882
  };
147769
147883
  }
147770
- return { success: true, output: finalOutput || output, usage, sessionId };
147884
+ return { success: true, output: finalOutput || output.toString(), usage, sessionId };
147771
147885
  } catch (error49) {
147772
147886
  params.todoTracker?.cancel();
147773
147887
  const duration4 = performance6.now() - startTime;
@@ -147786,7 +147900,7 @@ ${stderrContext}`
147786
147900
  );
147787
147901
  return {
147788
147902
  success: false,
147789
- output: finalOutput || output,
147903
+ output: finalOutput || output.toString(),
147790
147904
  error: `${errorMessage} [${diagnosis}]`,
147791
147905
  usage: buildUsage(),
147792
147906
  sessionId
@@ -148022,6 +148136,15 @@ function buildSecurityConfig(ctx, model) {
148022
148136
  [pullfrogMcpName]: { type: "remote", url: ctx.mcpServerUrl }
148023
148137
  },
148024
148138
  agent: buildReviewerAgentConfig(),
148139
+ // opt into opencode's experimental `batch` tool (added in
148140
+ // anomalyco/opencode PR #2983, opt-in via `experimental.batch_tool`). it
148141
+ // exposes a single `batch` tool that runs 1-25 independent tool calls
148142
+ // (read/grep/glob/bash/etc.) concurrently in one assistant turn, which
148143
+ // collapses the dominant grep→20×read pattern into a single round trip.
148144
+ // edits are explicitly disallowed inside the batch upstream. paired with
148145
+ // the "Parallel tool execution" guidance in utils/instructions.ts so the
148146
+ // model actually reaches for it. see wiki/prompt.md.
148147
+ experimental: { batch_tool: true },
148025
148148
  provider: {
148026
148149
  google: {
148027
148150
  models: Object.fromEntries(
@@ -148094,7 +148217,6 @@ function autoSelectModel(cliPath) {
148094
148217
  async function runOpenCode(params) {
148095
148218
  const startTime = performance7.now();
148096
148219
  let eventCount = 0;
148097
- const thinkingTimer = new ThinkingTimer();
148098
148220
  let finalOutput = "";
148099
148221
  let accumulatedTokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
148100
148222
  let accumulatedCostUsd = 0;
@@ -148111,6 +148233,16 @@ async function runOpenCode(params) {
148111
148233
  function withLabel(label, message) {
148112
148234
  return label === ORCHESTRATOR_LABEL ? message : formatWithLabel(label, message);
148113
148235
  }
148236
+ const thinkingTimers = /* @__PURE__ */ new Map();
148237
+ function timerFor(label) {
148238
+ let t2 = thinkingTimers.get(label);
148239
+ if (!t2) {
148240
+ const formatLine = (line) => label === ORCHESTRATOR_LABEL ? line : formatWithLabel(label, line);
148241
+ t2 = new ThinkingTimer(formatLine);
148242
+ thinkingTimers.set(label, t2);
148243
+ }
148244
+ return t2;
148245
+ }
148114
148246
  const taskDispatchByCallID = /* @__PURE__ */ new Map();
148115
148247
  const pendingTaskDispatches = [];
148116
148248
  const knownNonTaskCallIDs = /* @__PURE__ */ new Set();
@@ -148259,7 +148391,7 @@ async function runOpenCode(params) {
148259
148391
  input: event.part?.state?.input
148260
148392
  });
148261
148393
  }
148262
- thinkingTimer.markToolCall();
148394
+ timerFor(label).markToolCall();
148263
148395
  const inputFormatted = formatJsonValue(event.part?.state?.input || {});
148264
148396
  const toolCallLine = inputFormatted !== "{}" ? `\xBB ${toolName}(${inputFormatted})` : `\xBB ${toolName}()`;
148265
148397
  log.info(withLabel(label, toolCallLine));
@@ -148283,7 +148415,7 @@ async function runOpenCode(params) {
148283
148415
  const status = event.part?.state?.status || event.status || "unknown";
148284
148416
  const output2 = event.part?.state?.output || event.output;
148285
148417
  const label = eventLabel(event);
148286
- thinkingTimer.markToolResult();
148418
+ timerFor(label).markToolResult();
148287
148419
  if (taskDispatchByCallID.size > 0 || pendingTaskDispatches.length > 0) {
148288
148420
  if (toolId && taskDispatchByCallID.has(toolId)) {
148289
148421
  const dispatch = taskDispatchByCallID.get(toolId);
@@ -148408,7 +148540,7 @@ async function runOpenCode(params) {
148408
148540
  const recentStderr = [];
148409
148541
  let lastProviderError = null;
148410
148542
  let agentErrorEvent = null;
148411
- let output = "";
148543
+ const output = new TailBuffer(DEFAULT_MAX_RETAINED_BYTES);
148412
148544
  let stdoutBuffer = "";
148413
148545
  try {
148414
148546
  const result = await spawn({
@@ -148426,6 +148558,11 @@ async function runOpenCode(params) {
148426
148558
  // never fires — producing zombie runs. detached + killGroup nukes the
148427
148559
  // whole tree.
148428
148560
  killGroup: true,
148561
+ // we already drain every chunk via onStdout/onStderr (NDJSON parsing
148562
+ // + recentStderr ring buffer). retaining a second copy in the spawn
148563
+ // wrapper would grow unbounded for multi-lens Reviews and previously
148564
+ // crashed the wrapper with RangeError at ~1 GiB. see issue #680.
148565
+ retain: "none",
148429
148566
  // NB: we used to pass `isPausedExternally: isSubagentInFlight` to suspend
148430
148567
  // the activity timer during subagent dispatches. unnecessary now that
148431
148568
  // our injected plugin (action/agents/opencodePlugin.ts) re-emits
@@ -148435,7 +148572,7 @@ async function runOpenCode(params) {
148435
148572
  // (~3.3 plugin events/sec during a typical subagent run).
148436
148573
  onStdout: async (chunk) => {
148437
148574
  const text = chunk.toString();
148438
- output += text;
148575
+ output.append(text);
148439
148576
  markActivity();
148440
148577
  stdoutBuffer += text;
148441
148578
  const lines = stdoutBuffer.split("\n");
@@ -148524,18 +148661,25 @@ ${stderrContext}`);
148524
148661
  const usage = buildUsage();
148525
148662
  if (result.exitCode !== 0) {
148526
148663
  const errorContext = lastProviderError ? ` (${lastProviderError})` : "";
148527
- const errorMessage = result.stderr || result.stdout || `unknown error - no output from OpenCode CLI${errorContext}`;
148664
+ const stdoutSnapshot = output.toString();
148665
+ const stderrSnapshot = recentStderr.join("\n");
148666
+ const errorMessage = stderrSnapshot || stdoutSnapshot || `unknown error - no output from OpenCode CLI${errorContext}`;
148528
148667
  log.error(
148529
148668
  `${params.label} exited with code ${result.exitCode}${errorContext}: ${errorMessage}`
148530
148669
  );
148531
- log.debug(`stdout: ${result.stdout?.substring(0, 500)}`);
148532
- log.debug(`stderr: ${result.stderr?.substring(0, 500)}`);
148533
- return { success: false, output: finalOutput || output, error: errorMessage, usage };
148670
+ log.debug(`stdout: ${stdoutSnapshot.substring(0, 500)}`);
148671
+ log.debug(`stderr: ${stderrSnapshot.substring(0, 500)}`);
148672
+ return {
148673
+ success: false,
148674
+ output: finalOutput || stdoutSnapshot,
148675
+ error: errorMessage,
148676
+ usage
148677
+ };
148534
148678
  }
148535
148679
  if (eventCount === 0 && lastProviderError) {
148536
148680
  return {
148537
148681
  success: false,
148538
- output: finalOutput || output,
148682
+ output: finalOutput || output.toString(),
148539
148683
  error: `provider error: ${lastProviderError}`,
148540
148684
  usage
148541
148685
  };
@@ -148546,12 +148690,12 @@ ${stderrContext}`);
148546
148690
  const errorMessage = errorEvent.error?.data?.message || errorEvent.error?.name || JSON.stringify(errorEvent);
148547
148691
  return {
148548
148692
  success: false,
148549
- output: finalOutput || output,
148693
+ output: finalOutput || output.toString(),
148550
148694
  error: `${errorName}: ${errorMessage}`,
148551
148695
  usage
148552
148696
  };
148553
148697
  }
148554
- return { success: true, output: finalOutput || output, usage };
148698
+ return { success: true, output: finalOutput || output.toString(), usage };
148555
148699
  } catch (error49) {
148556
148700
  params.todoTracker?.cancel();
148557
148701
  const duration4 = performance7.now() - startTime;
@@ -148570,7 +148714,7 @@ ${stderrContext}`
148570
148714
  );
148571
148715
  return {
148572
148716
  success: false,
148573
- output: finalOutput || output,
148717
+ output: finalOutput || output.toString(),
148574
148718
  error: `${errorMessage} [${diagnosis}]`,
148575
148719
  usage: buildUsage()
148576
148720
  };
@@ -152476,6 +152620,14 @@ function isOIDCAvailable() {
152476
152620
  process.env.ACTIONS_ID_TOKEN_REQUEST_URL && process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN
152477
152621
  );
152478
152622
  }
152623
+ var TokenExchangeError = class extends Error {
152624
+ status;
152625
+ constructor(status, message) {
152626
+ super(message);
152627
+ this.name = "TokenExchangeError";
152628
+ this.status = status;
152629
+ }
152630
+ };
152479
152631
  async function acquireTokenViaOIDC(opts) {
152480
152632
  const oidcToken = await core2.getIDToken("pullfrog-api");
152481
152633
  const repos = [...opts?.repos ?? []];
@@ -152500,7 +152652,16 @@ async function acquireTokenViaOIDC(opts) {
152500
152652
  });
152501
152653
  clearTimeout(timeoutId);
152502
152654
  if (!tokenResponse.ok) {
152503
- throw new Error(`Token exchange failed: ${tokenResponse.status} ${tokenResponse.statusText}`);
152655
+ let serverMessage;
152656
+ try {
152657
+ const body = await tokenResponse.json();
152658
+ if (typeof body.error === "string") serverMessage = body.error;
152659
+ } catch {
152660
+ }
152661
+ throw new TokenExchangeError(
152662
+ tokenResponse.status,
152663
+ serverMessage ?? `Token exchange failed: ${tokenResponse.status} ${tokenResponse.statusText}`
152664
+ );
152504
152665
  }
152505
152666
  const tokenData = await tokenResponse.json();
152506
152667
  return tokenData.token;
@@ -152621,7 +152782,10 @@ async function acquireNewToken(opts) {
152621
152782
  if (isOIDCAvailable()) {
152622
152783
  return await retry(() => acquireTokenViaOIDC(opts), {
152623
152784
  label: "token exchange",
152624
- 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"))
152785
+ shouldRetry: (error49) => {
152786
+ if (error49 instanceof TokenExchangeError) return error49.status >= 500 || error49.status === 429;
152787
+ return error49 instanceof Error && (error49.message.includes("timed out") || error49.message.includes("fetch failed") || error49.message.includes("ECONNRESET") || error49.message.includes("ETIMEDOUT"));
152788
+ }
152625
152789
  });
152626
152790
  } else {
152627
152791
  return await acquireTokenViaGitHubApp(opts);
@@ -153168,6 +153332,21 @@ ${getStandaloneModeInstructions(ctx.payload.event.trigger, t2, ctx.outputSchema)
153168
153332
 
153169
153333
  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 \`${t2("push_branch")}\`, ensure the working tree is clean \u2014 that tool rejects dirty trees, and tests you ran earlier often leave untracked output.
153170
153334
 
153335
+ ### Parallel tool execution
153336
+
153337
+ 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:
153338
+ - reading multiple files (especially after a grep returns candidates)
153339
+ - multiple greps with different patterns
153340
+ - glob + grep + read combos
153341
+ - listing multiple directories
153342
+ - inspecting multiple MCP tools or resources
153343
+
153344
+ 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" ? `
153345
+
153346
+ 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.` : `
153347
+
153348
+ 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.`}
153349
+
153171
153350
  ### Command execution
153172
153351
 
153173
153352
  Never use \`sleep\` to wait for commands to complete. Commands run synchronously \u2014 when the shell tool returns, the command has finished.
@@ -153294,10 +153473,22 @@ async function readLearningsFile(path3) {
153294
153473
  }
153295
153474
 
153296
153475
  // utils/normalizeEnv.ts
153297
- function maskValue(value2) {
153298
- if (value2 && typeof value2 === "string" && value2.trim().length > 0) {
153299
- console.log(`::add-mask::${value2}`);
153476
+ var core4 = __toESM(require_core(), 1);
153477
+ function sanitizeSecret(key, value2) {
153478
+ const trimmed = value2.trim();
153479
+ if (trimmed.length === 0) {
153480
+ log.warning(
153481
+ `\xBB ${key} is whitespace-only \u2014 leaving env var unchanged. check your secret value.`
153482
+ );
153483
+ return null;
153484
+ }
153485
+ if (trimmed !== value2) {
153486
+ log.warning(
153487
+ `\xBB stripped whitespace from ${key} (whitespace in secret values breaks GitHub Actions log masking)`
153488
+ );
153300
153489
  }
153490
+ core4.setSecret(trimmed);
153491
+ return trimmed;
153301
153492
  }
153302
153493
  function normalizeEnv() {
153303
153494
  const upperKeys = /* @__PURE__ */ new Map();
@@ -153308,11 +153499,6 @@ function normalizeEnv() {
153308
153499
  upperKeys.set(upper2, existing);
153309
153500
  }
153310
153501
  for (const [upperKey, keys] of upperKeys) {
153311
- if (isSensitiveEnvName(upperKey)) {
153312
- for (const key of keys) {
153313
- maskValue(process.env[key]);
153314
- }
153315
- }
153316
153502
  if (keys.length === 1) {
153317
153503
  const key = keys[0];
153318
153504
  if (key !== upperKey) {
@@ -153335,10 +153521,17 @@ function normalizeEnv() {
153335
153521
  }
153336
153522
  process.env[upperKey] = preferredValue;
153337
153523
  }
153524
+ for (const key of Object.keys(process.env)) {
153525
+ if (!isSensitiveEnvName(key)) continue;
153526
+ const value2 = process.env[key];
153527
+ if (typeof value2 !== "string" || value2.length === 0) continue;
153528
+ const sanitized = sanitizeSecret(key, value2);
153529
+ if (sanitized !== null) process.env[key] = sanitized;
153530
+ }
153338
153531
  }
153339
153532
 
153340
153533
  // utils/payload.ts
153341
- var core4 = __toESM(require_core(), 1);
153534
+ var core5 = __toESM(require_core(), 1);
153342
153535
  import { isAbsolute as isAbsolute2, resolve as resolve2 } from "node:path";
153343
153536
 
153344
153537
  // utils/versioning.ts
@@ -153402,7 +153595,7 @@ function resolveCwd(cwd) {
153402
153595
  return workspace ? resolve2(workspace, cwd) : cwd;
153403
153596
  }
153404
153597
  function resolvePromptInput() {
153405
- const prompt = core4.getInput("prompt", { required: true });
153598
+ const prompt = core5.getInput("prompt", { required: true });
153406
153599
  let parsed2;
153407
153600
  try {
153408
153601
  parsed2 = JSON.parse(prompt);
@@ -153418,11 +153611,11 @@ function resolvePromptInput() {
153418
153611
  }
153419
153612
  function resolveNonPromptInputs() {
153420
153613
  return Inputs.omit("prompt").assert({
153421
- model: core4.getInput("model") || void 0,
153422
- timeout: core4.getInput("timeout") || void 0,
153423
- cwd: core4.getInput("cwd") || void 0,
153424
- push: core4.getInput("push") || void 0,
153425
- shell: core4.getInput("shell") || void 0
153614
+ model: core5.getInput("model") || void 0,
153615
+ timeout: core5.getInput("timeout") || void 0,
153616
+ cwd: core5.getInput("cwd") || void 0,
153617
+ push: core5.getInput("push") || void 0,
153618
+ shell: core5.getInput("shell") || void 0
153426
153619
  });
153427
153620
  }
153428
153621
  var isPullfrog = (actor) => {
@@ -153637,8 +153830,7 @@ async function fetchRunContext(params) {
153637
153830
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
153638
153831
  try {
153639
153832
  const headers = {
153640
- Authorization: `Bearer ${params.token}`,
153641
- "Content-Type": "application/json"
153833
+ Authorization: `Bearer ${params.token}`
153642
153834
  };
153643
153835
  if (params.oidcToken) {
153644
153836
  headers["X-GitHub-OIDC-Token"] = params.oidcToken;
@@ -153679,13 +153871,13 @@ async function fetchRunContext(params) {
153679
153871
  }
153680
153872
 
153681
153873
  // utils/runContextData.ts
153682
- var core5 = __toESM(require_core(), 1);
153874
+ var core6 = __toESM(require_core(), 1);
153683
153875
  async function resolveRunContextData(params) {
153684
153876
  log.info(`\xBB running Pullfrog v${package_default.version}...`);
153685
153877
  const repoContext = parseRepoContext();
153686
153878
  let oidcToken;
153687
153879
  try {
153688
- oidcToken = await core5.getIDToken("pullfrog-api");
153880
+ oidcToken = await core6.getIDToken("pullfrog-api");
153689
153881
  } catch {
153690
153882
  }
153691
153883
  const [repoResponse, runContext] = await Promise.all([
@@ -153988,7 +154180,7 @@ async function resolveRun(params) {
153988
154180
 
153989
154181
  // main.ts
153990
154182
  function resolveOutputSchema() {
153991
- const raw2 = core6.getInput("output_schema");
154183
+ const raw2 = core7.getInput("output_schema");
153992
154184
  if (!raw2) return void 0;
153993
154185
  let parsed2;
153994
154186
  try {
@@ -154054,7 +154246,7 @@ function formatBillingErrorSummary(error49, owner) {
154054
154246
  return [
154055
154247
  "**Add a card to start using Pullfrog Router.**",
154056
154248
  "",
154057
- "Router proxies OpenRouter at raw cost \u2014 no platform markup, and your first $20 of usage is on us.",
154249
+ "Router proxies OpenRouter at raw cost \u2014 no platform markup. Add a card and we'll auto-reload your wallet so runs keep flowing.",
154058
154250
  "",
154059
154251
  `[Add a card \u2192](${billingConsoleUrl(owner, "model-access")})`
154060
154252
  ].join("\n");
@@ -154156,7 +154348,7 @@ async function buildProxyTokenHeaders(ctx) {
154156
154348
  if (ctx.oidcCredentials) {
154157
154349
  process.env.ACTIONS_ID_TOKEN_REQUEST_URL = ctx.oidcCredentials.requestUrl;
154158
154350
  process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = ctx.oidcCredentials.requestToken;
154159
- const oidcToken = await core6.getIDToken("pullfrog-api");
154351
+ const oidcToken = await core7.getIDToken("pullfrog-api");
154160
154352
  delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
154161
154353
  delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
154162
154354
  return { Authorization: `Bearer ${oidcToken}` };
@@ -154178,7 +154370,7 @@ async function resolveProxyModel(ctx) {
154178
154370
  const key = await mintProxyKey({ oidcCredentials: ctx.oidcCredentials, repo: ctx.repo });
154179
154371
  if (!key) return;
154180
154372
  process.env.OPENROUTER_API_KEY = key;
154181
- core6.setSecret(key);
154373
+ core7.setSecret(key);
154182
154374
  ctx.payload.proxyModel = ctx.proxyModel;
154183
154375
  const label = ctx.oss ? "oss" : "router";
154184
154376
  log.info(`\xBB proxy: ${label} \u2192 ${ctx.proxyModel}`);
@@ -154230,12 +154422,12 @@ async function persistLearnings(ctx) {
154230
154422
  });
154231
154423
  if (!response.ok) {
154232
154424
  const error49 = await response.text().catch(() => "(no body)");
154233
- log.debug(`learnings persist failed (${response.status}): ${error49}`);
154425
+ log.warning(`learnings persist failed (${response.status}): ${error49}`);
154234
154426
  return;
154235
154427
  }
154236
154428
  log.info("\xBB learnings updated");
154237
154429
  } catch (err) {
154238
- log.debug(`learnings persist failed: ${err instanceof Error ? err.message : String(err)}`);
154430
+ log.warning(`learnings persist failed: ${err instanceof Error ? err.message : String(err)}`);
154239
154431
  }
154240
154432
  }
154241
154433
  async function persistSummary(ctx) {
@@ -154290,8 +154482,8 @@ async function main() {
154290
154482
  if (runContext.dbSecrets) {
154291
154483
  for (const [key, value2] of Object.entries(runContext.dbSecrets)) {
154292
154484
  if (!process.env[key]) {
154293
- process.env[key] = value2;
154294
- core6.setSecret(value2);
154485
+ const sanitized = sanitizeSecret(key, value2);
154486
+ if (sanitized !== null) process.env[key] = sanitized;
154295
154487
  }
154296
154488
  }
154297
154489
  const count = Object.keys(runContext.dbSecrets).length;
@@ -154629,7 +154821,7 @@ ${instructions.user}` : null,
154629
154821
  }
154630
154822
  if (toolState.output) {
154631
154823
  log.info(`::pullfrog-output::${Buffer.from(toolState.output).toString("base64")}`);
154632
- core6.setOutput("result", toolState.output);
154824
+ core7.setOutput("result", toolState.output);
154633
154825
  }
154634
154826
  return await handleAgentResult({
154635
154827
  result,
@@ -154718,27 +154910,27 @@ async function runMain() {
154718
154910
  }
154719
154911
  } catch (error49) {
154720
154912
  const errorMessage = error49 instanceof Error ? error49.message : "unknown error occurred";
154721
- core7.setFailed(`action failed: ${errorMessage}`);
154913
+ core8.setFailed(`action failed: ${errorMessage}`);
154722
154914
  }
154723
154915
  }
154724
154916
  async function tokenMain() {
154725
- const reposInput = core7.getInput("repos");
154917
+ const reposInput = core8.getInput("repos");
154726
154918
  const additionalRepos = reposInput ? reposInput.split(",").map((r) => r.trim()).filter(Boolean) : [];
154727
154919
  const token = await acquireNewToken({ repos: additionalRepos });
154728
- core7.setSecret(token);
154729
- core7.saveState(STATE_TOKEN, token);
154730
- core7.setOutput("token", token);
154920
+ core8.setSecret(token);
154921
+ core8.saveState(STATE_TOKEN, token);
154922
+ core8.setOutput("token", token);
154731
154923
  const scope2 = additionalRepos.length ? `current repo + ${additionalRepos.join(", ")}` : "current repo only";
154732
- core7.info(`\xBB installation token acquired (${scope2})`);
154924
+ core8.info(`\xBB installation token acquired (${scope2})`);
154733
154925
  }
154734
154926
  async function tokenPost() {
154735
- const token = core7.getState(STATE_TOKEN);
154927
+ const token = core8.getState(STATE_TOKEN);
154736
154928
  if (!token) {
154737
- core7.debug("no token found in state, skipping revocation");
154929
+ core8.debug("no token found in state, skipping revocation");
154738
154930
  return;
154739
154931
  }
154740
154932
  await revokeGitHubInstallationToken(token);
154741
- core7.info("\xBB installation token revoked");
154933
+ core8.info("\xBB installation token revoked");
154742
154934
  }
154743
154935
  function printGhaUsage(params) {
154744
154936
  params.stream(`usage: ${params.prog} gha [subcommand]
@@ -154854,7 +155046,7 @@ async function run(args2) {
154854
155046
  }
154855
155047
  } catch (error49) {
154856
155048
  const message = error49 instanceof Error ? error49.message : String(error49);
154857
- core7.setFailed(message);
155049
+ core8.setFailed(message);
154858
155050
  }
154859
155051
  }
154860
155052
 
@@ -156518,7 +156710,7 @@ async function run2() {
156518
156710
  }
156519
156711
 
156520
156712
  // cli.ts
156521
- var VERSION10 = "0.1.4";
156713
+ var VERSION10 = "0.1.6";
156522
156714
  var bin = basename2(process.argv[1] || "");
156523
156715
  var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
156524
156716
  var rawArgs = process.argv.slice(2);