pullfrog 0.1.5 → 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";
@@ -110025,12 +110025,41 @@ function installSignalHandler() {
110025
110025
  killTrackedChildren();
110026
110026
  });
110027
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
+ };
110028
110055
  async function spawn(options) {
110029
110056
  const activityTimeoutMs = options.activityTimeout ?? DEFAULT_ACTIVITY_TIMEOUT_MS;
110030
110057
  installSignalHandler();
110031
110058
  const startTime = performance3.now();
110032
- let stdoutBuffer = "";
110033
- 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);
110034
110063
  const killGroup = options.killGroup ?? false;
110035
110064
  return new Promise((resolve3, reject) => {
110036
110065
  const child = nodeSpawn(options.cmd, options.args, {
@@ -110104,17 +110133,29 @@ async function spawn(options) {
110104
110133
  }
110105
110134
  if (child.stdout) {
110106
110135
  child.stdout.on("data", (data) => {
110107
- updateActivity();
110108
- const chunk = data.toString();
110109
- stdoutBuffer += chunk;
110110
- 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
+ }
110111
110146
  });
110112
110147
  }
110113
110148
  if (child.stderr) {
110114
110149
  child.stderr.on("data", (data) => {
110115
- const chunk = data.toString();
110116
- stderrBuffer += chunk;
110117
- 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
+ }
110118
110159
  });
110119
110160
  }
110120
110161
  child.on("close", (exitCode, signal) => {
@@ -110141,7 +110182,7 @@ async function spawn(options) {
110141
110182
  return;
110142
110183
  }
110143
110184
  let resolvedExitCode = exitCode ?? 0;
110144
- let resolvedStderr = stderrBuffer;
110185
+ let resolvedStderr = stderrBuffer?.toString() ?? "";
110145
110186
  if (exitCode === null && signal) {
110146
110187
  const killMsg = `[spawn] ${options.cmd}: killed by signal ${signal}`;
110147
110188
  resolvedStderr = resolvedStderr ? `${resolvedStderr}
@@ -110149,7 +110190,7 @@ ${killMsg}` : killMsg;
110149
110190
  resolvedExitCode = 1;
110150
110191
  }
110151
110192
  resolve3({
110152
- stdout: stdoutBuffer,
110193
+ stdout: stdoutBuffer?.toString() ?? "",
110153
110194
  stderr: resolvedStderr,
110154
110195
  exitCode: resolvedExitCode,
110155
110196
  durationMs
@@ -110163,11 +110204,12 @@ ${killMsg}` : killMsg;
110163
110204
  if (activityCheckIntervalId) clearInterval(activityCheckIntervalId);
110164
110205
  const errMsg = `[spawn] ${options.cmd}: ${error49.message}`;
110165
110206
  console.error(errMsg);
110166
- stderrBuffer = stderrBuffer ? `${stderrBuffer}
110207
+ const existingStderr = stderrBuffer?.toString() ?? "";
110208
+ const finalStderr = existingStderr ? `${existingStderr}
110167
110209
  ${errMsg}` : errMsg;
110168
110210
  resolve3({
110169
- stdout: stdoutBuffer,
110170
- stderr: stderrBuffer,
110211
+ stdout: stdoutBuffer?.toString() ?? "",
110212
+ stderr: finalStderr,
110171
110213
  exitCode: 1,
110172
110214
  durationMs
110173
110215
  });
@@ -138076,7 +138118,7 @@ var require_core4 = /* @__PURE__ */ __commonJSMin(((exports) => {
138076
138118
  Object.defineProperty(exports, "__esModule", { value: true });
138077
138119
  const id_1 = require_id2();
138078
138120
  const ref_1 = require_ref2();
138079
- const core8 = [
138121
+ const core9 = [
138080
138122
  "$schema",
138081
138123
  "$id",
138082
138124
  "$defs",
@@ -138086,7 +138128,7 @@ var require_core4 = /* @__PURE__ */ __commonJSMin(((exports) => {
138086
138128
  id_1.default,
138087
138129
  ref_1.default
138088
138130
  ];
138089
- exports.default = core8;
138131
+ exports.default = core9;
138090
138132
  }));
138091
138133
  var require_limitNumber2 = /* @__PURE__ */ __commonJSMin(((exports) => {
138092
138134
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -142596,7 +142638,7 @@ var import_semver = __toESM(require_semver2(), 1);
142596
142638
  // package.json
142597
142639
  var package_default = {
142598
142640
  name: "pullfrog",
142599
- version: "0.1.5",
142641
+ version: "0.1.6",
142600
142642
  type: "module",
142601
142643
  bin: {
142602
142644
  pullfrog: "dist/cli.mjs",
@@ -146673,6 +146715,8 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
146673
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)
146674
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.
146675
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
+
146676
146720
  lenses come in two flavors, and you can mix them:
146677
146721
  - **themed lenses** \u2014 a perspective applied across the whole diff (correctness, security, user-journey, performance, etc.).
146678
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.
@@ -146680,7 +146724,7 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
146680
146724
  starter menu (combine, omit, or invent your own):
146681
146725
  - **correctness & invariants** \u2014 bugs, races, error handling, edge cases, state-machine boundaries
146682
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
146683
- - **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.
146684
146728
  - **security** \u2014 new endpoints, authZ, input validation, secrets handling, replay/CSRF/injection, cross-tenant isolation
146685
146729
  - **user-journey** \u2014 UX-touching flows: walk through happy path and failure modes as a user
146686
146730
  - **operational readiness** \u2014 observability, alerting, migrations (forward + rollback), feature flags, on-call burden
@@ -146769,7 +146813,7 @@ ${PR_SUMMARY_FORMAT}`
146769
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.
146770
146814
  When unsure, treat as non-trivial.
146771
146815
 
146772
- 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.
146773
146817
 
146774
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:
146775
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
@@ -147139,20 +147183,30 @@ var ThinkingTimer = class {
147139
147183
  maximumFractionDigits: 1
147140
147184
  });
147141
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
+ }
147142
147192
  markToolResult() {
147143
147193
  this.lastToolResultTimestamp = performance5.now();
147144
- log.debug(`\xBB thinking timer: markToolResult at ${this.lastToolResultTimestamp}`);
147194
+ log.debug(
147195
+ this.formatLine(`\xBB thinking timer: markToolResult at ${this.lastToolResultTimestamp}`)
147196
+ );
147145
147197
  }
147146
147198
  markToolCall() {
147147
147199
  const now = performance5.now();
147148
147200
  log.debug(
147149
- `\xBB thinking timer: markToolCall at ${now}, lastToolResult=${this.lastToolResultTimestamp}`
147201
+ this.formatLine(
147202
+ `\xBB thinking timer: markToolCall at ${now}, lastToolResult=${this.lastToolResultTimestamp}`
147203
+ )
147150
147204
  );
147151
147205
  if (this.lastToolResultTimestamp === null) return;
147152
147206
  const elapsed = now - this.lastToolResultTimestamp;
147153
147207
  if (elapsed < THINKING_THRESHOLD) return;
147154
147208
  const seconds = elapsed / 1e3;
147155
- log.info(`\xBB thought for ${this.durationFormatter.format(seconds)}`);
147209
+ log.info(this.formatLine(`\xBB thought for ${this.durationFormatter.format(seconds)}`));
147156
147210
  }
147157
147211
  };
147158
147212
 
@@ -147401,19 +147455,39 @@ function deriveLabelFromTaskInput(input) {
147401
147455
  }
147402
147456
  var SessionLabeler = class {
147403
147457
  labels = /* @__PURE__ */ new Map();
147458
+ labelsByToolUseId = /* @__PURE__ */ new Map();
147404
147459
  pendingLabels = [];
147405
147460
  fallbackCounter = 0;
147406
- 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) {
147407
147472
  const label = deriveLabelFromTaskInput(input);
147408
147473
  this.pendingLabels.push(label);
147474
+ if (toolUseId) this.labelsByToolUseId.set(toolUseId, label);
147409
147475
  return label;
147410
147476
  }
147411
147477
  /**
147412
- * Return a label for the given sessionID. Binds on first call.
147413
- * Pass undefined/empty for events that lack a session id — the caller
147414
- * 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.
147415
147485
  */
147416
- labelFor(sessionID) {
147486
+ labelFor(sessionID, parentToolUseId) {
147487
+ if (parentToolUseId) {
147488
+ const direct = this.labelsByToolUseId.get(parentToolUseId);
147489
+ if (direct) return direct;
147490
+ }
147417
147491
  if (!sessionID) return ORCHESTRATOR_LABEL;
147418
147492
  const existing = this.labels.get(sessionID);
147419
147493
  if (existing) return existing;
@@ -147497,7 +147571,23 @@ function tailLines(text, maxCodeUnits) {
147497
147571
  async function runClaude(params) {
147498
147572
  const startTime = performance6.now();
147499
147573
  let eventCount = 0;
147500
- 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
+ }
147501
147591
  let finalOutput = "";
147502
147592
  let sessionId;
147503
147593
  let resultErrorSubtype = null;
@@ -147518,17 +147608,22 @@ async function runClaude(params) {
147518
147608
  } : void 0;
147519
147609
  }
147520
147610
  const handlers2 = {
147521
- system: (_event) => {
147522
- 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`));
147523
147614
  },
147524
147615
  assistant: (event) => {
147525
147616
  const content = event.message?.content;
147526
147617
  if (!content) return;
147618
+ const label = eventLabel(event);
147619
+ const boxTitle = label === ORCHESTRATOR_LABEL ? params.label : `${params.label} [${label}]`;
147527
147620
  for (const block of content) {
147528
147621
  if (block.type === "text" && block.text?.trim()) {
147529
147622
  const message = block.text.trim();
147530
- log.box(message, { title: params.label });
147531
- finalOutput = message;
147623
+ log.box(message, { title: boxTitle });
147624
+ if (label === ORCHESTRATOR_LABEL) {
147625
+ finalOutput = message;
147626
+ }
147532
147627
  } else if (block.type === "tool_use") {
147533
147628
  const toolName = block.name || "unknown";
147534
147629
  if (params.onToolUse) {
@@ -147537,20 +147632,25 @@ async function runClaude(params) {
147537
147632
  input: block.input
147538
147633
  });
147539
147634
  }
147540
- thinkingTimer.markToolCall();
147541
- log.toolCall({ toolName, input: block.input || {} });
147542
- 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") {
147543
147640
  const taskInput = block.input;
147544
- const label = deriveLabelFromTaskInput(taskInput);
147641
+ const dispatchedLabel = labeler.recordTaskDispatch(taskInput, block.id ?? null);
147545
147642
  log.info(
147546
- `\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
+ )
147547
147647
  );
147548
147648
  }
147549
147649
  if (toolName.includes("report_progress") && params.todoTracker) {
147550
147650
  log.debug("\xBB report_progress detected, disabling todo tracking");
147551
147651
  params.todoTracker.cancel();
147552
147652
  }
147553
- if (toolName === "TodoWrite" && params.todoTracker?.enabled) {
147653
+ if (toolName === "TodoWrite" && params.todoTracker?.enabled && label === ORCHESTRATOR_LABEL) {
147554
147654
  params.todoTracker.update(block.input);
147555
147655
  }
147556
147656
  }
@@ -147566,17 +147666,18 @@ async function runClaude(params) {
147566
147666
  user: (event) => {
147567
147667
  const content = event.message?.content;
147568
147668
  if (!content) return;
147669
+ const label = eventLabel(event);
147569
147670
  for (const block of content) {
147570
147671
  if (typeof block === "string") continue;
147571
147672
  if (block.type === "tool_result") {
147572
- thinkingTimer.markToolResult();
147673
+ timerFor(label).markToolResult();
147573
147674
  const outputContent = typeof block.content === "string" ? block.content : Array.isArray(block.content) ? block.content.map(
147574
147675
  (entry) => typeof entry === "string" ? entry : typeof entry === "object" && entry !== null && "text" in entry ? String(entry.text) : JSON.stringify(entry)
147575
147676
  ).join("\n") : String(block.content);
147576
147677
  if (block.is_error) {
147577
- log.info(`\xBB tool error: ${outputContent}`);
147678
+ log.info(withLabel(label, `\xBB tool error: ${outputContent}`));
147578
147679
  } else {
147579
- log.debug(`\xBB tool output: ${outputContent}`);
147680
+ log.debug(withLabel(label, `\xBB tool output: ${outputContent}`));
147580
147681
  }
147581
147682
  }
147582
147683
  }
@@ -147646,7 +147747,7 @@ async function runClaude(params) {
147646
147747
  };
147647
147748
  const recentStderr = [];
147648
147749
  let lastProviderError = null;
147649
- let output = "";
147750
+ const output = new TailBuffer(DEFAULT_MAX_RETAINED_BYTES);
147650
147751
  let stdoutBuffer = "";
147651
147752
  try {
147652
147753
  const result = await spawn({
@@ -147663,9 +147764,14 @@ async function runClaude(params) {
147663
147764
  // there's no shim-orphan issue like opencode-ai/bin/opencode, but
147664
147765
  // detached + killGroup is the right default for any agent runtime.
147665
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",
147666
147772
  onStdout: async (chunk) => {
147667
147773
  const text = chunk.toString();
147668
- output += text;
147774
+ output.append(text);
147669
147775
  markActivity();
147670
147776
  stdoutBuffer += text;
147671
147777
  const lines = stdoutBuffer.split("\n");
@@ -147740,16 +147846,18 @@ ${stderrContext}`);
147740
147846
  const usage = buildUsage();
147741
147847
  if (result.exitCode !== 0) {
147742
147848
  const errorContext = lastProviderError ? ` (${lastProviderError})` : "";
147743
- const truncatedStdout = result.stdout ? tailLines(result.stdout, 2048) : "";
147744
- 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}`;
147745
147853
  log.error(
147746
147854
  `${params.label} exited with code ${result.exitCode}${errorContext}: ${errorMessage}`
147747
147855
  );
147748
- log.debug(`stdout: ${result.stdout?.substring(0, 500)}`);
147749
- log.debug(`stderr: ${result.stderr?.substring(0, 500)}`);
147856
+ log.debug(`stdout: ${stdoutSnapshot.substring(0, 500)}`);
147857
+ log.debug(`stderr: ${stderrSnapshot.substring(0, 500)}`);
147750
147858
  return {
147751
147859
  success: false,
147752
- output: finalOutput || output,
147860
+ output: finalOutput || stdoutSnapshot,
147753
147861
  error: errorMessage,
147754
147862
  usage,
147755
147863
  sessionId
@@ -147758,7 +147866,7 @@ ${stderrContext}`);
147758
147866
  if (eventCount === 0 && lastProviderError) {
147759
147867
  return {
147760
147868
  success: false,
147761
- output: finalOutput || output,
147869
+ output: finalOutput || output.toString(),
147762
147870
  error: `provider error: ${lastProviderError}`,
147763
147871
  usage,
147764
147872
  sessionId
@@ -147767,13 +147875,13 @@ ${stderrContext}`);
147767
147875
  if (resultErrorSubtype) {
147768
147876
  return {
147769
147877
  success: false,
147770
- output: finalOutput || output,
147878
+ output: finalOutput || output.toString(),
147771
147879
  error: lastResultError || `result subtype: ${resultErrorSubtype}`,
147772
147880
  usage,
147773
147881
  sessionId
147774
147882
  };
147775
147883
  }
147776
- return { success: true, output: finalOutput || output, usage, sessionId };
147884
+ return { success: true, output: finalOutput || output.toString(), usage, sessionId };
147777
147885
  } catch (error49) {
147778
147886
  params.todoTracker?.cancel();
147779
147887
  const duration4 = performance6.now() - startTime;
@@ -147792,7 +147900,7 @@ ${stderrContext}`
147792
147900
  );
147793
147901
  return {
147794
147902
  success: false,
147795
- output: finalOutput || output,
147903
+ output: finalOutput || output.toString(),
147796
147904
  error: `${errorMessage} [${diagnosis}]`,
147797
147905
  usage: buildUsage(),
147798
147906
  sessionId
@@ -148028,6 +148136,15 @@ function buildSecurityConfig(ctx, model) {
148028
148136
  [pullfrogMcpName]: { type: "remote", url: ctx.mcpServerUrl }
148029
148137
  },
148030
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 },
148031
148148
  provider: {
148032
148149
  google: {
148033
148150
  models: Object.fromEntries(
@@ -148100,7 +148217,6 @@ function autoSelectModel(cliPath) {
148100
148217
  async function runOpenCode(params) {
148101
148218
  const startTime = performance7.now();
148102
148219
  let eventCount = 0;
148103
- const thinkingTimer = new ThinkingTimer();
148104
148220
  let finalOutput = "";
148105
148221
  let accumulatedTokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
148106
148222
  let accumulatedCostUsd = 0;
@@ -148117,6 +148233,16 @@ async function runOpenCode(params) {
148117
148233
  function withLabel(label, message) {
148118
148234
  return label === ORCHESTRATOR_LABEL ? message : formatWithLabel(label, message);
148119
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
+ }
148120
148246
  const taskDispatchByCallID = /* @__PURE__ */ new Map();
148121
148247
  const pendingTaskDispatches = [];
148122
148248
  const knownNonTaskCallIDs = /* @__PURE__ */ new Set();
@@ -148265,7 +148391,7 @@ async function runOpenCode(params) {
148265
148391
  input: event.part?.state?.input
148266
148392
  });
148267
148393
  }
148268
- thinkingTimer.markToolCall();
148394
+ timerFor(label).markToolCall();
148269
148395
  const inputFormatted = formatJsonValue(event.part?.state?.input || {});
148270
148396
  const toolCallLine = inputFormatted !== "{}" ? `\xBB ${toolName}(${inputFormatted})` : `\xBB ${toolName}()`;
148271
148397
  log.info(withLabel(label, toolCallLine));
@@ -148289,7 +148415,7 @@ async function runOpenCode(params) {
148289
148415
  const status = event.part?.state?.status || event.status || "unknown";
148290
148416
  const output2 = event.part?.state?.output || event.output;
148291
148417
  const label = eventLabel(event);
148292
- thinkingTimer.markToolResult();
148418
+ timerFor(label).markToolResult();
148293
148419
  if (taskDispatchByCallID.size > 0 || pendingTaskDispatches.length > 0) {
148294
148420
  if (toolId && taskDispatchByCallID.has(toolId)) {
148295
148421
  const dispatch = taskDispatchByCallID.get(toolId);
@@ -148414,7 +148540,7 @@ async function runOpenCode(params) {
148414
148540
  const recentStderr = [];
148415
148541
  let lastProviderError = null;
148416
148542
  let agentErrorEvent = null;
148417
- let output = "";
148543
+ const output = new TailBuffer(DEFAULT_MAX_RETAINED_BYTES);
148418
148544
  let stdoutBuffer = "";
148419
148545
  try {
148420
148546
  const result = await spawn({
@@ -148432,6 +148558,11 @@ async function runOpenCode(params) {
148432
148558
  // never fires — producing zombie runs. detached + killGroup nukes the
148433
148559
  // whole tree.
148434
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",
148435
148566
  // NB: we used to pass `isPausedExternally: isSubagentInFlight` to suspend
148436
148567
  // the activity timer during subagent dispatches. unnecessary now that
148437
148568
  // our injected plugin (action/agents/opencodePlugin.ts) re-emits
@@ -148441,7 +148572,7 @@ async function runOpenCode(params) {
148441
148572
  // (~3.3 plugin events/sec during a typical subagent run).
148442
148573
  onStdout: async (chunk) => {
148443
148574
  const text = chunk.toString();
148444
- output += text;
148575
+ output.append(text);
148445
148576
  markActivity();
148446
148577
  stdoutBuffer += text;
148447
148578
  const lines = stdoutBuffer.split("\n");
@@ -148530,18 +148661,25 @@ ${stderrContext}`);
148530
148661
  const usage = buildUsage();
148531
148662
  if (result.exitCode !== 0) {
148532
148663
  const errorContext = lastProviderError ? ` (${lastProviderError})` : "";
148533
- 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}`;
148534
148667
  log.error(
148535
148668
  `${params.label} exited with code ${result.exitCode}${errorContext}: ${errorMessage}`
148536
148669
  );
148537
- log.debug(`stdout: ${result.stdout?.substring(0, 500)}`);
148538
- log.debug(`stderr: ${result.stderr?.substring(0, 500)}`);
148539
- 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
+ };
148540
148678
  }
148541
148679
  if (eventCount === 0 && lastProviderError) {
148542
148680
  return {
148543
148681
  success: false,
148544
- output: finalOutput || output,
148682
+ output: finalOutput || output.toString(),
148545
148683
  error: `provider error: ${lastProviderError}`,
148546
148684
  usage
148547
148685
  };
@@ -148552,12 +148690,12 @@ ${stderrContext}`);
148552
148690
  const errorMessage = errorEvent.error?.data?.message || errorEvent.error?.name || JSON.stringify(errorEvent);
148553
148691
  return {
148554
148692
  success: false,
148555
- output: finalOutput || output,
148693
+ output: finalOutput || output.toString(),
148556
148694
  error: `${errorName}: ${errorMessage}`,
148557
148695
  usage
148558
148696
  };
148559
148697
  }
148560
- return { success: true, output: finalOutput || output, usage };
148698
+ return { success: true, output: finalOutput || output.toString(), usage };
148561
148699
  } catch (error49) {
148562
148700
  params.todoTracker?.cancel();
148563
148701
  const duration4 = performance7.now() - startTime;
@@ -148576,7 +148714,7 @@ ${stderrContext}`
148576
148714
  );
148577
148715
  return {
148578
148716
  success: false,
148579
- output: finalOutput || output,
148717
+ output: finalOutput || output.toString(),
148580
148718
  error: `${errorMessage} [${diagnosis}]`,
148581
148719
  usage: buildUsage()
148582
148720
  };
@@ -152482,6 +152620,14 @@ function isOIDCAvailable() {
152482
152620
  process.env.ACTIONS_ID_TOKEN_REQUEST_URL && process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN
152483
152621
  );
152484
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
+ };
152485
152631
  async function acquireTokenViaOIDC(opts) {
152486
152632
  const oidcToken = await core2.getIDToken("pullfrog-api");
152487
152633
  const repos = [...opts?.repos ?? []];
@@ -152506,7 +152652,16 @@ async function acquireTokenViaOIDC(opts) {
152506
152652
  });
152507
152653
  clearTimeout(timeoutId);
152508
152654
  if (!tokenResponse.ok) {
152509
- 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
+ );
152510
152665
  }
152511
152666
  const tokenData = await tokenResponse.json();
152512
152667
  return tokenData.token;
@@ -152627,7 +152782,10 @@ async function acquireNewToken(opts) {
152627
152782
  if (isOIDCAvailable()) {
152628
152783
  return await retry(() => acquireTokenViaOIDC(opts), {
152629
152784
  label: "token exchange",
152630
- 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
+ }
152631
152789
  });
152632
152790
  } else {
152633
152791
  return await acquireTokenViaGitHubApp(opts);
@@ -153174,6 +153332,21 @@ ${getStandaloneModeInstructions(ctx.payload.event.trigger, t2, ctx.outputSchema)
153174
153332
 
153175
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.
153176
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
+
153177
153350
  ### Command execution
153178
153351
 
153179
153352
  Never use \`sleep\` to wait for commands to complete. Commands run synchronously \u2014 when the shell tool returns, the command has finished.
@@ -153300,10 +153473,22 @@ async function readLearningsFile(path3) {
153300
153473
  }
153301
153474
 
153302
153475
  // utils/normalizeEnv.ts
153303
- function maskValue(value2) {
153304
- if (value2 && typeof value2 === "string" && value2.trim().length > 0) {
153305
- 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;
153306
153484
  }
153485
+ if (trimmed !== value2) {
153486
+ log.warning(
153487
+ `\xBB stripped whitespace from ${key} (whitespace in secret values breaks GitHub Actions log masking)`
153488
+ );
153489
+ }
153490
+ core4.setSecret(trimmed);
153491
+ return trimmed;
153307
153492
  }
153308
153493
  function normalizeEnv() {
153309
153494
  const upperKeys = /* @__PURE__ */ new Map();
@@ -153314,11 +153499,6 @@ function normalizeEnv() {
153314
153499
  upperKeys.set(upper2, existing);
153315
153500
  }
153316
153501
  for (const [upperKey, keys] of upperKeys) {
153317
- if (isSensitiveEnvName(upperKey)) {
153318
- for (const key of keys) {
153319
- maskValue(process.env[key]);
153320
- }
153321
- }
153322
153502
  if (keys.length === 1) {
153323
153503
  const key = keys[0];
153324
153504
  if (key !== upperKey) {
@@ -153341,10 +153521,17 @@ function normalizeEnv() {
153341
153521
  }
153342
153522
  process.env[upperKey] = preferredValue;
153343
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
+ }
153344
153531
  }
153345
153532
 
153346
153533
  // utils/payload.ts
153347
- var core4 = __toESM(require_core(), 1);
153534
+ var core5 = __toESM(require_core(), 1);
153348
153535
  import { isAbsolute as isAbsolute2, resolve as resolve2 } from "node:path";
153349
153536
 
153350
153537
  // utils/versioning.ts
@@ -153408,7 +153595,7 @@ function resolveCwd(cwd) {
153408
153595
  return workspace ? resolve2(workspace, cwd) : cwd;
153409
153596
  }
153410
153597
  function resolvePromptInput() {
153411
- const prompt = core4.getInput("prompt", { required: true });
153598
+ const prompt = core5.getInput("prompt", { required: true });
153412
153599
  let parsed2;
153413
153600
  try {
153414
153601
  parsed2 = JSON.parse(prompt);
@@ -153424,11 +153611,11 @@ function resolvePromptInput() {
153424
153611
  }
153425
153612
  function resolveNonPromptInputs() {
153426
153613
  return Inputs.omit("prompt").assert({
153427
- model: core4.getInput("model") || void 0,
153428
- timeout: core4.getInput("timeout") || void 0,
153429
- cwd: core4.getInput("cwd") || void 0,
153430
- push: core4.getInput("push") || void 0,
153431
- 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
153432
153619
  });
153433
153620
  }
153434
153621
  var isPullfrog = (actor) => {
@@ -153684,13 +153871,13 @@ async function fetchRunContext(params) {
153684
153871
  }
153685
153872
 
153686
153873
  // utils/runContextData.ts
153687
- var core5 = __toESM(require_core(), 1);
153874
+ var core6 = __toESM(require_core(), 1);
153688
153875
  async function resolveRunContextData(params) {
153689
153876
  log.info(`\xBB running Pullfrog v${package_default.version}...`);
153690
153877
  const repoContext = parseRepoContext();
153691
153878
  let oidcToken;
153692
153879
  try {
153693
- oidcToken = await core5.getIDToken("pullfrog-api");
153880
+ oidcToken = await core6.getIDToken("pullfrog-api");
153694
153881
  } catch {
153695
153882
  }
153696
153883
  const [repoResponse, runContext] = await Promise.all([
@@ -153993,7 +154180,7 @@ async function resolveRun(params) {
153993
154180
 
153994
154181
  // main.ts
153995
154182
  function resolveOutputSchema() {
153996
- const raw2 = core6.getInput("output_schema");
154183
+ const raw2 = core7.getInput("output_schema");
153997
154184
  if (!raw2) return void 0;
153998
154185
  let parsed2;
153999
154186
  try {
@@ -154161,7 +154348,7 @@ async function buildProxyTokenHeaders(ctx) {
154161
154348
  if (ctx.oidcCredentials) {
154162
154349
  process.env.ACTIONS_ID_TOKEN_REQUEST_URL = ctx.oidcCredentials.requestUrl;
154163
154350
  process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = ctx.oidcCredentials.requestToken;
154164
- const oidcToken = await core6.getIDToken("pullfrog-api");
154351
+ const oidcToken = await core7.getIDToken("pullfrog-api");
154165
154352
  delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
154166
154353
  delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
154167
154354
  return { Authorization: `Bearer ${oidcToken}` };
@@ -154183,7 +154370,7 @@ async function resolveProxyModel(ctx) {
154183
154370
  const key = await mintProxyKey({ oidcCredentials: ctx.oidcCredentials, repo: ctx.repo });
154184
154371
  if (!key) return;
154185
154372
  process.env.OPENROUTER_API_KEY = key;
154186
- core6.setSecret(key);
154373
+ core7.setSecret(key);
154187
154374
  ctx.payload.proxyModel = ctx.proxyModel;
154188
154375
  const label = ctx.oss ? "oss" : "router";
154189
154376
  log.info(`\xBB proxy: ${label} \u2192 ${ctx.proxyModel}`);
@@ -154295,8 +154482,8 @@ async function main() {
154295
154482
  if (runContext.dbSecrets) {
154296
154483
  for (const [key, value2] of Object.entries(runContext.dbSecrets)) {
154297
154484
  if (!process.env[key]) {
154298
- process.env[key] = value2;
154299
- core6.setSecret(value2);
154485
+ const sanitized = sanitizeSecret(key, value2);
154486
+ if (sanitized !== null) process.env[key] = sanitized;
154300
154487
  }
154301
154488
  }
154302
154489
  const count = Object.keys(runContext.dbSecrets).length;
@@ -154634,7 +154821,7 @@ ${instructions.user}` : null,
154634
154821
  }
154635
154822
  if (toolState.output) {
154636
154823
  log.info(`::pullfrog-output::${Buffer.from(toolState.output).toString("base64")}`);
154637
- core6.setOutput("result", toolState.output);
154824
+ core7.setOutput("result", toolState.output);
154638
154825
  }
154639
154826
  return await handleAgentResult({
154640
154827
  result,
@@ -154723,27 +154910,27 @@ async function runMain() {
154723
154910
  }
154724
154911
  } catch (error49) {
154725
154912
  const errorMessage = error49 instanceof Error ? error49.message : "unknown error occurred";
154726
- core7.setFailed(`action failed: ${errorMessage}`);
154913
+ core8.setFailed(`action failed: ${errorMessage}`);
154727
154914
  }
154728
154915
  }
154729
154916
  async function tokenMain() {
154730
- const reposInput = core7.getInput("repos");
154917
+ const reposInput = core8.getInput("repos");
154731
154918
  const additionalRepos = reposInput ? reposInput.split(",").map((r) => r.trim()).filter(Boolean) : [];
154732
154919
  const token = await acquireNewToken({ repos: additionalRepos });
154733
- core7.setSecret(token);
154734
- core7.saveState(STATE_TOKEN, token);
154735
- core7.setOutput("token", token);
154920
+ core8.setSecret(token);
154921
+ core8.saveState(STATE_TOKEN, token);
154922
+ core8.setOutput("token", token);
154736
154923
  const scope2 = additionalRepos.length ? `current repo + ${additionalRepos.join(", ")}` : "current repo only";
154737
- core7.info(`\xBB installation token acquired (${scope2})`);
154924
+ core8.info(`\xBB installation token acquired (${scope2})`);
154738
154925
  }
154739
154926
  async function tokenPost() {
154740
- const token = core7.getState(STATE_TOKEN);
154927
+ const token = core8.getState(STATE_TOKEN);
154741
154928
  if (!token) {
154742
- core7.debug("no token found in state, skipping revocation");
154929
+ core8.debug("no token found in state, skipping revocation");
154743
154930
  return;
154744
154931
  }
154745
154932
  await revokeGitHubInstallationToken(token);
154746
- core7.info("\xBB installation token revoked");
154933
+ core8.info("\xBB installation token revoked");
154747
154934
  }
154748
154935
  function printGhaUsage(params) {
154749
154936
  params.stream(`usage: ${params.prog} gha [subcommand]
@@ -154859,7 +155046,7 @@ async function run(args2) {
154859
155046
  }
154860
155047
  } catch (error49) {
154861
155048
  const message = error49 instanceof Error ? error49.message : String(error49);
154862
- core7.setFailed(message);
155049
+ core8.setFailed(message);
154863
155050
  }
154864
155051
  }
154865
155052
 
@@ -156523,7 +156710,7 @@ async function run2() {
156523
156710
  }
156524
156711
 
156525
156712
  // cli.ts
156526
- var VERSION10 = "0.1.5";
156713
+ var VERSION10 = "0.1.6";
156527
156714
  var bin = basename2(process.argv[1] || "");
156528
156715
  var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
156529
156716
  var rawArgs = process.argv.slice(2);