pullfrog 0.1.29 → 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 join24(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 = join24(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 join24(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 join23 } 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.29",
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 join14 } 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";
@@ -151901,7 +152009,274 @@ function postProcessRangeDiff(raw2, contextLines = 3) {
151901
152009
  // mcp/git.ts
151902
152010
  import { randomUUID as randomUUID3 } from "node:crypto";
151903
152011
  import { writeFileSync as writeFileSync7 } from "node:fs";
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";
151904
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
151905
152280
  function getPushDestination(branch, storedDest) {
151906
152281
  if (storedDest && storedDest.localBranch === branch) {
151907
152282
  log.debug(`using stored push destination: ${storedDest.remoteName}/${storedDest.remoteBranch}`);
@@ -151959,6 +152334,10 @@ function validateTagName(tag) {
151959
152334
  );
151960
152335
  }
151961
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
+ }
151962
152341
  function validatePushDestination(ctx, branch) {
151963
152342
  const pushUrl = ctx.toolState.pushUrl;
151964
152343
  if (!pushUrl) throw new Error("pushUrl not set - setupGit must run before push_branch");
@@ -151977,6 +152356,47 @@ var PushBranch = type({
151977
152356
  branchName: type.string.describe("The branch name to push (defaults to current branch)").optional(),
151978
152357
  force: type.boolean.describe("Force push (use with caution)").default(false)
151979
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
+ }
151980
152400
  var CONCURRENT_PUSH_PATTERNS = ["fetch first", "non-fast-forward", "cannot lock ref"];
151981
152401
  var TRANSIENT_PATTERNS = [
151982
152402
  /RPC failed/i,
@@ -152036,7 +152456,6 @@ async function pushWithRetry(args2, token) {
152036
152456
  throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
152037
152457
  }
152038
152458
  function PushBranchTool(ctx) {
152039
- const defaultBranch = ctx.repo.data.default_branch || "main";
152040
152459
  const pushPermission = ctx.payload.push;
152041
152460
  return tool({
152042
152461
  name: "push_branch",
@@ -152048,6 +152467,11 @@ function PushBranchTool(ctx) {
152048
152467
  }
152049
152468
  const branch = branchName || $2("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false });
152050
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
+ }
152051
152475
  const status = $2("git", ["status", "--porcelain"], { log: false });
152052
152476
  if (status) {
152053
152477
  throw new Error(
@@ -152058,36 +152482,11 @@ ${status}` + (ctx.toolState.prepushFailureCount > 0 ? "\n\nnote: the prepush hoo
152058
152482
  );
152059
152483
  }
152060
152484
  const pushDest = validatePushDestination(ctx, branch);
152061
- const prBranchMatch = branch.match(/^pr-(\d+)$/);
152062
- if (prBranchMatch && pushDest.remoteBranch !== branch) {
152063
- const prNumber = Number(prBranchMatch[1]);
152064
- const event = ctx.payload.event;
152065
- const runScoped = event.is_pr === true && event.issue_number === prNumber;
152066
- if (!runScoped) {
152067
- throw new Error(
152068
- `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.`
152069
- );
152070
- }
152071
- }
152072
- if (pushPermission === "restricted" && pushDest.remoteBranch === defaultBranch) {
152073
- throw new Error(
152074
- `Push blocked: cannot push directly to default branch '${pushDest.remoteBranch}'. Create a feature branch and open a PR instead.`
152075
- );
152076
- }
152485
+ assertPushTarget(ctx, branch, pushDest);
152077
152486
  const refspec = branch === pushDest.remoteBranch ? branch : `${branch}:${pushDest.remoteBranch}`;
152078
152487
  const pushArgs = force ? ["--force", "-u", pushDest.remoteName, refspec] : ["-u", pushDest.remoteName, refspec];
152079
- const prepushSkipped = ctx.toolState.prepushFailureCount > 0;
152080
- if (prepushSkipped) {
152081
- log.info(`\xBB skipping prepush hook (failed earlier this run)`);
152082
- } else if (ctx.prepushScript) {
152083
- const prepushHook = await executeLifecycleHook({
152084
- event: "prepush",
152085
- script: ctx.prepushScript
152086
- });
152087
- if (prepushHook.failure) {
152088
- ctx.toolState.prepushFailureCount += 1;
152089
- throw new Error(buildPrepushFailureMessage(prepushHook.failure, ctx.payload.shell));
152090
- }
152488
+ const prepushSkipped = await runPrepushHook(ctx, "push_branch");
152489
+ if (!prepushSkipped && ctx.prepushScript) {
152091
152490
  const postHookStatus = $2("git", ["status", "--porcelain"], { log: false });
152092
152491
  if (postHookStatus) {
152093
152492
  throw new Error(
@@ -152138,15 +152537,138 @@ ${integrateStep}
152138
152537
  })
152139
152538
  });
152140
152539
  }
152141
- function buildPrepushFailureMessage(failure, shell) {
152540
+ function buildPrepushFailureMessage(params) {
152541
+ const failure = params.failure;
152142
152542
  const header = failure.kind === "exit" ? `prepush hook failed with exit code ${failure.exitCode}.
152143
152543
 
152144
152544
  script output:
152145
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}.`;
152146
- 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).`;
152147
152547
  return `${header}
152148
152548
 
152149
- 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
+ });
152150
152672
  }
152151
152673
  var AUTH_REQUIRED_REDIRECT = {
152152
152674
  push: "use the push_branch tool instead \u2014 it handles authentication and permission checks.",
@@ -152219,7 +152741,7 @@ function countAhead(head, base) {
152219
152741
  function spillGitOutput(params) {
152220
152742
  const tempDir = process.env.PULLFROG_TEMP_DIR;
152221
152743
  if (!tempDir) throw new Error("PULLFROG_TEMP_DIR not set");
152222
- const outputPath = join12(tempDir, `git-${params.command}-${randomUUID3().slice(0, 8)}.txt`);
152744
+ const outputPath = join13(tempDir, `git-${params.command}-${randomUUID3().slice(0, 8)}.txt`);
152223
152745
  writeFileSync7(outputPath, params.output);
152224
152746
  const previewByLines = params.output.split("\n").slice(0, OVERFLOW_PREVIEW_LINES).join("\n");
152225
152747
  const preview = previewByLines.length <= OVERFLOW_PREVIEW_MAX_CHARS ? previewByLines : `${previewByLines.slice(0, OVERFLOW_PREVIEW_MAX_CHARS)}\u2026`;
@@ -152253,8 +152775,30 @@ function GitTool(ctx) {
152253
152775
  }
152254
152776
  const redirect = AUTH_REQUIRED_REDIRECT[command];
152255
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
+ }
152256
152783
  throw new Error(`git ${command} is not available through this tool \u2014 ${redirect}`);
152257
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
+ }
152258
152802
  if (ctx.payload.shell === "disabled") {
152259
152803
  const blocked = NOSHELL_BLOCKED_SUBCOMMANDS[command];
152260
152804
  if (blocked) {
@@ -152887,7 +153431,6 @@ async function createAndSubmitWithFooter(ctx, params, opts) {
152887
153431
  workflowRun: ctx.runId ? { owner: ctx.repo.owner, repo: ctx.repo.name, runId: ctx.runId, jobId: ctx.jobId } : void 0,
152888
153432
  customParts,
152889
153433
  model: ctx.toolState.model,
152890
- fallbackFrom: ctx.toolState.modelFallback?.from,
152891
153434
  oss: ctx.oss
152892
153435
  });
152893
153436
  return await ctx.octokit.rest.pulls.submitReview({
@@ -152920,12 +153463,12 @@ async function reportReviewNodeId(ctx, params) {
152920
153463
  }
152921
153464
 
152922
153465
  // utils/setup.ts
152923
- import { execFileSync as execFileSync7, execSync as execSync2 } from "node:child_process";
153466
+ import { execFileSync as execFileSync8, execSync as execSync2 } from "node:child_process";
152924
153467
  import { mkdtempSync as mkdtempSync2, readdirSync, realpathSync as realpathSync2, unlinkSync as unlinkSync2 } from "node:fs";
152925
153468
  import { tmpdir as tmpdir3 } from "node:os";
152926
- import { join as join13 } from "node:path";
153469
+ import { join as join14 } from "node:path";
152927
153470
  function createTempDirectory() {
152928
- const sharedTempDir = mkdtempSync2(join13(tmpdir3(), "pullfrog-"));
153471
+ const sharedTempDir = mkdtempSync2(join14(tmpdir3(), "pullfrog-"));
152929
153472
  process.env.PULLFROG_TEMP_DIR = sharedTempDir;
152930
153473
  log.info(`\xBB created temp dir at ${sharedTempDir}`);
152931
153474
  return sharedTempDir;
@@ -152970,13 +153513,13 @@ function wipeRunnerLeakSurface() {
152970
153513
  return [];
152971
153514
  }
152972
153515
  };
152973
- const fileCommandsDir = join13(runnerTemp, "_runner_file_commands");
153516
+ const fileCommandsDir = join14(runnerTemp, "_runner_file_commands");
152974
153517
  for (const entry of listDir(fileCommandsDir)) {
152975
- tryUnlink(join13(fileCommandsDir, entry));
153518
+ tryUnlink(join14(fileCommandsDir, entry));
152976
153519
  }
152977
153520
  for (const entry of listDir(runnerTemp)) {
152978
153521
  if (entry.endsWith(".sh") || /^git-credentials-.*\.config$/.test(entry)) {
152979
- tryUnlink(join13(runnerTemp, entry));
153522
+ tryUnlink(join14(runnerTemp, entry));
152980
153523
  }
152981
153524
  }
152982
153525
  if (wiped.length > 0) {
@@ -153013,7 +153556,7 @@ function removeIncludeIfEntries(repoDir) {
153013
153556
  if (!key || seen.has(key)) continue;
153014
153557
  seen.add(key);
153015
153558
  try {
153016
- execFileSync7("git", ["config", "--local", "--unset-all", key], {
153559
+ execFileSync8("git", ["config", "--local", "--unset-all", key], {
153017
153560
  cwd: repoDir,
153018
153561
  stdio: "pipe",
153019
153562
  env: env2
@@ -153485,7 +154028,7 @@ function CheckoutPrTool(ctx) {
153485
154028
  headSha: ctx.toolState.checkoutSha
153486
154029
  });
153487
154030
  if (incremental) {
153488
- incrementalDiffPath = join14(
154031
+ incrementalDiffPath = join15(
153489
154032
  tempDir,
153490
154033
  `pr-${pull_number}-${beforeShort}-${headShort}-incremental.diff`
153491
154034
  );
@@ -153499,7 +154042,7 @@ function CheckoutPrTool(ctx) {
153499
154042
  const diffPreview = formatResult.content.split("\n").slice(0, 100).join("\n");
153500
154043
  log.debug(`formatted diff preview (first 100 lines):
153501
154044
  ${diffPreview}`);
153502
- const diffPath = join14(tempDir, `pr-${pull_number}-${headShort}.diff`);
154045
+ const diffPath = join15(tempDir, `pr-${pull_number}-${headShort}.diff`);
153503
154046
  writeFileSync8(diffPath, formatResult.content);
153504
154047
  log.debug(`wrote diff to ${diffPath} (${formatResult.content.length} bytes)`);
153505
154048
  ctx.toolState.diffCoverage = createDiffCoverageState({
@@ -153608,7 +154151,7 @@ ${dirty}`
153608
154151
 
153609
154152
  // mcp/checkSuite.ts
153610
154153
  import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync9 } from "node:fs";
153611
- import { join as join15 } from "node:path";
154154
+ import { join as join16 } from "node:path";
153612
154155
  var GetCheckSuiteLogs = type({
153613
154156
  check_suite_id: type.number.describe("the id from check_suite.id")
153614
154157
  });
@@ -153704,7 +154247,7 @@ function GetCheckSuiteLogsTool(ctx) {
153704
154247
  if (!tempDir) {
153705
154248
  throw new Error("PULLFROG_TEMP_DIR not set");
153706
154249
  }
153707
- const logsDir = join15(tempDir, "ci-logs");
154250
+ const logsDir = join16(tempDir, "ci-logs");
153708
154251
  mkdirSync7(logsDir, { recursive: true });
153709
154252
  const jobResults = [];
153710
154253
  for (const run4 of failedRuns) {
@@ -153731,7 +154274,7 @@ function GetCheckSuiteLogsTool(ctx) {
153731
154274
  );
153732
154275
  }
153733
154276
  const logsText = await logsResult.text();
153734
- const logPath = join15(logsDir, `job-${job.id}.log`);
154277
+ const logPath = join16(logsDir, `job-${job.id}.log`);
153735
154278
  writeFileSync9(logPath, logsText);
153736
154279
  const analysis = analyzeLog(logsText, 80);
153737
154280
  const failedSteps = job.steps?.filter((s) => s.conclusion === "failure").map((s) => `Step ${s.number}: ${s.name}`) ?? [];
@@ -153781,7 +154324,7 @@ function GetCheckSuiteLogsTool(ctx) {
153781
154324
 
153782
154325
  // mcp/commitInfo.ts
153783
154326
  import { writeFileSync as writeFileSync10 } from "node:fs";
153784
- import { join as join16 } from "node:path";
154327
+ import { join as join17 } from "node:path";
153785
154328
  var CommitInfo = type({
153786
154329
  sha: type.string.describe("the commit SHA (full or abbreviated) to fetch")
153787
154330
  });
@@ -153805,7 +154348,7 @@ function CommitInfoTool(ctx) {
153805
154348
  "PULLFROG_TEMP_DIR not set - get_commit_info must run in pullfrog action context"
153806
154349
  );
153807
154350
  }
153808
- const diffFile = join16(tempDir, `commit-${sha.slice(0, 7)}.diff`);
154351
+ const diffFile = join17(tempDir, `commit-${sha.slice(0, 7)}.diff`);
153809
154352
  writeFileSync10(diffFile, formatResult.content);
153810
154353
  log.debug(`wrote commit diff to ${diffFile} (${formatResult.content.length} bytes)`);
153811
154354
  return {
@@ -154312,7 +154855,6 @@ function buildPrBodyWithFooter(ctx, body) {
154312
154855
  triggeredBy: true,
154313
154856
  workflowRun: ctx.runId ? { owner: ctx.repo.owner, repo: ctx.repo.name, runId: ctx.runId, jobId: ctx.jobId } : void 0,
154314
154857
  model: ctx.toolState.model,
154315
- fallbackFrom: ctx.toolState.modelFallback?.from,
154316
154858
  oss: ctx.oss
154317
154859
  });
154318
154860
  const bodyWithoutFooter = stripExistingFooter(fixDoubleEscapedString(body));
@@ -154467,7 +155009,7 @@ function PullRequestInfoTool(ctx) {
154467
155009
 
154468
155010
  // mcp/reviewComments.ts
154469
155011
  import { writeFileSync as writeFileSync11 } from "node:fs";
154470
- import { join as join17 } from "node:path";
155012
+ import { join as join18 } from "node:path";
154471
155013
  var REVIEW_THREADS_QUERY = `
154472
155014
  query ($owner: String!, $name: String!, $prNumber: Int!) {
154473
155015
  repository(owner: $owner, name: $name) {
@@ -154883,7 +155425,7 @@ function GetReviewCommentsTool(ctx) {
154883
155425
  throw new Error("PULLFROG_TEMP_DIR not set");
154884
155426
  }
154885
155427
  const filename = `review-${params.review_id}-threads.md`;
154886
- const commentsPath = join17(tempDir, filename);
155428
+ const commentsPath = join18(tempDir, filename);
154887
155429
  writeFileSync11(commentsPath, formatted.content);
154888
155430
  log.debug(`wrote ${threadBlocks.length} threads to ${commentsPath}`);
154889
155431
  return {
@@ -155121,7 +155663,7 @@ import { spawn as spawn4, spawnSync as spawnSync5 } from "node:child_process";
155121
155663
  import { randomUUID as randomUUID4 } from "node:crypto";
155122
155664
  import { closeSync, openSync, writeFileSync as writeFileSync12 } from "node:fs";
155123
155665
  import { userInfo as userInfo2 } from "node:os";
155124
- import { join as join18 } from "node:path";
155666
+ import { join as join19 } from "node:path";
155125
155667
  import { setTimeout as sleep2 } from "node:timers/promises";
155126
155668
  var ShellParams = type({
155127
155669
  command: "string",
@@ -155284,7 +155826,7 @@ function getTempDir() {
155284
155826
  var MAX_OUTPUT_CHARS = 5e3;
155285
155827
  function capOutput(output) {
155286
155828
  if (output.length <= MAX_OUTPUT_CHARS) return output;
155287
- const fullPath = join18(getTempDir(), `shell-${randomUUID4().slice(0, 8)}.log`);
155829
+ const fullPath = join19(getTempDir(), `shell-${randomUUID4().slice(0, 8)}.log`);
155288
155830
  writeFileSync12(fullPath, output);
155289
155831
  const elided = output.length - MAX_OUTPUT_CHARS;
155290
155832
  return `... [${elided} chars truncated; full output saved to ${fullPath}] ...
@@ -155339,8 +155881,8 @@ Do NOT use this tool for git commands \u2014 use the dedicated git tools instead
155339
155881
  if (params.background) {
155340
155882
  const tempDir = getTempDir();
155341
155883
  const handle = `bg-${randomUUID4().slice(0, 8)}`;
155342
- const outputPath = join18(tempDir, `${handle}.log`);
155343
- const pidPath = join18(tempDir, `${handle}.pid`);
155884
+ const outputPath = join19(tempDir, `${handle}.log`);
155885
+ const pidPath = join19(tempDir, `${handle}.pid`);
155344
155886
  const logFd = openSync(outputPath, "a");
155345
155887
  let proc2;
155346
155888
  try {
@@ -155571,7 +156113,7 @@ function buildCommonTools(ctx, outputSchema) {
155571
156113
  return tools;
155572
156114
  }
155573
156115
  function buildOrchestratorTools(ctx, outputSchema) {
155574
- return [
156116
+ const tools = [
155575
156117
  ...buildCommonTools(ctx, outputSchema),
155576
156118
  ReportProgressTool(ctx),
155577
156119
  SelectModeTool(ctx),
@@ -155581,6 +156123,10 @@ function buildOrchestratorTools(ctx, outputSchema) {
155581
156123
  CreatePullRequestTool(ctx),
155582
156124
  UpdatePullRequestBodyTool(ctx)
155583
156125
  ];
156126
+ if (ctx.signedCommits) {
156127
+ tools.push(CommitChangesTool(ctx));
156128
+ }
156129
+ return tools;
155584
156130
  }
155585
156131
  async function tryStartMcpServer(ctx, tools, port) {
155586
156132
  const server = new FastMCP({ name: pullfrogMcpName, version: "0.0.1" });
@@ -155771,8 +156317,14 @@ var MISSING_KEY_MARKER = "no API key found";
155771
156317
  function buildMissingApiKeyError(params) {
155772
156318
  const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
155773
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.`;
155774
156324
  return [
155775
- `**${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).",
155776
156328
  "",
155777
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)`
155778
156330
  ].join("\n");
@@ -155852,10 +156404,14 @@ function validateAgentApiKey(params) {
155852
156404
  }
155853
156405
  if (params.agent.name === "opencode") {
155854
156406
  if (params.authorized.has(params.model)) return;
155855
- 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
+ );
155856
156410
  }
155857
156411
  if (hasEnvVar3("ANTHROPIC_API_KEY") || hasEnvVar3("CLAUDE_CODE_OAUTH_TOKEN")) return;
155858
- 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
+ );
155859
156415
  }
155860
156416
  if (params.agent.name === "opencode") {
155861
156417
  if (params.authorized.size > 0) return;
@@ -155866,42 +156422,37 @@ function validateAgentApiKey(params) {
155866
156422
  }
155867
156423
  function isApiKeyAuthError(text) {
155868
156424
  if (!text) return false;
155869
- 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);
155870
156429
  }
155871
156430
  function formatApiKeyErrorSummary(params) {
155872
156431
  if (params.raw.includes(MISSING_KEY_MARKER)) {
156432
+ if (params.raw.startsWith(`**${MISSING_KEY_MARKER}**`)) return params.raw;
155873
156433
  return buildMissingApiKeyError({ owner: params.owner, name: params.name });
155874
156434
  }
155875
156435
  const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
155876
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
+ }
155877
156444
  return [
155878
- `**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.`,
155879
156446
  "",
155880
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)`
155881
156448
  ].join("\n");
155882
156449
  }
155883
156450
 
155884
- // utils/byokFallback.ts
155885
- var FREE_FALLBACK_SLUG = "opencode/big-pickle";
155886
- function selectFallbackModelIfNeeded(input) {
155887
- if (input.proxyModel) return { fallback: false };
155888
- if (!input.resolvedModel) return { fallback: false };
155889
- if (input.resolvedModel === FREE_FALLBACK_SLUG) return { fallback: false };
155890
- if (!input.resolvedModel.includes("/")) return { fallback: false };
155891
- if (input.agentName === "claude") return { fallback: false };
155892
- if (input.authorized.has(input.resolvedModel)) return { fallback: false };
155893
- return {
155894
- fallback: true,
155895
- from: input.resolvedModel,
155896
- to: FREE_FALLBACK_SLUG
155897
- };
155898
- }
155899
-
155900
156451
  // utils/gitAuthServer.ts
155901
156452
  import { randomUUID as randomUUID5 } from "node:crypto";
155902
156453
  import { writeFileSync as writeFileSync13 } from "node:fs";
155903
156454
  import { createServer as createServer3 } from "node:http";
155904
- import { join as join19 } from "node:path";
156455
+ import { join as join20 } from "node:path";
155905
156456
  var REVOKED_TRAP_MS = 6e4;
155906
156457
  function revokeGitHubToken(token) {
155907
156458
  fetch("https://api.github.com/installation/token", {
@@ -155970,7 +156521,7 @@ async function startGitAuthServer(tmpdir4) {
155970
156521
  function writeAskpassScript(code) {
155971
156522
  const scriptId = randomUUID5();
155972
156523
  const scriptName = `askpass-${scriptId}.js`;
155973
- const scriptPath = join19(tmpdir4, scriptName);
156524
+ const scriptPath = join20(tmpdir4, scriptName);
155974
156525
  const content = [
155975
156526
  `#!/usr/bin/env node`,
155976
156527
  `var a=process.argv[2]||"";`,
@@ -156008,7 +156559,7 @@ async function startGitAuthServer(tmpdir4) {
156008
156559
  var core3 = __toESM(require_core(), 1);
156009
156560
  import { createSign } from "node:crypto";
156010
156561
  import { rename, writeFile } from "node:fs/promises";
156011
- import { dirname as dirname3, join as join20 } from "node:path";
156562
+ import { dirname as dirname3, join as join21 } from "node:path";
156012
156563
 
156013
156564
  // node_modules/.pnpm/@octokit+plugin-throttling@11.0.3_@octokit+core@7.0.5/node_modules/@octokit/plugin-throttling/dist-bundle/index.js
156014
156565
  var import_light = __toESM(require_light(), 1);
@@ -159650,6 +160201,7 @@ var Octokit2 = Octokit.plugin(requestLog, legacyRestEndpointMethods, paginateRes
159650
160201
  );
159651
160202
 
159652
160203
  // utils/github.ts
160204
+ var OIDC_AUDIENCE = "pullfrog-api";
159653
160205
  function isObject4(value2) {
159654
160206
  return typeof value2 === "object" && value2 !== null;
159655
160207
  }
@@ -159666,8 +160218,39 @@ var TokenExchangeError = class extends Error {
159666
160218
  this.status = status;
159667
160219
  }
159668
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
+ }
159669
160252
  async function acquireTokenViaOIDC(opts) {
159670
- const oidcToken = await core3.getIDToken("pullfrog-api");
160253
+ const oidcToken = opts?.oidc ? await fetchIdTokenFromStash(opts.oidc) : await core3.getIDToken(OIDC_AUDIENCE);
159671
160254
  const repos = [...opts?.repos ?? []];
159672
160255
  const targetRepo = process.env.GITHUB_REPOSITORY?.split("/")[1];
159673
160256
  if (targetRepo) {
@@ -159816,14 +160399,15 @@ async function acquireTokenViaGitHubApp(opts) {
159816
160399
  const installationId = await findInstallationId(jwt2, config3.repoOwner, config3.repoName);
159817
160400
  return await createInstallationToken(jwt2, installationId, opts?.permissions);
159818
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
+ }
159819
160406
  async function acquireNewToken(opts) {
159820
- if (isOIDCAvailable()) {
160407
+ if (opts?.oidc || isOIDCAvailable()) {
159821
160408
  return await retry(() => acquireTokenViaOIDC(opts), {
159822
160409
  label: "token exchange",
159823
- shouldRetry: (error49) => {
159824
- if (error49 instanceof TokenExchangeError) return error49.status >= 500 || error49.status === 429;
159825
- return error49 instanceof Error && (error49.message.includes("timed out") || error49.message.includes("fetch failed") || error49.message.includes("ECONNRESET") || error49.message.includes("ETIMEDOUT"));
159826
- }
160410
+ shouldRetry: isTransientTokenError
159827
160411
  });
159828
160412
  }
159829
160413
  if (process.env.GITHUB_ACTIONS === "true") {
@@ -159866,14 +160450,14 @@ function getGitHubUsageSummary() {
159866
160450
  }
159867
160451
  async function writeGitHubUsageSummaryToFile(path4) {
159868
160452
  const summary2 = getGitHubUsageSummary();
159869
- const tmpPath = join20(dirname3(path4), `.usage-summary-${process.pid}.tmp`);
160453
+ const tmpPath = join21(dirname3(path4), `.usage-summary-${process.pid}.tmp`);
159870
160454
  await writeFile(tmpPath, JSON.stringify(summary2));
159871
160455
  await rename(tmpPath, path4);
159872
160456
  }
159873
- function createOctokit(token) {
160457
+ function createOctokit(token, refreshAuth) {
160458
+ let currentToken = token;
159874
160459
  const OctokitWithPlugins = Octokit2.plugin(throttling);
159875
160460
  const octokit = new OctokitWithPlugins({
159876
- auth: token,
159877
160461
  throttle: {
159878
160462
  onRateLimit: (_retryAfter, _options, _octokit, retryCount) => {
159879
160463
  return retryCount <= 2;
@@ -159902,6 +160486,8 @@ function createOctokit(token) {
159902
160486
  return response;
159903
160487
  };
159904
160488
  octokit.hook.wrap("request", async (request2, options) => {
160489
+ const sentToken = currentToken;
160490
+ options.headers.authorization = `token ${sentToken}`;
159905
160491
  try {
159906
160492
  const response = await request2(options);
159907
160493
  onResponse(response);
@@ -159910,6 +160496,13 @@ function createOctokit(token) {
159910
160496
  if (isObject4(error49) && "response" in error49 && isObject4(error49.response) && "headers" in error49.response && isObject4(error49.response.headers)) {
159911
160497
  onResponse(error49.response);
159912
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
+ }
159913
160506
  throw error49;
159914
160507
  }
159915
160508
  });
@@ -160108,7 +160701,17 @@ Use \`${t2("git")}\` for local git commands (status, log, add, commit, checkout,
160108
160701
  - \`${t2("checkout_pr")}\` - checkout a PR branch (fetches and configures push for forks)
160109
160702
  - \`${t2("delete_branch")}\` - delete a remote branch (requires push: enabled)
160110
160703
  - \`${t2("push_tags")}\` - push tags (requires push: enabled)
160111
-
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
+ ` : ""}
160112
160715
  Rules:
160113
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.
160114
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.
@@ -160284,7 +160887,7 @@ function resolveInstructions(ctx) {
160284
160887
 
160285
160888
  // utils/learnings.ts
160286
160889
  import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
160287
- import { dirname as dirname4, join as join21 } from "node:path";
160890
+ import { dirname as dirname4, join as join22 } from "node:path";
160288
160891
 
160289
160892
  // utils/learningsTruncate.ts
160290
160893
  var MAX_LEARNINGS_LENGTH = 1e5;
@@ -160301,7 +160904,7 @@ function truncateAtLineBoundary(body, cap) {
160301
160904
  // utils/learnings.ts
160302
160905
  var LEARNINGS_FILE_NAME = "pullfrog-learnings.md";
160303
160906
  function learningsFilePath(tmpdir4) {
160304
- return join21(tmpdir4, LEARNINGS_FILE_NAME);
160907
+ return join22(tmpdir4, LEARNINGS_FILE_NAME);
160305
160908
  }
160306
160909
  async function seedLearningsFile(params) {
160307
160910
  const path4 = learningsFilePath(params.tmpdir);
@@ -160721,6 +161324,10 @@ function formatTransientErrorSummary(error49, owner) {
160721
161324
  var core7 = __toESM(require_core(), 1);
160722
161325
  import assert2 from "node:assert/strict";
160723
161326
  var mcpTokenValue;
161327
+ var refreshMcpTokenFn;
161328
+ function getMcpTokenRefresh() {
161329
+ return refreshMcpTokenFn;
161330
+ }
160724
161331
  function getJobToken() {
160725
161332
  const inputToken = core7.getInput("token");
160726
161333
  if (inputToken) {
@@ -160772,6 +161379,29 @@ async function resolveTokens(params) {
160772
161379
  `\xBB acquired scoped MCP token (${Object.entries(mcpPermissions).map((e) => e.join(":")).join(", ")})`
160773
161380
  );
160774
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
+ };
160775
161405
  let disposingRef;
160776
161406
  const dispose = async () => {
160777
161407
  if (disposingRef) {
@@ -160780,9 +161410,10 @@ async function resolveTokens(params) {
160780
161410
  disposingRef = Promise.withResolvers();
160781
161411
  try {
160782
161412
  mcpTokenValue = void 0;
161413
+ refreshMcpTokenFn = void 0;
160783
161414
  await Promise.all([
160784
161415
  revokeGitHubInstallationToken(gitToken),
160785
- revokeGitHubInstallationToken(mcpToken)
161416
+ revokeGitHubInstallationToken(currentMcpToken)
160786
161417
  ]);
160787
161418
  } finally {
160788
161419
  removeSignalHandler();
@@ -160826,7 +161457,7 @@ async function reportErrorToComment(ctx) {
160826
161457
 
160827
161458
  ${ctx.error}` : ctx.error;
160828
161459
  const repoContext = parseRepoContext();
160829
- const octokit = createOctokit(getGitHubInstallationToken());
161460
+ const octokit = createOctokit(getGitHubInstallationToken(), getMcpTokenRefresh());
160830
161461
  const runId = process.env.GITHUB_RUN_ID ? Number.parseInt(process.env.GITHUB_RUN_ID, 10) : void 0;
160831
161462
  const customParts = [];
160832
161463
  if (runId) {
@@ -160840,7 +161471,6 @@ ${ctx.error}` : ctx.error;
160840
161471
  workflowRun: runId ? { owner: repoContext.owner, repo: repoContext.name, runId } : void 0,
160841
161472
  customParts,
160842
161473
  model: ctx.toolState.model,
160843
- fallbackFrom: ctx.toolState.modelFallback?.from,
160844
161474
  oss: ctx.toolState.oss
160845
161475
  });
160846
161476
  const body = `${formattedError}${footer}`;
@@ -160907,18 +161537,15 @@ async function mintProxyKey(ctx) {
160907
161537
  if (error49 instanceof TransientError) throw error49;
160908
161538
  log.warning(`proxy key mint error: ${error49 instanceof Error ? error49.message : String(error49)}`);
160909
161539
  return null;
160910
- } finally {
160911
- delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
160912
- delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
160913
161540
  }
160914
161541
  }
160915
161542
  async function buildProxyTokenHeaders(ctx) {
160916
161543
  if (ctx.oidcCredentials) {
160917
- process.env.ACTIONS_ID_TOKEN_REQUEST_URL = ctx.oidcCredentials.requestUrl;
160918
- process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = ctx.oidcCredentials.requestToken;
160919
- const oidcToken = await core8.getIDToken("pullfrog-api");
160920
- delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
160921
- 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
+ });
160922
161549
  return { Authorization: `Bearer ${oidcToken}` };
160923
161550
  }
160924
161551
  if (isLocalApiUrl()) {
@@ -160982,7 +161609,7 @@ async function runProxyResolution(ctx) {
160982
161609
 
160983
161610
  // utils/prSummary.ts
160984
161611
  import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
160985
- import { dirname as dirname5, join as join22 } from "node:path";
161612
+ import { dirname as dirname5, join as join23 } from "node:path";
160986
161613
  var SUMMARY_FILE_NAME = "pullfrog-summary.md";
160987
161614
  var SUMMARY_SCAFFOLD = `# PR summary
160988
161615
 
@@ -160992,7 +161619,7 @@ var SUMMARY_SCAFFOLD = `# PR summary
160992
161619
  var MIN_SNAPSHOT_LENGTH = 60;
160993
161620
  var MAX_SNAPSHOT_LENGTH = 32768;
160994
161621
  function summaryFilePath(tmpdir4) {
160995
- return join22(tmpdir4, SUMMARY_FILE_NAME);
161622
+ return join23(tmpdir4, SUMMARY_FILE_NAME);
160996
161623
  }
160997
161624
  async function seedSummaryFile(params) {
160998
161625
  const path4 = summaryFilePath(params.tmpdir);
@@ -161120,6 +161747,7 @@ var defaultSettings = {
161120
161747
  push: "restricted",
161121
161748
  shell: "restricted",
161122
161749
  prApproveEnabled: false,
161750
+ signedCommits: false,
161123
161751
  modeInstructions: {},
161124
161752
  learnings: null,
161125
161753
  learningsHeadings: [],
@@ -161352,7 +161980,7 @@ ${input.errorMessage}
161352
161980
  ].join("\n");
161353
161981
  }
161354
161982
  function formatProviderModelNotFoundSummary(input) {
161355
- 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.
161356
161984
 
161357
161985
  \`\`\`
161358
161986
  ${input.raw}
@@ -161793,7 +162421,7 @@ async function main() {
161793
162421
  const initialOctokit = createOctokit(jobToken);
161794
162422
  const runContext = await resolveRunContextData({ octokit: initialOctokit, token: jobToken });
161795
162423
  timer.checkpoint("runContextData");
161796
- const tmpdir4 = createTempDirectory();
162424
+ createTempDirectory();
161797
162425
  const opencodeCliPath = await agents.opencode.install();
161798
162426
  captureBaselineModels(opencodeCliPath);
161799
162427
  if (runContext.dbSecrets) {
@@ -161817,12 +162445,12 @@ async function main() {
161817
162445
  if (payload.event.trigger === "pull_request_synchronize") {
161818
162446
  toolState.beforeSha = payload.event.before_sha;
161819
162447
  }
161820
- const tokenRef = __using(_stack2, await resolveTokens({ push: payload.push }), true);
161821
- wipeRunnerLeakSurface();
161822
162448
  const oidcCredentials = process.env.ACTIONS_ID_TOKEN_REQUEST_URL && process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN ? {
161823
162449
  requestUrl: process.env.ACTIONS_ID_TOKEN_REQUEST_URL,
161824
162450
  requestToken: process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN
161825
162451
  } : null;
162452
+ const tokenRef = __using(_stack2, await resolveTokens({ push: payload.push, oidc: oidcCredentials }), true);
162453
+ wipeRunnerLeakSurface();
161826
162454
  if (payload.shell !== "enabled") {
161827
162455
  delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
161828
162456
  delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
@@ -161835,7 +162463,7 @@ async function main() {
161835
162463
  repo: runContext.repo,
161836
162464
  toolState
161837
162465
  });
161838
- const octokit = createOctokit(tokenRef.mcpToken);
162466
+ const octokit = createOctokit(tokenRef.mcpToken, getMcpTokenRefresh());
161839
162467
  const runInfo = await resolveRun({ octokit });
161840
162468
  let toolContext;
161841
162469
  let progressCallbackDisabled = false;
@@ -161847,13 +162475,13 @@ async function main() {
161847
162475
  if (payload.cwd && process.cwd() !== payload.cwd) {
161848
162476
  process.chdir(payload.cwd);
161849
162477
  }
161850
- const tmpdir5 = createTempDirectory();
162478
+ const tmpdir4 = createTempDirectory();
161851
162479
  const originalBody = payload.event.body;
161852
162480
  const resolvedBody = await resolveBody({
161853
162481
  event: payload.event,
161854
162482
  octokit,
161855
162483
  repo: runContext.repo,
161856
- tmpdir: tmpdir5,
162484
+ tmpdir: tmpdir4,
161857
162485
  githubToken: tokenRef.mcpToken
161858
162486
  });
161859
162487
  if (resolvedBody !== originalBody) {
@@ -161862,33 +162490,18 @@ async function main() {
161862
162490
  payload.prompt = payload.prompt.replace(originalBody, resolvedBody ?? "");
161863
162491
  }
161864
162492
  }
161865
- const gitAuthServer = __using(_stack, await startGitAuthServer(tmpdir5), true);
162493
+ const gitAuthServer = __using(_stack, await startGitAuthServer(tmpdir4), true);
161866
162494
  setGitAuthServer(gitAuthServer);
161867
- const initialResolvedModel = payload.proxyModel ? void 0 : resolveModel({ slug: payload.model });
161868
- const authorized2 = getAuthorizedModels();
161869
- const fallback = selectFallbackModelIfNeeded({
161870
- resolvedModel: initialResolvedModel,
161871
- proxyModel: payload.proxyModel,
161872
- authorized: authorized2,
161873
- agentName: resolveAgent({ model: initialResolvedModel }).name
161874
- });
161875
- const effectiveSlug = fallback.fallback ? fallback.to : payload.model;
161876
- const resolvedModel = fallback.fallback ? fallback.to : initialResolvedModel;
161877
- if (fallback.fallback) {
161878
- log.warning(
161879
- `\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.`
161880
- );
161881
- toolState.modelFallback = { from: fallback.from };
161882
- }
162495
+ const resolvedModel = payload.proxyModel ? void 0 : resolveModel({ slug: payload.model });
161883
162496
  vertexCredentials = materializeVertexCredentials({ model: resolvedModel });
161884
162497
  const agent2 = resolveAgent({ model: resolvedModel });
161885
- const effectiveModel = payload.proxyModel ?? resolvedModel ?? effectiveSlug;
162498
+ const effectiveModel = payload.proxyModel ?? resolvedModel ?? payload.model;
161886
162499
  toolState.model = effectiveModel;
161887
- if (!fallback.fallback && !payload.proxyModel) {
162500
+ if (!payload.proxyModel) {
161888
162501
  validateAgentApiKey({
161889
162502
  agent: agent2,
161890
162503
  model: effectiveModel,
161891
- authorized: authorized2,
162504
+ authorized: getAuthorizedModels(),
161892
162505
  owner: runContext.repo.owner,
161893
162506
  name: runContext.repo.name
161894
162507
  });
@@ -161905,7 +162518,7 @@ async function main() {
161905
162518
  timer.checkpoint("git");
161906
162519
  const pmSpec = await resolvePackageManagerSpec(process.cwd());
161907
162520
  if (pmSpec) {
161908
- await ensurePackageManager({ spec: pmSpec, binDir: packageManagerBinDir(tmpdir5) });
162521
+ await ensurePackageManager({ spec: pmSpec, binDir: packageManagerBinDir(tmpdir4) });
161909
162522
  }
161910
162523
  timer.checkpoint("packageManager");
161911
162524
  const setupHook = await executeLifecycleHook({
@@ -161918,26 +162531,34 @@ async function main() {
161918
162531
  }
161919
162532
  timer.checkpoint("lifecycleHooks::setup");
161920
162533
  const agentId = agent2.name;
161921
- const modes2 = [...computeModes(agentId), ...runContext.repoSettings.modes];
162534
+ const modes2 = [
162535
+ ...computeModes(agentId, runContext.repoSettings.signedCommits),
162536
+ ...runContext.repoSettings.modes
162537
+ ];
161922
162538
  const outputSchema = resolveOutputSchema();
161923
162539
  toolContext = {
161924
162540
  agentId,
161925
162541
  repo: runContext.repo,
161926
162542
  payload,
161927
162543
  octokit,
161928
- 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
+ },
161929
162549
  gitToken: tokenRef.gitToken,
161930
162550
  apiToken: runContext.apiToken,
161931
162551
  modes: modes2,
161932
162552
  postCheckoutScript: runContext.repoSettings.postCheckoutScript,
161933
162553
  prepushScript: runContext.repoSettings.prepushScript,
161934
162554
  prApproveEnabled: runContext.repoSettings.prApproveEnabled,
162555
+ signedCommits: runContext.repoSettings.signedCommits,
161935
162556
  modeInstructions: runContext.repoSettings.modeInstructions,
161936
162557
  toolState,
161937
162558
  runId: runInfo.runId,
161938
162559
  jobId: runInfo.jobId,
161939
162560
  mcpServerUrl: "",
161940
- tmpdir: tmpdir5,
162561
+ tmpdir: tmpdir4,
161941
162562
  oss: runContext.oss,
161942
162563
  plan: runContext.plan,
161943
162564
  resolvedModel
@@ -161948,7 +162569,7 @@ async function main() {
161948
162569
  timer.checkpoint("mcpServer");
161949
162570
  try {
161950
162571
  const learningsPath = await seedLearningsFile({
161951
- tmpdir: tmpdir5,
162572
+ tmpdir: tmpdir4,
161952
162573
  current: runContext.repoSettings.learnings
161953
162574
  });
161954
162575
  toolState.learningsFilePath = learningsPath;
@@ -161965,7 +162586,7 @@ async function main() {
161965
162586
  }
161966
162587
  if (payload.generateSummary && payload.event.is_pr && payload.event.issue_number) {
161967
162588
  const previousSnapshot = await fetchPreviousSnapshot(toolContext, payload.event.issue_number);
161968
- const filePath = await seedSummaryFile({ tmpdir: tmpdir5, previousSnapshot });
162589
+ const filePath = await seedSummaryFile({ tmpdir: tmpdir4, previousSnapshot });
161969
162590
  toolState.summaryFilePath = filePath;
161970
162591
  try {
161971
162592
  toolState.summarySeed = await readFile5(filePath, "utf8");
@@ -161985,6 +162606,7 @@ async function main() {
161985
162606
  modes: modes2,
161986
162607
  agentId,
161987
162608
  outputSchema,
162609
+ signedCommits: runContext.repoSettings.signedCommits,
161988
162610
  learningsFilePath: toolState.learningsFilePath ?? null,
161989
162611
  learningsHeadings: runContext.repoSettings.learningsHeadings,
161990
162612
  setupHookFailure: describeSetupFailure(setupHook.failure)
@@ -162003,7 +162625,7 @@ ${instructions.user}` : null,
162003
162625
  log.info(instructions.full);
162004
162626
  });
162005
162627
  if (agentId === "opencode") {
162006
- const pluginDir = join23(process.cwd(), ".opencode", "plugin");
162628
+ const pluginDir = join24(process.cwd(), ".opencode", "plugin");
162007
162629
  const hasPlugins = existsSync8(pluginDir) && readdirSync2(pluginDir).some((f) => /\.[jt]sx?$/.test(f));
162008
162630
  if (hasPlugins && toolState.dependencyInstallation?.promise) {
162009
162631
  log.info(
@@ -162058,7 +162680,7 @@ ${instructions.user}` : null,
162058
162680
  payload,
162059
162681
  resolvedModel,
162060
162682
  mcpServerUrl: mcpHttpServer.url,
162061
- tmpdir: tmpdir5,
162683
+ tmpdir: tmpdir4,
162062
162684
  // PULLFROG_DATA_DIR (/var/lib/pullfrog) holds codex auth.json + any
162063
162685
  // future pullfrog-managed on-disk secrets. bash via MCP tmpfs-overlays
162064
162686
  // it; agent native FS tools deny it via the same secretDenyPaths plumbing
@@ -162334,7 +162956,7 @@ async function run(args2) {
162334
162956
  }
162335
162957
 
162336
162958
  // commands/init.ts
162337
- import { execFileSync as execFileSync8 } from "node:child_process";
162959
+ import { execFileSync as execFileSync9 } from "node:child_process";
162338
162960
  var import_arg3 = __toESM(require_arg(), 1);
162339
162961
  var import_picocolors3 = __toESM(require_picocolors(), 1);
162340
162962
  var PULLFROG_API_URL2 = (process.env.PULLFROG_API_URL || "https://pullfrog.com").replace(
@@ -162394,7 +163016,7 @@ function handleCancel2(value2) {
162394
163016
  function getGhToken2() {
162395
163017
  let token;
162396
163018
  try {
162397
- token = execFileSync8("gh", ["auth", "token"], { encoding: "utf-8" }).trim();
163019
+ token = execFileSync9("gh", ["auth", "token"], { encoding: "utf-8" }).trim();
162398
163020
  } catch {
162399
163021
  bail2(
162400
163022
  `gh cli not found or not authenticated.
@@ -162437,7 +163059,7 @@ async function ghApi(path4, token) {
162437
163059
  function parseGitRemote2() {
162438
163060
  let url4;
162439
163061
  try {
162440
- url4 = execFileSync8("git", ["remote", "get-url", "origin"], { encoding: "utf-8" }).trim();
163062
+ url4 = execFileSync9("git", ["remote", "get-url", "origin"], { encoding: "utf-8" }).trim();
162441
163063
  } catch {
162442
163064
  bail2("not a git repository or no 'origin' remote found.");
162443
163065
  }
@@ -162448,10 +163070,10 @@ function parseGitRemote2() {
162448
163070
  function openBrowser(url4) {
162449
163071
  try {
162450
163072
  const platform = process.platform;
162451
- if (platform === "darwin") execFileSync8("open", [url4], { stdio: "ignore" });
163073
+ if (platform === "darwin") execFileSync9("open", [url4], { stdio: "ignore" });
162452
163074
  else if (platform === "win32")
162453
- execFileSync8("cmd", ["/c", "start", "", url4], { stdio: "ignore" });
162454
- else execFileSync8("xdg-open", [url4], { stdio: "ignore" });
163075
+ execFileSync9("cmd", ["/c", "start", "", url4], { stdio: "ignore" });
163076
+ else execFileSync9("xdg-open", [url4], { stdio: "ignore" });
162455
163077
  } catch {
162456
163078
  }
162457
163079
  }
@@ -162678,7 +163300,7 @@ function setGhSecret(ctx) {
162678
163300
  let orgFailed = false;
162679
163301
  if (ctx.org) {
162680
163302
  try {
162681
- execFileSync8("gh", ["secret", "set", ctx.name, "--org", ctx.org, "--visibility", "all"], {
163303
+ execFileSync9("gh", ["secret", "set", ctx.name, "--org", ctx.org, "--visibility", "all"], {
162682
163304
  input: ctx.value,
162683
163305
  stdio: ["pipe", "ignore", "pipe"],
162684
163306
  encoding: "utf-8"
@@ -162689,7 +163311,7 @@ function setGhSecret(ctx) {
162689
163311
  }
162690
163312
  }
162691
163313
  try {
162692
- execFileSync8("gh", ["secret", "set", ctx.name, "--repo", ctx.repoSlug], {
163314
+ execFileSync9("gh", ["secret", "set", ctx.name, "--repo", ctx.repoSlug], {
162693
163315
  input: ctx.value,
162694
163316
  stdio: ["pipe", "ignore", "pipe"],
162695
163317
  encoding: "utf-8"
@@ -163061,7 +163683,7 @@ async function run2() {
163061
163683
  }
163062
163684
 
163063
163685
  // cli.ts
163064
- var VERSION10 = "0.1.29";
163686
+ var VERSION10 = "0.1.30";
163065
163687
  var bin = basename2(process.argv[1] || "");
163066
163688
  var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
163067
163689
  var rawArgs = process.argv.slice(2);