pullfrog 0.1.7 → 0.1.9
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/opencodeShared.d.ts +40 -0
- package/dist/agents/postRun.d.ts +11 -3
- package/dist/agents/shared.d.ts +7 -0
- package/dist/cli.mjs +4609 -3445
- package/dist/external.d.ts +1 -1
- package/dist/index.js +2048 -1416
- package/dist/internal/index.d.ts +2 -1
- package/dist/internal.js +245 -85
- package/dist/mcp/shell.d.ts +5 -0
- package/dist/models.d.ts +10 -0
- package/dist/modes.d.ts +1 -1
- package/dist/toolState.d.ts +6 -0
- package/dist/utils/activity.d.ts +31 -1
- package/dist/utils/agentHangReport.d.ts +38 -0
- package/dist/utils/apiKeys.d.ts +5 -1
- package/dist/utils/billingErrors.d.ts +85 -0
- package/dist/utils/buildPullfrogFooter.d.ts +7 -0
- package/dist/utils/byokFallback.d.ts +50 -0
- package/dist/utils/codexHome.d.ts +23 -0
- package/dist/utils/errorReport.d.ts +9 -0
- package/dist/utils/gitAuth.d.ts +27 -0
- package/dist/utils/learnings.d.ts +20 -0
- package/dist/utils/learningsTruncate.d.ts +25 -0
- package/dist/utils/lifecycle.d.ts +23 -3
- package/dist/utils/overrides.d.ts +40 -0
- package/dist/utils/payload.d.ts +7 -0
- package/dist/utils/prSummary.d.ts +21 -0
- package/dist/utils/providerErrors.d.ts +11 -0
- package/dist/utils/proxy.d.ts +47 -0
- package/dist/utils/runContext.d.ts +0 -9
- package/dist/utils/runErrorRenderer.d.ts +41 -0
- package/dist/utils/runLifecycle.d.ts +75 -0
- package/dist/utils/runStartupLog.d.ts +15 -0
- package/dist/utils/subprocess.d.ts +1 -0
- package/package.json +3 -2
- /package/dist/agents/{opencode.d.ts → opencode_v2.d.ts} +0 -0
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 setSecret5(secret) {
|
|
19722
19722
|
(0, command_1.issueCommand)("add-mask", {}, secret);
|
|
19723
19723
|
}
|
|
19724
|
-
exports.setSecret =
|
|
19724
|
+
exports.setSecret = setSecret5;
|
|
19725
19725
|
function addPath(inputPath) {
|
|
19726
19726
|
const filePath = process.env["GITHUB_PATH"] || "";
|
|
19727
19727
|
if (filePath) {
|
|
@@ -19732,7 +19732,7 @@ var require_core = __commonJS({
|
|
|
19732
19732
|
process.env["PATH"] = `${inputPath}${path3.delimiter}${process.env["PATH"]}`;
|
|
19733
19733
|
}
|
|
19734
19734
|
exports.addPath = addPath;
|
|
19735
|
-
function
|
|
19735
|
+
function getInput3(name, options) {
|
|
19736
19736
|
const val = process.env[`INPUT_${name.replace(/ /g, "_").toUpperCase()}`] || "";
|
|
19737
19737
|
if (options && options.required && !val) {
|
|
19738
19738
|
throw new Error(`Input required and not supplied: ${name}`);
|
|
@@ -19742,9 +19742,9 @@ var require_core = __commonJS({
|
|
|
19742
19742
|
}
|
|
19743
19743
|
return val.trim();
|
|
19744
19744
|
}
|
|
19745
|
-
exports.getInput =
|
|
19745
|
+
exports.getInput = getInput3;
|
|
19746
19746
|
function getMultilineInput(name, options) {
|
|
19747
|
-
const inputs =
|
|
19747
|
+
const inputs = getInput3(name, options).split("\n").filter((x) => x !== "");
|
|
19748
19748
|
if (options && options.trimWhitespace === false) {
|
|
19749
19749
|
return inputs;
|
|
19750
19750
|
}
|
|
@@ -19754,7 +19754,7 @@ var require_core = __commonJS({
|
|
|
19754
19754
|
function getBooleanInput(name, options) {
|
|
19755
19755
|
const trueValue = ["true", "True", "TRUE"];
|
|
19756
19756
|
const falseValue = ["false", "False", "FALSE"];
|
|
19757
|
-
const val =
|
|
19757
|
+
const val = getInput3(name, options);
|
|
19758
19758
|
if (trueValue.includes(val))
|
|
19759
19759
|
return true;
|
|
19760
19760
|
if (falseValue.includes(val))
|
|
@@ -19826,14 +19826,14 @@ Support boolean input list: \`true | True | TRUE | false | False | FALSE\``);
|
|
|
19826
19826
|
});
|
|
19827
19827
|
}
|
|
19828
19828
|
exports.group = group2;
|
|
19829
|
-
function
|
|
19829
|
+
function saveState2(name, value2) {
|
|
19830
19830
|
const filePath = process.env["GITHUB_STATE"] || "";
|
|
19831
19831
|
if (filePath) {
|
|
19832
19832
|
return (0, file_command_1.issueFileCommand)("STATE", (0, file_command_1.prepareKeyValueMessage)(name, value2));
|
|
19833
19833
|
}
|
|
19834
19834
|
(0, command_1.issueCommand)("save-state", { name }, (0, utils_1.toCommandValue)(value2));
|
|
19835
19835
|
}
|
|
19836
|
-
exports.saveState =
|
|
19836
|
+
exports.saveState = saveState2;
|
|
19837
19837
|
function getState(name) {
|
|
19838
19838
|
return process.env[`STATE_${name}`] || "";
|
|
19839
19839
|
}
|
|
@@ -47737,7 +47737,7 @@ var require_core3 = __commonJS({
|
|
|
47737
47737
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47738
47738
|
var id_1 = require_id();
|
|
47739
47739
|
var ref_1 = require_ref();
|
|
47740
|
-
var
|
|
47740
|
+
var core11 = [
|
|
47741
47741
|
"$schema",
|
|
47742
47742
|
"$id",
|
|
47743
47743
|
"$defs",
|
|
@@ -47747,7 +47747,7 @@ var require_core3 = __commonJS({
|
|
|
47747
47747
|
id_1.default,
|
|
47748
47748
|
ref_1.default
|
|
47749
47749
|
];
|
|
47750
|
-
exports.default =
|
|
47750
|
+
exports.default = core11;
|
|
47751
47751
|
}
|
|
47752
47752
|
});
|
|
47753
47753
|
|
|
@@ -97475,14 +97475,14 @@ var require_turndown_cjs = __commonJS({
|
|
|
97475
97475
|
} else if (node2.nodeType === 1) {
|
|
97476
97476
|
replacement = replacementForNode.call(self2, node2);
|
|
97477
97477
|
}
|
|
97478
|
-
return
|
|
97478
|
+
return join19(output, replacement);
|
|
97479
97479
|
}, "");
|
|
97480
97480
|
}
|
|
97481
97481
|
function postProcess(output) {
|
|
97482
97482
|
var self2 = this;
|
|
97483
97483
|
this.rules.forEach(function(rule) {
|
|
97484
97484
|
if (typeof rule.append === "function") {
|
|
97485
|
-
output =
|
|
97485
|
+
output = join19(output, rule.append(self2.options));
|
|
97486
97486
|
}
|
|
97487
97487
|
});
|
|
97488
97488
|
return output.replace(/^[\t\r\n]+/, "").replace(/[\t\r\n\s]+$/, "");
|
|
@@ -97494,7 +97494,7 @@ var require_turndown_cjs = __commonJS({
|
|
|
97494
97494
|
if (whitespace.leading || whitespace.trailing) content = content.trim();
|
|
97495
97495
|
return whitespace.leading + rule.replacement(content, node2, this.options) + whitespace.trailing;
|
|
97496
97496
|
}
|
|
97497
|
-
function
|
|
97497
|
+
function join19(output, replacement) {
|
|
97498
97498
|
var s1 = trimTrailingNewlines(output);
|
|
97499
97499
|
var s2 = trimLeadingNewlines(replacement);
|
|
97500
97500
|
var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
|
|
@@ -98924,10 +98924,9 @@ var require_fast_content_type_parse = __commonJS({
|
|
|
98924
98924
|
});
|
|
98925
98925
|
|
|
98926
98926
|
// main.ts
|
|
98927
|
-
var core7 = __toESM(require_core(), 1);
|
|
98928
98927
|
import { existsSync as existsSync7, readdirSync } from "node:fs";
|
|
98929
98928
|
import { readFile as readFile4 } from "node:fs/promises";
|
|
98930
|
-
import { join as
|
|
98929
|
+
import { join as join18 } from "node:path";
|
|
98931
98930
|
|
|
98932
98931
|
// node_modules/.pnpm/@ark+util@0.56.0/node_modules/@ark/util/out/arrays.js
|
|
98933
98932
|
var liftArray = (data) => Array.isArray(data) ? data : [data];
|
|
@@ -107762,6 +107761,7 @@ var providers = {
|
|
|
107762
107761
|
openai: provider({
|
|
107763
107762
|
displayName: "OpenAI",
|
|
107764
107763
|
envVars: ["OPENAI_API_KEY"],
|
|
107764
|
+
managedCredentials: ["CODEX_AUTH_JSON"],
|
|
107765
107765
|
models: {
|
|
107766
107766
|
gpt: {
|
|
107767
107767
|
displayName: "GPT",
|
|
@@ -107821,12 +107821,16 @@ var providers = {
|
|
|
107821
107821
|
displayName: "Gemini Pro",
|
|
107822
107822
|
resolve: "google/gemini-3.1-pro-preview",
|
|
107823
107823
|
openRouterResolve: "openrouter/google/gemini-3.1-pro-preview",
|
|
107824
|
-
preferred: true
|
|
107825
|
-
|
|
107824
|
+
preferred: true
|
|
107825
|
+
// Inherit (subagents stay on Pro). Google has no in-between tier;
|
|
107826
|
+
// dropping to Flash for review work was a meaningful capability cliff
|
|
107827
|
+
// (Flash missed the catastrophic camelCase/snake_case mismatch in
|
|
107828
|
+
// the v4 e2e test). Pro is cost-effective enough to use for both
|
|
107829
|
+
// orchestrator and lenses.
|
|
107826
107830
|
},
|
|
107827
107831
|
"gemini-flash": {
|
|
107828
107832
|
displayName: "Gemini Flash",
|
|
107829
|
-
resolve: "google/gemini-3-flash
|
|
107833
|
+
resolve: "google/gemini-3.5-flash",
|
|
107830
107834
|
openRouterResolve: "openrouter/google/gemini-3-flash-preview"
|
|
107831
107835
|
}
|
|
107832
107836
|
}
|
|
@@ -107841,15 +107845,22 @@ var providers = {
|
|
|
107841
107845
|
openRouterResolve: "openrouter/x-ai/grok-4.3",
|
|
107842
107846
|
preferred: true
|
|
107843
107847
|
},
|
|
107848
|
+
// legacy aliases — xAI retired the entire fast/code-fast line on
|
|
107849
|
+
// 2026-05-15 (https://docs.x.ai/developers/migration/may-15-deprecation)
|
|
107850
|
+
// and now redirects every deprecated text-model slug to grok-4.3 at
|
|
107851
|
+
// standard pricing. fall back to the live `xai/grok` so the alias
|
|
107852
|
+
// chain resolves to grok-4.3 for both direct-key and OpenRouter users.
|
|
107844
107853
|
"grok-fast": {
|
|
107845
107854
|
displayName: "Grok Fast",
|
|
107846
107855
|
resolve: "xai/grok-4-1-fast",
|
|
107847
|
-
openRouterResolve: "openrouter/x-ai/grok-4.
|
|
107856
|
+
openRouterResolve: "openrouter/x-ai/grok-4.3",
|
|
107857
|
+
fallback: "xai/grok"
|
|
107848
107858
|
},
|
|
107849
107859
|
"grok-code-fast": {
|
|
107850
107860
|
displayName: "Grok Code Fast",
|
|
107851
107861
|
resolve: "xai/grok-code-fast-1",
|
|
107852
|
-
openRouterResolve: "openrouter/x-ai/grok-
|
|
107862
|
+
openRouterResolve: "openrouter/x-ai/grok-4.3",
|
|
107863
|
+
fallback: "xai/grok"
|
|
107853
107864
|
}
|
|
107854
107865
|
}
|
|
107855
107866
|
}),
|
|
@@ -107963,8 +107974,8 @@ var providers = {
|
|
|
107963
107974
|
"gemini-pro": {
|
|
107964
107975
|
displayName: "Gemini Pro",
|
|
107965
107976
|
resolve: "opencode/gemini-3.1-pro",
|
|
107966
|
-
openRouterResolve: "openrouter/google/gemini-3.1-pro-preview"
|
|
107967
|
-
|
|
107977
|
+
openRouterResolve: "openrouter/google/gemini-3.1-pro-preview"
|
|
107978
|
+
// Inherit — see google/gemini-pro for rationale.
|
|
107968
107979
|
},
|
|
107969
107980
|
"gemini-flash": {
|
|
107970
107981
|
displayName: "Gemini Flash",
|
|
@@ -108076,8 +108087,8 @@ var providers = {
|
|
|
108076
108087
|
"gemini-pro": {
|
|
108077
108088
|
displayName: "Gemini Pro",
|
|
108078
108089
|
resolve: "openrouter/google/gemini-3.1-pro-preview",
|
|
108079
|
-
openRouterResolve: "openrouter/google/gemini-3.1-pro-preview"
|
|
108080
|
-
|
|
108090
|
+
openRouterResolve: "openrouter/google/gemini-3.1-pro-preview"
|
|
108091
|
+
// Inherit — see google/gemini-pro for rationale.
|
|
108081
108092
|
},
|
|
108082
108093
|
"gemini-flash": {
|
|
108083
108094
|
displayName: "Gemini Flash",
|
|
@@ -108180,14 +108191,25 @@ function isBedrockAnthropicId(bedrockModelId) {
|
|
|
108180
108191
|
// utils/buildPullfrogFooter.ts
|
|
108181
108192
|
var PULLFROG_DIVIDER = "<!-- PULLFROG_DIVIDER_DO_NOT_REMOVE_PLZ -->";
|
|
108182
108193
|
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>`;
|
|
108183
|
-
function
|
|
108184
|
-
|
|
108194
|
+
function providerDisplayName(slug2) {
|
|
108195
|
+
try {
|
|
108196
|
+
const key = getModelProvider(slug2);
|
|
108197
|
+
const meta3 = providers[key];
|
|
108198
|
+
return meta3?.displayName ?? key;
|
|
108199
|
+
} catch {
|
|
108200
|
+
return slug2;
|
|
108201
|
+
}
|
|
108202
|
+
}
|
|
108203
|
+
function formatModelLabel(params) {
|
|
108204
|
+
const alias = resolveDisplayAlias(params.model) ?? // reverse-lookup: when the caller passes an effective model (proxy or
|
|
108185
108205
|
// resolved target like "openrouter/anthropic/claude-opus-4.7") instead of
|
|
108186
108206
|
// a stored alias slug, find the alias whose resolve target matches so we
|
|
108187
108207
|
// still render a friendly display name.
|
|
108188
|
-
modelAliases.find((a) => a.resolve ===
|
|
108189
|
-
|
|
108190
|
-
|
|
108208
|
+
modelAliases.find((a) => a.resolve === params.model || a.openRouterResolve === params.model);
|
|
108209
|
+
const displayName = alias?.displayName ?? params.model;
|
|
108210
|
+
const base = alias?.isFree ? `\`${displayName}\` (free)` : `\`${displayName}\``;
|
|
108211
|
+
if (!params.fallbackFrom) return base;
|
|
108212
|
+
return `${base} (credentials for ${providerDisplayName(params.fallbackFrom)} not configured)`;
|
|
108191
108213
|
}
|
|
108192
108214
|
function buildPullfrogFooter(params) {
|
|
108193
108215
|
const parts = [];
|
|
@@ -108205,7 +108227,9 @@ function buildPullfrogFooter(params) {
|
|
|
108205
108227
|
parts.push("via [Pullfrog](https://pullfrog.com)");
|
|
108206
108228
|
}
|
|
108207
108229
|
if (params.model) {
|
|
108208
|
-
parts.push(
|
|
108230
|
+
parts.push(
|
|
108231
|
+
`Using ${formatModelLabel({ model: params.model, fallbackFrom: params.fallbackFrom })}`
|
|
108232
|
+
);
|
|
108209
108233
|
}
|
|
108210
108234
|
const allParts = [...parts, "[\u{1D54F}](https://x.com/pullfrogai)"];
|
|
108211
108235
|
return `
|
|
@@ -108998,7 +109022,8 @@ function buildCommentFooter(ctx, customParts) {
|
|
|
108998
109022
|
jobId: ctx.jobId
|
|
108999
109023
|
} : void 0,
|
|
109000
109024
|
customParts,
|
|
109001
|
-
model: ctx.toolState.model
|
|
109025
|
+
model: ctx.toolState.model,
|
|
109026
|
+
fallbackFrom: ctx.toolState.modelFallback?.from
|
|
109002
109027
|
});
|
|
109003
109028
|
}
|
|
109004
109029
|
function buildImplementPlanLink(ctx, issueNumber, commentId) {
|
|
@@ -109023,7 +109048,7 @@ var Comment = type({
|
|
|
109023
109048
|
function CreateCommentTool(ctx) {
|
|
109024
109049
|
return tool({
|
|
109025
109050
|
name: "create_issue_comment",
|
|
109026
|
-
description:
|
|
109051
|
+
description: 'Create a comment on a GitHub issue or PR. Example: `create_issue_comment({ issueNumber: 1234, body: "Thanks for the report." })`. For progress/plan updates on the current run use report_progress instead \u2014 plan output (initial post AND revisions) is always posted via report_progress, never via this tool.',
|
|
109027
109052
|
parameters: Comment,
|
|
109028
109053
|
execute: execute(async ({ issueNumber, body, type: commentType }) => {
|
|
109029
109054
|
const bodyWithFooter = addFooter(ctx, body);
|
|
@@ -109096,7 +109121,7 @@ function EditCommentTool(ctx) {
|
|
|
109096
109121
|
var ReportProgress = type({
|
|
109097
109122
|
body: type.string.describe("the progress update content to share"),
|
|
109098
109123
|
"target_plan_comment?": type("boolean").describe(
|
|
109099
|
-
"
|
|
109124
|
+
"for revising an existing plan comment ONLY. set to true only when the PlanEdit checklist from select_mode tells you to (i.e. a prior plan comment was found for this issue). NEVER set on the initial plan post \u2014 the initial plan reuses the run's progress comment and is posted by calling report_progress without this flag."
|
|
109100
109125
|
)
|
|
109101
109126
|
});
|
|
109102
109127
|
async function reportProgress(ctx, params) {
|
|
@@ -109643,12 +109668,37 @@ function isActivityNoise(chunk) {
|
|
|
109643
109668
|
});
|
|
109644
109669
|
}
|
|
109645
109670
|
var _lastActivity = performance2.now();
|
|
109671
|
+
var MAX_TOOL_CALL_SUSPENSION_MS = 15 * 60 * 1e3;
|
|
109672
|
+
var _suspendedAt = null;
|
|
109673
|
+
var _suspensionTimer = null;
|
|
109646
109674
|
function markActivity() {
|
|
109647
109675
|
_lastActivity = performance2.now();
|
|
109648
109676
|
}
|
|
109649
109677
|
function getIdleMs() {
|
|
109678
|
+
if (_suspendedAt !== null) return 0;
|
|
109650
109679
|
return Math.round(performance2.now() - _lastActivity);
|
|
109651
109680
|
}
|
|
109681
|
+
function suspendActivity(maxMs = MAX_TOOL_CALL_SUSPENSION_MS) {
|
|
109682
|
+
if (_suspendedAt !== null) return;
|
|
109683
|
+
_suspendedAt = performance2.now();
|
|
109684
|
+
_suspensionTimer = setTimeout(() => {
|
|
109685
|
+
log.warning(`activity watchdog suspended >${Math.round(maxMs / 1e3)}s \u2014 auto-resuming`);
|
|
109686
|
+
resumeActivity();
|
|
109687
|
+
}, maxMs);
|
|
109688
|
+
_suspensionTimer.unref?.();
|
|
109689
|
+
}
|
|
109690
|
+
function resumeActivity() {
|
|
109691
|
+
if (_suspendedAt === null) return;
|
|
109692
|
+
_suspendedAt = null;
|
|
109693
|
+
if (_suspensionTimer) {
|
|
109694
|
+
clearTimeout(_suspensionTimer);
|
|
109695
|
+
_suspensionTimer = null;
|
|
109696
|
+
}
|
|
109697
|
+
_lastActivity = performance2.now();
|
|
109698
|
+
}
|
|
109699
|
+
function isActivitySuspended() {
|
|
109700
|
+
return _suspendedAt !== null;
|
|
109701
|
+
}
|
|
109652
109702
|
function wrapWrite(original, onActivity) {
|
|
109653
109703
|
const wrapped = (chunk, encodingOrCb, cb) => {
|
|
109654
109704
|
if (!isActivityNoise(chunk)) {
|
|
@@ -109881,6 +109931,11 @@ async function spawn(options) {
|
|
|
109881
109931
|
`spawn activity timer: pid=${child.pid} cmd=${options.cmd} timeout=${activityTimeoutMs}ms`
|
|
109882
109932
|
);
|
|
109883
109933
|
activityCheckIntervalId = setInterval(() => {
|
|
109934
|
+
if (options.isPausedExternally?.()) {
|
|
109935
|
+
lastActivityTime = performance3.now();
|
|
109936
|
+
log.debug(`spawn activity check: pid=${child.pid} paused externally`);
|
|
109937
|
+
return;
|
|
109938
|
+
}
|
|
109884
109939
|
const idleMs = performance3.now() - lastActivityTime;
|
|
109885
109940
|
log.debug(
|
|
109886
109941
|
`spawn activity check: pid=${child.pid} idle=${Math.round(idleMs)}ms / ${activityTimeoutMs}ms`
|
|
@@ -137894,7 +137949,7 @@ var require_core4 = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
137894
137949
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
137895
137950
|
const id_1 = require_id2();
|
|
137896
137951
|
const ref_1 = require_ref2();
|
|
137897
|
-
const
|
|
137952
|
+
const core11 = [
|
|
137898
137953
|
"$schema",
|
|
137899
137954
|
"$id",
|
|
137900
137955
|
"$defs",
|
|
@@ -137904,7 +137959,7 @@ var require_core4 = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
137904
137959
|
id_1.default,
|
|
137905
137960
|
ref_1.default
|
|
137906
137961
|
];
|
|
137907
|
-
exports.default =
|
|
137962
|
+
exports.default = core11;
|
|
137908
137963
|
}));
|
|
137909
137964
|
var require_limitNumber2 = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
137910
137965
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -142414,7 +142469,7 @@ var import_semver = __toESM(require_semver2(), 1);
|
|
|
142414
142469
|
// package.json
|
|
142415
142470
|
var package_default = {
|
|
142416
142471
|
name: "pullfrog",
|
|
142417
|
-
version: "0.1.
|
|
142472
|
+
version: "0.1.9",
|
|
142418
142473
|
type: "module",
|
|
142419
142474
|
bin: {
|
|
142420
142475
|
pullfrog: "dist/cli.mjs",
|
|
@@ -142430,6 +142485,7 @@ var package_default = {
|
|
|
142430
142485
|
typecheck: "tsc --noEmit",
|
|
142431
142486
|
build: "node esbuild.config.js && tsc -p tsconfig.exports.json",
|
|
142432
142487
|
"check:entrypoints": "node scripts/check-entrypoint-imports.ts",
|
|
142488
|
+
docker: "node docker.ts",
|
|
142433
142489
|
play: "node play.ts",
|
|
142434
142490
|
runtest: "node test/run.ts",
|
|
142435
142491
|
scratch: "node scratch.ts",
|
|
@@ -142463,7 +142519,7 @@ var package_default = {
|
|
|
142463
142519
|
fastmcp: "^3.34.0",
|
|
142464
142520
|
"file-type": "^21.3.0",
|
|
142465
142521
|
husky: "^9.0.0",
|
|
142466
|
-
"opencode-ai": "1.1
|
|
142522
|
+
"opencode-ai": "1.15.1",
|
|
142467
142523
|
"package-manager-detector": "^1.6.0",
|
|
142468
142524
|
picocolors: "^1.1.1",
|
|
142469
142525
|
semver: "^7.7.3",
|
|
@@ -142882,6 +142938,52 @@ function readNumber(params) {
|
|
|
142882
142938
|
import { execSync } from "node:child_process";
|
|
142883
142939
|
import { createHash } from "node:crypto";
|
|
142884
142940
|
import { readFileSync as readFileSync2, realpathSync, unlinkSync } from "node:fs";
|
|
142941
|
+
|
|
142942
|
+
// utils/shell.ts
|
|
142943
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
142944
|
+
function $(cmd, args2, options) {
|
|
142945
|
+
const encoding = options?.encoding ?? "utf-8";
|
|
142946
|
+
const env2 = resolveEnv(options?.env);
|
|
142947
|
+
const result = spawnSync2(cmd, args2, {
|
|
142948
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
142949
|
+
encoding,
|
|
142950
|
+
cwd: options?.cwd,
|
|
142951
|
+
env: env2
|
|
142952
|
+
});
|
|
142953
|
+
const stdout = result.stdout ?? "";
|
|
142954
|
+
const stderr = result.stderr ?? "";
|
|
142955
|
+
if (options?.log !== false) {
|
|
142956
|
+
const canWriteToStdout = process.stdout.isTTY === true;
|
|
142957
|
+
if (stdout) {
|
|
142958
|
+
if (canWriteToStdout) {
|
|
142959
|
+
process.stdout.write(stdout);
|
|
142960
|
+
} else {
|
|
142961
|
+
process.stderr.write(stdout);
|
|
142962
|
+
}
|
|
142963
|
+
}
|
|
142964
|
+
if (stderr) {
|
|
142965
|
+
process.stderr.write(stderr);
|
|
142966
|
+
}
|
|
142967
|
+
}
|
|
142968
|
+
if (result.status !== 0) {
|
|
142969
|
+
const errorResult = {
|
|
142970
|
+
status: result.status ?? -1,
|
|
142971
|
+
stdout,
|
|
142972
|
+
stderr
|
|
142973
|
+
};
|
|
142974
|
+
if (options?.onError) {
|
|
142975
|
+
options.onError(errorResult);
|
|
142976
|
+
return stdout.trim();
|
|
142977
|
+
}
|
|
142978
|
+
const detail = [stderr, stdout].map((s) => s.trim()).filter(Boolean).join("\n");
|
|
142979
|
+
throw new Error(
|
|
142980
|
+
`Command failed with exit code ${errorResult.status}: ${detail || "Unknown error"}`
|
|
142981
|
+
);
|
|
142982
|
+
}
|
|
142983
|
+
return stdout.trim();
|
|
142984
|
+
}
|
|
142985
|
+
|
|
142986
|
+
// utils/gitAuth.ts
|
|
142885
142987
|
var gitBinary;
|
|
142886
142988
|
function hashFile(path3) {
|
|
142887
142989
|
return createHash("sha256").update(readFileSync2(path3)).digest("hex");
|
|
@@ -142973,6 +143075,27 @@ ${stdout}` : stderr || stdout || "(no output)";
|
|
|
142973
143075
|
}
|
|
142974
143076
|
}
|
|
142975
143077
|
}
|
|
143078
|
+
var SHALLOW_UNREACHABLE_PATTERNS = [
|
|
143079
|
+
/Could not read [a-f0-9]{40,64}/,
|
|
143080
|
+
/remote did not send all necessary objects/
|
|
143081
|
+
];
|
|
143082
|
+
var DEEPEN_RETRY_DEPTH = 1e3;
|
|
143083
|
+
async function $gitFetchWithDeepen(args2, options, label) {
|
|
143084
|
+
try {
|
|
143085
|
+
return await $git("fetch", args2, options);
|
|
143086
|
+
} catch (err) {
|
|
143087
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
143088
|
+
const isShallowUnreachable = SHALLOW_UNREACHABLE_PATTERNS.some((p) => p.test(msg));
|
|
143089
|
+
if (!isShallowUnreachable) throw err;
|
|
143090
|
+
const isShallow = $("git", ["rev-parse", "--is-shallow-repository"], { log: false }).trim() === "true";
|
|
143091
|
+
if (!isShallow) throw err;
|
|
143092
|
+
log.info(
|
|
143093
|
+
`\xBB ${label ?? "git fetch"} hit shallow-unreachable error, retrying with --deepen=${DEEPEN_RETRY_DEPTH}`
|
|
143094
|
+
);
|
|
143095
|
+
const retryArgs = args2.filter((a) => !a.startsWith("--depth="));
|
|
143096
|
+
return await $git("fetch", [`--deepen=${DEEPEN_RETRY_DEPTH}`, ...retryArgs], options);
|
|
143097
|
+
}
|
|
143098
|
+
}
|
|
142976
143099
|
|
|
142977
143100
|
// lifecycle.ts
|
|
142978
143101
|
var LIFECYCLE_HOOK_TIMEOUT_MS = 6e5;
|
|
@@ -142994,6 +143117,7 @@ async function executeLifecycleHook(params) {
|
|
|
142994
143117
|
if (result.exitCode !== 0) {
|
|
142995
143118
|
const output = (result.stderr || result.stdout).trim();
|
|
142996
143119
|
return {
|
|
143120
|
+
failure: { kind: "exit", output, exitCode: result.exitCode },
|
|
142997
143121
|
warning: `lifecycle hook '${params.event}' failed with exit code ${result.exitCode}. output: ${output || "(empty)"}. retry the operation if the failure looks flaky (network blips, transient rate limits). do NOT retry if the script is broken (missing commands, syntax errors) or the error is persistent.`
|
|
142998
143122
|
};
|
|
142999
143123
|
}
|
|
@@ -143004,59 +143128,18 @@ async function executeLifecycleHook(params) {
|
|
|
143004
143128
|
if (isTimeout) {
|
|
143005
143129
|
const minutes = Math.round(LIFECYCLE_HOOK_TIMEOUT_MS / 6e4);
|
|
143006
143130
|
return {
|
|
143131
|
+
failure: { kind: "timeout" },
|
|
143007
143132
|
warning: `lifecycle hook '${params.event}' timed out after ${minutes}min. do NOT retry \u2014 the script is likely hung or doing too much work. ask the repo owner to simplify the hook (e.g. move long-running work out of the hook, add caching, or split it).`
|
|
143008
143133
|
};
|
|
143009
143134
|
}
|
|
143010
143135
|
const msg = err instanceof Error ? err.message : String(err);
|
|
143011
143136
|
return {
|
|
143137
|
+
failure: { kind: "spawn", spawnError: msg },
|
|
143012
143138
|
warning: `lifecycle hook '${params.event}' failed to spawn: ${msg}. this is likely a transient failure \u2014 retry the operation.`
|
|
143013
143139
|
};
|
|
143014
143140
|
}
|
|
143015
143141
|
}
|
|
143016
143142
|
|
|
143017
|
-
// utils/shell.ts
|
|
143018
|
-
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
143019
|
-
function $(cmd, args2, options) {
|
|
143020
|
-
const encoding = options?.encoding ?? "utf-8";
|
|
143021
|
-
const env2 = resolveEnv(options?.env);
|
|
143022
|
-
const result = spawnSync2(cmd, args2, {
|
|
143023
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
143024
|
-
encoding,
|
|
143025
|
-
cwd: options?.cwd,
|
|
143026
|
-
env: env2
|
|
143027
|
-
});
|
|
143028
|
-
const stdout = result.stdout ?? "";
|
|
143029
|
-
const stderr = result.stderr ?? "";
|
|
143030
|
-
if (options?.log !== false) {
|
|
143031
|
-
const canWriteToStdout = process.stdout.isTTY === true;
|
|
143032
|
-
if (stdout) {
|
|
143033
|
-
if (canWriteToStdout) {
|
|
143034
|
-
process.stdout.write(stdout);
|
|
143035
|
-
} else {
|
|
143036
|
-
process.stderr.write(stdout);
|
|
143037
|
-
}
|
|
143038
|
-
}
|
|
143039
|
-
if (stderr) {
|
|
143040
|
-
process.stderr.write(stderr);
|
|
143041
|
-
}
|
|
143042
|
-
}
|
|
143043
|
-
if (result.status !== 0) {
|
|
143044
|
-
const errorResult = {
|
|
143045
|
-
status: result.status ?? -1,
|
|
143046
|
-
stdout,
|
|
143047
|
-
stderr
|
|
143048
|
-
};
|
|
143049
|
-
if (options?.onError) {
|
|
143050
|
-
options.onError(errorResult);
|
|
143051
|
-
return stdout.trim();
|
|
143052
|
-
}
|
|
143053
|
-
throw new Error(
|
|
143054
|
-
`Command failed with exit code ${errorResult.status}: ${stderr || "Unknown error"}`
|
|
143055
|
-
);
|
|
143056
|
-
}
|
|
143057
|
-
return stdout.trim();
|
|
143058
|
-
}
|
|
143059
|
-
|
|
143060
143143
|
// utils/rangeDiff.ts
|
|
143061
143144
|
function computeIncrementalDiff(params) {
|
|
143062
143145
|
try {
|
|
@@ -143270,7 +143353,7 @@ function PushBranchTool(ctx) {
|
|
|
143270
143353
|
const pushPermission = ctx.payload.push;
|
|
143271
143354
|
return tool({
|
|
143272
143355
|
name: "push_branch",
|
|
143273
|
-
description: "Push the current branch to the remote repository. Omit branchName to push the current branch (recommended). Example: `push_branch({})` to push the current branch. Example: `push_branch({ branchName: \"pr-1\" })` to push a specific local branch. If specifying branchName, use the LOCAL branch name (e.g., 'pr-1'), not the remote branch name. The correct remote and remote branch are determined automatically from branch config set by checkout_pr. Requires a clean working tree. Runs the repository prepush hook (if configured)
|
|
143356
|
+
description: "Push the current branch to the remote repository. Omit branchName to push the current branch (recommended). Example: `push_branch({})` to push the current branch. Example: `push_branch({ branchName: \"pr-1\" })` to push a specific local branch. If specifying branchName, use the LOCAL branch name (e.g., 'pr-1'), not the remote branch name. The correct remote and remote branch are determined automatically from branch config set by checkout_pr. Requires a clean working tree. Runs the repository prepush hook (if configured) \u2014 best-effort. If the hook fails, the tool returns the failure output and every subsequent call this run skips the hook. Never force push unless explicitly requested. Pushes to the default branch are blocked in restricted mode. If the response reports a timeout, the underlying push may have actually succeeded \u2014 verify with `git log origin/<branch>` (or this tool with command 'log') before retrying, otherwise you'll push a duplicate.",
|
|
143274
143357
|
parameters: PushBranch,
|
|
143275
143358
|
execute: execute(async ({ branchName, force }) => {
|
|
143276
143359
|
if (pushPermission === "disabled") {
|
|
@@ -143284,10 +143367,21 @@ function PushBranchTool(ctx) {
|
|
|
143284
143367
|
`push blocked: working tree is not clean (tracked changes and/or untracked files). commit, discard, or remove stray artifacts before pushing.
|
|
143285
143368
|
|
|
143286
143369
|
git status:
|
|
143287
|
-
${status}`
|
|
143370
|
+
${status}` + (ctx.toolState.prepushFailureCount > 0 ? "\n\nnote: the prepush hook failed earlier this run \u2014 once the working tree is clean, push_branch will skip the hook." : "")
|
|
143288
143371
|
);
|
|
143289
143372
|
}
|
|
143290
143373
|
const pushDest = validatePushDestination(ctx, branch);
|
|
143374
|
+
const prBranchMatch = branch.match(/^pr-(\d+)$/);
|
|
143375
|
+
if (prBranchMatch && pushDest.remoteBranch !== branch) {
|
|
143376
|
+
const prNumber = Number(prBranchMatch[1]);
|
|
143377
|
+
const event = ctx.payload.event;
|
|
143378
|
+
const runScoped = event.is_pr === true && event.issue_number === prNumber;
|
|
143379
|
+
if (!runScoped) {
|
|
143380
|
+
throw new Error(
|
|
143381
|
+
`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.`
|
|
143382
|
+
);
|
|
143383
|
+
}
|
|
143384
|
+
}
|
|
143291
143385
|
if (pushPermission === "restricted" && pushDest.remoteBranch === defaultBranch) {
|
|
143292
143386
|
throw new Error(
|
|
143293
143387
|
`Push blocked: cannot push directly to default branch '${pushDest.remoteBranch}'. Create a feature branch and open a PR instead.`
|
|
@@ -143295,21 +143389,27 @@ ${status}`
|
|
|
143295
143389
|
}
|
|
143296
143390
|
const refspec = branch === pushDest.remoteBranch ? branch : `${branch}:${pushDest.remoteBranch}`;
|
|
143297
143391
|
const pushArgs = force ? ["--force", "-u", pushDest.remoteName, refspec] : ["-u", pushDest.remoteName, refspec];
|
|
143298
|
-
const
|
|
143299
|
-
|
|
143300
|
-
|
|
143301
|
-
})
|
|
143302
|
-
|
|
143303
|
-
|
|
143304
|
-
|
|
143305
|
-
|
|
143306
|
-
|
|
143307
|
-
|
|
143308
|
-
|
|
143392
|
+
const prepushSkipped = ctx.toolState.prepushFailureCount > 0;
|
|
143393
|
+
if (prepushSkipped) {
|
|
143394
|
+
log.info(`\xBB skipping prepush hook (failed earlier this run)`);
|
|
143395
|
+
} else if (ctx.prepushScript) {
|
|
143396
|
+
const prepushHook = await executeLifecycleHook({
|
|
143397
|
+
event: "prepush",
|
|
143398
|
+
script: ctx.prepushScript
|
|
143399
|
+
});
|
|
143400
|
+
if (prepushHook.failure) {
|
|
143401
|
+
ctx.toolState.prepushFailureCount += 1;
|
|
143402
|
+
throw new Error(buildPrepushFailureMessage(prepushHook.failure, ctx.payload.shell));
|
|
143403
|
+
}
|
|
143404
|
+
const postHookStatus = $("git", ["status", "--porcelain"], { log: false });
|
|
143405
|
+
if (postHookStatus) {
|
|
143406
|
+
throw new Error(
|
|
143407
|
+
`push blocked: the prepush hook modified the working tree. those changes are not included in the push. commit or discard them (or change the hook to not mutate tracked files) before retrying.
|
|
143309
143408
|
|
|
143310
143409
|
git status:
|
|
143311
143410
|
${postHookStatus}`
|
|
143312
|
-
|
|
143411
|
+
);
|
|
143412
|
+
}
|
|
143313
143413
|
}
|
|
143314
143414
|
log.debug(`pushing ${branch} to ${pushDest.remoteName}/${pushDest.remoteBranch}`);
|
|
143315
143415
|
if (force) {
|
|
@@ -143362,17 +143462,30 @@ ${integrateStep}
|
|
|
143362
143462
|
log.info(
|
|
143363
143463
|
`\xBB pushed branch ${branch} to ${pushDest.remoteName}/${pushDest.remoteBranch} (sha ${pushedSha})`
|
|
143364
143464
|
);
|
|
143465
|
+
const baseMsg = `successfully pushed ${branch} to ${pushDest.remoteName}/${pushDest.remoteBranch}`;
|
|
143466
|
+
const message = prepushSkipped ? `${baseMsg} (prepush hook skipped \u2014 failed earlier this run).` : baseMsg;
|
|
143365
143467
|
return {
|
|
143366
143468
|
success: true,
|
|
143367
143469
|
branch,
|
|
143368
143470
|
remoteBranch: pushDest.remoteBranch,
|
|
143369
143471
|
remote: pushDest.remoteName,
|
|
143370
143472
|
force,
|
|
143371
|
-
|
|
143473
|
+
prepushSkipped,
|
|
143474
|
+
message
|
|
143372
143475
|
};
|
|
143373
143476
|
})
|
|
143374
143477
|
});
|
|
143375
143478
|
}
|
|
143479
|
+
function buildPrepushFailureMessage(failure, shell) {
|
|
143480
|
+
const header = failure.kind === "exit" ? `prepush hook failed with exit code ${failure.exitCode}.
|
|
143481
|
+
|
|
143482
|
+
script output:
|
|
143483
|
+
${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}.`;
|
|
143484
|
+
const ifRealBug = shell === "disabled" ? `fix it before pushing again \u2014 shell access is disabled in this run, so you can't re-run the hook command yourself.` : `run the hook command yourself via the shell tool to iterate (push_branch will NOT re-run it).`;
|
|
143485
|
+
return `${header}
|
|
143486
|
+
|
|
143487
|
+
this repo's prepush hook is best-effort: the next push_branch call will SKIP the hook and proceed. if the failure is unrelated to your changes (pre-existing breakage, flaky check), just call push_branch again. if it could be a real bug in your code, ${ifRealBug}`;
|
|
143488
|
+
}
|
|
143376
143489
|
var AUTH_REQUIRED_REDIRECT = {
|
|
143377
143490
|
push: "use the push_branch tool instead \u2014 it handles authentication and permission checks.",
|
|
143378
143491
|
fetch: "use the git_fetch tool instead \u2014 it handles authentication.",
|
|
@@ -143434,6 +143547,23 @@ function GitTool(ctx) {
|
|
|
143434
143547
|
}
|
|
143435
143548
|
}
|
|
143436
143549
|
}
|
|
143550
|
+
if (command === "merge-base" && args2.includes("--is-ancestor")) {
|
|
143551
|
+
let isAncestor = true;
|
|
143552
|
+
$("git", [command, ...args2], {
|
|
143553
|
+
log: false,
|
|
143554
|
+
onError: (r) => {
|
|
143555
|
+
if (r.status === 1) {
|
|
143556
|
+
isAncestor = false;
|
|
143557
|
+
return;
|
|
143558
|
+
}
|
|
143559
|
+
const detail = [r.stderr, r.stdout].map((s) => s.trim()).filter(Boolean).join("\n");
|
|
143560
|
+
throw new Error(
|
|
143561
|
+
`git merge-base --is-ancestor failed (exit ${r.status}): ${detail || "Unknown error"}`
|
|
143562
|
+
);
|
|
143563
|
+
}
|
|
143564
|
+
});
|
|
143565
|
+
return { success: true, isAncestor };
|
|
143566
|
+
}
|
|
143437
143567
|
const output = $("git", [command, ...args2], { log: false });
|
|
143438
143568
|
const lineCount = output.split("\n").length;
|
|
143439
143569
|
if (lineCount > COLLAPSE_THRESHOLD) {
|
|
@@ -143451,11 +143581,6 @@ var GitFetch = type({
|
|
|
143451
143581
|
ref: type.string.describe("Ref to fetch: branch name, tag, or 'pull/N/head' for PRs"),
|
|
143452
143582
|
depth: type.number.describe("Fetch depth (for shallow clones)").optional()
|
|
143453
143583
|
});
|
|
143454
|
-
var SHALLOW_UNREACHABLE_PATTERNS = [
|
|
143455
|
-
/Could not read [a-f0-9]{40,64}/,
|
|
143456
|
-
/remote did not send all necessary objects/
|
|
143457
|
-
];
|
|
143458
|
-
var DEEPEN_RETRY_DEPTH = 1e3;
|
|
143459
143584
|
function GitFetchTool(ctx) {
|
|
143460
143585
|
return tool({
|
|
143461
143586
|
name: "git_fetch",
|
|
@@ -143467,20 +143592,7 @@ function GitFetchTool(ctx) {
|
|
|
143467
143592
|
if (params.depth !== void 0) {
|
|
143468
143593
|
fetchArgs.push(`--depth=${params.depth}`);
|
|
143469
143594
|
}
|
|
143470
|
-
|
|
143471
|
-
await $git("fetch", fetchArgs, { token: ctx.gitToken });
|
|
143472
|
-
} catch (err) {
|
|
143473
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
143474
|
-
const isShallowUnreachable = SHALLOW_UNREACHABLE_PATTERNS.some((p) => p.test(msg));
|
|
143475
|
-
const isShallow = isShallowUnreachable && $("git", ["rev-parse", "--is-shallow-repository"], { log: false }).trim() === "true";
|
|
143476
|
-
if (!isShallow) throw err;
|
|
143477
|
-
log.info(
|
|
143478
|
-
`\xBB git_fetch hit shallow-unreachable error, retrying with --deepen=${DEEPEN_RETRY_DEPTH}`
|
|
143479
|
-
);
|
|
143480
|
-
await $git("fetch", [`--deepen=${DEEPEN_RETRY_DEPTH}`, "--no-tags", "origin", params.ref], {
|
|
143481
|
-
token: ctx.gitToken
|
|
143482
|
-
});
|
|
143483
|
-
}
|
|
143595
|
+
await $gitFetchWithDeepen(fetchArgs, { token: ctx.gitToken }, "git_fetch");
|
|
143484
143596
|
return { success: true, ref: params.ref };
|
|
143485
143597
|
})
|
|
143486
143598
|
});
|
|
@@ -143691,7 +143803,7 @@ var CreatePullRequestReview = type({
|
|
|
143691
143803
|
"1-2 sentence high-level summary with urgency level, critical callouts, and feedback about code outside the diff. Specific feedback on diff lines goes in 'comments' array."
|
|
143692
143804
|
).optional(),
|
|
143693
143805
|
approved: type.boolean.describe(
|
|
143694
|
-
"Set to true to submit as an approval. Use for
|
|
143806
|
+
"Set to true to submit as an approval. Use for `> \u2705 No new issues found.` reviews where the PR is mergeable as-is and nothing in the body warrants code changes \u2014 approving also suppresses the Fix-button footer affordance so users don't dispatch a fix run on non-actionable feedback. Reserve approved: false for `> \u2139\uFE0F ...` (minor suggestions inline), `> [!IMPORTANT]` (recommended changes), and `> [!CAUTION]` (critical) reviews. Defaults to false (comment-only review). Rejections are not supported."
|
|
143695
143807
|
).optional(),
|
|
143696
143808
|
commit_id: type.string.describe(
|
|
143697
143809
|
"Optional SHA of the commit being reviewed. Defaults to latest. Must be the FULL 40-character SHA \u2014 abbreviated SHAs are rejected by GitHub with `422 Unprocessable Entity`. The PR-synchronize event payload's `head_sha` is already full-length."
|
|
@@ -144036,7 +144148,8 @@ async function createAndSubmitWithFooter(ctx, params, opts) {
|
|
|
144036
144148
|
const footer = buildPullfrogFooter({
|
|
144037
144149
|
workflowRun: ctx.runId ? { owner: ctx.repo.owner, repo: ctx.repo.name, runId: ctx.runId, jobId: ctx.jobId } : void 0,
|
|
144038
144150
|
customParts,
|
|
144039
|
-
model: ctx.toolState.model
|
|
144151
|
+
model: ctx.toolState.model,
|
|
144152
|
+
fallbackFrom: ctx.toolState.modelFallback?.from
|
|
144040
144153
|
});
|
|
144041
144154
|
return await ctx.octokit.rest.pulls.submitReview({
|
|
144042
144155
|
owner: params.owner,
|
|
@@ -144204,10 +144317,10 @@ async function ensureBeforeShaReachable(params) {
|
|
|
144204
144317
|
sha: params.sha,
|
|
144205
144318
|
ref: tempBranch
|
|
144206
144319
|
}), true);
|
|
144207
|
-
await $
|
|
144208
|
-
"fetch",
|
|
144320
|
+
await $gitFetchWithDeepen(
|
|
144209
144321
|
["--no-tags", ...params.isShallow ? ["--depth=1"] : [], "origin", tempBranch],
|
|
144210
|
-
{ token: params.gitToken }
|
|
144322
|
+
{ token: params.gitToken },
|
|
144323
|
+
`before_sha temp branch ${tempBranch}`
|
|
144211
144324
|
);
|
|
144212
144325
|
log.debug(`\xBB fetched before_sha via temp branch ${tempBranch}`);
|
|
144213
144326
|
return true;
|
|
@@ -144283,16 +144396,22 @@ async function checkoutPrBranch(pr, params) {
|
|
|
144283
144396
|
toolState.checkoutSha = $("git", ["rev-parse", "HEAD"], { log: false }).trim();
|
|
144284
144397
|
const alreadyOnBranch = toolState.checkoutSha === pr.headSha;
|
|
144285
144398
|
log.debug(`\xBB fetching base branch (${pr.baseRef})...`);
|
|
144286
|
-
await $
|
|
144399
|
+
await $gitFetchWithDeepen(
|
|
144400
|
+
["--no-tags", "origin", pr.baseRef],
|
|
144401
|
+
{ token: gitToken },
|
|
144402
|
+
`base branch ${pr.baseRef}`
|
|
144403
|
+
);
|
|
144287
144404
|
if (!alreadyOnBranch) {
|
|
144288
144405
|
$("git", ["checkout", "-B", pr.baseRef, `origin/${pr.baseRef}`], { log: false });
|
|
144289
144406
|
log.debug(`\xBB fetching PR #${pr.number} (${localBranch})...`);
|
|
144290
144407
|
await retry(
|
|
144291
144408
|
async () => {
|
|
144292
144409
|
try {
|
|
144293
|
-
await $
|
|
144294
|
-
|
|
144295
|
-
|
|
144410
|
+
await $gitFetchWithDeepen(
|
|
144411
|
+
["--no-tags", "origin", `+pull/${pr.number}/head:${localBranch}`],
|
|
144412
|
+
{ token: gitToken },
|
|
144413
|
+
`PR #${pr.number}`
|
|
144414
|
+
);
|
|
144296
144415
|
} catch (e) {
|
|
144297
144416
|
const msg = e instanceof Error ? e.message : String(e);
|
|
144298
144417
|
if (PULL_REF_MISSING_PATTERN.test(msg)) {
|
|
@@ -144393,134 +144512,159 @@ async function checkoutPrBranch(pr, params) {
|
|
|
144393
144512
|
});
|
|
144394
144513
|
return { hookWarning: postCheckoutHook.warning };
|
|
144395
144514
|
}
|
|
144515
|
+
var inFlightCheckouts = /* @__PURE__ */ new Map();
|
|
144396
144516
|
function CheckoutPrTool(ctx) {
|
|
144517
|
+
const runCheckout = async (pull_number) => {
|
|
144518
|
+
const prResponse = await ctx.octokit.rest.pulls.get({
|
|
144519
|
+
owner: ctx.repo.owner,
|
|
144520
|
+
repo: ctx.repo.name,
|
|
144521
|
+
pull_number
|
|
144522
|
+
});
|
|
144523
|
+
const headRepo = prResponse.data.head.repo;
|
|
144524
|
+
if (!headRepo) {
|
|
144525
|
+
throw new Error(`PR #${pull_number} source repository was deleted`);
|
|
144526
|
+
}
|
|
144527
|
+
const pr = {
|
|
144528
|
+
number: pull_number,
|
|
144529
|
+
headSha: prResponse.data.head.sha,
|
|
144530
|
+
headRef: prResponse.data.head.ref,
|
|
144531
|
+
headRepoFullName: headRepo.full_name,
|
|
144532
|
+
baseRef: prResponse.data.base.ref,
|
|
144533
|
+
baseRepoFullName: prResponse.data.base.repo.full_name,
|
|
144534
|
+
maintainerCanModify: prResponse.data.maintainer_can_modify
|
|
144535
|
+
};
|
|
144536
|
+
const checkoutResult = await checkoutPrBranch(pr, {
|
|
144537
|
+
octokit: ctx.octokit,
|
|
144538
|
+
owner: ctx.repo.owner,
|
|
144539
|
+
name: ctx.repo.name,
|
|
144540
|
+
gitToken: ctx.gitToken,
|
|
144541
|
+
toolState: ctx.toolState,
|
|
144542
|
+
shell: ctx.payload.shell,
|
|
144543
|
+
postCheckoutScript: ctx.postCheckoutScript,
|
|
144544
|
+
beforeSha: ctx.toolState.beforeSha
|
|
144545
|
+
});
|
|
144546
|
+
const tempDir = process.env.PULLFROG_TEMP_DIR;
|
|
144547
|
+
if (!tempDir) {
|
|
144548
|
+
throw new Error(
|
|
144549
|
+
"PULLFROG_TEMP_DIR not set - checkout_pr must run in pullfrog action context"
|
|
144550
|
+
);
|
|
144551
|
+
}
|
|
144552
|
+
const headShort = ctx.toolState.checkoutSha.slice(0, 7);
|
|
144553
|
+
let incrementalDiffPath;
|
|
144554
|
+
if (ctx.toolState.beforeSha && ctx.toolState.checkoutSha) {
|
|
144555
|
+
const beforeShort = ctx.toolState.beforeSha.slice(0, 7);
|
|
144556
|
+
const incremental = computeIncrementalDiff({
|
|
144557
|
+
baseBranch: pr.baseRef,
|
|
144558
|
+
beforeSha: ctx.toolState.beforeSha,
|
|
144559
|
+
headSha: ctx.toolState.checkoutSha
|
|
144560
|
+
});
|
|
144561
|
+
if (incremental) {
|
|
144562
|
+
incrementalDiffPath = join3(
|
|
144563
|
+
tempDir,
|
|
144564
|
+
`pr-${pull_number}-${beforeShort}-${headShort}-incremental.diff`
|
|
144565
|
+
);
|
|
144566
|
+
writeFileSync(incrementalDiffPath, incremental);
|
|
144567
|
+
log.info(
|
|
144568
|
+
`\xBB incremental diff computed (${incremental.length} bytes) \u2192 ${incrementalDiffPath}`
|
|
144569
|
+
);
|
|
144570
|
+
}
|
|
144571
|
+
}
|
|
144572
|
+
const formatResult = await fetchAndFormatPrDiff(ctx, pull_number);
|
|
144573
|
+
const diffPreview = formatResult.content.split("\n").slice(0, 100).join("\n");
|
|
144574
|
+
log.debug(`formatted diff preview (first 100 lines):
|
|
144575
|
+
${diffPreview}`);
|
|
144576
|
+
const diffPath = join3(tempDir, `pr-${pull_number}-${headShort}.diff`);
|
|
144577
|
+
writeFileSync(diffPath, formatResult.content);
|
|
144578
|
+
log.debug(`wrote diff to ${diffPath} (${formatResult.content.length} bytes)`);
|
|
144579
|
+
ctx.toolState.diffCoverage = createDiffCoverageState({
|
|
144580
|
+
diffPath,
|
|
144581
|
+
totalLines: countLines({ content: formatResult.content }),
|
|
144582
|
+
toc: formatResult.toc,
|
|
144583
|
+
previous: ctx.toolState.diffCoverage
|
|
144584
|
+
});
|
|
144585
|
+
log.debug(
|
|
144586
|
+
`\xBB diff coverage initialized: diffPath=${diffPath}, totalLines=${ctx.toolState.diffCoverage.totalLines}, tocEntries=${ctx.toolState.diffCoverage.tocEntries.length}`
|
|
144587
|
+
);
|
|
144588
|
+
const cached4 = /* @__PURE__ */ new Map();
|
|
144589
|
+
for (const file2 of formatResult.files) {
|
|
144590
|
+
cached4.set(file2.filename, commentableLinesForFile(file2.patch));
|
|
144591
|
+
}
|
|
144592
|
+
ctx.toolState.commentableLinesByFile = cached4;
|
|
144593
|
+
ctx.toolState.commentableLinesPullNumber = pull_number;
|
|
144594
|
+
ctx.toolState.commentableLinesCheckoutSha = ctx.toolState.checkoutSha;
|
|
144595
|
+
const incrementalInstructions = incrementalDiffPath ? ` IMPORTANT: incrementalDiffPath contains ONLY the changes since the last reviewed version (computed via range-diff). you MUST read incrementalDiffPath FIRST to understand what changed, then use diffPath for full PR context. do NOT skip the incremental diff.` : "";
|
|
144596
|
+
const COMMIT_LOG_MAX = 200;
|
|
144597
|
+
const baseRange = `origin/${pr.baseRef}..HEAD`;
|
|
144598
|
+
let commitCount = 0;
|
|
144599
|
+
let commitLog = "";
|
|
144600
|
+
let commitLogUnavailable = false;
|
|
144601
|
+
try {
|
|
144602
|
+
commitCount = parseInt(
|
|
144603
|
+
$("git", ["rev-list", "--count", baseRange], { log: false }).trim() || "0",
|
|
144604
|
+
10
|
|
144605
|
+
);
|
|
144606
|
+
commitLog = $("git", ["log", "--oneline", `--max-count=${COMMIT_LOG_MAX}`, baseRange], {
|
|
144607
|
+
log: false
|
|
144608
|
+
});
|
|
144609
|
+
} catch (err) {
|
|
144610
|
+
commitLogUnavailable = true;
|
|
144611
|
+
log.debug(
|
|
144612
|
+
`\xBB unable to compute commit metadata for ${baseRange}: ${err instanceof Error ? err.message : String(err)}`
|
|
144613
|
+
);
|
|
144614
|
+
}
|
|
144615
|
+
const commitLogTruncated = commitCount > COMMIT_LOG_MAX;
|
|
144616
|
+
const hookWarningInstructions = checkoutResult.hookWarning ? ` HOOK WARNING: the post-checkout lifecycle hook reported a non-fatal failure (see hookWarning). decide whether to retry based on the guidance in that field before proceeding.` : "";
|
|
144617
|
+
const commitLogInstructions = commitLogUnavailable ? ` NOTE: commit metadata is partial (base ref unreachable, likely a shallow fetch). commitCount/commitLog may be 0/empty or incomplete; treat them as "unknown" rather than "no commits", and use \`git log\` directly if you need the full history.` : commitLogTruncated ? ` NOTE: commitLog was capped at ${COMMIT_LOG_MAX} entries out of ${commitCount} commits; use \`git log\` directly if you need the full history.` : "";
|
|
144618
|
+
return {
|
|
144619
|
+
success: true,
|
|
144620
|
+
number: prResponse.data.number,
|
|
144621
|
+
title: prResponse.data.title,
|
|
144622
|
+
body: prResponse.data.body,
|
|
144623
|
+
base: pr.baseRef,
|
|
144624
|
+
localBranch: `pr-${pull_number}`,
|
|
144625
|
+
remoteBranch: `refs/heads/${pr.headRef}`,
|
|
144626
|
+
isFork: pr.headRepoFullName !== pr.baseRepoFullName,
|
|
144627
|
+
maintainerCanModify: pr.maintainerCanModify,
|
|
144628
|
+
url: prResponse.data.html_url,
|
|
144629
|
+
headRepo: pr.headRepoFullName,
|
|
144630
|
+
diffPath,
|
|
144631
|
+
incrementalDiffPath,
|
|
144632
|
+
toc: formatResult.toc,
|
|
144633
|
+
commitCount,
|
|
144634
|
+
commitLog,
|
|
144635
|
+
commitLogTruncated,
|
|
144636
|
+
commitLogUnavailable,
|
|
144637
|
+
hookWarning: checkoutResult.hookWarning,
|
|
144638
|
+
instructions: `the diff file at diffPath contains a table of contents (TOC) at the top listing every changed file with its line range. use the TOC line ranges as your checklist and read specific files from the diff instead of reading the entire file. for example, if the TOC says "src/foo.ts \u2192 lines 5-42", read lines 5-42 from diffPath to see that file's changes. review files selectively based on relevance rather than reading everything sequentially. to inspect the PR's changed files, use diffPath \u2014 do NOT run \`git diff <base>..<head>\` to re-derive what's already in diffPath. the formatted diff with line numbers is authoritative. \`git log\` and \`git diff --stat\` are fine for commit-range overview, and \`git diff\` / \`git diff --cached\` are fine for inspecting *your own* uncommitted changes \u2014 but PR review content MUST come from diffPath. before your review is submitted, a one-time coverage pre-flight may error listing unread TOC regions. retry the same create_pull_request_review call to proceed \u2014 optionally after reading the listed ranges. the pre-flight will not block again this session. the local branch is 'localBranch' (pr-{number}), not the remote branch name. when pushing, omit branchName to use the current branch. do not use remoteBranch as a local branch name.` + incrementalInstructions + hookWarningInstructions + commitLogInstructions
|
|
144639
|
+
};
|
|
144640
|
+
};
|
|
144397
144641
|
return tool({
|
|
144398
144642
|
name: "checkout_pr",
|
|
144399
144643
|
description: "Checkout a pull request branch locally. This fetches the PR branch and sets up push configuration for fork PRs. Returns diffPath pointing to the formatted diff file. Example: `checkout_pr({ pull_number: 1234 })`. Transient fetch timeouts are common \u2014 retry the same call up to a few times before treating the failure as terminal. If the error mentions `.git/shallow.lock: File exists` or `.git/index.lock: File exists`, that's a stale lock from a prior timed-out fetch \u2014 remove it via the shell tool (`rm -f .git/shallow.lock .git/index.lock`) and retry.",
|
|
144400
144644
|
parameters: CheckoutPr,
|
|
144401
144645
|
execute: execute(async ({ pull_number }) => {
|
|
144402
|
-
const
|
|
144403
|
-
|
|
144404
|
-
|
|
144405
|
-
|
|
144406
|
-
}
|
|
144407
|
-
const
|
|
144408
|
-
if (
|
|
144409
|
-
|
|
144410
|
-
|
|
144411
|
-
|
|
144412
|
-
|
|
144413
|
-
|
|
144414
|
-
headRef: prResponse.data.head.ref,
|
|
144415
|
-
headRepoFullName: headRepo.full_name,
|
|
144416
|
-
baseRef: prResponse.data.base.ref,
|
|
144417
|
-
baseRepoFullName: prResponse.data.base.repo.full_name,
|
|
144418
|
-
maintainerCanModify: prResponse.data.maintainer_can_modify
|
|
144419
|
-
};
|
|
144420
|
-
const checkoutResult = await checkoutPrBranch(pr, {
|
|
144421
|
-
octokit: ctx.octokit,
|
|
144422
|
-
owner: ctx.repo.owner,
|
|
144423
|
-
name: ctx.repo.name,
|
|
144424
|
-
gitToken: ctx.gitToken,
|
|
144425
|
-
toolState: ctx.toolState,
|
|
144426
|
-
shell: ctx.payload.shell,
|
|
144427
|
-
postCheckoutScript: ctx.postCheckoutScript,
|
|
144428
|
-
beforeSha: ctx.toolState.beforeSha
|
|
144429
|
-
});
|
|
144430
|
-
const tempDir = process.env.PULLFROG_TEMP_DIR;
|
|
144431
|
-
if (!tempDir) {
|
|
144432
|
-
throw new Error(
|
|
144433
|
-
"PULLFROG_TEMP_DIR not set - checkout_pr must run in pullfrog action context"
|
|
144434
|
-
);
|
|
144435
|
-
}
|
|
144436
|
-
const headShort = ctx.toolState.checkoutSha.slice(0, 7);
|
|
144437
|
-
let incrementalDiffPath;
|
|
144438
|
-
if (ctx.toolState.beforeSha && ctx.toolState.checkoutSha) {
|
|
144439
|
-
const beforeShort = ctx.toolState.beforeSha.slice(0, 7);
|
|
144440
|
-
const incremental = computeIncrementalDiff({
|
|
144441
|
-
baseBranch: pr.baseRef,
|
|
144442
|
-
beforeSha: ctx.toolState.beforeSha,
|
|
144443
|
-
headSha: ctx.toolState.checkoutSha
|
|
144444
|
-
});
|
|
144445
|
-
if (incremental) {
|
|
144446
|
-
incrementalDiffPath = join3(
|
|
144447
|
-
tempDir,
|
|
144448
|
-
`pr-${pull_number}-${beforeShort}-${headShort}-incremental.diff`
|
|
144449
|
-
);
|
|
144450
|
-
writeFileSync(incrementalDiffPath, incremental);
|
|
144451
|
-
log.info(
|
|
144452
|
-
`\xBB incremental diff computed (${incremental.length} bytes) \u2192 ${incrementalDiffPath}`
|
|
144646
|
+
const inFlight = inFlightCheckouts.get(pull_number);
|
|
144647
|
+
if (inFlight) {
|
|
144648
|
+
log.info(`\xBB checkout_pr({pull_number:${pull_number}}) already in flight \u2014 sharing result`);
|
|
144649
|
+
return inFlight;
|
|
144650
|
+
}
|
|
144651
|
+
const currentBranch = $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false }).trim();
|
|
144652
|
+
if (currentBranch !== `pr-${pull_number}`) {
|
|
144653
|
+
const dirty = $("git", ["status", "--porcelain"], { log: false }).trim();
|
|
144654
|
+
if (dirty) {
|
|
144655
|
+
throw new Error(
|
|
144656
|
+
`cannot checkout PR #${pull_number} while the working tree has uncommitted changes. commit, push, or discard them before switching. dirty paths:
|
|
144657
|
+
${dirty}`
|
|
144453
144658
|
);
|
|
144454
144659
|
}
|
|
144455
144660
|
}
|
|
144456
|
-
const
|
|
144457
|
-
|
|
144458
|
-
log.debug(`formatted diff preview (first 100 lines):
|
|
144459
|
-
${diffPreview}`);
|
|
144460
|
-
const diffPath = join3(tempDir, `pr-${pull_number}-${headShort}.diff`);
|
|
144461
|
-
writeFileSync(diffPath, formatResult.content);
|
|
144462
|
-
log.debug(`wrote diff to ${diffPath} (${formatResult.content.length} bytes)`);
|
|
144463
|
-
ctx.toolState.diffCoverage = createDiffCoverageState({
|
|
144464
|
-
diffPath,
|
|
144465
|
-
totalLines: countLines({ content: formatResult.content }),
|
|
144466
|
-
toc: formatResult.toc,
|
|
144467
|
-
previous: ctx.toolState.diffCoverage
|
|
144468
|
-
});
|
|
144469
|
-
log.debug(
|
|
144470
|
-
`\xBB diff coverage initialized: diffPath=${diffPath}, totalLines=${ctx.toolState.diffCoverage.totalLines}, tocEntries=${ctx.toolState.diffCoverage.tocEntries.length}`
|
|
144471
|
-
);
|
|
144472
|
-
const cached4 = /* @__PURE__ */ new Map();
|
|
144473
|
-
for (const file2 of formatResult.files) {
|
|
144474
|
-
cached4.set(file2.filename, commentableLinesForFile(file2.patch));
|
|
144475
|
-
}
|
|
144476
|
-
ctx.toolState.commentableLinesByFile = cached4;
|
|
144477
|
-
ctx.toolState.commentableLinesPullNumber = pull_number;
|
|
144478
|
-
ctx.toolState.commentableLinesCheckoutSha = ctx.toolState.checkoutSha;
|
|
144479
|
-
const incrementalInstructions = incrementalDiffPath ? ` IMPORTANT: incrementalDiffPath contains ONLY the changes since the last reviewed version (computed via range-diff). you MUST read incrementalDiffPath FIRST to understand what changed, then use diffPath for full PR context. do NOT skip the incremental diff.` : "";
|
|
144480
|
-
const COMMIT_LOG_MAX = 200;
|
|
144481
|
-
const baseRange = `origin/${pr.baseRef}..HEAD`;
|
|
144482
|
-
let commitCount = 0;
|
|
144483
|
-
let commitLog = "";
|
|
144484
|
-
let commitLogUnavailable = false;
|
|
144661
|
+
const promise2 = runCheckout(pull_number);
|
|
144662
|
+
inFlightCheckouts.set(pull_number, promise2);
|
|
144485
144663
|
try {
|
|
144486
|
-
|
|
144487
|
-
|
|
144488
|
-
|
|
144489
|
-
);
|
|
144490
|
-
commitLog = $("git", ["log", "--oneline", `--max-count=${COMMIT_LOG_MAX}`, baseRange], {
|
|
144491
|
-
log: false
|
|
144492
|
-
});
|
|
144493
|
-
} catch (err) {
|
|
144494
|
-
commitLogUnavailable = true;
|
|
144495
|
-
log.debug(
|
|
144496
|
-
`\xBB unable to compute commit metadata for ${baseRange}: ${err instanceof Error ? err.message : String(err)}`
|
|
144497
|
-
);
|
|
144664
|
+
return await promise2;
|
|
144665
|
+
} finally {
|
|
144666
|
+
inFlightCheckouts.delete(pull_number);
|
|
144498
144667
|
}
|
|
144499
|
-
const commitLogTruncated = commitCount > COMMIT_LOG_MAX;
|
|
144500
|
-
const hookWarningInstructions = checkoutResult.hookWarning ? ` HOOK WARNING: the post-checkout lifecycle hook reported a non-fatal failure (see hookWarning). decide whether to retry based on the guidance in that field before proceeding.` : "";
|
|
144501
|
-
const commitLogInstructions = commitLogUnavailable ? ` NOTE: commit metadata is partial (base ref unreachable, likely a shallow fetch). commitCount/commitLog may be 0/empty or incomplete; treat them as "unknown" rather than "no commits", and use \`git log\` directly if you need the full history.` : commitLogTruncated ? ` NOTE: commitLog was capped at ${COMMIT_LOG_MAX} entries out of ${commitCount} commits; use \`git log\` directly if you need the full history.` : "";
|
|
144502
|
-
return {
|
|
144503
|
-
success: true,
|
|
144504
|
-
number: prResponse.data.number,
|
|
144505
|
-
title: prResponse.data.title,
|
|
144506
|
-
body: prResponse.data.body,
|
|
144507
|
-
base: pr.baseRef,
|
|
144508
|
-
localBranch: `pr-${pull_number}`,
|
|
144509
|
-
remoteBranch: `refs/heads/${pr.headRef}`,
|
|
144510
|
-
isFork: pr.headRepoFullName !== pr.baseRepoFullName,
|
|
144511
|
-
maintainerCanModify: pr.maintainerCanModify,
|
|
144512
|
-
url: prResponse.data.html_url,
|
|
144513
|
-
headRepo: pr.headRepoFullName,
|
|
144514
|
-
diffPath,
|
|
144515
|
-
incrementalDiffPath,
|
|
144516
|
-
toc: formatResult.toc,
|
|
144517
|
-
commitCount,
|
|
144518
|
-
commitLog,
|
|
144519
|
-
commitLogTruncated,
|
|
144520
|
-
commitLogUnavailable,
|
|
144521
|
-
hookWarning: checkoutResult.hookWarning,
|
|
144522
|
-
instructions: `the diff file at diffPath contains a table of contents (TOC) at the top listing every changed file with its line range. use the TOC line ranges as your checklist and read specific files from the diff instead of reading the entire file. for example, if the TOC says "src/foo.ts \u2192 lines 5-42", read lines 5-42 from diffPath to see that file's changes. review files selectively based on relevance rather than reading everything sequentially. to inspect the PR's changed files, use diffPath \u2014 do NOT run \`git diff <base>..<head>\` to re-derive what's already in diffPath. the formatted diff with line numbers is authoritative. \`git log\` and \`git diff --stat\` are fine for commit-range overview, and \`git diff\` / \`git diff --cached\` are fine for inspecting *your own* uncommitted changes \u2014 but PR review content MUST come from diffPath. before your review is submitted, a one-time coverage pre-flight may error listing unread TOC regions. retry the same create_pull_request_review call to proceed \u2014 optionally after reading the listed ranges. the pre-flight will not block again this session. the local branch is 'localBranch' (pr-{number}), not the remote branch name. when pushing, omit branchName to use the current branch. do not use remoteBranch as a local branch name.` + incrementalInstructions + hookWarningInstructions + commitLogInstructions
|
|
144523
|
-
};
|
|
144524
144668
|
})
|
|
144525
144669
|
});
|
|
144526
144670
|
}
|
|
@@ -144838,9 +144982,8 @@ function GetIssueEventsTool(ctx) {
|
|
|
144838
144982
|
});
|
|
144839
144983
|
const relevantEventTypes = /* @__PURE__ */ new Set(["cross_referenced", "referenced"]);
|
|
144840
144984
|
const parsedEvents = events.flatMap((event) => {
|
|
144841
|
-
if (!("event" in event) ||
|
|
144842
|
-
|
|
144843
|
-
}
|
|
144985
|
+
if (!("event" in event) || typeof event.event !== "string") return [];
|
|
144986
|
+
if (!relevantEventTypes.has(event.event)) return [];
|
|
144844
144987
|
const baseEvent = {
|
|
144845
144988
|
event: event.event
|
|
144846
144989
|
};
|
|
@@ -145040,7 +145183,8 @@ function buildPrBodyWithFooter(ctx, body) {
|
|
|
145040
145183
|
const footer = buildPullfrogFooter({
|
|
145041
145184
|
triggeredBy: true,
|
|
145042
145185
|
workflowRun: ctx.runId ? { owner: ctx.repo.owner, repo: ctx.repo.name, runId: ctx.runId, jobId: ctx.jobId } : void 0,
|
|
145043
|
-
model: ctx.toolState.model
|
|
145186
|
+
model: ctx.toolState.model,
|
|
145187
|
+
fallbackFrom: ctx.toolState.modelFallback?.from
|
|
145044
145188
|
});
|
|
145045
145189
|
const bodyWithoutFooter = stripExistingFooter(fixDoubleEscapedString(body));
|
|
145046
145190
|
return `${bodyWithoutFooter}${footer}`;
|
|
@@ -145611,7 +145755,9 @@ function ListPullRequestReviewsTool(ctx) {
|
|
|
145611
145755
|
body: review.body,
|
|
145612
145756
|
state: review.state,
|
|
145613
145757
|
user: review.user?.login,
|
|
145614
|
-
submitted_at: review.submitted_at
|
|
145758
|
+
submitted_at: review.submitted_at,
|
|
145759
|
+
commit_id: review.commit_id,
|
|
145760
|
+
html_url: review.html_url
|
|
145615
145761
|
})),
|
|
145616
145762
|
count: reviews.length
|
|
145617
145763
|
};
|
|
@@ -145851,6 +145997,14 @@ function detectSandboxMethod() {
|
|
|
145851
145997
|
return "none";
|
|
145852
145998
|
}
|
|
145853
145999
|
var PROC_CLEANUP = "umount /proc 2>/dev/null; umount /proc 2>/dev/null; mount -t proc proc /proc 2>/dev/null;";
|
|
146000
|
+
var SOCKET_CLEANUP = [
|
|
146001
|
+
"/var/run/docker.sock",
|
|
146002
|
+
"/run/docker.sock",
|
|
146003
|
+
"/var/run/podman/podman.sock",
|
|
146004
|
+
"/run/podman/podman.sock",
|
|
146005
|
+
"/run/containerd/containerd.sock",
|
|
146006
|
+
"/var/run/crio/crio.sock"
|
|
146007
|
+
].map((path3) => `mount --bind /dev/null ${path3} 2>/dev/null;`).join(" ");
|
|
145854
146008
|
function spawnShell(params) {
|
|
145855
146009
|
const spawnOpts = { env: params.env, cwd: params.cwd, stdio: params.stdio, detached: true };
|
|
145856
146010
|
const sandboxMethod = detectSandboxMethod();
|
|
@@ -145863,7 +146017,14 @@ function spawnShell(params) {
|
|
|
145863
146017
|
if (sandboxMethod === "unshare") {
|
|
145864
146018
|
return spawn2(
|
|
145865
146019
|
"unshare",
|
|
145866
|
-
[
|
|
146020
|
+
[
|
|
146021
|
+
"--pid",
|
|
146022
|
+
"--fork",
|
|
146023
|
+
"--mount-proc",
|
|
146024
|
+
"bash",
|
|
146025
|
+
"-c",
|
|
146026
|
+
`${PROC_CLEANUP} ${SOCKET_CLEANUP} ${params.command}`
|
|
146027
|
+
],
|
|
145867
146028
|
spawnOpts
|
|
145868
146029
|
);
|
|
145869
146030
|
}
|
|
@@ -145889,7 +146050,7 @@ function spawnShell(params) {
|
|
|
145889
146050
|
"--mount-proc",
|
|
145890
146051
|
"bash",
|
|
145891
146052
|
"-c",
|
|
145892
|
-
`${PROC_CLEANUP} exec su -p -s /bin/bash ${username} -c '${escaped}'`
|
|
146053
|
+
`${PROC_CLEANUP} ${SOCKET_CLEANUP} exec su -p -s /bin/bash ${username} -c '${escaped}'`
|
|
145893
146054
|
],
|
|
145894
146055
|
{ ...spawnOpts, env: {} }
|
|
145895
146056
|
);
|
|
@@ -145916,6 +146077,15 @@ function getTempDir() {
|
|
|
145916
146077
|
}
|
|
145917
146078
|
return tempDir;
|
|
145918
146079
|
}
|
|
146080
|
+
var MAX_OUTPUT_CHARS = 5e3;
|
|
146081
|
+
function capOutput(output) {
|
|
146082
|
+
if (output.length <= MAX_OUTPUT_CHARS) return output;
|
|
146083
|
+
const fullPath = join7(getTempDir(), `shell-${randomUUID2().slice(0, 8)}.log`);
|
|
146084
|
+
writeFileSync5(fullPath, output);
|
|
146085
|
+
const elided = output.length - MAX_OUTPUT_CHARS;
|
|
146086
|
+
return `... [${elided} chars truncated; full output saved to ${fullPath}] ...
|
|
146087
|
+
${output.slice(-MAX_OUTPUT_CHARS)}`;
|
|
146088
|
+
}
|
|
145919
146089
|
function isGitCommand(command) {
|
|
145920
146090
|
const trimmed = command.trim();
|
|
145921
146091
|
if (trimmed === "git" || trimmed.startsWith("git ")) return true;
|
|
@@ -145934,6 +146104,8 @@ Use this tool to:
|
|
|
145934
146104
|
- Execute build tools (npm, pnpm, cargo, make, etc.)
|
|
145935
146105
|
- Run tests and linters
|
|
145936
146106
|
|
|
146107
|
+
Output is capped at ${MAX_OUTPUT_CHARS} chars: if exceeded, only the tail is returned and the full body is saved to a tempfile (path included in the response). Re-read the tempfile with cat/tail/grep when you need more.
|
|
146108
|
+
|
|
145937
146109
|
Do NOT use this tool for git commands \u2014 use the dedicated git tools instead.`,
|
|
145938
146110
|
parameters: ShellParams,
|
|
145939
146111
|
execute: execute(async (params) => {
|
|
@@ -146024,12 +146196,13 @@ ${stderr}` : stderr : stdout;
|
|
|
146024
146196
|
output = output ? `${output}
|
|
146025
146197
|
[timed out after ${timeout}ms]` : `[timed out after ${timeout}ms]`;
|
|
146026
146198
|
const finalExitCode = exitCode ?? (timedOut ? 124 : -1);
|
|
146199
|
+
const trimmed = output.trim();
|
|
146027
146200
|
if (finalExitCode !== 0) {
|
|
146028
146201
|
log.info(`shell command failed with exit code ${finalExitCode}: ${params.command}`);
|
|
146029
|
-
if (
|
|
146202
|
+
if (trimmed) log.info(`output: ${trimmed}`);
|
|
146030
146203
|
}
|
|
146031
146204
|
return {
|
|
146032
|
-
output:
|
|
146205
|
+
output: capOutput(trimmed),
|
|
146033
146206
|
exit_code: finalExitCode,
|
|
146034
146207
|
timed_out: timedOut
|
|
146035
146208
|
};
|
|
@@ -146312,52 +146485,145 @@ Report findings clearly with file:line references and quoted evidence where poss
|
|
|
146312
146485
|
// modes.ts
|
|
146313
146486
|
var PR_SUMMARY_FORMAT = `### Default format
|
|
146314
146487
|
|
|
146315
|
-
|
|
146488
|
+
The body has at most three parts in this exact order:
|
|
146316
146489
|
|
|
146317
|
-
|
|
146318
|
-
|
|
146490
|
+
1. **Reviewed changes preamble** \u2014 one bolded inline lead-in describing what was reviewed in this run, a bullet list of the substantive changes, and an HTML comment carrying review metadata for downstream agents.
|
|
146491
|
+
2. **Cross-cutting issue sections** (zero or more) \u2014 one \`### \` heading per concern, with a human-readable problem write-up and a collapsed \`<details>Technical details</details>\` block underneath.
|
|
146492
|
+
3. **\`### \u2139\uFE0F Nitpicks\`** at the very bottom (only if there are nits worth surfacing in the body) \u2014 a flat bullet list, no technical-details block.
|
|
146319
146493
|
|
|
146320
|
-
|
|
146494
|
+
Inline-vs-body split: concerns that anchor to a specific line go inline (use the \`comments\` parameter). Body \`### \` sections are reserved for concerns that **have no line to anchor to** \u2014 typically because the concern is about *absence* (something the diff should have done but didn't), *sequencing* (rollout / deletion / migration order), *design decisions only the human can make*, or *scope questions the diff implicitly raises but doesn't address*. A concern that anchors to a line but has broad implications still goes inline (use the technical-details block there to capture the implications \u2014 see Inline technical details below). If you found no non-anchorable concerns, the body has zero \`### \` issue sections \u2014 just the preamble + metadata.
|
|
146321
146495
|
|
|
146322
|
-
|
|
146496
|
+
## 1. Reviewed changes preamble
|
|
146323
146497
|
|
|
146324
|
-
|
|
146325
|
-
NOTE: the metadata line goes AFTER the bullet list, not before it.
|
|
146498
|
+
Open with a single bolded inline lead-in followed immediately by the bullet list (no \`### Key changes\` heading, no \`<b>TL;DR</b>\`):
|
|
146326
146499
|
|
|
146327
|
-
|
|
146500
|
+
\`\`\`
|
|
146501
|
+
**Reviewed changes** \u2014 one sentence on what was reviewed in this run. For Review (initial), this is what the PR does and why. For IncrementalReview, this is what changed since the prior pullfrog review. Focus on intent, not mechanics.
|
|
146502
|
+
|
|
146503
|
+
- **Short human-readable title** \u2014 1 sentence per substantive change. Write a short prose phrase; when you name a file, type, or function, put that name in backticks (e.g. **Add \\\`TodoTracker\\\` for live checklists**). A reviewer should understand the full reviewed scope from this list alone \u2014 this IS the dispassionate "what was reviewed and what changed" overview, so cover the substantive changes, not just the loudest ones.
|
|
146504
|
+
|
|
146505
|
+
<!--
|
|
146506
|
+
Pullfrog review metadata \u2014 for any agent (or human-with-agent) reading this
|
|
146507
|
+
review. Incorporate the fields below into your understanding of the context
|
|
146508
|
+
this review was made in. The findings below were written against
|
|
146509
|
+
{head_sha_short}; if new commits have landed on {head_ref} since this review
|
|
146510
|
+
was submitted, treat any specific bug, file, or line callout as POTENTIALLY
|
|
146511
|
+
STALE \u2014 re-diff against {head_sha_short} (or trigger a fresh review) and
|
|
146512
|
+
factor commits past {head_sha_short} into your understanding of the current
|
|
146513
|
+
state before acting on findings.
|
|
146514
|
+
|
|
146515
|
+
- Mode: Review (initial) or IncrementalReview (delta against prior pullfrog review)
|
|
146516
|
+
- Files reviewed: {file_count}
|
|
146517
|
+
- Commits reviewed: {commit_count}
|
|
146518
|
+
- Base: {base_ref} ({base_sha_short})
|
|
146519
|
+
- Head: {head_ref} ({head_sha_short})
|
|
146520
|
+
- Reviewed commits:
|
|
146521
|
+
- {sha_short} \u2014 {commit_subject}
|
|
146522
|
+
- ...
|
|
146523
|
+
- Prior pullfrog review: none or {prior_sha_short} ({prior_review_html_url})
|
|
146524
|
+
- Submitted at: {iso_timestamp}
|
|
146525
|
+
-->
|
|
146526
|
+
\`\`\`
|
|
146328
146527
|
|
|
146329
|
-
|
|
146528
|
+
Pull every metadata field from the \`checkout_pr\` tool's response \u2014 file count, commit count, base/head ref + SHA, the commit list. For \`IncrementalReview\` runs, populate \`Prior pullfrog review\` with the prior review's commit_id (short SHA) and \`html_url\` from \`list_pull_request_reviews\`.
|
|
146330
146529
|
|
|
146331
|
-
##
|
|
146530
|
+
## 2. Cross-cutting issue sections (zero or more)
|
|
146332
146531
|
|
|
146333
|
-
|
|
146334
|
-
IMPORTANT: Before and After MUST be on a SINGLE blockquote line with an inline <br/> between them. Two separate \`>\` lines creates a double line break.
|
|
146532
|
+
For each cross-cutting concern, one \`### \` section. Use this exact shape:
|
|
146335
146533
|
|
|
146336
|
-
|
|
146534
|
+
\`\`\`
|
|
146535
|
+
### {emoji} {short, descriptive title \u2014 what's wrong, not what to do}
|
|
146337
146536
|
|
|
146338
|
-
|
|
146339
|
-
> <details><summary>How does X work?</summary>
|
|
146340
|
-
> Extended explanation here.
|
|
146341
|
-
> </details>
|
|
146537
|
+
{Human-readable problem write-up. Describes the PROBLEM only \u2014 what's broken, what the symptom is, what the blast radius is. NO asks, NO suggested fixes, NO "the right thing to do is...". Asks and fixes live in the technical-details block below; the visible part is for the human to *understand* the problem, not to implement it.}
|
|
146342
146538
|
|
|
146343
|
-
|
|
146344
|
-
[\`file.ts\`](https://github.com/{owner}/{repo}/pull/{number}/files#diff-{sha256hex_of_filepath}) \xB7 ...
|
|
146539
|
+
<details><summary>Technical details</summary>
|
|
146345
146540
|
|
|
146346
|
-
|
|
146541
|
+
\\\`\\\`\\\`\\\`markdown
|
|
146542
|
+
# {title repeated}
|
|
146347
146543
|
|
|
146348
|
-
|
|
146349
|
-
|
|
146544
|
+
## Affected sites
|
|
146545
|
+
- {file path:line} \u2014 {what's wrong there}
|
|
146546
|
+
- ...
|
|
146350
146547
|
|
|
146351
|
-
|
|
146352
|
-
-
|
|
146353
|
-
-
|
|
146354
|
-
|
|
146355
|
-
|
|
146356
|
-
-
|
|
146357
|
-
|
|
146358
|
-
|
|
146359
|
-
-
|
|
146360
|
-
|
|
146548
|
+
## Required outcome
|
|
146549
|
+
- {what the fix needs to achieve, not how to achieve it}
|
|
146550
|
+
- ...
|
|
146551
|
+
|
|
146552
|
+
## Suggested approach (optional)
|
|
146553
|
+
{When the fix shape is non-obvious, sketch one or more reasonable directions. Skip when the outcome alone makes the fix obvious.}
|
|
146554
|
+
|
|
146555
|
+
## Open questions for the human (optional)
|
|
146556
|
+
- {Any decision an implementing agent shouldn't make unilaterally \u2014 pricing thresholds, breaking-change policy, naming, scope of follow-up.}
|
|
146557
|
+
\\\`\\\`\\\`\\\`
|
|
146558
|
+
|
|
146559
|
+
</details>
|
|
146560
|
+
\`\`\`
|
|
146561
|
+
|
|
146562
|
+
Concrete example of the visible part of a non-anchored section (technical-details block unchanged from the template above):
|
|
146563
|
+
|
|
146564
|
+
\`\`\`
|
|
146565
|
+
### \u2139\uFE0F Legacy \`opencode.ts\` has no documented deletion plan
|
|
146566
|
+
|
|
146567
|
+
The v2 harness lands alongside the v1 file and imports one helper from it. Worth a follow-up issue or a TODO so the next maintainer doesn't have to re-derive the cleanup plan.
|
|
146568
|
+
\`\`\`
|
|
146569
|
+
|
|
146570
|
+
The example's value is its *shape*: a finding about absence (no deletion plan), not a line-anchored bug. Body sections live or die on whether the concern genuinely doesn't fit on a line.
|
|
146571
|
+
|
|
146572
|
+
**Heading severity emoji** \u2014 every \`### \` heading carries one:
|
|
146573
|
+
|
|
146574
|
+
- \u{1F6A8} critical \u2014 blocks merge (data loss, security, broken core flow)
|
|
146575
|
+
- \u26A0\uFE0F important \u2014 must address before merging (regression, missing validation, incorrect behavior)
|
|
146576
|
+
- \u2139\uFE0F informational \u2014 surfaced for awareness; mergeable as-is
|
|
146577
|
+
|
|
146578
|
+
**Visible problem write-up rules:**
|
|
146579
|
+
|
|
146580
|
+
- **No asks, no suggested fixes** in the visible part. The visible portion describes the problem; the technical-details block describes the fix shape and any open questions. The exception: a fix so self-evident that NOT stating it would be weird (e.g. "the typo is missing an 'r'") \u2014 in that case, fold it into the problem statement and skip the suggested-approach block in technical details too.
|
|
146581
|
+
- **Never two successive plain paragraphs.** Every transition between block-level elements must alternate prose with structure: paragraph \u2192 bullet list \u2192 paragraph; paragraph \u2192 code fence \u2192 bullet list; paragraph \u2192 table \u2192 paragraph. Two consecutive paragraphs in a row create a wall of text that's impossible to digest. If you catch yourself writing one, find a way to split it: pull a list out of it, drop a 2-3 line code fence between them, or merge them into a single tighter paragraph.
|
|
146582
|
+
- **Per-paragraph budget:** ~3 sentences max. Past that, you're explaining where you should be structuring.
|
|
146583
|
+
- **Identifier discipline still applies** in the visible part. Lead with behavior in plain English; name an identifier only when it's the subject of the concern or a public surface a reader would recognize. The technical-details block is where dense identifier references belong.
|
|
146584
|
+
|
|
146585
|
+
**Technical-details block rules:**
|
|
146586
|
+
|
|
146587
|
+
- Wrapped in a 4-backtick markdown fence (\`\\\`\\\`\\\`\\\`markdown ... \\\`\\\`\\\`\\\`\`) so it's visually distinct, one-click copyable, and can contain its own 3-backtick code fences without escape gymnastics. The contents are agent-readable \u2014 a fix-agent will pull the body down and use this block as the brief.
|
|
146588
|
+
- File paths and \`file:line\` refs are encouraged (and necessary) \u2014 the next agent uses these to navigate. Identifier density is fine here.
|
|
146589
|
+
- Slightly more verbose than the absolute minimum is OK when it materially helps the next agent: a small code snippet showing the symptom, a short table of mismatched key/column pairs, a one-paragraph "why CI doesn't catch it" note. Skip massive regression-test scaffolding or full route rewrites \u2014 the implementing agent writes those.
|
|
146590
|
+
- Use the four standard sections (\`Affected sites\`, \`Required outcome\`, optional \`Suggested approach\`, optional \`Open questions for the human\`). Skip the optional sections when they wouldn't add anything.
|
|
146591
|
+
|
|
146592
|
+
## Inline technical details
|
|
146593
|
+
|
|
146594
|
+
Inline comments are short (~2-3 sentences) by default. When an inline finding has broader implications worth recording for a fix-agent \u2014 e.g. a localized bug whose proper fix requires touching several files, or where the right fix depends on a design decision the human needs to make \u2014 append a collapsed \`<details><summary>Technical details</summary>\` block to the inline comment's body. Same shape as the body-section technical-details block (4-backtick fenced markdown, \`## Affected sites\` / \`## Required outcome\` / optional \`## Suggested approach\` / optional \`## Open questions for the human\`).
|
|
146595
|
+
|
|
146596
|
+
GitHub renders the same markdown parser in inline comments as in the review body, so the collapsed-details affordance works the same way. The visible part of the inline comment stays scannable; the depth is one click away for any agent that needs it.
|
|
146597
|
+
|
|
146598
|
+
## 3. \`### \u2139\uFE0F Nitpicks\` (optional, last section)
|
|
146599
|
+
|
|
146600
|
+
Only when there are nits that for some reason can't be inlined. Filepaths in nit text are fine \u2014 these are simple enough that a human or agent reads once and acts. No technical-details block.
|
|
146601
|
+
|
|
146602
|
+
\`\`\`
|
|
146603
|
+
### \u2139\uFE0F Nitpicks
|
|
146604
|
+
|
|
146605
|
+
- {nit, with file path inline if useful, \u2264 ~200 chars}
|
|
146606
|
+
- ...
|
|
146607
|
+
\`\`\`
|
|
146608
|
+
|
|
146609
|
+
## Inline comment shape
|
|
146610
|
+
|
|
146611
|
+
Inline comments use the same severity framing as body \`### \` sections, scaled down for line-anchored use:
|
|
146612
|
+
|
|
146613
|
+
- **Lead with a 1-2 sentence problem statement.** The reader is looking at the line in question, so don't restate what the line says \u2014 describe what's wrong with it. Optionally prefix the visible line with a severity emoji (\u{1F6A8} / \u26A0\uFE0F / \u2139\uFE0F) when severity isn't obvious from context.
|
|
146614
|
+
- **Optional \`<details><summary>Technical details</summary>...</details>\` collapsible** for findings whose technical context (longer file:line references, related-code snippets, suggested approach, regression-risk notes) would overwhelm the human-readable lead-in. Same agent-readable purpose, same 4-backtick fence shape, and same 4-section structure as the body's technical-details block \u2014 see *Inline technical details* above. Encouraged whenever the depth helps a downstream fix-agent; don't force one when the inline lead-in already says everything.
|
|
146615
|
+
- **Visible portion \u2264 2-3 sentences.** If you find yourself writing more, that's the cue to split the depth into the \`Technical details\` collapsible.
|
|
146616
|
+
|
|
146617
|
+
## Body-wide rules
|
|
146618
|
+
|
|
146619
|
+
- **Inline-vs-body discipline (repeated for emphasis):** anything that anchors to a specific line goes inline (with a \`<details>Technical details</details>\` block when the implications are broad). The body is for non-anchorable concerns only \u2014 absence, sequencing, design decisions, scope questions, architectural risk.
|
|
146620
|
+
- **No \`### Issues found\` heading** above the issue sections \u2014 each \`### \` heading IS the issue.
|
|
146621
|
+
- **Severity emoji on every \`### \` heading** (\u{1F6A8} / \u26A0\uFE0F / \u2139\uFE0F). No emoji on the preamble lead-in or anywhere else.
|
|
146622
|
+
- **GitHub block-level rendering**: GitHub's markdown parser requires a blank line between ALL block-level elements (HTML tags like \`<br/>\`, \`<sub>\`, \`<details>\`, \`<b>\` and markdown syntax like headings, lists, blockquotes, code fences, paragraphs). Without a blank line, GitHub treats following content as a continuation of the HTML block and renders markdown syntax as literal text. ALWAYS separate block-level elements with a blank line.
|
|
146623
|
+
- **Backtick-wrap** every variable, identifier, or file name when you mention one (in either visible or technical-details portions).
|
|
146624
|
+
- **Don't repeat diff content**, don't include raw \`+123 / -45\` stats, don't include a changelog section, don't use horizontal rules (\`---\`).
|
|
146625
|
+
- **Pull file/commit counts from \`checkout_pr\` metadata** \u2014 never count manually.
|
|
146626
|
+
- **Legacy headings REMOVED.** Do not use \`### Key changes\`, \`### Issues found\`, \`<b>TL;DR</b>\`, or \`<sub><b>Summary</b>\`. The new structure subsumes them.`;
|
|
146361
146627
|
function computeModes(agentId) {
|
|
146362
146628
|
const t = (toolName) => formatMcpToolRef(agentId, toolName);
|
|
146363
146629
|
return [
|
|
@@ -146399,7 +146665,7 @@ function computeModes(agentId) {
|
|
|
146399
146665
|
|
|
146400
146666
|
Otherwise delegate the \`${REVIEWER_AGENT_NAME}\` subagent to review your diff with fresh eyes against YOUR TASK. The subagent's baked-in system prompt enforces a non-mutative + non-recursive contract: read-only file/search/web tools and read-only MCP queries only; no writes, shell side effects, state-changing MCP calls, or nested subagent dispatch. Enforcement is prose-only \u2014 restate the constraint in your dispatch instructions and do not relax it.
|
|
146401
146667
|
|
|
146402
|
-
Provide the subagent with YOUR TASK, the output of \`git diff
|
|
146668
|
+
Provide the subagent with YOUR TASK, the output of \`git diff origin/<base-branch>\` (single-rev form, no \`HEAD\` \u2014 this compares the working tree against the remote base and captures committed + staged + unstaged work; \`main...HEAD\` and \`--cached\` both miss the uncommitted edits Build self-review runs on, since self-review happens BEFORE the commit), and a tight summary (not raw output) of any lint/typecheck/test failures you fixed during build \u2014 what broke, root cause, the fix \u2014 so it can check that fixes addressed root causes rather than suppressed symptoms; say "no build-phase failures" if the build path was clean. Instruct it to flag bugs, logic errors, missing edge cases, gaps between request and diff, and unintended changes.
|
|
146403
146669
|
|
|
146404
146670
|
Delegation + research discipline (distilled from \`/anneal\` canonical \u2014 these are codified learnings from many review rounds, not theoretical best practices):
|
|
146405
146671
|
- Do NOT summarize what you implemented \u2014 that biases the subagent toward validating the shape of your solution rather than questioning it.
|
|
@@ -146408,7 +146674,7 @@ function computeModes(agentId) {
|
|
|
146408
146674
|
- 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.
|
|
146409
146675
|
- 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.
|
|
146410
146676
|
|
|
146411
|
-
|
|
146677
|
+
Be **discerning** about what comes back. The reviewer is an AI subagent and is fallible \u2014 treat every finding as a hypothesis, not a directive, and **verify each one yourself** against the diff and the code before deciding whether to apply. You are searching for a solution that is **complete, minimal, and elegant** \u2014 you may need to think hard to find it. Do not over-engineer, do not be over-defensive, **do not write AI slop**. Reviewers bias toward *recommending additions*, and that bias has a recognizable slop texture: defensive checks for cases that cannot happen, extra logging, new abstractions used once, comments restating code, tests asserting tautologies, "just-in-case" guards, error handlers for cases the type system already rules out. Reject those. For each surviving finding, ask: would applying it leave the code more sound, correct, AND elegant? Two-out-of-three means look harder for a fix that gets all three before settling. After applying the fixes you accept, re-read your diff and be discerning about what *you just changed*: if any fix turned out to be bloat in context, revert it. Then verify only intended changes are present, no debug artifacts or commented-out code remain, no unrelated files were modified. Commit locally via shell (\`git add . && git commit -m "..."\`).
|
|
146412
146678
|
|
|
146413
146679
|
6. **finalize**:
|
|
146414
146680
|
- confirm a clean working tree, then push via \`${t("push_branch")}\` (see *SYSTEM* Git rules if this fails \u2014 prepush errors are usually the repo's tests/lint, not infra timeouts)
|
|
@@ -146432,7 +146698,8 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
146432
146698
|
|
|
146433
146699
|
4. For each comment:
|
|
146434
146700
|
- understand the feedback
|
|
146435
|
-
-
|
|
146701
|
+
- **verify the finding yourself** against the actual code before deciding whether to apply \u2014 every comment (human or agent) is a hypothesis, not a directive. agent reviewers especially are fallible.
|
|
146702
|
+
- 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 impossible cases, extra abstractions used once, comments restating obvious code, tests asserting tautologies, "just-in-case" guards, error handlers for cases the type system already rules out. reject those. evaluate whether applying the finding would leave the code more **sound, correct, AND elegant**; two-out-of-three is a signal to look harder for a fix that gets all three. if a request would add bloat \u2014 ceremony without commensurate correctness benefit \u2014 push back in your reply rather than mechanically applying it.
|
|
146436
146703
|
- if the request stands, make the code change using your native tools; otherwise reply explaining why
|
|
146437
146704
|
- record what was done (or why nothing was done)
|
|
146438
146705
|
|
|
@@ -146440,11 +146707,13 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
146440
146707
|
- 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
|
|
146441
146708
|
- commit locally via shell (\`git add . && git commit -m "..."\`)
|
|
146442
146709
|
|
|
146443
|
-
6. Finalize:
|
|
146710
|
+
6. Finalize. Reply + resolve are paired write actions: do BOTH or NEITHER for each thread.
|
|
146444
146711
|
- confirm a clean working tree, then push via \`${t("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*)
|
|
146445
|
-
-
|
|
146446
|
-
-
|
|
146447
|
-
|
|
146712
|
+
- **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.
|
|
146713
|
+
- **on push success**, for each thread you acted on:
|
|
146714
|
+
- 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.
|
|
146715
|
+
- **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.
|
|
146716
|
+
- call \`${t("report_progress")}\` with a brief summary`
|
|
146448
146717
|
},
|
|
146449
146718
|
// Review and IncrementalReview use a 0-or-2+ lens pattern. The default is
|
|
146450
146719
|
// 0 lenses (orchestrator handles the review solo). Multi-lens (2+
|
|
@@ -146461,9 +146730,12 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
146461
146730
|
// the Review/IncrementalReview lens fan-out where independence between
|
|
146462
146731
|
// perspectives is what's being purchased.
|
|
146463
146732
|
//
|
|
146464
|
-
//
|
|
146465
|
-
//
|
|
146466
|
-
//
|
|
146733
|
+
// Severity categorization is split across two surfaces: the opening
|
|
146734
|
+
// callout (CAUTION/IMPORTANT/ℹ️/✅) sets the review's overall tier, and
|
|
146735
|
+
// per-bullet emoji prefixes (🚨/⚠️/ℹ️ in PR_SUMMARY_FORMAT) tag
|
|
146736
|
+
// individual points inside summary sections — scoping severity to the
|
|
146737
|
+
// specific bullet rather than the whole section keeps a section that
|
|
146738
|
+
// mixes a 🚨 and an ℹ️ from being mislabeled by either of them.
|
|
146467
146739
|
{
|
|
146468
146740
|
name: "Review",
|
|
146469
146741
|
description: "Review code, PRs, or implementations; provide feedback or suggestions; identify issues; or check code quality, style, and correctness",
|
|
@@ -146549,7 +146821,9 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
146549
146821
|
|
|
146550
146822
|
6. **aggregate & draft**: when the fan-out lands, merge findings; de-dup overlaps (two lenses catching the same issue = higher-confidence signal); trace each finding yourself before accepting it. drop praise, style preferences, speculative/unverified claims, findings about pre-existing code unrelated to the PR (heuristic: if the finding's root cause lives in lines this PR added or modified, it's in scope; otherwise drop unless the PR plausibly introduced or amplified the regression), and anything not actionable. also drop **bloat-shaped findings** \u2014 proposed fixes that would add defensive checks for cases that can't happen, abstractions used once, comments restating obvious code, tests asserting tautologies, or "just-in-case" guards. subagents are fallible and bias toward recommending changes; the bar for an actionable inline comment is sound + correct + elegant. recommending a change that improves only one of the three (or worse, degrades elegance to nominally improve correctness) makes the codebase worse, not better.
|
|
146551
146823
|
|
|
146552
|
-
for
|
|
146824
|
+
**Hunt for non-anchored concerns before drafting.** After collecting your anchored findings, deliberately scan for concerns that have no specific line to point at \u2014 typically: deletion / cleanup plans for code the diff replaces or shadows; rollout sequencing (what happens to in-flight state during deploy / revert?); coverage gaps the diff implies but doesn't add; scope questions that only the human can answer (e.g. is the legacy path going away or is this a long-term dual track?); architectural risks the diff opens up that aren't a single-line bug. On substantial PRs (migrations, refactors, multi-file rewrites, version bumps that change runtime semantics), at least one such concern almost always exists; if you can't think of any, your bar is probably too high.
|
|
146825
|
+
|
|
146826
|
+
for surviving findings, draft inline comments with NEW line numbers from the diff \u2014 attach a \`<details>Technical details</details>\` block to any inline comment whose fix is non-trivial or has cross-file implications (see Inline technical details in the format below). every comment must be actionable, 2-3 sentences max in the visible part. use GitHub permalink format for code references. for impact-analysis findings (stale references after rename/remove), report them in the review body ordered by severity (runtime breakage > incorrect docs > stale comments) rather than as inline comments unless they're anchored to a specific line.
|
|
146553
146827
|
|
|
146554
146828
|
7. **submit**: ALWAYS submit exactly one review via \`${t("create_pull_request_review")}\`. Do NOT call \`report_progress\` \u2014 the review is the final record and the progress comment will be cleaned up automatically.
|
|
146555
146829
|
|
|
@@ -146557,12 +146831,12 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
146557
146831
|
|
|
146558
146832
|
The review body is structured as: \`[optional alert blockquote]\` \u2192 \`[PR summary using the default format below]\`. Inline comments are passed via the \`comments\` parameter, not in the body.
|
|
146559
146833
|
|
|
146560
|
-
|
|
146834
|
+
The opening callout is what the author sees first \u2014 pick the one that matches what you want them to do. Five tiers, from loudest to friendliest:
|
|
146561
146835
|
|
|
146562
146836
|
- \`[!CAUTION]\` \u2014 large red banner. Reads as "this will break something."
|
|
146563
146837
|
- \`[!IMPORTANT]\` \u2014 large purple banner. Reads as "you need to look at this before merging."
|
|
146564
|
-
-
|
|
146565
|
-
-
|
|
146838
|
+
- \`> \u2139\uFE0F ...\` \u2014 informational blockquote. Reads as "minor suggestions, nothing blocking."
|
|
146839
|
+
- \`> \u2705 ...\` \u2014 green friendly blockquote. Reads as "no concerns, mergeable."
|
|
146566
146840
|
|
|
146567
146841
|
Two reinforcing levers: callout intensity (above) and \`approved\` (which gates the footer Fix-button affordance \u2014 Fix renders on every non-approving review, so \`approved: true\` suppresses it). Wrapping mergeable feedback in \`[!IMPORTANT]\` trains users to click Fix on reviews that don't need fixing. Pick the tier the author's actual next action justifies.
|
|
146568
146842
|
|
|
@@ -146571,25 +146845,25 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
146571
146845
|
- **must-address non-critical findings** (real consequences if shipped \u2014 incorrect behavior in non-critical paths, missing validation on user input, regressions the author should fix before merge):
|
|
146572
146846
|
\`approved: false\`. Body opens with \`> [!IMPORTANT]\\n> ...\`, followed by the PR summary. Reserve this tier for findings with concrete fallout \u2014 do NOT use \`[!IMPORTANT]\` for nits, style preferences, or "consider also" suggestions. Include all inline comments via \`comments\`.
|
|
146573
146847
|
- **minor suggestions only** (single-line nits, doc/comment polish, defer-able observations, "rough edges"):
|
|
146574
|
-
\`approved: false\`.
|
|
146848
|
+
\`approved: false\`. Body opens with \`> \u2139\uFE0F No critical issues \u2014 minor suggestions inline.\\n\\n\` followed by the PR summary. Include all inline comments via \`comments\`. Vary the wording after the emoji to fit the review (e.g. "Minor suggestions only.", "Two rough edges worth a look."), but always keep the \u2139\uFE0F prefix and keep it short.
|
|
146575
146849
|
- **informational observations** (mergeable as-is, nothing actionable \u2014 e.g. prior feedback addressed cleanly, surfacing a minor stale doc reference, calling out something noteworthy without recommending a change):
|
|
146576
|
-
\`approved: true\`. Body opens with \`>
|
|
146850
|
+
\`approved: true\`. Body opens with \`> \u2705 No new issues found.\\n\\n\` followed by the PR summary. Do NOT include inline \`comments\` \u2014 the \u2705 signals "no action needed", which contradicts an actionable anchor; if a point is concrete enough to anchor to a line, downgrade the whole review to "minor suggestions only" (\`approved: false\`) instead.
|
|
146577
146851
|
- **no actionable issues**:
|
|
146578
|
-
\`approved: true\`. Body opens with
|
|
146852
|
+
\`approved: true\`. Body opens with \`> \u2705 No new issues found.\\n\\n\` followed by the PR summary.
|
|
146579
146853
|
|
|
146580
146854
|
${PR_SUMMARY_FORMAT}`
|
|
146581
146855
|
},
|
|
146582
|
-
// IncrementalReview shares Review's 0-or-2+ lens pattern
|
|
146583
|
-
//
|
|
146584
|
-
//
|
|
146585
|
-
//
|
|
146586
|
-
// subagents matches the canonical anneal
|
|
146587
|
-
// pre-existing failures — don't flag these"
|
|
146588
|
-
// regressions the new commits amplified.
|
|
146589
|
-
//
|
|
146590
|
-
//
|
|
146591
|
-
//
|
|
146592
|
-
//
|
|
146856
|
+
// IncrementalReview shares Review's 0-or-2+ lens pattern AND its body
|
|
146857
|
+
// format (PR_SUMMARY_FORMAT), scoped to the incremental delta against the
|
|
146858
|
+
// prior pullfrog review. The "issues must be NEW since the last Pullfrog
|
|
146859
|
+
// review" filter lives at aggregation time (step 8), NOT in the subagent
|
|
146860
|
+
// prompt — pushing the filter into subagents matches the canonical anneal
|
|
146861
|
+
// anti-pattern of "list known pre-existing failures — don't flag these"
|
|
146862
|
+
// and suppresses signal on regressions the new commits amplified. A
|
|
146863
|
+
// separate "Prior review feedback" checklist would duplicate the rolling
|
|
146864
|
+
// PR summary snapshot's record of what earlier runs already addressed and
|
|
146865
|
+
// add noise to the user-facing body. Same opening-callout + per-bullet
|
|
146866
|
+
// emoji severity split as Review.
|
|
146593
146867
|
{
|
|
146594
146868
|
name: "IncrementalReview",
|
|
146595
146869
|
description: "Re-review a PR after new commits are pushed; focus on new changes since the last review",
|
|
@@ -146601,7 +146875,15 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
146601
146875
|
|
|
146602
146876
|
3. **incremental scope**: if \`incrementalDiffPath\` is present, read it to see what changed since the last review. this is a range-diff that isolates the net changes, filtering out base branch noise. if not present, fall back to reviewing the full PR diff and determine what changed since Pullfrog's most recent review.
|
|
146603
146877
|
|
|
146604
|
-
4. **prior feedback**: fetch previous reviews via \`${t("list_pull_request_reviews")}
|
|
146878
|
+
4. **prior feedback \u2014 read AND retire it**: fetch previous reviews via \`${t("list_pull_request_reviews")}\`, then call \`${t("get_review_comments")}\` on each prior Pullfrog review. Each thread renders as a section whose first line is a fenced tag \`comment author=<login> id=<fullDatabaseId> review=<reviewId> thread=<graphqlId>\`; section headers carry \`[RESOLVED]\` / \`[OUTDATED]\` when relevant. For every **open, Pullfrog-originated** thread, decide and act:
|
|
146879
|
+
|
|
146880
|
+
- **Pullfrog-originated** means the FIRST \`comment author=...\` tag in the section is \`author=pullfrog[bot]\`. The \`*\` marker on individual comments is unrelated \u2014 it flags whether a comment belongs to the queried review, not whether it is the thread root.
|
|
146881
|
+
- **addressed?** read the file at the thread's anchor and judge whether the substantive concern is now resolved by the new commits. Lines being modified isn't enough: reformatting, renaming, or moving the same code elsewhere doesn't address a concern. If the comment raised multiple distinct concerns, ALL must be addressed. The \`[OUTDATED]\` tag means GitHub moved the anchor (line shift, force-push, rename) \u2014 it does NOT mean the concern was addressed; re-read the code at its new location before deciding.
|
|
146882
|
+
- **if addressed**: call \`${t("reply_to_review_comment")}\` with the root tag's numeric \`id=\` as \`comment_id\` (NOT the \`thread=\` value \u2014 that's a separate GraphQL ID used only by resolve) and a one-line body (e.g. \`Addressed in <short-sha>.\`), then call \`${t("resolve_review_thread")}\` with the root tag's \`thread=\` value as \`thread_id\`. Do this BEFORE drafting the new review so the GitHub thread state aligns with the new review by the time it lands.
|
|
146883
|
+
- **if uncertain or partially addressed**: leave open. False-positive resolutions erode trust faster than false negatives.
|
|
146884
|
+
- **scope**: only retire Pullfrog-originated threads. Threads from human reviewers belong to those humans to resolve, even if the commit happened to address them.
|
|
146885
|
+
|
|
146886
|
+
The remaining open threads feed step 8's dedup filter \u2014 anything already flagged and unchanged by the new commits should not be re-raised. The rolling PR summary snapshot is the durable record of retire activity; you don't need to surface it in the review body.
|
|
146605
146887
|
|
|
146606
146888
|
5. **triage**: orient on the *incremental* changes \u2014 domain, seams, external contracts, user-facing surfaces. pull as much context as you need to render a confident review: read related files, grep for callers of changed symbols, check tests that exercise the touched paths. **you are the synthesizer.**
|
|
146607
146889
|
|
|
@@ -146647,22 +146929,28 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
146647
146929
|
- do NOT pre-shape their output with a finding schema
|
|
146648
146930
|
- do NOT mention the other lenses (independence is the point)
|
|
146649
146931
|
|
|
146650
|
-
8. **aggregate, draft, self-critique**: merge findings (yours + any subagent output if you went multi-lens); de-dup overlaps; trace each finding yourself. drop praise, style preferences, speculative/unverified claims, findings about pre-existing code unrelated to the new commits, anything not actionable, and anything that re-states prior review feedback (heuristic: if the finding's root cause lives in lines the *new commits* added or modified, it's in scope; otherwise drop). also drop **bloat-shaped findings** \u2014 proposed fixes that would add defensive checks for cases that can't happen, abstractions used once, comments restating obvious code, tests asserting tautologies, or "just-in-case" guards. subagents are fallible and bias toward recommending changes; the bar for an actionable inline comment is sound + correct + elegant. recommending a change that improves only one of the three (or degrades elegance to nominally improve correctness) makes the codebase worse, not better. To compute "lines the new commits added or modified": if \`incrementalDiffPath\` from step 2 is present, use it directly. Otherwise, take the prior Pullfrog review's \`commit_id\` (returned alongside each entry from \`${t("list_pull_request_reviews")}\` in step 4) and run \`git diff <prior-review-sha>..HEAD\` to isolate the lines added since that review.
|
|
146932
|
+
8. **aggregate, draft, self-critique**: merge findings (yours + any subagent output if you went multi-lens); de-dup overlaps; trace each finding yourself. drop praise, style preferences, speculative/unverified claims, findings about pre-existing code unrelated to the new commits, anything not actionable, and anything that re-states prior review feedback (heuristic: if the finding's root cause lives in lines the *new commits* added or modified, it's in scope; otherwise drop). also drop **bloat-shaped findings** \u2014 proposed fixes that would add defensive checks for cases that can't happen, abstractions used once, comments restating obvious code, tests asserting tautologies, or "just-in-case" guards. subagents are fallible and bias toward recommending changes; the bar for an actionable inline comment is sound + correct + elegant. recommending a change that improves only one of the three (or degrades elegance to nominally improve correctness) makes the codebase worse, not better. To compute "lines the new commits added or modified": if \`incrementalDiffPath\` from step 2 is present, use it directly. Otherwise, take the prior Pullfrog review's \`commit_id\` (returned alongside each entry from \`${t("list_pull_request_reviews")}\` in step 4) and run \`git diff <prior-review-sha>..HEAD\` to isolate the lines added since that review.
|
|
146933
|
+
|
|
146934
|
+
**Hunt for non-anchored concerns before drafting.** After collecting your anchored findings, deliberately scan for concerns that have no specific line to point at \u2014 typically: deletion / cleanup plans for code the new commits replace or shadow; rollout sequencing (what happens to in-flight state during deploy / revert?); coverage gaps the new commits imply but don't add; scope questions that only the human can answer (e.g. is the legacy path going away or is this a long-term dual track?); architectural risks the new commits open up that aren't a single-line bug. On substantial incremental diffs (migrations, refactors, multi-file rewrites, version bumps that change runtime semantics), at least one such concern almost always exists; if you can't think of any, your bar is probably too high.
|
|
146651
146935
|
|
|
146652
|
-
|
|
146936
|
+
draft inline comments with NEW line numbers from the full PR diff \u2014 attach a \`<details>Technical details</details>\` block to any inline comment whose fix is non-trivial or has cross-file implications (see Inline technical details in the format below). every comment must be actionable, 2-3 sentences max in the visible part.
|
|
146937
|
+
|
|
146938
|
+
9. **build the review body**: use the same default format as Review mode (preamble + optional cross-cutting \`### \` sections + optional \`### \u2139\uFE0F Nitpicks\`) \u2014 scoped to the **incremental delta**, not the full PR. The "Reviewed changes" bullets describe what changed since the prior pullfrog review (each bullet starts with a past-tense verb, e.g. \`- Extracted shared CLI runtime into a single module\`). Do NOT include a separate "Prior review feedback" checklist \u2014 that's tracked in the rolling PR summary snapshot for the next agent run, and surfacing it in the user-facing body is noise (changes that addressed prior feedback are already covered by the Reviewed-changes bullets). In some cases you may receive a complete diff for the whole PR instead of an incremental one; when this happens, determine what changed since Pullfrog's most recent review yourself before drafting bullets.
|
|
146653
146939
|
|
|
146654
146940
|
10. Submit \u2014 every run must end with EXACTLY ONE of \`${t("create_pull_request_review")}\` (substantive review) or \`${t("report_progress")}\` (no-review acknowledgement). do NOT call \`create_issue_comment\` for review output.
|
|
146655
146941
|
|
|
146656
|
-
Same callout
|
|
146942
|
+
Same callout ladder as Review mode \u2014 \`[!CAUTION]\` (red, "will break") \u2192 \`[!IMPORTANT]\` (purple, "must address before merging") \u2192 \`> \u2139\uFE0F ...\` (informational, "minor suggestions only") \u2192 \`> \u2705 ...\` (green friendly, "no concerns"). Same Fix-button lever: the footer renders a Fix button on every non-approving review, so \`approved: true\` suppresses it. Wrapping mergeable feedback in \`[!IMPORTANT]\` trains users to click Fix on reviews that don't need fixing \u2014 pick the tier the author's actual next action justifies.
|
|
146657
146943
|
|
|
146658
146944
|
Follow these rules:
|
|
146659
146945
|
- note: the first create_pull_request_review submission may error with a one-time diff-coverage nudge listing unread TOC regions. retry the same call to proceed \u2014 optionally after reading the listed ranges. the pre-flight will not block again this session.
|
|
146660
146946
|
- IF NO NEW ISSUES, NON-SUBSTANTIVE CHANGES ONLY (trivial formatting, import reordering, comment tweaks): do NOT submit a review. Instead call \`${t("report_progress")}\` with a 1-2 sentence note explaining no review was warranted (e.g. "No new issues. Changes since last review are formatting-only."). this leaves a visible signal that the run completed.
|
|
146661
|
-
- ELSE IF NEW CRITICAL ISSUES (blocks merge \u2014 bugs, security, data loss, broken core flows): call \`${t("create_pull_request_review")}\` with \`approved: false\`, all comments, and the review body. body opens with \`> [!CAUTION]\\n> This PR introduces ...\`,
|
|
146662
|
-
- ELSE IF NEW MUST-ADDRESS NON-CRITICAL FINDINGS (real consequences if shipped \u2014 incorrect behavior, missing validation, regressions the author should fix before merge): call \`${t("create_pull_request_review")}\` with \`approved: false\`, all comments, and the review body. body opens with \`> [!IMPORTANT]\\n> ...\`,
|
|
146663
|
-
- ELSE IF NEW MINOR SUGGESTIONS ONLY (single-line nits, doc/comment polish, defer-able observations, "rough edges"): call \`${t("create_pull_request_review")}\` with \`approved: false\`, all comments, and the review body. body opens
|
|
146664
|
-
- ELSE IF INFORMATIONAL OBSERVATIONS (mergeable as-is, but worth surfacing \u2014 e.g. prior feedback addressed cleanly with one minor stale doc reference, or a noteworthy positive observation): call \`${t("create_pull_request_review")}\` with \`approved: true\`, NO inline comments, and the review body. body opens with \`>
|
|
146665
|
-
- ELSE IF NO NEW ISSUES, SUBSTANTIVE CHANGES (new functionality, behavior changes, or fixes to prior review feedback): call \`${t("create_pull_request_review")}\` to create a PR review. If all previous reviews have been properly addressed and no new issues were discovered,
|
|
146947
|
+
- ELSE IF NEW CRITICAL ISSUES (blocks merge \u2014 bugs, security, data loss, broken core flows): call \`${t("create_pull_request_review")}\` with \`approved: false\`, all comments, and the review body. body opens with \`> [!CAUTION]\\n> This PR introduces ...\`, followed by the PR summary using the default format below.
|
|
146948
|
+
- ELSE IF NEW MUST-ADDRESS NON-CRITICAL FINDINGS (real consequences if shipped \u2014 incorrect behavior, missing validation, regressions the author should fix before merge): call \`${t("create_pull_request_review")}\` with \`approved: false\`, all comments, and the review body. body opens with \`> [!IMPORTANT]\\n> ...\`, followed by the PR summary using the default format below. Do NOT use this tier for nits, style preferences, or "consider also" suggestions.
|
|
146949
|
+
- ELSE IF NEW MINOR SUGGESTIONS ONLY (single-line nits, doc/comment polish, defer-able observations, "rough edges"): call \`${t("create_pull_request_review")}\` with \`approved: false\`, all comments, and the review body. body opens with \`> \u2139\uFE0F No critical issues \u2014 minor suggestions inline.\\n\\n\` (vary the wording after \u2139\uFE0F to fit the review), followed by the PR summary using the default format below.
|
|
146950
|
+
- ELSE IF INFORMATIONAL OBSERVATIONS (mergeable as-is, but worth surfacing \u2014 e.g. prior feedback addressed cleanly with one minor stale doc reference, or a noteworthy positive observation): call \`${t("create_pull_request_review")}\` with \`approved: true\`, NO inline comments, and the review body. body opens with \`> \u2705 No new issues found.\\n\\n\` (or similar friendly green opener), followed by the PR summary using the default format below. If a point is concrete enough to anchor to a line, downgrade the whole review to "minor suggestions only" (\`approved: false\`) instead \u2014 the \u2705 signals "no action needed", which contradicts an actionable anchor.
|
|
146951
|
+
- ELSE IF NO NEW ISSUES, SUBSTANTIVE CHANGES (new functionality, behavior changes, or fixes to prior review feedback): call \`${t("create_pull_request_review")}\` to create a PR review. If all previous reviews have been properly addressed and no new issues were discovered, set \`approved: true\`. body opens with \`> \u2705 No new issues found.\\n\\n\`, followed by the PR summary using the default format below.
|
|
146952
|
+
|
|
146953
|
+
${PR_SUMMARY_FORMAT}`
|
|
146666
146954
|
},
|
|
146667
146955
|
{
|
|
146668
146956
|
name: "Plan",
|
|
@@ -146677,7 +146965,7 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
146677
146965
|
|
|
146678
146966
|
3. Produce a structured, actionable plan with clear milestones.
|
|
146679
146967
|
|
|
146680
|
-
4. Call \`${t("report_progress")}\` with the plan.`
|
|
146968
|
+
4. Call \`${t("report_progress")}\` with the plan body. Do NOT set \`target_plan_comment\` \u2014 that flag is exclusively for revising an existing plan, and \`${t("select_mode")}\` will route you to a separate PlanEdit checklist when a prior plan comment exists for this issue.`
|
|
146681
146969
|
},
|
|
146682
146970
|
{
|
|
146683
146971
|
name: "Fix",
|
|
@@ -146769,6 +147057,7 @@ function initToolState(params) {
|
|
|
146769
147057
|
return {
|
|
146770
147058
|
progressComment: resolved,
|
|
146771
147059
|
hadProgressComment: !!resolved,
|
|
147060
|
+
prepushFailureCount: 0,
|
|
146772
147061
|
backgroundProcesses: /* @__PURE__ */ new Map(),
|
|
146773
147062
|
usageEntries: []
|
|
146774
147063
|
};
|
|
@@ -146868,6 +147157,17 @@ async function installFromNpmTarball(params) {
|
|
|
146868
147157
|
// utils/providerErrors.ts
|
|
146869
147158
|
var statusKey = `\\b(?:status[_ ]?code|http[_ ]?status|status)["']?\\s*[:=]\\s*["']?`;
|
|
146870
147159
|
var PROVIDER_ERROR_PATTERNS = [
|
|
147160
|
+
// billing-payload patterns come BEFORE bare status-code patterns. providers
|
|
147161
|
+
// commonly return 401 / 429 for billing/quota exhaustion (OpenCode Zen
|
|
147162
|
+
// `CreditsError` / `FreeUsageLimitError`, Gemini `RESOURCE_EXHAUSTED` +
|
|
147163
|
+
// "spending cap", Anthropic "Insufficient balance"). these are non-retryable
|
|
147164
|
+
// and require user-billing action — distinct from a transient auth error or
|
|
147165
|
+
// rate-limit. status-code patterns would otherwise win and surface
|
|
147166
|
+
// "auth error (401)" / "rate limited (429)" with no billing hint. see #778.
|
|
147167
|
+
{ regex: /\bCreditsError\b/, label: "provider billing exhausted" },
|
|
147168
|
+
{ regex: /\bFreeUsageLimitError\b/, label: "provider billing exhausted" },
|
|
147169
|
+
{ regex: /Insufficient balance/i, label: "provider billing exhausted" },
|
|
147170
|
+
{ regex: /spending cap/i, label: "provider billing exhausted" },
|
|
146871
147171
|
// auth patterns must come BEFORE rate-limit patterns. OpenRouter 401 error
|
|
146872
147172
|
// payloads carry `x-ratelimit-*` response headers in the dump, and the
|
|
146873
147173
|
// free-form rate-limit regex below would otherwise win on word-boundary
|
|
@@ -146902,12 +147202,38 @@ var PROVIDER_ERROR_PATTERNS = [
|
|
|
146902
147202
|
// around `limit` rejects keys like `time_limit` or `field_limit`.
|
|
146903
147203
|
{ regex: /["']?\blimit\b["']?\s*:\s*0\b/, label: "zero quota" }
|
|
146904
147204
|
];
|
|
146905
|
-
|
|
147205
|
+
var EXCERPT_MAX_BYTES = 600;
|
|
147206
|
+
var LINES_BEFORE = 1;
|
|
147207
|
+
var LINES_AFTER = 2;
|
|
147208
|
+
function findProviderErrorMatch(text) {
|
|
146906
147209
|
for (const entry of PROVIDER_ERROR_PATTERNS) {
|
|
146907
|
-
|
|
147210
|
+
const m = entry.regex.exec(text);
|
|
147211
|
+
if (!m) continue;
|
|
147212
|
+
return { label: entry.label, excerpt: extractExcerpt(text, m.index) };
|
|
146908
147213
|
}
|
|
146909
147214
|
return null;
|
|
146910
147215
|
}
|
|
147216
|
+
function extractExcerpt(text, matchIndex) {
|
|
147217
|
+
const lineStart = text.lastIndexOf("\n", matchIndex - 1) + 1;
|
|
147218
|
+
const lineEndRaw = text.indexOf("\n", matchIndex);
|
|
147219
|
+
const lineEnd = lineEndRaw === -1 ? text.length : lineEndRaw;
|
|
147220
|
+
let start = lineStart;
|
|
147221
|
+
for (let i = 0; i < LINES_BEFORE && start > 0; i++) {
|
|
147222
|
+
const prev = text.lastIndexOf("\n", start - 2);
|
|
147223
|
+
start = prev < 0 ? 0 : prev + 1;
|
|
147224
|
+
}
|
|
147225
|
+
let end = lineEnd;
|
|
147226
|
+
for (let i = 0; i < LINES_AFTER && end < text.length; i++) {
|
|
147227
|
+
const next2 = text.indexOf("\n", end + 1);
|
|
147228
|
+
end = next2 < 0 ? text.length : next2;
|
|
147229
|
+
}
|
|
147230
|
+
let excerpt = text.slice(start, end);
|
|
147231
|
+
if (excerpt.length > EXCERPT_MAX_BYTES) {
|
|
147232
|
+
excerpt = text.slice(lineStart, lineEnd);
|
|
147233
|
+
if (excerpt.length > EXCERPT_MAX_BYTES) excerpt = excerpt.slice(0, EXCERPT_MAX_BYTES);
|
|
147234
|
+
}
|
|
147235
|
+
return excerpt.trim();
|
|
147236
|
+
}
|
|
146911
147237
|
var ROUTER_KEYLIMIT_EXHAUSTED_PATTERN = /requires more credits.*?fewer max_tokens|requested up to \d+ tokens.*?can only afford/is;
|
|
146912
147238
|
function isRouterKeylimitExhaustedError(text) {
|
|
146913
147239
|
return ROUTER_KEYLIMIT_EXHAUSTED_PATTERN.test(text);
|
|
@@ -146968,11 +147294,25 @@ function addSkill(params) {
|
|
|
146968
147294
|
);
|
|
146969
147295
|
if (result.status === 0) {
|
|
146970
147296
|
log.success(`installed ${params.skill} skill (${params.agent})`);
|
|
146971
|
-
|
|
146972
|
-
const stderr = (result.stderr?.toString() || "").trim();
|
|
146973
|
-
const errorMsg = result.error ? result.error.message : stderr;
|
|
146974
|
-
log.info(`${params.skill} skill install failed: ${errorMsg}`);
|
|
147297
|
+
return;
|
|
146975
147298
|
}
|
|
147299
|
+
const stdout = (result.stdout?.toString() || "").trim();
|
|
147300
|
+
const stderr = (result.stderr?.toString() || "").trim();
|
|
147301
|
+
const parts = [
|
|
147302
|
+
`exit=${result.status ?? "null"} signal=${result.signal ?? "null"}`,
|
|
147303
|
+
result.error ? `spawn error: ${result.error.message}` : null,
|
|
147304
|
+
stderr ? `stderr:
|
|
147305
|
+
${tailLines(stderr, 20)}` : null,
|
|
147306
|
+
stdout ? `stdout:
|
|
147307
|
+
${tailLines(stdout, 20)}` : null
|
|
147308
|
+
].filter(Boolean);
|
|
147309
|
+
log.warning(`${params.skill} skill install failed \u2014 ${parts.join(" | ")}`);
|
|
147310
|
+
}
|
|
147311
|
+
function tailLines(text, n) {
|
|
147312
|
+
const lines = text.split("\n");
|
|
147313
|
+
if (lines.length <= n) return text;
|
|
147314
|
+
return `...(truncated, last ${n} of ${lines.length} lines)
|
|
147315
|
+
${lines.slice(-n).join("\n")}`;
|
|
146976
147316
|
}
|
|
146977
147317
|
|
|
146978
147318
|
// utils/timer.ts
|
|
@@ -147069,7 +147409,7 @@ function buildUnsubmittedReviewPrompt(mode) {
|
|
|
147069
147409
|
return [
|
|
147070
147410
|
`MISSING REVIEW OUTPUT \u2014 you selected Review mode but stopped without calling \`create_pull_request_review\`. the user has no visible signal that this run produced anything; the progress comment will be deleted on exit and no review will appear on the PR.`,
|
|
147071
147411
|
"",
|
|
147072
|
-
"call `create_pull_request_review` now with your aggregated review (body + inline comments). pick the tier per the mode prompt \u2014 Review mode has no no-submit exit, so even informational `>
|
|
147412
|
+
"call `create_pull_request_review` now with your aggregated review (body + inline comments). pick the tier per the mode prompt \u2014 Review mode has no no-submit exit, so even informational `> \u2705 No new issues found.` reviews must be submitted (with `approved: true`). the first call may error once with a diff-coverage nudge \u2014 retry the same call to proceed.",
|
|
147073
147413
|
"",
|
|
147074
147414
|
"do NOT stop again until `create_pull_request_review` has been called successfully."
|
|
147075
147415
|
].join("\n");
|
|
@@ -147115,6 +147455,11 @@ function buildPostRunPrompt(issues) {
|
|
|
147115
147455
|
if (issues.summaryStale) parts.push(buildSummaryStalePrompt(issues.summaryStale.filePath));
|
|
147116
147456
|
return parts.join("\n\n---\n\n");
|
|
147117
147457
|
}
|
|
147458
|
+
var REFLECTION_SKIP_MODES = /* @__PURE__ */ new Set(["IncrementalReview"]);
|
|
147459
|
+
function shouldRunReflection(mode) {
|
|
147460
|
+
if (!mode) return true;
|
|
147461
|
+
return !REFLECTION_SKIP_MODES.has(mode);
|
|
147462
|
+
}
|
|
147118
147463
|
function buildLearningsReflectionPrompt(filePath) {
|
|
147119
147464
|
return [
|
|
147120
147465
|
`REFLECTION \u2014 before you finish, think back over this task: did you discover anything about this repo's setup, test commands, conventions, or patterns that is high-confidence and would reliably help future runs?`,
|
|
@@ -147126,18 +147471,16 @@ function buildLearningsReflectionPrompt(filePath) {
|
|
|
147126
147471
|
`- **no section over ~300 lines.** when a section is approaching that, split it: introduce \`### \` subsections grouping related bullets, or hoist a coherent group into a new top-level \`## \` section. granular sections mean future runs read targeted line ranges instead of slurping the whole file. this is the most important hygiene rule on long-lived repos.`,
|
|
147127
147472
|
`- if you find a flat unstructured list (legacy content from before this format), restructure it: read it, group related bullets, rewrite the file with \`## \` / \`### \` headings around them. don't preserve bad structure \u2014 fix it.`,
|
|
147128
147473
|
"",
|
|
147474
|
+
`the only test: would a future run on this repo do its work better because this bullet exists? useful for future runs in this repo \u2014 prevent wasted tool calls, rabbit holes, and mistakes.`,
|
|
147475
|
+
"",
|
|
147129
147476
|
`bullet hygiene:`,
|
|
147130
|
-
`- one fact per line starting with \`-
|
|
147131
|
-
`-
|
|
147132
|
-
`-
|
|
147133
|
-
|
|
147134
|
-
|
|
147477
|
+
`- one fact per line starting with \`- \`, \u2264 240 chars.`,
|
|
147478
|
+
`- only add when high-confidence, broadly useful, evergreen.`,
|
|
147479
|
+
`- prune wrong or low-signal bullets; merge overlaps; dedupe across sections.`,
|
|
147480
|
+
"",
|
|
147481
|
+
`don't anchor facts to repo state that will move: PR / review / commit / branch refs, dates, version pins, line numbers. state the rule directly. if it needs the anchor to be load-bearing, it isn't evergreen.`,
|
|
147135
147482
|
"",
|
|
147136
|
-
`
|
|
147137
|
-
`- pullfrog tool quirks (e.g. "\`shell\` timeout is in milliseconds", "\`git\` args must be a JSON array", "\`create_pull_request_review\` drops out-of-hunk comments", "\`push_branch\` may report timeout when push succeeded"). these are universal across repos and belong in the tool descriptions \u2014 flag the gap rather than hoarding the workaround per-repo.`,
|
|
147138
|
-
`- references to specific PR numbers, review IDs, commit SHAs, branch names, or person handles ("PR #595 introduced X", "flagged in review 12345", "as of commit abc123"). repo state changes; these decay into noise within weeks.`,
|
|
147139
|
-
`- dated assertions ("as of May 2026", "currently...", "for now..."). if a fact needs a date to be true, it isn't durable enough to belong here.`,
|
|
147140
|
-
`- play-by-play of what THIS run did. learnings are for the NEXT run, not a retrospective.`,
|
|
147483
|
+
`tool-quirk bullets are fine when you burned calls discovering the quirk and a future run would repeat them. write the workaround, not the war story.`,
|
|
147141
147484
|
"",
|
|
147142
147485
|
`if you have nothing substantively new to add AND the existing entries still look healthy and well-structured, leave the file alone \u2014 just reply "done" and stop. silence is a valid outcome.`
|
|
147143
147486
|
].join("\n");
|
|
@@ -147357,7 +147700,7 @@ function stripProviderPrefix(specifier) {
|
|
|
147357
147700
|
function resolveEffort(_model) {
|
|
147358
147701
|
return "high";
|
|
147359
147702
|
}
|
|
147360
|
-
function
|
|
147703
|
+
function tailLines2(text, maxCodeUnits) {
|
|
147361
147704
|
if (text.length <= maxCodeUnits) return text;
|
|
147362
147705
|
const tail = text.slice(-maxCodeUnits);
|
|
147363
147706
|
const firstNewline = tail.indexOf("\n");
|
|
@@ -147421,6 +147764,7 @@ async function runClaude(params) {
|
|
|
147421
147764
|
}
|
|
147422
147765
|
} else if (block.type === "tool_use") {
|
|
147423
147766
|
const toolName = block.name || "unknown";
|
|
147767
|
+
suspendActivity();
|
|
147424
147768
|
if (params.onToolUse) {
|
|
147425
147769
|
params.onToolUse({
|
|
147426
147770
|
toolName,
|
|
@@ -147465,6 +147809,7 @@ async function runClaude(params) {
|
|
|
147465
147809
|
for (const block of content) {
|
|
147466
147810
|
if (typeof block === "string") continue;
|
|
147467
147811
|
if (block.type === "tool_result") {
|
|
147812
|
+
resumeActivity();
|
|
147468
147813
|
timerFor(label).markToolResult();
|
|
147469
147814
|
const outputContent = typeof block.content === "string" ? block.content : Array.isArray(block.content) ? block.content.map(
|
|
147470
147815
|
(entry) => typeof entry === "string" ? entry : typeof entry === "object" && entry !== null && "text" in entry ? String(entry.text) : JSON.stringify(entry)
|
|
@@ -147553,6 +147898,7 @@ async function runClaude(params) {
|
|
|
147553
147898
|
env: params.env,
|
|
147554
147899
|
activityTimeout: 3e5,
|
|
147555
147900
|
onActivityTimeout: params.onActivityTimeout,
|
|
147901
|
+
isPausedExternally: isActivitySuspended,
|
|
147556
147902
|
stdio: ["ignore", "pipe", "pipe"],
|
|
147557
147903
|
// run claude in its own process group so SIGKILL on activity timeout /
|
|
147558
147904
|
// outer cancellation reaches any subprocesses it spawns (rg, file
|
|
@@ -147612,10 +147958,10 @@ async function runClaude(params) {
|
|
|
147612
147958
|
if (!trimmed) return;
|
|
147613
147959
|
recentStderr.push(trimmed);
|
|
147614
147960
|
if (recentStderr.length > MAX_STDERR_LINES) recentStderr.shift();
|
|
147615
|
-
const
|
|
147616
|
-
if (
|
|
147617
|
-
lastProviderError =
|
|
147618
|
-
log.info(`\xBB provider error detected (${
|
|
147961
|
+
const match3 = findProviderErrorMatch(trimmed);
|
|
147962
|
+
if (match3) {
|
|
147963
|
+
lastProviderError = match3.label;
|
|
147964
|
+
log.info(`\xBB provider error detected (${match3.label}): ${match3.excerpt}`);
|
|
147619
147965
|
} else {
|
|
147620
147966
|
log.debug(trimmed);
|
|
147621
147967
|
}
|
|
@@ -147646,7 +147992,7 @@ ${stderrContext}`);
|
|
|
147646
147992
|
const errorContext = lastProviderError ? ` (${lastProviderError})` : "";
|
|
147647
147993
|
const stdoutSnapshot = output.toString();
|
|
147648
147994
|
const stderrSnapshot = recentStderr.join("\n");
|
|
147649
|
-
const truncatedStdout = stdoutSnapshot ?
|
|
147995
|
+
const truncatedStdout = stdoutSnapshot ? tailLines2(stdoutSnapshot, 2048) : "";
|
|
147650
147996
|
const nonJsonStdoutSnapshot = recentNonJsonStdout.join("\n");
|
|
147651
147997
|
const errorMessage = lastResultError || stderrSnapshot || nonJsonStdoutSnapshot || truncatedStdout || `unknown error - no output from Claude CLI${errorContext}`;
|
|
147652
147998
|
log.error(
|
|
@@ -147708,6 +148054,7 @@ ${stderrContext}`
|
|
|
147708
148054
|
}
|
|
147709
148055
|
var MANAGED_SETTINGS_DIR = "/etc/claude-code";
|
|
147710
148056
|
var MANAGED_SETTINGS_PATH = `${MANAGED_SETTINGS_DIR}/managed-settings.json`;
|
|
148057
|
+
var CODEX_AUTH_DENY_PATH = "~/.local/share/opencode/auth.json";
|
|
147711
148058
|
var managedSettings = {
|
|
147712
148059
|
allowManagedPermissionRulesOnly: true,
|
|
147713
148060
|
allowManagedHooksOnly: true,
|
|
@@ -147720,12 +148067,16 @@ var managedSettings = {
|
|
|
147720
148067
|
"Edit(//proc/**)",
|
|
147721
148068
|
"Edit(//sys/**)",
|
|
147722
148069
|
"Glob(//proc/**)",
|
|
147723
|
-
"Glob(//sys/**)"
|
|
148070
|
+
"Glob(//sys/**)",
|
|
148071
|
+
`Read(${CODEX_AUTH_DENY_PATH})`,
|
|
148072
|
+
`Grep(${CODEX_AUTH_DENY_PATH})`,
|
|
148073
|
+
`Edit(${CODEX_AUTH_DENY_PATH})`,
|
|
148074
|
+
`Glob(${CODEX_AUTH_DENY_PATH})`
|
|
147724
148075
|
]
|
|
147725
148076
|
},
|
|
147726
148077
|
sandbox: {
|
|
147727
148078
|
filesystem: {
|
|
147728
|
-
denyRead: ["/proc", "/sys"]
|
|
148079
|
+
denyRead: ["/proc", "/sys", CODEX_AUTH_DENY_PATH]
|
|
147729
148080
|
}
|
|
147730
148081
|
}
|
|
147731
148082
|
};
|
|
@@ -147786,14 +148137,21 @@ var claude = agent({
|
|
|
147786
148137
|
if (model) {
|
|
147787
148138
|
baseArgs.push("--model", model);
|
|
147788
148139
|
}
|
|
148140
|
+
const repoDir = process.cwd();
|
|
147789
148141
|
const env2 = {
|
|
147790
148142
|
...process.env,
|
|
147791
|
-
...homeEnv
|
|
148143
|
+
...homeEnv,
|
|
148144
|
+
PWD: repoDir
|
|
147792
148145
|
};
|
|
147793
148146
|
if (isBedrockRoute) {
|
|
147794
148147
|
env2.CLAUDE_CODE_USE_BEDROCK = "1";
|
|
147795
148148
|
}
|
|
147796
|
-
|
|
148149
|
+
if (env2.CLAUDE_CODE_OAUTH_TOKEN && !isBedrockRoute && env2.ANTHROPIC_API_KEY) {
|
|
148150
|
+
log.debug(
|
|
148151
|
+
"\xBB CLAUDE_CODE_OAUTH_TOKEN present \u2014 stripping ANTHROPIC_API_KEY from Claude Code env so the OAuth subscription is used"
|
|
148152
|
+
);
|
|
148153
|
+
delete env2.ANTHROPIC_API_KEY;
|
|
148154
|
+
}
|
|
147797
148155
|
log.info(`\xBB effort: ${effort}`);
|
|
147798
148156
|
log.debug(`\xBB starting Pullfrog (Claude Code): node ${baseArgs.join(" ")}`);
|
|
147799
148157
|
log.debug(`\xBB working directory: ${repoDir}`);
|
|
@@ -147813,7 +148171,7 @@ var claude = agent({
|
|
|
147813
148171
|
ctx,
|
|
147814
148172
|
initialResult: result,
|
|
147815
148173
|
initialUsage: result.usage,
|
|
147816
|
-
reflectionPrompt: ctx.toolState.learningsFilePath ? buildLearningsReflectionPrompt(ctx.toolState.learningsFilePath) : void 0,
|
|
148174
|
+
reflectionPrompt: ctx.toolState.learningsFilePath && shouldRunReflection(ctx.toolState.selectedMode) ? buildLearningsReflectionPrompt(ctx.toolState.learningsFilePath) : void 0,
|
|
147817
148175
|
canResume: (r) => Boolean(r.sessionId),
|
|
147818
148176
|
resume: async (c) => {
|
|
147819
148177
|
const sessionId = c.previousResult.sessionId;
|
|
@@ -147827,11 +148185,167 @@ var claude = agent({
|
|
|
147827
148185
|
}
|
|
147828
148186
|
});
|
|
147829
148187
|
|
|
147830
|
-
// agents/
|
|
147831
|
-
|
|
148188
|
+
// agents/opencode_v2.ts
|
|
148189
|
+
var core2 = __toESM(require_core(), 1);
|
|
148190
|
+
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync9 } from "node:fs";
|
|
148191
|
+
import { join as join12 } from "node:path";
|
|
148192
|
+
import { performance as performance7 } from "node:perf_hooks";
|
|
148193
|
+
|
|
148194
|
+
// utils/agentHangReport.ts
|
|
148195
|
+
var MAX_STDERR_BYTES = 3e3;
|
|
148196
|
+
function formatAgentHangBody(input) {
|
|
148197
|
+
if (!input.diagnostic) return null;
|
|
148198
|
+
if (input.diagnostic.lastProviderError === "provider billing exhausted") {
|
|
148199
|
+
return formatBillingExhaustedBody(input.diagnostic);
|
|
148200
|
+
}
|
|
148201
|
+
const verb = input.isHang ? "stalled" : "failed";
|
|
148202
|
+
const cause = input.diagnostic.lastProviderError ? ` \u2014 likely cause: \`${input.diagnostic.lastProviderError}\`` : "";
|
|
148203
|
+
const headline = `**${input.diagnostic.label} ${verb}**${cause}`;
|
|
148204
|
+
const explanation = formatExplanation({
|
|
148205
|
+
isHang: input.isHang,
|
|
148206
|
+
errorMessage: input.errorMessage
|
|
148207
|
+
});
|
|
148208
|
+
const parts = [headline, "", `${explanation} ${formatEventsPart(input.diagnostic)}`];
|
|
148209
|
+
const tail = renderStderrTail(input.diagnostic.recentStderr);
|
|
148210
|
+
if (tail) {
|
|
148211
|
+
const fence = pickFence(tail);
|
|
148212
|
+
parts.push(
|
|
148213
|
+
"",
|
|
148214
|
+
"<details><summary>Recent agent stderr</summary>",
|
|
148215
|
+
"",
|
|
148216
|
+
fence,
|
|
148217
|
+
tail,
|
|
148218
|
+
fence,
|
|
148219
|
+
"",
|
|
148220
|
+
"</details>"
|
|
148221
|
+
);
|
|
148222
|
+
}
|
|
148223
|
+
return parts.join("\n");
|
|
148224
|
+
}
|
|
148225
|
+
function formatExplanation(input) {
|
|
148226
|
+
if (!input.isHang) return `The agent exited unexpectedly: ${input.errorMessage}`;
|
|
148227
|
+
const idleSec = parseIdleSec(input.errorMessage);
|
|
148228
|
+
if (idleSec === void 0) {
|
|
148229
|
+
return "The agent stopped emitting events and was killed by the activity-timeout watchdog.";
|
|
148230
|
+
}
|
|
148231
|
+
return `The agent stopped emitting events for ${idleSec}s and was killed by the activity-timeout watchdog.`;
|
|
148232
|
+
}
|
|
148233
|
+
function parseIdleSec(message) {
|
|
148234
|
+
const match3 = /no output for (\d+)s/.exec(message);
|
|
148235
|
+
return match3 ? Number(match3[1]) : void 0;
|
|
148236
|
+
}
|
|
148237
|
+
function formatEventsPart(diagnostic) {
|
|
148238
|
+
if (diagnostic.eventCount > 0) {
|
|
148239
|
+
return `${diagnostic.eventCount} events were processed before the failure.`;
|
|
148240
|
+
}
|
|
148241
|
+
if (diagnostic.lastProviderError) return "No events were emitted before the failure.";
|
|
148242
|
+
return "No events were emitted \u2014 check whether the model provider is reachable.";
|
|
148243
|
+
}
|
|
148244
|
+
function renderStderrTail(lines) {
|
|
148245
|
+
if (lines.length === 0) return "";
|
|
148246
|
+
const joined = lines.join("\n");
|
|
148247
|
+
if (joined.length <= MAX_STDERR_BYTES) return joined;
|
|
148248
|
+
return `... (older lines truncated)
|
|
148249
|
+
${joined.slice(-MAX_STDERR_BYTES)}`;
|
|
148250
|
+
}
|
|
148251
|
+
function pickFence(content) {
|
|
148252
|
+
let max = 0;
|
|
148253
|
+
for (const match3 of content.matchAll(/`+/g)) {
|
|
148254
|
+
if (match3[0].length > max) max = match3[0].length;
|
|
148255
|
+
}
|
|
148256
|
+
return "`".repeat(Math.max(3, max + 1));
|
|
148257
|
+
}
|
|
148258
|
+
function extractBillingUrl(lines) {
|
|
148259
|
+
const urlPattern = /https:\/\/(?:opencode\.ai\/[^\s"]*billing[^\s"]*|console\.anthropic\.com[^\s"]*|console\.cloud\.google\.com[^\s"]*billing[^\s"]*)/i;
|
|
148260
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
148261
|
+
const m = urlPattern.exec(lines[i] ?? "");
|
|
148262
|
+
if (m) return m[0];
|
|
148263
|
+
}
|
|
148264
|
+
return void 0;
|
|
148265
|
+
}
|
|
148266
|
+
function formatBillingExhaustedBody(diagnostic) {
|
|
148267
|
+
const headline = `**${diagnostic.label} stopped** \u2014 your model provider returned a billing-exhausted response.`;
|
|
148268
|
+
const billingUrl = extractBillingUrl(diagnostic.recentStderr);
|
|
148269
|
+
const cta = billingUrl ? `Top up your provider balance, then re-run: [${billingUrl}](${billingUrl})` : "Top up your model-provider balance (or rotate to a key with remaining credits) and re-run.";
|
|
148270
|
+
const explanation = "The agent kept retrying the request because the provider marked the failure as transient. Pullfrog's activity-timeout watchdog ended the run after no further events were emitted.";
|
|
148271
|
+
const parts = [headline, "", explanation, "", cta];
|
|
148272
|
+
const tail = renderStderrTail(diagnostic.recentStderr);
|
|
148273
|
+
if (tail) {
|
|
148274
|
+
const fence = pickFence(tail);
|
|
148275
|
+
parts.push(
|
|
148276
|
+
"",
|
|
148277
|
+
"<details><summary>Recent agent stderr</summary>",
|
|
148278
|
+
"",
|
|
148279
|
+
fence,
|
|
148280
|
+
tail,
|
|
148281
|
+
fence,
|
|
148282
|
+
"",
|
|
148283
|
+
"</details>"
|
|
148284
|
+
);
|
|
148285
|
+
}
|
|
148286
|
+
return parts.join("\n");
|
|
148287
|
+
}
|
|
148288
|
+
|
|
148289
|
+
// utils/codexHome.ts
|
|
147832
148290
|
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "node:fs";
|
|
148291
|
+
import { homedir } from "node:os";
|
|
147833
148292
|
import { join as join11 } from "node:path";
|
|
147834
|
-
|
|
148293
|
+
var CODEX_AUTH_ENV = "CODEX_AUTH_JSON";
|
|
148294
|
+
function installCodexAuth() {
|
|
148295
|
+
const raw2 = process.env[CODEX_AUTH_ENV];
|
|
148296
|
+
if (!raw2) return null;
|
|
148297
|
+
const blob = parseCodexBlob(raw2);
|
|
148298
|
+
if (!blob) {
|
|
148299
|
+
log.warning(`\xBB ${CODEX_AUTH_ENV} present but malformed; ignoring`);
|
|
148300
|
+
return null;
|
|
148301
|
+
}
|
|
148302
|
+
const xdgDataHome = join11(homedir(), ".local", "share");
|
|
148303
|
+
const opencodeDir = join11(xdgDataHome, "opencode");
|
|
148304
|
+
const authPath = join11(opencodeDir, "auth.json");
|
|
148305
|
+
const opencodeAuth = {
|
|
148306
|
+
openai: {
|
|
148307
|
+
type: "oauth",
|
|
148308
|
+
refresh: blob.tokens.refresh_token,
|
|
148309
|
+
access: blob.tokens.access_token,
|
|
148310
|
+
// expires: 0 forces OpenCode's CodexAuthPlugin to refresh on first
|
|
148311
|
+
// request (it checks `expires < Date.now()`). safest default — we
|
|
148312
|
+
// don't carry an `expires_in` from the Codex blob.
|
|
148313
|
+
expires: 0,
|
|
148314
|
+
...blob.tokens.account_id ? { accountId: blob.tokens.account_id } : {}
|
|
148315
|
+
}
|
|
148316
|
+
};
|
|
148317
|
+
mkdirSync5(opencodeDir, { recursive: true });
|
|
148318
|
+
writeFileSync8(authPath, `${JSON.stringify(opencodeAuth, null, 2)}
|
|
148319
|
+
`, { mode: 384 });
|
|
148320
|
+
log.info(`\xBB installed Codex auth at ${authPath}`);
|
|
148321
|
+
return { authPath, xdgDataHome, originalRefresh: blob.tokens.refresh_token };
|
|
148322
|
+
}
|
|
148323
|
+
function parseCodexBlob(raw2) {
|
|
148324
|
+
let parsed2;
|
|
148325
|
+
try {
|
|
148326
|
+
parsed2 = JSON.parse(raw2);
|
|
148327
|
+
} catch {
|
|
148328
|
+
return null;
|
|
148329
|
+
}
|
|
148330
|
+
if (!parsed2 || typeof parsed2 !== "object") return null;
|
|
148331
|
+
const v = parsed2;
|
|
148332
|
+
if (v.auth_mode !== "chatgpt") return null;
|
|
148333
|
+
const tokens = v.tokens;
|
|
148334
|
+
if (!tokens || typeof tokens !== "object") return null;
|
|
148335
|
+
const t = tokens;
|
|
148336
|
+
if (typeof t.access_token !== "string" || t.access_token.length === 0) return null;
|
|
148337
|
+
if (typeof t.refresh_token !== "string" || t.refresh_token.length === 0) return null;
|
|
148338
|
+
return {
|
|
148339
|
+
auth_mode: "chatgpt",
|
|
148340
|
+
tokens: {
|
|
148341
|
+
access_token: t.access_token,
|
|
148342
|
+
refresh_token: t.refresh_token,
|
|
148343
|
+
...typeof t.id_token === "string" ? { id_token: t.id_token } : {},
|
|
148344
|
+
...typeof t.account_id === "string" ? { account_id: t.account_id } : {}
|
|
148345
|
+
},
|
|
148346
|
+
...typeof v.last_refresh === "string" ? { last_refresh: v.last_refresh } : {}
|
|
148347
|
+
};
|
|
148348
|
+
}
|
|
147835
148349
|
|
|
147836
148350
|
// agents/opencodePlugin.ts
|
|
147837
148351
|
var PULLFROG_BUS_EVENT_TYPE = "pullfrog_bus_event";
|
|
@@ -147914,6 +148428,9 @@ export default async function pullfrogEventsPlugin() {
|
|
|
147914
148428
|
}
|
|
147915
148429
|
`;
|
|
147916
148430
|
|
|
148431
|
+
// agents/opencodeShared.ts
|
|
148432
|
+
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
148433
|
+
|
|
147917
148434
|
// agents/subagentModels.ts
|
|
147918
148435
|
function deriveSubagentModels(orchestratorSpec) {
|
|
147919
148436
|
if (!orchestratorSpec) return { reviewer: void 0 };
|
|
@@ -147930,68 +148447,14 @@ function deriveSubagentModels(orchestratorSpec) {
|
|
|
147930
148447
|
return { reviewer: void 0 };
|
|
147931
148448
|
}
|
|
147932
148449
|
|
|
147933
|
-
// agents/
|
|
147934
|
-
|
|
147935
|
-
return
|
|
147936
|
-
|
|
147937
|
-
|
|
147938
|
-
|
|
147939
|
-
|
|
147940
|
-
|
|
147941
|
-
}
|
|
147942
|
-
var GEMINI_3_DIRECT_THINKING_LEVEL = "medium";
|
|
147943
|
-
var GEMINI_3_DIRECT_API_IDS = ["gemini-3.1-pro-preview", "gemini-3-flash-preview"];
|
|
147944
|
-
function buildSecurityConfig(ctx, model) {
|
|
147945
|
-
const config3 = {
|
|
147946
|
-
permission: {
|
|
147947
|
-
bash: "deny",
|
|
147948
|
-
edit: "allow",
|
|
147949
|
-
read: "allow",
|
|
147950
|
-
webfetch: "allow",
|
|
147951
|
-
external_directory: "allow",
|
|
147952
|
-
skill: "allow"
|
|
147953
|
-
},
|
|
147954
|
-
mcp: {
|
|
147955
|
-
[pullfrogMcpName]: { type: "remote", url: ctx.mcpServerUrl }
|
|
147956
|
-
},
|
|
147957
|
-
agent: (() => {
|
|
147958
|
-
const cfg = buildReviewerAgentConfig(model);
|
|
147959
|
-
const reviewerModel = cfg[REVIEWER_AGENT_NAME]?.model ?? "(inherit)";
|
|
147960
|
-
log.info(`\xBB subagent models: reviewfrog=${reviewerModel}`);
|
|
147961
|
-
return cfg;
|
|
147962
|
-
})(),
|
|
147963
|
-
// opt into opencode's experimental `batch` tool (added in
|
|
147964
|
-
// anomalyco/opencode PR #2983, opt-in via `experimental.batch_tool`). it
|
|
147965
|
-
// exposes a single `batch` tool that runs 1-25 independent tool calls
|
|
147966
|
-
// (read/grep/glob/bash/etc.) concurrently in one assistant turn, which
|
|
147967
|
-
// collapses the dominant grep→20×read pattern into a single round trip.
|
|
147968
|
-
// edits are explicitly disallowed inside the batch upstream. paired with
|
|
147969
|
-
// the "Parallel tool execution" guidance in utils/instructions.ts so the
|
|
147970
|
-
// model actually reaches for it. see wiki/prompt.md.
|
|
147971
|
-
experimental: { batch_tool: true },
|
|
147972
|
-
provider: {
|
|
147973
|
-
google: {
|
|
147974
|
-
models: Object.fromEntries(
|
|
147975
|
-
GEMINI_3_DIRECT_API_IDS.map((id) => [
|
|
147976
|
-
id,
|
|
147977
|
-
{
|
|
147978
|
-
options: {
|
|
147979
|
-
thinkingConfig: { thinkingLevel: GEMINI_3_DIRECT_THINKING_LEVEL }
|
|
147980
|
-
}
|
|
147981
|
-
}
|
|
147982
|
-
])
|
|
147983
|
-
)
|
|
147984
|
-
}
|
|
147985
|
-
}
|
|
147986
|
-
};
|
|
147987
|
-
if (model) {
|
|
147988
|
-
config3.model = model;
|
|
147989
|
-
const slashIndex = model.indexOf("/");
|
|
147990
|
-
if (slashIndex > 0) {
|
|
147991
|
-
config3.enabled_providers = [model.slice(0, slashIndex).toLowerCase()];
|
|
147992
|
-
}
|
|
147993
|
-
}
|
|
147994
|
-
return JSON.stringify(config3);
|
|
148450
|
+
// agents/opencodeShared.ts
|
|
148451
|
+
function geminiHighThinkingOverrides() {
|
|
148452
|
+
return Object.fromEntries(
|
|
148453
|
+
modelAliases.filter((a) => a.provider === "google").map((a) => [
|
|
148454
|
+
a.resolve.replace(/^google\//, ""),
|
|
148455
|
+
{ options: { thinkingConfig: { thinkingLevel: "high" } } }
|
|
148456
|
+
])
|
|
148457
|
+
);
|
|
147995
148458
|
}
|
|
147996
148459
|
function buildReviewerAgentConfig(orchestratorModel) {
|
|
147997
148460
|
const overrides = deriveSubagentModels(orchestratorModel);
|
|
@@ -148004,6 +148467,15 @@ function buildReviewerAgentConfig(orchestratorModel) {
|
|
|
148004
148467
|
}
|
|
148005
148468
|
};
|
|
148006
148469
|
}
|
|
148470
|
+
async function installOpencodeCli(params) {
|
|
148471
|
+
return await installFromNpmTarball({
|
|
148472
|
+
packageName: "opencode-ai",
|
|
148473
|
+
version: getDevDependencyVersion("opencode-ai"),
|
|
148474
|
+
executablePath: params.binPath,
|
|
148475
|
+
installDependencies: true
|
|
148476
|
+
});
|
|
148477
|
+
}
|
|
148478
|
+
var AUTO_SELECT_WARNING = "select a model explicitly in the Pullfrog console (https://pullfrog.com/console) to avoid this.";
|
|
148007
148479
|
function getOpenCodeModels(cliPath) {
|
|
148008
148480
|
try {
|
|
148009
148481
|
const output = execFileSync4(cliPath, ["models"], {
|
|
@@ -148019,7 +148491,6 @@ function getOpenCodeModels(cliPath) {
|
|
|
148019
148491
|
return [];
|
|
148020
148492
|
}
|
|
148021
148493
|
}
|
|
148022
|
-
var AUTO_SELECT_WARNING = "select a model explicitly in the Pullfrog console (https://pullfrog.com/console) to avoid this.";
|
|
148023
148494
|
function autoSelectModel(cliPath) {
|
|
148024
148495
|
const availableModels = getOpenCodeModels(cliPath);
|
|
148025
148496
|
const availableSet = new Set(availableModels);
|
|
@@ -148040,6 +148511,58 @@ function autoSelectModel(cliPath) {
|
|
|
148040
148511
|
log.warning(`\xBB no model resolved. letting OpenCode auto-select. ${AUTO_SELECT_WARNING}`);
|
|
148041
148512
|
return void 0;
|
|
148042
148513
|
}
|
|
148514
|
+
|
|
148515
|
+
// agents/opencode_v2.ts
|
|
148516
|
+
var installCli = () => installOpencodeCli({ binPath: "bin/opencode.exe" });
|
|
148517
|
+
function buildSecurityConfig(ctx, model) {
|
|
148518
|
+
const config3 = {
|
|
148519
|
+
permission: {
|
|
148520
|
+
bash: "deny",
|
|
148521
|
+
edit: "allow",
|
|
148522
|
+
read: "allow",
|
|
148523
|
+
webfetch: "allow",
|
|
148524
|
+
external_directory: "allow",
|
|
148525
|
+
skill: "allow"
|
|
148526
|
+
},
|
|
148527
|
+
mcp: {
|
|
148528
|
+
[pullfrogMcpName]: { type: "remote", url: ctx.mcpServerUrl }
|
|
148529
|
+
},
|
|
148530
|
+
agent: (() => {
|
|
148531
|
+
const cfg = buildReviewerAgentConfig(model);
|
|
148532
|
+
const reviewerModel = cfg[REVIEWER_AGENT_NAME]?.model ?? "(inherit)";
|
|
148533
|
+
log.info(`\xBB subagent models: reviewfrog=${reviewerModel}`);
|
|
148534
|
+
return cfg;
|
|
148535
|
+
})(),
|
|
148536
|
+
// NB: `experimental.batch_tool: true` was opt-in at v1.4.x but is
|
|
148537
|
+
// declared-but-inert at v1.15.0 — the schema accepts it (`config/config.ts`)
|
|
148538
|
+
// and the SDK exposes the type, but no runtime call site reads it. removed
|
|
148539
|
+
// here to avoid carrying dead config; re-add when upstream wires the batch
|
|
148540
|
+
// tool back. see wiki/prompt.md and the v2 plan doc for the audit trail.
|
|
148541
|
+
//
|
|
148542
|
+
// gemini-3 thinking pinned to high for review depth; gpt and anthropic
|
|
148543
|
+
// effort set elsewhere (gpt: upstream default, anthropic: --effort flag in claude.ts).
|
|
148544
|
+
provider: { google: { models: geminiHighThinkingOverrides() } }
|
|
148545
|
+
};
|
|
148546
|
+
if (model) {
|
|
148547
|
+
config3.model = model;
|
|
148548
|
+
const slashIndex = model.indexOf("/");
|
|
148549
|
+
if (slashIndex > 0) {
|
|
148550
|
+
config3.enabled_providers = [model.slice(0, slashIndex).toLowerCase()];
|
|
148551
|
+
}
|
|
148552
|
+
}
|
|
148553
|
+
return JSON.stringify(config3);
|
|
148554
|
+
}
|
|
148555
|
+
function formatPartDuration(time4) {
|
|
148556
|
+
if (!time4 || typeof time4.start !== "number" || typeof time4.end !== "number") return "";
|
|
148557
|
+
if (time4.end <= time4.start) return "";
|
|
148558
|
+
return ` (${((time4.end - time4.start) / 1e3).toFixed(1)}s)`;
|
|
148559
|
+
}
|
|
148560
|
+
function terminalPayload(state) {
|
|
148561
|
+
if (!state) return void 0;
|
|
148562
|
+
if (state.status === "completed") return state.output;
|
|
148563
|
+
if (state.status === "error") return state.error;
|
|
148564
|
+
return void 0;
|
|
148565
|
+
}
|
|
148043
148566
|
async function runOpenCode(params) {
|
|
148044
148567
|
const startTime = performance7.now();
|
|
148045
148568
|
let eventCount = 0;
|
|
@@ -148048,9 +148571,10 @@ async function runOpenCode(params) {
|
|
|
148048
148571
|
let accumulatedCostUsd = 0;
|
|
148049
148572
|
let tokensLogged = false;
|
|
148050
148573
|
const toolCallTimings = /* @__PURE__ */ new Map();
|
|
148051
|
-
let
|
|
148052
|
-
|
|
148053
|
-
let
|
|
148574
|
+
let lastEventAt = performance7.now();
|
|
148575
|
+
const recentStderr = [];
|
|
148576
|
+
let lastProviderError = null;
|
|
148577
|
+
let agentErrorEvent = null;
|
|
148054
148578
|
const labeler = new SessionLabeler();
|
|
148055
148579
|
function eventLabel(event) {
|
|
148056
148580
|
const sid = event.sessionID ?? event.session_id;
|
|
@@ -148059,30 +148583,15 @@ async function runOpenCode(params) {
|
|
|
148059
148583
|
function withLabel(label, message) {
|
|
148060
148584
|
return label === ORCHESTRATOR_LABEL ? message : formatWithLabel(label, message);
|
|
148061
148585
|
}
|
|
148062
|
-
const thinkingTimers = /* @__PURE__ */ new Map();
|
|
148063
|
-
function timerFor(label) {
|
|
148064
|
-
let t = thinkingTimers.get(label);
|
|
148065
|
-
if (!t) {
|
|
148066
|
-
const formatLine = (line) => label === ORCHESTRATOR_LABEL ? line : formatWithLabel(label, line);
|
|
148067
|
-
t = new ThinkingTimer(formatLine);
|
|
148068
|
-
thinkingTimers.set(label, t);
|
|
148069
|
-
}
|
|
148070
|
-
return t;
|
|
148071
|
-
}
|
|
148072
148586
|
const taskDispatchByCallID = /* @__PURE__ */ new Map();
|
|
148073
|
-
|
|
148074
|
-
const knownNonTaskCallIDs = /* @__PURE__ */ new Set();
|
|
148075
|
-
function emitSubagentFinished(dispatch, status, output2, matchKind) {
|
|
148587
|
+
function emitSubagentFinished(dispatch, status, output2) {
|
|
148076
148588
|
const subagentDuration = performance7.now() - dispatch.startedAt;
|
|
148077
148589
|
const outputStr = typeof output2 === "string" ? output2 : "";
|
|
148078
148590
|
const outputPreview = outputStr.length > 120 ? `${outputStr.slice(0, 120)}\u2026` : outputStr;
|
|
148079
|
-
const matchSuffix = matchKind === "fifo" ? " [fifo-matched]" : "";
|
|
148080
148591
|
log.info(
|
|
148081
|
-
`\xBB subagent finished: ${dispatch.label} (${(subagentDuration / 1e3).toFixed(1)}s, status=${status})
|
|
148592
|
+
`\xBB subagent finished: ${dispatch.label} (${(subagentDuration / 1e3).toFixed(1)}s, status=${status})` + (outputPreview ? ` \u2014 ${outputPreview.replace(/\n/g, " ")}` : "")
|
|
148082
148593
|
);
|
|
148083
148594
|
taskDispatchByCallID.delete(dispatch.toolUseCallID);
|
|
148084
|
-
const idx = pendingTaskDispatches.indexOf(dispatch);
|
|
148085
|
-
if (idx >= 0) pendingTaskDispatches.splice(idx, 1);
|
|
148086
148595
|
}
|
|
148087
148596
|
function buildUsage() {
|
|
148088
148597
|
const totalInput = accumulatedTokens.input + accumulatedTokens.cacheRead + accumulatedTokens.cacheWrite;
|
|
@@ -148096,55 +148605,6 @@ async function runOpenCode(params) {
|
|
|
148096
148605
|
} : void 0;
|
|
148097
148606
|
}
|
|
148098
148607
|
const handlers2 = {
|
|
148099
|
-
init: (event) => {
|
|
148100
|
-
const label = labeler.labelFor(event.session_id ?? null);
|
|
148101
|
-
log.debug(
|
|
148102
|
-
withLabel(
|
|
148103
|
-
label,
|
|
148104
|
-
`\xBB ${params.label} init: session_id=${event.session_id || "unknown"}, model=${event.model || "unknown"}`
|
|
148105
|
-
)
|
|
148106
|
-
);
|
|
148107
|
-
log.debug(withLabel(label, `\xBB ${params.label} init event (full): ${JSON.stringify(event)}`));
|
|
148108
|
-
if (label === ORCHESTRATOR_LABEL) {
|
|
148109
|
-
finalOutput = "";
|
|
148110
|
-
accumulatedTokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
148111
|
-
accumulatedCostUsd = 0;
|
|
148112
|
-
tokensLogged = false;
|
|
148113
|
-
} else {
|
|
148114
|
-
log.info(`\xBB ${params.label} subagent init: ${label} (session ${event.session_id || "?"})`);
|
|
148115
|
-
}
|
|
148116
|
-
},
|
|
148117
|
-
message: (event) => {
|
|
148118
|
-
const label = eventLabel(event);
|
|
148119
|
-
if (event.role === "assistant" && event.content?.trim()) {
|
|
148120
|
-
const message = event.content.trim();
|
|
148121
|
-
if (event.delta) {
|
|
148122
|
-
log.debug(
|
|
148123
|
-
withLabel(
|
|
148124
|
-
label,
|
|
148125
|
-
`\xBB ${params.label} thinking: ${message.substring(0, 300)}${message.length > 300 ? "..." : ""}`
|
|
148126
|
-
)
|
|
148127
|
-
);
|
|
148128
|
-
} else {
|
|
148129
|
-
log.debug(
|
|
148130
|
-
withLabel(
|
|
148131
|
-
label,
|
|
148132
|
-
`\xBB ${params.label} message (${event.role}): ${message.substring(0, 100)}${message.length > 100 ? "..." : ""}`
|
|
148133
|
-
)
|
|
148134
|
-
);
|
|
148135
|
-
if (label === ORCHESTRATOR_LABEL) {
|
|
148136
|
-
finalOutput = message;
|
|
148137
|
-
}
|
|
148138
|
-
}
|
|
148139
|
-
} else if (event.role === "user") {
|
|
148140
|
-
log.debug(
|
|
148141
|
-
withLabel(
|
|
148142
|
-
label,
|
|
148143
|
-
`\xBB ${params.label} message (${event.role}): ${event.content?.substring(0, 100) || ""}${event.content && event.content.length > 100 ? "..." : ""}`
|
|
148144
|
-
)
|
|
148145
|
-
);
|
|
148146
|
-
}
|
|
148147
|
-
},
|
|
148148
148608
|
text: (event) => {
|
|
148149
148609
|
if (event.part?.text?.trim()) {
|
|
148150
148610
|
const message = event.part.text.trim();
|
|
@@ -148156,124 +148616,90 @@ async function runOpenCode(params) {
|
|
|
148156
148616
|
}
|
|
148157
148617
|
}
|
|
148158
148618
|
},
|
|
148159
|
-
|
|
148160
|
-
|
|
148161
|
-
|
|
148162
|
-
|
|
148163
|
-
|
|
148164
|
-
|
|
148619
|
+
/**
|
|
148620
|
+
* Reasoning blocks (only emitted when `--thinking` is set in baseArgs).
|
|
148621
|
+
* `part.time.{start,end}` give us a precise duration from opencode
|
|
148622
|
+
* itself. Not folded into `finalOutput` — that's the final answer,
|
|
148623
|
+
* not inner monologue.
|
|
148624
|
+
*/
|
|
148625
|
+
reasoning: (event) => {
|
|
148626
|
+
const text = event.part?.text?.trim();
|
|
148627
|
+
if (!text) return;
|
|
148628
|
+
const label = eventLabel(event);
|
|
148629
|
+
const durationStr = formatPartDuration(event.part?.time);
|
|
148630
|
+
const preview = text.length > 280 ? `${text.slice(0, 280)}\u2026` : text;
|
|
148631
|
+
log.info(withLabel(label, `\xBB thinking${durationStr}: ${preview.replace(/\n+/g, " ")}`));
|
|
148632
|
+
if (text.length > 280) {
|
|
148633
|
+
log.debug(withLabel(label, `\xBB thinking (full): ${text}`));
|
|
148634
|
+
}
|
|
148635
|
+
},
|
|
148636
|
+
// step_start carries no information we surface today (token / cost are
|
|
148637
|
+
// reported on step_finish). explicit no-op so the dispatcher doesn't
|
|
148638
|
+
// log "unhandled event" for every step.
|
|
148639
|
+
step_start: () => {
|
|
148165
148640
|
},
|
|
148166
|
-
step_finish:
|
|
148167
|
-
const
|
|
148168
|
-
|
|
148169
|
-
|
|
148170
|
-
accumulatedTokens.
|
|
148171
|
-
accumulatedTokens.
|
|
148172
|
-
accumulatedTokens.
|
|
148173
|
-
accumulatedTokens.cacheWrite += eventTokens.cache?.write || 0;
|
|
148641
|
+
step_finish: (event) => {
|
|
148642
|
+
const t = event.part?.tokens;
|
|
148643
|
+
if (t) {
|
|
148644
|
+
accumulatedTokens.input += t.input || 0;
|
|
148645
|
+
accumulatedTokens.output += t.output || 0;
|
|
148646
|
+
accumulatedTokens.cacheRead += t.cache?.read || 0;
|
|
148647
|
+
accumulatedTokens.cacheWrite += t.cache?.write || 0;
|
|
148174
148648
|
}
|
|
148175
148649
|
if (typeof event.part?.cost === "number" && Number.isFinite(event.part.cost)) {
|
|
148176
148650
|
accumulatedCostUsd += event.part.cost;
|
|
148177
148651
|
}
|
|
148178
|
-
if (currentStepId === stepId) {
|
|
148179
|
-
currentStepId = null;
|
|
148180
|
-
currentStepType = null;
|
|
148181
|
-
}
|
|
148182
148652
|
},
|
|
148653
|
+
/**
|
|
148654
|
+
* Tool lifecycle event — at v1.15 a single event covers both completed
|
|
148655
|
+
* and error terminal states (read `part.state.status`). Subagent tool
|
|
148656
|
+
* parts arrive here via the bus-envelope re-emit too.
|
|
148657
|
+
*/
|
|
148183
148658
|
tool_use: (event) => {
|
|
148184
148659
|
const toolName = event.part?.tool;
|
|
148185
148660
|
const toolId = event.part?.callID;
|
|
148661
|
+
const state = event.part?.state;
|
|
148186
148662
|
if (!toolName || !toolId) {
|
|
148187
148663
|
log.info(
|
|
148188
148664
|
`\xBB tool_use event missing toolName or toolId: ${JSON.stringify(event).substring(0, 500)}`
|
|
148189
148665
|
);
|
|
148190
148666
|
return;
|
|
148191
148667
|
}
|
|
148192
|
-
|
|
148193
|
-
|
|
148194
|
-
const taskInput = event.part?.state?.input ?? {};
|
|
148195
|
-
const dispatchedLabel = labeler.recordTaskDispatch(taskInput);
|
|
148196
|
-
const dispatch = {
|
|
148197
|
-
label: dispatchedLabel,
|
|
148198
|
-
startedAt: performance7.now(),
|
|
148199
|
-
toolUseCallID: toolId
|
|
148200
|
-
};
|
|
148201
|
-
taskDispatchByCallID.set(toolId, dispatch);
|
|
148202
|
-
pendingTaskDispatches.push(dispatch);
|
|
148203
|
-
log.info(
|
|
148204
|
-
`\xBB dispatching subagent: ${dispatchedLabel}` + (taskInput.subagent_type ? ` (subagent_type=${taskInput.subagent_type})` : "")
|
|
148205
|
-
);
|
|
148206
|
-
}
|
|
148207
|
-
} else {
|
|
148208
|
-
knownNonTaskCallIDs.add(toolId);
|
|
148209
|
-
}
|
|
148668
|
+
const status = state?.status;
|
|
148669
|
+
const isTerminal2 = status === "completed" || status === "error";
|
|
148210
148670
|
const label = eventLabel(event);
|
|
148211
|
-
if (
|
|
148212
|
-
|
|
148213
|
-
|
|
148214
|
-
|
|
148215
|
-
|
|
148216
|
-
|
|
148217
|
-
|
|
148671
|
+
if (toolName === "task" && !taskDispatchByCallID.has(toolId)) {
|
|
148672
|
+
const taskInput = state?.input ?? {};
|
|
148673
|
+
const dispatchedLabel = labeler.recordTaskDispatch(taskInput);
|
|
148674
|
+
taskDispatchByCallID.set(toolId, {
|
|
148675
|
+
label: dispatchedLabel,
|
|
148676
|
+
startedAt: performance7.now(),
|
|
148677
|
+
toolUseCallID: toolId
|
|
148218
148678
|
});
|
|
148679
|
+
log.info(
|
|
148680
|
+
`\xBB dispatching subagent: ${dispatchedLabel}` + (taskInput.subagent_type ? ` (subagent_type=${taskInput.subagent_type})` : "")
|
|
148681
|
+
);
|
|
148219
148682
|
}
|
|
148220
|
-
|
|
148221
|
-
|
|
148222
|
-
|
|
148223
|
-
log.info(withLabel(label, toolCallLine));
|
|
148224
|
-
if (event.part?.state?.status === "completed" && event.part.state.output) {
|
|
148225
|
-
log.debug(withLabel(label, ` output: ${event.part.state.output}`));
|
|
148683
|
+
params.onToolUse?.({ toolName, input: state?.input });
|
|
148684
|
+
if (!toolCallTimings.has(toolId)) {
|
|
148685
|
+
toolCallTimings.set(toolId, performance7.now());
|
|
148226
148686
|
}
|
|
148227
|
-
|
|
148228
|
-
|
|
148229
|
-
|
|
148687
|
+
const inputFormatted = formatJsonValue(state?.input || {});
|
|
148688
|
+
const callLine = inputFormatted !== "{}" ? `\xBB ${toolName}(${inputFormatted})` : `\xBB ${toolName}()`;
|
|
148689
|
+
log.info(withLabel(label, callLine));
|
|
148690
|
+
if (state?.status === "completed") {
|
|
148691
|
+
log.debug(withLabel(label, ` output: ${state.output}`));
|
|
148230
148692
|
}
|
|
148231
|
-
if (
|
|
148232
|
-
log.
|
|
148233
|
-
params.todoTracker.cancel();
|
|
148693
|
+
if (state?.status === "error") {
|
|
148694
|
+
log.info(withLabel(label, `\xBB tool call failed: ${state.error}`));
|
|
148234
148695
|
}
|
|
148235
|
-
if (
|
|
148236
|
-
|
|
148237
|
-
|
|
148238
|
-
},
|
|
148239
|
-
tool_result: (event) => {
|
|
148240
|
-
const toolId = event.part?.callID || event.tool_id;
|
|
148241
|
-
const status = event.part?.state?.status || event.status || "unknown";
|
|
148242
|
-
const output2 = event.part?.state?.output || event.output;
|
|
148243
|
-
const label = eventLabel(event);
|
|
148244
|
-
timerFor(label).markToolResult();
|
|
148245
|
-
if (taskDispatchByCallID.size > 0 || pendingTaskDispatches.length > 0) {
|
|
148246
|
-
if (toolId && taskDispatchByCallID.has(toolId)) {
|
|
148247
|
-
const dispatch = taskDispatchByCallID.get(toolId);
|
|
148248
|
-
if (dispatch) emitSubagentFinished(dispatch, status, output2, "exact");
|
|
148249
|
-
} else {
|
|
148250
|
-
const callIDIsKnownNonTask = toolId ? knownNonTaskCallIDs.has(toolId) : false;
|
|
148251
|
-
if (!callIDIsKnownNonTask && pendingTaskDispatches.length > 0) {
|
|
148252
|
-
const dispatch = pendingTaskDispatches[0];
|
|
148253
|
-
emitSubagentFinished(dispatch, status, output2, "fifo");
|
|
148254
|
-
}
|
|
148255
|
-
}
|
|
148256
|
-
}
|
|
148257
|
-
if (toolId) {
|
|
148696
|
+
if (isTerminal2) {
|
|
148697
|
+
const dispatch = toolName === "task" ? taskDispatchByCallID.get(toolId) : void 0;
|
|
148698
|
+
if (dispatch) emitSubagentFinished(dispatch, status, terminalPayload(state));
|
|
148258
148699
|
const toolStartTime = toolCallTimings.get(toolId);
|
|
148259
|
-
if (toolStartTime) {
|
|
148700
|
+
if (toolStartTime !== void 0) {
|
|
148260
148701
|
const toolDuration = performance7.now() - toolStartTime;
|
|
148261
148702
|
toolCallTimings.delete(toolId);
|
|
148262
|
-
const stepContext = currentStepId ? ` (step=${currentStepType || "unknown"})` : "";
|
|
148263
|
-
log.debug(
|
|
148264
|
-
withLabel(
|
|
148265
|
-
label,
|
|
148266
|
-
`\xBB ${params.label} tool_result${stepContext}: id=${toolId}, status=${status}, duration=${Math.round(toolDuration)}ms`
|
|
148267
|
-
)
|
|
148268
|
-
);
|
|
148269
|
-
if (output2) {
|
|
148270
|
-
log.debug(
|
|
148271
|
-
withLabel(
|
|
148272
|
-
label,
|
|
148273
|
-
` output: ${typeof output2 === "string" ? output2 : JSON.stringify(output2)}`
|
|
148274
|
-
)
|
|
148275
|
-
);
|
|
148276
|
-
}
|
|
148277
148703
|
if (toolDuration > 5e3) {
|
|
148278
148704
|
log.info(
|
|
148279
148705
|
withLabel(
|
|
@@ -148284,12 +148710,12 @@ async function runOpenCode(params) {
|
|
|
148284
148710
|
}
|
|
148285
148711
|
}
|
|
148286
148712
|
}
|
|
148287
|
-
if (
|
|
148288
|
-
|
|
148289
|
-
|
|
148290
|
-
}
|
|
148291
|
-
|
|
148292
|
-
|
|
148713
|
+
if (toolName.includes("report_progress") && params.todoTracker) {
|
|
148714
|
+
log.debug("\xBB report_progress detected, disabling todo tracking");
|
|
148715
|
+
params.todoTracker.cancel();
|
|
148716
|
+
}
|
|
148717
|
+
if (toolName === "todowrite" && params.todoTracker?.enabled && isTerminal2) {
|
|
148718
|
+
params.todoTracker.update(state?.input);
|
|
148293
148719
|
}
|
|
148294
148720
|
},
|
|
148295
148721
|
error: (event) => {
|
|
@@ -148298,23 +148724,18 @@ async function runOpenCode(params) {
|
|
|
148298
148724
|
const errorMessage = event.error?.data?.message || event.error?.name || JSON.stringify(event);
|
|
148299
148725
|
log.info(`\xBB ${params.label} error event: ${errorName}: ${errorMessage}`);
|
|
148300
148726
|
},
|
|
148301
|
-
|
|
148302
|
-
|
|
148303
|
-
|
|
148304
|
-
|
|
148305
|
-
|
|
148306
|
-
|
|
148307
|
-
|
|
148308
|
-
|
|
148309
|
-
|
|
148310
|
-
|
|
148311
|
-
|
|
148312
|
-
|
|
148313
|
-
logTokenTable({ ...accumulatedTokens, costUsd: accumulatedCostUsd });
|
|
148314
|
-
tokensLogged = true;
|
|
148315
|
-
}
|
|
148316
|
-
}
|
|
148317
|
-
},
|
|
148727
|
+
/**
|
|
148728
|
+
* Bus envelope (re-emitted by `opencodePlugin.ts`). Synthesizes a
|
|
148729
|
+
* CLI-style event for each part type and routes it through the
|
|
148730
|
+
* orchestrator's handlers — same labeling / attribution / logging path.
|
|
148731
|
+
* Mirrors the dispatch in upstream's `cli/cmd/run.ts` `loop()`.
|
|
148732
|
+
*
|
|
148733
|
+
* NOT routed: subagent `step-start` / `step-finish`. step_finish carries
|
|
148734
|
+
* `tokens` and `cost` that the orchestrator's handler folds into run-wide
|
|
148735
|
+
* accumulators — double-counting subagent tokens would inflate usage
|
|
148736
|
+
* telemetry. text/tool_use already gate on ORCHESTRATOR_LABEL inside their
|
|
148737
|
+
* handlers for the same reason.
|
|
148738
|
+
*/
|
|
148318
148739
|
[PULLFROG_BUS_EVENT_TYPE]: async (event) => {
|
|
148319
148740
|
const busEvent = event.bus_event;
|
|
148320
148741
|
if (!busEvent || busEvent.type !== "message.part.updated") return;
|
|
@@ -148324,20 +148745,15 @@ async function runOpenCode(params) {
|
|
|
148324
148745
|
const partType = part.type;
|
|
148325
148746
|
if (partType === "tool") {
|
|
148326
148747
|
const status = part.state?.status;
|
|
148327
|
-
|
|
148328
|
-
|
|
148329
|
-
|
|
148330
|
-
const callID = partWithToolFields.callID;
|
|
148331
|
-
if (typeof callID === "string" && !taskDispatchByCallID.has(callID)) {
|
|
148332
|
-
const taskInput = partWithToolFields.state?.input ?? {};
|
|
148748
|
+
if (part.tool === "task" && status === "running" && part.callID) {
|
|
148749
|
+
if (!taskDispatchByCallID.has(part.callID)) {
|
|
148750
|
+
const taskInput = part.state?.input ?? {};
|
|
148333
148751
|
const dispatchedLabel = labeler.recordTaskDispatch(taskInput);
|
|
148334
|
-
|
|
148752
|
+
taskDispatchByCallID.set(part.callID, {
|
|
148335
148753
|
label: dispatchedLabel,
|
|
148336
148754
|
startedAt: performance7.now(),
|
|
148337
|
-
toolUseCallID: callID
|
|
148338
|
-
};
|
|
148339
|
-
taskDispatchByCallID.set(callID, dispatch);
|
|
148340
|
-
pendingTaskDispatches.push(dispatch);
|
|
148755
|
+
toolUseCallID: part.callID
|
|
148756
|
+
});
|
|
148341
148757
|
log.info(
|
|
148342
148758
|
`\xBB dispatching subagent: ${dispatchedLabel}` + (taskInput.subagent_type ? ` (subagent_type=${taskInput.subagent_type})` : "")
|
|
148343
148759
|
);
|
|
@@ -148345,27 +148761,26 @@ async function runOpenCode(params) {
|
|
|
148345
148761
|
return;
|
|
148346
148762
|
}
|
|
148347
148763
|
if (status !== "completed" && status !== "error") return;
|
|
148348
|
-
await handlers2.tool_use({
|
|
148349
|
-
type: "tool_use",
|
|
148350
|
-
sessionID,
|
|
148351
|
-
part
|
|
148352
|
-
});
|
|
148764
|
+
await handlers2.tool_use({ type: "tool_use", sessionID, part });
|
|
148353
148765
|
return;
|
|
148354
148766
|
}
|
|
148355
148767
|
if (partType === "step-start" || partType === "step-finish") return;
|
|
148356
148768
|
if (partType === "text" && part.time?.end !== void 0) {
|
|
148357
|
-
|
|
148358
|
-
type: "text",
|
|
148359
|
-
sessionID,
|
|
148360
|
-
part
|
|
148361
|
-
});
|
|
148769
|
+
handlers2.text({ type: "text", sessionID, part });
|
|
148362
148770
|
return;
|
|
148363
148771
|
}
|
|
148772
|
+
if (partType === "reasoning" && part.time?.end !== void 0) {
|
|
148773
|
+
handlers2.reasoning({ type: "reasoning", sessionID, part });
|
|
148774
|
+
}
|
|
148364
148775
|
}
|
|
148365
148776
|
};
|
|
148366
|
-
const
|
|
148367
|
-
|
|
148368
|
-
|
|
148777
|
+
const diagnostic = {
|
|
148778
|
+
label: params.label,
|
|
148779
|
+
recentStderr,
|
|
148780
|
+
lastProviderError: void 0,
|
|
148781
|
+
eventCount: 0
|
|
148782
|
+
};
|
|
148783
|
+
params.toolState.agentDiagnostic = diagnostic;
|
|
148369
148784
|
const output = new TailBuffer(DEFAULT_MAX_RETAINED_BYTES);
|
|
148370
148785
|
let stdoutBuffer = "";
|
|
148371
148786
|
try {
|
|
@@ -148414,16 +148829,17 @@ async function runOpenCode(params) {
|
|
|
148414
148829
|
continue;
|
|
148415
148830
|
}
|
|
148416
148831
|
eventCount++;
|
|
148832
|
+
diagnostic.eventCount = eventCount;
|
|
148417
148833
|
log.debug(JSON.stringify(event, null, 2));
|
|
148418
|
-
const
|
|
148419
|
-
if (
|
|
148834
|
+
const idleMs = performance7.now() - lastEventAt;
|
|
148835
|
+
if (idleMs > 1e4) {
|
|
148420
148836
|
const activeToolCalls = toolCallTimings.size;
|
|
148421
148837
|
const toolCallInfo = activeToolCalls > 0 ? ` (waiting for ${activeToolCalls} tool call${activeToolCalls > 1 ? "s" : ""})` : ` (${params.label} may be processing internally - LLM calls, planning, etc.)`;
|
|
148422
148838
|
log.info(
|
|
148423
|
-
`\xBB no activity for ${(
|
|
148839
|
+
`\xBB no activity for ${(idleMs / 1e3).toFixed(1)}s${toolCallInfo} (${eventCount} events processed so far)`
|
|
148424
148840
|
);
|
|
148425
148841
|
}
|
|
148426
|
-
|
|
148842
|
+
lastEventAt = performance7.now();
|
|
148427
148843
|
const handler2 = handlers2[event.type];
|
|
148428
148844
|
if (!handler2) {
|
|
148429
148845
|
log.info(
|
|
@@ -148445,10 +148861,11 @@ async function runOpenCode(params) {
|
|
|
148445
148861
|
if (!trimmed) return;
|
|
148446
148862
|
recentStderr.push(trimmed);
|
|
148447
148863
|
if (recentStderr.length > MAX_STDERR_LINES) recentStderr.shift();
|
|
148448
|
-
const
|
|
148449
|
-
if (
|
|
148450
|
-
lastProviderError =
|
|
148451
|
-
|
|
148864
|
+
const match3 = findProviderErrorMatch(trimmed);
|
|
148865
|
+
if (match3) {
|
|
148866
|
+
lastProviderError = match3.label;
|
|
148867
|
+
diagnostic.lastProviderError = match3.label;
|
|
148868
|
+
log.info(`\xBB provider error detected (${match3.label}): ${match3.excerpt}`);
|
|
148452
148869
|
} else {
|
|
148453
148870
|
log.debug(trimmed);
|
|
148454
148871
|
}
|
|
@@ -148459,14 +148876,13 @@ async function runOpenCode(params) {
|
|
|
148459
148876
|
} else {
|
|
148460
148877
|
params.todoTracker?.cancel();
|
|
148461
148878
|
}
|
|
148462
|
-
if (
|
|
148463
|
-
for (const dispatch of
|
|
148879
|
+
if (taskDispatchByCallID.size > 0) {
|
|
148880
|
+
for (const dispatch of taskDispatchByCallID.values()) {
|
|
148464
148881
|
const elapsed = performance7.now() - dispatch.startedAt;
|
|
148465
148882
|
log.info(
|
|
148466
|
-
`\xBB subagent finished (inferred at run-end): ${dispatch.label} (\u2264${(elapsed / 1e3).toFixed(1)}s) \u2014 no
|
|
148883
|
+
`\xBB subagent finished (inferred at run-end): ${dispatch.label} (\u2264${(elapsed / 1e3).toFixed(1)}s) \u2014 no terminal tool_use observed; reply likely arrived via assistant message`
|
|
148467
148884
|
);
|
|
148468
148885
|
}
|
|
148469
|
-
pendingTaskDispatches.length = 0;
|
|
148470
148886
|
taskDispatchByCallID.clear();
|
|
148471
148887
|
}
|
|
148472
148888
|
const duration4 = performance7.now() - startTime;
|
|
@@ -148538,32 +148954,33 @@ ${stderrContext}`);
|
|
|
148538
148954
|
`\xBB recent stderr (last ${Math.min(recentStderr.length, 10)} lines):
|
|
148539
148955
|
${stderrContext}`
|
|
148540
148956
|
);
|
|
148957
|
+
const body = formatAgentHangBody({ diagnostic, isHang: isActivityTimeout, errorMessage });
|
|
148541
148958
|
return {
|
|
148542
148959
|
success: false,
|
|
148543
148960
|
output: finalOutput || output.toString(),
|
|
148544
|
-
error: `${errorMessage} [${diagnosis}]`,
|
|
148961
|
+
error: body ?? `${errorMessage} [${diagnosis}]`,
|
|
148545
148962
|
usage: buildUsage()
|
|
148546
148963
|
};
|
|
148547
148964
|
}
|
|
148548
148965
|
}
|
|
148549
148966
|
var opencode = agent({
|
|
148550
148967
|
name: "opencode",
|
|
148551
|
-
install:
|
|
148968
|
+
install: installCli,
|
|
148552
148969
|
run: async (ctx) => {
|
|
148553
|
-
const cliPath = await
|
|
148970
|
+
const cliPath = await installCli();
|
|
148554
148971
|
const rawModel = ctx.payload.proxyModel ?? ctx.resolvedModel ?? autoSelectModel(cliPath);
|
|
148555
148972
|
const bedrockModelId = process.env[BEDROCK_MODEL_ID_ENV]?.trim();
|
|
148556
148973
|
const isBedrockRoute = rawModel !== void 0 && bedrockModelId !== void 0 && bedrockModelId === rawModel;
|
|
148557
148974
|
const model = isBedrockRoute ? `amazon-bedrock/${rawModel}` : rawModel;
|
|
148558
148975
|
const homeEnv = {
|
|
148559
148976
|
HOME: ctx.tmpdir,
|
|
148560
|
-
XDG_CONFIG_HOME:
|
|
148977
|
+
XDG_CONFIG_HOME: join12(ctx.tmpdir, ".config")
|
|
148561
148978
|
};
|
|
148562
|
-
|
|
148563
|
-
const opencodePluginDir =
|
|
148564
|
-
|
|
148565
|
-
|
|
148566
|
-
|
|
148979
|
+
mkdirSync6(join12(homeEnv.XDG_CONFIG_HOME, "opencode"), { recursive: true });
|
|
148980
|
+
const opencodePluginDir = join12(homeEnv.XDG_CONFIG_HOME, "opencode", "plugin");
|
|
148981
|
+
mkdirSync6(opencodePluginDir, { recursive: true });
|
|
148982
|
+
writeFileSync9(
|
|
148983
|
+
join12(opencodePluginDir, PULLFROG_OPENCODE_PLUGIN_FILENAME),
|
|
148567
148984
|
PULLFROG_OPENCODE_PLUGIN_SOURCE
|
|
148568
148985
|
);
|
|
148569
148986
|
const agentBrowserVersion = getDevDependencyVersion("agent-browser");
|
|
@@ -148574,18 +148991,32 @@ var opencode = agent({
|
|
|
148574
148991
|
agent: "opencode"
|
|
148575
148992
|
});
|
|
148576
148993
|
installBundledSkills({ home: homeEnv.HOME });
|
|
148577
|
-
const
|
|
148994
|
+
const codexAuth = installCodexAuth();
|
|
148995
|
+
const baseArgs = ["run", "--format", "json", "--print-logs", "--thinking"];
|
|
148578
148996
|
const permissionOverride = JSON.stringify({
|
|
148579
148997
|
external_directory: { "*": "deny", "/tmp/*": "allow" }
|
|
148580
148998
|
});
|
|
148999
|
+
const repoDir = process.cwd();
|
|
148581
149000
|
const env2 = {
|
|
148582
149001
|
...process.env,
|
|
148583
149002
|
...homeEnv,
|
|
149003
|
+
PWD: repoDir,
|
|
148584
149004
|
OPENCODE_CONFIG_CONTENT: buildSecurityConfig(ctx, model),
|
|
148585
149005
|
OPENCODE_PERMISSION: permissionOverride,
|
|
148586
149006
|
GOOGLE_GENERATIVE_AI_API_KEY: process.env.GOOGLE_GENERATIVE_AI_API_KEY || process.env.GEMINI_API_KEY
|
|
148587
149007
|
};
|
|
148588
|
-
|
|
149008
|
+
if (codexAuth) {
|
|
149009
|
+
env2.XDG_DATA_HOME = codexAuth.xdgDataHome;
|
|
149010
|
+
delete env2.OPENAI_API_KEY;
|
|
149011
|
+
core2.saveState(
|
|
149012
|
+
"codex_writeback",
|
|
149013
|
+
JSON.stringify({
|
|
149014
|
+
apiToken: ctx.apiToken,
|
|
149015
|
+
authPath: codexAuth.authPath,
|
|
149016
|
+
originalRefresh: codexAuth.originalRefresh
|
|
149017
|
+
})
|
|
149018
|
+
);
|
|
149019
|
+
}
|
|
148589
149020
|
log.debug(`\xBB starting Pullfrog (OpenCode): ${cliPath} ${baseArgs.join(" ")}`);
|
|
148590
149021
|
log.debug(`\xBB working directory: ${repoDir}`);
|
|
148591
149022
|
const runParams = {
|
|
@@ -148593,6 +149024,7 @@ var opencode = agent({
|
|
|
148593
149024
|
cliPath,
|
|
148594
149025
|
cwd: repoDir,
|
|
148595
149026
|
env: env2,
|
|
149027
|
+
toolState: ctx.toolState,
|
|
148596
149028
|
todoTracker: ctx.todoTracker,
|
|
148597
149029
|
onActivityTimeout: ctx.onActivityTimeout,
|
|
148598
149030
|
onToolUse: ctx.onToolUse
|
|
@@ -148605,7 +149037,7 @@ var opencode = agent({
|
|
|
148605
149037
|
ctx,
|
|
148606
149038
|
initialResult: result,
|
|
148607
149039
|
initialUsage: result.usage,
|
|
148608
|
-
reflectionPrompt: ctx.toolState.learningsFilePath ? buildLearningsReflectionPrompt(ctx.toolState.learningsFilePath) : void 0,
|
|
149040
|
+
reflectionPrompt: ctx.toolState.learningsFilePath && shouldRunReflection(ctx.toolState.selectedMode) ? buildLearningsReflectionPrompt(ctx.toolState.learningsFilePath) : void 0,
|
|
148609
149041
|
resume: async (c) => runOpenCode({
|
|
148610
149042
|
...runParams,
|
|
148611
149043
|
args: [...baseArgs, "--continue", c.prompt]
|
|
@@ -148679,7 +149111,9 @@ function resolveAgent(ctx) {
|
|
|
148679
149111
|
}
|
|
148680
149112
|
|
|
148681
149113
|
// utils/apiKeys.ts
|
|
148682
|
-
var knownApiKeys = new Set(
|
|
149114
|
+
var knownApiKeys = new Set(
|
|
149115
|
+
Object.values(providers).flatMap((p) => [...p.envVars, ...p.managedCredentials ?? []])
|
|
149116
|
+
);
|
|
148683
149117
|
var MISSING_KEY_MARKER = "no API key found";
|
|
148684
149118
|
function buildMissingApiKeyError(params) {
|
|
148685
149119
|
const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
|
|
@@ -148708,6 +149142,11 @@ function hasEnvVar2(name) {
|
|
|
148708
149142
|
const value2 = process.env[name];
|
|
148709
149143
|
return typeof value2 === "string" && value2.length > 0;
|
|
148710
149144
|
}
|
|
149145
|
+
function hasProviderKey(model) {
|
|
149146
|
+
const requiredVars = getModelEnvVars(model);
|
|
149147
|
+
if (requiredVars.length === 0) return true;
|
|
149148
|
+
return requiredVars.some((v) => hasEnvVar2(v));
|
|
149149
|
+
}
|
|
148711
149150
|
function validateBedrockSetup(params) {
|
|
148712
149151
|
const hasAuth = hasEnvVar2("AWS_BEARER_TOKEN_BEDROCK") || hasEnvVar2("AWS_ACCESS_KEY_ID") && hasEnvVar2("AWS_SECRET_ACCESS_KEY");
|
|
148713
149152
|
const missing = [];
|
|
@@ -148742,7 +149181,7 @@ function validateAgentApiKey(params) {
|
|
|
148742
149181
|
}
|
|
148743
149182
|
function isApiKeyAuthError(text) {
|
|
148744
149183
|
if (!text) return false;
|
|
148745
|
-
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);
|
|
149184
|
+
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);
|
|
148746
149185
|
}
|
|
148747
149186
|
function formatApiKeyErrorSummary(params) {
|
|
148748
149187
|
if (params.raw.includes(MISSING_KEY_MARKER)) {
|
|
@@ -148854,11 +149293,137 @@ async function fetchBodyHtml(ctx) {
|
|
|
148854
149293
|
}
|
|
148855
149294
|
}
|
|
148856
149295
|
|
|
149296
|
+
// utils/byokFallback.ts
|
|
149297
|
+
var FREE_FALLBACK_SLUG = "opencode/minimax-m2.5-free";
|
|
149298
|
+
function selectFallbackModelIfNeeded(input) {
|
|
149299
|
+
if (input.proxyModel) return { fallback: false };
|
|
149300
|
+
if (!input.resolvedModel) return { fallback: false };
|
|
149301
|
+
if (input.resolvedModel === FREE_FALLBACK_SLUG) return { fallback: false };
|
|
149302
|
+
if (!input.resolvedModel.includes("/")) return { fallback: false };
|
|
149303
|
+
if (hasProviderKey(input.resolvedModel)) return { fallback: false };
|
|
149304
|
+
return {
|
|
149305
|
+
fallback: true,
|
|
149306
|
+
from: input.resolvedModel,
|
|
149307
|
+
to: FREE_FALLBACK_SLUG
|
|
149308
|
+
};
|
|
149309
|
+
}
|
|
149310
|
+
|
|
149311
|
+
// utils/gitAuthServer.ts
|
|
149312
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
149313
|
+
import { writeFileSync as writeFileSync10 } from "node:fs";
|
|
149314
|
+
import { createServer as createServer2 } from "node:http";
|
|
149315
|
+
import { join as join13 } from "node:path";
|
|
149316
|
+
var CODE_TTL_MS = 5 * 60 * 1e3;
|
|
149317
|
+
var TAMPER_WINDOW_MS = 6e4;
|
|
149318
|
+
function revokeGitHubToken(token) {
|
|
149319
|
+
fetch("https://api.github.com/installation/token", {
|
|
149320
|
+
method: "DELETE",
|
|
149321
|
+
headers: {
|
|
149322
|
+
Authorization: `Bearer ${token}`,
|
|
149323
|
+
Accept: "application/vnd.github+json",
|
|
149324
|
+
"User-Agent": "pullfrog"
|
|
149325
|
+
}
|
|
149326
|
+
}).then(
|
|
149327
|
+
(r) => log.info(`token revocation response: ${r.status}`),
|
|
149328
|
+
() => log.warning("token revocation request failed")
|
|
149329
|
+
);
|
|
149330
|
+
}
|
|
149331
|
+
async function startGitAuthServer(tmpdir3) {
|
|
149332
|
+
const codes = /* @__PURE__ */ new Map();
|
|
149333
|
+
const server = createServer2((req, res) => {
|
|
149334
|
+
if (req.method !== "GET") {
|
|
149335
|
+
res.writeHead(405).end();
|
|
149336
|
+
return;
|
|
149337
|
+
}
|
|
149338
|
+
const code = req.url?.slice(1);
|
|
149339
|
+
if (!code) {
|
|
149340
|
+
res.writeHead(400).end();
|
|
149341
|
+
return;
|
|
149342
|
+
}
|
|
149343
|
+
const entry = codes.get(code);
|
|
149344
|
+
if (!entry) {
|
|
149345
|
+
res.writeHead(404).end();
|
|
149346
|
+
return;
|
|
149347
|
+
}
|
|
149348
|
+
if (entry.state === "pending") {
|
|
149349
|
+
entry.state = "consumed";
|
|
149350
|
+
clearTimeout(entry.timeout);
|
|
149351
|
+
entry.timeout = setTimeout(() => codes.delete(code), TAMPER_WINDOW_MS);
|
|
149352
|
+
entry.timeout.unref();
|
|
149353
|
+
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
149354
|
+
res.end(entry.token);
|
|
149355
|
+
return;
|
|
149356
|
+
}
|
|
149357
|
+
log.info("askpass code used twice \u2014 revoking token");
|
|
149358
|
+
revokeGitHubToken(entry.token);
|
|
149359
|
+
clearTimeout(entry.timeout);
|
|
149360
|
+
codes.delete(code);
|
|
149361
|
+
res.writeHead(409, { "Content-Type": "text/plain" });
|
|
149362
|
+
res.end("compromised");
|
|
149363
|
+
});
|
|
149364
|
+
await new Promise((resolve3, reject) => {
|
|
149365
|
+
server.on("error", reject);
|
|
149366
|
+
server.listen(0, "127.0.0.1", () => resolve3());
|
|
149367
|
+
});
|
|
149368
|
+
const rawAddr = server.address();
|
|
149369
|
+
if (!rawAddr || typeof rawAddr === "string") {
|
|
149370
|
+
throw new Error("git auth server failed to bind");
|
|
149371
|
+
}
|
|
149372
|
+
const port = rawAddr.port;
|
|
149373
|
+
log.debug(`git auth server listening on 127.0.0.1:${port}`);
|
|
149374
|
+
function register4(token) {
|
|
149375
|
+
const code = randomUUID3();
|
|
149376
|
+
const timeout = setTimeout(() => {
|
|
149377
|
+
codes.delete(code);
|
|
149378
|
+
log.debug(`git auth code expired: ${code.slice(0, 8)}...`);
|
|
149379
|
+
}, CODE_TTL_MS);
|
|
149380
|
+
timeout.unref();
|
|
149381
|
+
codes.set(code, { token, state: "pending", timeout });
|
|
149382
|
+
return code;
|
|
149383
|
+
}
|
|
149384
|
+
function writeAskpassScript(code) {
|
|
149385
|
+
const scriptId = randomUUID3();
|
|
149386
|
+
const scriptName = `askpass-${scriptId}.js`;
|
|
149387
|
+
const scriptPath = join13(tmpdir3, scriptName);
|
|
149388
|
+
const content = [
|
|
149389
|
+
`#!/usr/bin/env node`,
|
|
149390
|
+
`var a=process.argv[2]||"";`,
|
|
149391
|
+
`if(/^Username/i.test(a)){process.stdout.write("x-access-token\\n")}`,
|
|
149392
|
+
`else{var h=require("http");`,
|
|
149393
|
+
`h.get("http://127.0.0.1:${port}/${code}",function(r){`,
|
|
149394
|
+
`if(r.statusCode===409){process.stderr.write("askpass-compromised\\n");process.exit(1)}`,
|
|
149395
|
+
`if(r.statusCode!==200){process.exit(1)}`,
|
|
149396
|
+
`var d="";r.on("data",function(c){d+=c});`,
|
|
149397
|
+
`r.on("end",function(){`,
|
|
149398
|
+
`process.stdout.write(d+"\\n");`,
|
|
149399
|
+
`try{require("fs").unlinkSync("${scriptPath.replace(/\\/g, "\\\\")}")}catch(e){}`,
|
|
149400
|
+
`})}).on("error",function(){process.exit(1)})}`
|
|
149401
|
+
].join("\n");
|
|
149402
|
+
writeFileSync10(scriptPath, content, { mode: 448 });
|
|
149403
|
+
return scriptPath;
|
|
149404
|
+
}
|
|
149405
|
+
async function close() {
|
|
149406
|
+
for (const entry of codes.values()) {
|
|
149407
|
+
clearTimeout(entry.timeout);
|
|
149408
|
+
}
|
|
149409
|
+
codes.clear();
|
|
149410
|
+
await new Promise((resolve3) => server.close(() => resolve3()));
|
|
149411
|
+
log.debug("git auth server closed");
|
|
149412
|
+
}
|
|
149413
|
+
return {
|
|
149414
|
+
port,
|
|
149415
|
+
register: register4,
|
|
149416
|
+
writeAskpassScript,
|
|
149417
|
+
close,
|
|
149418
|
+
[Symbol.asyncDispose]: close
|
|
149419
|
+
};
|
|
149420
|
+
}
|
|
149421
|
+
|
|
148857
149422
|
// utils/github.ts
|
|
148858
|
-
var
|
|
149423
|
+
var core3 = __toESM(require_core(), 1);
|
|
148859
149424
|
import { createSign } from "node:crypto";
|
|
148860
149425
|
import { rename, writeFile } from "node:fs/promises";
|
|
148861
|
-
import { dirname as dirname3, join as
|
|
149426
|
+
import { dirname as dirname3, join as join14 } from "node:path";
|
|
148862
149427
|
|
|
148863
149428
|
// node_modules/.pnpm/@octokit+plugin-throttling@11.0.3_@octokit+core@7.0.5/node_modules/@octokit/plugin-throttling/dist-bundle/index.js
|
|
148864
149429
|
var import_light = __toESM(require_light(), 1);
|
|
@@ -152517,7 +153082,7 @@ var TokenExchangeError = class extends Error {
|
|
|
152517
153082
|
}
|
|
152518
153083
|
};
|
|
152519
153084
|
async function acquireTokenViaOIDC(opts) {
|
|
152520
|
-
const oidcToken = await
|
|
153085
|
+
const oidcToken = await core3.getIDToken("pullfrog-api");
|
|
152521
153086
|
const repos = [...opts?.repos ?? []];
|
|
152522
153087
|
const targetRepo = process.env.GITHUB_REPOSITORY?.split("/")[1];
|
|
152523
153088
|
if (targetRepo) {
|
|
@@ -152675,9 +153240,13 @@ async function acquireNewToken(opts) {
|
|
|
152675
153240
|
return error49 instanceof Error && (error49.message.includes("timed out") || error49.message.includes("fetch failed") || error49.message.includes("ECONNRESET") || error49.message.includes("ETIMEDOUT"));
|
|
152676
153241
|
}
|
|
152677
153242
|
});
|
|
152678
|
-
} else {
|
|
152679
|
-
return await acquireTokenViaGitHubApp(opts);
|
|
152680
153243
|
}
|
|
153244
|
+
if (process.env.GITHUB_ACTIONS === "true") {
|
|
153245
|
+
throw new Error(
|
|
153246
|
+
"missing `permissions: id-token: write` on the Pullfrog workflow job.\n\nPullfrog mints short-lived GitHub App installation tokens via OIDC and\nrequires `id-token: write` to be granted at the job level. add the\nfollowing to your workflow yaml:\n\n jobs:\n pullfrog:\n permissions:\n id-token: write # mint Pullfrog installation tokens via OIDC\n contents: read # for actions/checkout\n\nsee https://docs.pullfrog.com/headless-action#required-permissions for the full template."
|
|
153247
|
+
);
|
|
153248
|
+
}
|
|
153249
|
+
return await acquireTokenViaGitHubApp(opts);
|
|
152681
153250
|
}
|
|
152682
153251
|
function parseRepoContext() {
|
|
152683
153252
|
const githubRepo = process.env.GITHUB_REPOSITORY;
|
|
@@ -152712,7 +153281,7 @@ function getGitHubUsageSummary() {
|
|
|
152712
153281
|
}
|
|
152713
153282
|
async function writeGitHubUsageSummaryToFile(path3) {
|
|
152714
153283
|
const summary2 = getGitHubUsageSummary();
|
|
152715
|
-
const tmpPath =
|
|
153284
|
+
const tmpPath = join14(dirname3(path3), `.usage-summary-${process.pid}.tmp`);
|
|
152716
153285
|
await writeFile(tmpPath, JSON.stringify(summary2));
|
|
152717
153286
|
await rename(tmpPath, path3);
|
|
152718
153287
|
}
|
|
@@ -152762,253 +153331,6 @@ function createOctokit(token) {
|
|
|
152762
153331
|
return octokit;
|
|
152763
153332
|
}
|
|
152764
153333
|
|
|
152765
|
-
// utils/token.ts
|
|
152766
|
-
var core3 = __toESM(require_core(), 1);
|
|
152767
|
-
import assert2 from "node:assert/strict";
|
|
152768
|
-
var mcpTokenValue;
|
|
152769
|
-
function getJobToken() {
|
|
152770
|
-
const inputToken = core3.getInput("token");
|
|
152771
|
-
if (inputToken) {
|
|
152772
|
-
return inputToken;
|
|
152773
|
-
}
|
|
152774
|
-
const fallbackToken = process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
|
|
152775
|
-
if (fallbackToken) {
|
|
152776
|
-
return fallbackToken;
|
|
152777
|
-
}
|
|
152778
|
-
throw new Error("token input is required");
|
|
152779
|
-
}
|
|
152780
|
-
async function resolveTokens(params) {
|
|
152781
|
-
assert2(!mcpTokenValue, "tokens are already resolved");
|
|
152782
|
-
const externalToken = process.env.GH_TOKEN;
|
|
152783
|
-
if (externalToken) {
|
|
152784
|
-
mcpTokenValue = externalToken;
|
|
152785
|
-
if (isGitHubActions) {
|
|
152786
|
-
core3.setSecret(externalToken);
|
|
152787
|
-
}
|
|
152788
|
-
log.info("\xBB using external GH_TOKEN for both git and MCP");
|
|
152789
|
-
return {
|
|
152790
|
-
gitToken: externalToken,
|
|
152791
|
-
mcpToken: externalToken,
|
|
152792
|
-
async [Symbol.asyncDispose]() {
|
|
152793
|
-
mcpTokenValue = void 0;
|
|
152794
|
-
}
|
|
152795
|
-
};
|
|
152796
|
-
}
|
|
152797
|
-
const gitPermissions = params.push === "disabled" ? { contents: "read" } : { contents: "write", workflows: "write" };
|
|
152798
|
-
const gitToken = await acquireNewToken({ permissions: gitPermissions });
|
|
152799
|
-
if (isGitHubActions) {
|
|
152800
|
-
core3.setSecret(gitToken);
|
|
152801
|
-
}
|
|
152802
|
-
log.info(
|
|
152803
|
-
`\xBB acquired git token (${Object.entries(gitPermissions).map((e) => e.join(":")).join(", ")})`
|
|
152804
|
-
);
|
|
152805
|
-
const mcpPermissions = {
|
|
152806
|
-
contents: "write",
|
|
152807
|
-
pull_requests: "write",
|
|
152808
|
-
issues: "write",
|
|
152809
|
-
checks: "read",
|
|
152810
|
-
actions: "read"
|
|
152811
|
-
};
|
|
152812
|
-
const mcpToken = await acquireNewToken({ permissions: mcpPermissions });
|
|
152813
|
-
if (isGitHubActions) {
|
|
152814
|
-
core3.setSecret(mcpToken);
|
|
152815
|
-
}
|
|
152816
|
-
log.info(
|
|
152817
|
-
`\xBB acquired scoped MCP token (${Object.entries(mcpPermissions).map((e) => e.join(":")).join(", ")})`
|
|
152818
|
-
);
|
|
152819
|
-
mcpTokenValue = mcpToken;
|
|
152820
|
-
let disposingRef;
|
|
152821
|
-
const dispose = async () => {
|
|
152822
|
-
if (disposingRef) {
|
|
152823
|
-
return disposingRef.promise;
|
|
152824
|
-
}
|
|
152825
|
-
disposingRef = Promise.withResolvers();
|
|
152826
|
-
try {
|
|
152827
|
-
mcpTokenValue = void 0;
|
|
152828
|
-
await Promise.all([
|
|
152829
|
-
revokeGitHubInstallationToken(gitToken),
|
|
152830
|
-
revokeGitHubInstallationToken(mcpToken)
|
|
152831
|
-
]);
|
|
152832
|
-
} finally {
|
|
152833
|
-
removeSignalHandler();
|
|
152834
|
-
disposingRef.resolve();
|
|
152835
|
-
disposingRef = void 0;
|
|
152836
|
-
}
|
|
152837
|
-
};
|
|
152838
|
-
const removeSignalHandler = onExitSignal(dispose);
|
|
152839
|
-
return {
|
|
152840
|
-
gitToken,
|
|
152841
|
-
mcpToken,
|
|
152842
|
-
[Symbol.asyncDispose]: dispose
|
|
152843
|
-
};
|
|
152844
|
-
}
|
|
152845
|
-
function getGitHubInstallationToken() {
|
|
152846
|
-
assert2(mcpTokenValue, "tokens not set. call resolveTokens first.");
|
|
152847
|
-
return mcpTokenValue;
|
|
152848
|
-
}
|
|
152849
|
-
async function revokeGitHubInstallationToken(token) {
|
|
152850
|
-
const apiUrl = process.env.GITHUB_API_URL || "https://api.github.com";
|
|
152851
|
-
try {
|
|
152852
|
-
await fetch(`${apiUrl}/installation/token`, {
|
|
152853
|
-
method: "DELETE",
|
|
152854
|
-
headers: {
|
|
152855
|
-
Accept: "application/vnd.github+json",
|
|
152856
|
-
Authorization: `Bearer ${token}`,
|
|
152857
|
-
"X-GitHub-Api-Version": "2022-11-28"
|
|
152858
|
-
}
|
|
152859
|
-
});
|
|
152860
|
-
log.debug("\xBB installation token revoked");
|
|
152861
|
-
} catch (error49) {
|
|
152862
|
-
log.info(
|
|
152863
|
-
`Failed to revoke installation token: ${error49 instanceof Error ? error49.message : String(error49)}`
|
|
152864
|
-
);
|
|
152865
|
-
}
|
|
152866
|
-
}
|
|
152867
|
-
|
|
152868
|
-
// utils/errorReport.ts
|
|
152869
|
-
async function reportErrorToComment(ctx) {
|
|
152870
|
-
const formattedError = ctx.title ? `${ctx.title}
|
|
152871
|
-
|
|
152872
|
-
${ctx.error}` : ctx.error;
|
|
152873
|
-
const comment = ctx.toolState.progressComment;
|
|
152874
|
-
if (!comment) {
|
|
152875
|
-
return;
|
|
152876
|
-
}
|
|
152877
|
-
const repoContext = parseRepoContext();
|
|
152878
|
-
const octokit = createOctokit(getGitHubInstallationToken());
|
|
152879
|
-
const runId = process.env.GITHUB_RUN_ID ? Number.parseInt(process.env.GITHUB_RUN_ID, 10) : void 0;
|
|
152880
|
-
const customParts = [];
|
|
152881
|
-
if (runId) {
|
|
152882
|
-
const apiUrl = getApiUrl();
|
|
152883
|
-
customParts.push(
|
|
152884
|
-
`[Rerun failed job \u2794](${apiUrl}/trigger/${repoContext.owner}/${repoContext.name}/${runId}?action=rerun)`
|
|
152885
|
-
);
|
|
152886
|
-
}
|
|
152887
|
-
const footer = buildPullfrogFooter({
|
|
152888
|
-
triggeredBy: true,
|
|
152889
|
-
workflowRun: runId ? { owner: repoContext.owner, repo: repoContext.name, runId } : void 0,
|
|
152890
|
-
customParts,
|
|
152891
|
-
model: ctx.toolState.model
|
|
152892
|
-
});
|
|
152893
|
-
await updateProgressComment(
|
|
152894
|
-
{ octokit, owner: repoContext.owner, repo: repoContext.name },
|
|
152895
|
-
comment,
|
|
152896
|
-
`${formattedError}${footer}`
|
|
152897
|
-
);
|
|
152898
|
-
ctx.toolState.wasUpdated = true;
|
|
152899
|
-
}
|
|
152900
|
-
|
|
152901
|
-
// utils/gitAuthServer.ts
|
|
152902
|
-
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
152903
|
-
import { writeFileSync as writeFileSync9 } from "node:fs";
|
|
152904
|
-
import { createServer as createServer2 } from "node:http";
|
|
152905
|
-
import { join as join13 } from "node:path";
|
|
152906
|
-
var CODE_TTL_MS = 5 * 60 * 1e3;
|
|
152907
|
-
var TAMPER_WINDOW_MS = 6e4;
|
|
152908
|
-
function revokeGitHubToken(token) {
|
|
152909
|
-
fetch("https://api.github.com/installation/token", {
|
|
152910
|
-
method: "DELETE",
|
|
152911
|
-
headers: {
|
|
152912
|
-
Authorization: `Bearer ${token}`,
|
|
152913
|
-
Accept: "application/vnd.github+json",
|
|
152914
|
-
"User-Agent": "pullfrog"
|
|
152915
|
-
}
|
|
152916
|
-
}).then(
|
|
152917
|
-
(r) => log.info(`token revocation response: ${r.status}`),
|
|
152918
|
-
() => log.warning("token revocation request failed")
|
|
152919
|
-
);
|
|
152920
|
-
}
|
|
152921
|
-
async function startGitAuthServer(tmpdir3) {
|
|
152922
|
-
const codes = /* @__PURE__ */ new Map();
|
|
152923
|
-
const server = createServer2((req, res) => {
|
|
152924
|
-
if (req.method !== "GET") {
|
|
152925
|
-
res.writeHead(405).end();
|
|
152926
|
-
return;
|
|
152927
|
-
}
|
|
152928
|
-
const code = req.url?.slice(1);
|
|
152929
|
-
if (!code) {
|
|
152930
|
-
res.writeHead(400).end();
|
|
152931
|
-
return;
|
|
152932
|
-
}
|
|
152933
|
-
const entry = codes.get(code);
|
|
152934
|
-
if (!entry) {
|
|
152935
|
-
res.writeHead(404).end();
|
|
152936
|
-
return;
|
|
152937
|
-
}
|
|
152938
|
-
if (entry.state === "pending") {
|
|
152939
|
-
entry.state = "consumed";
|
|
152940
|
-
clearTimeout(entry.timeout);
|
|
152941
|
-
entry.timeout = setTimeout(() => codes.delete(code), TAMPER_WINDOW_MS);
|
|
152942
|
-
entry.timeout.unref();
|
|
152943
|
-
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
152944
|
-
res.end(entry.token);
|
|
152945
|
-
return;
|
|
152946
|
-
}
|
|
152947
|
-
log.info("askpass code used twice \u2014 revoking token");
|
|
152948
|
-
revokeGitHubToken(entry.token);
|
|
152949
|
-
clearTimeout(entry.timeout);
|
|
152950
|
-
codes.delete(code);
|
|
152951
|
-
res.writeHead(409, { "Content-Type": "text/plain" });
|
|
152952
|
-
res.end("compromised");
|
|
152953
|
-
});
|
|
152954
|
-
await new Promise((resolve3, reject) => {
|
|
152955
|
-
server.on("error", reject);
|
|
152956
|
-
server.listen(0, "127.0.0.1", () => resolve3());
|
|
152957
|
-
});
|
|
152958
|
-
const rawAddr = server.address();
|
|
152959
|
-
if (!rawAddr || typeof rawAddr === "string") {
|
|
152960
|
-
throw new Error("git auth server failed to bind");
|
|
152961
|
-
}
|
|
152962
|
-
const port = rawAddr.port;
|
|
152963
|
-
log.debug(`git auth server listening on 127.0.0.1:${port}`);
|
|
152964
|
-
function register4(token) {
|
|
152965
|
-
const code = randomUUID3();
|
|
152966
|
-
const timeout = setTimeout(() => {
|
|
152967
|
-
codes.delete(code);
|
|
152968
|
-
log.debug(`git auth code expired: ${code.slice(0, 8)}...`);
|
|
152969
|
-
}, CODE_TTL_MS);
|
|
152970
|
-
timeout.unref();
|
|
152971
|
-
codes.set(code, { token, state: "pending", timeout });
|
|
152972
|
-
return code;
|
|
152973
|
-
}
|
|
152974
|
-
function writeAskpassScript(code) {
|
|
152975
|
-
const scriptId = randomUUID3();
|
|
152976
|
-
const scriptName = `askpass-${scriptId}.js`;
|
|
152977
|
-
const scriptPath = join13(tmpdir3, scriptName);
|
|
152978
|
-
const content = [
|
|
152979
|
-
`#!/usr/bin/env node`,
|
|
152980
|
-
`var a=process.argv[2]||"";`,
|
|
152981
|
-
`if(/^Username/i.test(a)){process.stdout.write("x-access-token\\n")}`,
|
|
152982
|
-
`else{var h=require("http");`,
|
|
152983
|
-
`h.get("http://127.0.0.1:${port}/${code}",function(r){`,
|
|
152984
|
-
`if(r.statusCode===409){process.stderr.write("askpass-compromised\\n");process.exit(1)}`,
|
|
152985
|
-
`if(r.statusCode!==200){process.exit(1)}`,
|
|
152986
|
-
`var d="";r.on("data",function(c){d+=c});`,
|
|
152987
|
-
`r.on("end",function(){`,
|
|
152988
|
-
`process.stdout.write(d+"\\n");`,
|
|
152989
|
-
`try{require("fs").unlinkSync("${scriptPath.replace(/\\/g, "\\\\")}")}catch(e){}`,
|
|
152990
|
-
`})}).on("error",function(){process.exit(1)})}`
|
|
152991
|
-
].join("\n");
|
|
152992
|
-
writeFileSync9(scriptPath, content, { mode: 448 });
|
|
152993
|
-
return scriptPath;
|
|
152994
|
-
}
|
|
152995
|
-
async function close() {
|
|
152996
|
-
for (const entry of codes.values()) {
|
|
152997
|
-
clearTimeout(entry.timeout);
|
|
152998
|
-
}
|
|
152999
|
-
codes.clear();
|
|
153000
|
-
await new Promise((resolve3) => server.close(() => resolve3()));
|
|
153001
|
-
log.debug("git auth server closed");
|
|
153002
|
-
}
|
|
153003
|
-
return {
|
|
153004
|
-
port,
|
|
153005
|
-
register: register4,
|
|
153006
|
-
writeAskpassScript,
|
|
153007
|
-
close,
|
|
153008
|
-
[Symbol.asyncDispose]: close
|
|
153009
|
-
};
|
|
153010
|
-
}
|
|
153011
|
-
|
|
153012
153334
|
// utils/instructions.ts
|
|
153013
153335
|
import { execSync as execSync2 } from "node:child_process";
|
|
153014
153336
|
function buildRuntimeContext(ctx) {
|
|
@@ -153201,7 +153523,7 @@ Rules:
|
|
|
153201
153523
|
- Never push commits directly to the default branch or any protected branch (commonly: main, master, production, develop, staging). Always create a feature branch following the pattern: \`pullfrog/<issue-number>-<kebab-case-description>\` (e.g., \`pullfrog/123-fix-login-bug\`).
|
|
153202
153524
|
- Never add co-author trailers (e.g., "Co-authored-by" or "Co-Authored-By") to commit messages.
|
|
153203
153525
|
- Untracked files from tests or tooling (e.g. \`coverage/\`) often remain *after* your last commit and still block \`${t("push_branch")}\` \u2014 delete them, extend \`.gitignore\`, or only add files that truly belong in the repo.
|
|
153204
|
-
- \`${t("push_branch")}\` runs the repository's optional **prepush** hook
|
|
153526
|
+
- \`${t("push_branch")}\` runs the repository's optional **prepush** hook (commonly tests or lint) \u2014 best-effort. On failure the output is returned, the hook is latched off, and every subsequent \`${t("push_branch")}\` call this run skips it. If the failure is unrelated to your changes (pre-existing breakage, env-dependent test, flaky check), just call \`${t("push_branch")}\` again. If it could be a real bug in your code, ${ctx.payload.shell === "disabled" ? `fix it from the failure output (shell is disabled, so you can't re-run the hook)` : `re-run the hook via the shell tool to iterate \u2014 \`${t("push_branch")}\` itself won't re-run it`}. Don't describe the failure as an infrastructure "timeout" unless the tool output clearly shows one.
|
|
153205
153527
|
- If push or PR creation fails, \`${t("report_progress")}\` must summarize using the **actual** error from the tool. Do not substitute vague causes unless they match what failed.
|
|
153206
153528
|
|
|
153207
153529
|
### GitHub
|
|
@@ -153229,11 +153551,9 @@ For maximum efficiency, whenever you need to perform multiple independent operat
|
|
|
153229
153551
|
- listing multiple directories
|
|
153230
153552
|
- inspecting multiple MCP tools or resources
|
|
153231
153553
|
|
|
153232
|
-
Do NOT parallelize operations that depend on prior output (e.g. create a file then read it), or ordered stateful mutations. Edits are not parallelizable \u2014 sequence those normally
|
|
153554
|
+
Do NOT parallelize operations that depend on prior output (e.g. create a file then read it), or ordered stateful mutations. Edits are not parallelizable \u2014 sequence those normally.
|
|
153233
153555
|
|
|
153234
|
-
|
|
153235
|
-
|
|
153236
|
-
Emit multiple \`tool_use\` blocks in the same assistant message for independent calls \u2014 the runtime executes them concurrently. Do not wait for one tool result before issuing the next independent call.`}
|
|
153556
|
+
Emit multiple \`tool_use\` blocks in the same assistant message for independent calls \u2014 the runtime executes them concurrently. Do not wait for one tool result before issuing the next independent call.
|
|
153237
153557
|
|
|
153238
153558
|
### Command execution
|
|
153239
153559
|
|
|
@@ -153251,7 +153571,7 @@ When embedding images (e.g. uploaded screenshots) in comments or PR bodies, alwa
|
|
|
153251
153571
|
|
|
153252
153572
|
**\`report_progress\`**: call this exactly once at the end of every run with a brief final summary (1-3 sentences) unless the mode guidance instructs otherwise. Never call it for intermediate status updates (e.g., "Checking for changes...", "Starting review...") \u2014 the task list handles live progress automatically. Calling \`report_progress\` replaces the task list with your summary and preserves the current task list in a collapsible section. Keep the summary concise \u2014 do not repeat what the task list already shows. Focus on the outcome (what was accomplished, links to artifacts) rather than listing individual steps. If something failed, include the tool's error text even when that makes the summary longer.
|
|
153253
153573
|
|
|
153254
|
-
Never use \`create_issue_comment\` for task progress \u2014 that creates duplicate comments and leaves the progress comment stuck in its initial state. \`create_issue_comment\` is only for standalone comments unrelated to your current task (
|
|
153574
|
+
Never use \`create_issue_comment\` for task progress \u2014 that creates duplicate comments and leaves the progress comment stuck in its initial state. \`create_issue_comment\` is only for standalone comments unrelated to your current task. Plan output (initial post AND revisions) goes through \`report_progress\` \u2014 see the Plan mode guidance for details.
|
|
153255
153575
|
|
|
153256
153576
|
### If you get stuck
|
|
153257
153577
|
|
|
@@ -153290,8 +153610,8 @@ function renderLearningsToc(headings) {
|
|
|
153290
153610
|
}
|
|
153291
153611
|
function buildLearningsSection(ctx) {
|
|
153292
153612
|
if (!ctx.filePath) return "";
|
|
153293
|
-
const intro = `
|
|
153294
|
-
const tocBody = ctx.headings.length === 0 ? "(no headings yet \u2014 file is empty or a flat list. read the whole file. during the post-run reflection turn, structure it with `## ` / `### ` headings so future runs can read targeted ranges.)" : `Read targeted line ranges via your native file tool \u2014 do NOT slurp the whole file. Each range starts at the section heading line, so reading the range gives you heading + body together.
|
|
153613
|
+
const intro = `The repo-level learnings file at \`${ctx.filePath}\` holds durable context (test commands, conventions, gotchas, architecture notes) maintained across runs.`;
|
|
153614
|
+
const tocBody = ctx.headings.length === 0 ? "(no headings yet \u2014 the file is empty or contains a flat list. read the whole file if it has content. during the post-run reflection turn, structure it with `## ` / `### ` headings so future runs can read targeted ranges.)" : `Read targeted line ranges via your native file tool \u2014 do NOT slurp the whole file. Each range starts at the section heading line, so reading the range gives you heading + body together. The ranges below are a run-start snapshot: any edit shifts the line numbers of every later section, so re-read the TOC range you need before relying on it.
|
|
153295
153615
|
|
|
153296
153616
|
${renderLearningsToc(ctx.headings)}`;
|
|
153297
153617
|
return `************* LEARNINGS *************
|
|
@@ -153361,18 +153681,10 @@ function resolveInstructions(ctx) {
|
|
|
153361
153681
|
|
|
153362
153682
|
// utils/learnings.ts
|
|
153363
153683
|
import { mkdir, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
153364
|
-
import { dirname as dirname4, join as
|
|
153365
|
-
|
|
153684
|
+
import { dirname as dirname4, join as join15 } from "node:path";
|
|
153685
|
+
|
|
153686
|
+
// utils/learningsTruncate.ts
|
|
153366
153687
|
var MAX_LEARNINGS_LENGTH = 1e5;
|
|
153367
|
-
function learningsFilePath(tmpdir3) {
|
|
153368
|
-
return join14(tmpdir3, LEARNINGS_FILE_NAME);
|
|
153369
|
-
}
|
|
153370
|
-
async function seedLearningsFile(params) {
|
|
153371
|
-
const path3 = learningsFilePath(params.tmpdir);
|
|
153372
|
-
await mkdir(dirname4(path3), { recursive: true });
|
|
153373
|
-
await writeFile2(path3, params.current ?? "", "utf8");
|
|
153374
|
-
return path3;
|
|
153375
|
-
}
|
|
153376
153688
|
var TRUNCATION_LINE_BOUNDARY_TOLERANCE = 4096;
|
|
153377
153689
|
function truncateAtLineBoundary(body, cap) {
|
|
153378
153690
|
if (body.length <= cap) return body;
|
|
@@ -153382,6 +153694,18 @@ function truncateAtLineBoundary(body, cap) {
|
|
|
153382
153694
|
if (cap - lastNewline > TRUNCATION_LINE_BOUNDARY_TOLERANCE) return head;
|
|
153383
153695
|
return head.slice(0, lastNewline);
|
|
153384
153696
|
}
|
|
153697
|
+
|
|
153698
|
+
// utils/learnings.ts
|
|
153699
|
+
var LEARNINGS_FILE_NAME = "pullfrog-learnings.md";
|
|
153700
|
+
function learningsFilePath(tmpdir3) {
|
|
153701
|
+
return join15(tmpdir3, LEARNINGS_FILE_NAME);
|
|
153702
|
+
}
|
|
153703
|
+
async function seedLearningsFile(params) {
|
|
153704
|
+
const path3 = learningsFilePath(params.tmpdir);
|
|
153705
|
+
await mkdir(dirname4(path3), { recursive: true });
|
|
153706
|
+
await writeFile2(path3, params.current ?? "", "utf8");
|
|
153707
|
+
return path3;
|
|
153708
|
+
}
|
|
153385
153709
|
async function readLearningsFile(path3) {
|
|
153386
153710
|
let raw2;
|
|
153387
153711
|
try {
|
|
@@ -153391,6 +153715,45 @@ async function readLearningsFile(path3) {
|
|
|
153391
153715
|
}
|
|
153392
153716
|
return truncateAtLineBoundary(raw2.trim(), MAX_LEARNINGS_LENGTH);
|
|
153393
153717
|
}
|
|
153718
|
+
async function persistLearnings(ctx) {
|
|
153719
|
+
const filePath = ctx.toolState.learningsFilePath;
|
|
153720
|
+
if (!filePath) return;
|
|
153721
|
+
if (ctx.toolState.learningsPersistAttempted) return;
|
|
153722
|
+
ctx.toolState.learningsPersistAttempted = true;
|
|
153723
|
+
const current = await readLearningsFile(filePath);
|
|
153724
|
+
if (current === null) {
|
|
153725
|
+
log.debug(`learnings tmpfile missing or unreadable at ${filePath} \u2014 skipping persist`);
|
|
153726
|
+
return;
|
|
153727
|
+
}
|
|
153728
|
+
const seed = ctx.toolState.learningsSeed?.trim() ?? "";
|
|
153729
|
+
if (current === seed) {
|
|
153730
|
+
log.debug("learnings tmpfile unchanged from seed \u2014 skipping persist");
|
|
153731
|
+
return;
|
|
153732
|
+
}
|
|
153733
|
+
try {
|
|
153734
|
+
const response = await apiFetch({
|
|
153735
|
+
path: `/api/repo/${ctx.repo.owner}/${ctx.repo.name}/learnings`,
|
|
153736
|
+
method: "PATCH",
|
|
153737
|
+
headers: {
|
|
153738
|
+
authorization: `Bearer ${ctx.apiToken}`,
|
|
153739
|
+
"content-type": "application/json"
|
|
153740
|
+
},
|
|
153741
|
+
body: JSON.stringify({
|
|
153742
|
+
learnings: current,
|
|
153743
|
+
model: ctx.toolState.model
|
|
153744
|
+
}),
|
|
153745
|
+
signal: AbortSignal.timeout(1e4)
|
|
153746
|
+
});
|
|
153747
|
+
if (!response.ok) {
|
|
153748
|
+
const error49 = await response.text().catch(() => "(no body)");
|
|
153749
|
+
log.warning(`learnings persist failed (${response.status}): ${error49}`);
|
|
153750
|
+
return;
|
|
153751
|
+
}
|
|
153752
|
+
log.info("\xBB learnings updated");
|
|
153753
|
+
} catch (err) {
|
|
153754
|
+
log.warning(`learnings persist failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
153755
|
+
}
|
|
153756
|
+
}
|
|
153394
153757
|
|
|
153395
153758
|
// utils/normalizeEnv.ts
|
|
153396
153759
|
var core4 = __toESM(require_core(), 1);
|
|
@@ -153450,8 +153813,63 @@ function normalizeEnv() {
|
|
|
153450
153813
|
}
|
|
153451
153814
|
}
|
|
153452
153815
|
|
|
153453
|
-
// utils/
|
|
153816
|
+
// utils/overrides.ts
|
|
153454
153817
|
var core5 = __toESM(require_core(), 1);
|
|
153818
|
+
var DENIED_OVERRIDE_NAMES = /* @__PURE__ */ new Set([
|
|
153819
|
+
"GITHUB_TOKEN",
|
|
153820
|
+
"GH_TOKEN",
|
|
153821
|
+
"ACTIONS_RUNTIME_TOKEN",
|
|
153822
|
+
"ACTIONS_RUNTIME_URL",
|
|
153823
|
+
"ACTIONS_ID_TOKEN_REQUEST_URL",
|
|
153824
|
+
"ACTIONS_ID_TOKEN_REQUEST_TOKEN",
|
|
153825
|
+
"ACTIONS_CACHE_URL",
|
|
153826
|
+
"PULLFROG_API_SECRET",
|
|
153827
|
+
"VERCEL_AUTOMATION_BYPASS_SECRET"
|
|
153828
|
+
]);
|
|
153829
|
+
function parseOverrides(raw2) {
|
|
153830
|
+
const trimmed = raw2.trim();
|
|
153831
|
+
if (!trimmed) return {};
|
|
153832
|
+
let parsed2;
|
|
153833
|
+
try {
|
|
153834
|
+
parsed2 = JSON.parse(trimmed);
|
|
153835
|
+
} catch (err) {
|
|
153836
|
+
throw new Error(
|
|
153837
|
+
`invalid UNSAFE_OVERRIDES: not valid JSON (${err instanceof Error ? err.message : String(err)})`
|
|
153838
|
+
);
|
|
153839
|
+
}
|
|
153840
|
+
if (!parsed2 || typeof parsed2 !== "object" || Array.isArray(parsed2)) {
|
|
153841
|
+
throw new Error(`invalid UNSAFE_OVERRIDES: must be a JSON object`);
|
|
153842
|
+
}
|
|
153843
|
+
const out = {};
|
|
153844
|
+
for (const [key, value2] of Object.entries(parsed2)) {
|
|
153845
|
+
if (typeof value2 !== "string") {
|
|
153846
|
+
throw new Error(
|
|
153847
|
+
`invalid UNSAFE_OVERRIDES: key "${key}" must have a string value (got ${typeof value2})`
|
|
153848
|
+
);
|
|
153849
|
+
}
|
|
153850
|
+
out[key] = value2;
|
|
153851
|
+
}
|
|
153852
|
+
return out;
|
|
153853
|
+
}
|
|
153854
|
+
function applyOverrides(params) {
|
|
153855
|
+
const overrides = parseOverrides(params.raw);
|
|
153856
|
+
const applied = [];
|
|
153857
|
+
const denied = [];
|
|
153858
|
+
for (const [key, value2] of Object.entries(overrides)) {
|
|
153859
|
+
if (DENIED_OVERRIDE_NAMES.has(key)) {
|
|
153860
|
+
denied.push(key);
|
|
153861
|
+
continue;
|
|
153862
|
+
}
|
|
153863
|
+
if (value2.length > 0) core5.setSecret(value2);
|
|
153864
|
+
params.env[key] = value2;
|
|
153865
|
+
applied.push(key);
|
|
153866
|
+
}
|
|
153867
|
+
delete params.env.UNSAFE_OVERRIDES;
|
|
153868
|
+
return { applied, denied };
|
|
153869
|
+
}
|
|
153870
|
+
|
|
153871
|
+
// utils/payload.ts
|
|
153872
|
+
var core6 = __toESM(require_core(), 1);
|
|
153455
153873
|
import { isAbsolute as isAbsolute2, resolve as resolve2 } from "node:path";
|
|
153456
153874
|
|
|
153457
153875
|
// utils/versioning.ts
|
|
@@ -153515,7 +153933,7 @@ function resolveCwd(cwd) {
|
|
|
153515
153933
|
return workspace ? resolve2(workspace, cwd) : cwd;
|
|
153516
153934
|
}
|
|
153517
153935
|
function resolvePromptInput() {
|
|
153518
|
-
const prompt =
|
|
153936
|
+
const prompt = core6.getInput("prompt", { required: true });
|
|
153519
153937
|
let parsed2;
|
|
153520
153938
|
try {
|
|
153521
153939
|
parsed2 = JSON.parse(prompt);
|
|
@@ -153531,11 +153949,11 @@ function resolvePromptInput() {
|
|
|
153531
153949
|
}
|
|
153532
153950
|
function resolveNonPromptInputs() {
|
|
153533
153951
|
return Inputs.omit("prompt").assert({
|
|
153534
|
-
model:
|
|
153535
|
-
timeout:
|
|
153536
|
-
cwd:
|
|
153537
|
-
push:
|
|
153538
|
-
shell:
|
|
153952
|
+
model: core6.getInput("model") || void 0,
|
|
153953
|
+
timeout: core6.getInput("timeout") || void 0,
|
|
153954
|
+
cwd: core6.getInput("cwd") || void 0,
|
|
153955
|
+
push: core6.getInput("push") || void 0,
|
|
153956
|
+
shell: core6.getInput("shell") || void 0
|
|
153539
153957
|
});
|
|
153540
153958
|
}
|
|
153541
153959
|
var isPullfrog = (actor) => {
|
|
@@ -153581,10 +153999,386 @@ function resolvePayload(resolvedPromptInput, repoSettings) {
|
|
|
153581
153999
|
proxyModel: void 0
|
|
153582
154000
|
};
|
|
153583
154001
|
}
|
|
154002
|
+
function resolveOutputSchema() {
|
|
154003
|
+
const raw2 = core6.getInput("output_schema");
|
|
154004
|
+
if (!raw2) return void 0;
|
|
154005
|
+
let parsed2;
|
|
154006
|
+
try {
|
|
154007
|
+
parsed2 = JSON.parse(raw2);
|
|
154008
|
+
} catch {
|
|
154009
|
+
throw new Error(`invalid output_schema: not valid JSON`);
|
|
154010
|
+
}
|
|
154011
|
+
if (!parsed2 || typeof parsed2 !== "object" || Array.isArray(parsed2)) {
|
|
154012
|
+
throw new Error(`invalid output_schema: must be a JSON object`);
|
|
154013
|
+
}
|
|
154014
|
+
log.info("\xBB structured output schema provided \u2014 output will be required");
|
|
154015
|
+
return parsed2;
|
|
154016
|
+
}
|
|
154017
|
+
|
|
154018
|
+
// utils/proxy.ts
|
|
154019
|
+
var core8 = __toESM(require_core(), 1);
|
|
154020
|
+
|
|
154021
|
+
// utils/billingErrors.ts
|
|
154022
|
+
var BillingError = class extends Error {
|
|
154023
|
+
code;
|
|
154024
|
+
declineCode;
|
|
154025
|
+
needsReauthentication;
|
|
154026
|
+
constructor(message, opts = {}) {
|
|
154027
|
+
super(message);
|
|
154028
|
+
this.name = "BillingError";
|
|
154029
|
+
this.code = opts.code ?? null;
|
|
154030
|
+
this.declineCode = opts.declineCode ?? null;
|
|
154031
|
+
this.needsReauthentication = opts.needsReauthentication ?? false;
|
|
154032
|
+
}
|
|
154033
|
+
};
|
|
154034
|
+
var TransientError = class extends Error {
|
|
154035
|
+
constructor(message) {
|
|
154036
|
+
super(message);
|
|
154037
|
+
this.name = "TransientError";
|
|
154038
|
+
}
|
|
154039
|
+
};
|
|
154040
|
+
function billingConsoleUrl(owner, anchor) {
|
|
154041
|
+
return `https://pullfrog.com/console/${encodeURIComponent(owner)}#${anchor}`;
|
|
154042
|
+
}
|
|
154043
|
+
function formatBillingErrorSummary(error49, owner) {
|
|
154044
|
+
if (error49.code === "router_requires_card") {
|
|
154045
|
+
return [
|
|
154046
|
+
"**Add a card to start using Pullfrog Router.**",
|
|
154047
|
+
"",
|
|
154048
|
+
"Router proxies OpenRouter at raw cost \u2014 no platform markup. Add a card and we'll auto-reload your wallet so runs keep flowing.",
|
|
154049
|
+
"",
|
|
154050
|
+
`[Add a card \u2192](${billingConsoleUrl(owner, "model-access")})`
|
|
154051
|
+
].join("\n");
|
|
154052
|
+
}
|
|
154053
|
+
if (error49.code === "router_balance_exhausted") {
|
|
154054
|
+
return [
|
|
154055
|
+
"**Your Pullfrog Router balance is exhausted.**",
|
|
154056
|
+
"",
|
|
154057
|
+
"You have a card on file but auto-reload is disabled, so runs paused once your balance went past the overdraft buffer.",
|
|
154058
|
+
"",
|
|
154059
|
+
`[Top up balance \u2192](${billingConsoleUrl(owner, "billing")}) \xB7 [Enable auto-reload \u2192](${billingConsoleUrl(owner, "model-access")})`
|
|
154060
|
+
].join("\n");
|
|
154061
|
+
}
|
|
154062
|
+
if (error49.code === "router_keylimit_exhausted") {
|
|
154063
|
+
return [
|
|
154064
|
+
"**This run was cut short \u2014 your Pullfrog Router balance ran out mid-run.**",
|
|
154065
|
+
"",
|
|
154066
|
+
"OpenRouter stopped the agent because the per-run budget was exhausted. Your wallet is now negative; top up or enable auto-reload to keep runs flowing.",
|
|
154067
|
+
"",
|
|
154068
|
+
`[Top up balance \u2192](${billingConsoleUrl(owner, "billing")}) \xB7 [Enable auto-reload \u2192](${billingConsoleUrl(owner, "model-access")})`
|
|
154069
|
+
].join("\n");
|
|
154070
|
+
}
|
|
154071
|
+
if (error49.code === "router_monthly_limit") {
|
|
154072
|
+
return [
|
|
154073
|
+
"**Pullfrog Router hit its monthly spend limit.**",
|
|
154074
|
+
"",
|
|
154075
|
+
"Auto-reloads are paused for the rest of this UTC month. Ask your admin to raise the cap, or wait for it to reset at 00:00 UTC on the 1st.",
|
|
154076
|
+
"",
|
|
154077
|
+
`[Adjust limit \u2192](${billingConsoleUrl(owner, "model-access")})`
|
|
154078
|
+
].join("\n");
|
|
154079
|
+
}
|
|
154080
|
+
if (error49.needsReauthentication) {
|
|
154081
|
+
const code = error49.declineCode ?? "authentication_required";
|
|
154082
|
+
return [
|
|
154083
|
+
`**Your card issuer requires 3D Secure on every charge** (\`${code}\`).`,
|
|
154084
|
+
"",
|
|
154085
|
+
"Pullfrog can't complete a 3DS challenge from inside a workflow. Top up your Router balance once in Stripe Checkout \u2014 subsequent runs draw from the prepaid balance without re-triggering 3DS.",
|
|
154086
|
+
"",
|
|
154087
|
+
`[Top up balance \u2192](${billingConsoleUrl(owner, "billing")})`
|
|
154088
|
+
].join("\n");
|
|
154089
|
+
}
|
|
154090
|
+
if (error49.declineCode) {
|
|
154091
|
+
return [
|
|
154092
|
+
`**Your card was declined** (\`${error49.declineCode}\`).`,
|
|
154093
|
+
"",
|
|
154094
|
+
"Update your payment method and Pullfrog will retry on the next run.",
|
|
154095
|
+
"",
|
|
154096
|
+
`[Update payment method \u2192](${billingConsoleUrl(owner, "billing")})`
|
|
154097
|
+
].join("\n");
|
|
154098
|
+
}
|
|
154099
|
+
return [
|
|
154100
|
+
"**Your Pullfrog balance is empty.**",
|
|
154101
|
+
"",
|
|
154102
|
+
"Top up your balance or enable auto-reload to keep runs flowing.",
|
|
154103
|
+
"",
|
|
154104
|
+
`[Manage billing \u2192](${billingConsoleUrl(owner, "billing")})`
|
|
154105
|
+
].join("\n");
|
|
154106
|
+
}
|
|
154107
|
+
function formatTransientErrorSummary(error49, owner) {
|
|
154108
|
+
return [
|
|
154109
|
+
"**Pullfrog billing is temporarily unavailable.**",
|
|
154110
|
+
"",
|
|
154111
|
+
error49.message,
|
|
154112
|
+
"",
|
|
154113
|
+
`Usually transient \u2014 the next dispatch should succeed. If it persists, check [status.pullfrog.com](https://status.pullfrog.com) or [your console](${billingConsoleUrl(owner, "billing")}).`
|
|
154114
|
+
].join("\n");
|
|
154115
|
+
}
|
|
154116
|
+
|
|
154117
|
+
// utils/token.ts
|
|
154118
|
+
var core7 = __toESM(require_core(), 1);
|
|
154119
|
+
import assert2 from "node:assert/strict";
|
|
154120
|
+
var mcpTokenValue;
|
|
154121
|
+
function getJobToken() {
|
|
154122
|
+
const inputToken = core7.getInput("token");
|
|
154123
|
+
if (inputToken) {
|
|
154124
|
+
return inputToken;
|
|
154125
|
+
}
|
|
154126
|
+
const fallbackToken = process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
|
|
154127
|
+
if (fallbackToken) {
|
|
154128
|
+
return fallbackToken;
|
|
154129
|
+
}
|
|
154130
|
+
throw new Error("token input is required");
|
|
154131
|
+
}
|
|
154132
|
+
async function resolveTokens(params) {
|
|
154133
|
+
assert2(!mcpTokenValue, "tokens are already resolved");
|
|
154134
|
+
const externalToken = process.env.GH_TOKEN;
|
|
154135
|
+
if (externalToken) {
|
|
154136
|
+
mcpTokenValue = externalToken;
|
|
154137
|
+
if (isGitHubActions) {
|
|
154138
|
+
core7.setSecret(externalToken);
|
|
154139
|
+
}
|
|
154140
|
+
log.info("\xBB using external GH_TOKEN for both git and MCP");
|
|
154141
|
+
return {
|
|
154142
|
+
gitToken: externalToken,
|
|
154143
|
+
mcpToken: externalToken,
|
|
154144
|
+
async [Symbol.asyncDispose]() {
|
|
154145
|
+
mcpTokenValue = void 0;
|
|
154146
|
+
}
|
|
154147
|
+
};
|
|
154148
|
+
}
|
|
154149
|
+
const gitPermissions = params.push === "disabled" ? { contents: "read" } : { contents: "write", workflows: "write" };
|
|
154150
|
+
const gitToken = await acquireNewToken({ permissions: gitPermissions });
|
|
154151
|
+
if (isGitHubActions) {
|
|
154152
|
+
core7.setSecret(gitToken);
|
|
154153
|
+
}
|
|
154154
|
+
log.info(
|
|
154155
|
+
`\xBB acquired git token (${Object.entries(gitPermissions).map((e) => e.join(":")).join(", ")})`
|
|
154156
|
+
);
|
|
154157
|
+
const mcpPermissions = {
|
|
154158
|
+
contents: "write",
|
|
154159
|
+
pull_requests: "write",
|
|
154160
|
+
issues: "write",
|
|
154161
|
+
checks: "read",
|
|
154162
|
+
actions: "read"
|
|
154163
|
+
};
|
|
154164
|
+
const mcpToken = await acquireNewToken({ permissions: mcpPermissions });
|
|
154165
|
+
if (isGitHubActions) {
|
|
154166
|
+
core7.setSecret(mcpToken);
|
|
154167
|
+
}
|
|
154168
|
+
log.info(
|
|
154169
|
+
`\xBB acquired scoped MCP token (${Object.entries(mcpPermissions).map((e) => e.join(":")).join(", ")})`
|
|
154170
|
+
);
|
|
154171
|
+
mcpTokenValue = mcpToken;
|
|
154172
|
+
let disposingRef;
|
|
154173
|
+
const dispose = async () => {
|
|
154174
|
+
if (disposingRef) {
|
|
154175
|
+
return disposingRef.promise;
|
|
154176
|
+
}
|
|
154177
|
+
disposingRef = Promise.withResolvers();
|
|
154178
|
+
try {
|
|
154179
|
+
mcpTokenValue = void 0;
|
|
154180
|
+
await Promise.all([
|
|
154181
|
+
revokeGitHubInstallationToken(gitToken),
|
|
154182
|
+
revokeGitHubInstallationToken(mcpToken)
|
|
154183
|
+
]);
|
|
154184
|
+
} finally {
|
|
154185
|
+
removeSignalHandler();
|
|
154186
|
+
disposingRef.resolve();
|
|
154187
|
+
disposingRef = void 0;
|
|
154188
|
+
}
|
|
154189
|
+
};
|
|
154190
|
+
const removeSignalHandler = onExitSignal(dispose);
|
|
154191
|
+
return {
|
|
154192
|
+
gitToken,
|
|
154193
|
+
mcpToken,
|
|
154194
|
+
[Symbol.asyncDispose]: dispose
|
|
154195
|
+
};
|
|
154196
|
+
}
|
|
154197
|
+
function getGitHubInstallationToken() {
|
|
154198
|
+
assert2(mcpTokenValue, "tokens not set. call resolveTokens first.");
|
|
154199
|
+
return mcpTokenValue;
|
|
154200
|
+
}
|
|
154201
|
+
async function revokeGitHubInstallationToken(token) {
|
|
154202
|
+
const apiUrl = process.env.GITHUB_API_URL || "https://api.github.com";
|
|
154203
|
+
try {
|
|
154204
|
+
await fetch(`${apiUrl}/installation/token`, {
|
|
154205
|
+
method: "DELETE",
|
|
154206
|
+
headers: {
|
|
154207
|
+
Accept: "application/vnd.github+json",
|
|
154208
|
+
Authorization: `Bearer ${token}`,
|
|
154209
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
154210
|
+
}
|
|
154211
|
+
});
|
|
154212
|
+
log.debug("\xBB installation token revoked");
|
|
154213
|
+
} catch (error49) {
|
|
154214
|
+
log.info(
|
|
154215
|
+
`Failed to revoke installation token: ${error49 instanceof Error ? error49.message : String(error49)}`
|
|
154216
|
+
);
|
|
154217
|
+
}
|
|
154218
|
+
}
|
|
154219
|
+
|
|
154220
|
+
// utils/errorReport.ts
|
|
154221
|
+
async function reportErrorToComment(ctx) {
|
|
154222
|
+
const formattedError = ctx.title ? `${ctx.title}
|
|
154223
|
+
|
|
154224
|
+
${ctx.error}` : ctx.error;
|
|
154225
|
+
const repoContext = parseRepoContext();
|
|
154226
|
+
const octokit = createOctokit(getGitHubInstallationToken());
|
|
154227
|
+
const runId = process.env.GITHUB_RUN_ID ? Number.parseInt(process.env.GITHUB_RUN_ID, 10) : void 0;
|
|
154228
|
+
const customParts = [];
|
|
154229
|
+
if (runId) {
|
|
154230
|
+
const apiUrl = getApiUrl();
|
|
154231
|
+
customParts.push(
|
|
154232
|
+
`[Rerun failed job \u2794](${apiUrl}/trigger/${repoContext.owner}/${repoContext.name}/${runId}?action=rerun)`
|
|
154233
|
+
);
|
|
154234
|
+
}
|
|
154235
|
+
const footer = buildPullfrogFooter({
|
|
154236
|
+
triggeredBy: true,
|
|
154237
|
+
workflowRun: runId ? { owner: repoContext.owner, repo: repoContext.name, runId } : void 0,
|
|
154238
|
+
customParts,
|
|
154239
|
+
model: ctx.toolState.model,
|
|
154240
|
+
fallbackFrom: ctx.toolState.modelFallback?.from
|
|
154241
|
+
});
|
|
154242
|
+
const body = `${formattedError}${footer}`;
|
|
154243
|
+
const comment = ctx.toolState.progressComment;
|
|
154244
|
+
if (comment) {
|
|
154245
|
+
await updateProgressComment(
|
|
154246
|
+
{ octokit, owner: repoContext.owner, repo: repoContext.name },
|
|
154247
|
+
comment,
|
|
154248
|
+
body
|
|
154249
|
+
);
|
|
154250
|
+
ctx.toolState.wasUpdated = true;
|
|
154251
|
+
return;
|
|
154252
|
+
}
|
|
154253
|
+
if (!ctx.createIfMissing) return;
|
|
154254
|
+
if (!ctx.toolState.issueNumber) return;
|
|
154255
|
+
try {
|
|
154256
|
+
const created = await octokit.rest.issues.createComment({
|
|
154257
|
+
owner: repoContext.owner,
|
|
154258
|
+
repo: repoContext.name,
|
|
154259
|
+
issue_number: ctx.toolState.issueNumber,
|
|
154260
|
+
body
|
|
154261
|
+
});
|
|
154262
|
+
ctx.toolState.progressComment = { id: created.data.id, type: "issue" };
|
|
154263
|
+
ctx.toolState.wasUpdated = true;
|
|
154264
|
+
} catch (error49) {
|
|
154265
|
+
log.warning(
|
|
154266
|
+
`[errorReport] fallback comment create failed: ${error49 instanceof Error ? error49.message : String(error49)}`
|
|
154267
|
+
);
|
|
154268
|
+
}
|
|
154269
|
+
}
|
|
154270
|
+
|
|
154271
|
+
// utils/proxy.ts
|
|
154272
|
+
async function mintProxyKey(ctx) {
|
|
154273
|
+
try {
|
|
154274
|
+
const headers = await buildProxyTokenHeaders(ctx);
|
|
154275
|
+
if (!headers) return null;
|
|
154276
|
+
const response = await apiFetch({
|
|
154277
|
+
path: "/api/proxy-token",
|
|
154278
|
+
method: "POST",
|
|
154279
|
+
headers
|
|
154280
|
+
});
|
|
154281
|
+
if (response.status === 402) {
|
|
154282
|
+
const body = await response.json().catch(() => null);
|
|
154283
|
+
throw new BillingError(body?.error ?? "insufficient balance", {
|
|
154284
|
+
code: body?.code ?? null,
|
|
154285
|
+
declineCode: body?.declineCode ?? null,
|
|
154286
|
+
needsReauthentication: body?.needsReauthentication ?? false
|
|
154287
|
+
});
|
|
154288
|
+
}
|
|
154289
|
+
if (response.status === 503) {
|
|
154290
|
+
const body = await response.json().catch(() => null);
|
|
154291
|
+
throw new TransientError(
|
|
154292
|
+
body?.error ?? "billing service temporarily unavailable \u2014 retry shortly"
|
|
154293
|
+
);
|
|
154294
|
+
}
|
|
154295
|
+
if (!response.ok) {
|
|
154296
|
+
log.warning(`proxy key mint failed (${response.status})`);
|
|
154297
|
+
return null;
|
|
154298
|
+
}
|
|
154299
|
+
const data = await response.json();
|
|
154300
|
+
return data.key;
|
|
154301
|
+
} catch (error49) {
|
|
154302
|
+
if (error49 instanceof BillingError) throw error49;
|
|
154303
|
+
if (error49 instanceof TransientError) throw error49;
|
|
154304
|
+
log.warning(`proxy key mint error: ${error49 instanceof Error ? error49.message : String(error49)}`);
|
|
154305
|
+
return null;
|
|
154306
|
+
} finally {
|
|
154307
|
+
delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
154308
|
+
delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
154309
|
+
}
|
|
154310
|
+
}
|
|
154311
|
+
async function buildProxyTokenHeaders(ctx) {
|
|
154312
|
+
if (ctx.oidcCredentials) {
|
|
154313
|
+
process.env.ACTIONS_ID_TOKEN_REQUEST_URL = ctx.oidcCredentials.requestUrl;
|
|
154314
|
+
process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = ctx.oidcCredentials.requestToken;
|
|
154315
|
+
const oidcToken = await core8.getIDToken("pullfrog-api");
|
|
154316
|
+
delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
154317
|
+
delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
154318
|
+
return { Authorization: `Bearer ${oidcToken}` };
|
|
154319
|
+
}
|
|
154320
|
+
if (isLocalApiUrl()) {
|
|
154321
|
+
log.info(`\xBB proxy: dev bypass (x-dev-repo) for ${ctx.repo.owner}/${ctx.repo.name}`);
|
|
154322
|
+
return { "x-dev-repo": `${ctx.repo.owner}/${ctx.repo.name}` };
|
|
154323
|
+
}
|
|
154324
|
+
return null;
|
|
154325
|
+
}
|
|
154326
|
+
async function resolveProxyModel(ctx) {
|
|
154327
|
+
if (process.env.PULLFROG_MODEL?.trim()) return;
|
|
154328
|
+
if (!ctx.proxyModel) return;
|
|
154329
|
+
if (!ctx.oidcCredentials && !isLocalApiUrl()) {
|
|
154330
|
+
log.warning("\xBB proxy requested but no OIDC credentials available \u2014 skipping");
|
|
154331
|
+
return;
|
|
154332
|
+
}
|
|
154333
|
+
const key = await mintProxyKey({ oidcCredentials: ctx.oidcCredentials, repo: ctx.repo });
|
|
154334
|
+
if (!key) return;
|
|
154335
|
+
process.env.OPENROUTER_API_KEY = key;
|
|
154336
|
+
core8.setSecret(key);
|
|
154337
|
+
ctx.payload.proxyModel = ctx.proxyModel;
|
|
154338
|
+
const label = ctx.oss ? "oss" : "router";
|
|
154339
|
+
log.info(`\xBB proxy: ${label} \u2192 ${ctx.proxyModel}`);
|
|
154340
|
+
}
|
|
154341
|
+
async function runProxyResolution(ctx) {
|
|
154342
|
+
try {
|
|
154343
|
+
await resolveProxyModel({
|
|
154344
|
+
payload: ctx.payload,
|
|
154345
|
+
oss: ctx.oss,
|
|
154346
|
+
proxyModel: ctx.proxyModel,
|
|
154347
|
+
oidcCredentials: ctx.oidcCredentials,
|
|
154348
|
+
repo: ctx.repo
|
|
154349
|
+
});
|
|
154350
|
+
} catch (error49) {
|
|
154351
|
+
if (error49 instanceof BillingError) {
|
|
154352
|
+
const summary2 = formatBillingErrorSummary(error49, ctx.repo.owner);
|
|
154353
|
+
await writeSummary(summary2).catch(() => {
|
|
154354
|
+
});
|
|
154355
|
+
await reportErrorToComment({
|
|
154356
|
+
toolState: ctx.toolState,
|
|
154357
|
+
error: summary2,
|
|
154358
|
+
createIfMissing: true
|
|
154359
|
+
}).catch(() => {
|
|
154360
|
+
});
|
|
154361
|
+
throw error49;
|
|
154362
|
+
}
|
|
154363
|
+
if (error49 instanceof TransientError) {
|
|
154364
|
+
const summary2 = formatTransientErrorSummary(error49, ctx.repo.owner);
|
|
154365
|
+
await writeSummary(summary2).catch(() => {
|
|
154366
|
+
});
|
|
154367
|
+
await reportErrorToComment({
|
|
154368
|
+
toolState: ctx.toolState,
|
|
154369
|
+
error: summary2,
|
|
154370
|
+
createIfMissing: true
|
|
154371
|
+
}).catch(() => {
|
|
154372
|
+
});
|
|
154373
|
+
throw error49;
|
|
154374
|
+
}
|
|
154375
|
+
throw error49;
|
|
154376
|
+
}
|
|
154377
|
+
}
|
|
153584
154378
|
|
|
153585
154379
|
// utils/prSummary.ts
|
|
153586
154380
|
import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
|
|
153587
|
-
import { dirname as dirname5, join as
|
|
154381
|
+
import { dirname as dirname5, join as join16 } from "node:path";
|
|
153588
154382
|
var SUMMARY_FILE_NAME = "pullfrog-summary.md";
|
|
153589
154383
|
var SUMMARY_SCAFFOLD = `# PR summary
|
|
153590
154384
|
|
|
@@ -153594,7 +154388,7 @@ var SUMMARY_SCAFFOLD = `# PR summary
|
|
|
153594
154388
|
var MIN_SNAPSHOT_LENGTH = 60;
|
|
153595
154389
|
var MAX_SNAPSHOT_LENGTH = 32768;
|
|
153596
154390
|
function summaryFilePath(tmpdir3) {
|
|
153597
|
-
return
|
|
154391
|
+
return join16(tmpdir3, SUMMARY_FILE_NAME);
|
|
153598
154392
|
}
|
|
153599
154393
|
async function seedSummaryFile(params) {
|
|
153600
154394
|
const path3 = summaryFilePath(params.tmpdir);
|
|
@@ -153615,76 +154409,43 @@ async function readSummaryFile(path3) {
|
|
|
153615
154409
|
if (trimmed.length > MAX_SNAPSHOT_LENGTH) return trimmed.slice(0, MAX_SNAPSHOT_LENGTH);
|
|
153616
154410
|
return trimmed;
|
|
153617
154411
|
}
|
|
153618
|
-
|
|
153619
|
-
|
|
153620
|
-
var RE_REVIEW_PREAMBLE = "Incrementally re-review the new commits on this pull request. Use the IncrementalReview mode.";
|
|
153621
|
-
async function postReviewCleanup(ctx) {
|
|
153622
|
-
const review = ctx.toolState.review;
|
|
153623
|
-
if (!review) return;
|
|
153624
|
-
delete ctx.toolState.review;
|
|
153625
|
-
await bestEffort(() => reportReviewNodeId(ctx, { nodeId: review.nodeId }), "reportReviewNodeId");
|
|
153626
|
-
if (review.reviewedSha) {
|
|
153627
|
-
await bestEffort(
|
|
153628
|
-
() => dispatchFollowUpReReview(ctx, review.reviewedSha),
|
|
153629
|
-
"follow-up re-review dispatch"
|
|
153630
|
-
);
|
|
153631
|
-
}
|
|
153632
|
-
}
|
|
153633
|
-
async function bestEffort(fn2, label) {
|
|
154412
|
+
async function fetchPreviousSnapshot(ctx, prNumber) {
|
|
154413
|
+
if (!ctx.githubInstallationToken) return null;
|
|
153634
154414
|
try {
|
|
153635
|
-
await
|
|
153636
|
-
|
|
153637
|
-
|
|
154415
|
+
const response = await apiFetch({
|
|
154416
|
+
path: `/api/repo/${ctx.repo.owner}/${ctx.repo.name}/pr/${prNumber}/summary-comment`,
|
|
154417
|
+
method: "GET",
|
|
154418
|
+
headers: { authorization: `Bearer ${ctx.githubInstallationToken}` },
|
|
154419
|
+
signal: AbortSignal.timeout(1e4)
|
|
154420
|
+
});
|
|
154421
|
+
if (!response.ok) return null;
|
|
154422
|
+
const data = await response.json();
|
|
154423
|
+
return typeof data.snapshot === "string" && data.snapshot.length > 0 ? data.snapshot : null;
|
|
154424
|
+
} catch {
|
|
154425
|
+
return null;
|
|
153638
154426
|
}
|
|
153639
154427
|
}
|
|
153640
|
-
async function
|
|
153641
|
-
const
|
|
153642
|
-
if (!
|
|
153643
|
-
|
|
153644
|
-
|
|
153645
|
-
|
|
153646
|
-
|
|
153647
|
-
|
|
153648
|
-
|
|
153649
|
-
if (pr.data.state !== "open") return;
|
|
153650
|
-
if (pr.data.draft) return;
|
|
153651
|
-
log.info(
|
|
153652
|
-
`safety net: pr HEAD moved from ${reviewedSha.slice(0, 7)} to ${pr.data.head.sha.slice(0, 7)} and agent did not review inline \u2014 dispatching follow-up re-review`
|
|
153653
|
-
);
|
|
153654
|
-
const event = {
|
|
153655
|
-
trigger: "pull_request_synchronize",
|
|
153656
|
-
issue_number: issueNumber,
|
|
153657
|
-
is_pr: true,
|
|
153658
|
-
title: pr.data.title,
|
|
153659
|
-
body: null,
|
|
153660
|
-
branch: pr.data.head.ref,
|
|
153661
|
-
before_sha: reviewedSha,
|
|
153662
|
-
silent: true
|
|
153663
|
-
};
|
|
153664
|
-
if (ctx.payload.event.authorPermission) {
|
|
153665
|
-
event.authorPermission = ctx.payload.event.authorPermission;
|
|
154428
|
+
async function persistSummary(ctx) {
|
|
154429
|
+
const filePath = ctx.toolState.summaryFilePath;
|
|
154430
|
+
if (!filePath) return;
|
|
154431
|
+
if (ctx.toolState.summaryPersistAttempted) return;
|
|
154432
|
+
ctx.toolState.summaryPersistAttempted = true;
|
|
154433
|
+
const snapshot2 = await readSummaryFile(filePath);
|
|
154434
|
+
if (!snapshot2) {
|
|
154435
|
+
log.debug(`pr summary tmpfile missing or invalid at ${filePath} \u2014 skipping persist`);
|
|
154436
|
+
return;
|
|
153666
154437
|
}
|
|
153667
|
-
const
|
|
153668
|
-
|
|
153669
|
-
|
|
153670
|
-
|
|
153671
|
-
|
|
153672
|
-
|
|
153673
|
-
|
|
153674
|
-
}
|
|
153675
|
-
|
|
153676
|
-
owner: ctx.repo.owner,
|
|
153677
|
-
repo: ctx.repo.name,
|
|
153678
|
-
workflow_id: getCurrentWorkflowFilename(),
|
|
153679
|
-
ref: pr.data.base.repo.default_branch,
|
|
153680
|
-
inputs: { prompt: JSON.stringify(payload) }
|
|
154438
|
+
const seed = ctx.toolState.summarySeed?.trim();
|
|
154439
|
+
if (seed !== void 0 && snapshot2 === seed) {
|
|
154440
|
+
log.warning(
|
|
154441
|
+
"\xBB pr summary tmpfile unchanged from seed \u2014 skipping persist (agent did not edit it)"
|
|
154442
|
+
);
|
|
154443
|
+
return;
|
|
154444
|
+
}
|
|
154445
|
+
await patchWorkflowRunFields(ctx, { summarySnapshot: snapshot2 }).catch((err) => {
|
|
154446
|
+
log.debug(`pr summary persist failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
153681
154447
|
});
|
|
153682
154448
|
}
|
|
153683
|
-
function getCurrentWorkflowFilename() {
|
|
153684
|
-
const ref = process.env.GITHUB_WORKFLOW_REF ?? "";
|
|
153685
|
-
const match3 = ref.match(/\/([^/]+)@/);
|
|
153686
|
-
return match3?.[1] ?? "pullfrog.yml";
|
|
153687
|
-
}
|
|
153688
154449
|
|
|
153689
154450
|
// utils/run.ts
|
|
153690
154451
|
async function handleAgentResult(ctx) {
|
|
@@ -153720,10 +154481,10 @@ async function handleAgentResult(ctx) {
|
|
|
153720
154481
|
};
|
|
153721
154482
|
}
|
|
153722
154483
|
|
|
154484
|
+
// utils/runContextData.ts
|
|
154485
|
+
var core9 = __toESM(require_core(), 1);
|
|
154486
|
+
|
|
153723
154487
|
// utils/runContext.ts
|
|
153724
|
-
function isInfraCovered(params) {
|
|
153725
|
-
return params.isOss || params.plan === "payg";
|
|
153726
|
-
}
|
|
153727
154488
|
var defaultSettings = {
|
|
153728
154489
|
model: null,
|
|
153729
154490
|
modes: [],
|
|
@@ -153793,13 +154554,12 @@ async function fetchRunContext(params) {
|
|
|
153793
154554
|
}
|
|
153794
154555
|
|
|
153795
154556
|
// utils/runContextData.ts
|
|
153796
|
-
var core6 = __toESM(require_core(), 1);
|
|
153797
154557
|
async function resolveRunContextData(params) {
|
|
153798
154558
|
log.info(`\xBB running Pullfrog v${package_default.version}...`);
|
|
153799
154559
|
const repoContext = parseRepoContext();
|
|
153800
154560
|
let oidcToken;
|
|
153801
154561
|
try {
|
|
153802
|
-
oidcToken = await
|
|
154562
|
+
oidcToken = await core9.getIDToken("pullfrog-api");
|
|
153803
154563
|
} catch {
|
|
153804
154564
|
}
|
|
153805
154565
|
const [repoResponse, runContext] = await Promise.all([
|
|
@@ -153821,13 +154581,240 @@ async function resolveRunContextData(params) {
|
|
|
153821
154581
|
};
|
|
153822
154582
|
}
|
|
153823
154583
|
|
|
154584
|
+
// utils/runErrorRenderer.ts
|
|
154585
|
+
function renderRunError(input) {
|
|
154586
|
+
const billingError = isRouterKeylimitExhaustedError(input.errorMessage) ? new BillingError(input.errorMessage, { code: "router_keylimit_exhausted" }) : null;
|
|
154587
|
+
if (billingError) {
|
|
154588
|
+
const body = formatBillingErrorSummary(billingError, input.repo.owner);
|
|
154589
|
+
return { summary: body, comment: body };
|
|
154590
|
+
}
|
|
154591
|
+
const isHang = input.errorMessage.startsWith("activity timeout") || input.errorMessage.startsWith("agent still pending");
|
|
154592
|
+
const hangBody = isHang ? formatAgentHangBody({
|
|
154593
|
+
diagnostic: input.agentDiagnostic,
|
|
154594
|
+
isHang: true,
|
|
154595
|
+
errorMessage: input.errorMessage
|
|
154596
|
+
}) : null;
|
|
154597
|
+
const apiKeySource = hangBody ?? input.errorMessage;
|
|
154598
|
+
const apiKeyErrorSummary = isApiKeyAuthError(apiKeySource) ? formatApiKeyErrorSummary({
|
|
154599
|
+
owner: input.repo.owner,
|
|
154600
|
+
name: input.repo.name,
|
|
154601
|
+
raw: apiKeySource
|
|
154602
|
+
}) : null;
|
|
154603
|
+
if (apiKeyErrorSummary) {
|
|
154604
|
+
return { summary: apiKeyErrorSummary, comment: apiKeyErrorSummary };
|
|
154605
|
+
}
|
|
154606
|
+
if (hangBody) {
|
|
154607
|
+
return {
|
|
154608
|
+
summary: `### \u274C Pullfrog failed
|
|
154609
|
+
|
|
154610
|
+
${hangBody}`,
|
|
154611
|
+
comment: hangBody
|
|
154612
|
+
};
|
|
154613
|
+
}
|
|
154614
|
+
return {
|
|
154615
|
+
summary: `### \u274C Pullfrog failed
|
|
154616
|
+
|
|
154617
|
+
\`\`\`
|
|
154618
|
+
${input.errorMessage}
|
|
154619
|
+
\`\`\``,
|
|
154620
|
+
comment: input.errorMessage
|
|
154621
|
+
};
|
|
154622
|
+
}
|
|
154623
|
+
|
|
154624
|
+
// utils/runLifecycle.ts
|
|
154625
|
+
var core10 = __toESM(require_core(), 1);
|
|
154626
|
+
|
|
154627
|
+
// utils/reviewCleanup.ts
|
|
154628
|
+
var RE_REVIEW_PREAMBLE = "Incrementally re-review the new commits on this pull request. Use the IncrementalReview mode.";
|
|
154629
|
+
async function postReviewCleanup(ctx) {
|
|
154630
|
+
const review = ctx.toolState.review;
|
|
154631
|
+
if (!review) return;
|
|
154632
|
+
delete ctx.toolState.review;
|
|
154633
|
+
await bestEffort(() => reportReviewNodeId(ctx, { nodeId: review.nodeId }), "reportReviewNodeId");
|
|
154634
|
+
if (review.reviewedSha) {
|
|
154635
|
+
await bestEffort(
|
|
154636
|
+
() => dispatchFollowUpReReview(ctx, review.reviewedSha),
|
|
154637
|
+
"follow-up re-review dispatch"
|
|
154638
|
+
);
|
|
154639
|
+
}
|
|
154640
|
+
}
|
|
154641
|
+
async function bestEffort(fn2, label) {
|
|
154642
|
+
try {
|
|
154643
|
+
await fn2();
|
|
154644
|
+
} catch (error49) {
|
|
154645
|
+
log.debug(`${label} failed: ${error49}`);
|
|
154646
|
+
}
|
|
154647
|
+
}
|
|
154648
|
+
async function dispatchFollowUpReReview(ctx, reviewedSha) {
|
|
154649
|
+
const issueNumber = ctx.payload.event.issue_number;
|
|
154650
|
+
if (!issueNumber) return;
|
|
154651
|
+
const pr = await ctx.octokit.rest.pulls.get({
|
|
154652
|
+
owner: ctx.repo.owner,
|
|
154653
|
+
repo: ctx.repo.name,
|
|
154654
|
+
pull_number: issueNumber
|
|
154655
|
+
});
|
|
154656
|
+
if (pr.data.head.sha === reviewedSha) return;
|
|
154657
|
+
if (pr.data.state !== "open") return;
|
|
154658
|
+
if (pr.data.draft) return;
|
|
154659
|
+
log.info(
|
|
154660
|
+
`safety net: pr HEAD moved from ${reviewedSha.slice(0, 7)} to ${pr.data.head.sha.slice(0, 7)} and agent did not review inline \u2014 dispatching follow-up re-review`
|
|
154661
|
+
);
|
|
154662
|
+
const event = {
|
|
154663
|
+
trigger: "pull_request_synchronize",
|
|
154664
|
+
issue_number: issueNumber,
|
|
154665
|
+
is_pr: true,
|
|
154666
|
+
title: pr.data.title,
|
|
154667
|
+
body: null,
|
|
154668
|
+
branch: pr.data.head.ref,
|
|
154669
|
+
before_sha: reviewedSha,
|
|
154670
|
+
silent: true
|
|
154671
|
+
};
|
|
154672
|
+
if (ctx.payload.event.authorPermission) {
|
|
154673
|
+
event.authorPermission = ctx.payload.event.authorPermission;
|
|
154674
|
+
}
|
|
154675
|
+
const payload = {
|
|
154676
|
+
"~pullfrog": true,
|
|
154677
|
+
version: ctx.payload.version,
|
|
154678
|
+
model: ctx.payload.model,
|
|
154679
|
+
prompt: "",
|
|
154680
|
+
eventInstructions: RE_REVIEW_PREAMBLE,
|
|
154681
|
+
event
|
|
154682
|
+
};
|
|
154683
|
+
await ctx.octokit.rest.actions.createWorkflowDispatch({
|
|
154684
|
+
owner: ctx.repo.owner,
|
|
154685
|
+
repo: ctx.repo.name,
|
|
154686
|
+
workflow_id: getCurrentWorkflowFilename(),
|
|
154687
|
+
ref: pr.data.base.repo.default_branch,
|
|
154688
|
+
inputs: { prompt: JSON.stringify(payload) }
|
|
154689
|
+
});
|
|
154690
|
+
}
|
|
154691
|
+
function getCurrentWorkflowFilename() {
|
|
154692
|
+
const ref = process.env.GITHUB_WORKFLOW_REF ?? "";
|
|
154693
|
+
const match3 = ref.match(/\/([^/]+)@/);
|
|
154694
|
+
return match3?.[1] ?? "pullfrog.yml";
|
|
154695
|
+
}
|
|
154696
|
+
|
|
154697
|
+
// utils/runLifecycle.ts
|
|
154698
|
+
async function persistRunArtifacts(toolContext) {
|
|
154699
|
+
await postReviewCleanup(toolContext).catch((error49) => {
|
|
154700
|
+
log.debug(`post-review cleanup failed: ${error49}`);
|
|
154701
|
+
});
|
|
154702
|
+
await persistSummary(toolContext);
|
|
154703
|
+
await persistLearnings(toolContext);
|
|
154704
|
+
}
|
|
154705
|
+
async function finalizeSuccessRun(input) {
|
|
154706
|
+
await persistRunArtifacts(input.toolContext);
|
|
154707
|
+
if (!input.result.success && input.toolState.progressComment) {
|
|
154708
|
+
const rawError = input.result.error || "agent run failed";
|
|
154709
|
+
const errorBody = isApiKeyAuthError(rawError) ? formatApiKeyErrorSummary({
|
|
154710
|
+
owner: input.repo.owner,
|
|
154711
|
+
name: input.repo.name,
|
|
154712
|
+
raw: rawError
|
|
154713
|
+
}) : rawError;
|
|
154714
|
+
await reportErrorToComment({ toolState: input.toolState, error: errorBody }).catch((error49) => {
|
|
154715
|
+
log.debug(`failure error report failed: ${error49}`);
|
|
154716
|
+
});
|
|
154717
|
+
}
|
|
154718
|
+
if (input.result.success && input.toolState.progressComment && !input.toolState.finalSummaryWritten) {
|
|
154719
|
+
await deleteProgressComment(input.toolContext).catch((error49) => {
|
|
154720
|
+
log.debug(`stranded progress comment cleanup failed: ${error49}`);
|
|
154721
|
+
});
|
|
154722
|
+
}
|
|
154723
|
+
try {
|
|
154724
|
+
const usageSummary = formatUsageSummary(input.toolState.usageEntries);
|
|
154725
|
+
const body = input.toolState.lastProgressBody || input.result.output;
|
|
154726
|
+
const parts = [body, usageSummary].filter(Boolean);
|
|
154727
|
+
if (parts.length > 0) {
|
|
154728
|
+
await writeSummary(parts.join("\n\n"));
|
|
154729
|
+
}
|
|
154730
|
+
} catch (error49) {
|
|
154731
|
+
log.debug(`job summary write failed: ${error49}`);
|
|
154732
|
+
}
|
|
154733
|
+
if (input.toolState.output) {
|
|
154734
|
+
log.info(`::pullfrog-output::${Buffer.from(input.toolState.output).toString("base64")}`);
|
|
154735
|
+
core10.setOutput("result", input.toolState.output);
|
|
154736
|
+
}
|
|
154737
|
+
}
|
|
154738
|
+
async function writeRunErrorOutputs(input) {
|
|
154739
|
+
try {
|
|
154740
|
+
const usageSummary = formatUsageSummary(input.toolState.usageEntries);
|
|
154741
|
+
const parts = [input.rendered.summary, input.toolState.lastProgressBody, usageSummary].filter(
|
|
154742
|
+
Boolean
|
|
154743
|
+
);
|
|
154744
|
+
await writeSummary(parts.join("\n\n"));
|
|
154745
|
+
} catch {
|
|
154746
|
+
}
|
|
154747
|
+
try {
|
|
154748
|
+
await reportErrorToComment({ toolState: input.toolState, error: input.rendered.comment });
|
|
154749
|
+
} catch {
|
|
154750
|
+
}
|
|
154751
|
+
}
|
|
154752
|
+
|
|
154753
|
+
// utils/time.ts
|
|
154754
|
+
var TIMEOUT_DISABLED = "none";
|
|
154755
|
+
var TIME_STRING_REGEX = /^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/;
|
|
154756
|
+
function parseTimeString(input) {
|
|
154757
|
+
const match3 = input.match(TIME_STRING_REGEX);
|
|
154758
|
+
if (!match3 || !match3[1] && !match3[2] && !match3[3]) return null;
|
|
154759
|
+
const hours = parseInt(match3[1] || "0", 10);
|
|
154760
|
+
const minutes = parseInt(match3[2] || "0", 10);
|
|
154761
|
+
const seconds = parseInt(match3[3] || "0", 10);
|
|
154762
|
+
return (hours * 3600 + minutes * 60 + seconds) * 1e3;
|
|
154763
|
+
}
|
|
154764
|
+
var TIMEOUT_MAX_MS = 2147483647;
|
|
154765
|
+
function resolveTimeoutMs(input) {
|
|
154766
|
+
if (!input) return null;
|
|
154767
|
+
const parsed2 = parseTimeString(input);
|
|
154768
|
+
if (parsed2 === null || parsed2 <= 0 || parsed2 > TIMEOUT_MAX_MS) return null;
|
|
154769
|
+
return parsed2;
|
|
154770
|
+
}
|
|
154771
|
+
|
|
154772
|
+
// utils/runStartupLog.ts
|
|
154773
|
+
function resolveTimeoutForLog(timeout) {
|
|
154774
|
+
if (!timeout) return "1h (default)";
|
|
154775
|
+
if (timeout === TIMEOUT_DISABLED) return "none (disabled)";
|
|
154776
|
+
return timeout;
|
|
154777
|
+
}
|
|
154778
|
+
function resolveModelForLog(ctx) {
|
|
154779
|
+
const envModel = process.env.PULLFROG_MODEL?.trim();
|
|
154780
|
+
if (envModel) return `${envModel} (override via PULLFROG_MODEL)`;
|
|
154781
|
+
if (ctx.payload.proxyModel) return `${ctx.payload.proxyModel} (proxy)`;
|
|
154782
|
+
if (ctx.resolvedModel && ctx.payload.model && ctx.payload.model !== ctx.resolvedModel) {
|
|
154783
|
+
return `${ctx.resolvedModel} (resolved from ${ctx.payload.model})`;
|
|
154784
|
+
}
|
|
154785
|
+
if (ctx.resolvedModel) return ctx.resolvedModel;
|
|
154786
|
+
if (ctx.payload.model) return `${ctx.payload.model} (unresolved)`;
|
|
154787
|
+
return "auto";
|
|
154788
|
+
}
|
|
154789
|
+
function resolveAgentForLog(ctx) {
|
|
154790
|
+
const envAgent = process.env.PULLFROG_AGENT?.trim();
|
|
154791
|
+
if (envAgent && envAgent === ctx.agentName) {
|
|
154792
|
+
return `${ctx.agentName} (override via PULLFROG_AGENT)`;
|
|
154793
|
+
}
|
|
154794
|
+
if (ctx.agentName === "claude" && ctx.resolvedModel) {
|
|
154795
|
+
return `${ctx.agentName} (auto-selected for ${ctx.resolvedModel})`;
|
|
154796
|
+
}
|
|
154797
|
+
return ctx.agentName;
|
|
154798
|
+
}
|
|
154799
|
+
function logRunStartup(ctx) {
|
|
154800
|
+
log.info(
|
|
154801
|
+
`\xBB model: ${resolveModelForLog({ payload: ctx.payload, resolvedModel: ctx.resolvedModel })}`
|
|
154802
|
+
);
|
|
154803
|
+
log.info(
|
|
154804
|
+
`\xBB agent: ${resolveAgentForLog({ agentName: ctx.agentName, resolvedModel: ctx.resolvedModel })}`
|
|
154805
|
+
);
|
|
154806
|
+
log.info(`\xBB push: ${ctx.payload.push}`);
|
|
154807
|
+
log.info(`\xBB shell: ${ctx.payload.shell}`);
|
|
154808
|
+
log.info(`\xBB timeout: ${resolveTimeoutForLog(ctx.payload.timeout)}`);
|
|
154809
|
+
}
|
|
154810
|
+
|
|
153824
154811
|
// utils/setup.ts
|
|
153825
154812
|
import { execFileSync as execFileSync5, execSync as execSync3 } from "node:child_process";
|
|
153826
154813
|
import { mkdtempSync } from "node:fs";
|
|
153827
154814
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
153828
|
-
import { join as
|
|
154815
|
+
import { join as join17 } from "node:path";
|
|
153829
154816
|
function createTempDirectory() {
|
|
153830
|
-
const sharedTempDir = mkdtempSync(
|
|
154817
|
+
const sharedTempDir = mkdtempSync(join17(tmpdir2(), "pullfrog-"));
|
|
153831
154818
|
process.env.PULLFROG_TEMP_DIR = sharedTempDir;
|
|
153832
154819
|
log.info(`\xBB created temp dir at ${sharedTempDir}`);
|
|
153833
154820
|
return sharedTempDir;
|
|
@@ -153931,25 +154918,6 @@ async function setupGit(params) {
|
|
|
153931
154918
|
log.info("\xBB git authentication configured");
|
|
153932
154919
|
}
|
|
153933
154920
|
|
|
153934
|
-
// utils/time.ts
|
|
153935
|
-
var TIMEOUT_DISABLED = "none";
|
|
153936
|
-
var TIME_STRING_REGEX = /^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/;
|
|
153937
|
-
function parseTimeString(input) {
|
|
153938
|
-
const match3 = input.match(TIME_STRING_REGEX);
|
|
153939
|
-
if (!match3 || !match3[1] && !match3[2] && !match3[3]) return null;
|
|
153940
|
-
const hours = parseInt(match3[1] || "0", 10);
|
|
153941
|
-
const minutes = parseInt(match3[2] || "0", 10);
|
|
153942
|
-
const seconds = parseInt(match3[3] || "0", 10);
|
|
153943
|
-
return (hours * 3600 + minutes * 60 + seconds) * 1e3;
|
|
153944
|
-
}
|
|
153945
|
-
var TIMEOUT_MAX_MS = 2147483647;
|
|
153946
|
-
function resolveTimeoutMs(input) {
|
|
153947
|
-
if (!input) return null;
|
|
153948
|
-
const parsed2 = parseTimeString(input);
|
|
153949
|
-
if (parsed2 === null || parsed2 <= 0 || parsed2 > TIMEOUT_MAX_MS) return null;
|
|
153950
|
-
return parsed2;
|
|
153951
|
-
}
|
|
153952
|
-
|
|
153953
154921
|
// utils/todoTracking.ts
|
|
153954
154922
|
function isValidTodoStatus(value2) {
|
|
153955
154923
|
return value2 === "pending" || value2 === "in_progress" || value2 === "completed" || value2 === "cancelled";
|
|
@@ -154086,305 +155054,42 @@ async function resolveRun(params) {
|
|
|
154086
155054
|
let jobId;
|
|
154087
155055
|
const jobName = process.env.GITHUB_JOB;
|
|
154088
155056
|
if (jobName && runId) {
|
|
154089
|
-
|
|
154090
|
-
|
|
154091
|
-
|
|
154092
|
-
|
|
154093
|
-
|
|
154094
|
-
|
|
154095
|
-
|
|
154096
|
-
|
|
154097
|
-
|
|
155057
|
+
try {
|
|
155058
|
+
const jobs = await params.octokit.rest.actions.listJobsForWorkflowRun({
|
|
155059
|
+
owner,
|
|
155060
|
+
repo,
|
|
155061
|
+
run_id: runId
|
|
155062
|
+
});
|
|
155063
|
+
const matchingJob = jobs.data.jobs.find((job) => job.name === jobName);
|
|
155064
|
+
if (matchingJob) {
|
|
155065
|
+
jobId = String(matchingJob.id);
|
|
155066
|
+
log.debug(`\xBB found job ID: ${jobId}`);
|
|
155067
|
+
}
|
|
155068
|
+
} catch (err) {
|
|
155069
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
155070
|
+
log.debug(`\xBB listJobsForWorkflowRun failed (jobId stays undefined): ${msg}`);
|
|
154098
155071
|
}
|
|
154099
155072
|
}
|
|
154100
155073
|
return { runId, jobId };
|
|
154101
155074
|
}
|
|
154102
155075
|
|
|
154103
155076
|
// main.ts
|
|
154104
|
-
function resolveOutputSchema() {
|
|
154105
|
-
const raw2 = core7.getInput("output_schema");
|
|
154106
|
-
if (!raw2) return void 0;
|
|
154107
|
-
let parsed2;
|
|
154108
|
-
try {
|
|
154109
|
-
parsed2 = JSON.parse(raw2);
|
|
154110
|
-
} catch {
|
|
154111
|
-
throw new Error(`invalid output_schema: not valid JSON`);
|
|
154112
|
-
}
|
|
154113
|
-
if (!parsed2 || typeof parsed2 !== "object" || Array.isArray(parsed2)) {
|
|
154114
|
-
throw new Error(`invalid output_schema: must be a JSON object`);
|
|
154115
|
-
}
|
|
154116
|
-
log.info("\xBB structured output schema provided \u2014 output will be required");
|
|
154117
|
-
return parsed2;
|
|
154118
|
-
}
|
|
154119
|
-
function resolveTimeoutForLog(timeout) {
|
|
154120
|
-
if (!timeout) return "1h (default)";
|
|
154121
|
-
if (timeout === TIMEOUT_DISABLED) return "none (disabled)";
|
|
154122
|
-
return timeout;
|
|
154123
|
-
}
|
|
154124
|
-
function resolveModelForLog(ctx) {
|
|
154125
|
-
const envModel = process.env.PULLFROG_MODEL?.trim();
|
|
154126
|
-
if (envModel) return `${envModel} (override via PULLFROG_MODEL)`;
|
|
154127
|
-
if (ctx.payload.proxyModel) return `${ctx.payload.proxyModel} (proxy)`;
|
|
154128
|
-
if (ctx.resolvedModel && ctx.payload.model && ctx.payload.model !== ctx.resolvedModel) {
|
|
154129
|
-
return `${ctx.resolvedModel} (resolved from ${ctx.payload.model})`;
|
|
154130
|
-
}
|
|
154131
|
-
if (ctx.resolvedModel) return ctx.resolvedModel;
|
|
154132
|
-
if (ctx.payload.model) return `${ctx.payload.model} (unresolved)`;
|
|
154133
|
-
return "auto";
|
|
154134
|
-
}
|
|
154135
|
-
function resolveAgentForLog(ctx) {
|
|
154136
|
-
const envAgent = process.env.PULLFROG_AGENT?.trim();
|
|
154137
|
-
if (envAgent && envAgent === ctx.agentName) {
|
|
154138
|
-
return `${ctx.agentName} (override via PULLFROG_AGENT)`;
|
|
154139
|
-
}
|
|
154140
|
-
if (ctx.agentName === "claude" && ctx.resolvedModel) {
|
|
154141
|
-
return `${ctx.agentName} (auto-selected for ${ctx.resolvedModel})`;
|
|
154142
|
-
}
|
|
154143
|
-
return ctx.agentName;
|
|
154144
|
-
}
|
|
154145
|
-
var BillingError = class extends Error {
|
|
154146
|
-
code;
|
|
154147
|
-
declineCode;
|
|
154148
|
-
needsReauthentication;
|
|
154149
|
-
constructor(message, opts = {}) {
|
|
154150
|
-
super(message);
|
|
154151
|
-
this.name = "BillingError";
|
|
154152
|
-
this.code = opts.code ?? null;
|
|
154153
|
-
this.declineCode = opts.declineCode ?? null;
|
|
154154
|
-
this.needsReauthentication = opts.needsReauthentication ?? false;
|
|
154155
|
-
}
|
|
154156
|
-
};
|
|
154157
|
-
var TransientError = class extends Error {
|
|
154158
|
-
constructor(message) {
|
|
154159
|
-
super(message);
|
|
154160
|
-
this.name = "TransientError";
|
|
154161
|
-
}
|
|
154162
|
-
};
|
|
154163
|
-
function billingConsoleUrl(owner, anchor) {
|
|
154164
|
-
return `https://pullfrog.com/console/${encodeURIComponent(owner)}#${anchor}`;
|
|
154165
|
-
}
|
|
154166
|
-
function formatBillingErrorSummary(error49, owner) {
|
|
154167
|
-
if (error49.code === "router_requires_card") {
|
|
154168
|
-
return [
|
|
154169
|
-
"**Add a card to start using Pullfrog Router.**",
|
|
154170
|
-
"",
|
|
154171
|
-
"Router proxies OpenRouter at raw cost \u2014 no platform markup. Add a card and we'll auto-reload your wallet so runs keep flowing.",
|
|
154172
|
-
"",
|
|
154173
|
-
`[Add a card \u2192](${billingConsoleUrl(owner, "model-access")})`
|
|
154174
|
-
].join("\n");
|
|
154175
|
-
}
|
|
154176
|
-
if (error49.code === "router_balance_exhausted") {
|
|
154177
|
-
return [
|
|
154178
|
-
"**Your Pullfrog Router balance is exhausted.**",
|
|
154179
|
-
"",
|
|
154180
|
-
"You have a card on file but auto-reload is disabled, so runs paused once your balance went past the overdraft buffer.",
|
|
154181
|
-
"",
|
|
154182
|
-
`[Top up balance \u2192](${billingConsoleUrl(owner, "billing")}) \xB7 [Enable auto-reload \u2192](${billingConsoleUrl(owner, "model-access")})`
|
|
154183
|
-
].join("\n");
|
|
154184
|
-
}
|
|
154185
|
-
if (error49.code === "router_keylimit_exhausted") {
|
|
154186
|
-
return [
|
|
154187
|
-
"**This run was cut short \u2014 your Pullfrog Router balance ran out mid-run.**",
|
|
154188
|
-
"",
|
|
154189
|
-
"OpenRouter stopped the agent because the per-run budget was exhausted. Your wallet is now negative; top up or enable auto-reload to keep runs flowing.",
|
|
154190
|
-
"",
|
|
154191
|
-
`[Top up balance \u2192](${billingConsoleUrl(owner, "billing")}) \xB7 [Enable auto-reload \u2192](${billingConsoleUrl(owner, "model-access")})`
|
|
154192
|
-
].join("\n");
|
|
154193
|
-
}
|
|
154194
|
-
if (error49.needsReauthentication) {
|
|
154195
|
-
const code = error49.declineCode ?? "authentication_required";
|
|
154196
|
-
return [
|
|
154197
|
-
`**Your card issuer requires 3D Secure on every charge** (\`${code}\`).`,
|
|
154198
|
-
"",
|
|
154199
|
-
"Pullfrog can't complete a 3DS challenge from inside a workflow. Top up your Router balance once in Stripe Checkout \u2014 subsequent runs draw from the prepaid balance without re-triggering 3DS.",
|
|
154200
|
-
"",
|
|
154201
|
-
`[Top up balance \u2192](${billingConsoleUrl(owner, "billing")})`
|
|
154202
|
-
].join("\n");
|
|
154203
|
-
}
|
|
154204
|
-
if (error49.declineCode) {
|
|
154205
|
-
return [
|
|
154206
|
-
`**Your card was declined** (\`${error49.declineCode}\`).`,
|
|
154207
|
-
"",
|
|
154208
|
-
"Update your payment method and Pullfrog will retry on the next run.",
|
|
154209
|
-
"",
|
|
154210
|
-
`[Update payment method \u2192](${billingConsoleUrl(owner, "billing")})`
|
|
154211
|
-
].join("\n");
|
|
154212
|
-
}
|
|
154213
|
-
return [
|
|
154214
|
-
"**Your Pullfrog balance is empty.**",
|
|
154215
|
-
"",
|
|
154216
|
-
"Top up your balance or enable auto-reload to keep runs flowing.",
|
|
154217
|
-
"",
|
|
154218
|
-
`[Manage billing \u2192](${billingConsoleUrl(owner, "billing")})`
|
|
154219
|
-
].join("\n");
|
|
154220
|
-
}
|
|
154221
|
-
function formatTransientErrorSummary(error49, owner) {
|
|
154222
|
-
return [
|
|
154223
|
-
"**Pullfrog billing is temporarily unavailable.**",
|
|
154224
|
-
"",
|
|
154225
|
-
error49.message,
|
|
154226
|
-
"",
|
|
154227
|
-
`Usually transient \u2014 the next dispatch should succeed. If it persists, check [status.pullfrog.com](https://status.pullfrog.com) or [your console](${billingConsoleUrl(owner, "billing")}).`
|
|
154228
|
-
].join("\n");
|
|
154229
|
-
}
|
|
154230
|
-
async function mintProxyKey(ctx) {
|
|
154231
|
-
try {
|
|
154232
|
-
const headers = await buildProxyTokenHeaders(ctx);
|
|
154233
|
-
if (!headers) return null;
|
|
154234
|
-
const response = await apiFetch({
|
|
154235
|
-
path: "/api/proxy-token",
|
|
154236
|
-
method: "POST",
|
|
154237
|
-
headers
|
|
154238
|
-
});
|
|
154239
|
-
if (response.status === 402) {
|
|
154240
|
-
const body = await response.json().catch(() => null);
|
|
154241
|
-
throw new BillingError(body?.error ?? "insufficient balance", {
|
|
154242
|
-
code: body?.code ?? null,
|
|
154243
|
-
declineCode: body?.declineCode ?? null,
|
|
154244
|
-
needsReauthentication: body?.needsReauthentication ?? false
|
|
154245
|
-
});
|
|
154246
|
-
}
|
|
154247
|
-
if (response.status === 503) {
|
|
154248
|
-
const body = await response.json().catch(() => null);
|
|
154249
|
-
throw new TransientError(
|
|
154250
|
-
body?.error ?? "billing service temporarily unavailable \u2014 retry shortly"
|
|
154251
|
-
);
|
|
154252
|
-
}
|
|
154253
|
-
if (!response.ok) {
|
|
154254
|
-
log.warning(`proxy key mint failed (${response.status})`);
|
|
154255
|
-
return null;
|
|
154256
|
-
}
|
|
154257
|
-
const data = await response.json();
|
|
154258
|
-
return data.key;
|
|
154259
|
-
} catch (error49) {
|
|
154260
|
-
if (error49 instanceof BillingError) throw error49;
|
|
154261
|
-
if (error49 instanceof TransientError) throw error49;
|
|
154262
|
-
log.warning(`proxy key mint error: ${error49 instanceof Error ? error49.message : String(error49)}`);
|
|
154263
|
-
return null;
|
|
154264
|
-
} finally {
|
|
154265
|
-
delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
154266
|
-
delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
154267
|
-
}
|
|
154268
|
-
}
|
|
154269
|
-
async function buildProxyTokenHeaders(ctx) {
|
|
154270
|
-
if (ctx.oidcCredentials) {
|
|
154271
|
-
process.env.ACTIONS_ID_TOKEN_REQUEST_URL = ctx.oidcCredentials.requestUrl;
|
|
154272
|
-
process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = ctx.oidcCredentials.requestToken;
|
|
154273
|
-
const oidcToken = await core7.getIDToken("pullfrog-api");
|
|
154274
|
-
delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
154275
|
-
delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
154276
|
-
return { Authorization: `Bearer ${oidcToken}` };
|
|
154277
|
-
}
|
|
154278
|
-
if (isLocalApiUrl()) {
|
|
154279
|
-
log.info(`\xBB proxy: dev bypass (x-dev-repo) for ${ctx.repo.owner}/${ctx.repo.name}`);
|
|
154280
|
-
return { "x-dev-repo": `${ctx.repo.owner}/${ctx.repo.name}` };
|
|
154281
|
-
}
|
|
154282
|
-
return null;
|
|
154283
|
-
}
|
|
154284
|
-
async function resolveProxyModel(ctx) {
|
|
154285
|
-
if (process.env.PULLFROG_MODEL?.trim()) return;
|
|
154286
|
-
const needsProxy = isInfraCovered({ isOss: ctx.oss, plan: ctx.plan }) && ctx.proxyModel;
|
|
154287
|
-
if (!needsProxy) return;
|
|
154288
|
-
if (!ctx.oidcCredentials && !isLocalApiUrl()) {
|
|
154289
|
-
log.warning("\xBB proxy requested but no OIDC credentials available \u2014 skipping");
|
|
154290
|
-
return;
|
|
154291
|
-
}
|
|
154292
|
-
const key = await mintProxyKey({ oidcCredentials: ctx.oidcCredentials, repo: ctx.repo });
|
|
154293
|
-
if (!key) return;
|
|
154294
|
-
process.env.OPENROUTER_API_KEY = key;
|
|
154295
|
-
core7.setSecret(key);
|
|
154296
|
-
ctx.payload.proxyModel = ctx.proxyModel;
|
|
154297
|
-
const label = ctx.oss ? "oss" : "router";
|
|
154298
|
-
log.info(`\xBB proxy: ${label} \u2192 ${ctx.proxyModel}`);
|
|
154299
|
-
}
|
|
154300
|
-
async function fetchPreviousSnapshot(ctx, prNumber) {
|
|
154301
|
-
if (!ctx.githubInstallationToken) return null;
|
|
154302
|
-
try {
|
|
154303
|
-
const response = await apiFetch({
|
|
154304
|
-
path: `/api/repo/${ctx.repo.owner}/${ctx.repo.name}/pr/${prNumber}/summary-comment`,
|
|
154305
|
-
method: "GET",
|
|
154306
|
-
headers: { authorization: `Bearer ${ctx.githubInstallationToken}` },
|
|
154307
|
-
signal: AbortSignal.timeout(1e4)
|
|
154308
|
-
});
|
|
154309
|
-
if (!response.ok) return null;
|
|
154310
|
-
const data = await response.json();
|
|
154311
|
-
return typeof data.snapshot === "string" && data.snapshot.length > 0 ? data.snapshot : null;
|
|
154312
|
-
} catch {
|
|
154313
|
-
return null;
|
|
154314
|
-
}
|
|
154315
|
-
}
|
|
154316
|
-
async function persistLearnings(ctx) {
|
|
154317
|
-
const filePath = ctx.toolState.learningsFilePath;
|
|
154318
|
-
if (!filePath) return;
|
|
154319
|
-
if (ctx.toolState.learningsPersistAttempted) return;
|
|
154320
|
-
ctx.toolState.learningsPersistAttempted = true;
|
|
154321
|
-
const current = await readLearningsFile(filePath);
|
|
154322
|
-
if (current === null) {
|
|
154323
|
-
log.debug(`learnings tmpfile missing or unreadable at ${filePath} \u2014 skipping persist`);
|
|
154324
|
-
return;
|
|
154325
|
-
}
|
|
154326
|
-
const seed = ctx.toolState.learningsSeed?.trim() ?? "";
|
|
154327
|
-
if (current === seed) {
|
|
154328
|
-
log.debug("learnings tmpfile unchanged from seed \u2014 skipping persist");
|
|
154329
|
-
return;
|
|
154330
|
-
}
|
|
154331
|
-
try {
|
|
154332
|
-
const response = await apiFetch({
|
|
154333
|
-
path: `/api/repo/${ctx.repo.owner}/${ctx.repo.name}/learnings`,
|
|
154334
|
-
method: "PATCH",
|
|
154335
|
-
headers: {
|
|
154336
|
-
authorization: `Bearer ${ctx.apiToken}`,
|
|
154337
|
-
"content-type": "application/json"
|
|
154338
|
-
},
|
|
154339
|
-
body: JSON.stringify({
|
|
154340
|
-
learnings: current,
|
|
154341
|
-
model: ctx.toolState.model
|
|
154342
|
-
}),
|
|
154343
|
-
signal: AbortSignal.timeout(1e4)
|
|
154344
|
-
});
|
|
154345
|
-
if (!response.ok) {
|
|
154346
|
-
const error49 = await response.text().catch(() => "(no body)");
|
|
154347
|
-
log.warning(`learnings persist failed (${response.status}): ${error49}`);
|
|
154348
|
-
return;
|
|
154349
|
-
}
|
|
154350
|
-
log.info("\xBB learnings updated");
|
|
154351
|
-
} catch (err) {
|
|
154352
|
-
log.warning(`learnings persist failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
154353
|
-
}
|
|
154354
|
-
}
|
|
154355
|
-
async function persistSummary(ctx) {
|
|
154356
|
-
const filePath = ctx.toolState.summaryFilePath;
|
|
154357
|
-
if (!filePath) return;
|
|
154358
|
-
if (ctx.toolState.summaryPersistAttempted) return;
|
|
154359
|
-
ctx.toolState.summaryPersistAttempted = true;
|
|
154360
|
-
const snapshot2 = await readSummaryFile(filePath);
|
|
154361
|
-
if (!snapshot2) {
|
|
154362
|
-
log.debug(`pr summary tmpfile missing or invalid at ${filePath} \u2014 skipping persist`);
|
|
154363
|
-
return;
|
|
154364
|
-
}
|
|
154365
|
-
const seed = ctx.toolState.summarySeed?.trim();
|
|
154366
|
-
if (seed !== void 0 && snapshot2 === seed) {
|
|
154367
|
-
log.warning(
|
|
154368
|
-
"\xBB pr summary tmpfile unchanged from seed \u2014 skipping persist (agent did not edit it)"
|
|
154369
|
-
);
|
|
154370
|
-
return;
|
|
154371
|
-
}
|
|
154372
|
-
await patchWorkflowRunFields(ctx, { summarySnapshot: snapshot2 }).catch((err) => {
|
|
154373
|
-
log.debug(`pr summary persist failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
154374
|
-
});
|
|
154375
|
-
}
|
|
154376
|
-
async function writeJobSummary(toolState, finalOutput) {
|
|
154377
|
-
const usageSummary = formatUsageSummary(toolState.usageEntries);
|
|
154378
|
-
const body = toolState.lastProgressBody || finalOutput;
|
|
154379
|
-
const summaryParts = [body, usageSummary].filter(Boolean);
|
|
154380
|
-
if (summaryParts.length > 0) {
|
|
154381
|
-
await writeSummary(summaryParts.join("\n\n"));
|
|
154382
|
-
}
|
|
154383
|
-
}
|
|
154384
155077
|
async function main() {
|
|
154385
155078
|
var _stack2 = [];
|
|
154386
155079
|
try {
|
|
154387
155080
|
normalizeEnv();
|
|
155081
|
+
const overridesRaw = process.env.UNSAFE_OVERRIDES ?? "";
|
|
155082
|
+
if (overridesRaw.trim()) {
|
|
155083
|
+
const result = applyOverrides({ raw: overridesRaw, env: process.env });
|
|
155084
|
+
if (result.applied.length > 0) {
|
|
155085
|
+
log.info(`\xBB applied ${result.applied.length} env override(s): ${result.applied.join(", ")}`);
|
|
155086
|
+
}
|
|
155087
|
+
if (result.denied.length > 0) {
|
|
155088
|
+
log.warning(
|
|
155089
|
+
`\xBB refused to override ${result.denied.length} protected env var(s): ${result.denied.join(", ")}`
|
|
155090
|
+
);
|
|
155091
|
+
}
|
|
155092
|
+
}
|
|
154388
155093
|
const usageSummaryPath = process.env.PULLFROG_USAGE_SUMMARY_PATH;
|
|
154389
155094
|
if (usageSummaryPath) {
|
|
154390
155095
|
onExitSignal(() => writeGitHubUsageSummaryToFile(usageSummaryPath));
|
|
@@ -154428,34 +155133,14 @@ async function main() {
|
|
|
154428
155133
|
delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
154429
155134
|
delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
154430
155135
|
}
|
|
154431
|
-
|
|
154432
|
-
|
|
154433
|
-
|
|
154434
|
-
|
|
154435
|
-
|
|
154436
|
-
|
|
154437
|
-
|
|
154438
|
-
|
|
154439
|
-
});
|
|
154440
|
-
} catch (error49) {
|
|
154441
|
-
if (error49 instanceof BillingError) {
|
|
154442
|
-
const summary2 = formatBillingErrorSummary(error49, runContext.repo.owner);
|
|
154443
|
-
await writeSummary(summary2).catch(() => {
|
|
154444
|
-
});
|
|
154445
|
-
await reportErrorToComment({ toolState, error: summary2 }).catch(() => {
|
|
154446
|
-
});
|
|
154447
|
-
throw error49;
|
|
154448
|
-
}
|
|
154449
|
-
if (error49 instanceof TransientError) {
|
|
154450
|
-
const summary2 = formatTransientErrorSummary(error49, runContext.repo.owner);
|
|
154451
|
-
await writeSummary(summary2).catch(() => {
|
|
154452
|
-
});
|
|
154453
|
-
await reportErrorToComment({ toolState, error: summary2 }).catch(() => {
|
|
154454
|
-
});
|
|
154455
|
-
throw error49;
|
|
154456
|
-
}
|
|
154457
|
-
throw error49;
|
|
154458
|
-
}
|
|
155136
|
+
await runProxyResolution({
|
|
155137
|
+
payload,
|
|
155138
|
+
oss: runContext.oss,
|
|
155139
|
+
proxyModel: runContext.proxyModel,
|
|
155140
|
+
oidcCredentials,
|
|
155141
|
+
repo: runContext.repo,
|
|
155142
|
+
toolState
|
|
155143
|
+
});
|
|
154459
155144
|
const octokit = createOctokit(tokenRef.mcpToken);
|
|
154460
155145
|
const runInfo = await resolveRun({ octokit });
|
|
154461
155146
|
let toolContext;
|
|
@@ -154482,12 +155167,24 @@ async function main() {
|
|
|
154482
155167
|
const tmpdir3 = createTempDirectory();
|
|
154483
155168
|
const gitAuthServer = __using(_stack, await startGitAuthServer(tmpdir3), true);
|
|
154484
155169
|
setGitAuthServer(gitAuthServer);
|
|
154485
|
-
const
|
|
155170
|
+
const initialResolvedModel = payload.proxyModel ? void 0 : resolveModel({ slug: payload.model });
|
|
155171
|
+
const fallback = selectFallbackModelIfNeeded({
|
|
155172
|
+
resolvedModel: initialResolvedModel,
|
|
155173
|
+
proxyModel: payload.proxyModel
|
|
155174
|
+
});
|
|
155175
|
+
const effectiveSlug = fallback.fallback ? fallback.to : payload.model;
|
|
155176
|
+
const resolvedModel = fallback.fallback ? fallback.to : initialResolvedModel;
|
|
155177
|
+
if (fallback.fallback) {
|
|
155178
|
+
log.warning(
|
|
155179
|
+
`\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.`
|
|
155180
|
+
);
|
|
155181
|
+
toolState.modelFallback = { from: fallback.from };
|
|
155182
|
+
}
|
|
154486
155183
|
const agent2 = resolveAgent({ model: resolvedModel });
|
|
154487
|
-
toolState.model = payload.proxyModel ?? resolvedModel ??
|
|
155184
|
+
toolState.model = payload.proxyModel ?? resolvedModel ?? effectiveSlug;
|
|
154488
155185
|
validateAgentApiKey({
|
|
154489
155186
|
agent: agent2,
|
|
154490
|
-
model: payload.proxyModel ?? resolvedModel ??
|
|
155187
|
+
model: payload.proxyModel ?? resolvedModel ?? effectiveSlug,
|
|
154491
155188
|
owner: runContext.repo.owner,
|
|
154492
155189
|
name: runContext.repo.name
|
|
154493
155190
|
});
|
|
@@ -154570,14 +155267,7 @@ async function main() {
|
|
|
154570
155267
|
onExitSignal(() => persistSummary(ctxForExit));
|
|
154571
155268
|
}
|
|
154572
155269
|
startInstallation(toolContext);
|
|
154573
|
-
|
|
154574
|
-
const agentForLog = resolveAgentForLog({ agentName: agent2.name, resolvedModel });
|
|
154575
|
-
const timeoutForLog = resolveTimeoutForLog(payload.timeout);
|
|
154576
|
-
log.info(`\xBB model: ${modelForLog}`);
|
|
154577
|
-
log.info(`\xBB agent: ${agentForLog}`);
|
|
154578
|
-
log.info(`\xBB push: ${payload.push}`);
|
|
154579
|
-
log.info(`\xBB shell: ${payload.shell}`);
|
|
154580
|
-
log.info(`\xBB timeout: ${timeoutForLog}`);
|
|
155270
|
+
logRunStartup({ payload, resolvedModel, agentName: agent2.name });
|
|
154581
155271
|
const instructions = resolveInstructions({
|
|
154582
155272
|
payload,
|
|
154583
155273
|
repo: runContext.repo,
|
|
@@ -154601,7 +155291,7 @@ ${instructions.user}` : null,
|
|
|
154601
155291
|
log.info(instructions.full);
|
|
154602
155292
|
});
|
|
154603
155293
|
if (agentId === "opencode") {
|
|
154604
|
-
const pluginDir =
|
|
155294
|
+
const pluginDir = join18(process.cwd(), ".opencode", "plugin");
|
|
154605
155295
|
const hasPlugins = existsSync7(pluginDir) && readdirSync(pluginDir).some((f) => /\.[jt]sx?$/.test(f));
|
|
154606
155296
|
if (hasPlugins && toolState.dependencyInstallation?.promise) {
|
|
154607
155297
|
log.info(
|
|
@@ -154661,6 +155351,7 @@ ${instructions.user}` : null,
|
|
|
154661
155351
|
todoTracker,
|
|
154662
155352
|
stopScript: runContext.repoSettings.stopScript,
|
|
154663
155353
|
toolState,
|
|
155354
|
+
apiToken: runContext.apiToken,
|
|
154664
155355
|
onActivityTimeout: onInnerActivityTimeout,
|
|
154665
155356
|
onToolUse: (event) => {
|
|
154666
155357
|
const wasTracked = recordDiffReadFromToolUse({
|
|
@@ -154710,42 +155401,7 @@ ${instructions.user}` : null,
|
|
|
154710
155401
|
"output_schema was provided but agent did not call set_output \u2014 structured output is required"
|
|
154711
155402
|
);
|
|
154712
155403
|
}
|
|
154713
|
-
|
|
154714
|
-
await postReviewCleanup(toolContext).catch((error49) => {
|
|
154715
|
-
log.debug(`post-review cleanup failed: ${error49}`);
|
|
154716
|
-
});
|
|
154717
|
-
}
|
|
154718
|
-
if (toolContext) {
|
|
154719
|
-
await persistSummary(toolContext);
|
|
154720
|
-
}
|
|
154721
|
-
if (toolContext) {
|
|
154722
|
-
await persistLearnings(toolContext);
|
|
154723
|
-
}
|
|
154724
|
-
if (!result.success && toolContext && toolState.progressComment) {
|
|
154725
|
-
const rawError = result.error || "agent run failed";
|
|
154726
|
-
const errorBody = isApiKeyAuthError(rawError) ? formatApiKeyErrorSummary({
|
|
154727
|
-
owner: runContext.repo.owner,
|
|
154728
|
-
name: runContext.repo.name,
|
|
154729
|
-
raw: rawError
|
|
154730
|
-
}) : rawError;
|
|
154731
|
-
await reportErrorToComment({ toolState, error: errorBody }).catch((error49) => {
|
|
154732
|
-
log.debug(`failure error report failed: ${error49}`);
|
|
154733
|
-
});
|
|
154734
|
-
}
|
|
154735
|
-
if (toolContext && result.success && toolState.progressComment && !toolState.finalSummaryWritten) {
|
|
154736
|
-
await deleteProgressComment(toolContext).catch((error49) => {
|
|
154737
|
-
log.debug(`stranded progress comment cleanup failed: ${error49}`);
|
|
154738
|
-
});
|
|
154739
|
-
}
|
|
154740
|
-
try {
|
|
154741
|
-
await writeJobSummary(toolState, result.output);
|
|
154742
|
-
} catch (error49) {
|
|
154743
|
-
log.debug(`job summary write failed: ${error49}`);
|
|
154744
|
-
}
|
|
154745
|
-
if (toolState.output) {
|
|
154746
|
-
log.info(`::pullfrog-output::${Buffer.from(toolState.output).toString("base64")}`);
|
|
154747
|
-
core7.setOutput("result", toolState.output);
|
|
154748
|
-
}
|
|
155404
|
+
await finalizeSuccessRun({ toolContext, toolState, result, repo: runContext.repo });
|
|
154749
155405
|
return await handleAgentResult({
|
|
154750
155406
|
result,
|
|
154751
155407
|
toolState,
|
|
@@ -154763,38 +155419,14 @@ ${instructions.user}` : null,
|
|
|
154763
155419
|
todoTracker?.cancel();
|
|
154764
155420
|
killTrackedChildren();
|
|
154765
155421
|
log.error(errorMessage);
|
|
154766
|
-
const
|
|
154767
|
-
|
|
154768
|
-
|
|
154769
|
-
|
|
154770
|
-
|
|
154771
|
-
})
|
|
154772
|
-
try {
|
|
154773
|
-
const errorSummary = billingError ? formatBillingErrorSummary(billingError, runContext.repo.owner) : apiKeyErrorSummary ?? `### \u274C Pullfrog failed
|
|
154774
|
-
|
|
154775
|
-
\`\`\`
|
|
154776
|
-
${errorMessage}
|
|
154777
|
-
\`\`\``;
|
|
154778
|
-
const usageSummary = formatUsageSummary(toolState.usageEntries);
|
|
154779
|
-
const parts = [errorSummary, toolState.lastProgressBody, usageSummary].filter(Boolean);
|
|
154780
|
-
await writeSummary(parts.join("\n\n"));
|
|
154781
|
-
} catch {
|
|
154782
|
-
}
|
|
154783
|
-
try {
|
|
154784
|
-
const commentBody = billingError ? formatBillingErrorSummary(billingError, runContext.repo.owner) : apiKeyErrorSummary ?? errorMessage;
|
|
154785
|
-
await reportErrorToComment({ toolState, error: commentBody });
|
|
154786
|
-
} catch {
|
|
154787
|
-
}
|
|
154788
|
-
if (toolContext) {
|
|
154789
|
-
await postReviewCleanup(toolContext).catch((error50) => {
|
|
154790
|
-
log.debug(`post-review cleanup failed: ${error50}`);
|
|
154791
|
-
});
|
|
154792
|
-
}
|
|
154793
|
-
if (toolContext) {
|
|
154794
|
-
await persistSummary(toolContext);
|
|
154795
|
-
}
|
|
155422
|
+
const rendered = renderRunError({
|
|
155423
|
+
errorMessage,
|
|
155424
|
+
repo: runContext.repo,
|
|
155425
|
+
agentDiagnostic: toolState.agentDiagnostic
|
|
155426
|
+
});
|
|
155427
|
+
await writeRunErrorOutputs({ rendered, toolState });
|
|
154796
155428
|
if (toolContext) {
|
|
154797
|
-
await
|
|
155429
|
+
await persistRunArtifacts(toolContext);
|
|
154798
155430
|
}
|
|
154799
155431
|
return {
|
|
154800
155432
|
success: false,
|