pullfrog 0.1.28 → 0.1.30

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
@@ -19991,10 +19991,10 @@ var require_core = __commonJS({
19991
19991
  (0, command_1.issueCommand)("set-env", { name }, convertedVal);
19992
19992
  }
19993
19993
  exports.exportVariable = exportVariable;
19994
- function setSecret6(secret) {
19994
+ function setSecret7(secret) {
19995
19995
  (0, command_1.issueCommand)("add-mask", {}, secret);
19996
19996
  }
19997
- exports.setSecret = setSecret6;
19997
+ exports.setSecret = setSecret7;
19998
19998
  function addPath(inputPath) {
19999
19999
  const filePath = process.env["GITHUB_PATH"] || "";
20000
20000
  if (filePath) {
@@ -20111,12 +20111,12 @@ Support boolean input list: \`true | True | TRUE | false | False | FALSE\``);
20111
20111
  return process.env[`STATE_${name}`] || "";
20112
20112
  }
20113
20113
  exports.getState = getState2;
20114
- function getIDToken4(aud) {
20114
+ function getIDToken3(aud) {
20115
20115
  return __awaiter(this, void 0, void 0, function* () {
20116
20116
  return yield oidc_utils_1.OidcClient.getIDToken(aud);
20117
20117
  });
20118
20118
  }
20119
- exports.getIDToken = getIDToken4;
20119
+ exports.getIDToken = getIDToken3;
20120
20120
  var summary_1 = require_summary();
20121
20121
  Object.defineProperty(exports, "summary", { enumerable: true, get: function() {
20122
20122
  return summary_1.summary;
@@ -93765,14 +93765,14 @@ var require_turndown_cjs = __commonJS({
93765
93765
  } else if (node2.nodeType === 1) {
93766
93766
  replacement = replacementForNode.call(self2, node2);
93767
93767
  }
93768
- return join23(output, replacement);
93768
+ return join25(output, replacement);
93769
93769
  }, "");
93770
93770
  }
93771
93771
  function postProcess(output) {
93772
93772
  var self2 = this;
93773
93773
  this.rules.forEach(function(rule) {
93774
93774
  if (typeof rule.append === "function") {
93775
- output = join23(output, rule.append(self2.options));
93775
+ output = join25(output, rule.append(self2.options));
93776
93776
  }
93777
93777
  });
93778
93778
  return output.replace(/^[\t\r\n]+/, "").replace(/[\t\r\n\s]+$/, "");
@@ -93784,7 +93784,7 @@ var require_turndown_cjs = __commonJS({
93784
93784
  if (whitespace.leading || whitespace.trailing) content = content.trim();
93785
93785
  return whitespace.leading + rule.replacement(content, node2, this.options) + whitespace.trailing;
93786
93786
  }
93787
- function join23(output, replacement) {
93787
+ function join25(output, replacement) {
93788
93788
  var s1 = trimTrailingNewlines(output);
93789
93789
  var s2 = trimLeadingNewlines(replacement);
93790
93790
  var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
@@ -100726,7 +100726,7 @@ import { dirname as dirname6 } from "node:path";
100726
100726
  // main.ts
100727
100727
  import { existsSync as existsSync8, readdirSync as readdirSync2 } from "node:fs";
100728
100728
  import { readFile as readFile5 } from "node:fs/promises";
100729
- import { join as join22 } from "node:path";
100729
+ import { join as join24 } from "node:path";
100730
100730
 
100731
100731
  // agents/claude.ts
100732
100732
  import { execFileSync as execFileSync3 } from "node:child_process";
@@ -100743,11 +100743,20 @@ var providers = {
100743
100743
  displayName: "Anthropic",
100744
100744
  envVars: ["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"],
100745
100745
  models: {
100746
+ // OpenRouter serves claude-fable-5, but models.dev's OpenRouter mirror
100747
+ // hasn't indexed it yet (shipped 2026-06-09), so the catalog drift gate
100748
+ // can't validate an openRouterResolve. omit it until the mirror catches
100749
+ // up; direct BYOK / Claude Code resolves anthropic/claude-fable-5 fine.
100750
+ "claude-fable": {
100751
+ displayName: "Claude Fable",
100752
+ resolve: "anthropic/claude-fable-5",
100753
+ preferred: true,
100754
+ subagentModel: "claude-sonnet"
100755
+ },
100746
100756
  "claude-opus": {
100747
100757
  displayName: "Claude Opus",
100748
100758
  resolve: "anthropic/claude-opus-4-8",
100749
100759
  openRouterResolve: "openrouter/anthropic/claude-opus-4.8",
100750
- preferred: true,
100751
100760
  subagentModel: "claude-sonnet"
100752
100761
  },
100753
100762
  "claude-sonnet": {
@@ -100813,7 +100822,8 @@ var providers = {
100813
100822
  },
100814
100823
  o3: {
100815
100824
  displayName: "O3",
100816
- resolve: "openai/o3"
100825
+ resolve: "openai/o3",
100826
+ openRouterResolve: "openrouter/openai/o3"
100817
100827
  }
100818
100828
  }
100819
100829
  }),
@@ -101183,6 +101193,18 @@ function parseModel(slug2) {
101183
101193
  function getModelProvider(slug2) {
101184
101194
  return parseModel(slug2).provider;
101185
101195
  }
101196
+ function getModelEnvVars(slug2) {
101197
+ const parsed2 = parseModel(slug2);
101198
+ const providerConfig = providers[parsed2.provider];
101199
+ if (!providerConfig) {
101200
+ return [];
101201
+ }
101202
+ const modelConfig = providerConfig.models[parsed2.model];
101203
+ if (modelConfig?.envVars) {
101204
+ return modelConfig.envVars.slice();
101205
+ }
101206
+ return providerConfig.envVars.slice();
101207
+ }
101186
101208
  var modelAliases = Object.entries(providers).flatMap(
101187
101209
  ([providerKey, config3]) => Object.entries(config3.models).map(([modelId, def]) => ({
101188
101210
  slug: `${providerKey}/${modelId}`,
@@ -101207,6 +101229,9 @@ if (!defaultProxyAlias?.openRouterResolve) {
101207
101229
  }
101208
101230
  var DEFAULT_PROXY_MODEL = defaultProxyAlias.openRouterResolve;
101209
101231
  var defaultProxyDisplayName = defaultProxyAlias.displayName;
101232
+ function resolveModelSlug(slug2) {
101233
+ return modelAliases.find((a) => a.slug === slug2)?.resolve;
101234
+ }
101210
101235
  var MAX_FALLBACK_DEPTH = 10;
101211
101236
  function resolveDisplayAlias(slug2) {
101212
101237
  let current = slug2;
@@ -101354,6 +101379,51 @@ function createProcessOutputActivityTimeout(ctx) {
101354
101379
  };
101355
101380
  }
101356
101381
 
101382
+ // utils/claudeSubscription.ts
101383
+ var CLAUDE_CODE_IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude.";
101384
+ var fallbackResolve = resolveModelSlug("anthropic/claude-haiku");
101385
+ if (!fallbackResolve) {
101386
+ throw new Error("claudeSubscription preflight: anthropic/claude-haiku missing from registry");
101387
+ }
101388
+ var FALLBACK_PROBE_MODEL = fallbackResolve.slice(fallbackResolve.indexOf("/") + 1);
101389
+ async function preflightClaudeSubscription(params) {
101390
+ let res;
101391
+ try {
101392
+ res = await fetch("https://api.anthropic.com/v1/messages", {
101393
+ method: "POST",
101394
+ headers: {
101395
+ authorization: `Bearer ${params.token}`,
101396
+ "anthropic-beta": "claude-code-20250219,oauth-2025-04-20",
101397
+ "anthropic-version": "2023-06-01",
101398
+ "content-type": "application/json",
101399
+ "x-app": "cli"
101400
+ },
101401
+ body: JSON.stringify({
101402
+ model: params.model ?? FALLBACK_PROBE_MODEL,
101403
+ max_tokens: 1,
101404
+ system: CLAUDE_CODE_IDENTITY,
101405
+ messages: [{ role: "user", content: "ok" }]
101406
+ }),
101407
+ signal: AbortSignal.timeout(1e4)
101408
+ });
101409
+ } catch {
101410
+ return { usable: true };
101411
+ }
101412
+ if (res.status !== 401 && res.status !== 429) return { usable: true };
101413
+ const body = await res.text().catch(() => "");
101414
+ return { usable: false, reason: `${res.status}: ${extractApiErrorMessage(body)}` };
101415
+ }
101416
+ function extractApiErrorMessage(body) {
101417
+ try {
101418
+ const parsed2 = JSON.parse(body);
101419
+ if (typeof parsed2 === "object" && parsed2 !== null && "error" in parsed2 && typeof parsed2.error === "object" && parsed2.error !== null && "message" in parsed2.error && typeof parsed2.error.message === "string") {
101420
+ return parsed2.error.message;
101421
+ }
101422
+ } catch {
101423
+ }
101424
+ return body.slice(0, 200);
101425
+ }
101426
+
101357
101427
  // utils/log.ts
101358
101428
  var core = __toESM(require_core(), 1);
101359
101429
  var import_table = __toESM(require_src2(), 1);
@@ -101867,7 +101937,7 @@ var import_semver = __toESM(require_semver2(), 1);
101867
101937
  // package.json
101868
101938
  var package_default = {
101869
101939
  name: "pullfrog",
101870
- version: "0.1.28",
101940
+ version: "0.1.30",
101871
101941
  type: "module",
101872
101942
  bin: {
101873
101943
  pullfrog: "dist/cli.mjs",
@@ -102442,10 +102512,13 @@ function resolveVertexOpenCodeModel(model) {
102442
102512
  var SUBAGENT_DENIED_TOOLS = [
102443
102513
  // working-tree mutation: switches HEAD onto pr-N and registers a push remote
102444
102514
  "checkout_pr",
102445
- // remote mutation: pushes commits / branches / tags / deletes a branch
102515
+ // remote mutation: pushes commits / branches / tags / deletes a branch.
102516
+ // commit_changes lands the orchestrator's (possibly half-finished) shared
102517
+ // working tree directly on the remote — strictly worse than push_branch.
102446
102518
  "push_branch",
102447
102519
  "push_tags",
102448
102520
  "delete_branch",
102521
+ "commit_changes",
102449
102522
  // GitHub PR state mutation
102450
102523
  "create_pull_request",
102451
102524
  "update_pull_request_body",
@@ -102710,8 +102783,10 @@ Inline comments use the same severity framing as body \`### \` sections, scaled
102710
102783
  - **Don't repeat diff content**, don't include raw \`+123 / -45\` stats, don't include a changelog section, don't use horizontal rules (\`---\`).
102711
102784
  - **Pull file/commit counts from \`checkout_pr\` metadata** \u2014 never count manually.
102712
102785
  - **Legacy headings REMOVED.** Do not use \`### Key changes\`, \`### Issues found\`, \`<b>TL;DR</b>\`, or \`<sub><b>Summary</b>\`. The new structure subsumes them.`;
102713
- function computeModes(agentId) {
102786
+ function computeModes(agentId, signedCommits = false) {
102714
102787
  const t2 = (toolName) => formatMcpToolRef(agentId, toolName);
102788
+ const commitStep = signedCommits ? `commit via \`${t2("commit_changes")}\` \u2014 it lands a GitHub-signed commit directly on the remote branch (no push step)` : `commit locally via shell (\`git add . && git commit -m "..."\`)`;
102789
+ const finalizeStep = signedCommits ? `confirm a clean working tree (\`git status\`) \u2014 your \`${t2("commit_changes")}\` calls already landed the work on the remote` : `confirm a clean working tree, then push via \`${t2("push_branch")}\``;
102715
102790
  return [
102716
102791
  {
102717
102792
  name: "Build",
@@ -102782,10 +102857,10 @@ function computeModes(agentId) {
102782
102857
  - Do NOT defect-hunt the diff yourself in parallel with the subagent. Your role is dispatch + evaluation; doing the review yourself reintroduces the implementation bias the subagent is meant to mitigate.
102783
102858
  - For diffs that rely on third-party API contracts, SDK semantics, framework directives, or DB engine specifics, instruct the subagent to verify load-bearing claims via web search and quote source URLs rather than trust training data \u2014 this is the single most common review-quality failure mode.
102784
102859
 
102785
- Be **discerning** about what comes back. The reviewer is an AI subagent and is fallible \u2014 treat every finding as a hypothesis, not a directive, and **verify each one yourself** against the diff and the code before deciding whether to apply. You are searching for a solution that is **complete, minimal, and elegant** \u2014 you may need to think hard to find it. Do not over-engineer, do not be over-defensive, **do not write AI slop**. Reviewers bias toward *recommending additions*, and that bias has a recognizable slop texture: defensive checks for cases that cannot happen, extra logging, new abstractions used once, comments restating code, tests asserting tautologies, "just-in-case" guards, error handlers for cases the type system already rules out. Reject those. For each surviving finding, ask: would applying it leave the code more sound, correct, AND elegant? Two-out-of-three means look harder for a fix that gets all three before settling. After applying the fixes you accept, re-read your diff and be discerning about what *you just changed*: if any fix turned out to be bloat in context, revert it. Then verify only intended changes are present, no debug artifacts or commented-out code remain, no unrelated files were modified. Commit locally via shell (\`git add . && git commit -m "..."\`).
102860
+ Be **discerning** about what comes back. The reviewer is an AI subagent and is fallible \u2014 treat every finding as a hypothesis, not a directive, and **verify each one yourself** against the diff and the code before deciding whether to apply. You are searching for a solution that is **complete, minimal, and elegant** \u2014 you may need to think hard to find it. Do not over-engineer, do not be over-defensive, **do not write AI slop**. Reviewers bias toward *recommending additions*, and that bias has a recognizable slop texture: defensive checks for cases that cannot happen, extra logging, new abstractions used once, comments restating code, tests asserting tautologies, "just-in-case" guards, error handlers for cases the type system already rules out. Reject those. For each surviving finding, ask: would applying it leave the code more sound, correct, AND elegant? Two-out-of-three means look harder for a fix that gets all three before settling. After applying the fixes you accept, re-read your diff and be discerning about what *you just changed*: if any fix turned out to be bloat in context, revert it. Then verify only intended changes are present, no debug artifacts or commented-out code remain, no unrelated files were modified. Then ${commitStep}.
102786
102861
 
102787
102862
  6. **finalize**:
102788
- - confirm a clean working tree, then push via \`${t2("push_branch")}\` (see *SYSTEM* Git rules if this fails \u2014 prepush errors are usually the repo's tests/lint, not infra timeouts)
102863
+ - ${finalizeStep} (see *SYSTEM* Git rules if this fails \u2014 prepush errors are usually the repo's tests/lint, not infra timeouts)
102789
102864
  - create a PR via \`${t2("create_pull_request")}\`
102790
102865
  - call \`${t2("report_progress")}\` with the PR link or the exact error if push/PR failed
102791
102866
 
@@ -102813,12 +102888,12 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
102813
102888
 
102814
102889
  5. Quality check:
102815
102890
  - test changes, then review the diff before committing \u2014 verify only intended changes are present, no debug artifacts remain, no fix turned out to be bloat in context (revert any that did), and the changes are clean enough that a senior engineer would approve without hesitation
102816
- - commit locally via shell (\`git add . && git commit -m "..."\`)
102891
+ - ${commitStep}
102817
102892
 
102818
102893
  6. Finalize. Reply + resolve are paired write actions: do BOTH or NEITHER for each thread.
102819
- - confirm a clean working tree, then push via \`${t2("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*)
102820
- - **if push fails**, call \`${t2("report_progress")}\` with the exact error and STOP \u2014 do NOT reply or resolve any thread until the fix is live on the remote. Resolving a thread without the fix landing misleads the reviewer.
102821
- - **on push success**, for each thread you acted on:
102894
+ - ${finalizeStep} (same push/prepush guidance as Build mode in *SYSTEM*)
102895
+ - **if the push/commit fails**, call \`${t2("report_progress")}\` with the exact error and STOP \u2014 do NOT reply or resolve any thread until the fix is live on the remote. Resolving a thread without the fix landing misleads the reviewer.
102896
+ - **once the fix is live on the remote**, for each thread you acted on:
102822
102897
  - reply ONCE via \`${t2("reply_to_review_comment")}\`. The \`comment_id\` parameter takes the root comment's numeric \`id=\` (from the first \`comment author=...\` tag in the \`${t2("get_review_comments")}\` output) \u2014 NOT the \`thread=\` value; that's a separate GraphQL ID used by resolve. The runtime dedupes identical bodies within a session.
102823
102898
  - **immediately** call \`${t2("resolve_review_thread")}\` with that thread's \`thread=\` value as \`thread_id\`. Resolve every thread where you (a) made the requested code change in full \u2014 partial fixes leave the thread open \u2014 OR (b) replied with a substantive answer the user explicitly asked for. Do NOT resolve threads where you pushed back on the request and the disagreement is unresolved; leave those open for the human to mediate.
102824
102899
  - call \`${t2("report_progress")}\` with a brief summary`
@@ -103093,10 +103168,10 @@ ${PR_SUMMARY_FORMAT}`
103093
103168
  - fix the issue using your native file and shell tools
103094
103169
  - verify the fix by re-running the exact CI command
103095
103170
  - review the diff before committing \u2014 verify only the fix is present, no debug artifacts, no unrelated changes. the fix should be clean enough that a senior engineer would approve without hesitation.
103096
- - commit locally via shell (\`git add . && git commit -m "..."\`)
103171
+ - ${commitStep}
103097
103172
 
103098
103173
  6. Finalize:
103099
- - confirm a clean working tree, then push via \`${t2("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*)
103174
+ - ${finalizeStep} (same push/prepush guidance as Build mode in *SYSTEM*)
103100
103175
  - call \`${t2("report_progress")}\` with the diagnosis and fix summary (or the exact push error if push failed)`
103101
103176
  },
103102
103177
  {
@@ -103112,8 +103187,8 @@ ${PR_SUMMARY_FORMAT}`
103112
103187
  - Call \`${t2("git_fetch")}\` to fetch the base branch.
103113
103188
 
103114
103189
  3. **Merge Attempt**:
103115
- - Run \`git merge origin/<base_branch>\` via shell.
103116
- - If it succeeds automatically, confirm a clean working tree, push via \`${t2("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*), and call \`${t2("report_progress")}\` with a brief success note or the exact push error if push failed \u2014 **then stop; do not run steps 4\u20135.**
103190
+ - Run \`git merge ${signedCommits ? "--no-commit " : ""}origin/<base_branch>\` via shell.
103191
+ - If it succeeds automatically, ${signedCommits ? `conclude it via \`${t2("commit_changes")}\` (it turns the pending merge into a signed merge commit on the remote)` : `confirm a clean working tree, push via \`${t2("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*)`}, and call \`${t2("report_progress")}\` with a brief success note or the exact error if it failed \u2014 **then stop; do not run steps 4\u20135.**
103117
103192
  - If it fails (conflicts), resolve them manually (continue to steps 4\u20135).
103118
103193
 
103119
103194
  4. **Resolve Conflicts**:
@@ -103123,8 +103198,8 @@ ${PR_SUMMARY_FORMAT}`
103123
103198
 
103124
103199
  5. **Finalize**:
103125
103200
  - Run a final verification (build/test) to ensure the resolution works.
103126
- - \`git add . && git commit -m "resolve merge conflicts"\`
103127
- - confirm a clean working tree, then push via \`${t2("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*)
103201
+ - ${signedCommits ? `\`git add .\`, then conclude via \`${t2("commit_changes")}\` with message "resolve merge conflicts"` : `\`git add . && git commit -m "resolve merge conflicts"\``}
103202
+ - ${finalizeStep} (same push/prepush guidance as Build mode in *SYSTEM*)
103128
103203
  - Call \`${t2("report_progress")}\` with a summary of what was resolved (or the exact push error if push failed)`
103129
103204
  },
103130
103205
  {
@@ -103143,7 +103218,7 @@ ${PR_SUMMARY_FORMAT}`
103143
103218
  - if code changes are needed: review your own diff before committing \u2014 verify only intended changes are present, no debug artifacts remain, and the changes are clean enough that a senior engineer would approve without hesitation
103144
103219
 
103145
103220
  4. Finalize:
103146
- - if code changes were made, push to a pull request (new or existing) using \`${t2("push_branch")}\` and \`${t2("create_pull_request")}\` as needed. \`git status\` must be clean before you finish (see *SYSTEM* Git rules if push fails).
103221
+ - if code changes were made, get them onto a pull request (new or existing) using ${signedCommits ? `\`${t2("commit_changes")}\`` : `\`${t2("push_branch")}\``} and \`${t2("create_pull_request")}\` as needed. \`git status\` must be clean before you finish (see *SYSTEM* Git rules if this fails).
103147
103222
  - call \`${t2("report_progress")}\` once with results \u2014 include exact tool errors if push or PR creation failed
103148
103223
  - if the task involved labeling, commenting, or other GitHub operations, perform those directly`
103149
103224
  }
@@ -104123,10 +104198,21 @@ var claude = agent({
104123
104198
  env2.ANTHROPIC_MODEL = specifier;
104124
104199
  }
104125
104200
  if (env2.CLAUDE_CODE_OAUTH_TOKEN && !isBedrockRoute && env2.ANTHROPIC_API_KEY) {
104126
- log.debug(
104127
- "\xBB CLAUDE_CODE_OAUTH_TOKEN present \u2014 stripping ANTHROPIC_API_KEY from Claude Code env so the OAuth subscription is used"
104128
- );
104129
- delete env2.ANTHROPIC_API_KEY;
104201
+ const preflight = await preflightClaudeSubscription({
104202
+ token: env2.CLAUDE_CODE_OAUTH_TOKEN,
104203
+ model
104204
+ });
104205
+ if (preflight.usable) {
104206
+ log.debug(
104207
+ "\xBB CLAUDE_CODE_OAUTH_TOKEN present \u2014 stripping ANTHROPIC_API_KEY from Claude Code env so the OAuth subscription is used"
104208
+ );
104209
+ delete env2.ANTHROPIC_API_KEY;
104210
+ } else {
104211
+ log.info(
104212
+ `\xBB Claude subscription unusable (${preflight.reason}) \u2014 falling back to ANTHROPIC_API_KEY`
104213
+ );
104214
+ delete env2.CLAUDE_CODE_OAUTH_TOKEN;
104215
+ }
104130
104216
  }
104131
104217
  log.info(`\xBB effort: ${effort}`);
104132
104218
  log.debug(`\xBB starting Pullfrog (Claude Code): ${cliPath} ${baseArgs.join(" ")}`);
@@ -117144,15 +117230,6 @@ function isLocalApiUrl() {
117144
117230
  // utils/buildPullfrogFooter.ts
117145
117231
  var PULLFROG_DIVIDER = "<!-- PULLFROG_DIVIDER_DO_NOT_REMOVE_PLZ -->";
117146
117232
  var FROG_LOGO = `<a href="https://pullfrog.com"><picture><source media="(prefers-color-scheme: dark)" srcset="https://pullfrog.com/logos/frog-white-full-18px.png"><img src="https://pullfrog.com/logos/frog-green-full-18px.png" width="9px" height="9px" style="vertical-align: middle; " alt="Pullfrog"></picture></a>`;
117147
- function providerDisplayName(slug2) {
117148
- try {
117149
- const key = getModelProvider(slug2);
117150
- const meta3 = providers[key];
117151
- return meta3?.displayName ?? key;
117152
- } catch {
117153
- return slug2;
117154
- }
117155
- }
117156
117233
  function formatModelLabel(params) {
117157
117234
  const alias = resolveDisplayAlias(params.model) ?? // reverse-lookup: when the caller passes an effective model (proxy or
117158
117235
  // resolved target like "openrouter/anthropic/claude-opus-4.7") instead of
@@ -117163,9 +117240,7 @@ function formatModelLabel(params) {
117163
117240
  if (params.oss) {
117164
117241
  return `\`${displayName}\` (free via [Pullfrog for OSS](https://pullfrog.com/for-oss))`;
117165
117242
  }
117166
- const base = alias?.isFree ? `\`${displayName}\` (free)` : `\`${displayName}\``;
117167
- if (!params.fallbackFrom) return base;
117168
- return `${base} (credentials for ${providerDisplayName(params.fallbackFrom)} not configured)`;
117243
+ return alias?.isFree ? `\`${displayName}\` (free)` : `\`${displayName}\``;
117169
117244
  }
117170
117245
  function buildPullfrogFooter(params) {
117171
117246
  const parts = [];
@@ -117183,9 +117258,7 @@ function buildPullfrogFooter(params) {
117183
117258
  parts.push("via [Pullfrog](https://pullfrog.com)");
117184
117259
  }
117185
117260
  if (params.model) {
117186
- parts.push(
117187
- `Using ${formatModelLabel({ model: params.model, fallbackFrom: params.fallbackFrom, oss: params.oss })}`
117188
- );
117261
+ parts.push(`Using ${formatModelLabel({ model: params.model, oss: params.oss })}`);
117189
117262
  }
117190
117263
  const allParts = [...parts, "[\u{1D54F}](https://x.com/pullfrogai)"];
117191
117264
  return `
@@ -117983,7 +118056,6 @@ function buildCommentFooter(ctx, customParts) {
117983
118056
  } : void 0,
117984
118057
  customParts,
117985
118058
  model: ctx.toolState.model,
117986
- fallbackFrom: ctx.toolState.modelFallback?.from,
117987
118059
  oss: ctx.oss
117988
118060
  });
117989
118061
  }
@@ -119057,24 +119129,60 @@ var installPythonDependencies = {
119057
119129
 
119058
119130
  // prep/index.ts
119059
119131
  var prepSteps = [installNodeDependencies, installPythonDependencies];
119132
+ async function dirtyTrackedPaths() {
119133
+ const result = await spawn3({
119134
+ cmd: "git",
119135
+ args: ["diff", "--name-only", "HEAD"],
119136
+ env: process.env,
119137
+ activityTimeout: 0
119138
+ });
119139
+ if (result.exitCode !== 0) {
119140
+ throw new Error(
119141
+ `git diff --name-only HEAD failed (exit ${result.exitCode}): ${result.stderr.trim() || "(no stderr)"}`
119142
+ );
119143
+ }
119144
+ return new Set(result.stdout.split("\n").filter(Boolean));
119145
+ }
119146
+ async function restorePrepDirtiedFiles(preDirty) {
119147
+ const dirtied = [...await dirtyTrackedPaths()].filter((path4) => !preDirty.has(path4));
119148
+ if (dirtied.length === 0) return;
119149
+ const result = await spawn3({
119150
+ cmd: "git",
119151
+ args: ["restore", "--staged", "--worktree", "--", ...dirtied],
119152
+ env: process.env,
119153
+ activityTimeout: 0
119154
+ });
119155
+ if (result.exitCode !== 0) {
119156
+ log.warning(
119157
+ `\xBB failed to restore ${dirtied.length} tracked file(s) modified by prep: ${result.stderr.trim() || "(no stderr)"}`
119158
+ );
119159
+ return;
119160
+ }
119161
+ log.info(`\xBB restored ${dirtied.length} tracked file(s) modified by prep: ${dirtied.join(", ")}`);
119162
+ }
119060
119163
  async function runPrepPhase(options) {
119061
119164
  log.debug("\xBB starting prep phase...");
119062
119165
  const startTime = performance7.now();
119063
119166
  const results = [];
119064
- for (const step of prepSteps) {
119065
- const shouldRun = await step.shouldRun();
119066
- if (!shouldRun) {
119067
- log.debug(`\xBB skipping ${step.name} (not applicable)`);
119068
- continue;
119069
- }
119070
- log.debug(`\xBB running ${step.name}...`);
119071
- const result = await step.run(options);
119072
- results.push(result);
119073
- if (result.dependenciesInstalled) {
119074
- log.debug(`\xBB ${step.name}: dependencies installed`);
119075
- } else if (result.issues.length > 0) {
119076
- log.warning(`\xBB ${step.name}: ${result.issues[0]}`);
119167
+ const preDirty = await dirtyTrackedPaths();
119168
+ try {
119169
+ for (const step of prepSteps) {
119170
+ const shouldRun = await step.shouldRun();
119171
+ if (!shouldRun) {
119172
+ log.debug(`\xBB skipping ${step.name} (not applicable)`);
119173
+ continue;
119174
+ }
119175
+ log.debug(`\xBB running ${step.name}...`);
119176
+ const result = await step.run(options);
119177
+ results.push(result);
119178
+ if (result.dependenciesInstalled) {
119179
+ log.debug(`\xBB ${step.name}: dependencies installed`);
119180
+ } else if (result.issues.length > 0) {
119181
+ log.warning(`\xBB ${step.name}: ${result.issues[0]}`);
119182
+ }
119077
119183
  }
119184
+ } finally {
119185
+ await restorePrepDirtiedFiles(preDirty);
119078
119186
  }
119079
119187
  const totalDurationMs = performance7.now() - startTime;
119080
119188
  log.debug(`\xBB prep phase completed (${Math.round(totalDurationMs)}ms)`);
@@ -151257,7 +151365,7 @@ function closeBrowserDaemon(toolState) {
151257
151365
  // mcp/checkout.ts
151258
151366
  import { createHash as createHash2 } from "node:crypto";
151259
151367
  import { statSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync8 } from "node:fs";
151260
- import { join as join13 } from "node:path";
151368
+ import { join as join15 } from "node:path";
151261
151369
 
151262
151370
  // utils/diffCoverage.ts
151263
151371
  import { isAbsolute, normalize as normalize2, resolve } from "node:path";
@@ -151527,6 +151635,7 @@ function readNumber(params) {
151527
151635
  import { execSync } from "node:child_process";
151528
151636
  import { createHash } from "node:crypto";
151529
151637
  import { readFileSync as readFileSync3, realpathSync, unlinkSync } from "node:fs";
151638
+ import { join as join11 } from "node:path";
151530
151639
 
151531
151640
  // utils/shell.ts
151532
151641
  import { spawnSync as spawnSync4 } from "node:child_process";
@@ -151596,6 +151705,18 @@ function verifyGitBinary() {
151596
151705
  }
151597
151706
  return gitBinary.path;
151598
151707
  }
151708
+ var hooksDirCache = /* @__PURE__ */ new Map();
151709
+ function resolveHooksDir(cwd, gitPath) {
151710
+ const cached4 = hooksDirCache.get(cwd);
151711
+ if (cached4) return cached4;
151712
+ const commonDir = $2(gitPath, ["rev-parse", "--path-format=absolute", "--git-common-dir"], {
151713
+ cwd,
151714
+ log: false
151715
+ }).trim();
151716
+ const hooksDir = join11(commonDir, "hooks");
151717
+ hooksDirCache.set(cwd, hooksDir);
151718
+ return hooksDir;
151719
+ }
151599
151720
  var authServer;
151600
151721
  function setGitAuthServer(server) {
151601
151722
  authServer = server;
@@ -151617,6 +151738,8 @@ async function $git(subcommand, args2, options) {
151617
151738
  "protocol.file.allow=never",
151618
151739
  "-c",
151619
151740
  "core.sshCommand=ssh",
151741
+ "-c",
151742
+ `core.hooksPath=${resolveHooksDir(cwd, gitPath)}`,
151620
151743
  subcommand,
151621
151744
  ...args2
151622
151745
  ];
@@ -151886,7 +152009,274 @@ function postProcessRangeDiff(raw2, contextLines = 3) {
151886
152009
  // mcp/git.ts
151887
152010
  import { randomUUID as randomUUID3 } from "node:crypto";
151888
152011
  import { writeFileSync as writeFileSync7 } from "node:fs";
151889
- import { join as join11 } from "node:path";
152012
+ import { join as join13 } from "node:path";
152013
+
152014
+ // utils/apiCommit.ts
152015
+ import { execFileSync as execFileSync7 } from "node:child_process";
152016
+ import { lstat, readlink } from "node:fs/promises";
152017
+ import { join as join12 } from "node:path";
152018
+ var GITHUB_API = "https://api.github.com";
152019
+ var MAX_BLOB_BYTES = 30 * 1024 * 1024;
152020
+ var BLOB_UPLOAD_CONCURRENCY = 8;
152021
+ function getRepoRoot() {
152022
+ return $2("git", ["rev-parse", "--show-toplevel"], { log: false }).trim();
152023
+ }
152024
+ async function gh(params) {
152025
+ const response = await fetch(`${GITHUB_API}${params.path}`, {
152026
+ method: params.method,
152027
+ headers: {
152028
+ Accept: "application/vnd.github+json",
152029
+ Authorization: `Bearer ${params.token}`,
152030
+ "X-GitHub-Api-Version": "2022-11-28",
152031
+ "Content-Type": "application/json"
152032
+ },
152033
+ body: params.body === void 0 ? null : JSON.stringify(params.body)
152034
+ });
152035
+ const text = await response.text();
152036
+ let json4;
152037
+ try {
152038
+ json4 = text ? JSON.parse(text) : null;
152039
+ } catch {
152040
+ json4 = text.slice(0, 500);
152041
+ }
152042
+ return { status: response.status, json: json4 };
152043
+ }
152044
+ function ghError(method, path4, result) {
152045
+ return new Error(`${method} ${path4} failed (${result.status}): ${JSON.stringify(result.json)}`);
152046
+ }
152047
+ function isRetryable(status) {
152048
+ return status === 403 || status === 429 || status >= 500;
152049
+ }
152050
+ function detectWorkingTreeChanges() {
152051
+ const byPath = /* @__PURE__ */ new Map();
152052
+ const diff = $2("git", ["diff", "--name-status", "--no-renames", "-z", "HEAD"], { log: false });
152053
+ const tokens = diff.split("\0").filter((t2) => t2.length > 0);
152054
+ for (let i = 0; i + 1 < tokens.length; i += 2) {
152055
+ const status = tokens[i];
152056
+ const path4 = tokens[i + 1];
152057
+ if (status === void 0 || path4 === void 0) break;
152058
+ if (status === "U") {
152059
+ throw new Error(
152060
+ `'${path4}' has unresolved merge conflicts. resolve the conflicts, stage the result with git add, then retry.`
152061
+ );
152062
+ }
152063
+ byPath.set(path4, { path: path4, deleted: status === "D" });
152064
+ }
152065
+ const porcelain = $2("git", ["status", "--porcelain=v1", "-z", "-uall", "--no-renames"], {
152066
+ log: false
152067
+ });
152068
+ for (const entry of porcelain.split("\0")) {
152069
+ if (entry.startsWith("?? ") && !byPath.has(entry.slice(3))) {
152070
+ const path4 = entry.slice(3);
152071
+ byPath.set(path4, { path: path4, deleted: false });
152072
+ }
152073
+ }
152074
+ return [...byPath.values()];
152075
+ }
152076
+ async function assertApiCommittable(files) {
152077
+ const present = files.filter((f) => !f.deleted).map((f) => f.path);
152078
+ if (present.length === 0) return;
152079
+ const root = getRepoRoot();
152080
+ const attrs = $2(
152081
+ "git",
152082
+ ["check-attr", "filter", "-z", "--", ...present.map((p2) => join12(root, p2))],
152083
+ { log: false }
152084
+ );
152085
+ const parts = attrs.split("\0");
152086
+ for (let i = 0; i + 2 < parts.length; i += 3) {
152087
+ if (parts[i + 2] === "lfs") {
152088
+ throw new Error(
152089
+ `'${parts[i]}' is tracked by git-lfs, which signed commits can't upload. remove it from the change set or ask the user to disable signed commits for this repo.`
152090
+ );
152091
+ }
152092
+ }
152093
+ for (const path4 of present) {
152094
+ const stat = await lstat(join12(root, path4));
152095
+ if (stat.isDirectory()) {
152096
+ throw new Error(
152097
+ `'${path4}' is a directory (nested repository or submodule?) \u2014 signed commits only support files and symlinks.`
152098
+ );
152099
+ }
152100
+ }
152101
+ }
152102
+ async function createBlobEntry(params) {
152103
+ const absPath = join12(params.repoRoot, params.path);
152104
+ const stat = await lstat(absPath);
152105
+ if (stat.size > MAX_BLOB_BYTES) {
152106
+ throw new Error(
152107
+ `'${params.path}' is ${Math.round(stat.size / 1024 / 1024)}MB \u2014 too large for signed commits (the GitHub blob API rejects large uploads). use git-lfs for large assets or ask the user to disable signed commits for this repo.`
152108
+ );
152109
+ }
152110
+ let mode;
152111
+ let content;
152112
+ if (stat.isSymbolicLink()) {
152113
+ mode = "120000";
152114
+ content = await readlink(absPath, { encoding: "buffer" });
152115
+ } else {
152116
+ mode = stat.mode & 64 ? "100755" : "100644";
152117
+ const cleanSha = $2("git", ["hash-object", "-w", "--", absPath], { log: false }).trim();
152118
+ content = execFileSync7("git", ["cat-file", "blob", cleanSha], {
152119
+ cwd: params.repoRoot,
152120
+ maxBuffer: 2 * MAX_BLOB_BYTES
152121
+ });
152122
+ }
152123
+ const path4 = `${params.repoPath}/git/blobs`;
152124
+ let result;
152125
+ for (const delayMs of [0, 1e3, 3e3]) {
152126
+ if (delayMs > 0) await new Promise((r) => setTimeout(r, delayMs));
152127
+ result = await gh({
152128
+ token: params.token,
152129
+ method: "POST",
152130
+ path: path4,
152131
+ body: { content: content.toString("base64"), encoding: "base64" }
152132
+ });
152133
+ if (result.status === 201) {
152134
+ const blob = result.json;
152135
+ return { path: params.path, mode, type: "blob", sha: blob.sha };
152136
+ }
152137
+ if (!isRetryable(result.status)) break;
152138
+ log.info(`blob upload for ${params.path} got ${result.status}, retrying`);
152139
+ }
152140
+ if (!result) throw new Error(`POST ${path4} failed`);
152141
+ throw ghError("POST", path4, result);
152142
+ }
152143
+ async function updateRefWithRetry(params) {
152144
+ const path4 = `${params.repoPath}/git/refs/heads/${encodeBranchPath(params.remoteBranch)}`;
152145
+ let lastResult;
152146
+ for (const delayMs of [0, 1e3, 3e3]) {
152147
+ if (delayMs > 0) await new Promise((r) => setTimeout(r, delayMs));
152148
+ const result = await gh({
152149
+ token: params.token,
152150
+ method: "PATCH",
152151
+ path: path4,
152152
+ body: { sha: params.sha, force: false }
152153
+ });
152154
+ if (result.status === 200) return;
152155
+ if (result.status === 422) {
152156
+ const detail = JSON.stringify(result.json);
152157
+ if (/fast.forward/i.test(detail)) {
152158
+ throw new Error(
152159
+ `the remote branch '${params.remoteBranch}' moved while committing (concurrent push). fetch it with git_fetch, integrate with git merge --no-commit origin/${params.remoteBranch}, resolve any conflicts, git add the results, then retry commit_changes.`
152160
+ );
152161
+ }
152162
+ throw ghError("PATCH", path4, result);
152163
+ }
152164
+ lastResult = result;
152165
+ if (!isRetryable(result.status)) break;
152166
+ log.info(`ref update got ${result.status}, retrying`);
152167
+ }
152168
+ if (lastResult) throw ghError("PATCH", path4, lastResult);
152169
+ throw new Error(`PATCH ${path4} failed`);
152170
+ }
152171
+ function encodeBranchPath(branch) {
152172
+ return branch.split("/").map(encodeURIComponent).join("/");
152173
+ }
152174
+ function validateRemoteBranch(branch) {
152175
+ const bad = branch.startsWith("-") || branch.startsWith("/") || branch.endsWith("/") || branch.includes("..") || branch.includes("@{") || /[\s~^:?*[\]\\]/.test(branch);
152176
+ if (bad) throw new Error(`invalid remote branch name '${branch}'`);
152177
+ }
152178
+ async function createSignedCommit(params) {
152179
+ validateRemoteBranch(params.remoteBranch);
152180
+ const repoPath = `/repos/${params.owner}/${params.repo}`;
152181
+ const branchPath = `${repoPath}/git/ref/heads/${encodeBranchPath(params.remoteBranch)}`;
152182
+ const refResult = await gh({ token: params.token, method: "GET", path: branchPath });
152183
+ const branchExists = refResult.status === 200;
152184
+ if (branchExists) {
152185
+ const ref = refResult.json;
152186
+ const remoteTip = ref.object.sha;
152187
+ if (!params.parents.includes(remoteTip)) {
152188
+ throw new Error(
152189
+ isAncestorOfHead(remoteTip) ? `your local branch has commits that were never pushed \u2014 signed-commits mode can't push local commits. run git reset --mixed ${remoteTip} (keeps every change in the working tree), then retry commit_changes.` : `the remote branch '${params.remoteBranch}' has commits you don't have locally (tip ${remoteTip.slice(0, 7)}). fetch it with git_fetch, integrate with git merge --no-commit origin/${params.remoteBranch}, resolve any conflicts, git add the results, then retry commit_changes.`
152190
+ );
152191
+ }
152192
+ } else if (refResult.status !== 404) {
152193
+ throw ghError("GET", branchPath, refResult);
152194
+ }
152195
+ const baseParent = params.parents[0];
152196
+ if (!baseParent) throw new Error("createSignedCommit requires at least one parent");
152197
+ const baseTree = $2("git", ["rev-parse", `${baseParent}^{tree}`], { log: false }).trim();
152198
+ let treeSha = baseTree;
152199
+ if (params.files.length > 0) {
152200
+ const repoRoot = getRepoRoot();
152201
+ const additions = params.files.filter((f) => !f.deleted);
152202
+ const blobEntries = [];
152203
+ for (let i = 0; i < additions.length; i += BLOB_UPLOAD_CONCURRENCY) {
152204
+ const chunk = additions.slice(i, i + BLOB_UPLOAD_CONCURRENCY);
152205
+ blobEntries.push(
152206
+ ...await Promise.all(
152207
+ chunk.map(
152208
+ (f) => createBlobEntry({ token: params.token, repoPath, repoRoot, path: f.path })
152209
+ )
152210
+ )
152211
+ );
152212
+ }
152213
+ const deletionEntries = params.files.filter((f) => f.deleted).map((f) => ({ path: f.path, mode: "100644", type: "blob", sha: null }));
152214
+ const treeResult = await gh({
152215
+ token: params.token,
152216
+ method: "POST",
152217
+ path: `${repoPath}/git/trees`,
152218
+ body: { base_tree: baseTree, tree: [...blobEntries, ...deletionEntries] }
152219
+ });
152220
+ if (treeResult.status !== 201) {
152221
+ throw wrapUnknownBaseError("POST", `${repoPath}/git/trees`, treeResult, params.remoteBranch);
152222
+ }
152223
+ treeSha = treeResult.json.sha;
152224
+ }
152225
+ const commitResult = await gh({
152226
+ token: params.token,
152227
+ method: "POST",
152228
+ path: `${repoPath}/git/commits`,
152229
+ body: { message: params.message, tree: treeSha, parents: params.parents }
152230
+ });
152231
+ if (commitResult.status !== 201) {
152232
+ throw wrapUnknownBaseError(
152233
+ "POST",
152234
+ `${repoPath}/git/commits`,
152235
+ commitResult,
152236
+ params.remoteBranch
152237
+ );
152238
+ }
152239
+ const commit = commitResult.json;
152240
+ if (branchExists) {
152241
+ await updateRefWithRetry({
152242
+ token: params.token,
152243
+ repoPath,
152244
+ remoteBranch: params.remoteBranch,
152245
+ sha: commit.sha
152246
+ });
152247
+ } else {
152248
+ const createResult = await gh({
152249
+ token: params.token,
152250
+ method: "POST",
152251
+ path: `${repoPath}/git/refs`,
152252
+ body: { ref: `refs/heads/${params.remoteBranch}`, sha: commit.sha }
152253
+ });
152254
+ if (createResult.status !== 201) {
152255
+ throw ghError("POST", `${repoPath}/git/refs`, createResult);
152256
+ }
152257
+ }
152258
+ return { sha: commit.sha, createdBranch: !branchExists };
152259
+ }
152260
+ function isAncestorOfHead(sha) {
152261
+ try {
152262
+ $2("git", ["merge-base", "--is-ancestor", sha, "HEAD"], { log: false });
152263
+ return true;
152264
+ } catch {
152265
+ return false;
152266
+ }
152267
+ }
152268
+ function wrapUnknownBaseError(method, path4, result, remoteBranch) {
152269
+ if (result.status === 404 || result.status === 422) {
152270
+ return new Error(
152271
+ `${ghError(method, path4, result).message}
152272
+
152273
+ this usually means your local branch has commits that were never pushed \u2014 signed-commits mode can't push local commits. run git reset --mixed origin/${remoteBranch} (or the commit you branched from; this keeps every change in the working tree), then retry commit_changes.`
152274
+ );
152275
+ }
152276
+ return ghError(method, path4, result);
152277
+ }
152278
+
152279
+ // mcp/git.ts
151890
152280
  function getPushDestination(branch, storedDest) {
151891
152281
  if (storedDest && storedDest.localBranch === branch) {
151892
152282
  log.debug(`using stored push destination: ${storedDest.remoteName}/${storedDest.remoteBranch}`);
@@ -151944,6 +152334,10 @@ function validateTagName(tag) {
151944
152334
  );
151945
152335
  }
151946
152336
  }
152337
+ function pushesToBaseRepo(ctx) {
152338
+ const baseUrl = `https://github.com/${ctx.repo.owner}/${ctx.repo.name}.git`;
152339
+ return normalizeUrl(ctx.toolState.pushUrl ?? "") === normalizeUrl(baseUrl);
152340
+ }
151947
152341
  function validatePushDestination(ctx, branch) {
151948
152342
  const pushUrl = ctx.toolState.pushUrl;
151949
152343
  if (!pushUrl) throw new Error("pushUrl not set - setupGit must run before push_branch");
@@ -151962,6 +152356,47 @@ var PushBranch = type({
151962
152356
  branchName: type.string.describe("The branch name to push (defaults to current branch)").optional(),
151963
152357
  force: type.boolean.describe("Force push (use with caution)").default(false)
151964
152358
  });
152359
+ function assertPushTarget(ctx, branch, pushDest) {
152360
+ const prBranchMatch = branch.match(/^pr-(\d+)$/);
152361
+ if (prBranchMatch && pushDest.remoteBranch !== branch) {
152362
+ const prNumber = Number(prBranchMatch[1]);
152363
+ const event = ctx.payload.event;
152364
+ const runScoped = event.is_pr === true && event.issue_number === prNumber;
152365
+ if (!runScoped) {
152366
+ throw new Error(
152367
+ `push blocked: local branch '${branch}' would push to '${pushDest.remoteName}/${pushDest.remoteBranch}', but this run is not scoped to PR #${prNumber}. the 'pr-${prNumber}' branch was created by a prior checkout_pr call (likely from a subagent \u2014 subagents share the working tree and toolState with the orchestrator). you have probably landed your commit on the wrong branch. switch to your own feature branch first (e.g. 'git checkout <feature-branch>') and then push. if the push to PR #${prNumber} is intentional, this run needs to be triggered against that PR.`
152368
+ );
152369
+ }
152370
+ }
152371
+ const defaultBranch = ctx.repo.data.default_branch || "main";
152372
+ if (ctx.payload.push === "restricted" && pushDest.remoteBranch === defaultBranch) {
152373
+ throw new Error(
152374
+ `Push blocked: cannot push directly to default branch '${pushDest.remoteBranch}'. Create a feature branch and open a PR instead.`
152375
+ );
152376
+ }
152377
+ }
152378
+ async function runPrepushHook(ctx, retryTool) {
152379
+ if (ctx.toolState.prepushFailureCount > 0) {
152380
+ log.info(`\xBB skipping prepush hook (failed earlier this run)`);
152381
+ return true;
152382
+ }
152383
+ if (!ctx.prepushScript) return false;
152384
+ const prepushHook = await executeLifecycleHook({
152385
+ event: "prepush",
152386
+ script: ctx.prepushScript
152387
+ });
152388
+ if (prepushHook.failure) {
152389
+ ctx.toolState.prepushFailureCount += 1;
152390
+ throw new Error(
152391
+ buildPrepushFailureMessage({
152392
+ failure: prepushHook.failure,
152393
+ shell: ctx.payload.shell,
152394
+ retryTool
152395
+ })
152396
+ );
152397
+ }
152398
+ return false;
152399
+ }
151965
152400
  var CONCURRENT_PUSH_PATTERNS = ["fetch first", "non-fast-forward", "cannot lock ref"];
151966
152401
  var TRANSIENT_PATTERNS = [
151967
152402
  /RPC failed/i,
@@ -152021,7 +152456,6 @@ async function pushWithRetry(args2, token) {
152021
152456
  throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
152022
152457
  }
152023
152458
  function PushBranchTool(ctx) {
152024
- const defaultBranch = ctx.repo.data.default_branch || "main";
152025
152459
  const pushPermission = ctx.payload.push;
152026
152460
  return tool({
152027
152461
  name: "push_branch",
@@ -152033,6 +152467,11 @@ function PushBranchTool(ctx) {
152033
152467
  }
152034
152468
  const branch = branchName || $2("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false });
152035
152469
  rejectSpecialRef(branch, "branch");
152470
+ if (ctx.signedCommits && pushesToBaseRepo(ctx)) {
152471
+ throw new Error(
152472
+ "push_branch is not used in signed-commits mode \u2014 commits land on the remote via the commit_changes tool. call commit_changes to commit your working-tree changes as a GitHub-signed commit. if you already called commit_changes, your work is already on the remote \u2014 there is nothing left to push."
152473
+ );
152474
+ }
152036
152475
  const status = $2("git", ["status", "--porcelain"], { log: false });
152037
152476
  if (status) {
152038
152477
  throw new Error(
@@ -152043,36 +152482,11 @@ ${status}` + (ctx.toolState.prepushFailureCount > 0 ? "\n\nnote: the prepush hoo
152043
152482
  );
152044
152483
  }
152045
152484
  const pushDest = validatePushDestination(ctx, branch);
152046
- const prBranchMatch = branch.match(/^pr-(\d+)$/);
152047
- if (prBranchMatch && pushDest.remoteBranch !== branch) {
152048
- const prNumber = Number(prBranchMatch[1]);
152049
- const event = ctx.payload.event;
152050
- const runScoped = event.is_pr === true && event.issue_number === prNumber;
152051
- if (!runScoped) {
152052
- throw new Error(
152053
- `push blocked: local branch '${branch}' would push to '${pushDest.remoteName}/${pushDest.remoteBranch}', but this run is not scoped to PR #${prNumber}. the 'pr-${prNumber}' branch was created by a prior checkout_pr call (likely from a subagent \u2014 subagents share the working tree and toolState with the orchestrator). you have probably landed your commit on the wrong branch. switch to your own feature branch first (e.g. 'git checkout <feature-branch>') and then push. if the push to PR #${prNumber} is intentional, this run needs to be triggered against that PR.`
152054
- );
152055
- }
152056
- }
152057
- if (pushPermission === "restricted" && pushDest.remoteBranch === defaultBranch) {
152058
- throw new Error(
152059
- `Push blocked: cannot push directly to default branch '${pushDest.remoteBranch}'. Create a feature branch and open a PR instead.`
152060
- );
152061
- }
152485
+ assertPushTarget(ctx, branch, pushDest);
152062
152486
  const refspec = branch === pushDest.remoteBranch ? branch : `${branch}:${pushDest.remoteBranch}`;
152063
152487
  const pushArgs = force ? ["--force", "-u", pushDest.remoteName, refspec] : ["-u", pushDest.remoteName, refspec];
152064
- const prepushSkipped = ctx.toolState.prepushFailureCount > 0;
152065
- if (prepushSkipped) {
152066
- log.info(`\xBB skipping prepush hook (failed earlier this run)`);
152067
- } else if (ctx.prepushScript) {
152068
- const prepushHook = await executeLifecycleHook({
152069
- event: "prepush",
152070
- script: ctx.prepushScript
152071
- });
152072
- if (prepushHook.failure) {
152073
- ctx.toolState.prepushFailureCount += 1;
152074
- throw new Error(buildPrepushFailureMessage(prepushHook.failure, ctx.payload.shell));
152075
- }
152488
+ const prepushSkipped = await runPrepushHook(ctx, "push_branch");
152489
+ if (!prepushSkipped && ctx.prepushScript) {
152076
152490
  const postHookStatus = $2("git", ["status", "--porcelain"], { log: false });
152077
152491
  if (postHookStatus) {
152078
152492
  throw new Error(
@@ -152123,15 +152537,138 @@ ${integrateStep}
152123
152537
  })
152124
152538
  });
152125
152539
  }
152126
- function buildPrepushFailureMessage(failure, shell) {
152540
+ function buildPrepushFailureMessage(params) {
152541
+ const failure = params.failure;
152127
152542
  const header = failure.kind === "exit" ? `prepush hook failed with exit code ${failure.exitCode}.
152128
152543
 
152129
152544
  script output:
152130
152545
  ${failure.output || "(empty)"}` : failure.kind === "timeout" ? `prepush hook timed out \u2014 the script is hung or doing too much work.` : `prepush hook failed to spawn: ${failure.spawnError}.`;
152131
- const ifRealBug = shell === "disabled" ? `fix it before pushing again \u2014 shell access is disabled in this run, so you can't re-run the hook command yourself.` : `run the hook command yourself via the shell tool to iterate (push_branch will NOT re-run it).`;
152546
+ const ifRealBug = params.shell === "disabled" ? `fix it before pushing again \u2014 shell access is disabled in this run, so you can't re-run the hook command yourself.` : `run the hook command yourself via the shell tool to iterate (${params.retryTool} will NOT re-run it).`;
152132
152547
  return `${header}
152133
152548
 
152134
- this repo's prepush hook is best-effort: the next push_branch call will SKIP the hook and proceed. if the failure is unrelated to your changes (pre-existing breakage, flaky check), just call push_branch again. if it could be a real bug in your code, ${ifRealBug}`;
152549
+ this repo's prepush hook is best-effort: the next ${params.retryTool} call will SKIP the hook and proceed. if the failure is unrelated to your changes (pre-existing breakage, flaky check), just call ${params.retryTool} again. if it could be a real bug in your code, ${ifRealBug}`;
152550
+ }
152551
+ function buildNothingToCommitMessage(pushDest) {
152552
+ const base = "nothing to commit \u2014 the working tree matches HEAD.";
152553
+ try {
152554
+ const remoteTip = $2(
152555
+ "git",
152556
+ ["rev-parse", `refs/remotes/${pushDest.remoteName}/${pushDest.remoteBranch}`],
152557
+ { log: false }
152558
+ ).trim();
152559
+ const head = $2("git", ["rev-parse", "HEAD"], { log: false }).trim();
152560
+ if (remoteTip === head) {
152561
+ return `${base} your work is already on ${pushDest.remoteName}/${pushDest.remoteBranch} \u2014 there is no push step in signed-commits mode.`;
152562
+ }
152563
+ $2("git", ["merge-base", "--is-ancestor", remoteTip, "HEAD"], { log: false });
152564
+ return `${base} but your local branch has commits that were never pushed \u2014 signed-commits mode can't push local commits. run git reset --mixed ${remoteTip} (keeps every change in the working tree), then retry commit_changes.`;
152565
+ } catch {
152566
+ return base;
152567
+ }
152568
+ }
152569
+ var CommitChanges = type({
152570
+ message: type.string.describe("Commit message (first line = subject)"),
152571
+ files: type.string.array().describe("Optional subset of changed paths to commit. Defaults to every working-tree change.").optional()
152572
+ });
152573
+ function CommitChangesTool(ctx) {
152574
+ const pushPermission = ctx.payload.push;
152575
+ return tool({
152576
+ name: "commit_changes",
152577
+ description: "Commit working-tree changes directly to the remote branch as a GitHub-signed (Verified) commit \u2014 this repository has signed commits enabled, so use this INSTEAD of git commit + push_branch. Edit files locally, then call this tool: it detects every working-tree change (new, modified, deleted files), or commits a subset via `files`. The commit lands on the remote immediately \u2014 there is no separate push step. The remote branch is created automatically on the first commit to a new local branch. A merge in progress (git merge --no-commit) is concluded as a signed merge commit \u2014 resolve conflicts and git add first. Runs the repository prepush hook (if configured) before committing \u2014 best-effort, same skip-on-failure behavior as push_branch.",
152578
+ parameters: CommitChanges,
152579
+ timeoutMs: 6e5,
152580
+ execute: execute(async (params) => {
152581
+ if (pushPermission === "disabled") {
152582
+ throw new Error("Push is disabled. This repository is configured for read-only access.");
152583
+ }
152584
+ const branch = $2("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false }).trim();
152585
+ if (branch === "HEAD") {
152586
+ throw new Error(
152587
+ "HEAD is detached \u2014 create or check out a branch before committing (e.g. git checkout -b pullfrog/<description>)."
152588
+ );
152589
+ }
152590
+ rejectSpecialRef(branch, "branch");
152591
+ const pushDest = validatePushDestination(ctx, branch);
152592
+ if (!pushesToBaseRepo(ctx)) {
152593
+ throw new Error(
152594
+ `'${branch}' pushes to the fork '${pushDest.url}', where the app can't create signed commits. commit locally via the git tool and use push_branch instead (those commits will be unsigned).`
152595
+ );
152596
+ }
152597
+ assertPushTarget(ctx, branch, pushDest);
152598
+ const prepushSkipped = await runPrepushHook(ctx, "commit_changes");
152599
+ const head = $2("git", ["rev-parse", "HEAD"], { log: false }).trim();
152600
+ let mergeHead = "";
152601
+ try {
152602
+ mergeHead = $2("git", ["rev-parse", "-q", "--verify", "MERGE_HEAD"], { log: false }).trim();
152603
+ } catch {
152604
+ }
152605
+ let changes = detectWorkingTreeChanges();
152606
+ if (params.files) {
152607
+ if (mergeHead) {
152608
+ throw new Error(
152609
+ "can't commit a subset of files while a merge is in progress \u2014 the merge commit must include every merged change. omit `files`."
152610
+ );
152611
+ }
152612
+ const requested = new Set(params.files);
152613
+ const known = new Set(changes.map((c2) => c2.path));
152614
+ const unknown4 = [...requested].filter((p2) => !known.has(p2));
152615
+ if (unknown4.length > 0) {
152616
+ throw new Error(
152617
+ `no detected change at: ${unknown4.join(", ")} \u2014 run git status to list changed paths.`
152618
+ );
152619
+ }
152620
+ changes = changes.filter((c2) => requested.has(c2.path));
152621
+ }
152622
+ if (changes.length === 0 && !mergeHead) {
152623
+ throw new Error(buildNothingToCommitMessage(pushDest));
152624
+ }
152625
+ await assertApiCommittable(changes);
152626
+ const parents = mergeHead ? [head, mergeHead] : [head];
152627
+ const result = await createSignedCommit({
152628
+ token: ctx.gitToken,
152629
+ owner: ctx.repo.owner,
152630
+ repo: ctx.repo.name,
152631
+ remoteBranch: pushDest.remoteBranch,
152632
+ message: params.message,
152633
+ parents,
152634
+ files: changes
152635
+ });
152636
+ await $git(
152637
+ "fetch",
152638
+ [
152639
+ "--no-tags",
152640
+ "origin",
152641
+ `+refs/heads/${pushDest.remoteBranch}:refs/remotes/origin/${pushDest.remoteBranch}`
152642
+ ],
152643
+ { token: ctx.gitToken }
152644
+ );
152645
+ $2("git", ["update-ref", `refs/heads/${branch}`, result.sha], { log: false });
152646
+ if (mergeHead) {
152647
+ $2("git", ["merge", "--quit"], { log: false });
152648
+ }
152649
+ $2("git", ["reset", "-q"], { log: false });
152650
+ if (result.createdBranch) {
152651
+ $2("git", ["config", `branch.${branch}.remote`, "origin"], { log: false });
152652
+ $2("git", ["config", `branch.${branch}.merge`, `refs/heads/${pushDest.remoteBranch}`], {
152653
+ log: false
152654
+ });
152655
+ }
152656
+ log.info(
152657
+ `\xBB created signed commit ${result.sha.slice(0, 7)} (${changes.length} file(s)) on ${pushDest.remoteName}/${pushDest.remoteBranch}`
152658
+ );
152659
+ return {
152660
+ success: true,
152661
+ sha: result.sha,
152662
+ branch,
152663
+ remoteBranch: pushDest.remoteBranch,
152664
+ files: changes.map((c2) => c2.deleted ? `D ${c2.path}` : c2.path),
152665
+ createdBranch: result.createdBranch,
152666
+ verified: true,
152667
+ prepushSkipped,
152668
+ message: `created signed commit ${result.sha.slice(0, 7)} on ${pushDest.remoteName}/${pushDest.remoteBranch}${result.createdBranch ? " (remote branch created)" : ""}`
152669
+ };
152670
+ })
152671
+ });
152135
152672
  }
152136
152673
  var AUTH_REQUIRED_REDIRECT = {
152137
152674
  push: "use the push_branch tool instead \u2014 it handles authentication and permission checks.",
@@ -152204,7 +152741,7 @@ function countAhead(head, base) {
152204
152741
  function spillGitOutput(params) {
152205
152742
  const tempDir = process.env.PULLFROG_TEMP_DIR;
152206
152743
  if (!tempDir) throw new Error("PULLFROG_TEMP_DIR not set");
152207
- const outputPath = join11(tempDir, `git-${params.command}-${randomUUID3().slice(0, 8)}.txt`);
152744
+ const outputPath = join13(tempDir, `git-${params.command}-${randomUUID3().slice(0, 8)}.txt`);
152208
152745
  writeFileSync7(outputPath, params.output);
152209
152746
  const previewByLines = params.output.split("\n").slice(0, OVERFLOW_PREVIEW_LINES).join("\n");
152210
152747
  const preview = previewByLines.length <= OVERFLOW_PREVIEW_MAX_CHARS ? previewByLines : `${previewByLines.slice(0, OVERFLOW_PREVIEW_MAX_CHARS)}\u2026`;
@@ -152238,8 +152775,30 @@ function GitTool(ctx) {
152238
152775
  }
152239
152776
  const redirect = AUTH_REQUIRED_REDIRECT[command];
152240
152777
  if (redirect) {
152778
+ if (command === "push" && ctx.signedCommits) {
152779
+ throw new Error(
152780
+ "git push is not available through this tool \u2014 in signed-commits mode use commit_changes instead: it commits your working-tree changes directly to the remote as a GitHub-signed commit (push_branch only applies to fork PRs)."
152781
+ );
152782
+ }
152241
152783
  throw new Error(`git ${command} is not available through this tool \u2014 ${redirect}`);
152242
152784
  }
152785
+ if (ctx.signedCommits && (command === "commit" || command === "merge")) {
152786
+ if (pushesToBaseRepo(ctx)) {
152787
+ if (command === "commit") {
152788
+ throw new Error(
152789
+ "git commit is blocked in signed-commits mode \u2014 use the commit_changes tool instead. it commits your working-tree changes directly to the remote as a GitHub-signed (Verified) commit. if you are concluding a merge, stage the resolutions with git add and call commit_changes \u2014 no local commit is needed."
152790
+ );
152791
+ }
152792
+ const noLocalCommit = args2.some(
152793
+ (a) => a === "--no-commit" || a === "--abort" || a === "--quit"
152794
+ );
152795
+ if (!noLocalCommit) {
152796
+ throw new Error(
152797
+ "bare git merge would create a local commit, which can't be pushed in signed-commits mode. use git merge --no-commit <ref>, resolve any conflicts, git add the results, then call commit_changes \u2014 it concludes the merge as a signed merge commit."
152798
+ );
152799
+ }
152800
+ }
152801
+ }
152243
152802
  if (ctx.payload.shell === "disabled") {
152244
152803
  const blocked = NOSHELL_BLOCKED_SUBCOMMANDS[command];
152245
152804
  if (blocked) {
@@ -152872,7 +153431,6 @@ async function createAndSubmitWithFooter(ctx, params, opts) {
152872
153431
  workflowRun: ctx.runId ? { owner: ctx.repo.owner, repo: ctx.repo.name, runId: ctx.runId, jobId: ctx.jobId } : void 0,
152873
153432
  customParts,
152874
153433
  model: ctx.toolState.model,
152875
- fallbackFrom: ctx.toolState.modelFallback?.from,
152876
153434
  oss: ctx.oss
152877
153435
  });
152878
153436
  return await ctx.octokit.rest.pulls.submitReview({
@@ -152905,12 +153463,12 @@ async function reportReviewNodeId(ctx, params) {
152905
153463
  }
152906
153464
 
152907
153465
  // utils/setup.ts
152908
- import { execFileSync as execFileSync7, execSync as execSync2 } from "node:child_process";
153466
+ import { execFileSync as execFileSync8, execSync as execSync2 } from "node:child_process";
152909
153467
  import { mkdtempSync as mkdtempSync2, readdirSync, realpathSync as realpathSync2, unlinkSync as unlinkSync2 } from "node:fs";
152910
153468
  import { tmpdir as tmpdir3 } from "node:os";
152911
- import { join as join12 } from "node:path";
153469
+ import { join as join14 } from "node:path";
152912
153470
  function createTempDirectory() {
152913
- const sharedTempDir = mkdtempSync2(join12(tmpdir3(), "pullfrog-"));
153471
+ const sharedTempDir = mkdtempSync2(join14(tmpdir3(), "pullfrog-"));
152914
153472
  process.env.PULLFROG_TEMP_DIR = sharedTempDir;
152915
153473
  log.info(`\xBB created temp dir at ${sharedTempDir}`);
152916
153474
  return sharedTempDir;
@@ -152955,13 +153513,13 @@ function wipeRunnerLeakSurface() {
152955
153513
  return [];
152956
153514
  }
152957
153515
  };
152958
- const fileCommandsDir = join12(runnerTemp, "_runner_file_commands");
153516
+ const fileCommandsDir = join14(runnerTemp, "_runner_file_commands");
152959
153517
  for (const entry of listDir(fileCommandsDir)) {
152960
- tryUnlink(join12(fileCommandsDir, entry));
153518
+ tryUnlink(join14(fileCommandsDir, entry));
152961
153519
  }
152962
153520
  for (const entry of listDir(runnerTemp)) {
152963
153521
  if (entry.endsWith(".sh") || /^git-credentials-.*\.config$/.test(entry)) {
152964
- tryUnlink(join12(runnerTemp, entry));
153522
+ tryUnlink(join14(runnerTemp, entry));
152965
153523
  }
152966
153524
  }
152967
153525
  if (wiped.length > 0) {
@@ -152998,7 +153556,7 @@ function removeIncludeIfEntries(repoDir) {
152998
153556
  if (!key || seen.has(key)) continue;
152999
153557
  seen.add(key);
153000
153558
  try {
153001
- execFileSync7("git", ["config", "--local", "--unset-all", key], {
153559
+ execFileSync8("git", ["config", "--local", "--unset-all", key], {
153002
153560
  cwd: repoDir,
153003
153561
  stdio: "pipe",
153004
153562
  env: env2
@@ -153470,7 +154028,7 @@ function CheckoutPrTool(ctx) {
153470
154028
  headSha: ctx.toolState.checkoutSha
153471
154029
  });
153472
154030
  if (incremental) {
153473
- incrementalDiffPath = join13(
154031
+ incrementalDiffPath = join15(
153474
154032
  tempDir,
153475
154033
  `pr-${pull_number}-${beforeShort}-${headShort}-incremental.diff`
153476
154034
  );
@@ -153484,7 +154042,7 @@ function CheckoutPrTool(ctx) {
153484
154042
  const diffPreview = formatResult.content.split("\n").slice(0, 100).join("\n");
153485
154043
  log.debug(`formatted diff preview (first 100 lines):
153486
154044
  ${diffPreview}`);
153487
- const diffPath = join13(tempDir, `pr-${pull_number}-${headShort}.diff`);
154045
+ const diffPath = join15(tempDir, `pr-${pull_number}-${headShort}.diff`);
153488
154046
  writeFileSync8(diffPath, formatResult.content);
153489
154047
  log.debug(`wrote diff to ${diffPath} (${formatResult.content.length} bytes)`);
153490
154048
  ctx.toolState.diffCoverage = createDiffCoverageState({
@@ -153593,7 +154151,7 @@ ${dirty}`
153593
154151
 
153594
154152
  // mcp/checkSuite.ts
153595
154153
  import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync9 } from "node:fs";
153596
- import { join as join14 } from "node:path";
154154
+ import { join as join16 } from "node:path";
153597
154155
  var GetCheckSuiteLogs = type({
153598
154156
  check_suite_id: type.number.describe("the id from check_suite.id")
153599
154157
  });
@@ -153689,7 +154247,7 @@ function GetCheckSuiteLogsTool(ctx) {
153689
154247
  if (!tempDir) {
153690
154248
  throw new Error("PULLFROG_TEMP_DIR not set");
153691
154249
  }
153692
- const logsDir = join14(tempDir, "ci-logs");
154250
+ const logsDir = join16(tempDir, "ci-logs");
153693
154251
  mkdirSync7(logsDir, { recursive: true });
153694
154252
  const jobResults = [];
153695
154253
  for (const run4 of failedRuns) {
@@ -153716,7 +154274,7 @@ function GetCheckSuiteLogsTool(ctx) {
153716
154274
  );
153717
154275
  }
153718
154276
  const logsText = await logsResult.text();
153719
- const logPath = join14(logsDir, `job-${job.id}.log`);
154277
+ const logPath = join16(logsDir, `job-${job.id}.log`);
153720
154278
  writeFileSync9(logPath, logsText);
153721
154279
  const analysis = analyzeLog(logsText, 80);
153722
154280
  const failedSteps = job.steps?.filter((s) => s.conclusion === "failure").map((s) => `Step ${s.number}: ${s.name}`) ?? [];
@@ -153766,7 +154324,7 @@ function GetCheckSuiteLogsTool(ctx) {
153766
154324
 
153767
154325
  // mcp/commitInfo.ts
153768
154326
  import { writeFileSync as writeFileSync10 } from "node:fs";
153769
- import { join as join15 } from "node:path";
154327
+ import { join as join17 } from "node:path";
153770
154328
  var CommitInfo = type({
153771
154329
  sha: type.string.describe("the commit SHA (full or abbreviated) to fetch")
153772
154330
  });
@@ -153790,7 +154348,7 @@ function CommitInfoTool(ctx) {
153790
154348
  "PULLFROG_TEMP_DIR not set - get_commit_info must run in pullfrog action context"
153791
154349
  );
153792
154350
  }
153793
- const diffFile = join15(tempDir, `commit-${sha.slice(0, 7)}.diff`);
154351
+ const diffFile = join17(tempDir, `commit-${sha.slice(0, 7)}.diff`);
153794
154352
  writeFileSync10(diffFile, formatResult.content);
153795
154353
  log.debug(`wrote commit diff to ${diffFile} (${formatResult.content.length} bytes)`);
153796
154354
  return {
@@ -154297,7 +154855,6 @@ function buildPrBodyWithFooter(ctx, body) {
154297
154855
  triggeredBy: true,
154298
154856
  workflowRun: ctx.runId ? { owner: ctx.repo.owner, repo: ctx.repo.name, runId: ctx.runId, jobId: ctx.jobId } : void 0,
154299
154857
  model: ctx.toolState.model,
154300
- fallbackFrom: ctx.toolState.modelFallback?.from,
154301
154858
  oss: ctx.oss
154302
154859
  });
154303
154860
  const bodyWithoutFooter = stripExistingFooter(fixDoubleEscapedString(body));
@@ -154452,7 +155009,7 @@ function PullRequestInfoTool(ctx) {
154452
155009
 
154453
155010
  // mcp/reviewComments.ts
154454
155011
  import { writeFileSync as writeFileSync11 } from "node:fs";
154455
- import { join as join16 } from "node:path";
155012
+ import { join as join18 } from "node:path";
154456
155013
  var REVIEW_THREADS_QUERY = `
154457
155014
  query ($owner: String!, $name: String!, $prNumber: Int!) {
154458
155015
  repository(owner: $owner, name: $name) {
@@ -154868,7 +155425,7 @@ function GetReviewCommentsTool(ctx) {
154868
155425
  throw new Error("PULLFROG_TEMP_DIR not set");
154869
155426
  }
154870
155427
  const filename = `review-${params.review_id}-threads.md`;
154871
- const commentsPath = join16(tempDir, filename);
155428
+ const commentsPath = join18(tempDir, filename);
154872
155429
  writeFileSync11(commentsPath, formatted.content);
154873
155430
  log.debug(`wrote ${threadBlocks.length} threads to ${commentsPath}`);
154874
155431
  return {
@@ -155106,7 +155663,7 @@ import { spawn as spawn4, spawnSync as spawnSync5 } from "node:child_process";
155106
155663
  import { randomUUID as randomUUID4 } from "node:crypto";
155107
155664
  import { closeSync, openSync, writeFileSync as writeFileSync12 } from "node:fs";
155108
155665
  import { userInfo as userInfo2 } from "node:os";
155109
- import { join as join17 } from "node:path";
155666
+ import { join as join19 } from "node:path";
155110
155667
  import { setTimeout as sleep2 } from "node:timers/promises";
155111
155668
  var ShellParams = type({
155112
155669
  command: "string",
@@ -155269,7 +155826,7 @@ function getTempDir() {
155269
155826
  var MAX_OUTPUT_CHARS = 5e3;
155270
155827
  function capOutput(output) {
155271
155828
  if (output.length <= MAX_OUTPUT_CHARS) return output;
155272
- const fullPath = join17(getTempDir(), `shell-${randomUUID4().slice(0, 8)}.log`);
155829
+ const fullPath = join19(getTempDir(), `shell-${randomUUID4().slice(0, 8)}.log`);
155273
155830
  writeFileSync12(fullPath, output);
155274
155831
  const elided = output.length - MAX_OUTPUT_CHARS;
155275
155832
  return `... [${elided} chars truncated; full output saved to ${fullPath}] ...
@@ -155324,8 +155881,8 @@ Do NOT use this tool for git commands \u2014 use the dedicated git tools instead
155324
155881
  if (params.background) {
155325
155882
  const tempDir = getTempDir();
155326
155883
  const handle = `bg-${randomUUID4().slice(0, 8)}`;
155327
- const outputPath = join17(tempDir, `${handle}.log`);
155328
- const pidPath = join17(tempDir, `${handle}.pid`);
155884
+ const outputPath = join19(tempDir, `${handle}.log`);
155885
+ const pidPath = join19(tempDir, `${handle}.pid`);
155329
155886
  const logFd = openSync(outputPath, "a");
155330
155887
  let proc2;
155331
155888
  try {
@@ -155556,7 +156113,7 @@ function buildCommonTools(ctx, outputSchema) {
155556
156113
  return tools;
155557
156114
  }
155558
156115
  function buildOrchestratorTools(ctx, outputSchema) {
155559
- return [
156116
+ const tools = [
155560
156117
  ...buildCommonTools(ctx, outputSchema),
155561
156118
  ReportProgressTool(ctx),
155562
156119
  SelectModeTool(ctx),
@@ -155566,6 +156123,10 @@ function buildOrchestratorTools(ctx, outputSchema) {
155566
156123
  CreatePullRequestTool(ctx),
155567
156124
  UpdatePullRequestBodyTool(ctx)
155568
156125
  ];
156126
+ if (ctx.signedCommits) {
156127
+ tools.push(CommitChangesTool(ctx));
156128
+ }
156129
+ return tools;
155569
156130
  }
155570
156131
  async function tryStartMcpServer(ctx, tools, port) {
155571
156132
  const server = new FastMCP({ name: pullfrogMcpName, version: "0.0.1" });
@@ -155756,8 +156317,14 @@ var MISSING_KEY_MARKER = "no API key found";
155756
156317
  function buildMissingApiKeyError(params) {
155757
156318
  const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
155758
156319
  const settingsUrl = `${getApiUrl()}/console/${params.owner}/${params.name}`;
156320
+ const envVars = params.model?.includes("/") ? getModelEnvVars(params.model) : [];
156321
+ const [primary, ...alternates] = envVars;
156322
+ const envVarList = primary ? `\`${primary}\`${alternates.length > 0 ? ` (or ${alternates.map((v) => `\`${v}\``).join(" / ")})` : ""}` : void 0;
156323
+ const lead = envVarList ? `**${MISSING_KEY_MARKER}** \u2014 this repo is configured to use \`${params.model}\`, which needs ${envVarList}, but the runner has no key for it.` : `**${MISSING_KEY_MARKER}** \u2014 Pullfrog needs at least one LLM provider API key (e.g. \`ANTHROPIC_API_KEY\`, \`OPENAI_API_KEY\`, \`GEMINI_API_KEY\`) configured as a GitHub Actions secret.`;
155759
156324
  return [
155760
- `**${MISSING_KEY_MARKER}** \u2014 Pullfrog needs at least one LLM provider API key (e.g. \`ANTHROPIC_API_KEY\`, \`OPENAI_API_KEY\`, \`GEMINI_API_KEY\`) configured as a GitHub Actions secret.`,
156325
+ lead,
156326
+ "",
156327
+ "**To fix:** add the key as a GitHub Actions secret (referenced from your workflow's `env:` block) or as a Pullfrog secret in the console \u2014 or switch this repo to a different model (free models need no key).",
155761
156328
  "",
155762
156329
  `[Open repo secrets \u2192](${githubSecretsUrl}) \xB7 [Configure model \u2192](${settingsUrl}) \xB7 [Setup docs \u2192](https://docs.pullfrog.com/keys) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
155763
156330
  ].join("\n");
@@ -155837,10 +156404,14 @@ function validateAgentApiKey(params) {
155837
156404
  }
155838
156405
  if (params.agent.name === "opencode") {
155839
156406
  if (params.authorized.has(params.model)) return;
155840
- throw new Error(buildMissingApiKeyError({ owner: params.owner, name: params.name }));
156407
+ throw new Error(
156408
+ buildMissingApiKeyError({ owner: params.owner, name: params.name, model: params.model })
156409
+ );
155841
156410
  }
155842
156411
  if (hasEnvVar3("ANTHROPIC_API_KEY") || hasEnvVar3("CLAUDE_CODE_OAUTH_TOKEN")) return;
155843
- throw new Error(buildMissingApiKeyError({ owner: params.owner, name: params.name }));
156412
+ throw new Error(
156413
+ buildMissingApiKeyError({ owner: params.owner, name: params.name, model: params.model })
156414
+ );
155844
156415
  }
155845
156416
  if (params.agent.name === "opencode") {
155846
156417
  if (params.authorized.size > 0) return;
@@ -155851,42 +156422,37 @@ function validateAgentApiKey(params) {
155851
156422
  }
155852
156423
  function isApiKeyAuthError(text) {
155853
156424
  if (!text) return false;
155854
- return text.includes(MISSING_KEY_MARKER) || /Invalid API key/i.test(text) || /\bUser not found\b/i.test(text) || /\bInvalid authentication\b/i.test(text) || /authentication_error/i.test(text) || /Invalid bearer token/i.test(text) || /api_error_status\s*=\s*401/i.test(text) || /API Error:\s*401/i.test(text);
156425
+ return text.includes(MISSING_KEY_MARKER) || /Invalid API key/i.test(text) || /\bUser not found\b/i.test(text) || /\bInvalid authentication\b/i.test(text) || /authentication_error/i.test(text) || /Invalid bearer token/i.test(text) || /api_error_status\s*=\s*401/i.test(text) || /API Error:\s*401/i.test(text) || /Failed to authenticate\. API Error:/i.test(text) || isOAuthCredentialExpiredError(text);
156426
+ }
156427
+ function isOAuthCredentialExpiredError(text) {
156428
+ return /authentication token has expired/i.test(text) || /Token refresh failed/i.test(text);
155855
156429
  }
155856
156430
  function formatApiKeyErrorSummary(params) {
155857
156431
  if (params.raw.includes(MISSING_KEY_MARKER)) {
156432
+ if (params.raw.startsWith(`**${MISSING_KEY_MARKER}**`)) return params.raw;
155858
156433
  return buildMissingApiKeyError({ owner: params.owner, name: params.name });
155859
156434
  }
155860
156435
  const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
155861
156436
  const settingsUrl = `${getApiUrl()}/console/${params.owner}/${params.name}`;
156437
+ if (isOAuthCredentialExpiredError(params.raw)) {
156438
+ return [
156439
+ `**Your provider OAuth credential has expired.** Re-authenticate the provider connection (e.g. \`pullfrog auth codex\`), then re-trigger the run.`,
156440
+ "",
156441
+ `[Model settings \u2192](${settingsUrl}) \xB7 [Setup docs \u2192](https://docs.pullfrog.com/keys) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
156442
+ ].join("\n");
156443
+ }
155862
156444
  return [
155863
- `**Your LLM provider API key was rejected (401).** Rotate the key in your provider dashboard, then update the matching GitHub Actions secret.`,
156445
+ `**Your LLM provider API key was rejected.** Rotate the key in your provider dashboard, then update the matching GitHub Actions secret.`,
155864
156446
  "",
155865
156447
  `[Update repo secret \u2192](${githubSecretsUrl}) \xB7 [Model settings \u2192](${settingsUrl}) \xB7 [Setup docs \u2192](https://docs.pullfrog.com/keys) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
155866
156448
  ].join("\n");
155867
156449
  }
155868
156450
 
155869
- // utils/byokFallback.ts
155870
- var FREE_FALLBACK_SLUG = "opencode/big-pickle";
155871
- function selectFallbackModelIfNeeded(input) {
155872
- if (input.proxyModel) return { fallback: false };
155873
- if (!input.resolvedModel) return { fallback: false };
155874
- if (input.resolvedModel === FREE_FALLBACK_SLUG) return { fallback: false };
155875
- if (!input.resolvedModel.includes("/")) return { fallback: false };
155876
- if (input.agentName === "claude") return { fallback: false };
155877
- if (input.authorized.has(input.resolvedModel)) return { fallback: false };
155878
- return {
155879
- fallback: true,
155880
- from: input.resolvedModel,
155881
- to: FREE_FALLBACK_SLUG
155882
- };
155883
- }
155884
-
155885
156451
  // utils/gitAuthServer.ts
155886
156452
  import { randomUUID as randomUUID5 } from "node:crypto";
155887
156453
  import { writeFileSync as writeFileSync13 } from "node:fs";
155888
156454
  import { createServer as createServer3 } from "node:http";
155889
- import { join as join18 } from "node:path";
156455
+ import { join as join20 } from "node:path";
155890
156456
  var REVOKED_TRAP_MS = 6e4;
155891
156457
  function revokeGitHubToken(token) {
155892
156458
  fetch("https://api.github.com/installation/token", {
@@ -155955,7 +156521,7 @@ async function startGitAuthServer(tmpdir4) {
155955
156521
  function writeAskpassScript(code) {
155956
156522
  const scriptId = randomUUID5();
155957
156523
  const scriptName = `askpass-${scriptId}.js`;
155958
- const scriptPath = join18(tmpdir4, scriptName);
156524
+ const scriptPath = join20(tmpdir4, scriptName);
155959
156525
  const content = [
155960
156526
  `#!/usr/bin/env node`,
155961
156527
  `var a=process.argv[2]||"";`,
@@ -155993,7 +156559,7 @@ async function startGitAuthServer(tmpdir4) {
155993
156559
  var core3 = __toESM(require_core(), 1);
155994
156560
  import { createSign } from "node:crypto";
155995
156561
  import { rename, writeFile } from "node:fs/promises";
155996
- import { dirname as dirname3, join as join19 } from "node:path";
156562
+ import { dirname as dirname3, join as join21 } from "node:path";
155997
156563
 
155998
156564
  // node_modules/.pnpm/@octokit+plugin-throttling@11.0.3_@octokit+core@7.0.5/node_modules/@octokit/plugin-throttling/dist-bundle/index.js
155999
156565
  var import_light = __toESM(require_light(), 1);
@@ -159635,6 +160201,7 @@ var Octokit2 = Octokit.plugin(requestLog, legacyRestEndpointMethods, paginateRes
159635
160201
  );
159636
160202
 
159637
160203
  // utils/github.ts
160204
+ var OIDC_AUDIENCE = "pullfrog-api";
159638
160205
  function isObject4(value2) {
159639
160206
  return typeof value2 === "object" && value2 !== null;
159640
160207
  }
@@ -159651,8 +160218,39 @@ var TokenExchangeError = class extends Error {
159651
160218
  this.status = status;
159652
160219
  }
159653
160220
  };
160221
+ async function fetchIdTokenFromStash(creds) {
160222
+ const url4 = new URL(creds.requestUrl);
160223
+ url4.searchParams.set("audience", OIDC_AUDIENCE);
160224
+ const timeoutMs = 3e4;
160225
+ let response;
160226
+ try {
160227
+ response = await fetch(url4, {
160228
+ headers: { Authorization: `Bearer ${creds.requestToken}` },
160229
+ signal: AbortSignal.timeout(timeoutMs)
160230
+ });
160231
+ } catch (error49) {
160232
+ if (error49 instanceof Error && error49.name === "TimeoutError") {
160233
+ throw new Error(`ID token request timed out after ${timeoutMs}ms`);
160234
+ }
160235
+ throw error49;
160236
+ }
160237
+ if (!response.ok) {
160238
+ throw new TokenExchangeError(
160239
+ response.status,
160240
+ `Failed to get ID token: ${response.status} ${response.statusText}`
160241
+ );
160242
+ }
160243
+ const body = await response.json();
160244
+ if (!body.value) {
160245
+ throw new Error("ID token response has no value field");
160246
+ }
160247
+ if (isGitHubActions) {
160248
+ core3.setSecret(body.value);
160249
+ }
160250
+ return body.value;
160251
+ }
159654
160252
  async function acquireTokenViaOIDC(opts) {
159655
- const oidcToken = await core3.getIDToken("pullfrog-api");
160253
+ const oidcToken = opts?.oidc ? await fetchIdTokenFromStash(opts.oidc) : await core3.getIDToken(OIDC_AUDIENCE);
159656
160254
  const repos = [...opts?.repos ?? []];
159657
160255
  const targetRepo = process.env.GITHUB_REPOSITORY?.split("/")[1];
159658
160256
  if (targetRepo) {
@@ -159801,14 +160399,15 @@ async function acquireTokenViaGitHubApp(opts) {
159801
160399
  const installationId = await findInstallationId(jwt2, config3.repoOwner, config3.repoName);
159802
160400
  return await createInstallationToken(jwt2, installationId, opts?.permissions);
159803
160401
  }
160402
+ function isTransientTokenError(error49) {
160403
+ if (error49 instanceof TokenExchangeError) return error49.status >= 500 || error49.status === 429;
160404
+ return error49 instanceof Error && (error49.message.includes("timed out") || error49.message.includes("fetch failed") || error49.message.includes("ECONNRESET") || error49.message.includes("ETIMEDOUT"));
160405
+ }
159804
160406
  async function acquireNewToken(opts) {
159805
- if (isOIDCAvailable()) {
160407
+ if (opts?.oidc || isOIDCAvailable()) {
159806
160408
  return await retry(() => acquireTokenViaOIDC(opts), {
159807
160409
  label: "token exchange",
159808
- shouldRetry: (error49) => {
159809
- if (error49 instanceof TokenExchangeError) return error49.status >= 500 || error49.status === 429;
159810
- return error49 instanceof Error && (error49.message.includes("timed out") || error49.message.includes("fetch failed") || error49.message.includes("ECONNRESET") || error49.message.includes("ETIMEDOUT"));
159811
- }
160410
+ shouldRetry: isTransientTokenError
159812
160411
  });
159813
160412
  }
159814
160413
  if (process.env.GITHUB_ACTIONS === "true") {
@@ -159851,14 +160450,14 @@ function getGitHubUsageSummary() {
159851
160450
  }
159852
160451
  async function writeGitHubUsageSummaryToFile(path4) {
159853
160452
  const summary2 = getGitHubUsageSummary();
159854
- const tmpPath = join19(dirname3(path4), `.usage-summary-${process.pid}.tmp`);
160453
+ const tmpPath = join21(dirname3(path4), `.usage-summary-${process.pid}.tmp`);
159855
160454
  await writeFile(tmpPath, JSON.stringify(summary2));
159856
160455
  await rename(tmpPath, path4);
159857
160456
  }
159858
- function createOctokit(token) {
160457
+ function createOctokit(token, refreshAuth) {
160458
+ let currentToken = token;
159859
160459
  const OctokitWithPlugins = Octokit2.plugin(throttling);
159860
160460
  const octokit = new OctokitWithPlugins({
159861
- auth: token,
159862
160461
  throttle: {
159863
160462
  onRateLimit: (_retryAfter, _options, _octokit, retryCount) => {
159864
160463
  return retryCount <= 2;
@@ -159887,6 +160486,8 @@ function createOctokit(token) {
159887
160486
  return response;
159888
160487
  };
159889
160488
  octokit.hook.wrap("request", async (request2, options) => {
160489
+ const sentToken = currentToken;
160490
+ options.headers.authorization = `token ${sentToken}`;
159890
160491
  try {
159891
160492
  const response = await request2(options);
159892
160493
  onResponse(response);
@@ -159895,6 +160496,13 @@ function createOctokit(token) {
159895
160496
  if (isObject4(error49) && "response" in error49 && isObject4(error49.response) && "headers" in error49.response && isObject4(error49.response.headers)) {
159896
160497
  onResponse(error49.response);
159897
160498
  }
160499
+ if (refreshAuth && isObject4(error49) && "status" in error49 && error49.status === 401) {
160500
+ currentToken = await refreshAuth(sentToken);
160501
+ options.headers.authorization = `token ${currentToken}`;
160502
+ const response = await request2(options);
160503
+ onResponse(response);
160504
+ return response;
160505
+ }
159898
160506
  throw error49;
159899
160507
  }
159900
160508
  });
@@ -160093,7 +160701,17 @@ Use \`${t2("git")}\` for local git commands (status, log, add, commit, checkout,
160093
160701
  - \`${t2("checkout_pr")}\` - checkout a PR branch (fetches and configures push for forks)
160094
160702
  - \`${t2("delete_branch")}\` - delete a remote branch (requires push: enabled)
160095
160703
  - \`${t2("push_tags")}\` - push tags (requires push: enabled)
160096
-
160704
+ ${ctx.signedCommits ? `
160705
+ #### Signed commits (enabled for this repository)
160706
+
160707
+ This repository requires GitHub-signed commits, which local git commits can never satisfy. This OVERRIDES any other instruction (including mode instructions) to commit via git or push via \`${t2("push_branch")}\`:
160708
+ - Do NOT use git commit or \`${t2("push_branch")}\` for same-repo branches \u2014 both are blocked. Instead: edit files, then call \`${t2("commit_changes")}\` with a commit message. It commits every working-tree change (or a \`files\` subset) directly to the remote branch as a GitHub-signed (Verified) commit. There is no separate push step.
160709
+ - New branches: create locally as usual (git checkout -b); the remote branch is created on the first \`${t2("commit_changes")}\` call.
160710
+ - To integrate remote changes (concurrent pushes, base branch): \`${t2("git_fetch")}\`, then git merge --no-commit <ref>, resolve conflicts, git add the results, then \`${t2("commit_changes")}\` \u2014 it concludes the merge as a signed merge commit.
160711
+ - \`${t2("commit_changes")}\` commits EVERY working-tree change by default \u2014 review \`git status\` first and clean up stray artifacts (or pass \`files\`).
160712
+ - cherry-pick/revert: use \`-n\`/\`--no-commit\` so no local commit is created, then \`${t2("commit_changes")}\`.
160713
+ - Fork PRs are the exception: signing is impossible there, so commit and push normally (those commits will be unsigned).
160714
+ ` : ""}
160097
160715
  Rules:
160098
160716
  - All code changes must be pushed to a pull request (new or existing) before the run ends. This environment is ephemeral \u2014 unpushed work is lost permanently. \`git status\` must be clean when you finish.
160099
160717
  - Protected branches (default branch) are blocked from direct pushes in restricted mode. Do not use \`git push\` directly \u2014 it will fail without credentials.
@@ -160269,7 +160887,7 @@ function resolveInstructions(ctx) {
160269
160887
 
160270
160888
  // utils/learnings.ts
160271
160889
  import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
160272
- import { dirname as dirname4, join as join20 } from "node:path";
160890
+ import { dirname as dirname4, join as join22 } from "node:path";
160273
160891
 
160274
160892
  // utils/learningsTruncate.ts
160275
160893
  var MAX_LEARNINGS_LENGTH = 1e5;
@@ -160286,7 +160904,7 @@ function truncateAtLineBoundary(body, cap) {
160286
160904
  // utils/learnings.ts
160287
160905
  var LEARNINGS_FILE_NAME = "pullfrog-learnings.md";
160288
160906
  function learningsFilePath(tmpdir4) {
160289
- return join20(tmpdir4, LEARNINGS_FILE_NAME);
160907
+ return join22(tmpdir4, LEARNINGS_FILE_NAME);
160290
160908
  }
160291
160909
  async function seedLearningsFile(params) {
160292
160910
  const path4 = learningsFilePath(params.tmpdir);
@@ -160706,6 +161324,10 @@ function formatTransientErrorSummary(error49, owner) {
160706
161324
  var core7 = __toESM(require_core(), 1);
160707
161325
  import assert2 from "node:assert/strict";
160708
161326
  var mcpTokenValue;
161327
+ var refreshMcpTokenFn;
161328
+ function getMcpTokenRefresh() {
161329
+ return refreshMcpTokenFn;
161330
+ }
160709
161331
  function getJobToken() {
160710
161332
  const inputToken = core7.getInput("token");
160711
161333
  if (inputToken) {
@@ -160757,6 +161379,29 @@ async function resolveTokens(params) {
160757
161379
  `\xBB acquired scoped MCP token (${Object.entries(mcpPermissions).map((e) => e.join(":")).join(", ")})`
160758
161380
  );
160759
161381
  mcpTokenValue = mcpToken;
161382
+ let currentMcpToken = mcpToken;
161383
+ let refreshPromise;
161384
+ refreshMcpTokenFn = (stale) => {
161385
+ assert2(mcpTokenValue, "tokens already disposed");
161386
+ if (stale !== currentMcpToken) {
161387
+ return Promise.resolve(currentMcpToken);
161388
+ }
161389
+ refreshPromise ??= acquireNewToken({
161390
+ permissions: mcpPermissions,
161391
+ oidc: params.oidc ?? void 0
161392
+ }).then((fresh) => {
161393
+ if (isGitHubActions) {
161394
+ core7.setSecret(fresh);
161395
+ }
161396
+ mcpTokenValue = fresh;
161397
+ currentMcpToken = fresh;
161398
+ log.warning("\xBB GitHub rejected the MCP token; re-acquired a fresh scoped MCP token");
161399
+ return fresh;
161400
+ }).finally(() => {
161401
+ refreshPromise = void 0;
161402
+ });
161403
+ return refreshPromise;
161404
+ };
160760
161405
  let disposingRef;
160761
161406
  const dispose = async () => {
160762
161407
  if (disposingRef) {
@@ -160765,9 +161410,10 @@ async function resolveTokens(params) {
160765
161410
  disposingRef = Promise.withResolvers();
160766
161411
  try {
160767
161412
  mcpTokenValue = void 0;
161413
+ refreshMcpTokenFn = void 0;
160768
161414
  await Promise.all([
160769
161415
  revokeGitHubInstallationToken(gitToken),
160770
- revokeGitHubInstallationToken(mcpToken)
161416
+ revokeGitHubInstallationToken(currentMcpToken)
160771
161417
  ]);
160772
161418
  } finally {
160773
161419
  removeSignalHandler();
@@ -160811,7 +161457,7 @@ async function reportErrorToComment(ctx) {
160811
161457
 
160812
161458
  ${ctx.error}` : ctx.error;
160813
161459
  const repoContext = parseRepoContext();
160814
- const octokit = createOctokit(getGitHubInstallationToken());
161460
+ const octokit = createOctokit(getGitHubInstallationToken(), getMcpTokenRefresh());
160815
161461
  const runId = process.env.GITHUB_RUN_ID ? Number.parseInt(process.env.GITHUB_RUN_ID, 10) : void 0;
160816
161462
  const customParts = [];
160817
161463
  if (runId) {
@@ -160825,7 +161471,6 @@ ${ctx.error}` : ctx.error;
160825
161471
  workflowRun: runId ? { owner: repoContext.owner, repo: repoContext.name, runId } : void 0,
160826
161472
  customParts,
160827
161473
  model: ctx.toolState.model,
160828
- fallbackFrom: ctx.toolState.modelFallback?.from,
160829
161474
  oss: ctx.toolState.oss
160830
161475
  });
160831
161476
  const body = `${formattedError}${footer}`;
@@ -160892,18 +161537,15 @@ async function mintProxyKey(ctx) {
160892
161537
  if (error49 instanceof TransientError) throw error49;
160893
161538
  log.warning(`proxy key mint error: ${error49 instanceof Error ? error49.message : String(error49)}`);
160894
161539
  return null;
160895
- } finally {
160896
- delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
160897
- delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
160898
161540
  }
160899
161541
  }
160900
161542
  async function buildProxyTokenHeaders(ctx) {
160901
161543
  if (ctx.oidcCredentials) {
160902
- process.env.ACTIONS_ID_TOKEN_REQUEST_URL = ctx.oidcCredentials.requestUrl;
160903
- process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = ctx.oidcCredentials.requestToken;
160904
- const oidcToken = await core8.getIDToken("pullfrog-api");
160905
- delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
160906
- delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
161544
+ const creds = ctx.oidcCredentials;
161545
+ const oidcToken = await retry(() => fetchIdTokenFromStash(creds), {
161546
+ label: "ID token mint",
161547
+ shouldRetry: isTransientTokenError
161548
+ });
160907
161549
  return { Authorization: `Bearer ${oidcToken}` };
160908
161550
  }
160909
161551
  if (isLocalApiUrl()) {
@@ -160967,7 +161609,7 @@ async function runProxyResolution(ctx) {
160967
161609
 
160968
161610
  // utils/prSummary.ts
160969
161611
  import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
160970
- import { dirname as dirname5, join as join21 } from "node:path";
161612
+ import { dirname as dirname5, join as join23 } from "node:path";
160971
161613
  var SUMMARY_FILE_NAME = "pullfrog-summary.md";
160972
161614
  var SUMMARY_SCAFFOLD = `# PR summary
160973
161615
 
@@ -160977,7 +161619,7 @@ var SUMMARY_SCAFFOLD = `# PR summary
160977
161619
  var MIN_SNAPSHOT_LENGTH = 60;
160978
161620
  var MAX_SNAPSHOT_LENGTH = 32768;
160979
161621
  function summaryFilePath(tmpdir4) {
160980
- return join21(tmpdir4, SUMMARY_FILE_NAME);
161622
+ return join23(tmpdir4, SUMMARY_FILE_NAME);
160981
161623
  }
160982
161624
  async function seedSummaryFile(params) {
160983
161625
  const path4 = summaryFilePath(params.tmpdir);
@@ -161105,6 +161747,7 @@ var defaultSettings = {
161105
161747
  push: "restricted",
161106
161748
  shell: "restricted",
161107
161749
  prApproveEnabled: false,
161750
+ signedCommits: false,
161108
161751
  modeInstructions: {},
161109
161752
  learnings: null,
161110
161753
  learningsHeadings: [],
@@ -161337,7 +161980,7 @@ ${input.errorMessage}
161337
161980
  ].join("\n");
161338
161981
  }
161339
161982
  function formatProviderModelNotFoundSummary(input) {
161340
- return `Pullfrog's free fallback model is no longer available in OpenCode's catalog. Add an API key for your configured model in the Pullfrog console for \`${input.owner}/${input.name}\`, or contact support if this persists.
161983
+ 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.
161341
161984
 
161342
161985
  \`\`\`
161343
161986
  ${input.raw}
@@ -161778,7 +162421,7 @@ async function main() {
161778
162421
  const initialOctokit = createOctokit(jobToken);
161779
162422
  const runContext = await resolveRunContextData({ octokit: initialOctokit, token: jobToken });
161780
162423
  timer.checkpoint("runContextData");
161781
- const tmpdir4 = createTempDirectory();
162424
+ createTempDirectory();
161782
162425
  const opencodeCliPath = await agents.opencode.install();
161783
162426
  captureBaselineModels(opencodeCliPath);
161784
162427
  if (runContext.dbSecrets) {
@@ -161802,12 +162445,12 @@ async function main() {
161802
162445
  if (payload.event.trigger === "pull_request_synchronize") {
161803
162446
  toolState.beforeSha = payload.event.before_sha;
161804
162447
  }
161805
- const tokenRef = __using(_stack2, await resolveTokens({ push: payload.push }), true);
161806
- wipeRunnerLeakSurface();
161807
162448
  const oidcCredentials = process.env.ACTIONS_ID_TOKEN_REQUEST_URL && process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN ? {
161808
162449
  requestUrl: process.env.ACTIONS_ID_TOKEN_REQUEST_URL,
161809
162450
  requestToken: process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN
161810
162451
  } : null;
162452
+ const tokenRef = __using(_stack2, await resolveTokens({ push: payload.push, oidc: oidcCredentials }), true);
162453
+ wipeRunnerLeakSurface();
161811
162454
  if (payload.shell !== "enabled") {
161812
162455
  delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
161813
162456
  delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
@@ -161820,7 +162463,7 @@ async function main() {
161820
162463
  repo: runContext.repo,
161821
162464
  toolState
161822
162465
  });
161823
- const octokit = createOctokit(tokenRef.mcpToken);
162466
+ const octokit = createOctokit(tokenRef.mcpToken, getMcpTokenRefresh());
161824
162467
  const runInfo = await resolveRun({ octokit });
161825
162468
  let toolContext;
161826
162469
  let progressCallbackDisabled = false;
@@ -161832,13 +162475,13 @@ async function main() {
161832
162475
  if (payload.cwd && process.cwd() !== payload.cwd) {
161833
162476
  process.chdir(payload.cwd);
161834
162477
  }
161835
- const tmpdir5 = createTempDirectory();
162478
+ const tmpdir4 = createTempDirectory();
161836
162479
  const originalBody = payload.event.body;
161837
162480
  const resolvedBody = await resolveBody({
161838
162481
  event: payload.event,
161839
162482
  octokit,
161840
162483
  repo: runContext.repo,
161841
- tmpdir: tmpdir5,
162484
+ tmpdir: tmpdir4,
161842
162485
  githubToken: tokenRef.mcpToken
161843
162486
  });
161844
162487
  if (resolvedBody !== originalBody) {
@@ -161847,33 +162490,18 @@ async function main() {
161847
162490
  payload.prompt = payload.prompt.replace(originalBody, resolvedBody ?? "");
161848
162491
  }
161849
162492
  }
161850
- const gitAuthServer = __using(_stack, await startGitAuthServer(tmpdir5), true);
162493
+ const gitAuthServer = __using(_stack, await startGitAuthServer(tmpdir4), true);
161851
162494
  setGitAuthServer(gitAuthServer);
161852
- const initialResolvedModel = payload.proxyModel ? void 0 : resolveModel({ slug: payload.model });
161853
- const authorized2 = getAuthorizedModels();
161854
- const fallback = selectFallbackModelIfNeeded({
161855
- resolvedModel: initialResolvedModel,
161856
- proxyModel: payload.proxyModel,
161857
- authorized: authorized2,
161858
- agentName: resolveAgent({ model: initialResolvedModel }).name
161859
- });
161860
- const effectiveSlug = fallback.fallback ? fallback.to : payload.model;
161861
- const resolvedModel = fallback.fallback ? fallback.to : initialResolvedModel;
161862
- if (fallback.fallback) {
161863
- log.warning(
161864
- `\xBB fell back from ${fallback.from} to ${fallback.to} \u2014 no BYOK key present in runner env. add a provider key in repo secrets to use ${fallback.from} instead.`
161865
- );
161866
- toolState.modelFallback = { from: fallback.from };
161867
- }
162495
+ const resolvedModel = payload.proxyModel ? void 0 : resolveModel({ slug: payload.model });
161868
162496
  vertexCredentials = materializeVertexCredentials({ model: resolvedModel });
161869
162497
  const agent2 = resolveAgent({ model: resolvedModel });
161870
- const effectiveModel = payload.proxyModel ?? resolvedModel ?? effectiveSlug;
162498
+ const effectiveModel = payload.proxyModel ?? resolvedModel ?? payload.model;
161871
162499
  toolState.model = effectiveModel;
161872
- if (!fallback.fallback && !payload.proxyModel) {
162500
+ if (!payload.proxyModel) {
161873
162501
  validateAgentApiKey({
161874
162502
  agent: agent2,
161875
162503
  model: effectiveModel,
161876
- authorized: authorized2,
162504
+ authorized: getAuthorizedModels(),
161877
162505
  owner: runContext.repo.owner,
161878
162506
  name: runContext.repo.name
161879
162507
  });
@@ -161890,7 +162518,7 @@ async function main() {
161890
162518
  timer.checkpoint("git");
161891
162519
  const pmSpec = await resolvePackageManagerSpec(process.cwd());
161892
162520
  if (pmSpec) {
161893
- await ensurePackageManager({ spec: pmSpec, binDir: packageManagerBinDir(tmpdir5) });
162521
+ await ensurePackageManager({ spec: pmSpec, binDir: packageManagerBinDir(tmpdir4) });
161894
162522
  }
161895
162523
  timer.checkpoint("packageManager");
161896
162524
  const setupHook = await executeLifecycleHook({
@@ -161903,26 +162531,34 @@ async function main() {
161903
162531
  }
161904
162532
  timer.checkpoint("lifecycleHooks::setup");
161905
162533
  const agentId = agent2.name;
161906
- const modes2 = [...computeModes(agentId), ...runContext.repoSettings.modes];
162534
+ const modes2 = [
162535
+ ...computeModes(agentId, runContext.repoSettings.signedCommits),
162536
+ ...runContext.repoSettings.modes
162537
+ ];
161907
162538
  const outputSchema = resolveOutputSchema();
161908
162539
  toolContext = {
161909
162540
  agentId,
161910
162541
  repo: runContext.repo,
161911
162542
  payload,
161912
162543
  octokit,
161913
- githubInstallationToken: tokenRef.mcpToken,
162544
+ // live getter so raw-token consumers (asset fetches, plan/summary-comment
162545
+ // GETs) see the refreshed MCP token after a mid-run re-acquisition (#891)
162546
+ get githubInstallationToken() {
162547
+ return getGitHubInstallationToken();
162548
+ },
161914
162549
  gitToken: tokenRef.gitToken,
161915
162550
  apiToken: runContext.apiToken,
161916
162551
  modes: modes2,
161917
162552
  postCheckoutScript: runContext.repoSettings.postCheckoutScript,
161918
162553
  prepushScript: runContext.repoSettings.prepushScript,
161919
162554
  prApproveEnabled: runContext.repoSettings.prApproveEnabled,
162555
+ signedCommits: runContext.repoSettings.signedCommits,
161920
162556
  modeInstructions: runContext.repoSettings.modeInstructions,
161921
162557
  toolState,
161922
162558
  runId: runInfo.runId,
161923
162559
  jobId: runInfo.jobId,
161924
162560
  mcpServerUrl: "",
161925
- tmpdir: tmpdir5,
162561
+ tmpdir: tmpdir4,
161926
162562
  oss: runContext.oss,
161927
162563
  plan: runContext.plan,
161928
162564
  resolvedModel
@@ -161933,7 +162569,7 @@ async function main() {
161933
162569
  timer.checkpoint("mcpServer");
161934
162570
  try {
161935
162571
  const learningsPath = await seedLearningsFile({
161936
- tmpdir: tmpdir5,
162572
+ tmpdir: tmpdir4,
161937
162573
  current: runContext.repoSettings.learnings
161938
162574
  });
161939
162575
  toolState.learningsFilePath = learningsPath;
@@ -161950,7 +162586,7 @@ async function main() {
161950
162586
  }
161951
162587
  if (payload.generateSummary && payload.event.is_pr && payload.event.issue_number) {
161952
162588
  const previousSnapshot = await fetchPreviousSnapshot(toolContext, payload.event.issue_number);
161953
- const filePath = await seedSummaryFile({ tmpdir: tmpdir5, previousSnapshot });
162589
+ const filePath = await seedSummaryFile({ tmpdir: tmpdir4, previousSnapshot });
161954
162590
  toolState.summaryFilePath = filePath;
161955
162591
  try {
161956
162592
  toolState.summarySeed = await readFile5(filePath, "utf8");
@@ -161970,6 +162606,7 @@ async function main() {
161970
162606
  modes: modes2,
161971
162607
  agentId,
161972
162608
  outputSchema,
162609
+ signedCommits: runContext.repoSettings.signedCommits,
161973
162610
  learningsFilePath: toolState.learningsFilePath ?? null,
161974
162611
  learningsHeadings: runContext.repoSettings.learningsHeadings,
161975
162612
  setupHookFailure: describeSetupFailure(setupHook.failure)
@@ -161988,7 +162625,7 @@ ${instructions.user}` : null,
161988
162625
  log.info(instructions.full);
161989
162626
  });
161990
162627
  if (agentId === "opencode") {
161991
- const pluginDir = join22(process.cwd(), ".opencode", "plugin");
162628
+ const pluginDir = join24(process.cwd(), ".opencode", "plugin");
161992
162629
  const hasPlugins = existsSync8(pluginDir) && readdirSync2(pluginDir).some((f) => /\.[jt]sx?$/.test(f));
161993
162630
  if (hasPlugins && toolState.dependencyInstallation?.promise) {
161994
162631
  log.info(
@@ -162043,7 +162680,7 @@ ${instructions.user}` : null,
162043
162680
  payload,
162044
162681
  resolvedModel,
162045
162682
  mcpServerUrl: mcpHttpServer.url,
162046
- tmpdir: tmpdir5,
162683
+ tmpdir: tmpdir4,
162047
162684
  // PULLFROG_DATA_DIR (/var/lib/pullfrog) holds codex auth.json + any
162048
162685
  // future pullfrog-managed on-disk secrets. bash via MCP tmpfs-overlays
162049
162686
  // it; agent native FS tools deny it via the same secretDenyPaths plumbing
@@ -162319,7 +162956,7 @@ async function run(args2) {
162319
162956
  }
162320
162957
 
162321
162958
  // commands/init.ts
162322
- import { execFileSync as execFileSync8 } from "node:child_process";
162959
+ import { execFileSync as execFileSync9 } from "node:child_process";
162323
162960
  var import_arg3 = __toESM(require_arg(), 1);
162324
162961
  var import_picocolors3 = __toESM(require_picocolors(), 1);
162325
162962
  var PULLFROG_API_URL2 = (process.env.PULLFROG_API_URL || "https://pullfrog.com").replace(
@@ -162379,7 +163016,7 @@ function handleCancel2(value2) {
162379
163016
  function getGhToken2() {
162380
163017
  let token;
162381
163018
  try {
162382
- token = execFileSync8("gh", ["auth", "token"], { encoding: "utf-8" }).trim();
163019
+ token = execFileSync9("gh", ["auth", "token"], { encoding: "utf-8" }).trim();
162383
163020
  } catch {
162384
163021
  bail2(
162385
163022
  `gh cli not found or not authenticated.
@@ -162422,7 +163059,7 @@ async function ghApi(path4, token) {
162422
163059
  function parseGitRemote2() {
162423
163060
  let url4;
162424
163061
  try {
162425
- url4 = execFileSync8("git", ["remote", "get-url", "origin"], { encoding: "utf-8" }).trim();
163062
+ url4 = execFileSync9("git", ["remote", "get-url", "origin"], { encoding: "utf-8" }).trim();
162426
163063
  } catch {
162427
163064
  bail2("not a git repository or no 'origin' remote found.");
162428
163065
  }
@@ -162433,10 +163070,10 @@ function parseGitRemote2() {
162433
163070
  function openBrowser(url4) {
162434
163071
  try {
162435
163072
  const platform = process.platform;
162436
- if (platform === "darwin") execFileSync8("open", [url4], { stdio: "ignore" });
163073
+ if (platform === "darwin") execFileSync9("open", [url4], { stdio: "ignore" });
162437
163074
  else if (platform === "win32")
162438
- execFileSync8("cmd", ["/c", "start", "", url4], { stdio: "ignore" });
162439
- else execFileSync8("xdg-open", [url4], { stdio: "ignore" });
163075
+ execFileSync9("cmd", ["/c", "start", "", url4], { stdio: "ignore" });
163076
+ else execFileSync9("xdg-open", [url4], { stdio: "ignore" });
162440
163077
  } catch {
162441
163078
  }
162442
163079
  }
@@ -162663,7 +163300,7 @@ function setGhSecret(ctx) {
162663
163300
  let orgFailed = false;
162664
163301
  if (ctx.org) {
162665
163302
  try {
162666
- execFileSync8("gh", ["secret", "set", ctx.name, "--org", ctx.org, "--visibility", "all"], {
163303
+ execFileSync9("gh", ["secret", "set", ctx.name, "--org", ctx.org, "--visibility", "all"], {
162667
163304
  input: ctx.value,
162668
163305
  stdio: ["pipe", "ignore", "pipe"],
162669
163306
  encoding: "utf-8"
@@ -162674,7 +163311,7 @@ function setGhSecret(ctx) {
162674
163311
  }
162675
163312
  }
162676
163313
  try {
162677
- execFileSync8("gh", ["secret", "set", ctx.name, "--repo", ctx.repoSlug], {
163314
+ execFileSync9("gh", ["secret", "set", ctx.name, "--repo", ctx.repoSlug], {
162678
163315
  input: ctx.value,
162679
163316
  stdio: ["pipe", "ignore", "pipe"],
162680
163317
  encoding: "utf-8"
@@ -163046,7 +163683,7 @@ async function run2() {
163046
163683
  }
163047
163684
 
163048
163685
  // cli.ts
163049
- var VERSION10 = "0.1.28";
163686
+ var VERSION10 = "0.1.30";
163050
163687
  var bin = basename2(process.argv[1] || "");
163051
163688
  var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
163052
163689
  var rawArgs = process.argv.slice(2);