pullfrog 0.1.96 → 0.1.98

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
@@ -98361,14 +98361,14 @@ var require_turndown_cjs = __commonJS({
98361
98361
  } else if (node2.nodeType === 1) {
98362
98362
  replacement = replacementForNode.call(self2, node2);
98363
98363
  }
98364
- return join30(output, replacement);
98364
+ return join31(output, replacement);
98365
98365
  }, "");
98366
98366
  }
98367
98367
  function postProcess(output) {
98368
98368
  var self2 = this;
98369
98369
  this.rules.forEach(function(rule) {
98370
98370
  if (typeof rule.append === "function") {
98371
- output = join30(output, rule.append(self2.options));
98371
+ output = join31(output, rule.append(self2.options));
98372
98372
  }
98373
98373
  });
98374
98374
  return output.replace(/^[\t\r\n]+/, "").replace(/[\t\r\n\s]+$/, "");
@@ -98380,7 +98380,7 @@ var require_turndown_cjs = __commonJS({
98380
98380
  if (whitespace.leading || whitespace.trailing) content = content.trim();
98381
98381
  return whitespace.leading + rule.replacement(content, node2, this.options) + whitespace.trailing;
98382
98382
  }
98383
- function join30(output, replacement) {
98383
+ function join31(output, replacement) {
98384
98384
  var s1 = trimTrailingNewlines(output);
98385
98385
  var s2 = trimLeadingNewlines(replacement);
98386
98386
  var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
@@ -106963,7 +106963,7 @@ var import_picocolors = __toESM(require_picocolors(), 1);
106963
106963
  // package.json
106964
106964
  var package_default = {
106965
106965
  name: "pullfrog",
106966
- version: "0.1.96",
106966
+ version: "0.1.98",
106967
106967
  cliContractVersion: "1",
106968
106968
  type: "module",
106969
106969
  bin: {
@@ -106991,7 +106991,7 @@ var package_default = {
106991
106991
  },
106992
106992
  devDependencies: {
106993
106993
  "@actions/core": "^1.11.1",
106994
- "@anthropic-ai/claude-code": "2.1.284",
106994
+ "@anthropic-ai/claude-code": "2.1.293",
106995
106995
  "@ark/fs": "0.56.0",
106996
106996
  "@ark/util": "0.56.0",
106997
106997
  "@clack/prompts": "^1.2.0",
@@ -108607,9 +108607,9 @@ var import_arg6 = __toESM(require_arg(), 1);
108607
108607
  import { dirname as dirname6 } from "node:path";
108608
108608
 
108609
108609
  // main.ts
108610
- import { existsSync as existsSync12, readdirSync as readdirSync2 } from "node:fs";
108610
+ import { existsSync as existsSync13, readdirSync as readdirSync2 } from "node:fs";
108611
108611
  import { readFile as readFile6 } from "node:fs/promises";
108612
- import { join as join28 } from "node:path";
108612
+ import { join as join29 } from "node:path";
108613
108613
 
108614
108614
  // node_modules/.pnpm/package-manager-detector@1.8.0/node_modules/package-manager-detector/dist/commands.mjs
108615
108615
  function splitRunArgs(args2, valueFlags = []) {
@@ -109042,8 +109042,9 @@ var providers = {
109042
109042
  },
109043
109043
  "claude-haiku": {
109044
109044
  displayName: "Claude Haiku",
109045
- resolve: "anthropic/claude-haiku-4-5",
109046
- openRouterResolve: "openrouter/anthropic/claude-haiku-4.5"
109045
+ resolve: "anthropic/claude-haiku-5-5",
109046
+ effort: ["low", "medium", "high", "xhigh", "max"],
109047
+ openRouterResolve: "openrouter/anthropic/claude-haiku-5.5"
109047
109048
  }
109048
109049
  }
109049
109050
  }),
@@ -109461,8 +109462,9 @@ var providers = {
109461
109462
  },
109462
109463
  "claude-haiku": {
109463
109464
  displayName: "Claude Haiku",
109464
- resolve: "opencode/claude-haiku-4-5",
109465
- openRouterResolve: "openrouter/anthropic/claude-haiku-4.5"
109465
+ resolve: "opencode/claude-haiku-5-5",
109466
+ effort: ["low", "medium", "high", "xhigh", "max"],
109467
+ openRouterResolve: "openrouter/anthropic/claude-haiku-5.5"
109466
109468
  },
109467
109469
  "gpt-sol": {
109468
109470
  displayName: "GPT Sol",
@@ -109618,6 +109620,14 @@ var providers = {
109618
109620
  openRouterEffort: [],
109619
109621
  openRouterResolve: "openrouter/qwen/qwen3.8-flash"
109620
109622
  },
109623
+ // Zen lists Mistral Large 4 at half its list price for launch, level with
109624
+ // the routers.
109625
+ "mistral-large": {
109626
+ displayName: "Mistral Large",
109627
+ resolve: "opencode/mistral-large-4",
109628
+ effort: ["none", "high"],
109629
+ openRouterResolve: "openrouter/mistralai/mistral-large-4-0"
109630
+ },
109621
109631
  "gpt-5-nano": {
109622
109632
  displayName: "GPT Nano",
109623
109633
  resolve: "opencode/gpt-5.4-nano",
@@ -109666,12 +109676,12 @@ var providers = {
109666
109676
  fallback: "opencode/big-pickle"
109667
109677
  },
109668
109678
  // Zen's FREE stealth preview (listed 2026-09-23), the third after
109669
- // big-pickle and union-alpha. OpenRouter's listing: prompts and
109670
- // completions may be retained by the provider, not trained on. unlike the
109671
- // first two it publishes an effort ladder, so the gate mirrors one here.
109679
+ // big-pickle and union-alpha. Zen's docs: zero retention, no training.
109680
+ // unlike the first two it publishes an effort ladder, so the gate
109681
+ // mirrors one here.
109672
109682
  "space-bunny": {
109673
109683
  displayName: "Space Bunny",
109674
- description: "Stealth preview; prompts may be retained",
109684
+ description: "Stealth preview, free for a limited time",
109675
109685
  resolve: "opencode/space-bunny-free",
109676
109686
  effort: ["low", "medium", "high", "xhigh", "max"],
109677
109687
  // free to run, still gated on the provider's own OPENCODE_API_KEY —
@@ -109689,17 +109699,46 @@ var providers = {
109689
109699
  // see the big-pickle note above (#1077).
109690
109700
  isFree: true
109691
109701
  },
109692
- // Zen's FREE stealth preview (listed 2026-10-01), the fifth promo. Zen
109693
- // has not published its data terms yet, so the description claims none.
109702
+ // Zen's FREE stealth preview (listed 2026-10-01), the fifth promo. Zen's
109703
+ // docs: data may be used to improve the model during the free period.
109694
109704
  "fledge-alpha": {
109695
109705
  displayName: "Fledge Alpha",
109696
- description: "Stealth preview, free for a limited time",
109706
+ description: "Stealth preview; data may be used to improve it",
109697
109707
  resolve: "opencode/fledge-alpha-free",
109698
109708
  effort: ["low", "high", "max"],
109699
109709
  // free to run, still gated on the provider's own OPENCODE_API_KEY —
109700
109710
  // see the big-pickle note above (#1077).
109711
+ isFree: true,
109712
+ // Zen dropped the id from `/v1/models` and its docs on 2026-10-08 and
109713
+ // answers a 500 for it; stored picks land on the other free stealth
109714
+ // model, as `union-alpha` does.
109715
+ fallback: "opencode/big-pickle"
109716
+ },
109717
+ // Zen's FREE preview (listed 2026-10-06), the sixth promo, on Fledge
109718
+ // Alpha's terms.
109719
+ exo: {
109720
+ displayName: "Exo",
109721
+ description: "Preview; data may be used to improve it",
109722
+ resolve: "opencode/exo-free",
109723
+ // Zen publishes a single rung.
109724
+ effort: ["high"],
109725
+ // free to run, still gated on the provider's own OPENCODE_API_KEY —
109726
+ // see the big-pickle note above (#1077).
109701
109727
  isFree: true
109702
109728
  },
109729
+ // Zen's FREE StepFun preview (listed 2026-10-08), the seventh promo. Zen's
109730
+ // docs: zero retention, no training. Go lists the same free id; one key
109731
+ // reaches both. hidden until a published `pullfrog` resolves it.
109732
+ "step-5-preview": {
109733
+ displayName: "Step 5 Preview",
109734
+ description: "StepFun preview, free for a limited time",
109735
+ resolve: "opencode/step-5-preview-free",
109736
+ effort: ["low", "medium", "high"],
109737
+ // free to run, still gated on the provider's own OPENCODE_API_KEY —
109738
+ // see the big-pickle note above (#1077).
109739
+ isFree: true,
109740
+ hidden: true
109741
+ },
109703
109742
  // Zen's live free MiMo, and the second free row in a menu that big-pickle
109704
109743
  // was alone in since `mimo-v2-pro-free` lost its model.
109705
109744
  mimo: {
@@ -109984,6 +110023,9 @@ var providers = {
109984
110023
  "claude-haiku": {
109985
110024
  displayName: "Claude Haiku",
109986
110025
  resolve: "openrouter/~anthropic/claude-haiku-latest",
110026
+ // the rolling alias moved to Haiku 5.5 on 2026-10-07, which publishes
110027
+ // the ladder Haiku 4.5 did not.
110028
+ effort: ["low", "medium", "high", "xhigh", "max"],
109987
110029
  openRouterResolve: "openrouter/~anthropic/claude-haiku-latest"
109988
110030
  },
109989
110031
  // pinned to the explicit Sol/Terra/Luna tiers (not the ~openai/gpt-latest
@@ -110155,6 +110197,14 @@ var providers = {
110155
110197
  resolve: "openrouter/meta/muse-spark-1.3-contributor",
110156
110198
  effort: ["minimal", "low", "medium", "high", "xhigh", "max"],
110157
110199
  openRouterResolve: "openrouter/meta/muse-spark-1.3-contributor"
110200
+ },
110201
+ // Mistral's flagship (public preview, 2026-10-06), on the routers and
110202
+ // Zen only: the catalog carries no direct Mistral provider.
110203
+ "mistral-large": {
110204
+ displayName: "Mistral Large",
110205
+ resolve: "openrouter/mistralai/mistral-large-4-0",
110206
+ effort: ["none", "high"],
110207
+ openRouterResolve: "openrouter/mistralai/mistral-large-4-0"
110158
110208
  }
110159
110209
  }
110160
110210
  }),
@@ -110185,7 +110235,8 @@ var providers = {
110185
110235
  },
110186
110236
  "claude-haiku": {
110187
110237
  displayName: "Claude Haiku",
110188
- resolve: "vercel/anthropic/claude-haiku-4.5"
110238
+ resolve: "vercel/anthropic/claude-haiku-5.5",
110239
+ effort: ["none", "low", "medium", "high", "xhigh", "max"]
110189
110240
  },
110190
110241
  "gpt-sol": {
110191
110242
  displayName: "GPT Sol",
@@ -110246,6 +110297,11 @@ var providers = {
110246
110297
  displayName: "Muse Spark",
110247
110298
  resolve: "vercel/meta/muse-spark-1.3",
110248
110299
  effort: ["minimal", "low", "medium", "high", "xhigh"]
110300
+ },
110301
+ "mistral-large": {
110302
+ displayName: "Mistral Large",
110303
+ resolve: "vercel/mistral/mistral-large-4",
110304
+ effort: ["none", "low", "medium", "high"]
110249
110305
  }
110250
110306
  }
110251
110307
  })
@@ -111126,7 +111182,13 @@ var PROVIDER_ERROR_PATTERNS = [
111126
111182
  // …/settings/credits`) matched neither this nor `requires more credits`, so
111127
111183
  // the one run in that episode that was a genuinely empty wallet rendered with
111128
111184
  // no CTA at all while runs 1-6 rendered correctly.
111129
- { regex: /Insufficient (?:balance|credits)/i, label: PROVIDER_BILLING_EXHAUSTED_LABEL },
111185
+ // `account funds` is Zen's (#1474).
111186
+ {
111187
+ regex: /Insufficient (?:balance|credits|account funds)/i,
111188
+ label: PROVIDER_BILLING_EXHAUSTED_LABEL
111189
+ },
111190
+ // OpenAI's `You have no credits remaining` (#1474).
111191
+ { regex: /no credits remaining/i, label: PROVIDER_BILLING_EXHAUSTED_LABEL },
111130
111192
  { regex: /credit balance is too low/i, label: PROVIDER_BILLING_EXHAUSTED_LABEL },
111131
111193
  // "spending cap" (Gemini) and "spending limit" (xAI, #1076) are the same
111132
111194
  // condition in two house styles — a configured ceiling was reached.
@@ -111247,6 +111309,9 @@ function isRouterKeylimitExhaustedError(text) {
111247
111309
  function isProviderBillingExhausted(text) {
111248
111310
  return findProviderErrorMatch(text)?.label === PROVIDER_BILLING_EXHAUSTED_LABEL;
111249
111311
  }
111312
+ function isProviderUsageLimit(text) {
111313
+ return findProviderErrorMatch(text)?.label === PROVIDER_USAGE_LIMIT_LABEL;
111314
+ }
111250
111315
  function isProviderNoRoutableEndpoints(text) {
111251
111316
  return findProviderErrorMatch(text)?.label === PROVIDER_NO_ENDPOINTS_LABEL;
111252
111317
  }
@@ -111505,6 +111570,7 @@ ${this.buffer}`;
111505
111570
  };
111506
111571
  async function spawn3(options) {
111507
111572
  const activityTimeoutMs = options.activityTimeout ?? DEFAULT_ACTIVITY_TIMEOUT_MS;
111573
+ options.signal?.throwIfAborted();
111508
111574
  installSignalHandler();
111509
111575
  const startTime = performance3.now();
111510
111576
  const retain = options.retain ?? "tail";
@@ -111540,17 +111606,18 @@ async function spawn3(options) {
111540
111606
  let isActivityTimedOut = false;
111541
111607
  let lastActivityTime = performance3.now();
111542
111608
  let killedAtIdleMs;
111609
+ const terminate = () => {
111610
+ if (sigkillEscalatorId) return;
111611
+ killSelf("SIGTERM");
111612
+ sigkillEscalatorId = setTimeout(() => killSelf("SIGKILL"), 5e3);
111613
+ };
111543
111614
  if (options.timeout) {
111544
111615
  timeoutId = setTimeout(() => {
111545
111616
  isTimedOut = true;
111546
- killSelf("SIGTERM");
111547
- sigkillEscalatorId = setTimeout(() => {
111548
- if (!child.killed) {
111549
- killSelf("SIGKILL");
111550
- }
111551
- }, 5e3);
111617
+ terminate();
111552
111618
  }, options.timeout);
111553
111619
  }
111620
+ options.signal?.addEventListener("abort", terminate, { once: true });
111554
111621
  if (activityTimeoutMs > 0) {
111555
111622
  log2.debug(
111556
111623
  `spawn activity timer: pid=${child.pid} cmd=${options.cmd} timeout=${activityTimeoutMs}ms`
@@ -111615,6 +111682,11 @@ async function spawn3(options) {
111615
111682
  if (timeoutId) clearTimeout(timeoutId);
111616
111683
  if (sigkillEscalatorId) clearTimeout(sigkillEscalatorId);
111617
111684
  if (activityCheckIntervalId) clearInterval(activityCheckIntervalId);
111685
+ options.signal?.removeEventListener("abort", terminate);
111686
+ if (options.signal?.aborted) {
111687
+ reject(options.signal.reason);
111688
+ return;
111689
+ }
111618
111690
  if (isTimedOut) {
111619
111691
  reject(
111620
111692
  new SpawnTimeoutError(`process timed out after ${options.timeout}ms`, SPAWN_TIMEOUT_CODE)
@@ -111653,6 +111725,7 @@ ${killMsg}` : killMsg;
111653
111725
  if (timeoutId) clearTimeout(timeoutId);
111654
111726
  if (sigkillEscalatorId) clearTimeout(sigkillEscalatorId);
111655
111727
  if (activityCheckIntervalId) clearInterval(activityCheckIntervalId);
111728
+ options.signal?.removeEventListener("abort", terminate);
111656
111729
  const errMsg = `[spawn] ${options.cmd}: ${error49.message}`;
111657
111730
  console.error(errMsg);
111658
111731
  const existingStderr = stderrBuffer?.toString() ?? "";
@@ -111894,17 +111967,16 @@ Report findings clearly with file:line references and quoted evidence where poss
111894
111967
  // modes.ts
111895
111968
  var PR_SUMMARY_FORMAT = `### Default format
111896
111969
 
111897
- The body has at most three parts, in this order:
111970
+ The body has at most two parts, in this order:
111898
111971
 
111899
111972
  1. **Reviewed changes preamble** \u2014 a bolded \`**Reviewed changes**\` lead-in with one sentence on what was reviewed in this run (for \`IncrementalReview\`: what changed since the prior pullfrog review), then a bullet list of the substantive changes \u2014 short bolded title, one sentence each. A reviewer should understand the full reviewed scope from this list alone. Close the preamble with the metadata comment below.
111900
111973
  2. **Cross-cutting issue sections** (zero or more) \u2014 one \`### {emoji} {what's wrong, not what to do}\` heading per concern.
111901
- 3. **\`### \u2139\uFE0F Nitpicks\`** at the very bottom, if any \u2014 a flat bullet list, no technical-details block. Only for what has no runtime effect: a stale or wrong comment or doc, a misleading name, a typo, dead code, a missing test. Never formatting or whitespace.
111902
111974
 
111903
111975
  **Inline vs. body.** Concerns that anchor to a specific line go inline (the \`comments\` parameter), even when their implications are broad. Body \`### \` sections are reserved for concerns that have **no line to anchor to** \u2014 *absence* (something the diff should have done but didn't), *sequencing* (rollout / deletion / migration order), *design decisions only the human can make*, or *scope questions the diff raises but doesn't address*. With no non-anchorable concerns, the body is just the preamble + metadata.
111904
111976
 
111905
111977
  **Severity emoji** on every \`### \` heading, and nowhere else: \u{1F6A8} critical (blocks merge \u2014 data loss, security, broken core flow) \xB7 \u26A0\uFE0F important (must address before merging) \xB7 \u2139\uFE0F informational (mergeable as-is).
111906
111978
 
111907
- **Behavior decides placement, not fix size.** A finding that changes what the code does \u2014 a wrong value or message, a crash, a race, a leak, a missed cleanup, a slow path \u2014 gets its own inline comment (or a \`### \` section if no line anchors it) at its real severity, however small the fix. Never put it in Nitpicks: before submitting, re-read each Nitpicks bullet and move any that fails this test inline.
111979
+ **Behavior decides placement, not fix size.** A finding that changes what the code does \u2014 a wrong value or message, a crash, a race, a leak, a missed cleanup, a slow path \u2014 gets its own inline comment (or a \`### \` section if no line anchors it) at its real severity, however small the fix.
111908
111980
 
111909
111981
  **Blank line between every block-level element.** GitHub's markdown parser requires one before and after HTML tags (\`<details>\`, \`<summary>\`, \`<sub>\`, \`<br/>\`) \u2014 without it GitHub treats what follows as a continuation of the HTML block and renders your markdown as literal text. This is a parser quirk, not a style preference, and it permanently breaks the posted review.
111910
111982
 
@@ -112053,8 +112125,6 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
112053
112125
 
112054
112126
  3. **triage**: orient yourself on the PR \u2014 identify *what kind of thing this is* (domain it touches, seams it crosses, external contracts it depends on, user-facing surfaces it changes). pull as much context as you need to render a confident, well-grounded review: read related files, grep for callers of changed symbols, check tests that exercise the touched paths, fetch related GitHub state. **you are the synthesizer** \u2014 never delegate understanding to subagents.
112055
112127
 
112056
- when the diff adds or changes a test, check that it can actually fail: a test that would still pass with the bug present is theatre, not coverage. the usual tell is a loose assertion standing where an exact one belongs \u2014 \`>=\` or a truthiness check over an expected value, or a snapshot that absorbs whatever it is handed. read the assertion against the behavior it claims to pin, not against whether it currently passes.
112057
-
112058
112128
  skip the deeper pass and submit a \`No new issues found.\` review per step 7 only when the diff has **no behavioral surface at all** \u2014 doc typos, whitespace/formatting, lockfile or generated-code regeneration, a mechanical rename whose only effect is import-path updates. line count is not the signal: a one-line change to auth, money, SQL, a comparison operator, a redirect, or a config default is not trivial.
112059
112129
 
112060
112130
  4. **specialist decision**: after reading the complete diff, name the questions you still cannot answer confidently yourself, and dispatch one \`${REVIEWER_AGENT_NAME}\` specialist per question. a question qualifies only when a specialist could return evidence that **changes your disposition** on the PR \u2014 generic requests for another look, extra confidence, or polish do not. most reviews need zero or one; some need several.
@@ -112078,8 +112148,6 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
112078
112148
 
112079
112149
  6. **aggregate & draft**: when specialist results land, merge findings; de-dup overlaps (two specialists catching the same issue = higher-confidence signal); trace each finding yourself before accepting it. drop praise, style preferences, speculative/unverified claims, findings about pre-existing code unrelated to the PR (heuristic: if the finding's root cause lives in lines this PR added or modified, it's in scope; otherwise drop unless the PR plausibly introduced or amplified the regression), and anything not actionable. also drop **bloat-shaped findings** \u2014 proposed fixes that would add defensive checks for cases that can't happen, abstractions used once, comments restating obvious code, tests asserting tautologies, or "just-in-case" guards. subagents are fallible and bias toward recommending changes; the bar for an actionable inline comment is sound + correct + elegant. recommending a change that improves only one of the three (or worse, degrades elegance to nominally improve correctness) makes the codebase worse, not better.
112080
112150
 
112081
- **Hunt for non-anchored concerns before drafting.** After collecting your anchored findings, deliberately scan for concerns that have no specific line to point at \u2014 typically: deletion / cleanup plans for code the diff replaces or shadows; rollout sequencing (what happens to in-flight state during deploy / revert?); scope questions that only the human can answer (e.g. is the legacy path going away or is this a long-term dual track?); architectural risks the diff opens up that aren't a single-line bug.
112082
-
112083
112151
  for surviving findings, draft inline comments with NEW line numbers from the diff \u2014 attach a \`<details>Technical details</details>\` block to any inline comment whose fix is non-trivial or has cross-file implications (see Inline technical details in the format below). every comment must be actionable, 2-3 sentences max in the visible part, one per root cause \u2014 name its other sites in that comment rather than repeating it. use GitHub permalink format for code references. for impact-analysis findings (stale references after rename/remove), report them in the review body ordered by severity (runtime breakage > incorrect docs > stale comments) rather than as inline comments unless they're anchored to a specific line.
112084
112152
 
112085
112153
  7. **submit**: ALWAYS submit exactly one review via \`${t2("create_pull_request_review")}\`. Do NOT call \`report_progress\` \u2014 the review is the final record and the progress comment will be cleaned up automatically.
@@ -112101,7 +112169,7 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
112101
112169
  \`approved: false\`. Body opens with \`> [!CAUTION]\\n> This PR introduces ...\`, followed by the PR summary. Include all inline comments via \`comments\`.
112102
112170
  - **must-address non-critical findings** (real consequences if shipped \u2014 incorrect behavior in non-critical paths, missing validation on user input, regressions the author should fix before merge):
112103
112171
  \`approved: false\`. Body opens with \`> [!IMPORTANT]\\n> ...\`, followed by the PR summary. Reserve this tier for findings with concrete fallout \u2014 do NOT use \`[!IMPORTANT]\` for nits, style preferences, or "consider also" suggestions. Include all inline comments via \`comments\`.
112104
- - **minor suggestions only** (low-stakes findings, or Nitpicks only):
112172
+ - **minor suggestions only** (low-stakes findings):
112105
112173
  \`approved: false\`. Body opens with \`> \u2139\uFE0F No critical issues \u2014 minor suggestions inline.\\n\\n\` followed by the PR summary. Include all inline comments via \`comments\`. Vary the wording after the emoji to fit the review (e.g. "Minor suggestions only.", "Two rough edges worth a look."), but always keep the \u2139\uFE0F prefix and keep it short.
112106
112174
  - **informational observations** (mergeable as-is, nothing actionable \u2014 e.g. prior feedback addressed cleanly, calling out something noteworthy without recommending a change):
112107
112175
  \`approved: true\`. Body opens with \`> \u2705 No new issues found.\\n\\n\` followed by the PR summary. Do NOT include inline \`comments\` \u2014 the \u2705 signals "no action needed", which contradicts an actionable anchor; if a point is concrete enough to anchor to a line, downgrade the whole review to "minor suggestions only" (\`approved: false\`) instead.
@@ -112144,8 +112212,6 @@ ${PR_SUMMARY_FORMAT}`
112144
112212
 
112145
112213
  5. **triage**: orient on the *incremental* changes \u2014 domain, seams, external contracts, user-facing surfaces. pull as much context as you need to render a confident review: read related files, grep for callers of changed symbols, check tests that exercise the touched paths. **you are the synthesizer.**
112146
112214
 
112147
- a test added or changed in this delta must be able to fail \u2014 one that would still pass with the bug present is theatre, not coverage. the tell is a loose assertion where an exact one belongs (\`>=\` or a truthiness check over an expected value, a snapshot that absorbs whatever it is handed).
112148
-
112149
112215
  skip the deeper pass and jump to step 10's non-substantive path (do NOT submit a review) only when the incremental changes have **no behavioral surface at all** \u2014 formatting, comment tweaks, import reordering, lockfile regen, a mechanical rename of import paths. line count is not the signal: a one-line change to auth, money, SQL, a comparison operator, a redirect, or a config default is not trivial.
112150
112216
 
112151
112217
  6. **specialist decision**: after covering the incremental and full diffs, name the questions about the new changes that you still cannot answer confidently yourself, and dispatch one \`${REVIEWER_AGENT_NAME}\` specialist per question. a question qualifies only when a specialist could return evidence that **changes your disposition** on the PR \u2014 generic requests for another look, extra confidence, or polish do not. most incremental reviews need zero or one, especially thread-reply re-reviews; some need several.
@@ -112169,11 +112235,9 @@ ${PR_SUMMARY_FORMAT}`
112169
112235
 
112170
112236
  8. **aggregate, draft, self-critique**: merge findings (yours + output from every specialist you dispatched); de-dup overlaps; trace each finding yourself. drop praise, style preferences, speculative/unverified claims, findings about pre-existing code unrelated to the new commits, anything not actionable, and anything that re-states prior review feedback (heuristic: if the finding's root cause lives in lines the *new commits* added or modified, it's in scope; otherwise drop). also drop **bloat-shaped findings** \u2014 proposed fixes that would add defensive checks for cases that can't happen, abstractions used once, comments restating obvious code, tests asserting tautologies, or "just-in-case" guards. subagents are fallible and bias toward recommending changes; the bar for an actionable inline comment is sound + correct + elegant. recommending a change that improves only one of the three (or degrades elegance to nominally improve correctness) makes the codebase worse, not better. To compute "lines the new commits added or modified": if \`incrementalDiffPath\` from step 2 is present, use it directly. Otherwise, take the prior Pullfrog review's \`commit_id\` (returned alongside each entry from \`${t2("list_pull_request_reviews")}\` in step 4) and run \`git diff <prior-review-sha>..HEAD\` to isolate the lines added since that review.
112171
112237
 
112172
- **Hunt for non-anchored concerns before drafting.** After collecting your anchored findings, deliberately scan for concerns that have no specific line to point at \u2014 typically: deletion / cleanup plans for code the new commits replace or shadow; rollout sequencing (what happens to in-flight state during deploy / revert?); scope questions that only the human can answer (e.g. is the legacy path going away or is this a long-term dual track?); architectural risks the new commits open up that aren't a single-line bug.
112173
-
112174
112238
  draft inline comments with NEW line numbers from the full PR diff \u2014 attach a \`<details>Technical details</details>\` block to any inline comment whose fix is non-trivial or has cross-file implications (see Inline technical details in the format below). every comment must be actionable, 2-3 sentences max in the visible part, one per root cause \u2014 name its other sites in that comment rather than repeating it.
112175
112239
 
112176
- 9. **build the review body**: use the same default format as Review mode (preamble + optional cross-cutting \`### \` sections + optional \`### \u2139\uFE0F Nitpicks\`) \u2014 scoped to the **incremental delta**, not the full PR. The "Reviewed changes" bullets describe what changed since the prior pullfrog review (each bullet starts with a past-tense verb, e.g. \`- Extracted shared CLI runtime into a single module\`). Do NOT include a separate "Prior review feedback" checklist \u2014 that's tracked in the rolling PR summary snapshot for the next agent run, and surfacing it in the user-facing body is noise (changes that addressed prior feedback are already covered by the Reviewed-changes bullets). In some cases you may receive a complete diff for the whole PR instead of an incremental one; when this happens, determine what changed since Pullfrog's most recent review yourself before drafting bullets.
112240
+ 9. **build the review body**: use the same default format as Review mode (preamble + optional cross-cutting \`### \` sections) \u2014 scoped to the **incremental delta**, not the full PR. The "Reviewed changes" bullets describe what changed since the prior pullfrog review (each bullet starts with a past-tense verb, e.g. \`- Extracted shared CLI runtime into a single module\`). Do NOT include a separate "Prior review feedback" checklist \u2014 that's tracked in the rolling PR summary snapshot for the next agent run, and surfacing it in the user-facing body is noise (changes that addressed prior feedback are already covered by the Reviewed-changes bullets). In some cases you may receive a complete diff for the whole PR instead of an incremental one; when this happens, determine what changed since Pullfrog's most recent review yourself before drafting bullets.
112177
112241
 
112178
112242
  10. Submit \u2014 every run must end with EXACTLY ONE of \`${t2("create_pull_request_review")}\` (substantive review) or \`${t2("report_progress")}\` (no-review acknowledgement). do NOT call \`create_issue_comment\` for review output.
112179
112243
 
@@ -112184,7 +112248,7 @@ ${PR_SUMMARY_FORMAT}`
112184
112248
  - IF NO NEW ISSUES, NON-SUBSTANTIVE CHANGES ONLY (trivial formatting, import reordering, comment tweaks): do NOT submit a review. Instead call \`${t2("report_progress")}\` with \`no_review_needed: true\` and a 1-2 sentence note explaining no review was warranted (e.g. "No new issues. Changes since last review are formatting-only."). this leaves a visible signal that the run completed.
112185
112249
  - ELSE IF NEW CRITICAL ISSUES (blocks merge \u2014 bugs, security, data loss, broken core flows): call \`${t2("create_pull_request_review")}\` with \`approved: false\`, all comments, and the review body. body opens with \`> [!CAUTION]\\n> This PR introduces ...\`, followed by the PR summary using the default format below.
112186
112250
  - ELSE IF NEW MUST-ADDRESS NON-CRITICAL FINDINGS (real consequences if shipped \u2014 incorrect behavior, missing validation, regressions the author should fix before merge): call \`${t2("create_pull_request_review")}\` with \`approved: false\`, all comments, and the review body. body opens with \`> [!IMPORTANT]\\n> ...\`, followed by the PR summary using the default format below. Do NOT use this tier for nits, style preferences, or "consider also" suggestions.
112187
- - ELSE IF NEW MINOR SUGGESTIONS ONLY (low-stakes findings, or Nitpicks only): call \`${t2("create_pull_request_review")}\` with \`approved: false\`, all comments, and the review body. body opens with \`> \u2139\uFE0F No critical issues \u2014 minor suggestions inline.\\n\\n\` (vary the wording after \u2139\uFE0F to fit the review), followed by the PR summary using the default format below.
112251
+ - ELSE IF NEW MINOR SUGGESTIONS ONLY (low-stakes findings): call \`${t2("create_pull_request_review")}\` with \`approved: false\`, all comments, and the review body. body opens with \`> \u2139\uFE0F No critical issues \u2014 minor suggestions inline.\\n\\n\` (vary the wording after \u2139\uFE0F to fit the review), followed by the PR summary using the default format below.
112188
112252
  - ELSE IF INFORMATIONAL OBSERVATIONS (mergeable as-is, but worth surfacing \u2014 e.g. prior feedback addressed cleanly, or a noteworthy positive observation): call \`${t2("create_pull_request_review")}\` with \`approved: true\`, NO inline comments, and the review body. body opens with \`> \u2705 No new issues found.\\n\\n\` (or similar friendly green opener), followed by the PR summary using the default format below. If a point is concrete enough to anchor to a line, downgrade the whole review to "minor suggestions only" (\`approved: false\`) instead \u2014 the \u2705 signals "no action needed", which contradicts an actionable anchor.
112189
112253
  - ELSE IF NO NEW ISSUES, SUBSTANTIVE CHANGES (new functionality, behavior changes, or fixes to prior review feedback): call \`${t2("create_pull_request_review")}\` to create a PR review. If all previous reviews have been properly addressed and no new issues were discovered, set \`approved: true\`. body opens with \`> \u2705 No new issues found.\\n\\n\`, followed by the PR summary using the default format below.
112190
112254
 
@@ -113401,16 +113465,253 @@ import { performance as performance6 } from "node:perf_hooks";
113401
113465
 
113402
113466
  // utils/codexHome.ts
113403
113467
  import { execFileSync as execFileSync4 } from "node:child_process";
113404
- import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync5 } from "node:fs";
113468
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, mkdtempSync as mkdtempSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "node:fs";
113405
113469
  import { homedir as homedir2, userInfo } from "node:os";
113406
113470
  import { join as join6 } from "node:path";
113471
+
113472
+ // utils/oauthWriteback.ts
113473
+ var core2 = __toESM(require_core(), 1);
113474
+ import { existsSync as existsSync5, readFileSync as readFileSync3, rmSync as rmSync3 } from "node:fs";
113475
+
113476
+ // utils/apiUrl.ts
113477
+ function isLocalUrl(url4) {
113478
+ return url4.hostname === "localhost" || url4.hostname === "127.0.0.1";
113479
+ }
113480
+ function getApiUrl() {
113481
+ const raw2 = process.env.API_URL || "https://pullfrog.com";
113482
+ const parsed2 = new URL(raw2);
113483
+ if (parsed2.protocol !== "https:" && !isLocalUrl(parsed2)) {
113484
+ throw new Error(
113485
+ `API_URL must use https:// (got ${parsed2.protocol}). only localhost is exempt.`
113486
+ );
113487
+ }
113488
+ log2.debug(`resolved API_URL: ${raw2}`);
113489
+ return raw2;
113490
+ }
113491
+ function isLocalApiUrl() {
113492
+ try {
113493
+ return isLocalUrl(new URL(getApiUrl()));
113494
+ } catch {
113495
+ return false;
113496
+ }
113497
+ }
113498
+
113499
+ // utils/apiFetch.ts
113500
+ async function apiFetch(options) {
113501
+ const apiUrl = getApiUrl();
113502
+ const url4 = new URL(options.path, apiUrl);
113503
+ const bypassSecret = process.env.VERCEL_AUTOMATION_BYPASS_SECRET;
113504
+ if (bypassSecret) {
113505
+ url4.searchParams.set("x-vercel-protection-bypass", bypassSecret);
113506
+ }
113507
+ const headers = {
113508
+ ...options.headers
113509
+ };
113510
+ if (bypassSecret) {
113511
+ headers["x-vercel-protection-bypass"] = bypassSecret;
113512
+ }
113513
+ if (!options.body) {
113514
+ for (const key of Object.keys(headers)) {
113515
+ if (key.toLowerCase() === "content-type") delete headers[key];
113516
+ }
113517
+ }
113518
+ log2.debug(`api fetch: ${options.method ?? "GET"} ${url4.pathname}`);
113519
+ const init = {
113520
+ method: options.method ?? "GET",
113521
+ headers
113522
+ };
113523
+ if (options.body) init.body = options.body;
113524
+ if (options.signal) init.signal = options.signal;
113525
+ return fetch(url4.toString(), init);
113526
+ }
113527
+
113528
+ // utils/codexRefreshDetect.ts
113529
+ function detectCodexRefresh(params) {
113530
+ let parsed2;
113531
+ try {
113532
+ parsed2 = JSON.parse(params.authFileContent);
113533
+ } catch {
113534
+ return null;
113535
+ }
113536
+ if (!parsed2 || typeof parsed2 !== "object") return null;
113537
+ const root = parsed2;
113538
+ const tokens = readCodexTokens(root) ?? readOpenCodeTokens(root);
113539
+ if (!tokens) return null;
113540
+ if (tokens.refresh_token === params.originalRefresh) return null;
113541
+ const idToken = tokens.id_token ?? params.originalIdToken;
113542
+ const codexShape = {
113543
+ auth_mode: "chatgpt",
113544
+ tokens: {
113545
+ ...idToken ? { id_token: idToken } : {},
113546
+ access_token: tokens.access_token,
113547
+ refresh_token: tokens.refresh_token,
113548
+ ...tokens.account_id ? { account_id: tokens.account_id } : {}
113549
+ },
113550
+ last_refresh: (/* @__PURE__ */ new Date()).toISOString()
113551
+ };
113552
+ return `${JSON.stringify(codexShape, null, 2)}
113553
+ `;
113554
+ }
113555
+ function readCodexTokens(root) {
113556
+ if (root.auth_mode !== "chatgpt") return null;
113557
+ const tokens = root.tokens;
113558
+ if (!tokens || typeof tokens !== "object") return null;
113559
+ const t2 = tokens;
113560
+ if (typeof t2.access_token !== "string" || typeof t2.refresh_token !== "string") return null;
113561
+ return {
113562
+ access_token: t2.access_token,
113563
+ refresh_token: t2.refresh_token,
113564
+ id_token: typeof t2.id_token === "string" ? t2.id_token : void 0,
113565
+ account_id: typeof t2.account_id === "string" ? t2.account_id : void 0
113566
+ };
113567
+ }
113568
+ function readOpenCodeTokens(root) {
113569
+ const oauth = root.openai;
113570
+ if (!oauth || typeof oauth !== "object") return null;
113571
+ const o = oauth;
113572
+ if (o.type !== "oauth") return null;
113573
+ if (typeof o.refresh !== "string" || typeof o.access !== "string") return null;
113574
+ return {
113575
+ access_token: o.access,
113576
+ refresh_token: o.refresh,
113577
+ id_token: void 0,
113578
+ account_id: typeof o.accountId === "string" ? o.accountId : void 0
113579
+ };
113580
+ }
113581
+ function detectXaiRefresh(params) {
113582
+ let parsed2;
113583
+ try {
113584
+ parsed2 = JSON.parse(params.authFileContent);
113585
+ } catch {
113586
+ return null;
113587
+ }
113588
+ if (!parsed2 || typeof parsed2 !== "object") return null;
113589
+ const oauth = parsed2.xai;
113590
+ if (!oauth || typeof oauth !== "object") return null;
113591
+ const o = oauth;
113592
+ if (o.type !== "oauth") return null;
113593
+ if (typeof o.access !== "string" || typeof o.refresh !== "string") return null;
113594
+ if (o.refresh === params.originalRefresh) return null;
113595
+ const grokShape = {
113596
+ auth_mode: "grok",
113597
+ tokens: { access_token: o.access, refresh_token: o.refresh },
113598
+ last_refresh: (/* @__PURE__ */ new Date()).toISOString()
113599
+ };
113600
+ return `${JSON.stringify(grokShape, null, 2)}
113601
+ `;
113602
+ }
113603
+
113604
+ // utils/oauthWriteback.ts
113605
+ var OAUTH_WRITEBACK_STATE = "oauth_writeback";
113606
+ var SUBSCRIPTION_DATA_DIR_STATE = "subscription_data_dir";
113607
+ async function runOAuthWriteback() {
113608
+ await writeBackEntries();
113609
+ const dataDir = core2.getState(SUBSCRIPTION_DATA_DIR_STATE);
113610
+ if (!dataDir) return;
113611
+ rmSync3(dataDir, { recursive: true, force: true });
113612
+ core2.info(`oauth post-hook: removed ${dataDir}`);
113613
+ }
113614
+ async function writeBackEntries() {
113615
+ const raw2 = core2.getState(OAUTH_WRITEBACK_STATE);
113616
+ if (!raw2) {
113617
+ core2.info("oauth post-hook: no writeback state \u2014 skipping");
113618
+ return;
113619
+ }
113620
+ let state;
113621
+ try {
113622
+ state = JSON.parse(raw2);
113623
+ } catch (err) {
113624
+ core2.warning(`oauth post-hook: malformed writeback state \u2014 ${err}`);
113625
+ return;
113626
+ }
113627
+ if (!state.apiToken || !Array.isArray(state.entries)) {
113628
+ core2.warning("oauth post-hook: incomplete writeback state \u2014 skipping");
113629
+ return;
113630
+ }
113631
+ for (const entry of state.entries) {
113632
+ await writeBackEntry(state.apiToken, entry);
113633
+ }
113634
+ }
113635
+ async function writeBackEntry(apiToken, entry) {
113636
+ if (!entry.secretName || !entry.authPath || !entry.originalRefresh) {
113637
+ core2.warning("oauth post-hook: incomplete writeback entry \u2014 skipping");
113638
+ return;
113639
+ }
113640
+ if (!existsSync5(entry.authPath)) {
113641
+ core2.info(`oauth post-hook: ${entry.authPath} not found \u2014 nothing to write back`);
113642
+ return;
113643
+ }
113644
+ let authFileContent;
113645
+ try {
113646
+ authFileContent = readFileSync3(entry.authPath, "utf8");
113647
+ } catch (err) {
113648
+ core2.warning(`oauth post-hook: cannot read ${entry.authPath} \u2014 ${err}`);
113649
+ return;
113650
+ }
113651
+ const refreshed = entry.provider === "xai" ? detectXaiRefresh({
113652
+ authFileContent,
113653
+ originalRefresh: entry.originalRefresh
113654
+ }) : detectCodexRefresh({
113655
+ authFileContent,
113656
+ originalRefresh: entry.originalRefresh,
113657
+ originalIdToken: entry.originalIdToken
113658
+ });
113659
+ if (!refreshed) {
113660
+ core2.info(`oauth post-hook: ${entry.secretName} chain unchanged \u2014 no writeback needed`);
113661
+ return;
113662
+ }
113663
+ try {
113664
+ const poolState = core2.getState("credential_receipts");
113665
+ const receipts2 = external_exports.record(external_exports.string(), external_exports.string()).parse(JSON.parse(poolState || "{}"));
113666
+ const receipt = receipts2[entry.secretName];
113667
+ if (poolState && !receipt) return;
113668
+ const response = await apiFetch({
113669
+ path: receipt ? "/api/runtime/credentials" : "/api/runtime/secret",
113670
+ method: "PUT",
113671
+ headers: {
113672
+ authorization: `Bearer ${apiToken}`,
113673
+ "content-type": "application/json"
113674
+ },
113675
+ body: JSON.stringify({ name: entry.secretName, value: refreshed, receipt }),
113676
+ // the workflow is already finished; a hung write-back would hold the
113677
+ // runner open for nothing.
113678
+ signal: AbortSignal.timeout(3e4)
113679
+ });
113680
+ if (!response.ok) {
113681
+ const body = await response.text().catch(() => "");
113682
+ core2.warning(`oauth post-hook: writeback returned ${response.status}: ${body}`);
113683
+ return;
113684
+ }
113685
+ core2.info(`oauth post-hook: refreshed ${entry.secretName} persisted to Pullfrog`);
113686
+ } catch (err) {
113687
+ core2.warning(`oauth post-hook: writeback failed \u2014 ${err}`);
113688
+ }
113689
+ }
113690
+
113691
+ // utils/secretCommands.ts
113692
+ var core3 = __toESM(require_core(), 1);
113693
+ function maskSecret(value2) {
113694
+ if (isGitHubActions) core3.setSecret(value2);
113695
+ }
113696
+ function saveSecretState(name, value2) {
113697
+ if (isGitHubActions) core3.saveState(name, value2);
113698
+ }
113699
+ function setSecretOutput(name, value2) {
113700
+ if (isGitHubActions) core3.setOutput(name, value2);
113701
+ }
113702
+
113703
+ // utils/codexHome.ts
113407
113704
  var CODEX_AUTH_ENV = "CODEX_AUTH_JSON";
113408
113705
  var XAI_AUTH_ENV = "GROK_AUTH_JSON";
113409
113706
  var installedSubscriptionPaths = /* @__PURE__ */ new Map();
113707
+ var installedDataHome;
113708
+ function getSubscriptionDataHome() {
113709
+ return installedDataHome;
113710
+ }
113410
113711
  function clearInstalledSubscription(name) {
113411
113712
  const path4 = installedSubscriptionPaths.get(name);
113412
- if (!path4 || !existsSync5(path4)) return;
113413
- const value2 = JSON.parse(readFileSync3(path4, "utf8"));
113713
+ if (!path4 || !existsSync6(path4)) return;
113714
+ const value2 = JSON.parse(readFileSync4(path4, "utf8"));
113414
113715
  if (!value2 || typeof value2 !== "object") return;
113415
113716
  if (name === "CODEX_AUTH_JSON" && "openai" in value2) delete value2.openai;
113416
113717
  if (name === "GROK_AUTH_JSON" && "xai" in value2) delete value2.xai;
@@ -113452,21 +113753,20 @@ function installCodexAuth() {
113452
113753
  ...body.tokens.account_id ? { accountId: body.tokens.account_id } : {}
113453
113754
  }
113454
113755
  });
113455
- process.env.XDG_DATA_HOME = xdgDataHome;
113756
+ installedDataHome = xdgDataHome;
113456
113757
  log2.info(`\xBB installed Codex auth at ${authPath}`);
113457
113758
  installedSubscriptionPaths.set(CODEX_AUTH_ENV, authPath);
113458
113759
  return {
113459
113760
  authPath,
113460
- xdgDataHome,
113461
113761
  originalRefresh: body.tokens.refresh_token,
113462
113762
  originalIdToken: body.tokens.id_token
113463
113763
  };
113464
113764
  }
113465
113765
  function writeOpenCodeAuthEntry(params) {
113466
113766
  let existing = {};
113467
- if (existsSync5(params.authPath)) {
113767
+ if (existsSync6(params.authPath)) {
113468
113768
  try {
113469
- const parsed2 = JSON.parse(readFileSync3(params.authPath, "utf8"));
113769
+ const parsed2 = JSON.parse(readFileSync4(params.authPath, "utf8"));
113470
113770
  if (parsed2 && typeof parsed2 === "object") existing = parsed2;
113471
113771
  } catch {
113472
113772
  log2.warning(`\xBB ${params.authPath} was unreadable; rewriting it`);
@@ -113505,10 +113805,10 @@ function installXaiAuth() {
113505
113805
  expires: decodeJwtExpMs(body.tokens.access_token) ?? 0
113506
113806
  }
113507
113807
  });
113508
- process.env.XDG_DATA_HOME = xdgDataHome;
113808
+ installedDataHome = xdgDataHome;
113509
113809
  log2.info(`\xBB installed Grok auth at ${authPath}`);
113510
113810
  installedSubscriptionPaths.set(XAI_AUTH_ENV, authPath);
113511
- return { authPath, xdgDataHome, originalRefresh: body.tokens.refresh_token };
113811
+ return { authPath, originalRefresh: body.tokens.refresh_token };
113512
113812
  }
113513
113813
  function isXaiRejectedChain(body) {
113514
113814
  if (!body.refresh_rejected_at) return false;
@@ -113575,230 +113875,11 @@ function bootstrapPullfrogDataDir() {
113575
113875
  }
113576
113876
  return null;
113577
113877
  }
113578
- return PULLFROG_DATA_DIR;
113579
- }
113580
-
113581
- // utils/oauthWriteback.ts
113582
- var core2 = __toESM(require_core(), 1);
113583
- import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:fs";
113584
-
113585
- // utils/apiUrl.ts
113586
- function isLocalUrl(url4) {
113587
- return url4.hostname === "localhost" || url4.hostname === "127.0.0.1";
113588
- }
113589
- function getApiUrl() {
113590
- const raw2 = process.env.API_URL || "https://pullfrog.com";
113591
- const parsed2 = new URL(raw2);
113592
- if (parsed2.protocol !== "https:" && !isLocalUrl(parsed2)) {
113593
- throw new Error(
113594
- `API_URL must use https:// (got ${parsed2.protocol}). only localhost is exempt.`
113595
- );
113596
- }
113597
- log2.debug(`resolved API_URL: ${raw2}`);
113598
- return raw2;
113599
- }
113600
- function isLocalApiUrl() {
113601
- try {
113602
- return isLocalUrl(new URL(getApiUrl()));
113603
- } catch {
113604
- return false;
113878
+ const runDir = mkdtempSync2(join6(PULLFROG_DATA_DIR, "run-"));
113879
+ if (process.env.RUNNER_ENVIRONMENT !== "github-hosted") {
113880
+ saveSecretState(SUBSCRIPTION_DATA_DIR_STATE, runDir);
113605
113881
  }
113606
- }
113607
-
113608
- // utils/apiFetch.ts
113609
- async function apiFetch(options) {
113610
- const apiUrl = getApiUrl();
113611
- const url4 = new URL(options.path, apiUrl);
113612
- const bypassSecret = process.env.VERCEL_AUTOMATION_BYPASS_SECRET;
113613
- if (bypassSecret) {
113614
- url4.searchParams.set("x-vercel-protection-bypass", bypassSecret);
113615
- }
113616
- const headers = {
113617
- ...options.headers
113618
- };
113619
- if (bypassSecret) {
113620
- headers["x-vercel-protection-bypass"] = bypassSecret;
113621
- }
113622
- if (!options.body) {
113623
- for (const key of Object.keys(headers)) {
113624
- if (key.toLowerCase() === "content-type") delete headers[key];
113625
- }
113626
- }
113627
- log2.debug(`api fetch: ${options.method ?? "GET"} ${url4.pathname}`);
113628
- const init = {
113629
- method: options.method ?? "GET",
113630
- headers
113631
- };
113632
- if (options.body) init.body = options.body;
113633
- if (options.signal) init.signal = options.signal;
113634
- return fetch(url4.toString(), init);
113635
- }
113636
-
113637
- // utils/codexRefreshDetect.ts
113638
- function detectCodexRefresh(params) {
113639
- let parsed2;
113640
- try {
113641
- parsed2 = JSON.parse(params.authFileContent);
113642
- } catch {
113643
- return null;
113644
- }
113645
- if (!parsed2 || typeof parsed2 !== "object") return null;
113646
- const root = parsed2;
113647
- const tokens = readCodexTokens(root) ?? readOpenCodeTokens(root);
113648
- if (!tokens) return null;
113649
- if (tokens.refresh_token === params.originalRefresh) return null;
113650
- const idToken = tokens.id_token ?? params.originalIdToken;
113651
- const codexShape = {
113652
- auth_mode: "chatgpt",
113653
- tokens: {
113654
- ...idToken ? { id_token: idToken } : {},
113655
- access_token: tokens.access_token,
113656
- refresh_token: tokens.refresh_token,
113657
- ...tokens.account_id ? { account_id: tokens.account_id } : {}
113658
- },
113659
- last_refresh: (/* @__PURE__ */ new Date()).toISOString()
113660
- };
113661
- return `${JSON.stringify(codexShape, null, 2)}
113662
- `;
113663
- }
113664
- function readCodexTokens(root) {
113665
- if (root.auth_mode !== "chatgpt") return null;
113666
- const tokens = root.tokens;
113667
- if (!tokens || typeof tokens !== "object") return null;
113668
- const t2 = tokens;
113669
- if (typeof t2.access_token !== "string" || typeof t2.refresh_token !== "string") return null;
113670
- return {
113671
- access_token: t2.access_token,
113672
- refresh_token: t2.refresh_token,
113673
- id_token: typeof t2.id_token === "string" ? t2.id_token : void 0,
113674
- account_id: typeof t2.account_id === "string" ? t2.account_id : void 0
113675
- };
113676
- }
113677
- function readOpenCodeTokens(root) {
113678
- const oauth = root.openai;
113679
- if (!oauth || typeof oauth !== "object") return null;
113680
- const o = oauth;
113681
- if (o.type !== "oauth") return null;
113682
- if (typeof o.refresh !== "string" || typeof o.access !== "string") return null;
113683
- return {
113684
- access_token: o.access,
113685
- refresh_token: o.refresh,
113686
- id_token: void 0,
113687
- account_id: typeof o.accountId === "string" ? o.accountId : void 0
113688
- };
113689
- }
113690
- function detectXaiRefresh(params) {
113691
- let parsed2;
113692
- try {
113693
- parsed2 = JSON.parse(params.authFileContent);
113694
- } catch {
113695
- return null;
113696
- }
113697
- if (!parsed2 || typeof parsed2 !== "object") return null;
113698
- const oauth = parsed2.xai;
113699
- if (!oauth || typeof oauth !== "object") return null;
113700
- const o = oauth;
113701
- if (o.type !== "oauth") return null;
113702
- if (typeof o.access !== "string" || typeof o.refresh !== "string") return null;
113703
- if (o.refresh === params.originalRefresh) return null;
113704
- const grokShape = {
113705
- auth_mode: "grok",
113706
- tokens: { access_token: o.access, refresh_token: o.refresh },
113707
- last_refresh: (/* @__PURE__ */ new Date()).toISOString()
113708
- };
113709
- return `${JSON.stringify(grokShape, null, 2)}
113710
- `;
113711
- }
113712
-
113713
- // utils/oauthWriteback.ts
113714
- var OAUTH_WRITEBACK_STATE = "oauth_writeback";
113715
- async function runOAuthWriteback() {
113716
- const raw2 = core2.getState(OAUTH_WRITEBACK_STATE);
113717
- if (!raw2) {
113718
- core2.info("oauth post-hook: no writeback state \u2014 skipping");
113719
- return;
113720
- }
113721
- let state;
113722
- try {
113723
- state = JSON.parse(raw2);
113724
- } catch (err) {
113725
- core2.warning(`oauth post-hook: malformed writeback state \u2014 ${err}`);
113726
- return;
113727
- }
113728
- if (!state.apiToken || !Array.isArray(state.entries)) {
113729
- core2.warning("oauth post-hook: incomplete writeback state \u2014 skipping");
113730
- return;
113731
- }
113732
- for (const entry of state.entries) {
113733
- await writeBackEntry(state.apiToken, entry);
113734
- }
113735
- }
113736
- async function writeBackEntry(apiToken, entry) {
113737
- if (!entry.secretName || !entry.authPath || !entry.originalRefresh) {
113738
- core2.warning("oauth post-hook: incomplete writeback entry \u2014 skipping");
113739
- return;
113740
- }
113741
- if (!existsSync6(entry.authPath)) {
113742
- core2.info(`oauth post-hook: ${entry.authPath} not found \u2014 nothing to write back`);
113743
- return;
113744
- }
113745
- let authFileContent;
113746
- try {
113747
- authFileContent = readFileSync4(entry.authPath, "utf8");
113748
- } catch (err) {
113749
- core2.warning(`oauth post-hook: cannot read ${entry.authPath} \u2014 ${err}`);
113750
- return;
113751
- }
113752
- const refreshed = entry.provider === "xai" ? detectXaiRefresh({
113753
- authFileContent,
113754
- originalRefresh: entry.originalRefresh
113755
- }) : detectCodexRefresh({
113756
- authFileContent,
113757
- originalRefresh: entry.originalRefresh,
113758
- originalIdToken: entry.originalIdToken
113759
- });
113760
- if (!refreshed) {
113761
- core2.info(`oauth post-hook: ${entry.secretName} chain unchanged \u2014 no writeback needed`);
113762
- return;
113763
- }
113764
- try {
113765
- const poolState = core2.getState("credential_receipts");
113766
- const receipts2 = external_exports.record(external_exports.string(), external_exports.string()).parse(JSON.parse(poolState || "{}"));
113767
- const receipt = receipts2[entry.secretName];
113768
- if (poolState && !receipt) return;
113769
- const response = await apiFetch({
113770
- path: receipt ? "/api/runtime/credentials" : "/api/runtime/secret",
113771
- method: "PUT",
113772
- headers: {
113773
- authorization: `Bearer ${apiToken}`,
113774
- "content-type": "application/json"
113775
- },
113776
- body: JSON.stringify({ name: entry.secretName, value: refreshed, receipt }),
113777
- // the workflow is already finished; a hung write-back would hold the
113778
- // runner open for nothing.
113779
- signal: AbortSignal.timeout(3e4)
113780
- });
113781
- if (!response.ok) {
113782
- const body = await response.text().catch(() => "");
113783
- core2.warning(`oauth post-hook: writeback returned ${response.status}: ${body}`);
113784
- return;
113785
- }
113786
- core2.info(`oauth post-hook: refreshed ${entry.secretName} persisted to Pullfrog`);
113787
- } catch (err) {
113788
- core2.warning(`oauth post-hook: writeback failed \u2014 ${err}`);
113789
- }
113790
- }
113791
-
113792
- // utils/secretCommands.ts
113793
- var core3 = __toESM(require_core(), 1);
113794
- function maskSecret(value2) {
113795
- if (isGitHubActions) core3.setSecret(value2);
113796
- }
113797
- function saveSecretState(name, value2) {
113798
- if (isGitHubActions) core3.saveState(name, value2);
113799
- }
113800
- function setSecretOutput(name, value2) {
113801
- if (isGitHubActions) core3.setOutput(name, value2);
113882
+ return runDir;
113802
113883
  }
113803
113884
 
113804
113885
  // utils/secrets.ts
@@ -114447,9 +114528,8 @@ var codex = agent({
114447
114528
  initialUsage: initial.usage,
114448
114529
  reflectionPrompt: buildReflectionPrompt(ctx.toolState),
114449
114530
  // a resume needs the thread codex recorded. without one there is nothing
114450
- // to continue, and `resume --last` would be worse than not resuming: it
114451
- // picks the newest recorded session in this cwd, which on a self-hosted
114452
- // runner with a shared CODEX_HOME is somebody else's run.
114531
+ // to continue, and `resume --last` could pick another run's session off CI,
114532
+ // where CODEX_HOME is the shared ~/.local/share/codex (in CI it is per run).
114453
114533
  canResume: (r2) => r2.threadId !== void 0,
114454
114534
  resume: async (c2) => runCodex2({
114455
114535
  ...runnerArgs,
@@ -114476,7 +114556,7 @@ var codex = agent({
114476
114556
  import { spawn as nodeSpawn2 } from "node:child_process";
114477
114557
  import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "node:fs";
114478
114558
  import { totalmem } from "node:os";
114479
- import { join as join8 } from "node:path";
114559
+ import { join as join9 } from "node:path";
114480
114560
  import { performance as performance7 } from "node:perf_hooks";
114481
114561
 
114482
114562
  // node_modules/.pnpm/@opencode-ai+sdk@1.18.29/node_modules/@opencode-ai/sdk/dist/v2/gen/core/serverSentEvents.gen.js
@@ -119885,6 +119965,68 @@ var import_cross_spawn = __toESM(require_cross_spawn(), 1);
119885
119965
  // agents/opencode.ts
119886
119966
  var import_undici = __toESM(require_undici2(), 1);
119887
119967
 
119968
+ // utils/openCodeModels.ts
119969
+ import { spawnSync as spawnSync4 } from "node:child_process";
119970
+ import { join as join8 } from "node:path";
119971
+ var baseline;
119972
+ var authorized;
119973
+ var failure;
119974
+ var ANSI_PATTERN = /\[[0-9;]*m/g;
119975
+ function openCodeHomeEnv(tmpdir4) {
119976
+ return {
119977
+ HOME: tmpdir4,
119978
+ XDG_CONFIG_HOME: join8(tmpdir4, ".config"),
119979
+ XDG_DATA_HOME: getSubscriptionDataHome() ?? join8(tmpdir4, ".local", "share"),
119980
+ XDG_CACHE_HOME: join8(tmpdir4, ".cache"),
119981
+ XDG_STATE_HOME: join8(tmpdir4, ".local", "state")
119982
+ };
119983
+ }
119984
+ function readModels(params) {
119985
+ const result = spawnSync4(params.cliPath, ["models"], {
119986
+ encoding: "utf-8",
119987
+ timeout: 3e4,
119988
+ env: { ...process.env, ...openCodeHomeEnv(params.tmpdir) },
119989
+ stdio: ["ignore", "pipe", "pipe"]
119990
+ });
119991
+ if (result.status !== 0) {
119992
+ const stderr = (result.stderr ?? "").replace(ANSI_PATTERN, "").trim();
119993
+ failure = stderr || result.error?.message;
119994
+ log2.debug(`\xBB \`opencode models\` failed (${result.status}): ${failure}`);
119995
+ return /* @__PURE__ */ new Set();
119996
+ }
119997
+ failure = void 0;
119998
+ return new Set(
119999
+ result.stdout.split("\n").map((line) => line.trim()).filter(Boolean)
120000
+ );
120001
+ }
120002
+ function getModelsFailure() {
120003
+ return failure;
120004
+ }
120005
+ function captureBaselineModels(params) {
120006
+ baseline = readModels(params);
120007
+ log2.debug(`\xBB opencode baseline: ${baseline.size} models`);
120008
+ }
120009
+ function captureAuthorizedModels(params) {
120010
+ authorized = readModels(params);
120011
+ const base = baseline;
120012
+ if (base) {
120013
+ const diff = [...authorized].filter((m) => !base.has(m));
120014
+ if (diff.length > 0) {
120015
+ log2.info(`\xBB BYOK auth enabled ${diff.length} model(s): ${diff.join(", ")}`);
120016
+ }
120017
+ }
120018
+ log2.debug(`\xBB opencode authorized: ${authorized.size} models`);
120019
+ }
120020
+ function authorizeModel(model) {
120021
+ getAuthorizedModels().add(model);
120022
+ }
120023
+ function getAuthorizedModels() {
120024
+ if (!authorized) {
120025
+ throw new Error("getAuthorizedModels called before captureAuthorizedModels");
120026
+ }
120027
+ return authorized;
120028
+ }
120029
+
119888
120030
  // utils/worktree.ts
119889
120031
  async function dirtyTrackedPaths(params = {}) {
119890
120032
  const result = await spawn3({
@@ -120001,7 +120143,8 @@ export default async function pullfrogEventsPlugin() {
120001
120143
  };
120002
120144
  }
120003
120145
  `;
120004
- function buildOpencodeSubagentGateSource(deniedTools) {
120146
+ function buildOpencodeSubagentGateSource(ctx) {
120147
+ const bashRefusal = ctx.payload.shell === "disabled" ? "shell execution is disabled for this run and no shell tool exists \u2014 use read, grep and glob instead." : `native bash is disabled in Pullfrog runs \u2014 call ${formatMcpToolRef("opencode", "shell")} with the same command instead.`;
120005
120148
  return `// AUTOGENERATED by Pullfrog. do not edit; it'll be overwritten on the next run.
120006
120149
  // hard-blocks state-mutating MCP tool calls from opencode subagents (PR
120007
120150
  // following pullfrog/app#796 \u2014 see action/agents/subagentToolGates.ts). see
@@ -120012,7 +120155,7 @@ function buildOpencodeSubagentGateSource(deniedTools) {
120012
120155
  // embedded at template-render time so the plugin source has no imports. the
120013
120156
  // denied set is derived from each tool's \`mutates\` flag \u2014 see
120014
120157
  // action/agents/subagentToolGates.ts.
120015
- const SUBAGENT_DENIED_TOOLS = new Set(${JSON.stringify(deniedTools)});
120158
+ const SUBAGENT_DENIED_TOOLS = new Set(${JSON.stringify(ctx.subagentDeniedTools)});
120016
120159
 
120017
120160
  // opencode presents MCP tools as \`pullfrog_<bare>\`; strip that to compare
120018
120161
  // against the canonical bare names in SUBAGENT_DENIED_TOOLS. native tools
@@ -120048,7 +120191,7 @@ export default async function pullfrogSubagentGatePlugin() {
120048
120191
  // above the sessionID check: the bash block must never depend on a
120049
120192
  // session id being present.
120050
120193
  if (tool === "bash") {
120051
- throw new Error("the native bash tool is disabled in Pullfrog runs.");
120194
+ throw new Error(${JSON.stringify(bashRefusal)});
120052
120195
  }
120053
120196
  if (!sessionID || !tool) return;
120054
120197
  if (orchestratorSessionID === undefined) orchestratorSessionID = sessionID;
@@ -120068,58 +120211,6 @@ export default async function pullfrogSubagentGatePlugin() {
120068
120211
  `;
120069
120212
  }
120070
120213
 
120071
- // utils/openCodeModels.ts
120072
- import { spawnSync as spawnSync4 } from "node:child_process";
120073
- var baseline;
120074
- var authorized;
120075
- var failure;
120076
- var ANSI_PATTERN = /\[[0-9;]*m/g;
120077
- function readModels(cliPath) {
120078
- const result = spawnSync4(cliPath, ["models"], {
120079
- encoding: "utf-8",
120080
- timeout: 3e4,
120081
- env: process.env,
120082
- stdio: ["ignore", "pipe", "pipe"]
120083
- });
120084
- if (result.status !== 0) {
120085
- const stderr = (result.stderr ?? "").replace(ANSI_PATTERN, "").trim();
120086
- failure = stderr || result.error?.message;
120087
- log2.debug(`\xBB \`opencode models\` failed (${result.status}): ${failure}`);
120088
- return /* @__PURE__ */ new Set();
120089
- }
120090
- failure = void 0;
120091
- return new Set(
120092
- result.stdout.split("\n").map((line) => line.trim()).filter(Boolean)
120093
- );
120094
- }
120095
- function getModelsFailure() {
120096
- return failure;
120097
- }
120098
- function captureBaselineModels(cliPath) {
120099
- baseline = readModels(cliPath);
120100
- log2.debug(`\xBB opencode baseline: ${baseline.size} models`);
120101
- }
120102
- function captureAuthorizedModels(cliPath) {
120103
- authorized = readModels(cliPath);
120104
- const base = baseline;
120105
- if (base) {
120106
- const diff = [...authorized].filter((m) => !base.has(m));
120107
- if (diff.length > 0) {
120108
- log2.info(`\xBB BYOK auth enabled ${diff.length} model(s): ${diff.join(", ")}`);
120109
- }
120110
- }
120111
- log2.debug(`\xBB opencode authorized: ${authorized.size} models`);
120112
- }
120113
- function authorizeModel(model) {
120114
- getAuthorizedModels().add(model);
120115
- }
120116
- function getAuthorizedModels() {
120117
- if (!authorized) {
120118
- throw new Error("getAuthorizedModels called before captureAuthorizedModels");
120119
- }
120120
- return authorized;
120121
- }
120122
-
120123
120214
  // agents/subagentModels.ts
120124
120215
  function deriveSubagentModels(orchestratorSpec) {
120125
120216
  if (!orchestratorSpec) return { reviewer: void 0 };
@@ -120494,6 +120585,7 @@ async function dispatchEvent(ctx, event) {
120494
120585
  const err = event.properties.error;
120495
120586
  const message = err ? extractErrorMessage(err) : "(no error payload)";
120496
120587
  log2.info(`\xBB ${ctx.label} session error: ${message}`);
120588
+ if (err) ctx.lastSessionError = message;
120497
120589
  return;
120498
120590
  }
120499
120591
  }
@@ -120632,6 +120724,7 @@ async function runPromptTurn(ctx, params) {
120632
120724
  ctx.promptMessageID = void 0;
120633
120725
  ctx.diagnostic.sawModelOutput = false;
120634
120726
  ctx.diagnostic.idleSec = void 0;
120727
+ ctx.lastSessionError = void 0;
120635
120728
  let assistant;
120636
120729
  let returnedParts;
120637
120730
  let networkError = null;
@@ -120676,11 +120769,12 @@ async function runPromptTurn(ctx, params) {
120676
120769
  }
120677
120770
  const cause = ctx.serverExitCause() ?? ctx.diagnostic.lastProviderError;
120678
120771
  const diagnosis = cause ? ` \u2014 likely cause: ${cause}` : "";
120772
+ const promptCause = ctx.serverExitCause() ?? (ctx.diagnostic.sawModelOutput ? void 0 : ctx.lastSessionError) ?? ctx.diagnostic.lastProviderError;
120679
120773
  if (networkError) {
120680
120774
  return {
120681
120775
  success: false,
120682
120776
  output: finalText,
120683
- error: params.signal.aborted ? `activity timeout: the model went silent and the turn was aborted by the activity watchdog (${networkError})${diagnosis}` : `opencode prompt failed: ${networkError}${diagnosis}`,
120777
+ error: params.signal.aborted ? `activity timeout: the model went silent and the turn was aborted by the activity watchdog (${networkError})${diagnosis}` : `opencode prompt failed: ${networkError}${promptCause ? ` \u2014 likely cause: ${promptCause}` : ""}`,
120684
120778
  usage
120685
120779
  };
120686
120780
  }
@@ -120872,15 +120966,14 @@ var opencode = agent({
120872
120966
  const isBedrockRoute = rawModel !== void 0 && bedrockModelId !== void 0 && bedrockModelId === rawModel;
120873
120967
  const vertexModel = resolveVertexOpenCodeModel(rawModel);
120874
120968
  const model = vertexModel ?? (isBedrockRoute ? `amazon-bedrock/${rawModel}` : rawModel);
120875
- const homeEnv = {
120876
- HOME: ctx.tmpdir,
120877
- XDG_CONFIG_HOME: join8(ctx.tmpdir, ".config")
120878
- };
120879
- const opencodePluginDir = join8(homeEnv.XDG_CONFIG_HOME, "opencode", "plugin");
120969
+ const codexAuth = installCodexAuth();
120970
+ const xaiAuth = installXaiAuth();
120971
+ const homeEnv = openCodeHomeEnv(ctx.tmpdir);
120972
+ const opencodePluginDir = join9(homeEnv.XDG_CONFIG_HOME, "opencode", "plugin");
120880
120973
  mkdirSync7(opencodePluginDir, { recursive: true });
120881
120974
  writeFileSync7(
120882
- join8(opencodePluginDir, PULLFROG_OPENCODE_GATE_PLUGIN_FILENAME),
120883
- buildOpencodeSubagentGateSource(ctx.subagentDeniedTools)
120975
+ join9(opencodePluginDir, PULLFROG_OPENCODE_GATE_PLUGIN_FILENAME),
120976
+ buildOpencodeSubagentGateSource(ctx)
120884
120977
  );
120885
120978
  const agentBrowserVersion = getDevDependencyVersion("agent-browser");
120886
120979
  addSkill({
@@ -120890,8 +120983,6 @@ var opencode = agent({
120890
120983
  agent: "opencode"
120891
120984
  });
120892
120985
  installBundledSkills({ home: homeEnv.HOME });
120893
- const codexAuth = installCodexAuth();
120894
- const xaiAuth = installXaiAuth();
120895
120986
  const permissionOverride = JSON.stringify({
120896
120987
  external_directory: { "*": "deny", "/tmp/*": "allow" },
120897
120988
  read: { "*": "allow", ...GIT_NATIVE_READ_DENY_OPENCODE },
@@ -120910,7 +121001,6 @@ var opencode = agent({
120910
121001
  delete env2.OPENCODE_EXPERIMENTAL_CODE_MODE;
120911
121002
  const writebacks = [];
120912
121003
  if (codexAuth) {
120913
- env2.XDG_DATA_HOME = codexAuth.xdgDataHome;
120914
121004
  delete env2.OPENAI_API_KEY;
120915
121005
  writebacks.push({
120916
121006
  secretName: "CODEX_AUTH_JSON",
@@ -120921,7 +121011,6 @@ var opencode = agent({
120921
121011
  });
120922
121012
  }
120923
121013
  if (xaiAuth) {
120924
- env2.XDG_DATA_HOME = xaiAuth.xdgDataHome;
120925
121014
  delete env2.XAI_API_KEY;
120926
121015
  writebacks.push({
120927
121016
  secretName: "GROK_AUTH_JSON",
@@ -120994,6 +121083,7 @@ var opencode = agent({
120994
121083
  mcpToolCalls: 0,
120995
121084
  recentStderr: server.recentStderr,
120996
121085
  serverExitCause: server.exitCause,
121086
+ lastSessionError: void 0,
120997
121087
  diagnostic: {
120998
121088
  label: "Pullfrog",
120999
121089
  recentStderr: server.recentStderr,
@@ -175912,7 +176002,7 @@ function closeBrowserDaemon(toolState) {
175912
176002
  // mcp/checkout.ts
175913
176003
  import { createHash as createHash2 } from "node:crypto";
175914
176004
  import { statSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync11 } from "node:fs";
175915
- import { join as join16 } from "node:path";
176005
+ import { join as join17 } from "node:path";
175916
176006
 
175917
176007
  // node_modules/.pnpm/@ark+schema@0.56.0/node_modules/@ark/schema/out/shared/errors.js
175918
176008
  var ArkError = class _ArkError extends CastableBase {
@@ -184283,13 +184373,14 @@ function ensureRepoState(toolState, init) {
184283
184373
  // utils/changeImpact.ts
184284
184374
  import { spawn as spawn4 } from "node:child_process";
184285
184375
  import { writeFileSync as writeFileSync8 } from "node:fs";
184286
- import { join as join9 } from "node:path";
184376
+ import { join as join10 } from "node:path";
184287
184377
  var MAX_CANDIDATES = 24;
184288
184378
  var MAX_ATOM_LENGTH = 128;
184289
184379
  var MAX_ATOMS = 12;
184290
184380
  var MAX_REFERENCES = 6;
184291
184381
  var MAX_EXCERPT = 160;
184292
184382
  var MAX_BYTES = 16e3;
184383
+ var LOOKUP_BUDGET_MS = 6e4;
184293
184384
  function isSymbolShaped(atom) {
184294
184385
  return atom.length >= 4 && atom.length <= MAX_ATOM_LENGTH && /[A-Z_]/.test(atom);
184295
184386
  }
@@ -184492,7 +184583,7 @@ function consumeGrepChunk(params) {
184492
184583
  async function lookupReferences(params) {
184493
184584
  const buckets = emptyBuckets(params.candidates);
184494
184585
  if (params.candidates.length === 0) return { buckets, error: void 0 };
184495
- const patterns = params.candidates.flatMap((candidate) => ["-e", candidate.atom]);
184586
+ const pattern = params.candidates.map((candidate) => candidate.atom).join("|");
184496
184587
  const child = spawn4(
184497
184588
  "git",
184498
184589
  [
@@ -184503,13 +184594,14 @@ async function lookupReferences(params) {
184503
184594
  "-z",
184504
184595
  "-I",
184505
184596
  "-w",
184506
- "-F",
184507
- ...patterns,
184597
+ "-E",
184598
+ "-e",
184599
+ pattern,
184508
184600
  params.treeish,
184509
184601
  "--",
184510
184602
  ":(top)"
184511
184603
  ],
184512
- { env: resolveEnv(void 0), stdio: ["ignore", "pipe", "pipe"] }
184604
+ { env: resolveEnv(void 0), stdio: ["ignore", "pipe", "pipe"], signal: params.signal }
184513
184605
  );
184514
184606
  const state = createGrepRecordState();
184515
184607
  const maxAtomLength = Math.max(...params.candidates.map((candidate) => candidate.atom.length));
@@ -184529,7 +184621,7 @@ async function lookupReferences(params) {
184529
184621
  child.stderr.resume();
184530
184622
  const status = await new Promise((resolve3) => {
184531
184623
  child.once("error", (error49) => {
184532
- spawnError = error49.message;
184624
+ spawnError = error49.cause instanceof Error ? error49.cause.message : error49.message;
184533
184625
  resolve3(null);
184534
184626
  });
184535
184627
  child.once("close", resolve3);
@@ -184617,8 +184709,10 @@ async function createChangeImpactArtifact(ctx, params) {
184617
184709
  const lookup2 = await lookupReferences({
184618
184710
  candidates: candidates.selected,
184619
184711
  addedLines: candidates.addedLines,
184620
- treeish: params.headSha
184712
+ treeish: params.headSha,
184713
+ signal: AbortSignal.any([params.signal, AbortSignal.timeout(LOOKUP_BUDGET_MS)])
184621
184714
  });
184715
+ params.signal.throwIfAborted();
184622
184716
  const artifact = formatArtifact({
184623
184717
  entries: candidates.selected.map((change) => {
184624
184718
  return { change, bucket: lookup2.buckets.get(change.atom) ?? { count: 0, samples: [] } };
@@ -184630,7 +184724,7 @@ async function createChangeImpactArtifact(ctx, params) {
184630
184724
  headSha: params.headSha,
184631
184725
  lookupError: lookup2.error
184632
184726
  });
184633
- const path4 = join9(ctx.tmpdir, `pr-${params.pullNumber}-${params.headSha.slice(0, 7)}-impact.md`);
184727
+ const path4 = join10(ctx.tmpdir, `pr-${params.pullNumber}-${params.headSha.slice(0, 7)}-impact.md`);
184634
184728
  writeFileSync8(path4, artifact.content);
184635
184729
  return {
184636
184730
  path: path4,
@@ -184884,7 +184978,7 @@ function readNumber(params) {
184884
184978
  import { execSync } from "node:child_process";
184885
184979
  import { createHash } from "node:crypto";
184886
184980
  import { readFileSync as readFileSync6, realpathSync, unlinkSync } from "node:fs";
184887
- import { join as join10 } from "node:path";
184981
+ import { join as join11 } from "node:path";
184888
184982
 
184889
184983
  // utils/shell.ts
184890
184984
  import { spawnSync as spawnSync6 } from "node:child_process";
@@ -184974,7 +185068,7 @@ function resolveHooksDir(cwd, gitPath) {
184974
185068
  cwd,
184975
185069
  log: false
184976
185070
  }).trim();
184977
- const hooksDir = join10(commonDir, "hooks");
185071
+ const hooksDir = join11(commonDir, "hooks");
184978
185072
  hooksDirCache.set(cwd, hooksDir);
184979
185073
  return hooksDir;
184980
185074
  }
@@ -185021,7 +185115,8 @@ async function $git(subcommand, args2, options) {
185021
185115
  GIT_CONFIG_COUNT: "0",
185022
185116
  GIT_CONFIG_PARAMETERS: ""
185023
185117
  },
185024
- activityTimeout: 0
185118
+ activityTimeout: 0,
185119
+ signal: options.signal
185025
185120
  });
185026
185121
  if (result.stderr.includes("askpass-compromised")) {
185027
185122
  log2.info("askpass code was replayed after revoke \u2014 token has been revoked");
@@ -185088,9 +185183,10 @@ import { spawnSync as spawnSync8 } from "node:child_process";
185088
185183
  // mcp/shell.ts
185089
185184
  import { spawn as spawn5, spawnSync as spawnSync7 } from "node:child_process";
185090
185185
  import { randomUUID as randomUUID3 } from "node:crypto";
185091
- import { closeSync, openSync, writeFileSync as writeFileSync9 } from "node:fs";
185186
+ import { once } from "node:events";
185187
+ import { closeSync, existsSync as existsSync9, openSync, writeFileSync as writeFileSync9 } from "node:fs";
185092
185188
  import { userInfo as userInfo2 } from "node:os";
185093
- import { join as join11 } from "node:path";
185189
+ import { join as join12 } from "node:path";
185094
185190
  import { setTimeout as sleep3 } from "node:timers/promises";
185095
185191
 
185096
185192
  // node_modules/.pnpm/@toon-format+toon@1.4.0/node_modules/@toon-format/toon/dist/index.mjs
@@ -185734,7 +185830,7 @@ function buildFsMounts(repoDir) {
185734
185830
  function shimMount() {
185735
185831
  const tempDir = process.env.PULLFROG_TEMP_DIR;
185736
185832
  if (!tempDir) return "";
185737
- const dir = join11(tempDir, "pm-bin").replace(/'/g, "'\\''");
185833
+ const dir = join12(tempDir, "pm-bin").replace(/'/g, "'\\''");
185738
185834
  return `[ -d '${dir}' ] && mount --bind '${dir}' '${dir}' 2>/dev/null && mount -o remount,bind,ro '${dir}' 2>/dev/null;`;
185739
185835
  }
185740
185836
  var _repoRoot;
@@ -185905,7 +186001,7 @@ ${spawnError}` : spawnError;
185905
186001
  }
185906
186002
  function capOutput(output) {
185907
186003
  if (output.length <= MAX_OUTPUT_CHARS) return output;
185908
- const fullPath = join11(getTempDir(), `shell-${randomUUID3().slice(0, 8)}.log`);
186004
+ const fullPath = join12(getTempDir(), `shell-${randomUUID3().slice(0, 8)}.log`);
185909
186005
  writeFileSync9(fullPath, output);
185910
186006
  const elided = output.length - MAX_OUTPUT_CHARS;
185911
186007
  return `... [${elided} chars truncated; output saved to ${fullPath}] ...
@@ -185942,6 +186038,7 @@ Do NOT use this tool for git commands \u2014 use the dedicated git tools instead
185942
186038
  }
185943
186039
  const timeout = Math.min(params.timeout ?? 3e4, 12e4);
185944
186040
  const cwd = params.working_directory ?? process.cwd();
186041
+ if (!existsSync9(cwd)) throw new Error(`working_directory does not exist: ${cwd}`);
185945
186042
  const env2 = resolveEnv(ctx.payload.shell === "enabled" ? "inherit" : "restricted");
185946
186043
  if (params.command.includes("agent-browser")) {
185947
186044
  const daemonError = ensureBrowserDaemon(ctx.toolState);
@@ -185960,8 +186057,8 @@ Do NOT use this tool for git commands \u2014 use the dedicated git tools instead
185960
186057
  if (params.background) {
185961
186058
  const tempDir = getTempDir();
185962
186059
  const handle = `bg-${randomUUID3().slice(0, 8)}`;
185963
- const outputPath = join11(tempDir, `${handle}.log`);
185964
- const pidPath = join11(tempDir, `${handle}.pid`);
186060
+ const outputPath = join12(tempDir, `${handle}.log`);
186061
+ const pidPath = join12(tempDir, `${handle}.pid`);
185965
186062
  const logFd = openSync(outputPath, "a");
185966
186063
  let proc;
185967
186064
  try {
@@ -185975,7 +186072,8 @@ Do NOT use this tool for git commands \u2014 use the dedicated git tools instead
185975
186072
  closeSync(logFd);
185976
186073
  }
185977
186074
  if (!proc.pid) {
185978
- throw new Error("failed to start background process");
186075
+ const [error49] = await once(proc, "error");
186076
+ throw new Error(`failed to start background process: ${error49.message}`);
185979
186077
  }
185980
186078
  proc.unref();
185981
186079
  writeFileSync9(pidPath, `${proc.pid}
@@ -186252,12 +186350,12 @@ function postProcessRangeDiff(raw2, contextLines = 3) {
186252
186350
  // mcp/git.ts
186253
186351
  import { randomUUID as randomUUID4 } from "node:crypto";
186254
186352
  import { writeFileSync as writeFileSync10 } from "node:fs";
186255
- import { join as join14 } from "node:path";
186353
+ import { join as join15 } from "node:path";
186256
186354
 
186257
186355
  // utils/apiCommit.ts
186258
186356
  import { execFileSync as execFileSync6 } from "node:child_process";
186259
186357
  import { lstat, readlink } from "node:fs/promises";
186260
- import { join as join12 } from "node:path";
186358
+ import { join as join13 } from "node:path";
186261
186359
  var GITHUB_API = "https://api.github.com";
186262
186360
  var MAX_BLOB_BYTES = 30 * 1024 * 1024;
186263
186361
  var BLOB_UPLOAD_CONCURRENCY = 8;
@@ -186322,7 +186420,7 @@ async function assertApiCommittable(files) {
186322
186420
  const root = getRepoRoot();
186323
186421
  const attrs = $(
186324
186422
  "git",
186325
- ["check-attr", "filter", "-z", "--", ...present.map((p) => join12(root, p))],
186423
+ ["check-attr", "filter", "-z", "--", ...present.map((p) => join13(root, p))],
186326
186424
  { log: false }
186327
186425
  );
186328
186426
  const parts = attrs.split("\0");
@@ -186334,7 +186432,7 @@ async function assertApiCommittable(files) {
186334
186432
  }
186335
186433
  }
186336
186434
  for (const path4 of present) {
186337
- const stat = await lstat(join12(root, path4));
186435
+ const stat = await lstat(join13(root, path4));
186338
186436
  if (stat.isDirectory()) {
186339
186437
  throw new Error(
186340
186438
  `'${path4}' is a directory (nested repository or submodule?) \u2014 signed commits only support files and symlinks.`
@@ -186343,7 +186441,7 @@ async function assertApiCommittable(files) {
186343
186441
  }
186344
186442
  }
186345
186443
  async function createBlobEntry(params) {
186346
- const absPath = join12(params.repoRoot, params.path);
186444
+ const absPath = join13(params.repoRoot, params.path);
186347
186445
  const stat = await lstat(absPath);
186348
186446
  if (stat.size > MAX_BLOB_BYTES) {
186349
186447
  throw new Error(
@@ -186523,7 +186621,7 @@ this usually means your local branch has commits that were never pushed \u2014 s
186523
186621
  var core4 = __toESM(require_core(), 1);
186524
186622
  import { createSign } from "node:crypto";
186525
186623
  import { rename, writeFile } from "node:fs/promises";
186526
- import { dirname as dirname3, join as join13 } from "node:path";
186624
+ import { dirname as dirname3, join as join14 } from "node:path";
186527
186625
 
186528
186626
  // node_modules/.pnpm/@octokit+plugin-throttling@11.0.5_@octokit+core@7.0.8/node_modules/@octokit/plugin-throttling/dist-bundle/index.js
186529
186627
  var import_light = __toESM(require_light(), 1);
@@ -191072,7 +191170,7 @@ function getGitHubUsageSummary() {
191072
191170
  }
191073
191171
  async function writeGitHubUsageSummaryToFile(path4) {
191074
191172
  const summary2 = getGitHubUsageSummary();
191075
- const tmpPath = join13(dirname3(path4), `.usage-summary-${process.pid}.tmp`);
191173
+ const tmpPath = join14(dirname3(path4), `.usage-summary-${process.pid}.tmp`);
191076
191174
  await writeFile(tmpPath, JSON.stringify(summary2));
191077
191175
  await rename(tmpPath, path4);
191078
191176
  }
@@ -191700,7 +191798,7 @@ function countAhead(head, base, cwd) {
191700
191798
  function spillGitOutput(params) {
191701
191799
  const tempDir = process.env.PULLFROG_TEMP_DIR;
191702
191800
  if (!tempDir) throw new Error("PULLFROG_TEMP_DIR not set");
191703
- const outputPath = join14(tempDir, `git-${params.command}-${randomUUID4().slice(0, 8)}.txt`);
191801
+ const outputPath = join15(tempDir, `git-${params.command}-${randomUUID4().slice(0, 8)}.txt`);
191704
191802
  writeFileSync10(outputPath, params.output);
191705
191803
  const previewByLines = params.output.split("\n").slice(0, OVERFLOW_PREVIEW_LINES).join("\n");
191706
191804
  const preview = previewByLines.length <= OVERFLOW_PREVIEW_MAX_CHARS ? previewByLines : `${previewByLines.slice(0, OVERFLOW_PREVIEW_MAX_CHARS)}\u2026`;
@@ -193355,11 +193453,11 @@ async function reportReviewNodeId(ctx, params) {
193355
193453
 
193356
193454
  // utils/setup.ts
193357
193455
  import { execFileSync as execFileSync7, execSync as execSync2 } from "node:child_process";
193358
- import { mkdtempSync as mkdtempSync2, readdirSync, realpathSync as realpathSync2, rmSync as rmSync3, unlinkSync as unlinkSync2 } from "node:fs";
193456
+ import { mkdtempSync as mkdtempSync3, readdirSync, realpathSync as realpathSync2, rmSync as rmSync4, unlinkSync as unlinkSync2 } from "node:fs";
193359
193457
  import { tmpdir as tmpdir3 } from "node:os";
193360
- import { join as join15 } from "node:path";
193458
+ import { join as join16 } from "node:path";
193361
193459
  function createTempDirectory() {
193362
- const path4 = mkdtempSync2(join15(tmpdir3(), "pullfrog-"));
193460
+ const path4 = mkdtempSync3(join16(tmpdir3(), "pullfrog-"));
193363
193461
  process.env.PULLFROG_TEMP_DIR = path4;
193364
193462
  log2.info(`\xBB created temp dir at ${path4}`);
193365
193463
  return { path: path4, [Symbol.dispose]: () => removeTempDirectory(path4) };
@@ -193367,10 +193465,10 @@ function createTempDirectory() {
193367
193465
  function removeTempDirectory(path4) {
193368
193466
  try {
193369
193467
  try {
193370
- rmSync3(path4, { recursive: true, force: true });
193468
+ rmSync4(path4, { recursive: true, force: true });
193371
193469
  } catch {
193372
193470
  execFileSync7("chmod", ["-R", "u+w", path4]);
193373
- rmSync3(path4, { recursive: true, force: true });
193471
+ rmSync4(path4, { recursive: true, force: true });
193374
193472
  }
193375
193473
  log2.debug(`\xBB removed temp dir at ${path4}`);
193376
193474
  } catch (err) {
@@ -193419,13 +193517,13 @@ function wipeRunnerLeakSurface() {
193419
193517
  return [];
193420
193518
  }
193421
193519
  };
193422
- const fileCommandsDir = join15(runnerTemp, "_runner_file_commands");
193520
+ const fileCommandsDir = join16(runnerTemp, "_runner_file_commands");
193423
193521
  for (const entry of listDir(fileCommandsDir)) {
193424
- tryUnlink(join15(fileCommandsDir, entry));
193522
+ tryUnlink(join16(fileCommandsDir, entry));
193425
193523
  }
193426
193524
  for (const entry of listDir(runnerTemp)) {
193427
193525
  if (entry.endsWith(".sh") || /^git-credentials-.*\.config$/.test(entry)) {
193428
- tryUnlink(join15(runnerTemp, entry));
193526
+ tryUnlink(join16(runnerTemp, entry));
193429
193527
  }
193430
193528
  }
193431
193529
  if (wiped.length > 0) {
@@ -193697,7 +193795,7 @@ async function ensureBeforeShaReachable(params) {
193697
193795
  "origin",
193698
193796
  tempBranch
193699
193797
  ],
193700
- { token: params.gitToken, refreshGitToken: params.refreshGitToken },
193798
+ { token: params.gitToken, refreshGitToken: params.refreshGitToken, signal: params.signal },
193701
193799
  `before_sha temp branch ${tempBranch}`
193702
193800
  );
193703
193801
  log2.debug(`\xBB fetched before_sha via temp branch ${tempBranch}`);
@@ -193738,7 +193836,7 @@ function cleanupStaleGitLocks() {
193738
193836
  } catch {
193739
193837
  continue;
193740
193838
  }
193741
- if (now - mtimeMs < STALE_LOCK_AGE_MS) continue;
193839
+ if (now - mtimeMs < STALE_LOCK_AGE_MS || mtimeMs >= performance.timeOrigin) continue;
193742
193840
  try {
193743
193841
  unlinkSync3(relPath);
193744
193842
  log2.warning(`\xBB removed stale ${relPath} from prior run`);
@@ -193771,7 +193869,7 @@ async function abortIfPullRequestMoved(args2) {
193771
193869
  );
193772
193870
  }
193773
193871
  async function checkoutPrBranch(pr, params) {
193774
- const { octokit, owner, name, gitToken, refreshGitToken, toolState, beforeSha } = params;
193872
+ const { octokit, owner, name, gitToken, refreshGitToken, toolState, beforeSha, signal } = params;
193775
193873
  rejectIfLeadingDash(pr.baseRef, "PR base ref");
193776
193874
  rejectIfLeadingDash(pr.headRef, "PR head ref");
193777
193875
  const repoState = requireRepoState(toolState, owner, name);
@@ -193785,7 +193883,7 @@ async function checkoutPrBranch(pr, params) {
193785
193883
  log2.debug(`\xBB fetching base branch (${pr.baseRef})...`);
193786
193884
  await $gitFetchWithDeepen(
193787
193885
  ["--no-tags", "origin", pr.baseRef],
193788
- { token: gitToken, refreshGitToken },
193886
+ { token: gitToken, refreshGitToken, signal },
193789
193887
  `base branch ${pr.baseRef}`
193790
193888
  );
193791
193889
  if (!alreadyOnBranch) {
@@ -193796,7 +193894,7 @@ async function checkoutPrBranch(pr, params) {
193796
193894
  try {
193797
193895
  await $gitFetchWithDeepen(
193798
193896
  ["--no-tags", "origin", `+pull/${pr.number}/head:${localBranch}`],
193799
- { token: gitToken, refreshGitToken },
193897
+ { token: gitToken, refreshGitToken, signal },
193800
193898
  `PR #${pr.number}`
193801
193899
  );
193802
193900
  } catch (e) {
@@ -193821,7 +193919,8 @@ async function checkoutPrBranch(pr, params) {
193821
193919
  repo: name,
193822
193920
  gitToken,
193823
193921
  refreshGitToken,
193824
- isShallow
193922
+ isShallow,
193923
+ signal
193825
193924
  }) : false;
193826
193925
  if (isShallow) {
193827
193926
  let deepenDepth = 0;
@@ -193856,7 +193955,8 @@ async function checkoutPrBranch(pr, params) {
193856
193955
  if (deepenDepth) {
193857
193956
  log2.debug(`\xBB deepening by ${deepenDepth} to reach merge base...`);
193858
193957
  await $git("fetch", [`--deepen=${deepenDepth}`, "--no-tags", "origin"], {
193859
- token: gitToken
193958
+ token: gitToken,
193959
+ signal
193860
193960
  });
193861
193961
  }
193862
193962
  }
@@ -193894,6 +193994,7 @@ async function checkoutPrBranch(pr, params) {
193894
193994
  remoteBranch: pr.headRef,
193895
193995
  localBranch
193896
193996
  };
193997
+ signal.throwIfAborted();
193897
193998
  const postCheckoutHook = await executeLifecycleHook({
193898
193999
  event: "post-checkout",
193899
194000
  script: params.postCheckoutScript,
@@ -193904,19 +194005,30 @@ async function checkoutPrBranch(pr, params) {
193904
194005
  }
193905
194006
  var inFlightCheckouts = /* @__PURE__ */ new Map();
193906
194007
  var CHECKOUT_DEADLINE_MS = 57e4;
193907
- function withCheckoutDeadline(promise2, pullNumber) {
193908
- let timer;
193909
- const deadline = new Promise((_, reject) => {
193910
- timer = setTimeout(
194008
+ var CHECKOUT_ABORT_GRACE_MS = 15e3;
194009
+ function withCheckoutDeadline(run4, pullNumber) {
194010
+ const exceeded = `checkout_pr #${pullNumber} exceeded ${CHECKOUT_DEADLINE_MS / 1e3}s`;
194011
+ const retry = "retry the call to start a fresh attempt.";
194012
+ const controller = new AbortController();
194013
+ const abortTimer = setTimeout(
194014
+ () => controller.abort(new Error(`${exceeded} and was cancelled; ${retry}`)),
194015
+ CHECKOUT_DEADLINE_MS
194016
+ );
194017
+ let backstopTimer;
194018
+ const backstop = new Promise((_, reject) => {
194019
+ backstopTimer = setTimeout(
193911
194020
  () => reject(
193912
194021
  new Error(
193913
- `checkout_pr #${pullNumber} exceeded ${CHECKOUT_DEADLINE_MS / 1e3}s. the fetch may still be running in the background; retry the call to start a fresh attempt.`
194022
+ `${exceeded} and was asked to cancel, but a step that cannot be interrupted (the post-checkout hook or a GitHub API call) was still running ${CHECKOUT_ABORT_GRACE_MS / 1e3}s later. ${retry}`
193914
194023
  )
193915
194024
  ),
193916
- CHECKOUT_DEADLINE_MS
194025
+ CHECKOUT_DEADLINE_MS + CHECKOUT_ABORT_GRACE_MS
193917
194026
  );
193918
194027
  });
193919
- return Promise.race([promise2, deadline]).finally(() => clearTimeout(timer));
194028
+ return Promise.race([run4(controller.signal), backstop]).finally(() => {
194029
+ clearTimeout(abortTimer);
194030
+ clearTimeout(backstopTimer);
194031
+ });
193920
194032
  }
193921
194033
  function headsEqual(a2, b) {
193922
194034
  if (a2.kind === "branch" && b.kind === "branch") return a2.name === b.name;
@@ -193928,7 +194040,7 @@ function describeHead(h2) {
193928
194040
  return `detached HEAD \`${h2.sha}\``;
193929
194041
  }
193930
194042
  function CheckoutPrTool(ctx) {
193931
- const runCheckout = async (pull_number) => {
194043
+ const runCheckout = async (pull_number, signal) => {
193932
194044
  const prResponse = await ctx.octokit.rest.pulls.get({
193933
194045
  owner: ctx.repo.owner,
193934
194046
  repo: ctx.repo.name,
@@ -193957,7 +194069,8 @@ function CheckoutPrTool(ctx) {
193957
194069
  toolState: ctx.toolState,
193958
194070
  shell: ctx.payload.shell,
193959
194071
  postCheckoutScript: ctx.postCheckoutScript,
193960
- beforeSha: primary.beforeSha
194072
+ beforeSha: primary.beforeSha,
194073
+ signal
193961
194074
  });
193962
194075
  const tempDir = process.env.PULLFROG_TEMP_DIR;
193963
194076
  if (!tempDir) {
@@ -193979,7 +194092,7 @@ function CheckoutPrTool(ctx) {
193979
194092
  headSha: checkoutSha
193980
194093
  });
193981
194094
  if (incremental.status === "ok") {
193982
- incrementalDiffPath = join16(
194095
+ incrementalDiffPath = join17(
193983
194096
  tempDir,
193984
194097
  `pr-${pull_number}-${beforeShort}-${headShort}-incremental.diff`
193985
194098
  );
@@ -193996,7 +194109,7 @@ function CheckoutPrTool(ctx) {
193996
194109
  const diffPreview = formatResult.content.split("\n").slice(0, 100).join("\n");
193997
194110
  log2.debug(`formatted diff preview (first 100 lines):
193998
194111
  ${diffPreview}`);
193999
- const diffPath = join16(tempDir, `pr-${pull_number}-${headShort}.diff`);
194112
+ const diffPath = join17(tempDir, `pr-${pull_number}-${headShort}.diff`);
194000
194113
  writeFileSync11(diffPath, formatResult.content);
194001
194114
  log2.debug(`wrote diff to ${diffPath} (${formatResult.content.length} bytes)`);
194002
194115
  primary.diffCoverage = createDiffCoverageState({
@@ -194036,7 +194149,8 @@ ${diffPreview}`);
194036
194149
  files: formatResult.files,
194037
194150
  pullNumber: pull_number,
194038
194151
  baseSha: prResponse.data.base.sha,
194039
- headSha: checkoutSha
194152
+ headSha: checkoutSha,
194153
+ signal
194040
194154
  });
194041
194155
  impactPath = impact.path;
194042
194156
  log2.info(
@@ -194142,7 +194256,10 @@ ${dirty}`
194142
194256
  );
194143
194257
  }
194144
194258
  }
194145
- const promise2 = withCheckoutDeadline(runCheckout(pull_number), pull_number);
194259
+ const promise2 = withCheckoutDeadline(
194260
+ (signal) => runCheckout(pull_number, signal),
194261
+ pull_number
194262
+ );
194146
194263
  inFlightCheckouts.set(pull_number, promise2);
194147
194264
  try {
194148
194265
  return await promise2;
@@ -194155,7 +194272,7 @@ ${dirty}`
194155
194272
 
194156
194273
  // mcp/checkSuite.ts
194157
194274
  import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync12 } from "node:fs";
194158
- import { join as join17 } from "node:path";
194275
+ import { join as join18 } from "node:path";
194159
194276
  var GetCheckSuiteLogs = type({
194160
194277
  check_suite_id: type.number.describe("the id from check_suite.id")
194161
194278
  });
@@ -194251,7 +194368,7 @@ function GetCheckSuiteLogsTool(ctx) {
194251
194368
  if (!tempDir) {
194252
194369
  throw new Error("PULLFROG_TEMP_DIR not set");
194253
194370
  }
194254
- const logsDir = join17(tempDir, "ci-logs");
194371
+ const logsDir = join18(tempDir, "ci-logs");
194255
194372
  mkdirSync8(logsDir, { recursive: true });
194256
194373
  const jobResults = [];
194257
194374
  for (const run4 of failedRuns) {
@@ -194278,7 +194395,7 @@ function GetCheckSuiteLogsTool(ctx) {
194278
194395
  );
194279
194396
  }
194280
194397
  const logsText = await logsResult.text();
194281
- const logPath = join17(logsDir, `job-${job.id}.log`);
194398
+ const logPath = join18(logsDir, `job-${job.id}.log`);
194282
194399
  writeFileSync12(logPath, logsText);
194283
194400
  const analysis = analyzeLog(logsText, 80);
194284
194401
  const failedSteps = job.steps?.filter((s) => s.conclusion === "failure").map((s) => `Step ${s.number}: ${s.name}`) ?? [];
@@ -194328,7 +194445,7 @@ function GetCheckSuiteLogsTool(ctx) {
194328
194445
 
194329
194446
  // mcp/commitInfo.ts
194330
194447
  import { writeFileSync as writeFileSync13 } from "node:fs";
194331
- import { join as join18 } from "node:path";
194448
+ import { join as join19 } from "node:path";
194332
194449
  var CommitInfo = type({
194333
194450
  sha: type.string.describe("the commit SHA (full or abbreviated) to fetch")
194334
194451
  });
@@ -194352,7 +194469,7 @@ function CommitInfoTool(ctx) {
194352
194469
  "PULLFROG_TEMP_DIR not set - get_commit_info must run in pullfrog action context"
194353
194470
  );
194354
194471
  }
194355
- const diffFile = join18(tempDir, `commit-${sha.slice(0, 7)}.diff`);
194472
+ const diffFile = join19(tempDir, `commit-${sha.slice(0, 7)}.diff`);
194356
194473
  writeFileSync13(diffFile, formatResult.content);
194357
194474
  log2.debug(`wrote commit diff to ${diffFile} (${formatResult.content.length} bytes)`);
194358
194475
  return {
@@ -194489,14 +194606,14 @@ function ReopenCurrentTool(ctx) {
194489
194606
  import { performance as performance8 } from "node:perf_hooks";
194490
194607
 
194491
194608
  // prep/installNodeDependencies.ts
194492
- import { existsSync as existsSync10 } from "node:fs";
194493
- import { join as join20 } from "node:path";
194609
+ import { existsSync as existsSync11 } from "node:fs";
194610
+ import { join as join21 } from "node:path";
194494
194611
 
194495
194612
  // utils/packageManager.ts
194496
194613
  var import_semver2 = __toESM(require_semver2(), 1);
194497
- import { existsSync as existsSync9 } from "node:fs";
194614
+ import { existsSync as existsSync10 } from "node:fs";
194498
194615
  import { mkdir, readFile as readFile3 } from "node:fs/promises";
194499
- import { delimiter as delimiter2, join as join19 } from "node:path";
194616
+ import { delimiter as delimiter2, join as join20 } from "node:path";
194500
194617
  var SUPPORTED_NAMES = ["npm", "pnpm", "yarn", "bun"];
194501
194618
  var COREPACK_MANAGED = ["pnpm", "yarn"];
194502
194619
  function isSupported(name) {
@@ -194534,8 +194651,8 @@ function parseDevEnginesField(field) {
194534
194651
  };
194535
194652
  }
194536
194653
  async function resolvePackageManagerSpec(cwd) {
194537
- const pkgPath = join19(cwd, "package.json");
194538
- if (!existsSync9(pkgPath)) return null;
194654
+ const pkgPath = join20(cwd, "package.json");
194655
+ if (!existsSync10(pkgPath)) return null;
194539
194656
  let pkg;
194540
194657
  try {
194541
194658
  pkg = JSON.parse(await readFile3(pkgPath, "utf-8"));
@@ -194593,7 +194710,7 @@ async function currentVersion(name) {
194593
194710
  return result.stdout.trim();
194594
194711
  }
194595
194712
  function packageManagerBinDir(tmpdir4) {
194596
- return join19(tmpdir4, "pm-bin");
194713
+ return join20(tmpdir4, "pm-bin");
194597
194714
  }
194598
194715
  async function ensurePackageManager(params) {
194599
194716
  const spec = params.spec;
@@ -194656,7 +194773,7 @@ async function installFallback(name, installSpec) {
194656
194773
  });
194657
194774
  if (result.exitCode !== 0) return result.stderr || `failed to install ${name}`;
194658
194775
  if (name === "deno") {
194659
- const denoPath = join19(process.env.HOME || "", ".deno", "bin");
194776
+ const denoPath = join20(process.env.HOME || "", ".deno", "bin");
194660
194777
  process.env.PATH = `${denoPath}:${process.env.PATH}`;
194661
194778
  }
194662
194779
  log2.info(`\xBB installed ${name}`);
@@ -194689,8 +194806,8 @@ async function isCommandAvailable2(command) {
194689
194806
  var installNodeDependencies = {
194690
194807
  name: "installNodeDependencies",
194691
194808
  shouldRun: () => {
194692
- const packageJsonPath = join20(process.cwd(), "package.json");
194693
- return existsSync10(packageJsonPath);
194809
+ const packageJsonPath = join21(process.cwd(), "package.json");
194810
+ return existsSync11(packageJsonPath);
194694
194811
  },
194695
194812
  run: async (options) => {
194696
194813
  const declared = await resolvePackageManagerSpec(process.cwd());
@@ -194784,14 +194901,14 @@ ${errorMessage}`]
194784
194901
  };
194785
194902
 
194786
194903
  // prep/installPythonDependencies.ts
194787
- import { existsSync as existsSync11, readFileSync as readFileSync7 } from "node:fs";
194788
- import { join as join21 } from "node:path";
194904
+ import { existsSync as existsSync12, readFileSync as readFileSync7 } from "node:fs";
194905
+ import { join as join22 } from "node:path";
194789
194906
  function declaresBuildSystem(path4) {
194790
194907
  return /^\s*\[\s*build-system\s*\]/m.test(readFileSync7(path4, "utf8"));
194791
194908
  }
194792
194909
  function configApplies(config3, cwd) {
194793
- const path4 = join21(cwd, config3.file);
194794
- return existsSync11(path4) && (config3.applies?.(path4) ?? true);
194910
+ const path4 = join22(cwd, config3.file);
194911
+ return existsSync12(path4) && (config3.applies?.(path4) ?? true);
194795
194912
  }
194796
194913
  var PYTHON_CONFIGS = [
194797
194914
  {
@@ -195113,7 +195230,7 @@ function AwaitDependencyInstallationTool(ctx) {
195113
195230
 
195114
195231
  // mcp/gh.ts
195115
195232
  import { mkdirSync as mkdirSync9 } from "node:fs";
195116
- import { join as join22 } from "node:path";
195233
+ import { join as join23 } from "node:path";
195117
195234
  var GH_TIMEOUT_MS = 12e4;
195118
195235
  var GhParams = type({
195119
195236
  args: type.string.array().describe(
@@ -195135,7 +195252,7 @@ function GhTool(ctx) {
195135
195252
  description: `Run the GitHub CLI against ${ctx.repo.owner}/${ctx.repo.name}. Pass argv as an array \u2014 there is no shell, so pipes, redirects and \`&&\` do not work; use gh's own \`--json\`/\`--jq\` flags instead. \`gh api graphql\` is available. Authenticated with ${permissions ? formatPermissions(permissions) : "no permissions"} on this repo only; anything else 403s. Pushing, merging and ref writes are not available \u2014 use the git tools.`,
195136
195253
  parameters: GhParams,
195137
195254
  execute: execute(async (params) => {
195138
- const configDir = join22(ctx.tmpdir, "gh-config");
195255
+ const configDir = join23(ctx.tmpdir, "gh-config");
195139
195256
  mkdirSync9(configDir, { recursive: true });
195140
195257
  const command = ["gh", ...params.args.map(shellQuote)].join(" ");
195141
195258
  const result = await runSandboxed({
@@ -195859,7 +195976,7 @@ function PullRequestInfoTool(ctx) {
195859
195976
 
195860
195977
  // mcp/reviewComments.ts
195861
195978
  import { writeFileSync as writeFileSync14 } from "node:fs";
195862
- import { join as join23 } from "node:path";
195979
+ import { join as join24 } from "node:path";
195863
195980
  var COMMENTS_PER_THREAD = 50;
195864
195981
  var REVIEW_THREADS_QUERY = `
195865
195982
  query ($owner: String!, $name: String!, $prNumber: Int!, $cursor: String) {
@@ -196353,7 +196470,7 @@ function GetReviewCommentsTool(ctx) {
196353
196470
  throw new Error("PULLFROG_TEMP_DIR not set");
196354
196471
  }
196355
196472
  const filename = `review-${params.review_id}-threads.md`;
196356
- const commentsPath = join23(tempDir, filename);
196473
+ const commentsPath = join24(tempDir, filename);
196357
196474
  writeFileSync14(commentsPath, formatted.content);
196358
196475
  log2.debug(`wrote ${threadBlocks.length} threads to ${commentsPath}`);
196359
196476
  return {
@@ -196722,8 +196839,8 @@ function UploadFileTool(ctx) {
196722
196839
  }
196723
196840
 
196724
196841
  // mcp/xrepo.ts
196725
- import { mkdirSync as mkdirSync10, rmSync as rmSync4 } from "node:fs";
196726
- import { join as join24 } from "node:path";
196842
+ import { mkdirSync as mkdirSync10, rmSync as rmSync5 } from "node:fs";
196843
+ import { join as join25 } from "node:path";
196727
196844
  function assertValidRepoName(name) {
196728
196845
  if (!/^[A-Za-z0-9._-]+$/.test(name) || name.startsWith("-") || name.includes("..")) {
196729
196846
  throw new Error(
@@ -196803,7 +196920,7 @@ function CheckoutRepoTool(ctx) {
196803
196920
  return { path: existing.dir, access: existing.access, note: "already checked out." };
196804
196921
  }
196805
196922
  const access = accessFor(ctx, repo);
196806
- const dir = join24(ctx.tmpdir, "xrepo", repo);
196923
+ const dir = join25(ctx.tmpdir, "xrepo", repo);
196807
196924
  mkdirSync10(dir, { recursive: true });
196808
196925
  const state = ensureRepoState(ctx.toolState, { owner, name: repo, dir, access });
196809
196926
  const rc = resolveRepoCtx(ctx, repo);
@@ -196834,7 +196951,7 @@ function CheckoutRepoTool(ctx) {
196834
196951
  return { path: dir, access, defaultBranch };
196835
196952
  } catch (err) {
196836
196953
  ctx.toolState.repos.delete(repoKey(owner, repo));
196837
- rmSync4(dir, { recursive: true, force: true });
196954
+ rmSync5(dir, { recursive: true, force: true });
196838
196955
  throw err;
196839
196956
  }
196840
196957
  })
@@ -197169,6 +197286,7 @@ function isByokSetupError(text) {
197169
197286
  var SECRETS_UNAVAILABLE_MARKER = "couldn't load your Pullfrog secrets";
197170
197287
  var ROUTER_UNFUNDED_MARKER = "your Pullfrog Router balance is empty";
197171
197288
  var CREDENTIAL_REJECTED_MARKER = "was rejected by its provider";
197289
+ var CREDENTIAL_POOL_REFUSED_MARKER = "No configured credential can serve this run";
197172
197290
  var NoUsableCredentialError = class extends Error {
197173
197291
  };
197174
197292
  function throwKeyError(params) {
@@ -197435,7 +197553,8 @@ function isOAuthCredentialExpiredError(text) {
197435
197553
  text
197436
197554
  ) || // the provider no longer recognises the token at all (#1086) — same dead
197437
197555
  // credential, phrased as a lookup miss rather than a state transition.
197438
- /Could not find the appropriate key in your authentication token/i.test(text) || /Token refresh failed/i.test(text)
197556
+ /Could not find the appropriate key in your authentication token/i.test(text) || // the reversed word order: `Encountered invalidated oauth token for user` (#1474).
197557
+ /(?:expired|invalidated|revoked) (?:OAuth|authentication) token/i.test(text) || /Token refresh failed/i.test(text)
197439
197558
  );
197440
197559
  }
197441
197560
  function isMalformedKeyError(text) {
@@ -197482,7 +197601,7 @@ function buildRejectedCredentialError(params) {
197482
197601
  ].join("\n");
197483
197602
  }
197484
197603
  const where = params.inPullfrogStore ? `[Update it in Pullfrog \u2192](${settingsUrl})` : `[Update the GitHub Actions secret \u2192](https://github.com/${params.owner}/${params.name}/settings/secrets/actions)`;
197485
- const keysUrl = providerKeysUrl(params.credential);
197604
+ const keysUrl = providerDashboard(params.credential)?.keys;
197486
197605
  const issue3 = keysUrl ? `[Issue a new key \u2192](${keysUrl}) \xB7 ` : "";
197487
197606
  return [
197488
197607
  `**Your \`${params.credential}\` ${CREDENTIAL_REJECTED_MARKER}**${detail}, so the agent never ran.`,
@@ -197492,12 +197611,43 @@ function buildRejectedCredentialError(params) {
197492
197611
  `${issue3}${where} \xB7 [Model settings \u2192](${settingsUrl}) \xB7 [Setup docs \u2192](https://docs.pullfrog.com/keys) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
197493
197612
  ].join("\n");
197494
197613
  }
197495
- function providerKeysUrl(credential) {
197614
+ function providerDashboard(credential) {
197496
197615
  for (const [id, provider2] of Object.entries(providers)) {
197497
- if (provider2.envVars.includes(credential)) return PROVIDER_DASHBOARDS[id]?.keys;
197616
+ if (provider2.envVars.includes(credential)) return PROVIDER_DASHBOARDS[id];
197498
197617
  }
197499
197618
  return void 0;
197500
197619
  }
197620
+ function buildCredentialPoolRefusedError(params) {
197621
+ const settingsUrl = `${getApiUrl()}/console/${params.owner}/${params.name}`;
197622
+ return [
197623
+ `**${CREDENTIAL_POOL_REFUSED_MARKER}.** Every credential configured for \`${params.model}\` was rejected or is out of quota, so the agent never ran.`,
197624
+ "",
197625
+ ...params.refused.map(
197626
+ (credential) => `- ${describeRefusal({ ...params, credential, settingsUrl })}`
197627
+ ),
197628
+ "",
197629
+ `[Model settings \u2192](${settingsUrl}) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
197630
+ ].join("\n");
197631
+ }
197632
+ function describeRefusal(params) {
197633
+ const credential = params.credential;
197634
+ const verdict = credential.verdict;
197635
+ const workflow = credential.source === "workflow";
197636
+ const head = `\`${credential.name}\` from ${workflow ? "the workflow" : `${credential.source} scope`}: \`${verdict.detail}\` \u2014`;
197637
+ const subscription = SUBSCRIPTION_CREDENTIALS[credential.name];
197638
+ const dashboard = providerDashboard(credential.name);
197639
+ if (verdict.status === "exhausted" && subscription) {
197640
+ const wait = verdict.resetAt ? `it resets at **${verdict.resetAt.toISOString().slice(0, 16).replace("T", " ")} UTC**; re-trigger after that` : "re-trigger after the limit resets";
197641
+ return `${head} ${wait}, or add an API key for this provider, which Pullfrog falls back to automatically.`;
197642
+ }
197643
+ if (verdict.status === "exhausted")
197644
+ return `${head} add credit or raise its quota with the provider.${dashboard ? ` [Top up \u2192](${dashboard.billing})` : ""}`;
197645
+ if (subscription)
197646
+ return `${head} re-authenticate with \`${subscription.command}\`, or switch this repo to a model you hold an API key for. [Re-authenticate \u2192](${subscription.docs})`;
197647
+ const where = workflow ? `[Update the GitHub Actions secret \u2192](https://github.com/${params.owner}/${params.name}/settings/secrets/actions)` : `[Update it in Pullfrog \u2192](${params.settingsUrl})`;
197648
+ const issue3 = dashboard?.keys ? `[Issue a new key \u2192](${dashboard.keys}) \xB7 ` : "";
197649
+ return `${head} issue a new key and update the copy Pullfrog uses. ${issue3}${where}`;
197650
+ }
197501
197651
  function formatApiKeyErrorSummary(params) {
197502
197652
  if (params.raw.includes(MISSING_KEY_MARKER)) {
197503
197653
  if (params.raw.startsWith(`**${MISSING_KEY_MARKER}**`)) return params.raw;
@@ -198227,11 +198377,16 @@ async function selectConfiguredCredential(input) {
198227
198377
  if (option.candidate) log2.info(`\xBB selected ${option.name} from ${from}`);
198228
198378
  return true;
198229
198379
  }
198230
- refused.push(`${option.name} from ${from}: ${verdict.detail}`);
198380
+ refused.push({ name: option.name, source: option.candidate?.source ?? "workflow", verdict });
198231
198381
  log2.info(`\xBB ${option.name} from ${from} unavailable; trying the next credential`);
198232
198382
  }
198233
198383
  throw new Error(
198234
- `all configured credentials for ${model} were rejected or exhausted (${refused.join("; ")}); no other model or provider was selected`
198384
+ buildCredentialPoolRefusedError({
198385
+ model,
198386
+ refused,
198387
+ owner: input.ctx.repo.owner,
198388
+ name: input.ctx.repo.name
198389
+ })
198235
198390
  );
198236
198391
  }
198237
198392
  async function probe(input) {
@@ -198272,7 +198427,7 @@ function activate(input) {
198272
198427
  import { randomUUID as randomUUID6 } from "node:crypto";
198273
198428
  import { writeFileSync as writeFileSync15 } from "node:fs";
198274
198429
  import { createServer as createServer3 } from "node:http";
198275
- import { join as join25 } from "node:path";
198430
+ import { join as join26 } from "node:path";
198276
198431
  var REVOKED_TRAP_MS = 6e4;
198277
198432
  function revokeGitHubToken(token) {
198278
198433
  fetch("https://api.github.com/installation/token", {
@@ -198341,7 +198496,7 @@ async function startGitAuthServer(tmpdir4) {
198341
198496
  function writeAskpassScript(code) {
198342
198497
  const scriptId = randomUUID6();
198343
198498
  const scriptName = `askpass-${scriptId}.js`;
198344
- const scriptPath = join25(tmpdir4, scriptName);
198499
+ const scriptPath = join26(tmpdir4, scriptName);
198345
198500
  const content = [
198346
198501
  `#!/usr/bin/env node`,
198347
198502
  `var a=process.argv[2]||"";`,
@@ -198790,7 +198945,7 @@ function resolveInstructions(ctx) {
198790
198945
 
198791
198946
  // utils/learnings.ts
198792
198947
  import { mkdir as mkdir2, readFile as readFile4, writeFile as writeFile2 } from "node:fs/promises";
198793
- import { dirname as dirname4, join as join26 } from "node:path";
198948
+ import { dirname as dirname4, join as join27 } from "node:path";
198794
198949
 
198795
198950
  // utils/learningsTruncate.ts
198796
198951
  var MAX_LEARNINGS_LENGTH = 1e5;
@@ -198808,10 +198963,10 @@ function truncateAtLineBoundary(body, cap) {
198808
198963
  var LEARNINGS_FILE_NAME = "pullfrog-learnings.md";
198809
198964
  var XREPO_LEARNINGS_FILE_NAME = "pullfrog-xrepo-learnings.md";
198810
198965
  function learningsFilePath(tmpdir4) {
198811
- return join26(tmpdir4, LEARNINGS_FILE_NAME);
198966
+ return join27(tmpdir4, LEARNINGS_FILE_NAME);
198812
198967
  }
198813
198968
  function xrepoLearningsFilePath(tmpdir4) {
198814
- return join26(tmpdir4, XREPO_LEARNINGS_FILE_NAME);
198969
+ return join27(tmpdir4, XREPO_LEARNINGS_FILE_NAME);
198815
198970
  }
198816
198971
  async function seedLearningsFile(params) {
198817
198972
  const path4 = learningsFilePath(params.tmpdir);
@@ -199362,7 +199517,9 @@ async function resolveTokens(params) {
199362
199517
  // commit-status check-runs for branch protection. the app already grants
199363
199518
  // checks:write; this scopes the MCP token up to use it.
199364
199519
  checks: "write",
199365
- actions: "read"
199520
+ // write (not read) so postReviewCleanup can dispatch the follow-up re-review
199521
+ // for commits pushed while a review was in flight; read 403s it.
199522
+ actions: "write"
199366
199523
  };
199367
199524
  const mcpToken = await acquireNewToken({
199368
199525
  repos: writeRepos,
@@ -199748,7 +199905,7 @@ async function resolveTrialFallback(ctx) {
199748
199905
 
199749
199906
  // utils/prSummary.ts
199750
199907
  import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile3 } from "node:fs/promises";
199751
- import { dirname as dirname5, join as join27 } from "node:path";
199908
+ import { dirname as dirname5, join as join28 } from "node:path";
199752
199909
  var SUMMARY_FILE_NAME = "pullfrog-summary.md";
199753
199910
  var SUMMARY_SCAFFOLD = `# PR summary
199754
199911
 
@@ -199758,7 +199915,7 @@ var SUMMARY_SCAFFOLD = `# PR summary
199758
199915
  var MIN_SNAPSHOT_LENGTH = 60;
199759
199916
  var MAX_SNAPSHOT_LENGTH = 32768;
199760
199917
  function summaryFilePath(tmpdir4) {
199761
- return join27(tmpdir4, SUMMARY_FILE_NAME);
199918
+ return join28(tmpdir4, SUMMARY_FILE_NAME);
199762
199919
  }
199763
199920
  async function seedSummaryFile(params) {
199764
199921
  const path4 = summaryFilePath(params.tmpdir);
@@ -200027,7 +200184,7 @@ function formatBillingExhaustedBody(diagnostic) {
200027
200184
 
200028
200185
  // utils/runErrorRenderer.ts
200029
200186
  function isProviderModelNotFoundError(message) {
200030
- return message.includes("ProviderModelNotFoundError");
200187
+ return /ProviderModelNotFoundError|\bModel not found: /.test(message);
200031
200188
  }
200032
200189
  function isContextOverflowError(message) {
200033
200190
  return /Prompt is too long/i.test(message) || /Input exceeds context window/i.test(message) || /maximum context length is \d+ tokens/i.test(message) || /Session too large to compact/i.test(message);
@@ -200124,6 +200281,7 @@ function detectProviderId(message) {
200124
200281
  if (/requires more credits|Key limit exceeded \(total limit\)|openrouter\.ai/i.test(message)) {
200125
200282
  return "openrouter";
200126
200283
  }
200284
+ if (/platform\.openai\.com/i.test(message)) return "openai";
200127
200285
  return null;
200128
200286
  }
200129
200287
  var OPENROUTER_PRIVACY_URL = "https://openrouter.ai/settings/privacy";
@@ -200221,12 +200379,38 @@ function formatByokSetupSummary(input) {
200221
200379
  ].join("\n");
200222
200380
  }
200223
200381
  function formatProviderModelNotFoundSummary(input) {
200224
- return `The configured model is no longer available in OpenCode's catalog. Pick a different model in the Pullfrog console for \`${input.owner}/${input.name}\`, or contact support if this persists.
200382
+ return `The configured model is not in OpenCode's catalog. Pick a different model for \`${input.owner}/${input.name}\` where it is set \u2014 the Pullfrog console, the workflow's \`model\` input, or the \`PULLFROG_MODEL\` variable \u2014 or contact support if this persists.
200225
200383
 
200226
200384
  \`\`\`
200227
200385
  ${input.raw}
200228
200386
  \`\`\``;
200229
200387
  }
200388
+ function formatProviderUsageLimit(input) {
200389
+ return [
200390
+ "**The provider's usage limit for this account has been reached.**",
200391
+ "",
200392
+ "A subscription window resets on its own, so re-trigger after it does; a limit set on a pay-as-you-go workspace has to be raised. A model on another provider can run in the meantime.",
200393
+ "",
200394
+ `[Model settings \u2192](${getApiUrl()}/console/${input.owner}/${input.name}) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`,
200395
+ "",
200396
+ `\`\`\`
200397
+ ${input.raw}
200398
+ \`\`\``
200399
+ ].join("\n");
200400
+ }
200401
+ function formatProviderErrorBody(input) {
200402
+ return [
200403
+ "**The model provider returned an error, so this run stopped.**",
200404
+ "",
200405
+ "The error is below. If it names your key, plan or balance, fix that with the provider or pick another model before re-triggering.",
200406
+ "",
200407
+ `[Model settings \u2192](${getApiUrl()}/console/${input.owner}/${input.name}) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`,
200408
+ "",
200409
+ `\`\`\`
200410
+ ${input.raw}
200411
+ \`\`\``
200412
+ ].join("\n");
200413
+ }
200230
200414
  function renderRunError(input) {
200231
200415
  const billingError = input.routerActive && isRouterKeylimitExhaustedError(input.errorMessage) ? new BillingError(input.errorMessage, { code: "router_keylimit_exhausted" }) : null;
200232
200416
  if (billingError) {
@@ -200242,7 +200426,7 @@ function renderRunError(input) {
200242
200426
  if (input.errorMessage.includes(ROUTER_UNFUNDED_MARKER)) {
200243
200427
  return { summary: input.errorMessage, comment: input.errorMessage };
200244
200428
  }
200245
- if (input.errorMessage.includes(CREDENTIAL_REJECTED_MARKER)) {
200429
+ if (input.errorMessage.includes(CREDENTIAL_REJECTED_MARKER) || input.errorMessage.includes(CREDENTIAL_POOL_REFUSED_MARKER)) {
200246
200430
  return { summary: input.errorMessage, comment: input.errorMessage };
200247
200431
  }
200248
200432
  const isHang = input.errorMessage.startsWith("activity timeout") || input.errorMessage.startsWith("agent still pending");
@@ -200265,6 +200449,16 @@ ${body}`, comment: body };
200265
200449
  });
200266
200450
  return { summary: `### \u274C Pullfrog failed
200267
200451
 
200452
+ ${body}`, comment: body };
200453
+ }
200454
+ if (isProviderUsageLimit(input.errorMessage)) {
200455
+ const body = formatProviderUsageLimit({
200456
+ owner: input.repo.owner,
200457
+ name: input.repo.name,
200458
+ raw: input.errorMessage
200459
+ });
200460
+ return { summary: `### \u274C Pullfrog failed
200461
+
200268
200462
  ${body}`, comment: body };
200269
200463
  }
200270
200464
  if (isProviderMissingCredential(input.errorMessage)) {
@@ -200359,6 +200553,16 @@ ${hangBody}`,
200359
200553
  comment: explained ? hangBody : formatMinimalFailureComment(input.repo)
200360
200554
  };
200361
200555
  }
200556
+ if (input.errorMessage.startsWith("provider error: ")) {
200557
+ const body = formatProviderErrorBody({
200558
+ owner: input.repo.owner,
200559
+ name: input.repo.name,
200560
+ raw: input.errorMessage
200561
+ });
200562
+ return { summary: `### \u274C Pullfrog failed
200563
+
200564
+ ${body}`, comment: body };
200565
+ }
200362
200566
  const genericBody = formatGenericFailure(input.errorMessage);
200363
200567
  return {
200364
200568
  summary: `### \u274C Pullfrog failed
@@ -201169,7 +201373,7 @@ async function main() {
201169
201373
  const repoDir = payload.cwd;
201170
201374
  const preIntrospectionDirty = repoDir ? await dirtyTrackedPaths({ cwd: repoDir }) : null;
201171
201375
  const opencodeCliPath = await agents.opencode.install();
201172
- captureBaselineModels(opencodeCliPath);
201376
+ captureBaselineModels({ cliPath: opencodeCliPath, tmpdir: tmpdir4 });
201173
201377
  const injected = [];
201174
201378
  if (runContext.dbSecrets) {
201175
201379
  for (const [key, value2] of Object.entries(runContext.dbSecrets)) {
@@ -201186,7 +201390,7 @@ async function main() {
201186
201390
  await initializeCredentialPool({ ctx: runContext, slug: payload.model, injected });
201187
201391
  installCodexAuth();
201188
201392
  installXaiAuth();
201189
- captureAuthorizedModels(opencodeCliPath);
201393
+ captureAuthorizedModels({ cliPath: opencodeCliPath, tmpdir: tmpdir4 });
201190
201394
  if (preIntrospectionDirty && repoDir) {
201191
201395
  await restoreDirtiedSince({
201192
201396
  before: preIntrospectionDirty,
@@ -201524,8 +201728,8 @@ ${instructions.user}` : null,
201524
201728
  log2.info(instructions.full);
201525
201729
  });
201526
201730
  if (agentId === "opencode") {
201527
- const pluginDir = join28(process.cwd(), ".opencode", "plugin");
201528
- const hasPlugins = existsSync12(pluginDir) && readdirSync2(pluginDir).some((f) => /\.[jt]sx?$/.test(f));
201731
+ const pluginDir = join29(process.cwd(), ".opencode", "plugin");
201732
+ const hasPlugins = existsSync13(pluginDir) && readdirSync2(pluginDir).some((f) => /\.[jt]sx?$/.test(f));
201529
201733
  if (hasPlugins && toolState.dependencyInstallation?.promise) {
201530
201734
  log2.info(
201531
201735
  "\xBB .opencode/plugin/ detected \u2014 awaiting dependency installation before agent start"
@@ -202134,7 +202338,7 @@ async function run2() {
202134
202338
  // commands/_prEvents.ts
202135
202339
  import { mkdirSync as mkdirSync11, readFileSync as readFileSync10, writeFileSync as writeFileSync16 } from "node:fs";
202136
202340
  import { homedir as homedir3 } from "node:os";
202137
- import { join as join29 } from "node:path";
202341
+ import { join as join30 } from "node:path";
202138
202342
  var REQUEST_TIMEOUT_MS = 35e3;
202139
202343
  var SERVER_POLL_WINDOW_MS = 2e4;
202140
202344
  var PrEventsAuthError = class extends Error {
@@ -202183,11 +202387,11 @@ async function pollPrEvents(ctx) {
202183
202387
  }
202184
202388
  }
202185
202389
  function stateDir() {
202186
- return process.env.PULLFROG_STATE_DIR || join29(homedir3(), ".pullfrog");
202390
+ return process.env.PULLFROG_STATE_DIR || join30(homedir3(), ".pullfrog");
202187
202391
  }
202188
202392
  function cursorPath(target) {
202189
202393
  const key = [target.owner, target.repo, String(target.pr)].map(encodeURIComponent).join("__");
202190
- return join29(stateDir(), "watch", `${key}.cursor`);
202394
+ return join30(stateDir(), "watch", `${key}.cursor`);
202191
202395
  }
202192
202396
  function readCursor(target) {
202193
202397
  try {
@@ -202200,7 +202404,7 @@ function readCursor(target) {
202200
202404
  function writeCursor(target, cursor3) {
202201
202405
  try {
202202
202406
  const path4 = cursorPath(target);
202203
- mkdirSync11(join29(stateDir(), "watch"), { recursive: true });
202407
+ mkdirSync11(join30(stateDir(), "watch"), { recursive: true });
202204
202408
  writeFileSync16(path4, cursor3, "utf-8");
202205
202409
  } catch {
202206
202410
  }
@@ -202255,7 +202459,7 @@ function resolveTarget2(repo) {
202255
202459
  return { owner: match3[1], repo: match3[2] };
202256
202460
  }
202257
202461
  function cliVersion() {
202258
- const parts = /^(\d+)\.(\d+)\.(\d+)$/.exec("0.1.96");
202462
+ const parts = /^(\d+)\.(\d+)\.(\d+)$/.exec("0.1.98");
202259
202463
  if (!parts) return "0.0.0";
202260
202464
  return `${Number(parts[1])}.${Number(parts[2])}.${Number(parts[3])}`;
202261
202465
  }
@@ -202422,7 +202626,7 @@ async function runCli8(input) {
202422
202626
  }
202423
202627
 
202424
202628
  // cli.ts
202425
- var VERSION10 = "0.1.96";
202629
+ var VERSION10 = "0.1.98";
202426
202630
  var bin = basename2(process.argv[1] || "");
202427
202631
  var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
202428
202632
  var rawArgs = process.argv.slice(2);