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/index.js
CHANGED
|
@@ -19718,10 +19718,10 @@ var require_core = __commonJS({
|
|
|
19718
19718
|
(0, command_1.issueCommand)("set-env", { name }, convertedVal);
|
|
19719
19719
|
}
|
|
19720
19720
|
exports.exportVariable = exportVariable;
|
|
19721
|
-
function
|
|
19721
|
+
function setSecret6(secret) {
|
|
19722
19722
|
(0, command_1.issueCommand)("add-mask", {}, secret);
|
|
19723
19723
|
}
|
|
19724
|
-
exports.setSecret =
|
|
19724
|
+
exports.setSecret = setSecret6;
|
|
19725
19725
|
function addPath(inputPath) {
|
|
19726
19726
|
const filePath = process.env["GITHUB_PATH"] || "";
|
|
19727
19727
|
if (filePath) {
|
|
@@ -19838,12 +19838,12 @@ Support boolean input list: \`true | True | TRUE | false | False | FALSE\``);
|
|
|
19838
19838
|
return process.env[`STATE_${name}`] || "";
|
|
19839
19839
|
}
|
|
19840
19840
|
exports.getState = getState;
|
|
19841
|
-
function
|
|
19841
|
+
function getIDToken3(aud) {
|
|
19842
19842
|
return __awaiter(this, void 0, void 0, function* () {
|
|
19843
19843
|
return yield oidc_utils_1.OidcClient.getIDToken(aud);
|
|
19844
19844
|
});
|
|
19845
19845
|
}
|
|
19846
|
-
exports.getIDToken =
|
|
19846
|
+
exports.getIDToken = getIDToken3;
|
|
19847
19847
|
var summary_1 = require_summary();
|
|
19848
19848
|
Object.defineProperty(exports, "summary", { enumerable: true, get: function() {
|
|
19849
19849
|
return summary_1.summary;
|
|
@@ -93492,14 +93492,14 @@ var require_turndown_cjs = __commonJS({
|
|
|
93492
93492
|
} else if (node2.nodeType === 1) {
|
|
93493
93493
|
replacement = replacementForNode.call(self2, node2);
|
|
93494
93494
|
}
|
|
93495
|
-
return
|
|
93495
|
+
return join24(output, replacement);
|
|
93496
93496
|
}, "");
|
|
93497
93497
|
}
|
|
93498
93498
|
function postProcess(output) {
|
|
93499
93499
|
var self2 = this;
|
|
93500
93500
|
this.rules.forEach(function(rule) {
|
|
93501
93501
|
if (typeof rule.append === "function") {
|
|
93502
|
-
output =
|
|
93502
|
+
output = join24(output, rule.append(self2.options));
|
|
93503
93503
|
}
|
|
93504
93504
|
});
|
|
93505
93505
|
return output.replace(/^[\t\r\n]+/, "").replace(/[\t\r\n\s]+$/, "");
|
|
@@ -93511,7 +93511,7 @@ var require_turndown_cjs = __commonJS({
|
|
|
93511
93511
|
if (whitespace.leading || whitespace.trailing) content = content.trim();
|
|
93512
93512
|
return whitespace.leading + rule.replacement(content, node2, this.options) + whitespace.trailing;
|
|
93513
93513
|
}
|
|
93514
|
-
function
|
|
93514
|
+
function join24(output, replacement) {
|
|
93515
93515
|
var s1 = trimTrailingNewlines(output);
|
|
93516
93516
|
var s2 = trimLeadingNewlines(replacement);
|
|
93517
93517
|
var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
|
|
@@ -98926,7 +98926,7 @@ var require_fast_content_type_parse = __commonJS({
|
|
|
98926
98926
|
// main.ts
|
|
98927
98927
|
import { existsSync as existsSync8, readdirSync as readdirSync2 } from "node:fs";
|
|
98928
98928
|
import { readFile as readFile5 } from "node:fs/promises";
|
|
98929
|
-
import { join as
|
|
98929
|
+
import { join as join23 } from "node:path";
|
|
98930
98930
|
|
|
98931
98931
|
// agents/claude.ts
|
|
98932
98932
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
@@ -98943,11 +98943,20 @@ var providers = {
|
|
|
98943
98943
|
displayName: "Anthropic",
|
|
98944
98944
|
envVars: ["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"],
|
|
98945
98945
|
models: {
|
|
98946
|
+
// OpenRouter serves claude-fable-5, but models.dev's OpenRouter mirror
|
|
98947
|
+
// hasn't indexed it yet (shipped 2026-06-09), so the catalog drift gate
|
|
98948
|
+
// can't validate an openRouterResolve. omit it until the mirror catches
|
|
98949
|
+
// up; direct BYOK / Claude Code resolves anthropic/claude-fable-5 fine.
|
|
98950
|
+
"claude-fable": {
|
|
98951
|
+
displayName: "Claude Fable",
|
|
98952
|
+
resolve: "anthropic/claude-fable-5",
|
|
98953
|
+
preferred: true,
|
|
98954
|
+
subagentModel: "claude-sonnet"
|
|
98955
|
+
},
|
|
98946
98956
|
"claude-opus": {
|
|
98947
98957
|
displayName: "Claude Opus",
|
|
98948
98958
|
resolve: "anthropic/claude-opus-4-8",
|
|
98949
98959
|
openRouterResolve: "openrouter/anthropic/claude-opus-4.8",
|
|
98950
|
-
preferred: true,
|
|
98951
98960
|
subagentModel: "claude-sonnet"
|
|
98952
98961
|
},
|
|
98953
98962
|
"claude-sonnet": {
|
|
@@ -99013,7 +99022,8 @@ var providers = {
|
|
|
99013
99022
|
},
|
|
99014
99023
|
o3: {
|
|
99015
99024
|
displayName: "O3",
|
|
99016
|
-
resolve: "openai/o3"
|
|
99025
|
+
resolve: "openai/o3",
|
|
99026
|
+
openRouterResolve: "openrouter/openai/o3"
|
|
99017
99027
|
}
|
|
99018
99028
|
}
|
|
99019
99029
|
}),
|
|
@@ -99383,6 +99393,18 @@ function parseModel(slug2) {
|
|
|
99383
99393
|
function getModelProvider(slug2) {
|
|
99384
99394
|
return parseModel(slug2).provider;
|
|
99385
99395
|
}
|
|
99396
|
+
function getModelEnvVars(slug2) {
|
|
99397
|
+
const parsed2 = parseModel(slug2);
|
|
99398
|
+
const providerConfig = providers[parsed2.provider];
|
|
99399
|
+
if (!providerConfig) {
|
|
99400
|
+
return [];
|
|
99401
|
+
}
|
|
99402
|
+
const modelConfig = providerConfig.models[parsed2.model];
|
|
99403
|
+
if (modelConfig?.envVars) {
|
|
99404
|
+
return modelConfig.envVars.slice();
|
|
99405
|
+
}
|
|
99406
|
+
return providerConfig.envVars.slice();
|
|
99407
|
+
}
|
|
99386
99408
|
var modelAliases = Object.entries(providers).flatMap(
|
|
99387
99409
|
([providerKey, config3]) => Object.entries(config3.models).map(([modelId, def]) => ({
|
|
99388
99410
|
slug: `${providerKey}/${modelId}`,
|
|
@@ -99407,6 +99429,9 @@ if (!defaultProxyAlias?.openRouterResolve) {
|
|
|
99407
99429
|
}
|
|
99408
99430
|
var DEFAULT_PROXY_MODEL = defaultProxyAlias.openRouterResolve;
|
|
99409
99431
|
var defaultProxyDisplayName = defaultProxyAlias.displayName;
|
|
99432
|
+
function resolveModelSlug(slug2) {
|
|
99433
|
+
return modelAliases.find((a) => a.slug === slug2)?.resolve;
|
|
99434
|
+
}
|
|
99410
99435
|
var MAX_FALLBACK_DEPTH = 10;
|
|
99411
99436
|
function resolveDisplayAlias(slug2) {
|
|
99412
99437
|
let current = slug2;
|
|
@@ -99554,6 +99579,51 @@ function createProcessOutputActivityTimeout(ctx) {
|
|
|
99554
99579
|
};
|
|
99555
99580
|
}
|
|
99556
99581
|
|
|
99582
|
+
// utils/claudeSubscription.ts
|
|
99583
|
+
var CLAUDE_CODE_IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude.";
|
|
99584
|
+
var fallbackResolve = resolveModelSlug("anthropic/claude-haiku");
|
|
99585
|
+
if (!fallbackResolve) {
|
|
99586
|
+
throw new Error("claudeSubscription preflight: anthropic/claude-haiku missing from registry");
|
|
99587
|
+
}
|
|
99588
|
+
var FALLBACK_PROBE_MODEL = fallbackResolve.slice(fallbackResolve.indexOf("/") + 1);
|
|
99589
|
+
async function preflightClaudeSubscription(params) {
|
|
99590
|
+
let res;
|
|
99591
|
+
try {
|
|
99592
|
+
res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
99593
|
+
method: "POST",
|
|
99594
|
+
headers: {
|
|
99595
|
+
authorization: `Bearer ${params.token}`,
|
|
99596
|
+
"anthropic-beta": "claude-code-20250219,oauth-2025-04-20",
|
|
99597
|
+
"anthropic-version": "2023-06-01",
|
|
99598
|
+
"content-type": "application/json",
|
|
99599
|
+
"x-app": "cli"
|
|
99600
|
+
},
|
|
99601
|
+
body: JSON.stringify({
|
|
99602
|
+
model: params.model ?? FALLBACK_PROBE_MODEL,
|
|
99603
|
+
max_tokens: 1,
|
|
99604
|
+
system: CLAUDE_CODE_IDENTITY,
|
|
99605
|
+
messages: [{ role: "user", content: "ok" }]
|
|
99606
|
+
}),
|
|
99607
|
+
signal: AbortSignal.timeout(1e4)
|
|
99608
|
+
});
|
|
99609
|
+
} catch {
|
|
99610
|
+
return { usable: true };
|
|
99611
|
+
}
|
|
99612
|
+
if (res.status !== 401 && res.status !== 429) return { usable: true };
|
|
99613
|
+
const body = await res.text().catch(() => "");
|
|
99614
|
+
return { usable: false, reason: `${res.status}: ${extractApiErrorMessage(body)}` };
|
|
99615
|
+
}
|
|
99616
|
+
function extractApiErrorMessage(body) {
|
|
99617
|
+
try {
|
|
99618
|
+
const parsed2 = JSON.parse(body);
|
|
99619
|
+
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") {
|
|
99620
|
+
return parsed2.error.message;
|
|
99621
|
+
}
|
|
99622
|
+
} catch {
|
|
99623
|
+
}
|
|
99624
|
+
return body.slice(0, 200);
|
|
99625
|
+
}
|
|
99626
|
+
|
|
99557
99627
|
// utils/log.ts
|
|
99558
99628
|
var core = __toESM(require_core(), 1);
|
|
99559
99629
|
var import_table = __toESM(require_src(), 1);
|
|
@@ -100067,7 +100137,7 @@ var import_semver = __toESM(require_semver2(), 1);
|
|
|
100067
100137
|
// package.json
|
|
100068
100138
|
var package_default = {
|
|
100069
100139
|
name: "pullfrog",
|
|
100070
|
-
version: "0.1.
|
|
100140
|
+
version: "0.1.30",
|
|
100071
100141
|
type: "module",
|
|
100072
100142
|
bin: {
|
|
100073
100143
|
pullfrog: "dist/cli.mjs",
|
|
@@ -100642,10 +100712,13 @@ function resolveVertexOpenCodeModel(model) {
|
|
|
100642
100712
|
var SUBAGENT_DENIED_TOOLS = [
|
|
100643
100713
|
// working-tree mutation: switches HEAD onto pr-N and registers a push remote
|
|
100644
100714
|
"checkout_pr",
|
|
100645
|
-
// remote mutation: pushes commits / branches / tags / deletes a branch
|
|
100715
|
+
// remote mutation: pushes commits / branches / tags / deletes a branch.
|
|
100716
|
+
// commit_changes lands the orchestrator's (possibly half-finished) shared
|
|
100717
|
+
// working tree directly on the remote — strictly worse than push_branch.
|
|
100646
100718
|
"push_branch",
|
|
100647
100719
|
"push_tags",
|
|
100648
100720
|
"delete_branch",
|
|
100721
|
+
"commit_changes",
|
|
100649
100722
|
// GitHub PR state mutation
|
|
100650
100723
|
"create_pull_request",
|
|
100651
100724
|
"update_pull_request_body",
|
|
@@ -100910,8 +100983,10 @@ Inline comments use the same severity framing as body \`### \` sections, scaled
|
|
|
100910
100983
|
- **Don't repeat diff content**, don't include raw \`+123 / -45\` stats, don't include a changelog section, don't use horizontal rules (\`---\`).
|
|
100911
100984
|
- **Pull file/commit counts from \`checkout_pr\` metadata** \u2014 never count manually.
|
|
100912
100985
|
- **Legacy headings REMOVED.** Do not use \`### Key changes\`, \`### Issues found\`, \`<b>TL;DR</b>\`, or \`<sub><b>Summary</b>\`. The new structure subsumes them.`;
|
|
100913
|
-
function computeModes(agentId) {
|
|
100986
|
+
function computeModes(agentId, signedCommits = false) {
|
|
100914
100987
|
const t = (toolName) => formatMcpToolRef(agentId, toolName);
|
|
100988
|
+
const commitStep = signedCommits ? `commit via \`${t("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 "..."\`)`;
|
|
100989
|
+
const finalizeStep = signedCommits ? `confirm a clean working tree (\`git status\`) \u2014 your \`${t("commit_changes")}\` calls already landed the work on the remote` : `confirm a clean working tree, then push via \`${t("push_branch")}\``;
|
|
100915
100990
|
return [
|
|
100916
100991
|
{
|
|
100917
100992
|
name: "Build",
|
|
@@ -100982,10 +101057,10 @@ function computeModes(agentId) {
|
|
|
100982
101057
|
- 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.
|
|
100983
101058
|
- 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.
|
|
100984
101059
|
|
|
100985
|
-
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.
|
|
101060
|
+
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}.
|
|
100986
101061
|
|
|
100987
101062
|
6. **finalize**:
|
|
100988
|
-
-
|
|
101063
|
+
- ${finalizeStep} (see *SYSTEM* Git rules if this fails \u2014 prepush errors are usually the repo's tests/lint, not infra timeouts)
|
|
100989
101064
|
- create a PR via \`${t("create_pull_request")}\`
|
|
100990
101065
|
- call \`${t("report_progress")}\` with the PR link or the exact error if push/PR failed
|
|
100991
101066
|
|
|
@@ -101013,12 +101088,12 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
101013
101088
|
|
|
101014
101089
|
5. Quality check:
|
|
101015
101090
|
- 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
|
|
101016
|
-
-
|
|
101091
|
+
- ${commitStep}
|
|
101017
101092
|
|
|
101018
101093
|
6. Finalize. Reply + resolve are paired write actions: do BOTH or NEITHER for each thread.
|
|
101019
|
-
-
|
|
101020
|
-
- **if push fails**, call \`${t("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.
|
|
101021
|
-
- **on
|
|
101094
|
+
- ${finalizeStep} (same push/prepush guidance as Build mode in *SYSTEM*)
|
|
101095
|
+
- **if the push/commit fails**, call \`${t("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.
|
|
101096
|
+
- **once the fix is live on the remote**, for each thread you acted on:
|
|
101022
101097
|
- reply ONCE via \`${t("reply_to_review_comment")}\`. The \`comment_id\` parameter takes the root comment's numeric \`id=\` (from the first \`comment author=...\` tag in the \`${t("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.
|
|
101023
101098
|
- **immediately** call \`${t("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.
|
|
101024
101099
|
- call \`${t("report_progress")}\` with a brief summary`
|
|
@@ -101293,10 +101368,10 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
101293
101368
|
- fix the issue using your native file and shell tools
|
|
101294
101369
|
- verify the fix by re-running the exact CI command
|
|
101295
101370
|
- 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.
|
|
101296
|
-
-
|
|
101371
|
+
- ${commitStep}
|
|
101297
101372
|
|
|
101298
101373
|
6. Finalize:
|
|
101299
|
-
-
|
|
101374
|
+
- ${finalizeStep} (same push/prepush guidance as Build mode in *SYSTEM*)
|
|
101300
101375
|
- call \`${t("report_progress")}\` with the diagnosis and fix summary (or the exact push error if push failed)`
|
|
101301
101376
|
},
|
|
101302
101377
|
{
|
|
@@ -101312,8 +101387,8 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
101312
101387
|
- Call \`${t("git_fetch")}\` to fetch the base branch.
|
|
101313
101388
|
|
|
101314
101389
|
3. **Merge Attempt**:
|
|
101315
|
-
- Run \`git merge origin/<base_branch>\` via shell.
|
|
101316
|
-
- If it succeeds automatically, confirm a clean working tree, push via \`${t("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*), and call \`${t("report_progress")}\` with a brief success note or the exact
|
|
101390
|
+
- Run \`git merge ${signedCommits ? "--no-commit " : ""}origin/<base_branch>\` via shell.
|
|
101391
|
+
- If it succeeds automatically, ${signedCommits ? `conclude it via \`${t("commit_changes")}\` (it turns the pending merge into a signed merge commit on the remote)` : `confirm a clean working tree, push via \`${t("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*)`}, and call \`${t("report_progress")}\` with a brief success note or the exact error if it failed \u2014 **then stop; do not run steps 4\u20135.**
|
|
101317
101392
|
- If it fails (conflicts), resolve them manually (continue to steps 4\u20135).
|
|
101318
101393
|
|
|
101319
101394
|
4. **Resolve Conflicts**:
|
|
@@ -101323,8 +101398,8 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
101323
101398
|
|
|
101324
101399
|
5. **Finalize**:
|
|
101325
101400
|
- Run a final verification (build/test) to ensure the resolution works.
|
|
101326
|
-
- \`git add . && git commit -m "resolve merge conflicts"
|
|
101327
|
-
-
|
|
101401
|
+
- ${signedCommits ? `\`git add .\`, then conclude via \`${t("commit_changes")}\` with message "resolve merge conflicts"` : `\`git add . && git commit -m "resolve merge conflicts"\``}
|
|
101402
|
+
- ${finalizeStep} (same push/prepush guidance as Build mode in *SYSTEM*)
|
|
101328
101403
|
- Call \`${t("report_progress")}\` with a summary of what was resolved (or the exact push error if push failed)`
|
|
101329
101404
|
},
|
|
101330
101405
|
{
|
|
@@ -101343,7 +101418,7 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
101343
101418
|
- 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
|
|
101344
101419
|
|
|
101345
101420
|
4. Finalize:
|
|
101346
|
-
- if code changes were made,
|
|
101421
|
+
- if code changes were made, get them onto a pull request (new or existing) using ${signedCommits ? `\`${t("commit_changes")}\`` : `\`${t("push_branch")}\``} and \`${t("create_pull_request")}\` as needed. \`git status\` must be clean before you finish (see *SYSTEM* Git rules if this fails).
|
|
101347
101422
|
- call \`${t("report_progress")}\` once with results \u2014 include exact tool errors if push or PR creation failed
|
|
101348
101423
|
- if the task involved labeling, commenting, or other GitHub operations, perform those directly`
|
|
101349
101424
|
}
|
|
@@ -102323,10 +102398,21 @@ var claude = agent({
|
|
|
102323
102398
|
env2.ANTHROPIC_MODEL = specifier;
|
|
102324
102399
|
}
|
|
102325
102400
|
if (env2.CLAUDE_CODE_OAUTH_TOKEN && !isBedrockRoute && env2.ANTHROPIC_API_KEY) {
|
|
102326
|
-
|
|
102327
|
-
|
|
102328
|
-
|
|
102329
|
-
|
|
102401
|
+
const preflight = await preflightClaudeSubscription({
|
|
102402
|
+
token: env2.CLAUDE_CODE_OAUTH_TOKEN,
|
|
102403
|
+
model
|
|
102404
|
+
});
|
|
102405
|
+
if (preflight.usable) {
|
|
102406
|
+
log.debug(
|
|
102407
|
+
"\xBB CLAUDE_CODE_OAUTH_TOKEN present \u2014 stripping ANTHROPIC_API_KEY from Claude Code env so the OAuth subscription is used"
|
|
102408
|
+
);
|
|
102409
|
+
delete env2.ANTHROPIC_API_KEY;
|
|
102410
|
+
} else {
|
|
102411
|
+
log.info(
|
|
102412
|
+
`\xBB Claude subscription unusable (${preflight.reason}) \u2014 falling back to ANTHROPIC_API_KEY`
|
|
102413
|
+
);
|
|
102414
|
+
delete env2.CLAUDE_CODE_OAUTH_TOKEN;
|
|
102415
|
+
}
|
|
102330
102416
|
}
|
|
102331
102417
|
log.info(`\xBB effort: ${effort}`);
|
|
102332
102418
|
log.debug(`\xBB starting Pullfrog (Claude Code): ${cliPath} ${baseArgs.join(" ")}`);
|
|
@@ -115386,15 +115472,6 @@ function isLocalApiUrl() {
|
|
|
115386
115472
|
// utils/buildPullfrogFooter.ts
|
|
115387
115473
|
var PULLFROG_DIVIDER = "<!-- PULLFROG_DIVIDER_DO_NOT_REMOVE_PLZ -->";
|
|
115388
115474
|
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>`;
|
|
115389
|
-
function providerDisplayName(slug2) {
|
|
115390
|
-
try {
|
|
115391
|
-
const key = getModelProvider(slug2);
|
|
115392
|
-
const meta3 = providers[key];
|
|
115393
|
-
return meta3?.displayName ?? key;
|
|
115394
|
-
} catch {
|
|
115395
|
-
return slug2;
|
|
115396
|
-
}
|
|
115397
|
-
}
|
|
115398
115475
|
function formatModelLabel(params) {
|
|
115399
115476
|
const alias = resolveDisplayAlias(params.model) ?? // reverse-lookup: when the caller passes an effective model (proxy or
|
|
115400
115477
|
// resolved target like "openrouter/anthropic/claude-opus-4.7") instead of
|
|
@@ -115405,9 +115482,7 @@ function formatModelLabel(params) {
|
|
|
115405
115482
|
if (params.oss) {
|
|
115406
115483
|
return `\`${displayName}\` (free via [Pullfrog for OSS](https://pullfrog.com/for-oss))`;
|
|
115407
115484
|
}
|
|
115408
|
-
|
|
115409
|
-
if (!params.fallbackFrom) return base;
|
|
115410
|
-
return `${base} (credentials for ${providerDisplayName(params.fallbackFrom)} not configured)`;
|
|
115485
|
+
return alias?.isFree ? `\`${displayName}\` (free)` : `\`${displayName}\``;
|
|
115411
115486
|
}
|
|
115412
115487
|
function buildPullfrogFooter(params) {
|
|
115413
115488
|
const parts = [];
|
|
@@ -115425,9 +115500,7 @@ function buildPullfrogFooter(params) {
|
|
|
115425
115500
|
parts.push("via [Pullfrog](https://pullfrog.com)");
|
|
115426
115501
|
}
|
|
115427
115502
|
if (params.model) {
|
|
115428
|
-
parts.push(
|
|
115429
|
-
`Using ${formatModelLabel({ model: params.model, fallbackFrom: params.fallbackFrom, oss: params.oss })}`
|
|
115430
|
-
);
|
|
115503
|
+
parts.push(`Using ${formatModelLabel({ model: params.model, oss: params.oss })}`);
|
|
115431
115504
|
}
|
|
115432
115505
|
const allParts = [...parts, "[\u{1D54F}](https://x.com/pullfrogai)"];
|
|
115433
115506
|
return `
|
|
@@ -116225,7 +116298,6 @@ function buildCommentFooter(ctx, customParts) {
|
|
|
116225
116298
|
} : void 0,
|
|
116226
116299
|
customParts,
|
|
116227
116300
|
model: ctx.toolState.model,
|
|
116228
|
-
fallbackFrom: ctx.toolState.modelFallback?.from,
|
|
116229
116301
|
oss: ctx.oss
|
|
116230
116302
|
});
|
|
116231
116303
|
}
|
|
@@ -117299,24 +117371,60 @@ var installPythonDependencies = {
|
|
|
117299
117371
|
|
|
117300
117372
|
// prep/index.ts
|
|
117301
117373
|
var prepSteps = [installNodeDependencies, installPythonDependencies];
|
|
117374
|
+
async function dirtyTrackedPaths() {
|
|
117375
|
+
const result = await spawn({
|
|
117376
|
+
cmd: "git",
|
|
117377
|
+
args: ["diff", "--name-only", "HEAD"],
|
|
117378
|
+
env: process.env,
|
|
117379
|
+
activityTimeout: 0
|
|
117380
|
+
});
|
|
117381
|
+
if (result.exitCode !== 0) {
|
|
117382
|
+
throw new Error(
|
|
117383
|
+
`git diff --name-only HEAD failed (exit ${result.exitCode}): ${result.stderr.trim() || "(no stderr)"}`
|
|
117384
|
+
);
|
|
117385
|
+
}
|
|
117386
|
+
return new Set(result.stdout.split("\n").filter(Boolean));
|
|
117387
|
+
}
|
|
117388
|
+
async function restorePrepDirtiedFiles(preDirty) {
|
|
117389
|
+
const dirtied = [...await dirtyTrackedPaths()].filter((path4) => !preDirty.has(path4));
|
|
117390
|
+
if (dirtied.length === 0) return;
|
|
117391
|
+
const result = await spawn({
|
|
117392
|
+
cmd: "git",
|
|
117393
|
+
args: ["restore", "--staged", "--worktree", "--", ...dirtied],
|
|
117394
|
+
env: process.env,
|
|
117395
|
+
activityTimeout: 0
|
|
117396
|
+
});
|
|
117397
|
+
if (result.exitCode !== 0) {
|
|
117398
|
+
log.warning(
|
|
117399
|
+
`\xBB failed to restore ${dirtied.length} tracked file(s) modified by prep: ${result.stderr.trim() || "(no stderr)"}`
|
|
117400
|
+
);
|
|
117401
|
+
return;
|
|
117402
|
+
}
|
|
117403
|
+
log.info(`\xBB restored ${dirtied.length} tracked file(s) modified by prep: ${dirtied.join(", ")}`);
|
|
117404
|
+
}
|
|
117302
117405
|
async function runPrepPhase(options) {
|
|
117303
117406
|
log.debug("\xBB starting prep phase...");
|
|
117304
117407
|
const startTime = performance7.now();
|
|
117305
117408
|
const results = [];
|
|
117306
|
-
|
|
117307
|
-
|
|
117308
|
-
|
|
117309
|
-
|
|
117310
|
-
|
|
117311
|
-
|
|
117312
|
-
|
|
117313
|
-
|
|
117314
|
-
|
|
117315
|
-
|
|
117316
|
-
|
|
117317
|
-
|
|
117318
|
-
|
|
117409
|
+
const preDirty = await dirtyTrackedPaths();
|
|
117410
|
+
try {
|
|
117411
|
+
for (const step of prepSteps) {
|
|
117412
|
+
const shouldRun = await step.shouldRun();
|
|
117413
|
+
if (!shouldRun) {
|
|
117414
|
+
log.debug(`\xBB skipping ${step.name} (not applicable)`);
|
|
117415
|
+
continue;
|
|
117416
|
+
}
|
|
117417
|
+
log.debug(`\xBB running ${step.name}...`);
|
|
117418
|
+
const result = await step.run(options);
|
|
117419
|
+
results.push(result);
|
|
117420
|
+
if (result.dependenciesInstalled) {
|
|
117421
|
+
log.debug(`\xBB ${step.name}: dependencies installed`);
|
|
117422
|
+
} else if (result.issues.length > 0) {
|
|
117423
|
+
log.warning(`\xBB ${step.name}: ${result.issues[0]}`);
|
|
117424
|
+
}
|
|
117319
117425
|
}
|
|
117426
|
+
} finally {
|
|
117427
|
+
await restorePrepDirtiedFiles(preDirty);
|
|
117320
117428
|
}
|
|
117321
117429
|
const totalDurationMs = performance7.now() - startTime;
|
|
117322
117430
|
log.debug(`\xBB prep phase completed (${Math.round(totalDurationMs)}ms)`);
|
|
@@ -149499,7 +149607,7 @@ function closeBrowserDaemon(toolState) {
|
|
|
149499
149607
|
// mcp/checkout.ts
|
|
149500
149608
|
import { createHash as createHash2 } from "node:crypto";
|
|
149501
149609
|
import { statSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync7 } from "node:fs";
|
|
149502
|
-
import { join as
|
|
149610
|
+
import { join as join14 } from "node:path";
|
|
149503
149611
|
|
|
149504
149612
|
// utils/diffCoverage.ts
|
|
149505
149613
|
import { isAbsolute, normalize as normalize2, resolve } from "node:path";
|
|
@@ -149769,6 +149877,7 @@ function readNumber(params) {
|
|
|
149769
149877
|
import { execSync } from "node:child_process";
|
|
149770
149878
|
import { createHash } from "node:crypto";
|
|
149771
149879
|
import { readFileSync as readFileSync2, realpathSync, unlinkSync } from "node:fs";
|
|
149880
|
+
import { join as join10 } from "node:path";
|
|
149772
149881
|
|
|
149773
149882
|
// utils/shell.ts
|
|
149774
149883
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
@@ -149838,6 +149947,18 @@ function verifyGitBinary() {
|
|
|
149838
149947
|
}
|
|
149839
149948
|
return gitBinary.path;
|
|
149840
149949
|
}
|
|
149950
|
+
var hooksDirCache = /* @__PURE__ */ new Map();
|
|
149951
|
+
function resolveHooksDir(cwd, gitPath) {
|
|
149952
|
+
const cached4 = hooksDirCache.get(cwd);
|
|
149953
|
+
if (cached4) return cached4;
|
|
149954
|
+
const commonDir = $(gitPath, ["rev-parse", "--path-format=absolute", "--git-common-dir"], {
|
|
149955
|
+
cwd,
|
|
149956
|
+
log: false
|
|
149957
|
+
}).trim();
|
|
149958
|
+
const hooksDir = join10(commonDir, "hooks");
|
|
149959
|
+
hooksDirCache.set(cwd, hooksDir);
|
|
149960
|
+
return hooksDir;
|
|
149961
|
+
}
|
|
149841
149962
|
var authServer;
|
|
149842
149963
|
function setGitAuthServer(server) {
|
|
149843
149964
|
authServer = server;
|
|
@@ -149859,6 +149980,8 @@ async function $git(subcommand, args2, options) {
|
|
|
149859
149980
|
"protocol.file.allow=never",
|
|
149860
149981
|
"-c",
|
|
149861
149982
|
"core.sshCommand=ssh",
|
|
149983
|
+
"-c",
|
|
149984
|
+
`core.hooksPath=${resolveHooksDir(cwd, gitPath)}`,
|
|
149862
149985
|
subcommand,
|
|
149863
149986
|
...args2
|
|
149864
149987
|
];
|
|
@@ -150128,7 +150251,274 @@ function postProcessRangeDiff(raw2, contextLines = 3) {
|
|
|
150128
150251
|
// mcp/git.ts
|
|
150129
150252
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
150130
150253
|
import { writeFileSync as writeFileSync6 } from "node:fs";
|
|
150131
|
-
import { join as
|
|
150254
|
+
import { join as join12 } from "node:path";
|
|
150255
|
+
|
|
150256
|
+
// utils/apiCommit.ts
|
|
150257
|
+
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
150258
|
+
import { lstat, readlink } from "node:fs/promises";
|
|
150259
|
+
import { join as join11 } from "node:path";
|
|
150260
|
+
var GITHUB_API = "https://api.github.com";
|
|
150261
|
+
var MAX_BLOB_BYTES = 30 * 1024 * 1024;
|
|
150262
|
+
var BLOB_UPLOAD_CONCURRENCY = 8;
|
|
150263
|
+
function getRepoRoot() {
|
|
150264
|
+
return $("git", ["rev-parse", "--show-toplevel"], { log: false }).trim();
|
|
150265
|
+
}
|
|
150266
|
+
async function gh(params) {
|
|
150267
|
+
const response = await fetch(`${GITHUB_API}${params.path}`, {
|
|
150268
|
+
method: params.method,
|
|
150269
|
+
headers: {
|
|
150270
|
+
Accept: "application/vnd.github+json",
|
|
150271
|
+
Authorization: `Bearer ${params.token}`,
|
|
150272
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
150273
|
+
"Content-Type": "application/json"
|
|
150274
|
+
},
|
|
150275
|
+
body: params.body === void 0 ? null : JSON.stringify(params.body)
|
|
150276
|
+
});
|
|
150277
|
+
const text = await response.text();
|
|
150278
|
+
let json4;
|
|
150279
|
+
try {
|
|
150280
|
+
json4 = text ? JSON.parse(text) : null;
|
|
150281
|
+
} catch {
|
|
150282
|
+
json4 = text.slice(0, 500);
|
|
150283
|
+
}
|
|
150284
|
+
return { status: response.status, json: json4 };
|
|
150285
|
+
}
|
|
150286
|
+
function ghError(method, path4, result) {
|
|
150287
|
+
return new Error(`${method} ${path4} failed (${result.status}): ${JSON.stringify(result.json)}`);
|
|
150288
|
+
}
|
|
150289
|
+
function isRetryable(status) {
|
|
150290
|
+
return status === 403 || status === 429 || status >= 500;
|
|
150291
|
+
}
|
|
150292
|
+
function detectWorkingTreeChanges() {
|
|
150293
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
150294
|
+
const diff = $("git", ["diff", "--name-status", "--no-renames", "-z", "HEAD"], { log: false });
|
|
150295
|
+
const tokens = diff.split("\0").filter((t) => t.length > 0);
|
|
150296
|
+
for (let i = 0; i + 1 < tokens.length; i += 2) {
|
|
150297
|
+
const status = tokens[i];
|
|
150298
|
+
const path4 = tokens[i + 1];
|
|
150299
|
+
if (status === void 0 || path4 === void 0) break;
|
|
150300
|
+
if (status === "U") {
|
|
150301
|
+
throw new Error(
|
|
150302
|
+
`'${path4}' has unresolved merge conflicts. resolve the conflicts, stage the result with git add, then retry.`
|
|
150303
|
+
);
|
|
150304
|
+
}
|
|
150305
|
+
byPath.set(path4, { path: path4, deleted: status === "D" });
|
|
150306
|
+
}
|
|
150307
|
+
const porcelain = $("git", ["status", "--porcelain=v1", "-z", "-uall", "--no-renames"], {
|
|
150308
|
+
log: false
|
|
150309
|
+
});
|
|
150310
|
+
for (const entry of porcelain.split("\0")) {
|
|
150311
|
+
if (entry.startsWith("?? ") && !byPath.has(entry.slice(3))) {
|
|
150312
|
+
const path4 = entry.slice(3);
|
|
150313
|
+
byPath.set(path4, { path: path4, deleted: false });
|
|
150314
|
+
}
|
|
150315
|
+
}
|
|
150316
|
+
return [...byPath.values()];
|
|
150317
|
+
}
|
|
150318
|
+
async function assertApiCommittable(files) {
|
|
150319
|
+
const present = files.filter((f) => !f.deleted).map((f) => f.path);
|
|
150320
|
+
if (present.length === 0) return;
|
|
150321
|
+
const root = getRepoRoot();
|
|
150322
|
+
const attrs = $(
|
|
150323
|
+
"git",
|
|
150324
|
+
["check-attr", "filter", "-z", "--", ...present.map((p) => join11(root, p))],
|
|
150325
|
+
{ log: false }
|
|
150326
|
+
);
|
|
150327
|
+
const parts = attrs.split("\0");
|
|
150328
|
+
for (let i = 0; i + 2 < parts.length; i += 3) {
|
|
150329
|
+
if (parts[i + 2] === "lfs") {
|
|
150330
|
+
throw new Error(
|
|
150331
|
+
`'${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.`
|
|
150332
|
+
);
|
|
150333
|
+
}
|
|
150334
|
+
}
|
|
150335
|
+
for (const path4 of present) {
|
|
150336
|
+
const stat = await lstat(join11(root, path4));
|
|
150337
|
+
if (stat.isDirectory()) {
|
|
150338
|
+
throw new Error(
|
|
150339
|
+
`'${path4}' is a directory (nested repository or submodule?) \u2014 signed commits only support files and symlinks.`
|
|
150340
|
+
);
|
|
150341
|
+
}
|
|
150342
|
+
}
|
|
150343
|
+
}
|
|
150344
|
+
async function createBlobEntry(params) {
|
|
150345
|
+
const absPath = join11(params.repoRoot, params.path);
|
|
150346
|
+
const stat = await lstat(absPath);
|
|
150347
|
+
if (stat.size > MAX_BLOB_BYTES) {
|
|
150348
|
+
throw new Error(
|
|
150349
|
+
`'${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.`
|
|
150350
|
+
);
|
|
150351
|
+
}
|
|
150352
|
+
let mode;
|
|
150353
|
+
let content;
|
|
150354
|
+
if (stat.isSymbolicLink()) {
|
|
150355
|
+
mode = "120000";
|
|
150356
|
+
content = await readlink(absPath, { encoding: "buffer" });
|
|
150357
|
+
} else {
|
|
150358
|
+
mode = stat.mode & 64 ? "100755" : "100644";
|
|
150359
|
+
const cleanSha = $("git", ["hash-object", "-w", "--", absPath], { log: false }).trim();
|
|
150360
|
+
content = execFileSync6("git", ["cat-file", "blob", cleanSha], {
|
|
150361
|
+
cwd: params.repoRoot,
|
|
150362
|
+
maxBuffer: 2 * MAX_BLOB_BYTES
|
|
150363
|
+
});
|
|
150364
|
+
}
|
|
150365
|
+
const path4 = `${params.repoPath}/git/blobs`;
|
|
150366
|
+
let result;
|
|
150367
|
+
for (const delayMs of [0, 1e3, 3e3]) {
|
|
150368
|
+
if (delayMs > 0) await new Promise((r) => setTimeout(r, delayMs));
|
|
150369
|
+
result = await gh({
|
|
150370
|
+
token: params.token,
|
|
150371
|
+
method: "POST",
|
|
150372
|
+
path: path4,
|
|
150373
|
+
body: { content: content.toString("base64"), encoding: "base64" }
|
|
150374
|
+
});
|
|
150375
|
+
if (result.status === 201) {
|
|
150376
|
+
const blob = result.json;
|
|
150377
|
+
return { path: params.path, mode, type: "blob", sha: blob.sha };
|
|
150378
|
+
}
|
|
150379
|
+
if (!isRetryable(result.status)) break;
|
|
150380
|
+
log.info(`blob upload for ${params.path} got ${result.status}, retrying`);
|
|
150381
|
+
}
|
|
150382
|
+
if (!result) throw new Error(`POST ${path4} failed`);
|
|
150383
|
+
throw ghError("POST", path4, result);
|
|
150384
|
+
}
|
|
150385
|
+
async function updateRefWithRetry(params) {
|
|
150386
|
+
const path4 = `${params.repoPath}/git/refs/heads/${encodeBranchPath(params.remoteBranch)}`;
|
|
150387
|
+
let lastResult;
|
|
150388
|
+
for (const delayMs of [0, 1e3, 3e3]) {
|
|
150389
|
+
if (delayMs > 0) await new Promise((r) => setTimeout(r, delayMs));
|
|
150390
|
+
const result = await gh({
|
|
150391
|
+
token: params.token,
|
|
150392
|
+
method: "PATCH",
|
|
150393
|
+
path: path4,
|
|
150394
|
+
body: { sha: params.sha, force: false }
|
|
150395
|
+
});
|
|
150396
|
+
if (result.status === 200) return;
|
|
150397
|
+
if (result.status === 422) {
|
|
150398
|
+
const detail = JSON.stringify(result.json);
|
|
150399
|
+
if (/fast.forward/i.test(detail)) {
|
|
150400
|
+
throw new Error(
|
|
150401
|
+
`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.`
|
|
150402
|
+
);
|
|
150403
|
+
}
|
|
150404
|
+
throw ghError("PATCH", path4, result);
|
|
150405
|
+
}
|
|
150406
|
+
lastResult = result;
|
|
150407
|
+
if (!isRetryable(result.status)) break;
|
|
150408
|
+
log.info(`ref update got ${result.status}, retrying`);
|
|
150409
|
+
}
|
|
150410
|
+
if (lastResult) throw ghError("PATCH", path4, lastResult);
|
|
150411
|
+
throw new Error(`PATCH ${path4} failed`);
|
|
150412
|
+
}
|
|
150413
|
+
function encodeBranchPath(branch) {
|
|
150414
|
+
return branch.split("/").map(encodeURIComponent).join("/");
|
|
150415
|
+
}
|
|
150416
|
+
function validateRemoteBranch(branch) {
|
|
150417
|
+
const bad = branch.startsWith("-") || branch.startsWith("/") || branch.endsWith("/") || branch.includes("..") || branch.includes("@{") || /[\s~^:?*[\]\\]/.test(branch);
|
|
150418
|
+
if (bad) throw new Error(`invalid remote branch name '${branch}'`);
|
|
150419
|
+
}
|
|
150420
|
+
async function createSignedCommit(params) {
|
|
150421
|
+
validateRemoteBranch(params.remoteBranch);
|
|
150422
|
+
const repoPath = `/repos/${params.owner}/${params.repo}`;
|
|
150423
|
+
const branchPath = `${repoPath}/git/ref/heads/${encodeBranchPath(params.remoteBranch)}`;
|
|
150424
|
+
const refResult = await gh({ token: params.token, method: "GET", path: branchPath });
|
|
150425
|
+
const branchExists = refResult.status === 200;
|
|
150426
|
+
if (branchExists) {
|
|
150427
|
+
const ref = refResult.json;
|
|
150428
|
+
const remoteTip = ref.object.sha;
|
|
150429
|
+
if (!params.parents.includes(remoteTip)) {
|
|
150430
|
+
throw new Error(
|
|
150431
|
+
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.`
|
|
150432
|
+
);
|
|
150433
|
+
}
|
|
150434
|
+
} else if (refResult.status !== 404) {
|
|
150435
|
+
throw ghError("GET", branchPath, refResult);
|
|
150436
|
+
}
|
|
150437
|
+
const baseParent = params.parents[0];
|
|
150438
|
+
if (!baseParent) throw new Error("createSignedCommit requires at least one parent");
|
|
150439
|
+
const baseTree = $("git", ["rev-parse", `${baseParent}^{tree}`], { log: false }).trim();
|
|
150440
|
+
let treeSha = baseTree;
|
|
150441
|
+
if (params.files.length > 0) {
|
|
150442
|
+
const repoRoot = getRepoRoot();
|
|
150443
|
+
const additions = params.files.filter((f) => !f.deleted);
|
|
150444
|
+
const blobEntries = [];
|
|
150445
|
+
for (let i = 0; i < additions.length; i += BLOB_UPLOAD_CONCURRENCY) {
|
|
150446
|
+
const chunk = additions.slice(i, i + BLOB_UPLOAD_CONCURRENCY);
|
|
150447
|
+
blobEntries.push(
|
|
150448
|
+
...await Promise.all(
|
|
150449
|
+
chunk.map(
|
|
150450
|
+
(f) => createBlobEntry({ token: params.token, repoPath, repoRoot, path: f.path })
|
|
150451
|
+
)
|
|
150452
|
+
)
|
|
150453
|
+
);
|
|
150454
|
+
}
|
|
150455
|
+
const deletionEntries = params.files.filter((f) => f.deleted).map((f) => ({ path: f.path, mode: "100644", type: "blob", sha: null }));
|
|
150456
|
+
const treeResult = await gh({
|
|
150457
|
+
token: params.token,
|
|
150458
|
+
method: "POST",
|
|
150459
|
+
path: `${repoPath}/git/trees`,
|
|
150460
|
+
body: { base_tree: baseTree, tree: [...blobEntries, ...deletionEntries] }
|
|
150461
|
+
});
|
|
150462
|
+
if (treeResult.status !== 201) {
|
|
150463
|
+
throw wrapUnknownBaseError("POST", `${repoPath}/git/trees`, treeResult, params.remoteBranch);
|
|
150464
|
+
}
|
|
150465
|
+
treeSha = treeResult.json.sha;
|
|
150466
|
+
}
|
|
150467
|
+
const commitResult = await gh({
|
|
150468
|
+
token: params.token,
|
|
150469
|
+
method: "POST",
|
|
150470
|
+
path: `${repoPath}/git/commits`,
|
|
150471
|
+
body: { message: params.message, tree: treeSha, parents: params.parents }
|
|
150472
|
+
});
|
|
150473
|
+
if (commitResult.status !== 201) {
|
|
150474
|
+
throw wrapUnknownBaseError(
|
|
150475
|
+
"POST",
|
|
150476
|
+
`${repoPath}/git/commits`,
|
|
150477
|
+
commitResult,
|
|
150478
|
+
params.remoteBranch
|
|
150479
|
+
);
|
|
150480
|
+
}
|
|
150481
|
+
const commit = commitResult.json;
|
|
150482
|
+
if (branchExists) {
|
|
150483
|
+
await updateRefWithRetry({
|
|
150484
|
+
token: params.token,
|
|
150485
|
+
repoPath,
|
|
150486
|
+
remoteBranch: params.remoteBranch,
|
|
150487
|
+
sha: commit.sha
|
|
150488
|
+
});
|
|
150489
|
+
} else {
|
|
150490
|
+
const createResult = await gh({
|
|
150491
|
+
token: params.token,
|
|
150492
|
+
method: "POST",
|
|
150493
|
+
path: `${repoPath}/git/refs`,
|
|
150494
|
+
body: { ref: `refs/heads/${params.remoteBranch}`, sha: commit.sha }
|
|
150495
|
+
});
|
|
150496
|
+
if (createResult.status !== 201) {
|
|
150497
|
+
throw ghError("POST", `${repoPath}/git/refs`, createResult);
|
|
150498
|
+
}
|
|
150499
|
+
}
|
|
150500
|
+
return { sha: commit.sha, createdBranch: !branchExists };
|
|
150501
|
+
}
|
|
150502
|
+
function isAncestorOfHead(sha) {
|
|
150503
|
+
try {
|
|
150504
|
+
$("git", ["merge-base", "--is-ancestor", sha, "HEAD"], { log: false });
|
|
150505
|
+
return true;
|
|
150506
|
+
} catch {
|
|
150507
|
+
return false;
|
|
150508
|
+
}
|
|
150509
|
+
}
|
|
150510
|
+
function wrapUnknownBaseError(method, path4, result, remoteBranch) {
|
|
150511
|
+
if (result.status === 404 || result.status === 422) {
|
|
150512
|
+
return new Error(
|
|
150513
|
+
`${ghError(method, path4, result).message}
|
|
150514
|
+
|
|
150515
|
+
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.`
|
|
150516
|
+
);
|
|
150517
|
+
}
|
|
150518
|
+
return ghError(method, path4, result);
|
|
150519
|
+
}
|
|
150520
|
+
|
|
150521
|
+
// mcp/git.ts
|
|
150132
150522
|
function getPushDestination(branch, storedDest) {
|
|
150133
150523
|
if (storedDest && storedDest.localBranch === branch) {
|
|
150134
150524
|
log.debug(`using stored push destination: ${storedDest.remoteName}/${storedDest.remoteBranch}`);
|
|
@@ -150186,6 +150576,10 @@ function validateTagName(tag) {
|
|
|
150186
150576
|
);
|
|
150187
150577
|
}
|
|
150188
150578
|
}
|
|
150579
|
+
function pushesToBaseRepo(ctx) {
|
|
150580
|
+
const baseUrl = `https://github.com/${ctx.repo.owner}/${ctx.repo.name}.git`;
|
|
150581
|
+
return normalizeUrl(ctx.toolState.pushUrl ?? "") === normalizeUrl(baseUrl);
|
|
150582
|
+
}
|
|
150189
150583
|
function validatePushDestination(ctx, branch) {
|
|
150190
150584
|
const pushUrl = ctx.toolState.pushUrl;
|
|
150191
150585
|
if (!pushUrl) throw new Error("pushUrl not set - setupGit must run before push_branch");
|
|
@@ -150204,6 +150598,47 @@ var PushBranch = type({
|
|
|
150204
150598
|
branchName: type.string.describe("The branch name to push (defaults to current branch)").optional(),
|
|
150205
150599
|
force: type.boolean.describe("Force push (use with caution)").default(false)
|
|
150206
150600
|
});
|
|
150601
|
+
function assertPushTarget(ctx, branch, pushDest) {
|
|
150602
|
+
const prBranchMatch = branch.match(/^pr-(\d+)$/);
|
|
150603
|
+
if (prBranchMatch && pushDest.remoteBranch !== branch) {
|
|
150604
|
+
const prNumber = Number(prBranchMatch[1]);
|
|
150605
|
+
const event = ctx.payload.event;
|
|
150606
|
+
const runScoped = event.is_pr === true && event.issue_number === prNumber;
|
|
150607
|
+
if (!runScoped) {
|
|
150608
|
+
throw new Error(
|
|
150609
|
+
`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.`
|
|
150610
|
+
);
|
|
150611
|
+
}
|
|
150612
|
+
}
|
|
150613
|
+
const defaultBranch = ctx.repo.data.default_branch || "main";
|
|
150614
|
+
if (ctx.payload.push === "restricted" && pushDest.remoteBranch === defaultBranch) {
|
|
150615
|
+
throw new Error(
|
|
150616
|
+
`Push blocked: cannot push directly to default branch '${pushDest.remoteBranch}'. Create a feature branch and open a PR instead.`
|
|
150617
|
+
);
|
|
150618
|
+
}
|
|
150619
|
+
}
|
|
150620
|
+
async function runPrepushHook(ctx, retryTool) {
|
|
150621
|
+
if (ctx.toolState.prepushFailureCount > 0) {
|
|
150622
|
+
log.info(`\xBB skipping prepush hook (failed earlier this run)`);
|
|
150623
|
+
return true;
|
|
150624
|
+
}
|
|
150625
|
+
if (!ctx.prepushScript) return false;
|
|
150626
|
+
const prepushHook = await executeLifecycleHook({
|
|
150627
|
+
event: "prepush",
|
|
150628
|
+
script: ctx.prepushScript
|
|
150629
|
+
});
|
|
150630
|
+
if (prepushHook.failure) {
|
|
150631
|
+
ctx.toolState.prepushFailureCount += 1;
|
|
150632
|
+
throw new Error(
|
|
150633
|
+
buildPrepushFailureMessage({
|
|
150634
|
+
failure: prepushHook.failure,
|
|
150635
|
+
shell: ctx.payload.shell,
|
|
150636
|
+
retryTool
|
|
150637
|
+
})
|
|
150638
|
+
);
|
|
150639
|
+
}
|
|
150640
|
+
return false;
|
|
150641
|
+
}
|
|
150207
150642
|
var CONCURRENT_PUSH_PATTERNS = ["fetch first", "non-fast-forward", "cannot lock ref"];
|
|
150208
150643
|
var TRANSIENT_PATTERNS = [
|
|
150209
150644
|
/RPC failed/i,
|
|
@@ -150263,7 +150698,6 @@ async function pushWithRetry(args2, token) {
|
|
|
150263
150698
|
throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
|
|
150264
150699
|
}
|
|
150265
150700
|
function PushBranchTool(ctx) {
|
|
150266
|
-
const defaultBranch = ctx.repo.data.default_branch || "main";
|
|
150267
150701
|
const pushPermission = ctx.payload.push;
|
|
150268
150702
|
return tool({
|
|
150269
150703
|
name: "push_branch",
|
|
@@ -150275,6 +150709,11 @@ function PushBranchTool(ctx) {
|
|
|
150275
150709
|
}
|
|
150276
150710
|
const branch = branchName || $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false });
|
|
150277
150711
|
rejectSpecialRef(branch, "branch");
|
|
150712
|
+
if (ctx.signedCommits && pushesToBaseRepo(ctx)) {
|
|
150713
|
+
throw new Error(
|
|
150714
|
+
"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."
|
|
150715
|
+
);
|
|
150716
|
+
}
|
|
150278
150717
|
const status = $("git", ["status", "--porcelain"], { log: false });
|
|
150279
150718
|
if (status) {
|
|
150280
150719
|
throw new Error(
|
|
@@ -150285,36 +150724,11 @@ ${status}` + (ctx.toolState.prepushFailureCount > 0 ? "\n\nnote: the prepush hoo
|
|
|
150285
150724
|
);
|
|
150286
150725
|
}
|
|
150287
150726
|
const pushDest = validatePushDestination(ctx, branch);
|
|
150288
|
-
|
|
150289
|
-
if (prBranchMatch && pushDest.remoteBranch !== branch) {
|
|
150290
|
-
const prNumber = Number(prBranchMatch[1]);
|
|
150291
|
-
const event = ctx.payload.event;
|
|
150292
|
-
const runScoped = event.is_pr === true && event.issue_number === prNumber;
|
|
150293
|
-
if (!runScoped) {
|
|
150294
|
-
throw new Error(
|
|
150295
|
-
`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.`
|
|
150296
|
-
);
|
|
150297
|
-
}
|
|
150298
|
-
}
|
|
150299
|
-
if (pushPermission === "restricted" && pushDest.remoteBranch === defaultBranch) {
|
|
150300
|
-
throw new Error(
|
|
150301
|
-
`Push blocked: cannot push directly to default branch '${pushDest.remoteBranch}'. Create a feature branch and open a PR instead.`
|
|
150302
|
-
);
|
|
150303
|
-
}
|
|
150727
|
+
assertPushTarget(ctx, branch, pushDest);
|
|
150304
150728
|
const refspec = branch === pushDest.remoteBranch ? branch : `${branch}:${pushDest.remoteBranch}`;
|
|
150305
150729
|
const pushArgs = force ? ["--force", "-u", pushDest.remoteName, refspec] : ["-u", pushDest.remoteName, refspec];
|
|
150306
|
-
const prepushSkipped = ctx
|
|
150307
|
-
if (prepushSkipped) {
|
|
150308
|
-
log.info(`\xBB skipping prepush hook (failed earlier this run)`);
|
|
150309
|
-
} else if (ctx.prepushScript) {
|
|
150310
|
-
const prepushHook = await executeLifecycleHook({
|
|
150311
|
-
event: "prepush",
|
|
150312
|
-
script: ctx.prepushScript
|
|
150313
|
-
});
|
|
150314
|
-
if (prepushHook.failure) {
|
|
150315
|
-
ctx.toolState.prepushFailureCount += 1;
|
|
150316
|
-
throw new Error(buildPrepushFailureMessage(prepushHook.failure, ctx.payload.shell));
|
|
150317
|
-
}
|
|
150730
|
+
const prepushSkipped = await runPrepushHook(ctx, "push_branch");
|
|
150731
|
+
if (!prepushSkipped && ctx.prepushScript) {
|
|
150318
150732
|
const postHookStatus = $("git", ["status", "--porcelain"], { log: false });
|
|
150319
150733
|
if (postHookStatus) {
|
|
150320
150734
|
throw new Error(
|
|
@@ -150365,15 +150779,138 @@ ${integrateStep}
|
|
|
150365
150779
|
})
|
|
150366
150780
|
});
|
|
150367
150781
|
}
|
|
150368
|
-
function buildPrepushFailureMessage(
|
|
150782
|
+
function buildPrepushFailureMessage(params) {
|
|
150783
|
+
const failure = params.failure;
|
|
150369
150784
|
const header = failure.kind === "exit" ? `prepush hook failed with exit code ${failure.exitCode}.
|
|
150370
150785
|
|
|
150371
150786
|
script output:
|
|
150372
150787
|
${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}.`;
|
|
150373
|
-
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 (
|
|
150788
|
+
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).`;
|
|
150374
150789
|
return `${header}
|
|
150375
150790
|
|
|
150376
|
-
this repo's prepush hook is best-effort: the next
|
|
150791
|
+
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}`;
|
|
150792
|
+
}
|
|
150793
|
+
function buildNothingToCommitMessage(pushDest) {
|
|
150794
|
+
const base = "nothing to commit \u2014 the working tree matches HEAD.";
|
|
150795
|
+
try {
|
|
150796
|
+
const remoteTip = $(
|
|
150797
|
+
"git",
|
|
150798
|
+
["rev-parse", `refs/remotes/${pushDest.remoteName}/${pushDest.remoteBranch}`],
|
|
150799
|
+
{ log: false }
|
|
150800
|
+
).trim();
|
|
150801
|
+
const head = $("git", ["rev-parse", "HEAD"], { log: false }).trim();
|
|
150802
|
+
if (remoteTip === head) {
|
|
150803
|
+
return `${base} your work is already on ${pushDest.remoteName}/${pushDest.remoteBranch} \u2014 there is no push step in signed-commits mode.`;
|
|
150804
|
+
}
|
|
150805
|
+
$("git", ["merge-base", "--is-ancestor", remoteTip, "HEAD"], { log: false });
|
|
150806
|
+
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.`;
|
|
150807
|
+
} catch {
|
|
150808
|
+
return base;
|
|
150809
|
+
}
|
|
150810
|
+
}
|
|
150811
|
+
var CommitChanges = type({
|
|
150812
|
+
message: type.string.describe("Commit message (first line = subject)"),
|
|
150813
|
+
files: type.string.array().describe("Optional subset of changed paths to commit. Defaults to every working-tree change.").optional()
|
|
150814
|
+
});
|
|
150815
|
+
function CommitChangesTool(ctx) {
|
|
150816
|
+
const pushPermission = ctx.payload.push;
|
|
150817
|
+
return tool({
|
|
150818
|
+
name: "commit_changes",
|
|
150819
|
+
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.",
|
|
150820
|
+
parameters: CommitChanges,
|
|
150821
|
+
timeoutMs: 6e5,
|
|
150822
|
+
execute: execute(async (params) => {
|
|
150823
|
+
if (pushPermission === "disabled") {
|
|
150824
|
+
throw new Error("Push is disabled. This repository is configured for read-only access.");
|
|
150825
|
+
}
|
|
150826
|
+
const branch = $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false }).trim();
|
|
150827
|
+
if (branch === "HEAD") {
|
|
150828
|
+
throw new Error(
|
|
150829
|
+
"HEAD is detached \u2014 create or check out a branch before committing (e.g. git checkout -b pullfrog/<description>)."
|
|
150830
|
+
);
|
|
150831
|
+
}
|
|
150832
|
+
rejectSpecialRef(branch, "branch");
|
|
150833
|
+
const pushDest = validatePushDestination(ctx, branch);
|
|
150834
|
+
if (!pushesToBaseRepo(ctx)) {
|
|
150835
|
+
throw new Error(
|
|
150836
|
+
`'${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).`
|
|
150837
|
+
);
|
|
150838
|
+
}
|
|
150839
|
+
assertPushTarget(ctx, branch, pushDest);
|
|
150840
|
+
const prepushSkipped = await runPrepushHook(ctx, "commit_changes");
|
|
150841
|
+
const head = $("git", ["rev-parse", "HEAD"], { log: false }).trim();
|
|
150842
|
+
let mergeHead = "";
|
|
150843
|
+
try {
|
|
150844
|
+
mergeHead = $("git", ["rev-parse", "-q", "--verify", "MERGE_HEAD"], { log: false }).trim();
|
|
150845
|
+
} catch {
|
|
150846
|
+
}
|
|
150847
|
+
let changes = detectWorkingTreeChanges();
|
|
150848
|
+
if (params.files) {
|
|
150849
|
+
if (mergeHead) {
|
|
150850
|
+
throw new Error(
|
|
150851
|
+
"can't commit a subset of files while a merge is in progress \u2014 the merge commit must include every merged change. omit `files`."
|
|
150852
|
+
);
|
|
150853
|
+
}
|
|
150854
|
+
const requested = new Set(params.files);
|
|
150855
|
+
const known = new Set(changes.map((c) => c.path));
|
|
150856
|
+
const unknown4 = [...requested].filter((p) => !known.has(p));
|
|
150857
|
+
if (unknown4.length > 0) {
|
|
150858
|
+
throw new Error(
|
|
150859
|
+
`no detected change at: ${unknown4.join(", ")} \u2014 run git status to list changed paths.`
|
|
150860
|
+
);
|
|
150861
|
+
}
|
|
150862
|
+
changes = changes.filter((c) => requested.has(c.path));
|
|
150863
|
+
}
|
|
150864
|
+
if (changes.length === 0 && !mergeHead) {
|
|
150865
|
+
throw new Error(buildNothingToCommitMessage(pushDest));
|
|
150866
|
+
}
|
|
150867
|
+
await assertApiCommittable(changes);
|
|
150868
|
+
const parents = mergeHead ? [head, mergeHead] : [head];
|
|
150869
|
+
const result = await createSignedCommit({
|
|
150870
|
+
token: ctx.gitToken,
|
|
150871
|
+
owner: ctx.repo.owner,
|
|
150872
|
+
repo: ctx.repo.name,
|
|
150873
|
+
remoteBranch: pushDest.remoteBranch,
|
|
150874
|
+
message: params.message,
|
|
150875
|
+
parents,
|
|
150876
|
+
files: changes
|
|
150877
|
+
});
|
|
150878
|
+
await $git(
|
|
150879
|
+
"fetch",
|
|
150880
|
+
[
|
|
150881
|
+
"--no-tags",
|
|
150882
|
+
"origin",
|
|
150883
|
+
`+refs/heads/${pushDest.remoteBranch}:refs/remotes/origin/${pushDest.remoteBranch}`
|
|
150884
|
+
],
|
|
150885
|
+
{ token: ctx.gitToken }
|
|
150886
|
+
);
|
|
150887
|
+
$("git", ["update-ref", `refs/heads/${branch}`, result.sha], { log: false });
|
|
150888
|
+
if (mergeHead) {
|
|
150889
|
+
$("git", ["merge", "--quit"], { log: false });
|
|
150890
|
+
}
|
|
150891
|
+
$("git", ["reset", "-q"], { log: false });
|
|
150892
|
+
if (result.createdBranch) {
|
|
150893
|
+
$("git", ["config", `branch.${branch}.remote`, "origin"], { log: false });
|
|
150894
|
+
$("git", ["config", `branch.${branch}.merge`, `refs/heads/${pushDest.remoteBranch}`], {
|
|
150895
|
+
log: false
|
|
150896
|
+
});
|
|
150897
|
+
}
|
|
150898
|
+
log.info(
|
|
150899
|
+
`\xBB created signed commit ${result.sha.slice(0, 7)} (${changes.length} file(s)) on ${pushDest.remoteName}/${pushDest.remoteBranch}`
|
|
150900
|
+
);
|
|
150901
|
+
return {
|
|
150902
|
+
success: true,
|
|
150903
|
+
sha: result.sha,
|
|
150904
|
+
branch,
|
|
150905
|
+
remoteBranch: pushDest.remoteBranch,
|
|
150906
|
+
files: changes.map((c) => c.deleted ? `D ${c.path}` : c.path),
|
|
150907
|
+
createdBranch: result.createdBranch,
|
|
150908
|
+
verified: true,
|
|
150909
|
+
prepushSkipped,
|
|
150910
|
+
message: `created signed commit ${result.sha.slice(0, 7)} on ${pushDest.remoteName}/${pushDest.remoteBranch}${result.createdBranch ? " (remote branch created)" : ""}`
|
|
150911
|
+
};
|
|
150912
|
+
})
|
|
150913
|
+
});
|
|
150377
150914
|
}
|
|
150378
150915
|
var AUTH_REQUIRED_REDIRECT = {
|
|
150379
150916
|
push: "use the push_branch tool instead \u2014 it handles authentication and permission checks.",
|
|
@@ -150446,7 +150983,7 @@ function countAhead(head, base) {
|
|
|
150446
150983
|
function spillGitOutput(params) {
|
|
150447
150984
|
const tempDir = process.env.PULLFROG_TEMP_DIR;
|
|
150448
150985
|
if (!tempDir) throw new Error("PULLFROG_TEMP_DIR not set");
|
|
150449
|
-
const outputPath =
|
|
150986
|
+
const outputPath = join12(tempDir, `git-${params.command}-${randomUUID3().slice(0, 8)}.txt`);
|
|
150450
150987
|
writeFileSync6(outputPath, params.output);
|
|
150451
150988
|
const previewByLines = params.output.split("\n").slice(0, OVERFLOW_PREVIEW_LINES).join("\n");
|
|
150452
150989
|
const preview = previewByLines.length <= OVERFLOW_PREVIEW_MAX_CHARS ? previewByLines : `${previewByLines.slice(0, OVERFLOW_PREVIEW_MAX_CHARS)}\u2026`;
|
|
@@ -150480,8 +151017,30 @@ function GitTool(ctx) {
|
|
|
150480
151017
|
}
|
|
150481
151018
|
const redirect = AUTH_REQUIRED_REDIRECT[command];
|
|
150482
151019
|
if (redirect) {
|
|
151020
|
+
if (command === "push" && ctx.signedCommits) {
|
|
151021
|
+
throw new Error(
|
|
151022
|
+
"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)."
|
|
151023
|
+
);
|
|
151024
|
+
}
|
|
150483
151025
|
throw new Error(`git ${command} is not available through this tool \u2014 ${redirect}`);
|
|
150484
151026
|
}
|
|
151027
|
+
if (ctx.signedCommits && (command === "commit" || command === "merge")) {
|
|
151028
|
+
if (pushesToBaseRepo(ctx)) {
|
|
151029
|
+
if (command === "commit") {
|
|
151030
|
+
throw new Error(
|
|
151031
|
+
"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."
|
|
151032
|
+
);
|
|
151033
|
+
}
|
|
151034
|
+
const noLocalCommit = args2.some(
|
|
151035
|
+
(a) => a === "--no-commit" || a === "--abort" || a === "--quit"
|
|
151036
|
+
);
|
|
151037
|
+
if (!noLocalCommit) {
|
|
151038
|
+
throw new Error(
|
|
151039
|
+
"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."
|
|
151040
|
+
);
|
|
151041
|
+
}
|
|
151042
|
+
}
|
|
151043
|
+
}
|
|
150485
151044
|
if (ctx.payload.shell === "disabled") {
|
|
150486
151045
|
const blocked = NOSHELL_BLOCKED_SUBCOMMANDS[command];
|
|
150487
151046
|
if (blocked) {
|
|
@@ -151114,7 +151673,6 @@ async function createAndSubmitWithFooter(ctx, params, opts) {
|
|
|
151114
151673
|
workflowRun: ctx.runId ? { owner: ctx.repo.owner, repo: ctx.repo.name, runId: ctx.runId, jobId: ctx.jobId } : void 0,
|
|
151115
151674
|
customParts,
|
|
151116
151675
|
model: ctx.toolState.model,
|
|
151117
|
-
fallbackFrom: ctx.toolState.modelFallback?.from,
|
|
151118
151676
|
oss: ctx.oss
|
|
151119
151677
|
});
|
|
151120
151678
|
return await ctx.octokit.rest.pulls.submitReview({
|
|
@@ -151147,12 +151705,12 @@ async function reportReviewNodeId(ctx, params) {
|
|
|
151147
151705
|
}
|
|
151148
151706
|
|
|
151149
151707
|
// utils/setup.ts
|
|
151150
|
-
import { execFileSync as
|
|
151708
|
+
import { execFileSync as execFileSync7, execSync as execSync2 } from "node:child_process";
|
|
151151
151709
|
import { mkdtempSync, readdirSync, realpathSync as realpathSync2, unlinkSync as unlinkSync2 } from "node:fs";
|
|
151152
151710
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
151153
|
-
import { join as
|
|
151711
|
+
import { join as join13 } from "node:path";
|
|
151154
151712
|
function createTempDirectory() {
|
|
151155
|
-
const sharedTempDir = mkdtempSync(
|
|
151713
|
+
const sharedTempDir = mkdtempSync(join13(tmpdir2(), "pullfrog-"));
|
|
151156
151714
|
process.env.PULLFROG_TEMP_DIR = sharedTempDir;
|
|
151157
151715
|
log.info(`\xBB created temp dir at ${sharedTempDir}`);
|
|
151158
151716
|
return sharedTempDir;
|
|
@@ -151197,13 +151755,13 @@ function wipeRunnerLeakSurface() {
|
|
|
151197
151755
|
return [];
|
|
151198
151756
|
}
|
|
151199
151757
|
};
|
|
151200
|
-
const fileCommandsDir =
|
|
151758
|
+
const fileCommandsDir = join13(runnerTemp, "_runner_file_commands");
|
|
151201
151759
|
for (const entry of listDir(fileCommandsDir)) {
|
|
151202
|
-
tryUnlink(
|
|
151760
|
+
tryUnlink(join13(fileCommandsDir, entry));
|
|
151203
151761
|
}
|
|
151204
151762
|
for (const entry of listDir(runnerTemp)) {
|
|
151205
151763
|
if (entry.endsWith(".sh") || /^git-credentials-.*\.config$/.test(entry)) {
|
|
151206
|
-
tryUnlink(
|
|
151764
|
+
tryUnlink(join13(runnerTemp, entry));
|
|
151207
151765
|
}
|
|
151208
151766
|
}
|
|
151209
151767
|
if (wiped.length > 0) {
|
|
@@ -151240,7 +151798,7 @@ function removeIncludeIfEntries(repoDir) {
|
|
|
151240
151798
|
if (!key || seen.has(key)) continue;
|
|
151241
151799
|
seen.add(key);
|
|
151242
151800
|
try {
|
|
151243
|
-
|
|
151801
|
+
execFileSync7("git", ["config", "--local", "--unset-all", key], {
|
|
151244
151802
|
cwd: repoDir,
|
|
151245
151803
|
stdio: "pipe",
|
|
151246
151804
|
env: env2
|
|
@@ -151712,7 +152270,7 @@ function CheckoutPrTool(ctx) {
|
|
|
151712
152270
|
headSha: ctx.toolState.checkoutSha
|
|
151713
152271
|
});
|
|
151714
152272
|
if (incremental) {
|
|
151715
|
-
incrementalDiffPath =
|
|
152273
|
+
incrementalDiffPath = join14(
|
|
151716
152274
|
tempDir,
|
|
151717
152275
|
`pr-${pull_number}-${beforeShort}-${headShort}-incremental.diff`
|
|
151718
152276
|
);
|
|
@@ -151726,7 +152284,7 @@ function CheckoutPrTool(ctx) {
|
|
|
151726
152284
|
const diffPreview = formatResult.content.split("\n").slice(0, 100).join("\n");
|
|
151727
152285
|
log.debug(`formatted diff preview (first 100 lines):
|
|
151728
152286
|
${diffPreview}`);
|
|
151729
|
-
const diffPath =
|
|
152287
|
+
const diffPath = join14(tempDir, `pr-${pull_number}-${headShort}.diff`);
|
|
151730
152288
|
writeFileSync7(diffPath, formatResult.content);
|
|
151731
152289
|
log.debug(`wrote diff to ${diffPath} (${formatResult.content.length} bytes)`);
|
|
151732
152290
|
ctx.toolState.diffCoverage = createDiffCoverageState({
|
|
@@ -151835,7 +152393,7 @@ ${dirty}`
|
|
|
151835
152393
|
|
|
151836
152394
|
// mcp/checkSuite.ts
|
|
151837
152395
|
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "node:fs";
|
|
151838
|
-
import { join as
|
|
152396
|
+
import { join as join15 } from "node:path";
|
|
151839
152397
|
var GetCheckSuiteLogs = type({
|
|
151840
152398
|
check_suite_id: type.number.describe("the id from check_suite.id")
|
|
151841
152399
|
});
|
|
@@ -151931,7 +152489,7 @@ function GetCheckSuiteLogsTool(ctx) {
|
|
|
151931
152489
|
if (!tempDir) {
|
|
151932
152490
|
throw new Error("PULLFROG_TEMP_DIR not set");
|
|
151933
152491
|
}
|
|
151934
|
-
const logsDir =
|
|
152492
|
+
const logsDir = join15(tempDir, "ci-logs");
|
|
151935
152493
|
mkdirSync7(logsDir, { recursive: true });
|
|
151936
152494
|
const jobResults = [];
|
|
151937
152495
|
for (const run of failedRuns) {
|
|
@@ -151958,7 +152516,7 @@ function GetCheckSuiteLogsTool(ctx) {
|
|
|
151958
152516
|
);
|
|
151959
152517
|
}
|
|
151960
152518
|
const logsText = await logsResult.text();
|
|
151961
|
-
const logPath =
|
|
152519
|
+
const logPath = join15(logsDir, `job-${job.id}.log`);
|
|
151962
152520
|
writeFileSync8(logPath, logsText);
|
|
151963
152521
|
const analysis = analyzeLog(logsText, 80);
|
|
151964
152522
|
const failedSteps = job.steps?.filter((s) => s.conclusion === "failure").map((s) => `Step ${s.number}: ${s.name}`) ?? [];
|
|
@@ -152008,7 +152566,7 @@ function GetCheckSuiteLogsTool(ctx) {
|
|
|
152008
152566
|
|
|
152009
152567
|
// mcp/commitInfo.ts
|
|
152010
152568
|
import { writeFileSync as writeFileSync9 } from "node:fs";
|
|
152011
|
-
import { join as
|
|
152569
|
+
import { join as join16 } from "node:path";
|
|
152012
152570
|
var CommitInfo = type({
|
|
152013
152571
|
sha: type.string.describe("the commit SHA (full or abbreviated) to fetch")
|
|
152014
152572
|
});
|
|
@@ -152032,7 +152590,7 @@ function CommitInfoTool(ctx) {
|
|
|
152032
152590
|
"PULLFROG_TEMP_DIR not set - get_commit_info must run in pullfrog action context"
|
|
152033
152591
|
);
|
|
152034
152592
|
}
|
|
152035
|
-
const diffFile =
|
|
152593
|
+
const diffFile = join16(tempDir, `commit-${sha.slice(0, 7)}.diff`);
|
|
152036
152594
|
writeFileSync9(diffFile, formatResult.content);
|
|
152037
152595
|
log.debug(`wrote commit diff to ${diffFile} (${formatResult.content.length} bytes)`);
|
|
152038
152596
|
return {
|
|
@@ -152539,7 +153097,6 @@ function buildPrBodyWithFooter(ctx, body) {
|
|
|
152539
153097
|
triggeredBy: true,
|
|
152540
153098
|
workflowRun: ctx.runId ? { owner: ctx.repo.owner, repo: ctx.repo.name, runId: ctx.runId, jobId: ctx.jobId } : void 0,
|
|
152541
153099
|
model: ctx.toolState.model,
|
|
152542
|
-
fallbackFrom: ctx.toolState.modelFallback?.from,
|
|
152543
153100
|
oss: ctx.oss
|
|
152544
153101
|
});
|
|
152545
153102
|
const bodyWithoutFooter = stripExistingFooter(fixDoubleEscapedString(body));
|
|
@@ -152694,7 +153251,7 @@ function PullRequestInfoTool(ctx) {
|
|
|
152694
153251
|
|
|
152695
153252
|
// mcp/reviewComments.ts
|
|
152696
153253
|
import { writeFileSync as writeFileSync10 } from "node:fs";
|
|
152697
|
-
import { join as
|
|
153254
|
+
import { join as join17 } from "node:path";
|
|
152698
153255
|
var REVIEW_THREADS_QUERY = `
|
|
152699
153256
|
query ($owner: String!, $name: String!, $prNumber: Int!) {
|
|
152700
153257
|
repository(owner: $owner, name: $name) {
|
|
@@ -153110,7 +153667,7 @@ function GetReviewCommentsTool(ctx) {
|
|
|
153110
153667
|
throw new Error("PULLFROG_TEMP_DIR not set");
|
|
153111
153668
|
}
|
|
153112
153669
|
const filename = `review-${params.review_id}-threads.md`;
|
|
153113
|
-
const commentsPath =
|
|
153670
|
+
const commentsPath = join17(tempDir, filename);
|
|
153114
153671
|
writeFileSync10(commentsPath, formatted.content);
|
|
153115
153672
|
log.debug(`wrote ${threadBlocks.length} threads to ${commentsPath}`);
|
|
153116
153673
|
return {
|
|
@@ -153348,7 +153905,7 @@ import { spawn as spawn2, spawnSync as spawnSync5 } from "node:child_process";
|
|
|
153348
153905
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
153349
153906
|
import { closeSync, openSync, writeFileSync as writeFileSync11 } from "node:fs";
|
|
153350
153907
|
import { userInfo as userInfo2 } from "node:os";
|
|
153351
|
-
import { join as
|
|
153908
|
+
import { join as join18 } from "node:path";
|
|
153352
153909
|
import { setTimeout as sleep2 } from "node:timers/promises";
|
|
153353
153910
|
var ShellParams = type({
|
|
153354
153911
|
command: "string",
|
|
@@ -153511,7 +154068,7 @@ function getTempDir() {
|
|
|
153511
154068
|
var MAX_OUTPUT_CHARS = 5e3;
|
|
153512
154069
|
function capOutput(output) {
|
|
153513
154070
|
if (output.length <= MAX_OUTPUT_CHARS) return output;
|
|
153514
|
-
const fullPath =
|
|
154071
|
+
const fullPath = join18(getTempDir(), `shell-${randomUUID4().slice(0, 8)}.log`);
|
|
153515
154072
|
writeFileSync11(fullPath, output);
|
|
153516
154073
|
const elided = output.length - MAX_OUTPUT_CHARS;
|
|
153517
154074
|
return `... [${elided} chars truncated; full output saved to ${fullPath}] ...
|
|
@@ -153566,8 +154123,8 @@ Do NOT use this tool for git commands \u2014 use the dedicated git tools instead
|
|
|
153566
154123
|
if (params.background) {
|
|
153567
154124
|
const tempDir = getTempDir();
|
|
153568
154125
|
const handle = `bg-${randomUUID4().slice(0, 8)}`;
|
|
153569
|
-
const outputPath =
|
|
153570
|
-
const pidPath =
|
|
154126
|
+
const outputPath = join18(tempDir, `${handle}.log`);
|
|
154127
|
+
const pidPath = join18(tempDir, `${handle}.pid`);
|
|
153571
154128
|
const logFd = openSync(outputPath, "a");
|
|
153572
154129
|
let proc2;
|
|
153573
154130
|
try {
|
|
@@ -153798,7 +154355,7 @@ function buildCommonTools(ctx, outputSchema) {
|
|
|
153798
154355
|
return tools;
|
|
153799
154356
|
}
|
|
153800
154357
|
function buildOrchestratorTools(ctx, outputSchema) {
|
|
153801
|
-
|
|
154358
|
+
const tools = [
|
|
153802
154359
|
...buildCommonTools(ctx, outputSchema),
|
|
153803
154360
|
ReportProgressTool(ctx),
|
|
153804
154361
|
SelectModeTool(ctx),
|
|
@@ -153808,6 +154365,10 @@ function buildOrchestratorTools(ctx, outputSchema) {
|
|
|
153808
154365
|
CreatePullRequestTool(ctx),
|
|
153809
154366
|
UpdatePullRequestBodyTool(ctx)
|
|
153810
154367
|
];
|
|
154368
|
+
if (ctx.signedCommits) {
|
|
154369
|
+
tools.push(CommitChangesTool(ctx));
|
|
154370
|
+
}
|
|
154371
|
+
return tools;
|
|
153811
154372
|
}
|
|
153812
154373
|
async function tryStartMcpServer(ctx, tools, port) {
|
|
153813
154374
|
const server = new FastMCP({ name: pullfrogMcpName, version: "0.0.1" });
|
|
@@ -153998,8 +154559,14 @@ var MISSING_KEY_MARKER = "no API key found";
|
|
|
153998
154559
|
function buildMissingApiKeyError(params) {
|
|
153999
154560
|
const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
|
|
154000
154561
|
const settingsUrl = `${getApiUrl()}/console/${params.owner}/${params.name}`;
|
|
154562
|
+
const envVars = params.model?.includes("/") ? getModelEnvVars(params.model) : [];
|
|
154563
|
+
const [primary, ...alternates] = envVars;
|
|
154564
|
+
const envVarList = primary ? `\`${primary}\`${alternates.length > 0 ? ` (or ${alternates.map((v) => `\`${v}\``).join(" / ")})` : ""}` : void 0;
|
|
154565
|
+
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.`;
|
|
154001
154566
|
return [
|
|
154002
|
-
|
|
154567
|
+
lead,
|
|
154568
|
+
"",
|
|
154569
|
+
"**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).",
|
|
154003
154570
|
"",
|
|
154004
154571
|
`[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)`
|
|
154005
154572
|
].join("\n");
|
|
@@ -154079,10 +154646,14 @@ function validateAgentApiKey(params) {
|
|
|
154079
154646
|
}
|
|
154080
154647
|
if (params.agent.name === "opencode") {
|
|
154081
154648
|
if (params.authorized.has(params.model)) return;
|
|
154082
|
-
throw new Error(
|
|
154649
|
+
throw new Error(
|
|
154650
|
+
buildMissingApiKeyError({ owner: params.owner, name: params.name, model: params.model })
|
|
154651
|
+
);
|
|
154083
154652
|
}
|
|
154084
154653
|
if (hasEnvVar3("ANTHROPIC_API_KEY") || hasEnvVar3("CLAUDE_CODE_OAUTH_TOKEN")) return;
|
|
154085
|
-
throw new Error(
|
|
154654
|
+
throw new Error(
|
|
154655
|
+
buildMissingApiKeyError({ owner: params.owner, name: params.name, model: params.model })
|
|
154656
|
+
);
|
|
154086
154657
|
}
|
|
154087
154658
|
if (params.agent.name === "opencode") {
|
|
154088
154659
|
if (params.authorized.size > 0) return;
|
|
@@ -154093,42 +154664,37 @@ function validateAgentApiKey(params) {
|
|
|
154093
154664
|
}
|
|
154094
154665
|
function isApiKeyAuthError(text) {
|
|
154095
154666
|
if (!text) return false;
|
|
154096
|
-
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);
|
|
154667
|
+
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);
|
|
154668
|
+
}
|
|
154669
|
+
function isOAuthCredentialExpiredError(text) {
|
|
154670
|
+
return /authentication token has expired/i.test(text) || /Token refresh failed/i.test(text);
|
|
154097
154671
|
}
|
|
154098
154672
|
function formatApiKeyErrorSummary(params) {
|
|
154099
154673
|
if (params.raw.includes(MISSING_KEY_MARKER)) {
|
|
154674
|
+
if (params.raw.startsWith(`**${MISSING_KEY_MARKER}**`)) return params.raw;
|
|
154100
154675
|
return buildMissingApiKeyError({ owner: params.owner, name: params.name });
|
|
154101
154676
|
}
|
|
154102
154677
|
const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
|
|
154103
154678
|
const settingsUrl = `${getApiUrl()}/console/${params.owner}/${params.name}`;
|
|
154679
|
+
if (isOAuthCredentialExpiredError(params.raw)) {
|
|
154680
|
+
return [
|
|
154681
|
+
`**Your provider OAuth credential has expired.** Re-authenticate the provider connection (e.g. \`pullfrog auth codex\`), then re-trigger the run.`,
|
|
154682
|
+
"",
|
|
154683
|
+
`[Model settings \u2192](${settingsUrl}) \xB7 [Setup docs \u2192](https://docs.pullfrog.com/keys) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
|
|
154684
|
+
].join("\n");
|
|
154685
|
+
}
|
|
154104
154686
|
return [
|
|
154105
|
-
`**Your LLM provider API key was rejected
|
|
154687
|
+
`**Your LLM provider API key was rejected.** Rotate the key in your provider dashboard, then update the matching GitHub Actions secret.`,
|
|
154106
154688
|
"",
|
|
154107
154689
|
`[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)`
|
|
154108
154690
|
].join("\n");
|
|
154109
154691
|
}
|
|
154110
154692
|
|
|
154111
|
-
// utils/byokFallback.ts
|
|
154112
|
-
var FREE_FALLBACK_SLUG = "opencode/big-pickle";
|
|
154113
|
-
function selectFallbackModelIfNeeded(input) {
|
|
154114
|
-
if (input.proxyModel) return { fallback: false };
|
|
154115
|
-
if (!input.resolvedModel) return { fallback: false };
|
|
154116
|
-
if (input.resolvedModel === FREE_FALLBACK_SLUG) return { fallback: false };
|
|
154117
|
-
if (!input.resolvedModel.includes("/")) return { fallback: false };
|
|
154118
|
-
if (input.agentName === "claude") return { fallback: false };
|
|
154119
|
-
if (input.authorized.has(input.resolvedModel)) return { fallback: false };
|
|
154120
|
-
return {
|
|
154121
|
-
fallback: true,
|
|
154122
|
-
from: input.resolvedModel,
|
|
154123
|
-
to: FREE_FALLBACK_SLUG
|
|
154124
|
-
};
|
|
154125
|
-
}
|
|
154126
|
-
|
|
154127
154693
|
// utils/gitAuthServer.ts
|
|
154128
154694
|
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
154129
154695
|
import { writeFileSync as writeFileSync12 } from "node:fs";
|
|
154130
154696
|
import { createServer as createServer3 } from "node:http";
|
|
154131
|
-
import { join as
|
|
154697
|
+
import { join as join19 } from "node:path";
|
|
154132
154698
|
var REVOKED_TRAP_MS = 6e4;
|
|
154133
154699
|
function revokeGitHubToken(token) {
|
|
154134
154700
|
fetch("https://api.github.com/installation/token", {
|
|
@@ -154197,7 +154763,7 @@ async function startGitAuthServer(tmpdir3) {
|
|
|
154197
154763
|
function writeAskpassScript(code) {
|
|
154198
154764
|
const scriptId = randomUUID5();
|
|
154199
154765
|
const scriptName = `askpass-${scriptId}.js`;
|
|
154200
|
-
const scriptPath =
|
|
154766
|
+
const scriptPath = join19(tmpdir3, scriptName);
|
|
154201
154767
|
const content = [
|
|
154202
154768
|
`#!/usr/bin/env node`,
|
|
154203
154769
|
`var a=process.argv[2]||"";`,
|
|
@@ -154235,7 +154801,7 @@ async function startGitAuthServer(tmpdir3) {
|
|
|
154235
154801
|
var core3 = __toESM(require_core(), 1);
|
|
154236
154802
|
import { createSign } from "node:crypto";
|
|
154237
154803
|
import { rename, writeFile } from "node:fs/promises";
|
|
154238
|
-
import { dirname as dirname3, join as
|
|
154804
|
+
import { dirname as dirname3, join as join20 } from "node:path";
|
|
154239
154805
|
|
|
154240
154806
|
// node_modules/.pnpm/@octokit+plugin-throttling@11.0.3_@octokit+core@7.0.5/node_modules/@octokit/plugin-throttling/dist-bundle/index.js
|
|
154241
154807
|
var import_light = __toESM(require_light(), 1);
|
|
@@ -157877,6 +158443,7 @@ var Octokit2 = Octokit.plugin(requestLog, legacyRestEndpointMethods, paginateRes
|
|
|
157877
158443
|
);
|
|
157878
158444
|
|
|
157879
158445
|
// utils/github.ts
|
|
158446
|
+
var OIDC_AUDIENCE = "pullfrog-api";
|
|
157880
158447
|
function isObject4(value2) {
|
|
157881
158448
|
return typeof value2 === "object" && value2 !== null;
|
|
157882
158449
|
}
|
|
@@ -157893,8 +158460,39 @@ var TokenExchangeError = class extends Error {
|
|
|
157893
158460
|
this.status = status;
|
|
157894
158461
|
}
|
|
157895
158462
|
};
|
|
158463
|
+
async function fetchIdTokenFromStash(creds) {
|
|
158464
|
+
const url4 = new URL(creds.requestUrl);
|
|
158465
|
+
url4.searchParams.set("audience", OIDC_AUDIENCE);
|
|
158466
|
+
const timeoutMs = 3e4;
|
|
158467
|
+
let response;
|
|
158468
|
+
try {
|
|
158469
|
+
response = await fetch(url4, {
|
|
158470
|
+
headers: { Authorization: `Bearer ${creds.requestToken}` },
|
|
158471
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
158472
|
+
});
|
|
158473
|
+
} catch (error49) {
|
|
158474
|
+
if (error49 instanceof Error && error49.name === "TimeoutError") {
|
|
158475
|
+
throw new Error(`ID token request timed out after ${timeoutMs}ms`);
|
|
158476
|
+
}
|
|
158477
|
+
throw error49;
|
|
158478
|
+
}
|
|
158479
|
+
if (!response.ok) {
|
|
158480
|
+
throw new TokenExchangeError(
|
|
158481
|
+
response.status,
|
|
158482
|
+
`Failed to get ID token: ${response.status} ${response.statusText}`
|
|
158483
|
+
);
|
|
158484
|
+
}
|
|
158485
|
+
const body = await response.json();
|
|
158486
|
+
if (!body.value) {
|
|
158487
|
+
throw new Error("ID token response has no value field");
|
|
158488
|
+
}
|
|
158489
|
+
if (isGitHubActions) {
|
|
158490
|
+
core3.setSecret(body.value);
|
|
158491
|
+
}
|
|
158492
|
+
return body.value;
|
|
158493
|
+
}
|
|
157896
158494
|
async function acquireTokenViaOIDC(opts) {
|
|
157897
|
-
const oidcToken = await core3.getIDToken(
|
|
158495
|
+
const oidcToken = opts?.oidc ? await fetchIdTokenFromStash(opts.oidc) : await core3.getIDToken(OIDC_AUDIENCE);
|
|
157898
158496
|
const repos = [...opts?.repos ?? []];
|
|
157899
158497
|
const targetRepo = process.env.GITHUB_REPOSITORY?.split("/")[1];
|
|
157900
158498
|
if (targetRepo) {
|
|
@@ -158043,14 +158641,15 @@ async function acquireTokenViaGitHubApp(opts) {
|
|
|
158043
158641
|
const installationId = await findInstallationId(jwt2, config3.repoOwner, config3.repoName);
|
|
158044
158642
|
return await createInstallationToken(jwt2, installationId, opts?.permissions);
|
|
158045
158643
|
}
|
|
158644
|
+
function isTransientTokenError(error49) {
|
|
158645
|
+
if (error49 instanceof TokenExchangeError) return error49.status >= 500 || error49.status === 429;
|
|
158646
|
+
return error49 instanceof Error && (error49.message.includes("timed out") || error49.message.includes("fetch failed") || error49.message.includes("ECONNRESET") || error49.message.includes("ETIMEDOUT"));
|
|
158647
|
+
}
|
|
158046
158648
|
async function acquireNewToken(opts) {
|
|
158047
|
-
if (isOIDCAvailable()) {
|
|
158649
|
+
if (opts?.oidc || isOIDCAvailable()) {
|
|
158048
158650
|
return await retry(() => acquireTokenViaOIDC(opts), {
|
|
158049
158651
|
label: "token exchange",
|
|
158050
|
-
shouldRetry:
|
|
158051
|
-
if (error49 instanceof TokenExchangeError) return error49.status >= 500 || error49.status === 429;
|
|
158052
|
-
return error49 instanceof Error && (error49.message.includes("timed out") || error49.message.includes("fetch failed") || error49.message.includes("ECONNRESET") || error49.message.includes("ETIMEDOUT"));
|
|
158053
|
-
}
|
|
158652
|
+
shouldRetry: isTransientTokenError
|
|
158054
158653
|
});
|
|
158055
158654
|
}
|
|
158056
158655
|
if (process.env.GITHUB_ACTIONS === "true") {
|
|
@@ -158093,14 +158692,14 @@ function getGitHubUsageSummary() {
|
|
|
158093
158692
|
}
|
|
158094
158693
|
async function writeGitHubUsageSummaryToFile(path4) {
|
|
158095
158694
|
const summary2 = getGitHubUsageSummary();
|
|
158096
|
-
const tmpPath =
|
|
158695
|
+
const tmpPath = join20(dirname3(path4), `.usage-summary-${process.pid}.tmp`);
|
|
158097
158696
|
await writeFile(tmpPath, JSON.stringify(summary2));
|
|
158098
158697
|
await rename(tmpPath, path4);
|
|
158099
158698
|
}
|
|
158100
|
-
function createOctokit(token) {
|
|
158699
|
+
function createOctokit(token, refreshAuth) {
|
|
158700
|
+
let currentToken = token;
|
|
158101
158701
|
const OctokitWithPlugins = Octokit2.plugin(throttling);
|
|
158102
158702
|
const octokit = new OctokitWithPlugins({
|
|
158103
|
-
auth: token,
|
|
158104
158703
|
throttle: {
|
|
158105
158704
|
onRateLimit: (_retryAfter, _options, _octokit, retryCount) => {
|
|
158106
158705
|
return retryCount <= 2;
|
|
@@ -158129,6 +158728,8 @@ function createOctokit(token) {
|
|
|
158129
158728
|
return response;
|
|
158130
158729
|
};
|
|
158131
158730
|
octokit.hook.wrap("request", async (request2, options) => {
|
|
158731
|
+
const sentToken = currentToken;
|
|
158732
|
+
options.headers.authorization = `token ${sentToken}`;
|
|
158132
158733
|
try {
|
|
158133
158734
|
const response = await request2(options);
|
|
158134
158735
|
onResponse(response);
|
|
@@ -158137,6 +158738,13 @@ function createOctokit(token) {
|
|
|
158137
158738
|
if (isObject4(error49) && "response" in error49 && isObject4(error49.response) && "headers" in error49.response && isObject4(error49.response.headers)) {
|
|
158138
158739
|
onResponse(error49.response);
|
|
158139
158740
|
}
|
|
158741
|
+
if (refreshAuth && isObject4(error49) && "status" in error49 && error49.status === 401) {
|
|
158742
|
+
currentToken = await refreshAuth(sentToken);
|
|
158743
|
+
options.headers.authorization = `token ${currentToken}`;
|
|
158744
|
+
const response = await request2(options);
|
|
158745
|
+
onResponse(response);
|
|
158746
|
+
return response;
|
|
158747
|
+
}
|
|
158140
158748
|
throw error49;
|
|
158141
158749
|
}
|
|
158142
158750
|
});
|
|
@@ -158335,7 +158943,17 @@ Use \`${t("git")}\` for local git commands (status, log, add, commit, checkout,
|
|
|
158335
158943
|
- \`${t("checkout_pr")}\` - checkout a PR branch (fetches and configures push for forks)
|
|
158336
158944
|
- \`${t("delete_branch")}\` - delete a remote branch (requires push: enabled)
|
|
158337
158945
|
- \`${t("push_tags")}\` - push tags (requires push: enabled)
|
|
158338
|
-
|
|
158946
|
+
${ctx.signedCommits ? `
|
|
158947
|
+
#### Signed commits (enabled for this repository)
|
|
158948
|
+
|
|
158949
|
+
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 \`${t("push_branch")}\`:
|
|
158950
|
+
- Do NOT use git commit or \`${t("push_branch")}\` for same-repo branches \u2014 both are blocked. Instead: edit files, then call \`${t("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.
|
|
158951
|
+
- New branches: create locally as usual (git checkout -b); the remote branch is created on the first \`${t("commit_changes")}\` call.
|
|
158952
|
+
- To integrate remote changes (concurrent pushes, base branch): \`${t("git_fetch")}\`, then git merge --no-commit <ref>, resolve conflicts, git add the results, then \`${t("commit_changes")}\` \u2014 it concludes the merge as a signed merge commit.
|
|
158953
|
+
- \`${t("commit_changes")}\` commits EVERY working-tree change by default \u2014 review \`git status\` first and clean up stray artifacts (or pass \`files\`).
|
|
158954
|
+
- cherry-pick/revert: use \`-n\`/\`--no-commit\` so no local commit is created, then \`${t("commit_changes")}\`.
|
|
158955
|
+
- Fork PRs are the exception: signing is impossible there, so commit and push normally (those commits will be unsigned).
|
|
158956
|
+
` : ""}
|
|
158339
158957
|
Rules:
|
|
158340
158958
|
- 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.
|
|
158341
158959
|
- Protected branches (default branch) are blocked from direct pushes in restricted mode. Do not use \`git push\` directly \u2014 it will fail without credentials.
|
|
@@ -158511,7 +159129,7 @@ function resolveInstructions(ctx) {
|
|
|
158511
159129
|
|
|
158512
159130
|
// utils/learnings.ts
|
|
158513
159131
|
import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
|
|
158514
|
-
import { dirname as dirname4, join as
|
|
159132
|
+
import { dirname as dirname4, join as join21 } from "node:path";
|
|
158515
159133
|
|
|
158516
159134
|
// utils/learningsTruncate.ts
|
|
158517
159135
|
var MAX_LEARNINGS_LENGTH = 1e5;
|
|
@@ -158528,7 +159146,7 @@ function truncateAtLineBoundary(body, cap) {
|
|
|
158528
159146
|
// utils/learnings.ts
|
|
158529
159147
|
var LEARNINGS_FILE_NAME = "pullfrog-learnings.md";
|
|
158530
159148
|
function learningsFilePath(tmpdir3) {
|
|
158531
|
-
return
|
|
159149
|
+
return join21(tmpdir3, LEARNINGS_FILE_NAME);
|
|
158532
159150
|
}
|
|
158533
159151
|
async function seedLearningsFile(params) {
|
|
158534
159152
|
const path4 = learningsFilePath(params.tmpdir);
|
|
@@ -158948,6 +159566,10 @@ function formatTransientErrorSummary(error49, owner) {
|
|
|
158948
159566
|
var core7 = __toESM(require_core(), 1);
|
|
158949
159567
|
import assert2 from "node:assert/strict";
|
|
158950
159568
|
var mcpTokenValue;
|
|
159569
|
+
var refreshMcpTokenFn;
|
|
159570
|
+
function getMcpTokenRefresh() {
|
|
159571
|
+
return refreshMcpTokenFn;
|
|
159572
|
+
}
|
|
158951
159573
|
function getJobToken() {
|
|
158952
159574
|
const inputToken = core7.getInput("token");
|
|
158953
159575
|
if (inputToken) {
|
|
@@ -158999,6 +159621,29 @@ async function resolveTokens(params) {
|
|
|
158999
159621
|
`\xBB acquired scoped MCP token (${Object.entries(mcpPermissions).map((e) => e.join(":")).join(", ")})`
|
|
159000
159622
|
);
|
|
159001
159623
|
mcpTokenValue = mcpToken;
|
|
159624
|
+
let currentMcpToken = mcpToken;
|
|
159625
|
+
let refreshPromise;
|
|
159626
|
+
refreshMcpTokenFn = (stale) => {
|
|
159627
|
+
assert2(mcpTokenValue, "tokens already disposed");
|
|
159628
|
+
if (stale !== currentMcpToken) {
|
|
159629
|
+
return Promise.resolve(currentMcpToken);
|
|
159630
|
+
}
|
|
159631
|
+
refreshPromise ??= acquireNewToken({
|
|
159632
|
+
permissions: mcpPermissions,
|
|
159633
|
+
oidc: params.oidc ?? void 0
|
|
159634
|
+
}).then((fresh) => {
|
|
159635
|
+
if (isGitHubActions) {
|
|
159636
|
+
core7.setSecret(fresh);
|
|
159637
|
+
}
|
|
159638
|
+
mcpTokenValue = fresh;
|
|
159639
|
+
currentMcpToken = fresh;
|
|
159640
|
+
log.warning("\xBB GitHub rejected the MCP token; re-acquired a fresh scoped MCP token");
|
|
159641
|
+
return fresh;
|
|
159642
|
+
}).finally(() => {
|
|
159643
|
+
refreshPromise = void 0;
|
|
159644
|
+
});
|
|
159645
|
+
return refreshPromise;
|
|
159646
|
+
};
|
|
159002
159647
|
let disposingRef;
|
|
159003
159648
|
const dispose = async () => {
|
|
159004
159649
|
if (disposingRef) {
|
|
@@ -159007,9 +159652,10 @@ async function resolveTokens(params) {
|
|
|
159007
159652
|
disposingRef = Promise.withResolvers();
|
|
159008
159653
|
try {
|
|
159009
159654
|
mcpTokenValue = void 0;
|
|
159655
|
+
refreshMcpTokenFn = void 0;
|
|
159010
159656
|
await Promise.all([
|
|
159011
159657
|
revokeGitHubInstallationToken(gitToken),
|
|
159012
|
-
revokeGitHubInstallationToken(
|
|
159658
|
+
revokeGitHubInstallationToken(currentMcpToken)
|
|
159013
159659
|
]);
|
|
159014
159660
|
} finally {
|
|
159015
159661
|
removeSignalHandler();
|
|
@@ -159053,7 +159699,7 @@ async function reportErrorToComment(ctx) {
|
|
|
159053
159699
|
|
|
159054
159700
|
${ctx.error}` : ctx.error;
|
|
159055
159701
|
const repoContext = parseRepoContext();
|
|
159056
|
-
const octokit = createOctokit(getGitHubInstallationToken());
|
|
159702
|
+
const octokit = createOctokit(getGitHubInstallationToken(), getMcpTokenRefresh());
|
|
159057
159703
|
const runId = process.env.GITHUB_RUN_ID ? Number.parseInt(process.env.GITHUB_RUN_ID, 10) : void 0;
|
|
159058
159704
|
const customParts = [];
|
|
159059
159705
|
if (runId) {
|
|
@@ -159067,7 +159713,6 @@ ${ctx.error}` : ctx.error;
|
|
|
159067
159713
|
workflowRun: runId ? { owner: repoContext.owner, repo: repoContext.name, runId } : void 0,
|
|
159068
159714
|
customParts,
|
|
159069
159715
|
model: ctx.toolState.model,
|
|
159070
|
-
fallbackFrom: ctx.toolState.modelFallback?.from,
|
|
159071
159716
|
oss: ctx.toolState.oss
|
|
159072
159717
|
});
|
|
159073
159718
|
const body = `${formattedError}${footer}`;
|
|
@@ -159134,18 +159779,15 @@ async function mintProxyKey(ctx) {
|
|
|
159134
159779
|
if (error49 instanceof TransientError) throw error49;
|
|
159135
159780
|
log.warning(`proxy key mint error: ${error49 instanceof Error ? error49.message : String(error49)}`);
|
|
159136
159781
|
return null;
|
|
159137
|
-
} finally {
|
|
159138
|
-
delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
159139
|
-
delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
159140
159782
|
}
|
|
159141
159783
|
}
|
|
159142
159784
|
async function buildProxyTokenHeaders(ctx) {
|
|
159143
159785
|
if (ctx.oidcCredentials) {
|
|
159144
|
-
|
|
159145
|
-
|
|
159146
|
-
|
|
159147
|
-
|
|
159148
|
-
|
|
159786
|
+
const creds = ctx.oidcCredentials;
|
|
159787
|
+
const oidcToken = await retry(() => fetchIdTokenFromStash(creds), {
|
|
159788
|
+
label: "ID token mint",
|
|
159789
|
+
shouldRetry: isTransientTokenError
|
|
159790
|
+
});
|
|
159149
159791
|
return { Authorization: `Bearer ${oidcToken}` };
|
|
159150
159792
|
}
|
|
159151
159793
|
if (isLocalApiUrl()) {
|
|
@@ -159209,7 +159851,7 @@ async function runProxyResolution(ctx) {
|
|
|
159209
159851
|
|
|
159210
159852
|
// utils/prSummary.ts
|
|
159211
159853
|
import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
|
|
159212
|
-
import { dirname as dirname5, join as
|
|
159854
|
+
import { dirname as dirname5, join as join22 } from "node:path";
|
|
159213
159855
|
var SUMMARY_FILE_NAME = "pullfrog-summary.md";
|
|
159214
159856
|
var SUMMARY_SCAFFOLD = `# PR summary
|
|
159215
159857
|
|
|
@@ -159219,7 +159861,7 @@ var SUMMARY_SCAFFOLD = `# PR summary
|
|
|
159219
159861
|
var MIN_SNAPSHOT_LENGTH = 60;
|
|
159220
159862
|
var MAX_SNAPSHOT_LENGTH = 32768;
|
|
159221
159863
|
function summaryFilePath(tmpdir3) {
|
|
159222
|
-
return
|
|
159864
|
+
return join22(tmpdir3, SUMMARY_FILE_NAME);
|
|
159223
159865
|
}
|
|
159224
159866
|
async function seedSummaryFile(params) {
|
|
159225
159867
|
const path4 = summaryFilePath(params.tmpdir);
|
|
@@ -159347,6 +159989,7 @@ var defaultSettings = {
|
|
|
159347
159989
|
push: "restricted",
|
|
159348
159990
|
shell: "restricted",
|
|
159349
159991
|
prApproveEnabled: false,
|
|
159992
|
+
signedCommits: false,
|
|
159350
159993
|
modeInstructions: {},
|
|
159351
159994
|
learnings: null,
|
|
159352
159995
|
learningsHeadings: [],
|
|
@@ -159579,7 +160222,7 @@ ${input.errorMessage}
|
|
|
159579
160222
|
].join("\n");
|
|
159580
160223
|
}
|
|
159581
160224
|
function formatProviderModelNotFoundSummary(input) {
|
|
159582
|
-
return `
|
|
160225
|
+
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.
|
|
159583
160226
|
|
|
159584
160227
|
\`\`\`
|
|
159585
160228
|
${input.raw}
|
|
@@ -160020,7 +160663,7 @@ async function main() {
|
|
|
160020
160663
|
const initialOctokit = createOctokit(jobToken);
|
|
160021
160664
|
const runContext = await resolveRunContextData({ octokit: initialOctokit, token: jobToken });
|
|
160022
160665
|
timer.checkpoint("runContextData");
|
|
160023
|
-
|
|
160666
|
+
createTempDirectory();
|
|
160024
160667
|
const opencodeCliPath = await agents.opencode.install();
|
|
160025
160668
|
captureBaselineModels(opencodeCliPath);
|
|
160026
160669
|
if (runContext.dbSecrets) {
|
|
@@ -160044,12 +160687,12 @@ async function main() {
|
|
|
160044
160687
|
if (payload.event.trigger === "pull_request_synchronize") {
|
|
160045
160688
|
toolState.beforeSha = payload.event.before_sha;
|
|
160046
160689
|
}
|
|
160047
|
-
const tokenRef = __using(_stack2, await resolveTokens({ push: payload.push }), true);
|
|
160048
|
-
wipeRunnerLeakSurface();
|
|
160049
160690
|
const oidcCredentials = process.env.ACTIONS_ID_TOKEN_REQUEST_URL && process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN ? {
|
|
160050
160691
|
requestUrl: process.env.ACTIONS_ID_TOKEN_REQUEST_URL,
|
|
160051
160692
|
requestToken: process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN
|
|
160052
160693
|
} : null;
|
|
160694
|
+
const tokenRef = __using(_stack2, await resolveTokens({ push: payload.push, oidc: oidcCredentials }), true);
|
|
160695
|
+
wipeRunnerLeakSurface();
|
|
160053
160696
|
if (payload.shell !== "enabled") {
|
|
160054
160697
|
delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
160055
160698
|
delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
@@ -160062,7 +160705,7 @@ async function main() {
|
|
|
160062
160705
|
repo: runContext.repo,
|
|
160063
160706
|
toolState
|
|
160064
160707
|
});
|
|
160065
|
-
const octokit = createOctokit(tokenRef.mcpToken);
|
|
160708
|
+
const octokit = createOctokit(tokenRef.mcpToken, getMcpTokenRefresh());
|
|
160066
160709
|
const runInfo = await resolveRun({ octokit });
|
|
160067
160710
|
let toolContext;
|
|
160068
160711
|
let progressCallbackDisabled = false;
|
|
@@ -160074,13 +160717,13 @@ async function main() {
|
|
|
160074
160717
|
if (payload.cwd && process.cwd() !== payload.cwd) {
|
|
160075
160718
|
process.chdir(payload.cwd);
|
|
160076
160719
|
}
|
|
160077
|
-
const
|
|
160720
|
+
const tmpdir3 = createTempDirectory();
|
|
160078
160721
|
const originalBody = payload.event.body;
|
|
160079
160722
|
const resolvedBody = await resolveBody({
|
|
160080
160723
|
event: payload.event,
|
|
160081
160724
|
octokit,
|
|
160082
160725
|
repo: runContext.repo,
|
|
160083
|
-
tmpdir:
|
|
160726
|
+
tmpdir: tmpdir3,
|
|
160084
160727
|
githubToken: tokenRef.mcpToken
|
|
160085
160728
|
});
|
|
160086
160729
|
if (resolvedBody !== originalBody) {
|
|
@@ -160089,33 +160732,18 @@ async function main() {
|
|
|
160089
160732
|
payload.prompt = payload.prompt.replace(originalBody, resolvedBody ?? "");
|
|
160090
160733
|
}
|
|
160091
160734
|
}
|
|
160092
|
-
const gitAuthServer = __using(_stack, await startGitAuthServer(
|
|
160735
|
+
const gitAuthServer = __using(_stack, await startGitAuthServer(tmpdir3), true);
|
|
160093
160736
|
setGitAuthServer(gitAuthServer);
|
|
160094
|
-
const
|
|
160095
|
-
const authorized2 = getAuthorizedModels();
|
|
160096
|
-
const fallback = selectFallbackModelIfNeeded({
|
|
160097
|
-
resolvedModel: initialResolvedModel,
|
|
160098
|
-
proxyModel: payload.proxyModel,
|
|
160099
|
-
authorized: authorized2,
|
|
160100
|
-
agentName: resolveAgent({ model: initialResolvedModel }).name
|
|
160101
|
-
});
|
|
160102
|
-
const effectiveSlug = fallback.fallback ? fallback.to : payload.model;
|
|
160103
|
-
const resolvedModel = fallback.fallback ? fallback.to : initialResolvedModel;
|
|
160104
|
-
if (fallback.fallback) {
|
|
160105
|
-
log.warning(
|
|
160106
|
-
`\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.`
|
|
160107
|
-
);
|
|
160108
|
-
toolState.modelFallback = { from: fallback.from };
|
|
160109
|
-
}
|
|
160737
|
+
const resolvedModel = payload.proxyModel ? void 0 : resolveModel({ slug: payload.model });
|
|
160110
160738
|
vertexCredentials = materializeVertexCredentials({ model: resolvedModel });
|
|
160111
160739
|
const agent2 = resolveAgent({ model: resolvedModel });
|
|
160112
|
-
const effectiveModel = payload.proxyModel ?? resolvedModel ??
|
|
160740
|
+
const effectiveModel = payload.proxyModel ?? resolvedModel ?? payload.model;
|
|
160113
160741
|
toolState.model = effectiveModel;
|
|
160114
|
-
if (!
|
|
160742
|
+
if (!payload.proxyModel) {
|
|
160115
160743
|
validateAgentApiKey({
|
|
160116
160744
|
agent: agent2,
|
|
160117
160745
|
model: effectiveModel,
|
|
160118
|
-
authorized:
|
|
160746
|
+
authorized: getAuthorizedModels(),
|
|
160119
160747
|
owner: runContext.repo.owner,
|
|
160120
160748
|
name: runContext.repo.name
|
|
160121
160749
|
});
|
|
@@ -160132,7 +160760,7 @@ async function main() {
|
|
|
160132
160760
|
timer.checkpoint("git");
|
|
160133
160761
|
const pmSpec = await resolvePackageManagerSpec(process.cwd());
|
|
160134
160762
|
if (pmSpec) {
|
|
160135
|
-
await ensurePackageManager({ spec: pmSpec, binDir: packageManagerBinDir(
|
|
160763
|
+
await ensurePackageManager({ spec: pmSpec, binDir: packageManagerBinDir(tmpdir3) });
|
|
160136
160764
|
}
|
|
160137
160765
|
timer.checkpoint("packageManager");
|
|
160138
160766
|
const setupHook = await executeLifecycleHook({
|
|
@@ -160145,26 +160773,34 @@ async function main() {
|
|
|
160145
160773
|
}
|
|
160146
160774
|
timer.checkpoint("lifecycleHooks::setup");
|
|
160147
160775
|
const agentId = agent2.name;
|
|
160148
|
-
const modes2 = [
|
|
160776
|
+
const modes2 = [
|
|
160777
|
+
...computeModes(agentId, runContext.repoSettings.signedCommits),
|
|
160778
|
+
...runContext.repoSettings.modes
|
|
160779
|
+
];
|
|
160149
160780
|
const outputSchema = resolveOutputSchema();
|
|
160150
160781
|
toolContext = {
|
|
160151
160782
|
agentId,
|
|
160152
160783
|
repo: runContext.repo,
|
|
160153
160784
|
payload,
|
|
160154
160785
|
octokit,
|
|
160155
|
-
|
|
160786
|
+
// live getter so raw-token consumers (asset fetches, plan/summary-comment
|
|
160787
|
+
// GETs) see the refreshed MCP token after a mid-run re-acquisition (#891)
|
|
160788
|
+
get githubInstallationToken() {
|
|
160789
|
+
return getGitHubInstallationToken();
|
|
160790
|
+
},
|
|
160156
160791
|
gitToken: tokenRef.gitToken,
|
|
160157
160792
|
apiToken: runContext.apiToken,
|
|
160158
160793
|
modes: modes2,
|
|
160159
160794
|
postCheckoutScript: runContext.repoSettings.postCheckoutScript,
|
|
160160
160795
|
prepushScript: runContext.repoSettings.prepushScript,
|
|
160161
160796
|
prApproveEnabled: runContext.repoSettings.prApproveEnabled,
|
|
160797
|
+
signedCommits: runContext.repoSettings.signedCommits,
|
|
160162
160798
|
modeInstructions: runContext.repoSettings.modeInstructions,
|
|
160163
160799
|
toolState,
|
|
160164
160800
|
runId: runInfo.runId,
|
|
160165
160801
|
jobId: runInfo.jobId,
|
|
160166
160802
|
mcpServerUrl: "",
|
|
160167
|
-
tmpdir:
|
|
160803
|
+
tmpdir: tmpdir3,
|
|
160168
160804
|
oss: runContext.oss,
|
|
160169
160805
|
plan: runContext.plan,
|
|
160170
160806
|
resolvedModel
|
|
@@ -160175,7 +160811,7 @@ async function main() {
|
|
|
160175
160811
|
timer.checkpoint("mcpServer");
|
|
160176
160812
|
try {
|
|
160177
160813
|
const learningsPath = await seedLearningsFile({
|
|
160178
|
-
tmpdir:
|
|
160814
|
+
tmpdir: tmpdir3,
|
|
160179
160815
|
current: runContext.repoSettings.learnings
|
|
160180
160816
|
});
|
|
160181
160817
|
toolState.learningsFilePath = learningsPath;
|
|
@@ -160192,7 +160828,7 @@ async function main() {
|
|
|
160192
160828
|
}
|
|
160193
160829
|
if (payload.generateSummary && payload.event.is_pr && payload.event.issue_number) {
|
|
160194
160830
|
const previousSnapshot = await fetchPreviousSnapshot(toolContext, payload.event.issue_number);
|
|
160195
|
-
const filePath = await seedSummaryFile({ tmpdir:
|
|
160831
|
+
const filePath = await seedSummaryFile({ tmpdir: tmpdir3, previousSnapshot });
|
|
160196
160832
|
toolState.summaryFilePath = filePath;
|
|
160197
160833
|
try {
|
|
160198
160834
|
toolState.summarySeed = await readFile5(filePath, "utf8");
|
|
@@ -160212,6 +160848,7 @@ async function main() {
|
|
|
160212
160848
|
modes: modes2,
|
|
160213
160849
|
agentId,
|
|
160214
160850
|
outputSchema,
|
|
160851
|
+
signedCommits: runContext.repoSettings.signedCommits,
|
|
160215
160852
|
learningsFilePath: toolState.learningsFilePath ?? null,
|
|
160216
160853
|
learningsHeadings: runContext.repoSettings.learningsHeadings,
|
|
160217
160854
|
setupHookFailure: describeSetupFailure(setupHook.failure)
|
|
@@ -160230,7 +160867,7 @@ ${instructions.user}` : null,
|
|
|
160230
160867
|
log.info(instructions.full);
|
|
160231
160868
|
});
|
|
160232
160869
|
if (agentId === "opencode") {
|
|
160233
|
-
const pluginDir =
|
|
160870
|
+
const pluginDir = join23(process.cwd(), ".opencode", "plugin");
|
|
160234
160871
|
const hasPlugins = existsSync8(pluginDir) && readdirSync2(pluginDir).some((f) => /\.[jt]sx?$/.test(f));
|
|
160235
160872
|
if (hasPlugins && toolState.dependencyInstallation?.promise) {
|
|
160236
160873
|
log.info(
|
|
@@ -160285,7 +160922,7 @@ ${instructions.user}` : null,
|
|
|
160285
160922
|
payload,
|
|
160286
160923
|
resolvedModel,
|
|
160287
160924
|
mcpServerUrl: mcpHttpServer.url,
|
|
160288
|
-
tmpdir:
|
|
160925
|
+
tmpdir: tmpdir3,
|
|
160289
160926
|
// PULLFROG_DATA_DIR (/var/lib/pullfrog) holds codex auth.json + any
|
|
160290
160927
|
// future pullfrog-managed on-disk secrets. bash via MCP tmpfs-overlays
|
|
160291
160928
|
// it; agent native FS tools deny it via the same secretDenyPaths plumbing
|