pullfrog 0.1.28 → 0.1.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/subagentToolGates.d.ts +1 -1
- package/dist/cli.mjs +847 -210
- package/dist/index.js +838 -201
- package/dist/internal.js +30 -31
- package/dist/mcp/git.d.ts +7 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/modes.d.ts +1 -1
- package/dist/toolState.d.ts +0 -3
- package/dist/utils/apiCommit.d.ts +40 -0
- package/dist/utils/apiKeys.d.ts +15 -0
- package/dist/utils/buildPullfrogFooter.d.ts +0 -7
- package/dist/utils/claudeSubscription.d.ts +30 -0
- package/dist/utils/github.d.ts +27 -1
- package/dist/utils/instructions.d.ts +3 -0
- package/dist/utils/openCodeModels.d.ts +2 -2
- package/dist/utils/proxy.d.ts +3 -5
- package/dist/utils/runContext.d.ts +1 -0
- package/dist/utils/runErrorRenderer.d.ts +2 -2
- package/dist/utils/token.d.ts +12 -1
- package/package.json +1 -1
- package/dist/utils/byokFallback.d.ts +0 -44
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
|
|
19994
|
+
function setSecret7(secret) {
|
|
19995
19995
|
(0, command_1.issueCommand)("add-mask", {}, secret);
|
|
19996
19996
|
}
|
|
19997
|
-
exports.setSecret =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
-
|
|
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
|
-
-
|
|
102891
|
+
- ${commitStep}
|
|
102817
102892
|
|
|
102818
102893
|
6. Finalize. Reply + resolve are paired write actions: do BOTH or NEITHER for each thread.
|
|
102819
|
-
-
|
|
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
|
|
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
|
-
-
|
|
103171
|
+
- ${commitStep}
|
|
103097
103172
|
|
|
103098
103173
|
6. Finalize:
|
|
103099
|
-
-
|
|
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
|
|
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
|
-
-
|
|
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,
|
|
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
|
-
|
|
104127
|
-
|
|
104128
|
-
|
|
104129
|
-
|
|
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
|
-
|
|
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
|
-
|
|
119065
|
-
|
|
119066
|
-
|
|
119067
|
-
|
|
119068
|
-
|
|
119069
|
-
|
|
119070
|
-
|
|
119071
|
-
|
|
119072
|
-
|
|
119073
|
-
|
|
119074
|
-
|
|
119075
|
-
|
|
119076
|
-
|
|
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
|
|
151368
|
+
import { join as join15 } from "node:path";
|
|
151261
151369
|
|
|
151262
151370
|
// utils/diffCoverage.ts
|
|
151263
151371
|
import { isAbsolute, normalize as normalize2, resolve } from "node:path";
|
|
@@ -151527,6 +151635,7 @@ function readNumber(params) {
|
|
|
151527
151635
|
import { execSync } from "node:child_process";
|
|
151528
151636
|
import { createHash } from "node:crypto";
|
|
151529
151637
|
import { readFileSync as readFileSync3, realpathSync, unlinkSync } from "node:fs";
|
|
151638
|
+
import { join as join11 } from "node:path";
|
|
151530
151639
|
|
|
151531
151640
|
// utils/shell.ts
|
|
151532
151641
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
@@ -151596,6 +151705,18 @@ function verifyGitBinary() {
|
|
|
151596
151705
|
}
|
|
151597
151706
|
return gitBinary.path;
|
|
151598
151707
|
}
|
|
151708
|
+
var hooksDirCache = /* @__PURE__ */ new Map();
|
|
151709
|
+
function resolveHooksDir(cwd, gitPath) {
|
|
151710
|
+
const cached4 = hooksDirCache.get(cwd);
|
|
151711
|
+
if (cached4) return cached4;
|
|
151712
|
+
const commonDir = $2(gitPath, ["rev-parse", "--path-format=absolute", "--git-common-dir"], {
|
|
151713
|
+
cwd,
|
|
151714
|
+
log: false
|
|
151715
|
+
}).trim();
|
|
151716
|
+
const hooksDir = join11(commonDir, "hooks");
|
|
151717
|
+
hooksDirCache.set(cwd, hooksDir);
|
|
151718
|
+
return hooksDir;
|
|
151719
|
+
}
|
|
151599
151720
|
var authServer;
|
|
151600
151721
|
function setGitAuthServer(server) {
|
|
151601
151722
|
authServer = server;
|
|
@@ -151617,6 +151738,8 @@ async function $git(subcommand, args2, options) {
|
|
|
151617
151738
|
"protocol.file.allow=never",
|
|
151618
151739
|
"-c",
|
|
151619
151740
|
"core.sshCommand=ssh",
|
|
151741
|
+
"-c",
|
|
151742
|
+
`core.hooksPath=${resolveHooksDir(cwd, gitPath)}`,
|
|
151620
151743
|
subcommand,
|
|
151621
151744
|
...args2
|
|
151622
151745
|
];
|
|
@@ -151886,7 +152009,274 @@ function postProcessRangeDiff(raw2, contextLines = 3) {
|
|
|
151886
152009
|
// mcp/git.ts
|
|
151887
152010
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
151888
152011
|
import { writeFileSync as writeFileSync7 } from "node:fs";
|
|
151889
|
-
import { join as
|
|
152012
|
+
import { join as join13 } from "node:path";
|
|
152013
|
+
|
|
152014
|
+
// utils/apiCommit.ts
|
|
152015
|
+
import { execFileSync as execFileSync7 } from "node:child_process";
|
|
152016
|
+
import { lstat, readlink } from "node:fs/promises";
|
|
152017
|
+
import { join as join12 } from "node:path";
|
|
152018
|
+
var GITHUB_API = "https://api.github.com";
|
|
152019
|
+
var MAX_BLOB_BYTES = 30 * 1024 * 1024;
|
|
152020
|
+
var BLOB_UPLOAD_CONCURRENCY = 8;
|
|
152021
|
+
function getRepoRoot() {
|
|
152022
|
+
return $2("git", ["rev-parse", "--show-toplevel"], { log: false }).trim();
|
|
152023
|
+
}
|
|
152024
|
+
async function gh(params) {
|
|
152025
|
+
const response = await fetch(`${GITHUB_API}${params.path}`, {
|
|
152026
|
+
method: params.method,
|
|
152027
|
+
headers: {
|
|
152028
|
+
Accept: "application/vnd.github+json",
|
|
152029
|
+
Authorization: `Bearer ${params.token}`,
|
|
152030
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
152031
|
+
"Content-Type": "application/json"
|
|
152032
|
+
},
|
|
152033
|
+
body: params.body === void 0 ? null : JSON.stringify(params.body)
|
|
152034
|
+
});
|
|
152035
|
+
const text = await response.text();
|
|
152036
|
+
let json4;
|
|
152037
|
+
try {
|
|
152038
|
+
json4 = text ? JSON.parse(text) : null;
|
|
152039
|
+
} catch {
|
|
152040
|
+
json4 = text.slice(0, 500);
|
|
152041
|
+
}
|
|
152042
|
+
return { status: response.status, json: json4 };
|
|
152043
|
+
}
|
|
152044
|
+
function ghError(method, path4, result) {
|
|
152045
|
+
return new Error(`${method} ${path4} failed (${result.status}): ${JSON.stringify(result.json)}`);
|
|
152046
|
+
}
|
|
152047
|
+
function isRetryable(status) {
|
|
152048
|
+
return status === 403 || status === 429 || status >= 500;
|
|
152049
|
+
}
|
|
152050
|
+
function detectWorkingTreeChanges() {
|
|
152051
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
152052
|
+
const diff = $2("git", ["diff", "--name-status", "--no-renames", "-z", "HEAD"], { log: false });
|
|
152053
|
+
const tokens = diff.split("\0").filter((t2) => t2.length > 0);
|
|
152054
|
+
for (let i = 0; i + 1 < tokens.length; i += 2) {
|
|
152055
|
+
const status = tokens[i];
|
|
152056
|
+
const path4 = tokens[i + 1];
|
|
152057
|
+
if (status === void 0 || path4 === void 0) break;
|
|
152058
|
+
if (status === "U") {
|
|
152059
|
+
throw new Error(
|
|
152060
|
+
`'${path4}' has unresolved merge conflicts. resolve the conflicts, stage the result with git add, then retry.`
|
|
152061
|
+
);
|
|
152062
|
+
}
|
|
152063
|
+
byPath.set(path4, { path: path4, deleted: status === "D" });
|
|
152064
|
+
}
|
|
152065
|
+
const porcelain = $2("git", ["status", "--porcelain=v1", "-z", "-uall", "--no-renames"], {
|
|
152066
|
+
log: false
|
|
152067
|
+
});
|
|
152068
|
+
for (const entry of porcelain.split("\0")) {
|
|
152069
|
+
if (entry.startsWith("?? ") && !byPath.has(entry.slice(3))) {
|
|
152070
|
+
const path4 = entry.slice(3);
|
|
152071
|
+
byPath.set(path4, { path: path4, deleted: false });
|
|
152072
|
+
}
|
|
152073
|
+
}
|
|
152074
|
+
return [...byPath.values()];
|
|
152075
|
+
}
|
|
152076
|
+
async function assertApiCommittable(files) {
|
|
152077
|
+
const present = files.filter((f) => !f.deleted).map((f) => f.path);
|
|
152078
|
+
if (present.length === 0) return;
|
|
152079
|
+
const root = getRepoRoot();
|
|
152080
|
+
const attrs = $2(
|
|
152081
|
+
"git",
|
|
152082
|
+
["check-attr", "filter", "-z", "--", ...present.map((p2) => join12(root, p2))],
|
|
152083
|
+
{ log: false }
|
|
152084
|
+
);
|
|
152085
|
+
const parts = attrs.split("\0");
|
|
152086
|
+
for (let i = 0; i + 2 < parts.length; i += 3) {
|
|
152087
|
+
if (parts[i + 2] === "lfs") {
|
|
152088
|
+
throw new Error(
|
|
152089
|
+
`'${parts[i]}' is tracked by git-lfs, which signed commits can't upload. remove it from the change set or ask the user to disable signed commits for this repo.`
|
|
152090
|
+
);
|
|
152091
|
+
}
|
|
152092
|
+
}
|
|
152093
|
+
for (const path4 of present) {
|
|
152094
|
+
const stat = await lstat(join12(root, path4));
|
|
152095
|
+
if (stat.isDirectory()) {
|
|
152096
|
+
throw new Error(
|
|
152097
|
+
`'${path4}' is a directory (nested repository or submodule?) \u2014 signed commits only support files and symlinks.`
|
|
152098
|
+
);
|
|
152099
|
+
}
|
|
152100
|
+
}
|
|
152101
|
+
}
|
|
152102
|
+
async function createBlobEntry(params) {
|
|
152103
|
+
const absPath = join12(params.repoRoot, params.path);
|
|
152104
|
+
const stat = await lstat(absPath);
|
|
152105
|
+
if (stat.size > MAX_BLOB_BYTES) {
|
|
152106
|
+
throw new Error(
|
|
152107
|
+
`'${params.path}' is ${Math.round(stat.size / 1024 / 1024)}MB \u2014 too large for signed commits (the GitHub blob API rejects large uploads). use git-lfs for large assets or ask the user to disable signed commits for this repo.`
|
|
152108
|
+
);
|
|
152109
|
+
}
|
|
152110
|
+
let mode;
|
|
152111
|
+
let content;
|
|
152112
|
+
if (stat.isSymbolicLink()) {
|
|
152113
|
+
mode = "120000";
|
|
152114
|
+
content = await readlink(absPath, { encoding: "buffer" });
|
|
152115
|
+
} else {
|
|
152116
|
+
mode = stat.mode & 64 ? "100755" : "100644";
|
|
152117
|
+
const cleanSha = $2("git", ["hash-object", "-w", "--", absPath], { log: false }).trim();
|
|
152118
|
+
content = execFileSync7("git", ["cat-file", "blob", cleanSha], {
|
|
152119
|
+
cwd: params.repoRoot,
|
|
152120
|
+
maxBuffer: 2 * MAX_BLOB_BYTES
|
|
152121
|
+
});
|
|
152122
|
+
}
|
|
152123
|
+
const path4 = `${params.repoPath}/git/blobs`;
|
|
152124
|
+
let result;
|
|
152125
|
+
for (const delayMs of [0, 1e3, 3e3]) {
|
|
152126
|
+
if (delayMs > 0) await new Promise((r) => setTimeout(r, delayMs));
|
|
152127
|
+
result = await gh({
|
|
152128
|
+
token: params.token,
|
|
152129
|
+
method: "POST",
|
|
152130
|
+
path: path4,
|
|
152131
|
+
body: { content: content.toString("base64"), encoding: "base64" }
|
|
152132
|
+
});
|
|
152133
|
+
if (result.status === 201) {
|
|
152134
|
+
const blob = result.json;
|
|
152135
|
+
return { path: params.path, mode, type: "blob", sha: blob.sha };
|
|
152136
|
+
}
|
|
152137
|
+
if (!isRetryable(result.status)) break;
|
|
152138
|
+
log.info(`blob upload for ${params.path} got ${result.status}, retrying`);
|
|
152139
|
+
}
|
|
152140
|
+
if (!result) throw new Error(`POST ${path4} failed`);
|
|
152141
|
+
throw ghError("POST", path4, result);
|
|
152142
|
+
}
|
|
152143
|
+
async function updateRefWithRetry(params) {
|
|
152144
|
+
const path4 = `${params.repoPath}/git/refs/heads/${encodeBranchPath(params.remoteBranch)}`;
|
|
152145
|
+
let lastResult;
|
|
152146
|
+
for (const delayMs of [0, 1e3, 3e3]) {
|
|
152147
|
+
if (delayMs > 0) await new Promise((r) => setTimeout(r, delayMs));
|
|
152148
|
+
const result = await gh({
|
|
152149
|
+
token: params.token,
|
|
152150
|
+
method: "PATCH",
|
|
152151
|
+
path: path4,
|
|
152152
|
+
body: { sha: params.sha, force: false }
|
|
152153
|
+
});
|
|
152154
|
+
if (result.status === 200) return;
|
|
152155
|
+
if (result.status === 422) {
|
|
152156
|
+
const detail = JSON.stringify(result.json);
|
|
152157
|
+
if (/fast.forward/i.test(detail)) {
|
|
152158
|
+
throw new Error(
|
|
152159
|
+
`the remote branch '${params.remoteBranch}' moved while committing (concurrent push). fetch it with git_fetch, integrate with git merge --no-commit origin/${params.remoteBranch}, resolve any conflicts, git add the results, then retry commit_changes.`
|
|
152160
|
+
);
|
|
152161
|
+
}
|
|
152162
|
+
throw ghError("PATCH", path4, result);
|
|
152163
|
+
}
|
|
152164
|
+
lastResult = result;
|
|
152165
|
+
if (!isRetryable(result.status)) break;
|
|
152166
|
+
log.info(`ref update got ${result.status}, retrying`);
|
|
152167
|
+
}
|
|
152168
|
+
if (lastResult) throw ghError("PATCH", path4, lastResult);
|
|
152169
|
+
throw new Error(`PATCH ${path4} failed`);
|
|
152170
|
+
}
|
|
152171
|
+
function encodeBranchPath(branch) {
|
|
152172
|
+
return branch.split("/").map(encodeURIComponent).join("/");
|
|
152173
|
+
}
|
|
152174
|
+
function validateRemoteBranch(branch) {
|
|
152175
|
+
const bad = branch.startsWith("-") || branch.startsWith("/") || branch.endsWith("/") || branch.includes("..") || branch.includes("@{") || /[\s~^:?*[\]\\]/.test(branch);
|
|
152176
|
+
if (bad) throw new Error(`invalid remote branch name '${branch}'`);
|
|
152177
|
+
}
|
|
152178
|
+
async function createSignedCommit(params) {
|
|
152179
|
+
validateRemoteBranch(params.remoteBranch);
|
|
152180
|
+
const repoPath = `/repos/${params.owner}/${params.repo}`;
|
|
152181
|
+
const branchPath = `${repoPath}/git/ref/heads/${encodeBranchPath(params.remoteBranch)}`;
|
|
152182
|
+
const refResult = await gh({ token: params.token, method: "GET", path: branchPath });
|
|
152183
|
+
const branchExists = refResult.status === 200;
|
|
152184
|
+
if (branchExists) {
|
|
152185
|
+
const ref = refResult.json;
|
|
152186
|
+
const remoteTip = ref.object.sha;
|
|
152187
|
+
if (!params.parents.includes(remoteTip)) {
|
|
152188
|
+
throw new Error(
|
|
152189
|
+
isAncestorOfHead(remoteTip) ? `your local branch has commits that were never pushed \u2014 signed-commits mode can't push local commits. run git reset --mixed ${remoteTip} (keeps every change in the working tree), then retry commit_changes.` : `the remote branch '${params.remoteBranch}' has commits you don't have locally (tip ${remoteTip.slice(0, 7)}). fetch it with git_fetch, integrate with git merge --no-commit origin/${params.remoteBranch}, resolve any conflicts, git add the results, then retry commit_changes.`
|
|
152190
|
+
);
|
|
152191
|
+
}
|
|
152192
|
+
} else if (refResult.status !== 404) {
|
|
152193
|
+
throw ghError("GET", branchPath, refResult);
|
|
152194
|
+
}
|
|
152195
|
+
const baseParent = params.parents[0];
|
|
152196
|
+
if (!baseParent) throw new Error("createSignedCommit requires at least one parent");
|
|
152197
|
+
const baseTree = $2("git", ["rev-parse", `${baseParent}^{tree}`], { log: false }).trim();
|
|
152198
|
+
let treeSha = baseTree;
|
|
152199
|
+
if (params.files.length > 0) {
|
|
152200
|
+
const repoRoot = getRepoRoot();
|
|
152201
|
+
const additions = params.files.filter((f) => !f.deleted);
|
|
152202
|
+
const blobEntries = [];
|
|
152203
|
+
for (let i = 0; i < additions.length; i += BLOB_UPLOAD_CONCURRENCY) {
|
|
152204
|
+
const chunk = additions.slice(i, i + BLOB_UPLOAD_CONCURRENCY);
|
|
152205
|
+
blobEntries.push(
|
|
152206
|
+
...await Promise.all(
|
|
152207
|
+
chunk.map(
|
|
152208
|
+
(f) => createBlobEntry({ token: params.token, repoPath, repoRoot, path: f.path })
|
|
152209
|
+
)
|
|
152210
|
+
)
|
|
152211
|
+
);
|
|
152212
|
+
}
|
|
152213
|
+
const deletionEntries = params.files.filter((f) => f.deleted).map((f) => ({ path: f.path, mode: "100644", type: "blob", sha: null }));
|
|
152214
|
+
const treeResult = await gh({
|
|
152215
|
+
token: params.token,
|
|
152216
|
+
method: "POST",
|
|
152217
|
+
path: `${repoPath}/git/trees`,
|
|
152218
|
+
body: { base_tree: baseTree, tree: [...blobEntries, ...deletionEntries] }
|
|
152219
|
+
});
|
|
152220
|
+
if (treeResult.status !== 201) {
|
|
152221
|
+
throw wrapUnknownBaseError("POST", `${repoPath}/git/trees`, treeResult, params.remoteBranch);
|
|
152222
|
+
}
|
|
152223
|
+
treeSha = treeResult.json.sha;
|
|
152224
|
+
}
|
|
152225
|
+
const commitResult = await gh({
|
|
152226
|
+
token: params.token,
|
|
152227
|
+
method: "POST",
|
|
152228
|
+
path: `${repoPath}/git/commits`,
|
|
152229
|
+
body: { message: params.message, tree: treeSha, parents: params.parents }
|
|
152230
|
+
});
|
|
152231
|
+
if (commitResult.status !== 201) {
|
|
152232
|
+
throw wrapUnknownBaseError(
|
|
152233
|
+
"POST",
|
|
152234
|
+
`${repoPath}/git/commits`,
|
|
152235
|
+
commitResult,
|
|
152236
|
+
params.remoteBranch
|
|
152237
|
+
);
|
|
152238
|
+
}
|
|
152239
|
+
const commit = commitResult.json;
|
|
152240
|
+
if (branchExists) {
|
|
152241
|
+
await updateRefWithRetry({
|
|
152242
|
+
token: params.token,
|
|
152243
|
+
repoPath,
|
|
152244
|
+
remoteBranch: params.remoteBranch,
|
|
152245
|
+
sha: commit.sha
|
|
152246
|
+
});
|
|
152247
|
+
} else {
|
|
152248
|
+
const createResult = await gh({
|
|
152249
|
+
token: params.token,
|
|
152250
|
+
method: "POST",
|
|
152251
|
+
path: `${repoPath}/git/refs`,
|
|
152252
|
+
body: { ref: `refs/heads/${params.remoteBranch}`, sha: commit.sha }
|
|
152253
|
+
});
|
|
152254
|
+
if (createResult.status !== 201) {
|
|
152255
|
+
throw ghError("POST", `${repoPath}/git/refs`, createResult);
|
|
152256
|
+
}
|
|
152257
|
+
}
|
|
152258
|
+
return { sha: commit.sha, createdBranch: !branchExists };
|
|
152259
|
+
}
|
|
152260
|
+
function isAncestorOfHead(sha) {
|
|
152261
|
+
try {
|
|
152262
|
+
$2("git", ["merge-base", "--is-ancestor", sha, "HEAD"], { log: false });
|
|
152263
|
+
return true;
|
|
152264
|
+
} catch {
|
|
152265
|
+
return false;
|
|
152266
|
+
}
|
|
152267
|
+
}
|
|
152268
|
+
function wrapUnknownBaseError(method, path4, result, remoteBranch) {
|
|
152269
|
+
if (result.status === 404 || result.status === 422) {
|
|
152270
|
+
return new Error(
|
|
152271
|
+
`${ghError(method, path4, result).message}
|
|
152272
|
+
|
|
152273
|
+
this usually means your local branch has commits that were never pushed \u2014 signed-commits mode can't push local commits. run git reset --mixed origin/${remoteBranch} (or the commit you branched from; this keeps every change in the working tree), then retry commit_changes.`
|
|
152274
|
+
);
|
|
152275
|
+
}
|
|
152276
|
+
return ghError(method, path4, result);
|
|
152277
|
+
}
|
|
152278
|
+
|
|
152279
|
+
// mcp/git.ts
|
|
151890
152280
|
function getPushDestination(branch, storedDest) {
|
|
151891
152281
|
if (storedDest && storedDest.localBranch === branch) {
|
|
151892
152282
|
log.debug(`using stored push destination: ${storedDest.remoteName}/${storedDest.remoteBranch}`);
|
|
@@ -151944,6 +152334,10 @@ function validateTagName(tag) {
|
|
|
151944
152334
|
);
|
|
151945
152335
|
}
|
|
151946
152336
|
}
|
|
152337
|
+
function pushesToBaseRepo(ctx) {
|
|
152338
|
+
const baseUrl = `https://github.com/${ctx.repo.owner}/${ctx.repo.name}.git`;
|
|
152339
|
+
return normalizeUrl(ctx.toolState.pushUrl ?? "") === normalizeUrl(baseUrl);
|
|
152340
|
+
}
|
|
151947
152341
|
function validatePushDestination(ctx, branch) {
|
|
151948
152342
|
const pushUrl = ctx.toolState.pushUrl;
|
|
151949
152343
|
if (!pushUrl) throw new Error("pushUrl not set - setupGit must run before push_branch");
|
|
@@ -151962,6 +152356,47 @@ var PushBranch = type({
|
|
|
151962
152356
|
branchName: type.string.describe("The branch name to push (defaults to current branch)").optional(),
|
|
151963
152357
|
force: type.boolean.describe("Force push (use with caution)").default(false)
|
|
151964
152358
|
});
|
|
152359
|
+
function assertPushTarget(ctx, branch, pushDest) {
|
|
152360
|
+
const prBranchMatch = branch.match(/^pr-(\d+)$/);
|
|
152361
|
+
if (prBranchMatch && pushDest.remoteBranch !== branch) {
|
|
152362
|
+
const prNumber = Number(prBranchMatch[1]);
|
|
152363
|
+
const event = ctx.payload.event;
|
|
152364
|
+
const runScoped = event.is_pr === true && event.issue_number === prNumber;
|
|
152365
|
+
if (!runScoped) {
|
|
152366
|
+
throw new Error(
|
|
152367
|
+
`push blocked: local branch '${branch}' would push to '${pushDest.remoteName}/${pushDest.remoteBranch}', but this run is not scoped to PR #${prNumber}. the 'pr-${prNumber}' branch was created by a prior checkout_pr call (likely from a subagent \u2014 subagents share the working tree and toolState with the orchestrator). you have probably landed your commit on the wrong branch. switch to your own feature branch first (e.g. 'git checkout <feature-branch>') and then push. if the push to PR #${prNumber} is intentional, this run needs to be triggered against that PR.`
|
|
152368
|
+
);
|
|
152369
|
+
}
|
|
152370
|
+
}
|
|
152371
|
+
const defaultBranch = ctx.repo.data.default_branch || "main";
|
|
152372
|
+
if (ctx.payload.push === "restricted" && pushDest.remoteBranch === defaultBranch) {
|
|
152373
|
+
throw new Error(
|
|
152374
|
+
`Push blocked: cannot push directly to default branch '${pushDest.remoteBranch}'. Create a feature branch and open a PR instead.`
|
|
152375
|
+
);
|
|
152376
|
+
}
|
|
152377
|
+
}
|
|
152378
|
+
async function runPrepushHook(ctx, retryTool) {
|
|
152379
|
+
if (ctx.toolState.prepushFailureCount > 0) {
|
|
152380
|
+
log.info(`\xBB skipping prepush hook (failed earlier this run)`);
|
|
152381
|
+
return true;
|
|
152382
|
+
}
|
|
152383
|
+
if (!ctx.prepushScript) return false;
|
|
152384
|
+
const prepushHook = await executeLifecycleHook({
|
|
152385
|
+
event: "prepush",
|
|
152386
|
+
script: ctx.prepushScript
|
|
152387
|
+
});
|
|
152388
|
+
if (prepushHook.failure) {
|
|
152389
|
+
ctx.toolState.prepushFailureCount += 1;
|
|
152390
|
+
throw new Error(
|
|
152391
|
+
buildPrepushFailureMessage({
|
|
152392
|
+
failure: prepushHook.failure,
|
|
152393
|
+
shell: ctx.payload.shell,
|
|
152394
|
+
retryTool
|
|
152395
|
+
})
|
|
152396
|
+
);
|
|
152397
|
+
}
|
|
152398
|
+
return false;
|
|
152399
|
+
}
|
|
151965
152400
|
var CONCURRENT_PUSH_PATTERNS = ["fetch first", "non-fast-forward", "cannot lock ref"];
|
|
151966
152401
|
var TRANSIENT_PATTERNS = [
|
|
151967
152402
|
/RPC failed/i,
|
|
@@ -152021,7 +152456,6 @@ async function pushWithRetry(args2, token) {
|
|
|
152021
152456
|
throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
|
|
152022
152457
|
}
|
|
152023
152458
|
function PushBranchTool(ctx) {
|
|
152024
|
-
const defaultBranch = ctx.repo.data.default_branch || "main";
|
|
152025
152459
|
const pushPermission = ctx.payload.push;
|
|
152026
152460
|
return tool({
|
|
152027
152461
|
name: "push_branch",
|
|
@@ -152033,6 +152467,11 @@ function PushBranchTool(ctx) {
|
|
|
152033
152467
|
}
|
|
152034
152468
|
const branch = branchName || $2("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false });
|
|
152035
152469
|
rejectSpecialRef(branch, "branch");
|
|
152470
|
+
if (ctx.signedCommits && pushesToBaseRepo(ctx)) {
|
|
152471
|
+
throw new Error(
|
|
152472
|
+
"push_branch is not used in signed-commits mode \u2014 commits land on the remote via the commit_changes tool. call commit_changes to commit your working-tree changes as a GitHub-signed commit. if you already called commit_changes, your work is already on the remote \u2014 there is nothing left to push."
|
|
152473
|
+
);
|
|
152474
|
+
}
|
|
152036
152475
|
const status = $2("git", ["status", "--porcelain"], { log: false });
|
|
152037
152476
|
if (status) {
|
|
152038
152477
|
throw new Error(
|
|
@@ -152043,36 +152482,11 @@ ${status}` + (ctx.toolState.prepushFailureCount > 0 ? "\n\nnote: the prepush hoo
|
|
|
152043
152482
|
);
|
|
152044
152483
|
}
|
|
152045
152484
|
const pushDest = validatePushDestination(ctx, branch);
|
|
152046
|
-
|
|
152047
|
-
if (prBranchMatch && pushDest.remoteBranch !== branch) {
|
|
152048
|
-
const prNumber = Number(prBranchMatch[1]);
|
|
152049
|
-
const event = ctx.payload.event;
|
|
152050
|
-
const runScoped = event.is_pr === true && event.issue_number === prNumber;
|
|
152051
|
-
if (!runScoped) {
|
|
152052
|
-
throw new Error(
|
|
152053
|
-
`push blocked: local branch '${branch}' would push to '${pushDest.remoteName}/${pushDest.remoteBranch}', but this run is not scoped to PR #${prNumber}. the 'pr-${prNumber}' branch was created by a prior checkout_pr call (likely from a subagent \u2014 subagents share the working tree and toolState with the orchestrator). you have probably landed your commit on the wrong branch. switch to your own feature branch first (e.g. 'git checkout <feature-branch>') and then push. if the push to PR #${prNumber} is intentional, this run needs to be triggered against that PR.`
|
|
152054
|
-
);
|
|
152055
|
-
}
|
|
152056
|
-
}
|
|
152057
|
-
if (pushPermission === "restricted" && pushDest.remoteBranch === defaultBranch) {
|
|
152058
|
-
throw new Error(
|
|
152059
|
-
`Push blocked: cannot push directly to default branch '${pushDest.remoteBranch}'. Create a feature branch and open a PR instead.`
|
|
152060
|
-
);
|
|
152061
|
-
}
|
|
152485
|
+
assertPushTarget(ctx, branch, pushDest);
|
|
152062
152486
|
const refspec = branch === pushDest.remoteBranch ? branch : `${branch}:${pushDest.remoteBranch}`;
|
|
152063
152487
|
const pushArgs = force ? ["--force", "-u", pushDest.remoteName, refspec] : ["-u", pushDest.remoteName, refspec];
|
|
152064
|
-
const prepushSkipped = ctx
|
|
152065
|
-
if (prepushSkipped) {
|
|
152066
|
-
log.info(`\xBB skipping prepush hook (failed earlier this run)`);
|
|
152067
|
-
} else if (ctx.prepushScript) {
|
|
152068
|
-
const prepushHook = await executeLifecycleHook({
|
|
152069
|
-
event: "prepush",
|
|
152070
|
-
script: ctx.prepushScript
|
|
152071
|
-
});
|
|
152072
|
-
if (prepushHook.failure) {
|
|
152073
|
-
ctx.toolState.prepushFailureCount += 1;
|
|
152074
|
-
throw new Error(buildPrepushFailureMessage(prepushHook.failure, ctx.payload.shell));
|
|
152075
|
-
}
|
|
152488
|
+
const prepushSkipped = await runPrepushHook(ctx, "push_branch");
|
|
152489
|
+
if (!prepushSkipped && ctx.prepushScript) {
|
|
152076
152490
|
const postHookStatus = $2("git", ["status", "--porcelain"], { log: false });
|
|
152077
152491
|
if (postHookStatus) {
|
|
152078
152492
|
throw new Error(
|
|
@@ -152123,15 +152537,138 @@ ${integrateStep}
|
|
|
152123
152537
|
})
|
|
152124
152538
|
});
|
|
152125
152539
|
}
|
|
152126
|
-
function buildPrepushFailureMessage(
|
|
152540
|
+
function buildPrepushFailureMessage(params) {
|
|
152541
|
+
const failure = params.failure;
|
|
152127
152542
|
const header = failure.kind === "exit" ? `prepush hook failed with exit code ${failure.exitCode}.
|
|
152128
152543
|
|
|
152129
152544
|
script output:
|
|
152130
152545
|
${failure.output || "(empty)"}` : failure.kind === "timeout" ? `prepush hook timed out \u2014 the script is hung or doing too much work.` : `prepush hook failed to spawn: ${failure.spawnError}.`;
|
|
152131
|
-
const ifRealBug = shell === "disabled" ? `fix it before pushing again \u2014 shell access is disabled in this run, so you can't re-run the hook command yourself.` : `run the hook command yourself via the shell tool to iterate (
|
|
152546
|
+
const ifRealBug = params.shell === "disabled" ? `fix it before pushing again \u2014 shell access is disabled in this run, so you can't re-run the hook command yourself.` : `run the hook command yourself via the shell tool to iterate (${params.retryTool} will NOT re-run it).`;
|
|
152132
152547
|
return `${header}
|
|
152133
152548
|
|
|
152134
|
-
this repo's prepush hook is best-effort: the next
|
|
152549
|
+
this repo's prepush hook is best-effort: the next ${params.retryTool} call will SKIP the hook and proceed. if the failure is unrelated to your changes (pre-existing breakage, flaky check), just call ${params.retryTool} again. if it could be a real bug in your code, ${ifRealBug}`;
|
|
152550
|
+
}
|
|
152551
|
+
function buildNothingToCommitMessage(pushDest) {
|
|
152552
|
+
const base = "nothing to commit \u2014 the working tree matches HEAD.";
|
|
152553
|
+
try {
|
|
152554
|
+
const remoteTip = $2(
|
|
152555
|
+
"git",
|
|
152556
|
+
["rev-parse", `refs/remotes/${pushDest.remoteName}/${pushDest.remoteBranch}`],
|
|
152557
|
+
{ log: false }
|
|
152558
|
+
).trim();
|
|
152559
|
+
const head = $2("git", ["rev-parse", "HEAD"], { log: false }).trim();
|
|
152560
|
+
if (remoteTip === head) {
|
|
152561
|
+
return `${base} your work is already on ${pushDest.remoteName}/${pushDest.remoteBranch} \u2014 there is no push step in signed-commits mode.`;
|
|
152562
|
+
}
|
|
152563
|
+
$2("git", ["merge-base", "--is-ancestor", remoteTip, "HEAD"], { log: false });
|
|
152564
|
+
return `${base} but your local branch has commits that were never pushed \u2014 signed-commits mode can't push local commits. run git reset --mixed ${remoteTip} (keeps every change in the working tree), then retry commit_changes.`;
|
|
152565
|
+
} catch {
|
|
152566
|
+
return base;
|
|
152567
|
+
}
|
|
152568
|
+
}
|
|
152569
|
+
var CommitChanges = type({
|
|
152570
|
+
message: type.string.describe("Commit message (first line = subject)"),
|
|
152571
|
+
files: type.string.array().describe("Optional subset of changed paths to commit. Defaults to every working-tree change.").optional()
|
|
152572
|
+
});
|
|
152573
|
+
function CommitChangesTool(ctx) {
|
|
152574
|
+
const pushPermission = ctx.payload.push;
|
|
152575
|
+
return tool({
|
|
152576
|
+
name: "commit_changes",
|
|
152577
|
+
description: "Commit working-tree changes directly to the remote branch as a GitHub-signed (Verified) commit \u2014 this repository has signed commits enabled, so use this INSTEAD of git commit + push_branch. Edit files locally, then call this tool: it detects every working-tree change (new, modified, deleted files), or commits a subset via `files`. The commit lands on the remote immediately \u2014 there is no separate push step. The remote branch is created automatically on the first commit to a new local branch. A merge in progress (git merge --no-commit) is concluded as a signed merge commit \u2014 resolve conflicts and git add first. Runs the repository prepush hook (if configured) before committing \u2014 best-effort, same skip-on-failure behavior as push_branch.",
|
|
152578
|
+
parameters: CommitChanges,
|
|
152579
|
+
timeoutMs: 6e5,
|
|
152580
|
+
execute: execute(async (params) => {
|
|
152581
|
+
if (pushPermission === "disabled") {
|
|
152582
|
+
throw new Error("Push is disabled. This repository is configured for read-only access.");
|
|
152583
|
+
}
|
|
152584
|
+
const branch = $2("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false }).trim();
|
|
152585
|
+
if (branch === "HEAD") {
|
|
152586
|
+
throw new Error(
|
|
152587
|
+
"HEAD is detached \u2014 create or check out a branch before committing (e.g. git checkout -b pullfrog/<description>)."
|
|
152588
|
+
);
|
|
152589
|
+
}
|
|
152590
|
+
rejectSpecialRef(branch, "branch");
|
|
152591
|
+
const pushDest = validatePushDestination(ctx, branch);
|
|
152592
|
+
if (!pushesToBaseRepo(ctx)) {
|
|
152593
|
+
throw new Error(
|
|
152594
|
+
`'${branch}' pushes to the fork '${pushDest.url}', where the app can't create signed commits. commit locally via the git tool and use push_branch instead (those commits will be unsigned).`
|
|
152595
|
+
);
|
|
152596
|
+
}
|
|
152597
|
+
assertPushTarget(ctx, branch, pushDest);
|
|
152598
|
+
const prepushSkipped = await runPrepushHook(ctx, "commit_changes");
|
|
152599
|
+
const head = $2("git", ["rev-parse", "HEAD"], { log: false }).trim();
|
|
152600
|
+
let mergeHead = "";
|
|
152601
|
+
try {
|
|
152602
|
+
mergeHead = $2("git", ["rev-parse", "-q", "--verify", "MERGE_HEAD"], { log: false }).trim();
|
|
152603
|
+
} catch {
|
|
152604
|
+
}
|
|
152605
|
+
let changes = detectWorkingTreeChanges();
|
|
152606
|
+
if (params.files) {
|
|
152607
|
+
if (mergeHead) {
|
|
152608
|
+
throw new Error(
|
|
152609
|
+
"can't commit a subset of files while a merge is in progress \u2014 the merge commit must include every merged change. omit `files`."
|
|
152610
|
+
);
|
|
152611
|
+
}
|
|
152612
|
+
const requested = new Set(params.files);
|
|
152613
|
+
const known = new Set(changes.map((c2) => c2.path));
|
|
152614
|
+
const unknown4 = [...requested].filter((p2) => !known.has(p2));
|
|
152615
|
+
if (unknown4.length > 0) {
|
|
152616
|
+
throw new Error(
|
|
152617
|
+
`no detected change at: ${unknown4.join(", ")} \u2014 run git status to list changed paths.`
|
|
152618
|
+
);
|
|
152619
|
+
}
|
|
152620
|
+
changes = changes.filter((c2) => requested.has(c2.path));
|
|
152621
|
+
}
|
|
152622
|
+
if (changes.length === 0 && !mergeHead) {
|
|
152623
|
+
throw new Error(buildNothingToCommitMessage(pushDest));
|
|
152624
|
+
}
|
|
152625
|
+
await assertApiCommittable(changes);
|
|
152626
|
+
const parents = mergeHead ? [head, mergeHead] : [head];
|
|
152627
|
+
const result = await createSignedCommit({
|
|
152628
|
+
token: ctx.gitToken,
|
|
152629
|
+
owner: ctx.repo.owner,
|
|
152630
|
+
repo: ctx.repo.name,
|
|
152631
|
+
remoteBranch: pushDest.remoteBranch,
|
|
152632
|
+
message: params.message,
|
|
152633
|
+
parents,
|
|
152634
|
+
files: changes
|
|
152635
|
+
});
|
|
152636
|
+
await $git(
|
|
152637
|
+
"fetch",
|
|
152638
|
+
[
|
|
152639
|
+
"--no-tags",
|
|
152640
|
+
"origin",
|
|
152641
|
+
`+refs/heads/${pushDest.remoteBranch}:refs/remotes/origin/${pushDest.remoteBranch}`
|
|
152642
|
+
],
|
|
152643
|
+
{ token: ctx.gitToken }
|
|
152644
|
+
);
|
|
152645
|
+
$2("git", ["update-ref", `refs/heads/${branch}`, result.sha], { log: false });
|
|
152646
|
+
if (mergeHead) {
|
|
152647
|
+
$2("git", ["merge", "--quit"], { log: false });
|
|
152648
|
+
}
|
|
152649
|
+
$2("git", ["reset", "-q"], { log: false });
|
|
152650
|
+
if (result.createdBranch) {
|
|
152651
|
+
$2("git", ["config", `branch.${branch}.remote`, "origin"], { log: false });
|
|
152652
|
+
$2("git", ["config", `branch.${branch}.merge`, `refs/heads/${pushDest.remoteBranch}`], {
|
|
152653
|
+
log: false
|
|
152654
|
+
});
|
|
152655
|
+
}
|
|
152656
|
+
log.info(
|
|
152657
|
+
`\xBB created signed commit ${result.sha.slice(0, 7)} (${changes.length} file(s)) on ${pushDest.remoteName}/${pushDest.remoteBranch}`
|
|
152658
|
+
);
|
|
152659
|
+
return {
|
|
152660
|
+
success: true,
|
|
152661
|
+
sha: result.sha,
|
|
152662
|
+
branch,
|
|
152663
|
+
remoteBranch: pushDest.remoteBranch,
|
|
152664
|
+
files: changes.map((c2) => c2.deleted ? `D ${c2.path}` : c2.path),
|
|
152665
|
+
createdBranch: result.createdBranch,
|
|
152666
|
+
verified: true,
|
|
152667
|
+
prepushSkipped,
|
|
152668
|
+
message: `created signed commit ${result.sha.slice(0, 7)} on ${pushDest.remoteName}/${pushDest.remoteBranch}${result.createdBranch ? " (remote branch created)" : ""}`
|
|
152669
|
+
};
|
|
152670
|
+
})
|
|
152671
|
+
});
|
|
152135
152672
|
}
|
|
152136
152673
|
var AUTH_REQUIRED_REDIRECT = {
|
|
152137
152674
|
push: "use the push_branch tool instead \u2014 it handles authentication and permission checks.",
|
|
@@ -152204,7 +152741,7 @@ function countAhead(head, base) {
|
|
|
152204
152741
|
function spillGitOutput(params) {
|
|
152205
152742
|
const tempDir = process.env.PULLFROG_TEMP_DIR;
|
|
152206
152743
|
if (!tempDir) throw new Error("PULLFROG_TEMP_DIR not set");
|
|
152207
|
-
const outputPath =
|
|
152744
|
+
const outputPath = join13(tempDir, `git-${params.command}-${randomUUID3().slice(0, 8)}.txt`);
|
|
152208
152745
|
writeFileSync7(outputPath, params.output);
|
|
152209
152746
|
const previewByLines = params.output.split("\n").slice(0, OVERFLOW_PREVIEW_LINES).join("\n");
|
|
152210
152747
|
const preview = previewByLines.length <= OVERFLOW_PREVIEW_MAX_CHARS ? previewByLines : `${previewByLines.slice(0, OVERFLOW_PREVIEW_MAX_CHARS)}\u2026`;
|
|
@@ -152238,8 +152775,30 @@ function GitTool(ctx) {
|
|
|
152238
152775
|
}
|
|
152239
152776
|
const redirect = AUTH_REQUIRED_REDIRECT[command];
|
|
152240
152777
|
if (redirect) {
|
|
152778
|
+
if (command === "push" && ctx.signedCommits) {
|
|
152779
|
+
throw new Error(
|
|
152780
|
+
"git push is not available through this tool \u2014 in signed-commits mode use commit_changes instead: it commits your working-tree changes directly to the remote as a GitHub-signed commit (push_branch only applies to fork PRs)."
|
|
152781
|
+
);
|
|
152782
|
+
}
|
|
152241
152783
|
throw new Error(`git ${command} is not available through this tool \u2014 ${redirect}`);
|
|
152242
152784
|
}
|
|
152785
|
+
if (ctx.signedCommits && (command === "commit" || command === "merge")) {
|
|
152786
|
+
if (pushesToBaseRepo(ctx)) {
|
|
152787
|
+
if (command === "commit") {
|
|
152788
|
+
throw new Error(
|
|
152789
|
+
"git commit is blocked in signed-commits mode \u2014 use the commit_changes tool instead. it commits your working-tree changes directly to the remote as a GitHub-signed (Verified) commit. if you are concluding a merge, stage the resolutions with git add and call commit_changes \u2014 no local commit is needed."
|
|
152790
|
+
);
|
|
152791
|
+
}
|
|
152792
|
+
const noLocalCommit = args2.some(
|
|
152793
|
+
(a) => a === "--no-commit" || a === "--abort" || a === "--quit"
|
|
152794
|
+
);
|
|
152795
|
+
if (!noLocalCommit) {
|
|
152796
|
+
throw new Error(
|
|
152797
|
+
"bare git merge would create a local commit, which can't be pushed in signed-commits mode. use git merge --no-commit <ref>, resolve any conflicts, git add the results, then call commit_changes \u2014 it concludes the merge as a signed merge commit."
|
|
152798
|
+
);
|
|
152799
|
+
}
|
|
152800
|
+
}
|
|
152801
|
+
}
|
|
152243
152802
|
if (ctx.payload.shell === "disabled") {
|
|
152244
152803
|
const blocked = NOSHELL_BLOCKED_SUBCOMMANDS[command];
|
|
152245
152804
|
if (blocked) {
|
|
@@ -152872,7 +153431,6 @@ async function createAndSubmitWithFooter(ctx, params, opts) {
|
|
|
152872
153431
|
workflowRun: ctx.runId ? { owner: ctx.repo.owner, repo: ctx.repo.name, runId: ctx.runId, jobId: ctx.jobId } : void 0,
|
|
152873
153432
|
customParts,
|
|
152874
153433
|
model: ctx.toolState.model,
|
|
152875
|
-
fallbackFrom: ctx.toolState.modelFallback?.from,
|
|
152876
153434
|
oss: ctx.oss
|
|
152877
153435
|
});
|
|
152878
153436
|
return await ctx.octokit.rest.pulls.submitReview({
|
|
@@ -152905,12 +153463,12 @@ async function reportReviewNodeId(ctx, params) {
|
|
|
152905
153463
|
}
|
|
152906
153464
|
|
|
152907
153465
|
// utils/setup.ts
|
|
152908
|
-
import { execFileSync as
|
|
153466
|
+
import { execFileSync as execFileSync8, execSync as execSync2 } from "node:child_process";
|
|
152909
153467
|
import { mkdtempSync as mkdtempSync2, readdirSync, realpathSync as realpathSync2, unlinkSync as unlinkSync2 } from "node:fs";
|
|
152910
153468
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
152911
|
-
import { join as
|
|
153469
|
+
import { join as join14 } from "node:path";
|
|
152912
153470
|
function createTempDirectory() {
|
|
152913
|
-
const sharedTempDir = mkdtempSync2(
|
|
153471
|
+
const sharedTempDir = mkdtempSync2(join14(tmpdir3(), "pullfrog-"));
|
|
152914
153472
|
process.env.PULLFROG_TEMP_DIR = sharedTempDir;
|
|
152915
153473
|
log.info(`\xBB created temp dir at ${sharedTempDir}`);
|
|
152916
153474
|
return sharedTempDir;
|
|
@@ -152955,13 +153513,13 @@ function wipeRunnerLeakSurface() {
|
|
|
152955
153513
|
return [];
|
|
152956
153514
|
}
|
|
152957
153515
|
};
|
|
152958
|
-
const fileCommandsDir =
|
|
153516
|
+
const fileCommandsDir = join14(runnerTemp, "_runner_file_commands");
|
|
152959
153517
|
for (const entry of listDir(fileCommandsDir)) {
|
|
152960
|
-
tryUnlink(
|
|
153518
|
+
tryUnlink(join14(fileCommandsDir, entry));
|
|
152961
153519
|
}
|
|
152962
153520
|
for (const entry of listDir(runnerTemp)) {
|
|
152963
153521
|
if (entry.endsWith(".sh") || /^git-credentials-.*\.config$/.test(entry)) {
|
|
152964
|
-
tryUnlink(
|
|
153522
|
+
tryUnlink(join14(runnerTemp, entry));
|
|
152965
153523
|
}
|
|
152966
153524
|
}
|
|
152967
153525
|
if (wiped.length > 0) {
|
|
@@ -152998,7 +153556,7 @@ function removeIncludeIfEntries(repoDir) {
|
|
|
152998
153556
|
if (!key || seen.has(key)) continue;
|
|
152999
153557
|
seen.add(key);
|
|
153000
153558
|
try {
|
|
153001
|
-
|
|
153559
|
+
execFileSync8("git", ["config", "--local", "--unset-all", key], {
|
|
153002
153560
|
cwd: repoDir,
|
|
153003
153561
|
stdio: "pipe",
|
|
153004
153562
|
env: env2
|
|
@@ -153470,7 +154028,7 @@ function CheckoutPrTool(ctx) {
|
|
|
153470
154028
|
headSha: ctx.toolState.checkoutSha
|
|
153471
154029
|
});
|
|
153472
154030
|
if (incremental) {
|
|
153473
|
-
incrementalDiffPath =
|
|
154031
|
+
incrementalDiffPath = join15(
|
|
153474
154032
|
tempDir,
|
|
153475
154033
|
`pr-${pull_number}-${beforeShort}-${headShort}-incremental.diff`
|
|
153476
154034
|
);
|
|
@@ -153484,7 +154042,7 @@ function CheckoutPrTool(ctx) {
|
|
|
153484
154042
|
const diffPreview = formatResult.content.split("\n").slice(0, 100).join("\n");
|
|
153485
154043
|
log.debug(`formatted diff preview (first 100 lines):
|
|
153486
154044
|
${diffPreview}`);
|
|
153487
|
-
const diffPath =
|
|
154045
|
+
const diffPath = join15(tempDir, `pr-${pull_number}-${headShort}.diff`);
|
|
153488
154046
|
writeFileSync8(diffPath, formatResult.content);
|
|
153489
154047
|
log.debug(`wrote diff to ${diffPath} (${formatResult.content.length} bytes)`);
|
|
153490
154048
|
ctx.toolState.diffCoverage = createDiffCoverageState({
|
|
@@ -153593,7 +154151,7 @@ ${dirty}`
|
|
|
153593
154151
|
|
|
153594
154152
|
// mcp/checkSuite.ts
|
|
153595
154153
|
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync9 } from "node:fs";
|
|
153596
|
-
import { join as
|
|
154154
|
+
import { join as join16 } from "node:path";
|
|
153597
154155
|
var GetCheckSuiteLogs = type({
|
|
153598
154156
|
check_suite_id: type.number.describe("the id from check_suite.id")
|
|
153599
154157
|
});
|
|
@@ -153689,7 +154247,7 @@ function GetCheckSuiteLogsTool(ctx) {
|
|
|
153689
154247
|
if (!tempDir) {
|
|
153690
154248
|
throw new Error("PULLFROG_TEMP_DIR not set");
|
|
153691
154249
|
}
|
|
153692
|
-
const logsDir =
|
|
154250
|
+
const logsDir = join16(tempDir, "ci-logs");
|
|
153693
154251
|
mkdirSync7(logsDir, { recursive: true });
|
|
153694
154252
|
const jobResults = [];
|
|
153695
154253
|
for (const run4 of failedRuns) {
|
|
@@ -153716,7 +154274,7 @@ function GetCheckSuiteLogsTool(ctx) {
|
|
|
153716
154274
|
);
|
|
153717
154275
|
}
|
|
153718
154276
|
const logsText = await logsResult.text();
|
|
153719
|
-
const logPath =
|
|
154277
|
+
const logPath = join16(logsDir, `job-${job.id}.log`);
|
|
153720
154278
|
writeFileSync9(logPath, logsText);
|
|
153721
154279
|
const analysis = analyzeLog(logsText, 80);
|
|
153722
154280
|
const failedSteps = job.steps?.filter((s) => s.conclusion === "failure").map((s) => `Step ${s.number}: ${s.name}`) ?? [];
|
|
@@ -153766,7 +154324,7 @@ function GetCheckSuiteLogsTool(ctx) {
|
|
|
153766
154324
|
|
|
153767
154325
|
// mcp/commitInfo.ts
|
|
153768
154326
|
import { writeFileSync as writeFileSync10 } from "node:fs";
|
|
153769
|
-
import { join as
|
|
154327
|
+
import { join as join17 } from "node:path";
|
|
153770
154328
|
var CommitInfo = type({
|
|
153771
154329
|
sha: type.string.describe("the commit SHA (full or abbreviated) to fetch")
|
|
153772
154330
|
});
|
|
@@ -153790,7 +154348,7 @@ function CommitInfoTool(ctx) {
|
|
|
153790
154348
|
"PULLFROG_TEMP_DIR not set - get_commit_info must run in pullfrog action context"
|
|
153791
154349
|
);
|
|
153792
154350
|
}
|
|
153793
|
-
const diffFile =
|
|
154351
|
+
const diffFile = join17(tempDir, `commit-${sha.slice(0, 7)}.diff`);
|
|
153794
154352
|
writeFileSync10(diffFile, formatResult.content);
|
|
153795
154353
|
log.debug(`wrote commit diff to ${diffFile} (${formatResult.content.length} bytes)`);
|
|
153796
154354
|
return {
|
|
@@ -154297,7 +154855,6 @@ function buildPrBodyWithFooter(ctx, body) {
|
|
|
154297
154855
|
triggeredBy: true,
|
|
154298
154856
|
workflowRun: ctx.runId ? { owner: ctx.repo.owner, repo: ctx.repo.name, runId: ctx.runId, jobId: ctx.jobId } : void 0,
|
|
154299
154857
|
model: ctx.toolState.model,
|
|
154300
|
-
fallbackFrom: ctx.toolState.modelFallback?.from,
|
|
154301
154858
|
oss: ctx.oss
|
|
154302
154859
|
});
|
|
154303
154860
|
const bodyWithoutFooter = stripExistingFooter(fixDoubleEscapedString(body));
|
|
@@ -154452,7 +155009,7 @@ function PullRequestInfoTool(ctx) {
|
|
|
154452
155009
|
|
|
154453
155010
|
// mcp/reviewComments.ts
|
|
154454
155011
|
import { writeFileSync as writeFileSync11 } from "node:fs";
|
|
154455
|
-
import { join as
|
|
155012
|
+
import { join as join18 } from "node:path";
|
|
154456
155013
|
var REVIEW_THREADS_QUERY = `
|
|
154457
155014
|
query ($owner: String!, $name: String!, $prNumber: Int!) {
|
|
154458
155015
|
repository(owner: $owner, name: $name) {
|
|
@@ -154868,7 +155425,7 @@ function GetReviewCommentsTool(ctx) {
|
|
|
154868
155425
|
throw new Error("PULLFROG_TEMP_DIR not set");
|
|
154869
155426
|
}
|
|
154870
155427
|
const filename = `review-${params.review_id}-threads.md`;
|
|
154871
|
-
const commentsPath =
|
|
155428
|
+
const commentsPath = join18(tempDir, filename);
|
|
154872
155429
|
writeFileSync11(commentsPath, formatted.content);
|
|
154873
155430
|
log.debug(`wrote ${threadBlocks.length} threads to ${commentsPath}`);
|
|
154874
155431
|
return {
|
|
@@ -155106,7 +155663,7 @@ import { spawn as spawn4, spawnSync as spawnSync5 } from "node:child_process";
|
|
|
155106
155663
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
155107
155664
|
import { closeSync, openSync, writeFileSync as writeFileSync12 } from "node:fs";
|
|
155108
155665
|
import { userInfo as userInfo2 } from "node:os";
|
|
155109
|
-
import { join as
|
|
155666
|
+
import { join as join19 } from "node:path";
|
|
155110
155667
|
import { setTimeout as sleep2 } from "node:timers/promises";
|
|
155111
155668
|
var ShellParams = type({
|
|
155112
155669
|
command: "string",
|
|
@@ -155269,7 +155826,7 @@ function getTempDir() {
|
|
|
155269
155826
|
var MAX_OUTPUT_CHARS = 5e3;
|
|
155270
155827
|
function capOutput(output) {
|
|
155271
155828
|
if (output.length <= MAX_OUTPUT_CHARS) return output;
|
|
155272
|
-
const fullPath =
|
|
155829
|
+
const fullPath = join19(getTempDir(), `shell-${randomUUID4().slice(0, 8)}.log`);
|
|
155273
155830
|
writeFileSync12(fullPath, output);
|
|
155274
155831
|
const elided = output.length - MAX_OUTPUT_CHARS;
|
|
155275
155832
|
return `... [${elided} chars truncated; full output saved to ${fullPath}] ...
|
|
@@ -155324,8 +155881,8 @@ Do NOT use this tool for git commands \u2014 use the dedicated git tools instead
|
|
|
155324
155881
|
if (params.background) {
|
|
155325
155882
|
const tempDir = getTempDir();
|
|
155326
155883
|
const handle = `bg-${randomUUID4().slice(0, 8)}`;
|
|
155327
|
-
const outputPath =
|
|
155328
|
-
const pidPath =
|
|
155884
|
+
const outputPath = join19(tempDir, `${handle}.log`);
|
|
155885
|
+
const pidPath = join19(tempDir, `${handle}.pid`);
|
|
155329
155886
|
const logFd = openSync(outputPath, "a");
|
|
155330
155887
|
let proc2;
|
|
155331
155888
|
try {
|
|
@@ -155556,7 +156113,7 @@ function buildCommonTools(ctx, outputSchema) {
|
|
|
155556
156113
|
return tools;
|
|
155557
156114
|
}
|
|
155558
156115
|
function buildOrchestratorTools(ctx, outputSchema) {
|
|
155559
|
-
|
|
156116
|
+
const tools = [
|
|
155560
156117
|
...buildCommonTools(ctx, outputSchema),
|
|
155561
156118
|
ReportProgressTool(ctx),
|
|
155562
156119
|
SelectModeTool(ctx),
|
|
@@ -155566,6 +156123,10 @@ function buildOrchestratorTools(ctx, outputSchema) {
|
|
|
155566
156123
|
CreatePullRequestTool(ctx),
|
|
155567
156124
|
UpdatePullRequestBodyTool(ctx)
|
|
155568
156125
|
];
|
|
156126
|
+
if (ctx.signedCommits) {
|
|
156127
|
+
tools.push(CommitChangesTool(ctx));
|
|
156128
|
+
}
|
|
156129
|
+
return tools;
|
|
155569
156130
|
}
|
|
155570
156131
|
async function tryStartMcpServer(ctx, tools, port) {
|
|
155571
156132
|
const server = new FastMCP({ name: pullfrogMcpName, version: "0.0.1" });
|
|
@@ -155756,8 +156317,14 @@ var MISSING_KEY_MARKER = "no API key found";
|
|
|
155756
156317
|
function buildMissingApiKeyError(params) {
|
|
155757
156318
|
const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
|
|
155758
156319
|
const settingsUrl = `${getApiUrl()}/console/${params.owner}/${params.name}`;
|
|
156320
|
+
const envVars = params.model?.includes("/") ? getModelEnvVars(params.model) : [];
|
|
156321
|
+
const [primary, ...alternates] = envVars;
|
|
156322
|
+
const envVarList = primary ? `\`${primary}\`${alternates.length > 0 ? ` (or ${alternates.map((v) => `\`${v}\``).join(" / ")})` : ""}` : void 0;
|
|
156323
|
+
const lead = envVarList ? `**${MISSING_KEY_MARKER}** \u2014 this repo is configured to use \`${params.model}\`, which needs ${envVarList}, but the runner has no key for it.` : `**${MISSING_KEY_MARKER}** \u2014 Pullfrog needs at least one LLM provider API key (e.g. \`ANTHROPIC_API_KEY\`, \`OPENAI_API_KEY\`, \`GEMINI_API_KEY\`) configured as a GitHub Actions secret.`;
|
|
155759
156324
|
return [
|
|
155760
|
-
|
|
156325
|
+
lead,
|
|
156326
|
+
"",
|
|
156327
|
+
"**To fix:** add the key as a GitHub Actions secret (referenced from your workflow's `env:` block) or as a Pullfrog secret in the console \u2014 or switch this repo to a different model (free models need no key).",
|
|
155761
156328
|
"",
|
|
155762
156329
|
`[Open repo secrets \u2192](${githubSecretsUrl}) \xB7 [Configure model \u2192](${settingsUrl}) \xB7 [Setup docs \u2192](https://docs.pullfrog.com/keys) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
|
|
155763
156330
|
].join("\n");
|
|
@@ -155837,10 +156404,14 @@ function validateAgentApiKey(params) {
|
|
|
155837
156404
|
}
|
|
155838
156405
|
if (params.agent.name === "opencode") {
|
|
155839
156406
|
if (params.authorized.has(params.model)) return;
|
|
155840
|
-
throw new Error(
|
|
156407
|
+
throw new Error(
|
|
156408
|
+
buildMissingApiKeyError({ owner: params.owner, name: params.name, model: params.model })
|
|
156409
|
+
);
|
|
155841
156410
|
}
|
|
155842
156411
|
if (hasEnvVar3("ANTHROPIC_API_KEY") || hasEnvVar3("CLAUDE_CODE_OAUTH_TOKEN")) return;
|
|
155843
|
-
throw new Error(
|
|
156412
|
+
throw new Error(
|
|
156413
|
+
buildMissingApiKeyError({ owner: params.owner, name: params.name, model: params.model })
|
|
156414
|
+
);
|
|
155844
156415
|
}
|
|
155845
156416
|
if (params.agent.name === "opencode") {
|
|
155846
156417
|
if (params.authorized.size > 0) return;
|
|
@@ -155851,42 +156422,37 @@ function validateAgentApiKey(params) {
|
|
|
155851
156422
|
}
|
|
155852
156423
|
function isApiKeyAuthError(text) {
|
|
155853
156424
|
if (!text) return false;
|
|
155854
|
-
return text.includes(MISSING_KEY_MARKER) || /Invalid API key/i.test(text) || /\bUser not found\b/i.test(text) || /\bInvalid authentication\b/i.test(text) || /authentication_error/i.test(text) || /Invalid bearer token/i.test(text) || /api_error_status\s*=\s*401/i.test(text) || /API Error:\s*401/i.test(text);
|
|
156425
|
+
return text.includes(MISSING_KEY_MARKER) || /Invalid API key/i.test(text) || /\bUser not found\b/i.test(text) || /\bInvalid authentication\b/i.test(text) || /authentication_error/i.test(text) || /Invalid bearer token/i.test(text) || /api_error_status\s*=\s*401/i.test(text) || /API Error:\s*401/i.test(text) || /Failed to authenticate\. API Error:/i.test(text) || isOAuthCredentialExpiredError(text);
|
|
156426
|
+
}
|
|
156427
|
+
function isOAuthCredentialExpiredError(text) {
|
|
156428
|
+
return /authentication token has expired/i.test(text) || /Token refresh failed/i.test(text);
|
|
155855
156429
|
}
|
|
155856
156430
|
function formatApiKeyErrorSummary(params) {
|
|
155857
156431
|
if (params.raw.includes(MISSING_KEY_MARKER)) {
|
|
156432
|
+
if (params.raw.startsWith(`**${MISSING_KEY_MARKER}**`)) return params.raw;
|
|
155858
156433
|
return buildMissingApiKeyError({ owner: params.owner, name: params.name });
|
|
155859
156434
|
}
|
|
155860
156435
|
const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
|
|
155861
156436
|
const settingsUrl = `${getApiUrl()}/console/${params.owner}/${params.name}`;
|
|
156437
|
+
if (isOAuthCredentialExpiredError(params.raw)) {
|
|
156438
|
+
return [
|
|
156439
|
+
`**Your provider OAuth credential has expired.** Re-authenticate the provider connection (e.g. \`pullfrog auth codex\`), then re-trigger the run.`,
|
|
156440
|
+
"",
|
|
156441
|
+
`[Model settings \u2192](${settingsUrl}) \xB7 [Setup docs \u2192](https://docs.pullfrog.com/keys) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
|
|
156442
|
+
].join("\n");
|
|
156443
|
+
}
|
|
155862
156444
|
return [
|
|
155863
|
-
`**Your LLM provider API key was rejected
|
|
156445
|
+
`**Your LLM provider API key was rejected.** Rotate the key in your provider dashboard, then update the matching GitHub Actions secret.`,
|
|
155864
156446
|
"",
|
|
155865
156447
|
`[Update repo secret \u2192](${githubSecretsUrl}) \xB7 [Model settings \u2192](${settingsUrl}) \xB7 [Setup docs \u2192](https://docs.pullfrog.com/keys) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
|
|
155866
156448
|
].join("\n");
|
|
155867
156449
|
}
|
|
155868
156450
|
|
|
155869
|
-
// utils/byokFallback.ts
|
|
155870
|
-
var FREE_FALLBACK_SLUG = "opencode/big-pickle";
|
|
155871
|
-
function selectFallbackModelIfNeeded(input) {
|
|
155872
|
-
if (input.proxyModel) return { fallback: false };
|
|
155873
|
-
if (!input.resolvedModel) return { fallback: false };
|
|
155874
|
-
if (input.resolvedModel === FREE_FALLBACK_SLUG) return { fallback: false };
|
|
155875
|
-
if (!input.resolvedModel.includes("/")) return { fallback: false };
|
|
155876
|
-
if (input.agentName === "claude") return { fallback: false };
|
|
155877
|
-
if (input.authorized.has(input.resolvedModel)) return { fallback: false };
|
|
155878
|
-
return {
|
|
155879
|
-
fallback: true,
|
|
155880
|
-
from: input.resolvedModel,
|
|
155881
|
-
to: FREE_FALLBACK_SLUG
|
|
155882
|
-
};
|
|
155883
|
-
}
|
|
155884
|
-
|
|
155885
156451
|
// utils/gitAuthServer.ts
|
|
155886
156452
|
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
155887
156453
|
import { writeFileSync as writeFileSync13 } from "node:fs";
|
|
155888
156454
|
import { createServer as createServer3 } from "node:http";
|
|
155889
|
-
import { join as
|
|
156455
|
+
import { join as join20 } from "node:path";
|
|
155890
156456
|
var REVOKED_TRAP_MS = 6e4;
|
|
155891
156457
|
function revokeGitHubToken(token) {
|
|
155892
156458
|
fetch("https://api.github.com/installation/token", {
|
|
@@ -155955,7 +156521,7 @@ async function startGitAuthServer(tmpdir4) {
|
|
|
155955
156521
|
function writeAskpassScript(code) {
|
|
155956
156522
|
const scriptId = randomUUID5();
|
|
155957
156523
|
const scriptName = `askpass-${scriptId}.js`;
|
|
155958
|
-
const scriptPath =
|
|
156524
|
+
const scriptPath = join20(tmpdir4, scriptName);
|
|
155959
156525
|
const content = [
|
|
155960
156526
|
`#!/usr/bin/env node`,
|
|
155961
156527
|
`var a=process.argv[2]||"";`,
|
|
@@ -155993,7 +156559,7 @@ async function startGitAuthServer(tmpdir4) {
|
|
|
155993
156559
|
var core3 = __toESM(require_core(), 1);
|
|
155994
156560
|
import { createSign } from "node:crypto";
|
|
155995
156561
|
import { rename, writeFile } from "node:fs/promises";
|
|
155996
|
-
import { dirname as dirname3, join as
|
|
156562
|
+
import { dirname as dirname3, join as join21 } from "node:path";
|
|
155997
156563
|
|
|
155998
156564
|
// node_modules/.pnpm/@octokit+plugin-throttling@11.0.3_@octokit+core@7.0.5/node_modules/@octokit/plugin-throttling/dist-bundle/index.js
|
|
155999
156565
|
var import_light = __toESM(require_light(), 1);
|
|
@@ -159635,6 +160201,7 @@ var Octokit2 = Octokit.plugin(requestLog, legacyRestEndpointMethods, paginateRes
|
|
|
159635
160201
|
);
|
|
159636
160202
|
|
|
159637
160203
|
// utils/github.ts
|
|
160204
|
+
var OIDC_AUDIENCE = "pullfrog-api";
|
|
159638
160205
|
function isObject4(value2) {
|
|
159639
160206
|
return typeof value2 === "object" && value2 !== null;
|
|
159640
160207
|
}
|
|
@@ -159651,8 +160218,39 @@ var TokenExchangeError = class extends Error {
|
|
|
159651
160218
|
this.status = status;
|
|
159652
160219
|
}
|
|
159653
160220
|
};
|
|
160221
|
+
async function fetchIdTokenFromStash(creds) {
|
|
160222
|
+
const url4 = new URL(creds.requestUrl);
|
|
160223
|
+
url4.searchParams.set("audience", OIDC_AUDIENCE);
|
|
160224
|
+
const timeoutMs = 3e4;
|
|
160225
|
+
let response;
|
|
160226
|
+
try {
|
|
160227
|
+
response = await fetch(url4, {
|
|
160228
|
+
headers: { Authorization: `Bearer ${creds.requestToken}` },
|
|
160229
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
160230
|
+
});
|
|
160231
|
+
} catch (error49) {
|
|
160232
|
+
if (error49 instanceof Error && error49.name === "TimeoutError") {
|
|
160233
|
+
throw new Error(`ID token request timed out after ${timeoutMs}ms`);
|
|
160234
|
+
}
|
|
160235
|
+
throw error49;
|
|
160236
|
+
}
|
|
160237
|
+
if (!response.ok) {
|
|
160238
|
+
throw new TokenExchangeError(
|
|
160239
|
+
response.status,
|
|
160240
|
+
`Failed to get ID token: ${response.status} ${response.statusText}`
|
|
160241
|
+
);
|
|
160242
|
+
}
|
|
160243
|
+
const body = await response.json();
|
|
160244
|
+
if (!body.value) {
|
|
160245
|
+
throw new Error("ID token response has no value field");
|
|
160246
|
+
}
|
|
160247
|
+
if (isGitHubActions) {
|
|
160248
|
+
core3.setSecret(body.value);
|
|
160249
|
+
}
|
|
160250
|
+
return body.value;
|
|
160251
|
+
}
|
|
159654
160252
|
async function acquireTokenViaOIDC(opts) {
|
|
159655
|
-
const oidcToken = await core3.getIDToken(
|
|
160253
|
+
const oidcToken = opts?.oidc ? await fetchIdTokenFromStash(opts.oidc) : await core3.getIDToken(OIDC_AUDIENCE);
|
|
159656
160254
|
const repos = [...opts?.repos ?? []];
|
|
159657
160255
|
const targetRepo = process.env.GITHUB_REPOSITORY?.split("/")[1];
|
|
159658
160256
|
if (targetRepo) {
|
|
@@ -159801,14 +160399,15 @@ async function acquireTokenViaGitHubApp(opts) {
|
|
|
159801
160399
|
const installationId = await findInstallationId(jwt2, config3.repoOwner, config3.repoName);
|
|
159802
160400
|
return await createInstallationToken(jwt2, installationId, opts?.permissions);
|
|
159803
160401
|
}
|
|
160402
|
+
function isTransientTokenError(error49) {
|
|
160403
|
+
if (error49 instanceof TokenExchangeError) return error49.status >= 500 || error49.status === 429;
|
|
160404
|
+
return error49 instanceof Error && (error49.message.includes("timed out") || error49.message.includes("fetch failed") || error49.message.includes("ECONNRESET") || error49.message.includes("ETIMEDOUT"));
|
|
160405
|
+
}
|
|
159804
160406
|
async function acquireNewToken(opts) {
|
|
159805
|
-
if (isOIDCAvailable()) {
|
|
160407
|
+
if (opts?.oidc || isOIDCAvailable()) {
|
|
159806
160408
|
return await retry(() => acquireTokenViaOIDC(opts), {
|
|
159807
160409
|
label: "token exchange",
|
|
159808
|
-
shouldRetry:
|
|
159809
|
-
if (error49 instanceof TokenExchangeError) return error49.status >= 500 || error49.status === 429;
|
|
159810
|
-
return error49 instanceof Error && (error49.message.includes("timed out") || error49.message.includes("fetch failed") || error49.message.includes("ECONNRESET") || error49.message.includes("ETIMEDOUT"));
|
|
159811
|
-
}
|
|
160410
|
+
shouldRetry: isTransientTokenError
|
|
159812
160411
|
});
|
|
159813
160412
|
}
|
|
159814
160413
|
if (process.env.GITHUB_ACTIONS === "true") {
|
|
@@ -159851,14 +160450,14 @@ function getGitHubUsageSummary() {
|
|
|
159851
160450
|
}
|
|
159852
160451
|
async function writeGitHubUsageSummaryToFile(path4) {
|
|
159853
160452
|
const summary2 = getGitHubUsageSummary();
|
|
159854
|
-
const tmpPath =
|
|
160453
|
+
const tmpPath = join21(dirname3(path4), `.usage-summary-${process.pid}.tmp`);
|
|
159855
160454
|
await writeFile(tmpPath, JSON.stringify(summary2));
|
|
159856
160455
|
await rename(tmpPath, path4);
|
|
159857
160456
|
}
|
|
159858
|
-
function createOctokit(token) {
|
|
160457
|
+
function createOctokit(token, refreshAuth) {
|
|
160458
|
+
let currentToken = token;
|
|
159859
160459
|
const OctokitWithPlugins = Octokit2.plugin(throttling);
|
|
159860
160460
|
const octokit = new OctokitWithPlugins({
|
|
159861
|
-
auth: token,
|
|
159862
160461
|
throttle: {
|
|
159863
160462
|
onRateLimit: (_retryAfter, _options, _octokit, retryCount) => {
|
|
159864
160463
|
return retryCount <= 2;
|
|
@@ -159887,6 +160486,8 @@ function createOctokit(token) {
|
|
|
159887
160486
|
return response;
|
|
159888
160487
|
};
|
|
159889
160488
|
octokit.hook.wrap("request", async (request2, options) => {
|
|
160489
|
+
const sentToken = currentToken;
|
|
160490
|
+
options.headers.authorization = `token ${sentToken}`;
|
|
159890
160491
|
try {
|
|
159891
160492
|
const response = await request2(options);
|
|
159892
160493
|
onResponse(response);
|
|
@@ -159895,6 +160496,13 @@ function createOctokit(token) {
|
|
|
159895
160496
|
if (isObject4(error49) && "response" in error49 && isObject4(error49.response) && "headers" in error49.response && isObject4(error49.response.headers)) {
|
|
159896
160497
|
onResponse(error49.response);
|
|
159897
160498
|
}
|
|
160499
|
+
if (refreshAuth && isObject4(error49) && "status" in error49 && error49.status === 401) {
|
|
160500
|
+
currentToken = await refreshAuth(sentToken);
|
|
160501
|
+
options.headers.authorization = `token ${currentToken}`;
|
|
160502
|
+
const response = await request2(options);
|
|
160503
|
+
onResponse(response);
|
|
160504
|
+
return response;
|
|
160505
|
+
}
|
|
159898
160506
|
throw error49;
|
|
159899
160507
|
}
|
|
159900
160508
|
});
|
|
@@ -160093,7 +160701,17 @@ Use \`${t2("git")}\` for local git commands (status, log, add, commit, checkout,
|
|
|
160093
160701
|
- \`${t2("checkout_pr")}\` - checkout a PR branch (fetches and configures push for forks)
|
|
160094
160702
|
- \`${t2("delete_branch")}\` - delete a remote branch (requires push: enabled)
|
|
160095
160703
|
- \`${t2("push_tags")}\` - push tags (requires push: enabled)
|
|
160096
|
-
|
|
160704
|
+
${ctx.signedCommits ? `
|
|
160705
|
+
#### Signed commits (enabled for this repository)
|
|
160706
|
+
|
|
160707
|
+
This repository requires GitHub-signed commits, which local git commits can never satisfy. This OVERRIDES any other instruction (including mode instructions) to commit via git or push via \`${t2("push_branch")}\`:
|
|
160708
|
+
- Do NOT use git commit or \`${t2("push_branch")}\` for same-repo branches \u2014 both are blocked. Instead: edit files, then call \`${t2("commit_changes")}\` with a commit message. It commits every working-tree change (or a \`files\` subset) directly to the remote branch as a GitHub-signed (Verified) commit. There is no separate push step.
|
|
160709
|
+
- New branches: create locally as usual (git checkout -b); the remote branch is created on the first \`${t2("commit_changes")}\` call.
|
|
160710
|
+
- To integrate remote changes (concurrent pushes, base branch): \`${t2("git_fetch")}\`, then git merge --no-commit <ref>, resolve conflicts, git add the results, then \`${t2("commit_changes")}\` \u2014 it concludes the merge as a signed merge commit.
|
|
160711
|
+
- \`${t2("commit_changes")}\` commits EVERY working-tree change by default \u2014 review \`git status\` first and clean up stray artifacts (or pass \`files\`).
|
|
160712
|
+
- cherry-pick/revert: use \`-n\`/\`--no-commit\` so no local commit is created, then \`${t2("commit_changes")}\`.
|
|
160713
|
+
- Fork PRs are the exception: signing is impossible there, so commit and push normally (those commits will be unsigned).
|
|
160714
|
+
` : ""}
|
|
160097
160715
|
Rules:
|
|
160098
160716
|
- All code changes must be pushed to a pull request (new or existing) before the run ends. This environment is ephemeral \u2014 unpushed work is lost permanently. \`git status\` must be clean when you finish.
|
|
160099
160717
|
- Protected branches (default branch) are blocked from direct pushes in restricted mode. Do not use \`git push\` directly \u2014 it will fail without credentials.
|
|
@@ -160269,7 +160887,7 @@ function resolveInstructions(ctx) {
|
|
|
160269
160887
|
|
|
160270
160888
|
// utils/learnings.ts
|
|
160271
160889
|
import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
|
|
160272
|
-
import { dirname as dirname4, join as
|
|
160890
|
+
import { dirname as dirname4, join as join22 } from "node:path";
|
|
160273
160891
|
|
|
160274
160892
|
// utils/learningsTruncate.ts
|
|
160275
160893
|
var MAX_LEARNINGS_LENGTH = 1e5;
|
|
@@ -160286,7 +160904,7 @@ function truncateAtLineBoundary(body, cap) {
|
|
|
160286
160904
|
// utils/learnings.ts
|
|
160287
160905
|
var LEARNINGS_FILE_NAME = "pullfrog-learnings.md";
|
|
160288
160906
|
function learningsFilePath(tmpdir4) {
|
|
160289
|
-
return
|
|
160907
|
+
return join22(tmpdir4, LEARNINGS_FILE_NAME);
|
|
160290
160908
|
}
|
|
160291
160909
|
async function seedLearningsFile(params) {
|
|
160292
160910
|
const path4 = learningsFilePath(params.tmpdir);
|
|
@@ -160706,6 +161324,10 @@ function formatTransientErrorSummary(error49, owner) {
|
|
|
160706
161324
|
var core7 = __toESM(require_core(), 1);
|
|
160707
161325
|
import assert2 from "node:assert/strict";
|
|
160708
161326
|
var mcpTokenValue;
|
|
161327
|
+
var refreshMcpTokenFn;
|
|
161328
|
+
function getMcpTokenRefresh() {
|
|
161329
|
+
return refreshMcpTokenFn;
|
|
161330
|
+
}
|
|
160709
161331
|
function getJobToken() {
|
|
160710
161332
|
const inputToken = core7.getInput("token");
|
|
160711
161333
|
if (inputToken) {
|
|
@@ -160757,6 +161379,29 @@ async function resolveTokens(params) {
|
|
|
160757
161379
|
`\xBB acquired scoped MCP token (${Object.entries(mcpPermissions).map((e) => e.join(":")).join(", ")})`
|
|
160758
161380
|
);
|
|
160759
161381
|
mcpTokenValue = mcpToken;
|
|
161382
|
+
let currentMcpToken = mcpToken;
|
|
161383
|
+
let refreshPromise;
|
|
161384
|
+
refreshMcpTokenFn = (stale) => {
|
|
161385
|
+
assert2(mcpTokenValue, "tokens already disposed");
|
|
161386
|
+
if (stale !== currentMcpToken) {
|
|
161387
|
+
return Promise.resolve(currentMcpToken);
|
|
161388
|
+
}
|
|
161389
|
+
refreshPromise ??= acquireNewToken({
|
|
161390
|
+
permissions: mcpPermissions,
|
|
161391
|
+
oidc: params.oidc ?? void 0
|
|
161392
|
+
}).then((fresh) => {
|
|
161393
|
+
if (isGitHubActions) {
|
|
161394
|
+
core7.setSecret(fresh);
|
|
161395
|
+
}
|
|
161396
|
+
mcpTokenValue = fresh;
|
|
161397
|
+
currentMcpToken = fresh;
|
|
161398
|
+
log.warning("\xBB GitHub rejected the MCP token; re-acquired a fresh scoped MCP token");
|
|
161399
|
+
return fresh;
|
|
161400
|
+
}).finally(() => {
|
|
161401
|
+
refreshPromise = void 0;
|
|
161402
|
+
});
|
|
161403
|
+
return refreshPromise;
|
|
161404
|
+
};
|
|
160760
161405
|
let disposingRef;
|
|
160761
161406
|
const dispose = async () => {
|
|
160762
161407
|
if (disposingRef) {
|
|
@@ -160765,9 +161410,10 @@ async function resolveTokens(params) {
|
|
|
160765
161410
|
disposingRef = Promise.withResolvers();
|
|
160766
161411
|
try {
|
|
160767
161412
|
mcpTokenValue = void 0;
|
|
161413
|
+
refreshMcpTokenFn = void 0;
|
|
160768
161414
|
await Promise.all([
|
|
160769
161415
|
revokeGitHubInstallationToken(gitToken),
|
|
160770
|
-
revokeGitHubInstallationToken(
|
|
161416
|
+
revokeGitHubInstallationToken(currentMcpToken)
|
|
160771
161417
|
]);
|
|
160772
161418
|
} finally {
|
|
160773
161419
|
removeSignalHandler();
|
|
@@ -160811,7 +161457,7 @@ async function reportErrorToComment(ctx) {
|
|
|
160811
161457
|
|
|
160812
161458
|
${ctx.error}` : ctx.error;
|
|
160813
161459
|
const repoContext = parseRepoContext();
|
|
160814
|
-
const octokit = createOctokit(getGitHubInstallationToken());
|
|
161460
|
+
const octokit = createOctokit(getGitHubInstallationToken(), getMcpTokenRefresh());
|
|
160815
161461
|
const runId = process.env.GITHUB_RUN_ID ? Number.parseInt(process.env.GITHUB_RUN_ID, 10) : void 0;
|
|
160816
161462
|
const customParts = [];
|
|
160817
161463
|
if (runId) {
|
|
@@ -160825,7 +161471,6 @@ ${ctx.error}` : ctx.error;
|
|
|
160825
161471
|
workflowRun: runId ? { owner: repoContext.owner, repo: repoContext.name, runId } : void 0,
|
|
160826
161472
|
customParts,
|
|
160827
161473
|
model: ctx.toolState.model,
|
|
160828
|
-
fallbackFrom: ctx.toolState.modelFallback?.from,
|
|
160829
161474
|
oss: ctx.toolState.oss
|
|
160830
161475
|
});
|
|
160831
161476
|
const body = `${formattedError}${footer}`;
|
|
@@ -160892,18 +161537,15 @@ async function mintProxyKey(ctx) {
|
|
|
160892
161537
|
if (error49 instanceof TransientError) throw error49;
|
|
160893
161538
|
log.warning(`proxy key mint error: ${error49 instanceof Error ? error49.message : String(error49)}`);
|
|
160894
161539
|
return null;
|
|
160895
|
-
} finally {
|
|
160896
|
-
delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
160897
|
-
delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
160898
161540
|
}
|
|
160899
161541
|
}
|
|
160900
161542
|
async function buildProxyTokenHeaders(ctx) {
|
|
160901
161543
|
if (ctx.oidcCredentials) {
|
|
160902
|
-
|
|
160903
|
-
|
|
160904
|
-
|
|
160905
|
-
|
|
160906
|
-
|
|
161544
|
+
const creds = ctx.oidcCredentials;
|
|
161545
|
+
const oidcToken = await retry(() => fetchIdTokenFromStash(creds), {
|
|
161546
|
+
label: "ID token mint",
|
|
161547
|
+
shouldRetry: isTransientTokenError
|
|
161548
|
+
});
|
|
160907
161549
|
return { Authorization: `Bearer ${oidcToken}` };
|
|
160908
161550
|
}
|
|
160909
161551
|
if (isLocalApiUrl()) {
|
|
@@ -160967,7 +161609,7 @@ async function runProxyResolution(ctx) {
|
|
|
160967
161609
|
|
|
160968
161610
|
// utils/prSummary.ts
|
|
160969
161611
|
import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
|
|
160970
|
-
import { dirname as dirname5, join as
|
|
161612
|
+
import { dirname as dirname5, join as join23 } from "node:path";
|
|
160971
161613
|
var SUMMARY_FILE_NAME = "pullfrog-summary.md";
|
|
160972
161614
|
var SUMMARY_SCAFFOLD = `# PR summary
|
|
160973
161615
|
|
|
@@ -160977,7 +161619,7 @@ var SUMMARY_SCAFFOLD = `# PR summary
|
|
|
160977
161619
|
var MIN_SNAPSHOT_LENGTH = 60;
|
|
160978
161620
|
var MAX_SNAPSHOT_LENGTH = 32768;
|
|
160979
161621
|
function summaryFilePath(tmpdir4) {
|
|
160980
|
-
return
|
|
161622
|
+
return join23(tmpdir4, SUMMARY_FILE_NAME);
|
|
160981
161623
|
}
|
|
160982
161624
|
async function seedSummaryFile(params) {
|
|
160983
161625
|
const path4 = summaryFilePath(params.tmpdir);
|
|
@@ -161105,6 +161747,7 @@ var defaultSettings = {
|
|
|
161105
161747
|
push: "restricted",
|
|
161106
161748
|
shell: "restricted",
|
|
161107
161749
|
prApproveEnabled: false,
|
|
161750
|
+
signedCommits: false,
|
|
161108
161751
|
modeInstructions: {},
|
|
161109
161752
|
learnings: null,
|
|
161110
161753
|
learningsHeadings: [],
|
|
@@ -161337,7 +161980,7 @@ ${input.errorMessage}
|
|
|
161337
161980
|
].join("\n");
|
|
161338
161981
|
}
|
|
161339
161982
|
function formatProviderModelNotFoundSummary(input) {
|
|
161340
|
-
return `
|
|
161983
|
+
return `The configured model is no longer available in OpenCode's catalog. Pick a different model in the Pullfrog console for \`${input.owner}/${input.name}\`, or contact support if this persists.
|
|
161341
161984
|
|
|
161342
161985
|
\`\`\`
|
|
161343
161986
|
${input.raw}
|
|
@@ -161778,7 +162421,7 @@ async function main() {
|
|
|
161778
162421
|
const initialOctokit = createOctokit(jobToken);
|
|
161779
162422
|
const runContext = await resolveRunContextData({ octokit: initialOctokit, token: jobToken });
|
|
161780
162423
|
timer.checkpoint("runContextData");
|
|
161781
|
-
|
|
162424
|
+
createTempDirectory();
|
|
161782
162425
|
const opencodeCliPath = await agents.opencode.install();
|
|
161783
162426
|
captureBaselineModels(opencodeCliPath);
|
|
161784
162427
|
if (runContext.dbSecrets) {
|
|
@@ -161802,12 +162445,12 @@ async function main() {
|
|
|
161802
162445
|
if (payload.event.trigger === "pull_request_synchronize") {
|
|
161803
162446
|
toolState.beforeSha = payload.event.before_sha;
|
|
161804
162447
|
}
|
|
161805
|
-
const tokenRef = __using(_stack2, await resolveTokens({ push: payload.push }), true);
|
|
161806
|
-
wipeRunnerLeakSurface();
|
|
161807
162448
|
const oidcCredentials = process.env.ACTIONS_ID_TOKEN_REQUEST_URL && process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN ? {
|
|
161808
162449
|
requestUrl: process.env.ACTIONS_ID_TOKEN_REQUEST_URL,
|
|
161809
162450
|
requestToken: process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN
|
|
161810
162451
|
} : null;
|
|
162452
|
+
const tokenRef = __using(_stack2, await resolveTokens({ push: payload.push, oidc: oidcCredentials }), true);
|
|
162453
|
+
wipeRunnerLeakSurface();
|
|
161811
162454
|
if (payload.shell !== "enabled") {
|
|
161812
162455
|
delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
161813
162456
|
delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
@@ -161820,7 +162463,7 @@ async function main() {
|
|
|
161820
162463
|
repo: runContext.repo,
|
|
161821
162464
|
toolState
|
|
161822
162465
|
});
|
|
161823
|
-
const octokit = createOctokit(tokenRef.mcpToken);
|
|
162466
|
+
const octokit = createOctokit(tokenRef.mcpToken, getMcpTokenRefresh());
|
|
161824
162467
|
const runInfo = await resolveRun({ octokit });
|
|
161825
162468
|
let toolContext;
|
|
161826
162469
|
let progressCallbackDisabled = false;
|
|
@@ -161832,13 +162475,13 @@ async function main() {
|
|
|
161832
162475
|
if (payload.cwd && process.cwd() !== payload.cwd) {
|
|
161833
162476
|
process.chdir(payload.cwd);
|
|
161834
162477
|
}
|
|
161835
|
-
const
|
|
162478
|
+
const tmpdir4 = createTempDirectory();
|
|
161836
162479
|
const originalBody = payload.event.body;
|
|
161837
162480
|
const resolvedBody = await resolveBody({
|
|
161838
162481
|
event: payload.event,
|
|
161839
162482
|
octokit,
|
|
161840
162483
|
repo: runContext.repo,
|
|
161841
|
-
tmpdir:
|
|
162484
|
+
tmpdir: tmpdir4,
|
|
161842
162485
|
githubToken: tokenRef.mcpToken
|
|
161843
162486
|
});
|
|
161844
162487
|
if (resolvedBody !== originalBody) {
|
|
@@ -161847,33 +162490,18 @@ async function main() {
|
|
|
161847
162490
|
payload.prompt = payload.prompt.replace(originalBody, resolvedBody ?? "");
|
|
161848
162491
|
}
|
|
161849
162492
|
}
|
|
161850
|
-
const gitAuthServer = __using(_stack, await startGitAuthServer(
|
|
162493
|
+
const gitAuthServer = __using(_stack, await startGitAuthServer(tmpdir4), true);
|
|
161851
162494
|
setGitAuthServer(gitAuthServer);
|
|
161852
|
-
const
|
|
161853
|
-
const authorized2 = getAuthorizedModels();
|
|
161854
|
-
const fallback = selectFallbackModelIfNeeded({
|
|
161855
|
-
resolvedModel: initialResolvedModel,
|
|
161856
|
-
proxyModel: payload.proxyModel,
|
|
161857
|
-
authorized: authorized2,
|
|
161858
|
-
agentName: resolveAgent({ model: initialResolvedModel }).name
|
|
161859
|
-
});
|
|
161860
|
-
const effectiveSlug = fallback.fallback ? fallback.to : payload.model;
|
|
161861
|
-
const resolvedModel = fallback.fallback ? fallback.to : initialResolvedModel;
|
|
161862
|
-
if (fallback.fallback) {
|
|
161863
|
-
log.warning(
|
|
161864
|
-
`\xBB fell back from ${fallback.from} to ${fallback.to} \u2014 no BYOK key present in runner env. add a provider key in repo secrets to use ${fallback.from} instead.`
|
|
161865
|
-
);
|
|
161866
|
-
toolState.modelFallback = { from: fallback.from };
|
|
161867
|
-
}
|
|
162495
|
+
const resolvedModel = payload.proxyModel ? void 0 : resolveModel({ slug: payload.model });
|
|
161868
162496
|
vertexCredentials = materializeVertexCredentials({ model: resolvedModel });
|
|
161869
162497
|
const agent2 = resolveAgent({ model: resolvedModel });
|
|
161870
|
-
const effectiveModel = payload.proxyModel ?? resolvedModel ??
|
|
162498
|
+
const effectiveModel = payload.proxyModel ?? resolvedModel ?? payload.model;
|
|
161871
162499
|
toolState.model = effectiveModel;
|
|
161872
|
-
if (!
|
|
162500
|
+
if (!payload.proxyModel) {
|
|
161873
162501
|
validateAgentApiKey({
|
|
161874
162502
|
agent: agent2,
|
|
161875
162503
|
model: effectiveModel,
|
|
161876
|
-
authorized:
|
|
162504
|
+
authorized: getAuthorizedModels(),
|
|
161877
162505
|
owner: runContext.repo.owner,
|
|
161878
162506
|
name: runContext.repo.name
|
|
161879
162507
|
});
|
|
@@ -161890,7 +162518,7 @@ async function main() {
|
|
|
161890
162518
|
timer.checkpoint("git");
|
|
161891
162519
|
const pmSpec = await resolvePackageManagerSpec(process.cwd());
|
|
161892
162520
|
if (pmSpec) {
|
|
161893
|
-
await ensurePackageManager({ spec: pmSpec, binDir: packageManagerBinDir(
|
|
162521
|
+
await ensurePackageManager({ spec: pmSpec, binDir: packageManagerBinDir(tmpdir4) });
|
|
161894
162522
|
}
|
|
161895
162523
|
timer.checkpoint("packageManager");
|
|
161896
162524
|
const setupHook = await executeLifecycleHook({
|
|
@@ -161903,26 +162531,34 @@ async function main() {
|
|
|
161903
162531
|
}
|
|
161904
162532
|
timer.checkpoint("lifecycleHooks::setup");
|
|
161905
162533
|
const agentId = agent2.name;
|
|
161906
|
-
const modes2 = [
|
|
162534
|
+
const modes2 = [
|
|
162535
|
+
...computeModes(agentId, runContext.repoSettings.signedCommits),
|
|
162536
|
+
...runContext.repoSettings.modes
|
|
162537
|
+
];
|
|
161907
162538
|
const outputSchema = resolveOutputSchema();
|
|
161908
162539
|
toolContext = {
|
|
161909
162540
|
agentId,
|
|
161910
162541
|
repo: runContext.repo,
|
|
161911
162542
|
payload,
|
|
161912
162543
|
octokit,
|
|
161913
|
-
|
|
162544
|
+
// live getter so raw-token consumers (asset fetches, plan/summary-comment
|
|
162545
|
+
// GETs) see the refreshed MCP token after a mid-run re-acquisition (#891)
|
|
162546
|
+
get githubInstallationToken() {
|
|
162547
|
+
return getGitHubInstallationToken();
|
|
162548
|
+
},
|
|
161914
162549
|
gitToken: tokenRef.gitToken,
|
|
161915
162550
|
apiToken: runContext.apiToken,
|
|
161916
162551
|
modes: modes2,
|
|
161917
162552
|
postCheckoutScript: runContext.repoSettings.postCheckoutScript,
|
|
161918
162553
|
prepushScript: runContext.repoSettings.prepushScript,
|
|
161919
162554
|
prApproveEnabled: runContext.repoSettings.prApproveEnabled,
|
|
162555
|
+
signedCommits: runContext.repoSettings.signedCommits,
|
|
161920
162556
|
modeInstructions: runContext.repoSettings.modeInstructions,
|
|
161921
162557
|
toolState,
|
|
161922
162558
|
runId: runInfo.runId,
|
|
161923
162559
|
jobId: runInfo.jobId,
|
|
161924
162560
|
mcpServerUrl: "",
|
|
161925
|
-
tmpdir:
|
|
162561
|
+
tmpdir: tmpdir4,
|
|
161926
162562
|
oss: runContext.oss,
|
|
161927
162563
|
plan: runContext.plan,
|
|
161928
162564
|
resolvedModel
|
|
@@ -161933,7 +162569,7 @@ async function main() {
|
|
|
161933
162569
|
timer.checkpoint("mcpServer");
|
|
161934
162570
|
try {
|
|
161935
162571
|
const learningsPath = await seedLearningsFile({
|
|
161936
|
-
tmpdir:
|
|
162572
|
+
tmpdir: tmpdir4,
|
|
161937
162573
|
current: runContext.repoSettings.learnings
|
|
161938
162574
|
});
|
|
161939
162575
|
toolState.learningsFilePath = learningsPath;
|
|
@@ -161950,7 +162586,7 @@ async function main() {
|
|
|
161950
162586
|
}
|
|
161951
162587
|
if (payload.generateSummary && payload.event.is_pr && payload.event.issue_number) {
|
|
161952
162588
|
const previousSnapshot = await fetchPreviousSnapshot(toolContext, payload.event.issue_number);
|
|
161953
|
-
const filePath = await seedSummaryFile({ tmpdir:
|
|
162589
|
+
const filePath = await seedSummaryFile({ tmpdir: tmpdir4, previousSnapshot });
|
|
161954
162590
|
toolState.summaryFilePath = filePath;
|
|
161955
162591
|
try {
|
|
161956
162592
|
toolState.summarySeed = await readFile5(filePath, "utf8");
|
|
@@ -161970,6 +162606,7 @@ async function main() {
|
|
|
161970
162606
|
modes: modes2,
|
|
161971
162607
|
agentId,
|
|
161972
162608
|
outputSchema,
|
|
162609
|
+
signedCommits: runContext.repoSettings.signedCommits,
|
|
161973
162610
|
learningsFilePath: toolState.learningsFilePath ?? null,
|
|
161974
162611
|
learningsHeadings: runContext.repoSettings.learningsHeadings,
|
|
161975
162612
|
setupHookFailure: describeSetupFailure(setupHook.failure)
|
|
@@ -161988,7 +162625,7 @@ ${instructions.user}` : null,
|
|
|
161988
162625
|
log.info(instructions.full);
|
|
161989
162626
|
});
|
|
161990
162627
|
if (agentId === "opencode") {
|
|
161991
|
-
const pluginDir =
|
|
162628
|
+
const pluginDir = join24(process.cwd(), ".opencode", "plugin");
|
|
161992
162629
|
const hasPlugins = existsSync8(pluginDir) && readdirSync2(pluginDir).some((f) => /\.[jt]sx?$/.test(f));
|
|
161993
162630
|
if (hasPlugins && toolState.dependencyInstallation?.promise) {
|
|
161994
162631
|
log.info(
|
|
@@ -162043,7 +162680,7 @@ ${instructions.user}` : null,
|
|
|
162043
162680
|
payload,
|
|
162044
162681
|
resolvedModel,
|
|
162045
162682
|
mcpServerUrl: mcpHttpServer.url,
|
|
162046
|
-
tmpdir:
|
|
162683
|
+
tmpdir: tmpdir4,
|
|
162047
162684
|
// PULLFROG_DATA_DIR (/var/lib/pullfrog) holds codex auth.json + any
|
|
162048
162685
|
// future pullfrog-managed on-disk secrets. bash via MCP tmpfs-overlays
|
|
162049
162686
|
// it; agent native FS tools deny it via the same secretDenyPaths plumbing
|
|
@@ -162319,7 +162956,7 @@ async function run(args2) {
|
|
|
162319
162956
|
}
|
|
162320
162957
|
|
|
162321
162958
|
// commands/init.ts
|
|
162322
|
-
import { execFileSync as
|
|
162959
|
+
import { execFileSync as execFileSync9 } from "node:child_process";
|
|
162323
162960
|
var import_arg3 = __toESM(require_arg(), 1);
|
|
162324
162961
|
var import_picocolors3 = __toESM(require_picocolors(), 1);
|
|
162325
162962
|
var PULLFROG_API_URL2 = (process.env.PULLFROG_API_URL || "https://pullfrog.com").replace(
|
|
@@ -162379,7 +163016,7 @@ function handleCancel2(value2) {
|
|
|
162379
163016
|
function getGhToken2() {
|
|
162380
163017
|
let token;
|
|
162381
163018
|
try {
|
|
162382
|
-
token =
|
|
163019
|
+
token = execFileSync9("gh", ["auth", "token"], { encoding: "utf-8" }).trim();
|
|
162383
163020
|
} catch {
|
|
162384
163021
|
bail2(
|
|
162385
163022
|
`gh cli not found or not authenticated.
|
|
@@ -162422,7 +163059,7 @@ async function ghApi(path4, token) {
|
|
|
162422
163059
|
function parseGitRemote2() {
|
|
162423
163060
|
let url4;
|
|
162424
163061
|
try {
|
|
162425
|
-
url4 =
|
|
163062
|
+
url4 = execFileSync9("git", ["remote", "get-url", "origin"], { encoding: "utf-8" }).trim();
|
|
162426
163063
|
} catch {
|
|
162427
163064
|
bail2("not a git repository or no 'origin' remote found.");
|
|
162428
163065
|
}
|
|
@@ -162433,10 +163070,10 @@ function parseGitRemote2() {
|
|
|
162433
163070
|
function openBrowser(url4) {
|
|
162434
163071
|
try {
|
|
162435
163072
|
const platform = process.platform;
|
|
162436
|
-
if (platform === "darwin")
|
|
163073
|
+
if (platform === "darwin") execFileSync9("open", [url4], { stdio: "ignore" });
|
|
162437
163074
|
else if (platform === "win32")
|
|
162438
|
-
|
|
162439
|
-
else
|
|
163075
|
+
execFileSync9("cmd", ["/c", "start", "", url4], { stdio: "ignore" });
|
|
163076
|
+
else execFileSync9("xdg-open", [url4], { stdio: "ignore" });
|
|
162440
163077
|
} catch {
|
|
162441
163078
|
}
|
|
162442
163079
|
}
|
|
@@ -162663,7 +163300,7 @@ function setGhSecret(ctx) {
|
|
|
162663
163300
|
let orgFailed = false;
|
|
162664
163301
|
if (ctx.org) {
|
|
162665
163302
|
try {
|
|
162666
|
-
|
|
163303
|
+
execFileSync9("gh", ["secret", "set", ctx.name, "--org", ctx.org, "--visibility", "all"], {
|
|
162667
163304
|
input: ctx.value,
|
|
162668
163305
|
stdio: ["pipe", "ignore", "pipe"],
|
|
162669
163306
|
encoding: "utf-8"
|
|
@@ -162674,7 +163311,7 @@ function setGhSecret(ctx) {
|
|
|
162674
163311
|
}
|
|
162675
163312
|
}
|
|
162676
163313
|
try {
|
|
162677
|
-
|
|
163314
|
+
execFileSync9("gh", ["secret", "set", ctx.name, "--repo", ctx.repoSlug], {
|
|
162678
163315
|
input: ctx.value,
|
|
162679
163316
|
stdio: ["pipe", "ignore", "pipe"],
|
|
162680
163317
|
encoding: "utf-8"
|
|
@@ -163046,7 +163683,7 @@ async function run2() {
|
|
|
163046
163683
|
}
|
|
163047
163684
|
|
|
163048
163685
|
// cli.ts
|
|
163049
|
-
var VERSION10 = "0.1.
|
|
163686
|
+
var VERSION10 = "0.1.30";
|
|
163050
163687
|
var bin = basename2(process.argv[1] || "");
|
|
163051
163688
|
var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
|
|
163052
163689
|
var rawArgs = process.argv.slice(2);
|