pullfrog 0.1.29 → 0.1.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/subagentToolGates.d.ts +1 -1
- package/dist/cli.mjs +831 -209
- package/dist/index.js +822 -200
- 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";
|
|
@@ -150143,7 +150251,274 @@ function postProcessRangeDiff(raw2, contextLines = 3) {
|
|
|
150143
150251
|
// mcp/git.ts
|
|
150144
150252
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
150145
150253
|
import { writeFileSync as writeFileSync6 } from "node:fs";
|
|
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";
|
|
150146
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
|
|
150147
150522
|
function getPushDestination(branch, storedDest) {
|
|
150148
150523
|
if (storedDest && storedDest.localBranch === branch) {
|
|
150149
150524
|
log.debug(`using stored push destination: ${storedDest.remoteName}/${storedDest.remoteBranch}`);
|
|
@@ -150201,6 +150576,10 @@ function validateTagName(tag) {
|
|
|
150201
150576
|
);
|
|
150202
150577
|
}
|
|
150203
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
|
+
}
|
|
150204
150583
|
function validatePushDestination(ctx, branch) {
|
|
150205
150584
|
const pushUrl = ctx.toolState.pushUrl;
|
|
150206
150585
|
if (!pushUrl) throw new Error("pushUrl not set - setupGit must run before push_branch");
|
|
@@ -150219,6 +150598,47 @@ var PushBranch = type({
|
|
|
150219
150598
|
branchName: type.string.describe("The branch name to push (defaults to current branch)").optional(),
|
|
150220
150599
|
force: type.boolean.describe("Force push (use with caution)").default(false)
|
|
150221
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
|
+
}
|
|
150222
150642
|
var CONCURRENT_PUSH_PATTERNS = ["fetch first", "non-fast-forward", "cannot lock ref"];
|
|
150223
150643
|
var TRANSIENT_PATTERNS = [
|
|
150224
150644
|
/RPC failed/i,
|
|
@@ -150278,7 +150698,6 @@ async function pushWithRetry(args2, token) {
|
|
|
150278
150698
|
throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
|
|
150279
150699
|
}
|
|
150280
150700
|
function PushBranchTool(ctx) {
|
|
150281
|
-
const defaultBranch = ctx.repo.data.default_branch || "main";
|
|
150282
150701
|
const pushPermission = ctx.payload.push;
|
|
150283
150702
|
return tool({
|
|
150284
150703
|
name: "push_branch",
|
|
@@ -150290,6 +150709,11 @@ function PushBranchTool(ctx) {
|
|
|
150290
150709
|
}
|
|
150291
150710
|
const branch = branchName || $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false });
|
|
150292
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
|
+
}
|
|
150293
150717
|
const status = $("git", ["status", "--porcelain"], { log: false });
|
|
150294
150718
|
if (status) {
|
|
150295
150719
|
throw new Error(
|
|
@@ -150300,36 +150724,11 @@ ${status}` + (ctx.toolState.prepushFailureCount > 0 ? "\n\nnote: the prepush hoo
|
|
|
150300
150724
|
);
|
|
150301
150725
|
}
|
|
150302
150726
|
const pushDest = validatePushDestination(ctx, branch);
|
|
150303
|
-
|
|
150304
|
-
if (prBranchMatch && pushDest.remoteBranch !== branch) {
|
|
150305
|
-
const prNumber = Number(prBranchMatch[1]);
|
|
150306
|
-
const event = ctx.payload.event;
|
|
150307
|
-
const runScoped = event.is_pr === true && event.issue_number === prNumber;
|
|
150308
|
-
if (!runScoped) {
|
|
150309
|
-
throw new Error(
|
|
150310
|
-
`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.`
|
|
150311
|
-
);
|
|
150312
|
-
}
|
|
150313
|
-
}
|
|
150314
|
-
if (pushPermission === "restricted" && pushDest.remoteBranch === defaultBranch) {
|
|
150315
|
-
throw new Error(
|
|
150316
|
-
`Push blocked: cannot push directly to default branch '${pushDest.remoteBranch}'. Create a feature branch and open a PR instead.`
|
|
150317
|
-
);
|
|
150318
|
-
}
|
|
150727
|
+
assertPushTarget(ctx, branch, pushDest);
|
|
150319
150728
|
const refspec = branch === pushDest.remoteBranch ? branch : `${branch}:${pushDest.remoteBranch}`;
|
|
150320
150729
|
const pushArgs = force ? ["--force", "-u", pushDest.remoteName, refspec] : ["-u", pushDest.remoteName, refspec];
|
|
150321
|
-
const prepushSkipped = ctx
|
|
150322
|
-
if (prepushSkipped) {
|
|
150323
|
-
log.info(`\xBB skipping prepush hook (failed earlier this run)`);
|
|
150324
|
-
} else if (ctx.prepushScript) {
|
|
150325
|
-
const prepushHook = await executeLifecycleHook({
|
|
150326
|
-
event: "prepush",
|
|
150327
|
-
script: ctx.prepushScript
|
|
150328
|
-
});
|
|
150329
|
-
if (prepushHook.failure) {
|
|
150330
|
-
ctx.toolState.prepushFailureCount += 1;
|
|
150331
|
-
throw new Error(buildPrepushFailureMessage(prepushHook.failure, ctx.payload.shell));
|
|
150332
|
-
}
|
|
150730
|
+
const prepushSkipped = await runPrepushHook(ctx, "push_branch");
|
|
150731
|
+
if (!prepushSkipped && ctx.prepushScript) {
|
|
150333
150732
|
const postHookStatus = $("git", ["status", "--porcelain"], { log: false });
|
|
150334
150733
|
if (postHookStatus) {
|
|
150335
150734
|
throw new Error(
|
|
@@ -150380,15 +150779,138 @@ ${integrateStep}
|
|
|
150380
150779
|
})
|
|
150381
150780
|
});
|
|
150382
150781
|
}
|
|
150383
|
-
function buildPrepushFailureMessage(
|
|
150782
|
+
function buildPrepushFailureMessage(params) {
|
|
150783
|
+
const failure = params.failure;
|
|
150384
150784
|
const header = failure.kind === "exit" ? `prepush hook failed with exit code ${failure.exitCode}.
|
|
150385
150785
|
|
|
150386
150786
|
script output:
|
|
150387
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}.`;
|
|
150388
|
-
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).`;
|
|
150389
150789
|
return `${header}
|
|
150390
150790
|
|
|
150391
|
-
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
|
+
});
|
|
150392
150914
|
}
|
|
150393
150915
|
var AUTH_REQUIRED_REDIRECT = {
|
|
150394
150916
|
push: "use the push_branch tool instead \u2014 it handles authentication and permission checks.",
|
|
@@ -150461,7 +150983,7 @@ function countAhead(head, base) {
|
|
|
150461
150983
|
function spillGitOutput(params) {
|
|
150462
150984
|
const tempDir = process.env.PULLFROG_TEMP_DIR;
|
|
150463
150985
|
if (!tempDir) throw new Error("PULLFROG_TEMP_DIR not set");
|
|
150464
|
-
const outputPath =
|
|
150986
|
+
const outputPath = join12(tempDir, `git-${params.command}-${randomUUID3().slice(0, 8)}.txt`);
|
|
150465
150987
|
writeFileSync6(outputPath, params.output);
|
|
150466
150988
|
const previewByLines = params.output.split("\n").slice(0, OVERFLOW_PREVIEW_LINES).join("\n");
|
|
150467
150989
|
const preview = previewByLines.length <= OVERFLOW_PREVIEW_MAX_CHARS ? previewByLines : `${previewByLines.slice(0, OVERFLOW_PREVIEW_MAX_CHARS)}\u2026`;
|
|
@@ -150495,8 +151017,30 @@ function GitTool(ctx) {
|
|
|
150495
151017
|
}
|
|
150496
151018
|
const redirect = AUTH_REQUIRED_REDIRECT[command];
|
|
150497
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
|
+
}
|
|
150498
151025
|
throw new Error(`git ${command} is not available through this tool \u2014 ${redirect}`);
|
|
150499
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
|
+
}
|
|
150500
151044
|
if (ctx.payload.shell === "disabled") {
|
|
150501
151045
|
const blocked = NOSHELL_BLOCKED_SUBCOMMANDS[command];
|
|
150502
151046
|
if (blocked) {
|
|
@@ -151129,7 +151673,6 @@ async function createAndSubmitWithFooter(ctx, params, opts) {
|
|
|
151129
151673
|
workflowRun: ctx.runId ? { owner: ctx.repo.owner, repo: ctx.repo.name, runId: ctx.runId, jobId: ctx.jobId } : void 0,
|
|
151130
151674
|
customParts,
|
|
151131
151675
|
model: ctx.toolState.model,
|
|
151132
|
-
fallbackFrom: ctx.toolState.modelFallback?.from,
|
|
151133
151676
|
oss: ctx.oss
|
|
151134
151677
|
});
|
|
151135
151678
|
return await ctx.octokit.rest.pulls.submitReview({
|
|
@@ -151162,12 +151705,12 @@ async function reportReviewNodeId(ctx, params) {
|
|
|
151162
151705
|
}
|
|
151163
151706
|
|
|
151164
151707
|
// utils/setup.ts
|
|
151165
|
-
import { execFileSync as
|
|
151708
|
+
import { execFileSync as execFileSync7, execSync as execSync2 } from "node:child_process";
|
|
151166
151709
|
import { mkdtempSync, readdirSync, realpathSync as realpathSync2, unlinkSync as unlinkSync2 } from "node:fs";
|
|
151167
151710
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
151168
|
-
import { join as
|
|
151711
|
+
import { join as join13 } from "node:path";
|
|
151169
151712
|
function createTempDirectory() {
|
|
151170
|
-
const sharedTempDir = mkdtempSync(
|
|
151713
|
+
const sharedTempDir = mkdtempSync(join13(tmpdir2(), "pullfrog-"));
|
|
151171
151714
|
process.env.PULLFROG_TEMP_DIR = sharedTempDir;
|
|
151172
151715
|
log.info(`\xBB created temp dir at ${sharedTempDir}`);
|
|
151173
151716
|
return sharedTempDir;
|
|
@@ -151212,13 +151755,13 @@ function wipeRunnerLeakSurface() {
|
|
|
151212
151755
|
return [];
|
|
151213
151756
|
}
|
|
151214
151757
|
};
|
|
151215
|
-
const fileCommandsDir =
|
|
151758
|
+
const fileCommandsDir = join13(runnerTemp, "_runner_file_commands");
|
|
151216
151759
|
for (const entry of listDir(fileCommandsDir)) {
|
|
151217
|
-
tryUnlink(
|
|
151760
|
+
tryUnlink(join13(fileCommandsDir, entry));
|
|
151218
151761
|
}
|
|
151219
151762
|
for (const entry of listDir(runnerTemp)) {
|
|
151220
151763
|
if (entry.endsWith(".sh") || /^git-credentials-.*\.config$/.test(entry)) {
|
|
151221
|
-
tryUnlink(
|
|
151764
|
+
tryUnlink(join13(runnerTemp, entry));
|
|
151222
151765
|
}
|
|
151223
151766
|
}
|
|
151224
151767
|
if (wiped.length > 0) {
|
|
@@ -151255,7 +151798,7 @@ function removeIncludeIfEntries(repoDir) {
|
|
|
151255
151798
|
if (!key || seen.has(key)) continue;
|
|
151256
151799
|
seen.add(key);
|
|
151257
151800
|
try {
|
|
151258
|
-
|
|
151801
|
+
execFileSync7("git", ["config", "--local", "--unset-all", key], {
|
|
151259
151802
|
cwd: repoDir,
|
|
151260
151803
|
stdio: "pipe",
|
|
151261
151804
|
env: env2
|
|
@@ -151727,7 +152270,7 @@ function CheckoutPrTool(ctx) {
|
|
|
151727
152270
|
headSha: ctx.toolState.checkoutSha
|
|
151728
152271
|
});
|
|
151729
152272
|
if (incremental) {
|
|
151730
|
-
incrementalDiffPath =
|
|
152273
|
+
incrementalDiffPath = join14(
|
|
151731
152274
|
tempDir,
|
|
151732
152275
|
`pr-${pull_number}-${beforeShort}-${headShort}-incremental.diff`
|
|
151733
152276
|
);
|
|
@@ -151741,7 +152284,7 @@ function CheckoutPrTool(ctx) {
|
|
|
151741
152284
|
const diffPreview = formatResult.content.split("\n").slice(0, 100).join("\n");
|
|
151742
152285
|
log.debug(`formatted diff preview (first 100 lines):
|
|
151743
152286
|
${diffPreview}`);
|
|
151744
|
-
const diffPath =
|
|
152287
|
+
const diffPath = join14(tempDir, `pr-${pull_number}-${headShort}.diff`);
|
|
151745
152288
|
writeFileSync7(diffPath, formatResult.content);
|
|
151746
152289
|
log.debug(`wrote diff to ${diffPath} (${formatResult.content.length} bytes)`);
|
|
151747
152290
|
ctx.toolState.diffCoverage = createDiffCoverageState({
|
|
@@ -151850,7 +152393,7 @@ ${dirty}`
|
|
|
151850
152393
|
|
|
151851
152394
|
// mcp/checkSuite.ts
|
|
151852
152395
|
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "node:fs";
|
|
151853
|
-
import { join as
|
|
152396
|
+
import { join as join15 } from "node:path";
|
|
151854
152397
|
var GetCheckSuiteLogs = type({
|
|
151855
152398
|
check_suite_id: type.number.describe("the id from check_suite.id")
|
|
151856
152399
|
});
|
|
@@ -151946,7 +152489,7 @@ function GetCheckSuiteLogsTool(ctx) {
|
|
|
151946
152489
|
if (!tempDir) {
|
|
151947
152490
|
throw new Error("PULLFROG_TEMP_DIR not set");
|
|
151948
152491
|
}
|
|
151949
|
-
const logsDir =
|
|
152492
|
+
const logsDir = join15(tempDir, "ci-logs");
|
|
151950
152493
|
mkdirSync7(logsDir, { recursive: true });
|
|
151951
152494
|
const jobResults = [];
|
|
151952
152495
|
for (const run of failedRuns) {
|
|
@@ -151973,7 +152516,7 @@ function GetCheckSuiteLogsTool(ctx) {
|
|
|
151973
152516
|
);
|
|
151974
152517
|
}
|
|
151975
152518
|
const logsText = await logsResult.text();
|
|
151976
|
-
const logPath =
|
|
152519
|
+
const logPath = join15(logsDir, `job-${job.id}.log`);
|
|
151977
152520
|
writeFileSync8(logPath, logsText);
|
|
151978
152521
|
const analysis = analyzeLog(logsText, 80);
|
|
151979
152522
|
const failedSteps = job.steps?.filter((s) => s.conclusion === "failure").map((s) => `Step ${s.number}: ${s.name}`) ?? [];
|
|
@@ -152023,7 +152566,7 @@ function GetCheckSuiteLogsTool(ctx) {
|
|
|
152023
152566
|
|
|
152024
152567
|
// mcp/commitInfo.ts
|
|
152025
152568
|
import { writeFileSync as writeFileSync9 } from "node:fs";
|
|
152026
|
-
import { join as
|
|
152569
|
+
import { join as join16 } from "node:path";
|
|
152027
152570
|
var CommitInfo = type({
|
|
152028
152571
|
sha: type.string.describe("the commit SHA (full or abbreviated) to fetch")
|
|
152029
152572
|
});
|
|
@@ -152047,7 +152590,7 @@ function CommitInfoTool(ctx) {
|
|
|
152047
152590
|
"PULLFROG_TEMP_DIR not set - get_commit_info must run in pullfrog action context"
|
|
152048
152591
|
);
|
|
152049
152592
|
}
|
|
152050
|
-
const diffFile =
|
|
152593
|
+
const diffFile = join16(tempDir, `commit-${sha.slice(0, 7)}.diff`);
|
|
152051
152594
|
writeFileSync9(diffFile, formatResult.content);
|
|
152052
152595
|
log.debug(`wrote commit diff to ${diffFile} (${formatResult.content.length} bytes)`);
|
|
152053
152596
|
return {
|
|
@@ -152554,7 +153097,6 @@ function buildPrBodyWithFooter(ctx, body) {
|
|
|
152554
153097
|
triggeredBy: true,
|
|
152555
153098
|
workflowRun: ctx.runId ? { owner: ctx.repo.owner, repo: ctx.repo.name, runId: ctx.runId, jobId: ctx.jobId } : void 0,
|
|
152556
153099
|
model: ctx.toolState.model,
|
|
152557
|
-
fallbackFrom: ctx.toolState.modelFallback?.from,
|
|
152558
153100
|
oss: ctx.oss
|
|
152559
153101
|
});
|
|
152560
153102
|
const bodyWithoutFooter = stripExistingFooter(fixDoubleEscapedString(body));
|
|
@@ -152709,7 +153251,7 @@ function PullRequestInfoTool(ctx) {
|
|
|
152709
153251
|
|
|
152710
153252
|
// mcp/reviewComments.ts
|
|
152711
153253
|
import { writeFileSync as writeFileSync10 } from "node:fs";
|
|
152712
|
-
import { join as
|
|
153254
|
+
import { join as join17 } from "node:path";
|
|
152713
153255
|
var REVIEW_THREADS_QUERY = `
|
|
152714
153256
|
query ($owner: String!, $name: String!, $prNumber: Int!) {
|
|
152715
153257
|
repository(owner: $owner, name: $name) {
|
|
@@ -153125,7 +153667,7 @@ function GetReviewCommentsTool(ctx) {
|
|
|
153125
153667
|
throw new Error("PULLFROG_TEMP_DIR not set");
|
|
153126
153668
|
}
|
|
153127
153669
|
const filename = `review-${params.review_id}-threads.md`;
|
|
153128
|
-
const commentsPath =
|
|
153670
|
+
const commentsPath = join17(tempDir, filename);
|
|
153129
153671
|
writeFileSync10(commentsPath, formatted.content);
|
|
153130
153672
|
log.debug(`wrote ${threadBlocks.length} threads to ${commentsPath}`);
|
|
153131
153673
|
return {
|
|
@@ -153363,7 +153905,7 @@ import { spawn as spawn2, spawnSync as spawnSync5 } from "node:child_process";
|
|
|
153363
153905
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
153364
153906
|
import { closeSync, openSync, writeFileSync as writeFileSync11 } from "node:fs";
|
|
153365
153907
|
import { userInfo as userInfo2 } from "node:os";
|
|
153366
|
-
import { join as
|
|
153908
|
+
import { join as join18 } from "node:path";
|
|
153367
153909
|
import { setTimeout as sleep2 } from "node:timers/promises";
|
|
153368
153910
|
var ShellParams = type({
|
|
153369
153911
|
command: "string",
|
|
@@ -153526,7 +154068,7 @@ function getTempDir() {
|
|
|
153526
154068
|
var MAX_OUTPUT_CHARS = 5e3;
|
|
153527
154069
|
function capOutput(output) {
|
|
153528
154070
|
if (output.length <= MAX_OUTPUT_CHARS) return output;
|
|
153529
|
-
const fullPath =
|
|
154071
|
+
const fullPath = join18(getTempDir(), `shell-${randomUUID4().slice(0, 8)}.log`);
|
|
153530
154072
|
writeFileSync11(fullPath, output);
|
|
153531
154073
|
const elided = output.length - MAX_OUTPUT_CHARS;
|
|
153532
154074
|
return `... [${elided} chars truncated; full output saved to ${fullPath}] ...
|
|
@@ -153581,8 +154123,8 @@ Do NOT use this tool for git commands \u2014 use the dedicated git tools instead
|
|
|
153581
154123
|
if (params.background) {
|
|
153582
154124
|
const tempDir = getTempDir();
|
|
153583
154125
|
const handle = `bg-${randomUUID4().slice(0, 8)}`;
|
|
153584
|
-
const outputPath =
|
|
153585
|
-
const pidPath =
|
|
154126
|
+
const outputPath = join18(tempDir, `${handle}.log`);
|
|
154127
|
+
const pidPath = join18(tempDir, `${handle}.pid`);
|
|
153586
154128
|
const logFd = openSync(outputPath, "a");
|
|
153587
154129
|
let proc2;
|
|
153588
154130
|
try {
|
|
@@ -153813,7 +154355,7 @@ function buildCommonTools(ctx, outputSchema) {
|
|
|
153813
154355
|
return tools;
|
|
153814
154356
|
}
|
|
153815
154357
|
function buildOrchestratorTools(ctx, outputSchema) {
|
|
153816
|
-
|
|
154358
|
+
const tools = [
|
|
153817
154359
|
...buildCommonTools(ctx, outputSchema),
|
|
153818
154360
|
ReportProgressTool(ctx),
|
|
153819
154361
|
SelectModeTool(ctx),
|
|
@@ -153823,6 +154365,10 @@ function buildOrchestratorTools(ctx, outputSchema) {
|
|
|
153823
154365
|
CreatePullRequestTool(ctx),
|
|
153824
154366
|
UpdatePullRequestBodyTool(ctx)
|
|
153825
154367
|
];
|
|
154368
|
+
if (ctx.signedCommits) {
|
|
154369
|
+
tools.push(CommitChangesTool(ctx));
|
|
154370
|
+
}
|
|
154371
|
+
return tools;
|
|
153826
154372
|
}
|
|
153827
154373
|
async function tryStartMcpServer(ctx, tools, port) {
|
|
153828
154374
|
const server = new FastMCP({ name: pullfrogMcpName, version: "0.0.1" });
|
|
@@ -154013,8 +154559,14 @@ var MISSING_KEY_MARKER = "no API key found";
|
|
|
154013
154559
|
function buildMissingApiKeyError(params) {
|
|
154014
154560
|
const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
|
|
154015
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.`;
|
|
154016
154566
|
return [
|
|
154017
|
-
|
|
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).",
|
|
154018
154570
|
"",
|
|
154019
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)`
|
|
154020
154572
|
].join("\n");
|
|
@@ -154094,10 +154646,14 @@ function validateAgentApiKey(params) {
|
|
|
154094
154646
|
}
|
|
154095
154647
|
if (params.agent.name === "opencode") {
|
|
154096
154648
|
if (params.authorized.has(params.model)) return;
|
|
154097
|
-
throw new Error(
|
|
154649
|
+
throw new Error(
|
|
154650
|
+
buildMissingApiKeyError({ owner: params.owner, name: params.name, model: params.model })
|
|
154651
|
+
);
|
|
154098
154652
|
}
|
|
154099
154653
|
if (hasEnvVar3("ANTHROPIC_API_KEY") || hasEnvVar3("CLAUDE_CODE_OAUTH_TOKEN")) return;
|
|
154100
|
-
throw new Error(
|
|
154654
|
+
throw new Error(
|
|
154655
|
+
buildMissingApiKeyError({ owner: params.owner, name: params.name, model: params.model })
|
|
154656
|
+
);
|
|
154101
154657
|
}
|
|
154102
154658
|
if (params.agent.name === "opencode") {
|
|
154103
154659
|
if (params.authorized.size > 0) return;
|
|
@@ -154108,42 +154664,37 @@ function validateAgentApiKey(params) {
|
|
|
154108
154664
|
}
|
|
154109
154665
|
function isApiKeyAuthError(text) {
|
|
154110
154666
|
if (!text) return false;
|
|
154111
|
-
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);
|
|
154112
154671
|
}
|
|
154113
154672
|
function formatApiKeyErrorSummary(params) {
|
|
154114
154673
|
if (params.raw.includes(MISSING_KEY_MARKER)) {
|
|
154674
|
+
if (params.raw.startsWith(`**${MISSING_KEY_MARKER}**`)) return params.raw;
|
|
154115
154675
|
return buildMissingApiKeyError({ owner: params.owner, name: params.name });
|
|
154116
154676
|
}
|
|
154117
154677
|
const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
|
|
154118
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
|
+
}
|
|
154119
154686
|
return [
|
|
154120
|
-
`**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.`,
|
|
154121
154688
|
"",
|
|
154122
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)`
|
|
154123
154690
|
].join("\n");
|
|
154124
154691
|
}
|
|
154125
154692
|
|
|
154126
|
-
// utils/byokFallback.ts
|
|
154127
|
-
var FREE_FALLBACK_SLUG = "opencode/big-pickle";
|
|
154128
|
-
function selectFallbackModelIfNeeded(input) {
|
|
154129
|
-
if (input.proxyModel) return { fallback: false };
|
|
154130
|
-
if (!input.resolvedModel) return { fallback: false };
|
|
154131
|
-
if (input.resolvedModel === FREE_FALLBACK_SLUG) return { fallback: false };
|
|
154132
|
-
if (!input.resolvedModel.includes("/")) return { fallback: false };
|
|
154133
|
-
if (input.agentName === "claude") return { fallback: false };
|
|
154134
|
-
if (input.authorized.has(input.resolvedModel)) return { fallback: false };
|
|
154135
|
-
return {
|
|
154136
|
-
fallback: true,
|
|
154137
|
-
from: input.resolvedModel,
|
|
154138
|
-
to: FREE_FALLBACK_SLUG
|
|
154139
|
-
};
|
|
154140
|
-
}
|
|
154141
|
-
|
|
154142
154693
|
// utils/gitAuthServer.ts
|
|
154143
154694
|
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
154144
154695
|
import { writeFileSync as writeFileSync12 } from "node:fs";
|
|
154145
154696
|
import { createServer as createServer3 } from "node:http";
|
|
154146
|
-
import { join as
|
|
154697
|
+
import { join as join19 } from "node:path";
|
|
154147
154698
|
var REVOKED_TRAP_MS = 6e4;
|
|
154148
154699
|
function revokeGitHubToken(token) {
|
|
154149
154700
|
fetch("https://api.github.com/installation/token", {
|
|
@@ -154212,7 +154763,7 @@ async function startGitAuthServer(tmpdir3) {
|
|
|
154212
154763
|
function writeAskpassScript(code) {
|
|
154213
154764
|
const scriptId = randomUUID5();
|
|
154214
154765
|
const scriptName = `askpass-${scriptId}.js`;
|
|
154215
|
-
const scriptPath =
|
|
154766
|
+
const scriptPath = join19(tmpdir3, scriptName);
|
|
154216
154767
|
const content = [
|
|
154217
154768
|
`#!/usr/bin/env node`,
|
|
154218
154769
|
`var a=process.argv[2]||"";`,
|
|
@@ -154250,7 +154801,7 @@ async function startGitAuthServer(tmpdir3) {
|
|
|
154250
154801
|
var core3 = __toESM(require_core(), 1);
|
|
154251
154802
|
import { createSign } from "node:crypto";
|
|
154252
154803
|
import { rename, writeFile } from "node:fs/promises";
|
|
154253
|
-
import { dirname as dirname3, join as
|
|
154804
|
+
import { dirname as dirname3, join as join20 } from "node:path";
|
|
154254
154805
|
|
|
154255
154806
|
// node_modules/.pnpm/@octokit+plugin-throttling@11.0.3_@octokit+core@7.0.5/node_modules/@octokit/plugin-throttling/dist-bundle/index.js
|
|
154256
154807
|
var import_light = __toESM(require_light(), 1);
|
|
@@ -157892,6 +158443,7 @@ var Octokit2 = Octokit.plugin(requestLog, legacyRestEndpointMethods, paginateRes
|
|
|
157892
158443
|
);
|
|
157893
158444
|
|
|
157894
158445
|
// utils/github.ts
|
|
158446
|
+
var OIDC_AUDIENCE = "pullfrog-api";
|
|
157895
158447
|
function isObject4(value2) {
|
|
157896
158448
|
return typeof value2 === "object" && value2 !== null;
|
|
157897
158449
|
}
|
|
@@ -157908,8 +158460,39 @@ var TokenExchangeError = class extends Error {
|
|
|
157908
158460
|
this.status = status;
|
|
157909
158461
|
}
|
|
157910
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
|
+
}
|
|
157911
158494
|
async function acquireTokenViaOIDC(opts) {
|
|
157912
|
-
const oidcToken = await core3.getIDToken(
|
|
158495
|
+
const oidcToken = opts?.oidc ? await fetchIdTokenFromStash(opts.oidc) : await core3.getIDToken(OIDC_AUDIENCE);
|
|
157913
158496
|
const repos = [...opts?.repos ?? []];
|
|
157914
158497
|
const targetRepo = process.env.GITHUB_REPOSITORY?.split("/")[1];
|
|
157915
158498
|
if (targetRepo) {
|
|
@@ -158058,14 +158641,15 @@ async function acquireTokenViaGitHubApp(opts) {
|
|
|
158058
158641
|
const installationId = await findInstallationId(jwt2, config3.repoOwner, config3.repoName);
|
|
158059
158642
|
return await createInstallationToken(jwt2, installationId, opts?.permissions);
|
|
158060
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
|
+
}
|
|
158061
158648
|
async function acquireNewToken(opts) {
|
|
158062
|
-
if (isOIDCAvailable()) {
|
|
158649
|
+
if (opts?.oidc || isOIDCAvailable()) {
|
|
158063
158650
|
return await retry(() => acquireTokenViaOIDC(opts), {
|
|
158064
158651
|
label: "token exchange",
|
|
158065
|
-
shouldRetry:
|
|
158066
|
-
if (error49 instanceof TokenExchangeError) return error49.status >= 500 || error49.status === 429;
|
|
158067
|
-
return error49 instanceof Error && (error49.message.includes("timed out") || error49.message.includes("fetch failed") || error49.message.includes("ECONNRESET") || error49.message.includes("ETIMEDOUT"));
|
|
158068
|
-
}
|
|
158652
|
+
shouldRetry: isTransientTokenError
|
|
158069
158653
|
});
|
|
158070
158654
|
}
|
|
158071
158655
|
if (process.env.GITHUB_ACTIONS === "true") {
|
|
@@ -158108,14 +158692,14 @@ function getGitHubUsageSummary() {
|
|
|
158108
158692
|
}
|
|
158109
158693
|
async function writeGitHubUsageSummaryToFile(path4) {
|
|
158110
158694
|
const summary2 = getGitHubUsageSummary();
|
|
158111
|
-
const tmpPath =
|
|
158695
|
+
const tmpPath = join20(dirname3(path4), `.usage-summary-${process.pid}.tmp`);
|
|
158112
158696
|
await writeFile(tmpPath, JSON.stringify(summary2));
|
|
158113
158697
|
await rename(tmpPath, path4);
|
|
158114
158698
|
}
|
|
158115
|
-
function createOctokit(token) {
|
|
158699
|
+
function createOctokit(token, refreshAuth) {
|
|
158700
|
+
let currentToken = token;
|
|
158116
158701
|
const OctokitWithPlugins = Octokit2.plugin(throttling);
|
|
158117
158702
|
const octokit = new OctokitWithPlugins({
|
|
158118
|
-
auth: token,
|
|
158119
158703
|
throttle: {
|
|
158120
158704
|
onRateLimit: (_retryAfter, _options, _octokit, retryCount) => {
|
|
158121
158705
|
return retryCount <= 2;
|
|
@@ -158144,6 +158728,8 @@ function createOctokit(token) {
|
|
|
158144
158728
|
return response;
|
|
158145
158729
|
};
|
|
158146
158730
|
octokit.hook.wrap("request", async (request2, options) => {
|
|
158731
|
+
const sentToken = currentToken;
|
|
158732
|
+
options.headers.authorization = `token ${sentToken}`;
|
|
158147
158733
|
try {
|
|
158148
158734
|
const response = await request2(options);
|
|
158149
158735
|
onResponse(response);
|
|
@@ -158152,6 +158738,13 @@ function createOctokit(token) {
|
|
|
158152
158738
|
if (isObject4(error49) && "response" in error49 && isObject4(error49.response) && "headers" in error49.response && isObject4(error49.response.headers)) {
|
|
158153
158739
|
onResponse(error49.response);
|
|
158154
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
|
+
}
|
|
158155
158748
|
throw error49;
|
|
158156
158749
|
}
|
|
158157
158750
|
});
|
|
@@ -158350,7 +158943,17 @@ Use \`${t("git")}\` for local git commands (status, log, add, commit, checkout,
|
|
|
158350
158943
|
- \`${t("checkout_pr")}\` - checkout a PR branch (fetches and configures push for forks)
|
|
158351
158944
|
- \`${t("delete_branch")}\` - delete a remote branch (requires push: enabled)
|
|
158352
158945
|
- \`${t("push_tags")}\` - push tags (requires push: enabled)
|
|
158353
|
-
|
|
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
|
+
` : ""}
|
|
158354
158957
|
Rules:
|
|
158355
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.
|
|
158356
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.
|
|
@@ -158526,7 +159129,7 @@ function resolveInstructions(ctx) {
|
|
|
158526
159129
|
|
|
158527
159130
|
// utils/learnings.ts
|
|
158528
159131
|
import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
|
|
158529
|
-
import { dirname as dirname4, join as
|
|
159132
|
+
import { dirname as dirname4, join as join21 } from "node:path";
|
|
158530
159133
|
|
|
158531
159134
|
// utils/learningsTruncate.ts
|
|
158532
159135
|
var MAX_LEARNINGS_LENGTH = 1e5;
|
|
@@ -158543,7 +159146,7 @@ function truncateAtLineBoundary(body, cap) {
|
|
|
158543
159146
|
// utils/learnings.ts
|
|
158544
159147
|
var LEARNINGS_FILE_NAME = "pullfrog-learnings.md";
|
|
158545
159148
|
function learningsFilePath(tmpdir3) {
|
|
158546
|
-
return
|
|
159149
|
+
return join21(tmpdir3, LEARNINGS_FILE_NAME);
|
|
158547
159150
|
}
|
|
158548
159151
|
async function seedLearningsFile(params) {
|
|
158549
159152
|
const path4 = learningsFilePath(params.tmpdir);
|
|
@@ -158963,6 +159566,10 @@ function formatTransientErrorSummary(error49, owner) {
|
|
|
158963
159566
|
var core7 = __toESM(require_core(), 1);
|
|
158964
159567
|
import assert2 from "node:assert/strict";
|
|
158965
159568
|
var mcpTokenValue;
|
|
159569
|
+
var refreshMcpTokenFn;
|
|
159570
|
+
function getMcpTokenRefresh() {
|
|
159571
|
+
return refreshMcpTokenFn;
|
|
159572
|
+
}
|
|
158966
159573
|
function getJobToken() {
|
|
158967
159574
|
const inputToken = core7.getInput("token");
|
|
158968
159575
|
if (inputToken) {
|
|
@@ -159014,6 +159621,29 @@ async function resolveTokens(params) {
|
|
|
159014
159621
|
`\xBB acquired scoped MCP token (${Object.entries(mcpPermissions).map((e) => e.join(":")).join(", ")})`
|
|
159015
159622
|
);
|
|
159016
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
|
+
};
|
|
159017
159647
|
let disposingRef;
|
|
159018
159648
|
const dispose = async () => {
|
|
159019
159649
|
if (disposingRef) {
|
|
@@ -159022,9 +159652,10 @@ async function resolveTokens(params) {
|
|
|
159022
159652
|
disposingRef = Promise.withResolvers();
|
|
159023
159653
|
try {
|
|
159024
159654
|
mcpTokenValue = void 0;
|
|
159655
|
+
refreshMcpTokenFn = void 0;
|
|
159025
159656
|
await Promise.all([
|
|
159026
159657
|
revokeGitHubInstallationToken(gitToken),
|
|
159027
|
-
revokeGitHubInstallationToken(
|
|
159658
|
+
revokeGitHubInstallationToken(currentMcpToken)
|
|
159028
159659
|
]);
|
|
159029
159660
|
} finally {
|
|
159030
159661
|
removeSignalHandler();
|
|
@@ -159068,7 +159699,7 @@ async function reportErrorToComment(ctx) {
|
|
|
159068
159699
|
|
|
159069
159700
|
${ctx.error}` : ctx.error;
|
|
159070
159701
|
const repoContext = parseRepoContext();
|
|
159071
|
-
const octokit = createOctokit(getGitHubInstallationToken());
|
|
159702
|
+
const octokit = createOctokit(getGitHubInstallationToken(), getMcpTokenRefresh());
|
|
159072
159703
|
const runId = process.env.GITHUB_RUN_ID ? Number.parseInt(process.env.GITHUB_RUN_ID, 10) : void 0;
|
|
159073
159704
|
const customParts = [];
|
|
159074
159705
|
if (runId) {
|
|
@@ -159082,7 +159713,6 @@ ${ctx.error}` : ctx.error;
|
|
|
159082
159713
|
workflowRun: runId ? { owner: repoContext.owner, repo: repoContext.name, runId } : void 0,
|
|
159083
159714
|
customParts,
|
|
159084
159715
|
model: ctx.toolState.model,
|
|
159085
|
-
fallbackFrom: ctx.toolState.modelFallback?.from,
|
|
159086
159716
|
oss: ctx.toolState.oss
|
|
159087
159717
|
});
|
|
159088
159718
|
const body = `${formattedError}${footer}`;
|
|
@@ -159149,18 +159779,15 @@ async function mintProxyKey(ctx) {
|
|
|
159149
159779
|
if (error49 instanceof TransientError) throw error49;
|
|
159150
159780
|
log.warning(`proxy key mint error: ${error49 instanceof Error ? error49.message : String(error49)}`);
|
|
159151
159781
|
return null;
|
|
159152
|
-
} finally {
|
|
159153
|
-
delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
159154
|
-
delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
159155
159782
|
}
|
|
159156
159783
|
}
|
|
159157
159784
|
async function buildProxyTokenHeaders(ctx) {
|
|
159158
159785
|
if (ctx.oidcCredentials) {
|
|
159159
|
-
|
|
159160
|
-
|
|
159161
|
-
|
|
159162
|
-
|
|
159163
|
-
|
|
159786
|
+
const creds = ctx.oidcCredentials;
|
|
159787
|
+
const oidcToken = await retry(() => fetchIdTokenFromStash(creds), {
|
|
159788
|
+
label: "ID token mint",
|
|
159789
|
+
shouldRetry: isTransientTokenError
|
|
159790
|
+
});
|
|
159164
159791
|
return { Authorization: `Bearer ${oidcToken}` };
|
|
159165
159792
|
}
|
|
159166
159793
|
if (isLocalApiUrl()) {
|
|
@@ -159224,7 +159851,7 @@ async function runProxyResolution(ctx) {
|
|
|
159224
159851
|
|
|
159225
159852
|
// utils/prSummary.ts
|
|
159226
159853
|
import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
|
|
159227
|
-
import { dirname as dirname5, join as
|
|
159854
|
+
import { dirname as dirname5, join as join22 } from "node:path";
|
|
159228
159855
|
var SUMMARY_FILE_NAME = "pullfrog-summary.md";
|
|
159229
159856
|
var SUMMARY_SCAFFOLD = `# PR summary
|
|
159230
159857
|
|
|
@@ -159234,7 +159861,7 @@ var SUMMARY_SCAFFOLD = `# PR summary
|
|
|
159234
159861
|
var MIN_SNAPSHOT_LENGTH = 60;
|
|
159235
159862
|
var MAX_SNAPSHOT_LENGTH = 32768;
|
|
159236
159863
|
function summaryFilePath(tmpdir3) {
|
|
159237
|
-
return
|
|
159864
|
+
return join22(tmpdir3, SUMMARY_FILE_NAME);
|
|
159238
159865
|
}
|
|
159239
159866
|
async function seedSummaryFile(params) {
|
|
159240
159867
|
const path4 = summaryFilePath(params.tmpdir);
|
|
@@ -159362,6 +159989,7 @@ var defaultSettings = {
|
|
|
159362
159989
|
push: "restricted",
|
|
159363
159990
|
shell: "restricted",
|
|
159364
159991
|
prApproveEnabled: false,
|
|
159992
|
+
signedCommits: false,
|
|
159365
159993
|
modeInstructions: {},
|
|
159366
159994
|
learnings: null,
|
|
159367
159995
|
learningsHeadings: [],
|
|
@@ -159594,7 +160222,7 @@ ${input.errorMessage}
|
|
|
159594
160222
|
].join("\n");
|
|
159595
160223
|
}
|
|
159596
160224
|
function formatProviderModelNotFoundSummary(input) {
|
|
159597
|
-
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.
|
|
159598
160226
|
|
|
159599
160227
|
\`\`\`
|
|
159600
160228
|
${input.raw}
|
|
@@ -160035,7 +160663,7 @@ async function main() {
|
|
|
160035
160663
|
const initialOctokit = createOctokit(jobToken);
|
|
160036
160664
|
const runContext = await resolveRunContextData({ octokit: initialOctokit, token: jobToken });
|
|
160037
160665
|
timer.checkpoint("runContextData");
|
|
160038
|
-
|
|
160666
|
+
createTempDirectory();
|
|
160039
160667
|
const opencodeCliPath = await agents.opencode.install();
|
|
160040
160668
|
captureBaselineModels(opencodeCliPath);
|
|
160041
160669
|
if (runContext.dbSecrets) {
|
|
@@ -160059,12 +160687,12 @@ async function main() {
|
|
|
160059
160687
|
if (payload.event.trigger === "pull_request_synchronize") {
|
|
160060
160688
|
toolState.beforeSha = payload.event.before_sha;
|
|
160061
160689
|
}
|
|
160062
|
-
const tokenRef = __using(_stack2, await resolveTokens({ push: payload.push }), true);
|
|
160063
|
-
wipeRunnerLeakSurface();
|
|
160064
160690
|
const oidcCredentials = process.env.ACTIONS_ID_TOKEN_REQUEST_URL && process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN ? {
|
|
160065
160691
|
requestUrl: process.env.ACTIONS_ID_TOKEN_REQUEST_URL,
|
|
160066
160692
|
requestToken: process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN
|
|
160067
160693
|
} : null;
|
|
160694
|
+
const tokenRef = __using(_stack2, await resolveTokens({ push: payload.push, oidc: oidcCredentials }), true);
|
|
160695
|
+
wipeRunnerLeakSurface();
|
|
160068
160696
|
if (payload.shell !== "enabled") {
|
|
160069
160697
|
delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
160070
160698
|
delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
@@ -160077,7 +160705,7 @@ async function main() {
|
|
|
160077
160705
|
repo: runContext.repo,
|
|
160078
160706
|
toolState
|
|
160079
160707
|
});
|
|
160080
|
-
const octokit = createOctokit(tokenRef.mcpToken);
|
|
160708
|
+
const octokit = createOctokit(tokenRef.mcpToken, getMcpTokenRefresh());
|
|
160081
160709
|
const runInfo = await resolveRun({ octokit });
|
|
160082
160710
|
let toolContext;
|
|
160083
160711
|
let progressCallbackDisabled = false;
|
|
@@ -160089,13 +160717,13 @@ async function main() {
|
|
|
160089
160717
|
if (payload.cwd && process.cwd() !== payload.cwd) {
|
|
160090
160718
|
process.chdir(payload.cwd);
|
|
160091
160719
|
}
|
|
160092
|
-
const
|
|
160720
|
+
const tmpdir3 = createTempDirectory();
|
|
160093
160721
|
const originalBody = payload.event.body;
|
|
160094
160722
|
const resolvedBody = await resolveBody({
|
|
160095
160723
|
event: payload.event,
|
|
160096
160724
|
octokit,
|
|
160097
160725
|
repo: runContext.repo,
|
|
160098
|
-
tmpdir:
|
|
160726
|
+
tmpdir: tmpdir3,
|
|
160099
160727
|
githubToken: tokenRef.mcpToken
|
|
160100
160728
|
});
|
|
160101
160729
|
if (resolvedBody !== originalBody) {
|
|
@@ -160104,33 +160732,18 @@ async function main() {
|
|
|
160104
160732
|
payload.prompt = payload.prompt.replace(originalBody, resolvedBody ?? "");
|
|
160105
160733
|
}
|
|
160106
160734
|
}
|
|
160107
|
-
const gitAuthServer = __using(_stack, await startGitAuthServer(
|
|
160735
|
+
const gitAuthServer = __using(_stack, await startGitAuthServer(tmpdir3), true);
|
|
160108
160736
|
setGitAuthServer(gitAuthServer);
|
|
160109
|
-
const
|
|
160110
|
-
const authorized2 = getAuthorizedModels();
|
|
160111
|
-
const fallback = selectFallbackModelIfNeeded({
|
|
160112
|
-
resolvedModel: initialResolvedModel,
|
|
160113
|
-
proxyModel: payload.proxyModel,
|
|
160114
|
-
authorized: authorized2,
|
|
160115
|
-
agentName: resolveAgent({ model: initialResolvedModel }).name
|
|
160116
|
-
});
|
|
160117
|
-
const effectiveSlug = fallback.fallback ? fallback.to : payload.model;
|
|
160118
|
-
const resolvedModel = fallback.fallback ? fallback.to : initialResolvedModel;
|
|
160119
|
-
if (fallback.fallback) {
|
|
160120
|
-
log.warning(
|
|
160121
|
-
`\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.`
|
|
160122
|
-
);
|
|
160123
|
-
toolState.modelFallback = { from: fallback.from };
|
|
160124
|
-
}
|
|
160737
|
+
const resolvedModel = payload.proxyModel ? void 0 : resolveModel({ slug: payload.model });
|
|
160125
160738
|
vertexCredentials = materializeVertexCredentials({ model: resolvedModel });
|
|
160126
160739
|
const agent2 = resolveAgent({ model: resolvedModel });
|
|
160127
|
-
const effectiveModel = payload.proxyModel ?? resolvedModel ??
|
|
160740
|
+
const effectiveModel = payload.proxyModel ?? resolvedModel ?? payload.model;
|
|
160128
160741
|
toolState.model = effectiveModel;
|
|
160129
|
-
if (!
|
|
160742
|
+
if (!payload.proxyModel) {
|
|
160130
160743
|
validateAgentApiKey({
|
|
160131
160744
|
agent: agent2,
|
|
160132
160745
|
model: effectiveModel,
|
|
160133
|
-
authorized:
|
|
160746
|
+
authorized: getAuthorizedModels(),
|
|
160134
160747
|
owner: runContext.repo.owner,
|
|
160135
160748
|
name: runContext.repo.name
|
|
160136
160749
|
});
|
|
@@ -160147,7 +160760,7 @@ async function main() {
|
|
|
160147
160760
|
timer.checkpoint("git");
|
|
160148
160761
|
const pmSpec = await resolvePackageManagerSpec(process.cwd());
|
|
160149
160762
|
if (pmSpec) {
|
|
160150
|
-
await ensurePackageManager({ spec: pmSpec, binDir: packageManagerBinDir(
|
|
160763
|
+
await ensurePackageManager({ spec: pmSpec, binDir: packageManagerBinDir(tmpdir3) });
|
|
160151
160764
|
}
|
|
160152
160765
|
timer.checkpoint("packageManager");
|
|
160153
160766
|
const setupHook = await executeLifecycleHook({
|
|
@@ -160160,26 +160773,34 @@ async function main() {
|
|
|
160160
160773
|
}
|
|
160161
160774
|
timer.checkpoint("lifecycleHooks::setup");
|
|
160162
160775
|
const agentId = agent2.name;
|
|
160163
|
-
const modes2 = [
|
|
160776
|
+
const modes2 = [
|
|
160777
|
+
...computeModes(agentId, runContext.repoSettings.signedCommits),
|
|
160778
|
+
...runContext.repoSettings.modes
|
|
160779
|
+
];
|
|
160164
160780
|
const outputSchema = resolveOutputSchema();
|
|
160165
160781
|
toolContext = {
|
|
160166
160782
|
agentId,
|
|
160167
160783
|
repo: runContext.repo,
|
|
160168
160784
|
payload,
|
|
160169
160785
|
octokit,
|
|
160170
|
-
|
|
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
|
+
},
|
|
160171
160791
|
gitToken: tokenRef.gitToken,
|
|
160172
160792
|
apiToken: runContext.apiToken,
|
|
160173
160793
|
modes: modes2,
|
|
160174
160794
|
postCheckoutScript: runContext.repoSettings.postCheckoutScript,
|
|
160175
160795
|
prepushScript: runContext.repoSettings.prepushScript,
|
|
160176
160796
|
prApproveEnabled: runContext.repoSettings.prApproveEnabled,
|
|
160797
|
+
signedCommits: runContext.repoSettings.signedCommits,
|
|
160177
160798
|
modeInstructions: runContext.repoSettings.modeInstructions,
|
|
160178
160799
|
toolState,
|
|
160179
160800
|
runId: runInfo.runId,
|
|
160180
160801
|
jobId: runInfo.jobId,
|
|
160181
160802
|
mcpServerUrl: "",
|
|
160182
|
-
tmpdir:
|
|
160803
|
+
tmpdir: tmpdir3,
|
|
160183
160804
|
oss: runContext.oss,
|
|
160184
160805
|
plan: runContext.plan,
|
|
160185
160806
|
resolvedModel
|
|
@@ -160190,7 +160811,7 @@ async function main() {
|
|
|
160190
160811
|
timer.checkpoint("mcpServer");
|
|
160191
160812
|
try {
|
|
160192
160813
|
const learningsPath = await seedLearningsFile({
|
|
160193
|
-
tmpdir:
|
|
160814
|
+
tmpdir: tmpdir3,
|
|
160194
160815
|
current: runContext.repoSettings.learnings
|
|
160195
160816
|
});
|
|
160196
160817
|
toolState.learningsFilePath = learningsPath;
|
|
@@ -160207,7 +160828,7 @@ async function main() {
|
|
|
160207
160828
|
}
|
|
160208
160829
|
if (payload.generateSummary && payload.event.is_pr && payload.event.issue_number) {
|
|
160209
160830
|
const previousSnapshot = await fetchPreviousSnapshot(toolContext, payload.event.issue_number);
|
|
160210
|
-
const filePath = await seedSummaryFile({ tmpdir:
|
|
160831
|
+
const filePath = await seedSummaryFile({ tmpdir: tmpdir3, previousSnapshot });
|
|
160211
160832
|
toolState.summaryFilePath = filePath;
|
|
160212
160833
|
try {
|
|
160213
160834
|
toolState.summarySeed = await readFile5(filePath, "utf8");
|
|
@@ -160227,6 +160848,7 @@ async function main() {
|
|
|
160227
160848
|
modes: modes2,
|
|
160228
160849
|
agentId,
|
|
160229
160850
|
outputSchema,
|
|
160851
|
+
signedCommits: runContext.repoSettings.signedCommits,
|
|
160230
160852
|
learningsFilePath: toolState.learningsFilePath ?? null,
|
|
160231
160853
|
learningsHeadings: runContext.repoSettings.learningsHeadings,
|
|
160232
160854
|
setupHookFailure: describeSetupFailure(setupHook.failure)
|
|
@@ -160245,7 +160867,7 @@ ${instructions.user}` : null,
|
|
|
160245
160867
|
log.info(instructions.full);
|
|
160246
160868
|
});
|
|
160247
160869
|
if (agentId === "opencode") {
|
|
160248
|
-
const pluginDir =
|
|
160870
|
+
const pluginDir = join23(process.cwd(), ".opencode", "plugin");
|
|
160249
160871
|
const hasPlugins = existsSync8(pluginDir) && readdirSync2(pluginDir).some((f) => /\.[jt]sx?$/.test(f));
|
|
160250
160872
|
if (hasPlugins && toolState.dependencyInstallation?.promise) {
|
|
160251
160873
|
log.info(
|
|
@@ -160300,7 +160922,7 @@ ${instructions.user}` : null,
|
|
|
160300
160922
|
payload,
|
|
160301
160923
|
resolvedModel,
|
|
160302
160924
|
mcpServerUrl: mcpHttpServer.url,
|
|
160303
|
-
tmpdir:
|
|
160925
|
+
tmpdir: tmpdir3,
|
|
160304
160926
|
// PULLFROG_DATA_DIR (/var/lib/pullfrog) holds codex auth.json + any
|
|
160305
160927
|
// future pullfrog-managed on-disk secrets. bash via MCP tmpfs-overlays
|
|
160306
160928
|
// it; agent native FS tools deny it via the same secretDenyPaths plumbing
|