pullfrog 0.1.8 → 0.1.10
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 +4275 -3256
- package/dist/external.d.ts +1 -1
- package/dist/index.js +1706 -1219
- package/dist/internal/index.d.ts +2 -1
- package/dist/internal.js +245 -85
- package/dist/models.d.ts +10 -0
- package/dist/modes.d.ts +1 -1
- package/dist/toolState.d.ts +4 -0
- package/dist/utils/activity.d.ts +31 -1
- 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/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/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.10",
|
|
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",
|
|
@@ -142919,8 +142975,9 @@ function $(cmd, args2, options) {
|
|
|
142919
142975
|
options.onError(errorResult);
|
|
142920
142976
|
return stdout.trim();
|
|
142921
142977
|
}
|
|
142978
|
+
const detail = [stderr, stdout].map((s) => s.trim()).filter(Boolean).join("\n");
|
|
142922
142979
|
throw new Error(
|
|
142923
|
-
`Command failed with exit code ${errorResult.status}: ${
|
|
142980
|
+
`Command failed with exit code ${errorResult.status}: ${detail || "Unknown error"}`
|
|
142924
142981
|
);
|
|
142925
142982
|
}
|
|
142926
142983
|
return stdout.trim();
|
|
@@ -143060,6 +143117,7 @@ async function executeLifecycleHook(params) {
|
|
|
143060
143117
|
if (result.exitCode !== 0) {
|
|
143061
143118
|
const output = (result.stderr || result.stdout).trim();
|
|
143062
143119
|
return {
|
|
143120
|
+
failure: { kind: "exit", output, exitCode: result.exitCode },
|
|
143063
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.`
|
|
143064
143122
|
};
|
|
143065
143123
|
}
|
|
@@ -143070,11 +143128,13 @@ async function executeLifecycleHook(params) {
|
|
|
143070
143128
|
if (isTimeout) {
|
|
143071
143129
|
const minutes = Math.round(LIFECYCLE_HOOK_TIMEOUT_MS / 6e4);
|
|
143072
143130
|
return {
|
|
143131
|
+
failure: { kind: "timeout" },
|
|
143073
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).`
|
|
143074
143133
|
};
|
|
143075
143134
|
}
|
|
143076
143135
|
const msg = err instanceof Error ? err.message : String(err);
|
|
143077
143136
|
return {
|
|
143137
|
+
failure: { kind: "spawn", spawnError: msg },
|
|
143078
143138
|
warning: `lifecycle hook '${params.event}' failed to spawn: ${msg}. this is likely a transient failure \u2014 retry the operation.`
|
|
143079
143139
|
};
|
|
143080
143140
|
}
|
|
@@ -143293,7 +143353,7 @@ function PushBranchTool(ctx) {
|
|
|
143293
143353
|
const pushPermission = ctx.payload.push;
|
|
143294
143354
|
return tool({
|
|
143295
143355
|
name: "push_branch",
|
|
143296
|
-
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.",
|
|
143297
143357
|
parameters: PushBranch,
|
|
143298
143358
|
execute: execute(async ({ branchName, force }) => {
|
|
143299
143359
|
if (pushPermission === "disabled") {
|
|
@@ -143307,10 +143367,21 @@ function PushBranchTool(ctx) {
|
|
|
143307
143367
|
`push blocked: working tree is not clean (tracked changes and/or untracked files). commit, discard, or remove stray artifacts before pushing.
|
|
143308
143368
|
|
|
143309
143369
|
git status:
|
|
143310
|
-
${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." : "")
|
|
143311
143371
|
);
|
|
143312
143372
|
}
|
|
143313
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
|
+
}
|
|
143314
143385
|
if (pushPermission === "restricted" && pushDest.remoteBranch === defaultBranch) {
|
|
143315
143386
|
throw new Error(
|
|
143316
143387
|
`Push blocked: cannot push directly to default branch '${pushDest.remoteBranch}'. Create a feature branch and open a PR instead.`
|
|
@@ -143318,21 +143389,27 @@ ${status}`
|
|
|
143318
143389
|
}
|
|
143319
143390
|
const refspec = branch === pushDest.remoteBranch ? branch : `${branch}:${pushDest.remoteBranch}`;
|
|
143320
143391
|
const pushArgs = force ? ["--force", "-u", pushDest.remoteName, refspec] : ["-u", pushDest.remoteName, refspec];
|
|
143321
|
-
const
|
|
143322
|
-
|
|
143323
|
-
|
|
143324
|
-
})
|
|
143325
|
-
|
|
143326
|
-
|
|
143327
|
-
|
|
143328
|
-
|
|
143329
|
-
|
|
143330
|
-
|
|
143331
|
-
|
|
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.
|
|
143332
143408
|
|
|
143333
143409
|
git status:
|
|
143334
143410
|
${postHookStatus}`
|
|
143335
|
-
|
|
143411
|
+
);
|
|
143412
|
+
}
|
|
143336
143413
|
}
|
|
143337
143414
|
log.debug(`pushing ${branch} to ${pushDest.remoteName}/${pushDest.remoteBranch}`);
|
|
143338
143415
|
if (force) {
|
|
@@ -143385,17 +143462,30 @@ ${integrateStep}
|
|
|
143385
143462
|
log.info(
|
|
143386
143463
|
`\xBB pushed branch ${branch} to ${pushDest.remoteName}/${pushDest.remoteBranch} (sha ${pushedSha})`
|
|
143387
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;
|
|
143388
143467
|
return {
|
|
143389
143468
|
success: true,
|
|
143390
143469
|
branch,
|
|
143391
143470
|
remoteBranch: pushDest.remoteBranch,
|
|
143392
143471
|
remote: pushDest.remoteName,
|
|
143393
143472
|
force,
|
|
143394
|
-
|
|
143473
|
+
prepushSkipped,
|
|
143474
|
+
message
|
|
143395
143475
|
};
|
|
143396
143476
|
})
|
|
143397
143477
|
});
|
|
143398
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
|
+
}
|
|
143399
143489
|
var AUTH_REQUIRED_REDIRECT = {
|
|
143400
143490
|
push: "use the push_branch tool instead \u2014 it handles authentication and permission checks.",
|
|
143401
143491
|
fetch: "use the git_fetch tool instead \u2014 it handles authentication.",
|
|
@@ -143457,6 +143547,23 @@ function GitTool(ctx) {
|
|
|
143457
143547
|
}
|
|
143458
143548
|
}
|
|
143459
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
|
+
}
|
|
143460
143567
|
const output = $("git", [command, ...args2], { log: false });
|
|
143461
143568
|
const lineCount = output.split("\n").length;
|
|
143462
143569
|
if (lineCount > COLLAPSE_THRESHOLD) {
|
|
@@ -143696,7 +143803,7 @@ var CreatePullRequestReview = type({
|
|
|
143696
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."
|
|
143697
143804
|
).optional(),
|
|
143698
143805
|
approved: type.boolean.describe(
|
|
143699
|
-
"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."
|
|
143700
143807
|
).optional(),
|
|
143701
143808
|
commit_id: type.string.describe(
|
|
143702
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."
|
|
@@ -144041,7 +144148,8 @@ async function createAndSubmitWithFooter(ctx, params, opts) {
|
|
|
144041
144148
|
const footer = buildPullfrogFooter({
|
|
144042
144149
|
workflowRun: ctx.runId ? { owner: ctx.repo.owner, repo: ctx.repo.name, runId: ctx.runId, jobId: ctx.jobId } : void 0,
|
|
144043
144150
|
customParts,
|
|
144044
|
-
model: ctx.toolState.model
|
|
144151
|
+
model: ctx.toolState.model,
|
|
144152
|
+
fallbackFrom: ctx.toolState.modelFallback?.from
|
|
144045
144153
|
});
|
|
144046
144154
|
return await ctx.octokit.rest.pulls.submitReview({
|
|
144047
144155
|
owner: params.owner,
|
|
@@ -144540,8 +144648,8 @@ ${diffPreview}`);
|
|
|
144540
144648
|
log.info(`\xBB checkout_pr({pull_number:${pull_number}}) already in flight \u2014 sharing result`);
|
|
144541
144649
|
return inFlight;
|
|
144542
144650
|
}
|
|
144543
|
-
const
|
|
144544
|
-
if (
|
|
144651
|
+
const currentBranch = $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false }).trim();
|
|
144652
|
+
if (currentBranch !== `pr-${pull_number}`) {
|
|
144545
144653
|
const dirty = $("git", ["status", "--porcelain"], { log: false }).trim();
|
|
144546
144654
|
if (dirty) {
|
|
144547
144655
|
throw new Error(
|
|
@@ -144874,9 +144982,8 @@ function GetIssueEventsTool(ctx) {
|
|
|
144874
144982
|
});
|
|
144875
144983
|
const relevantEventTypes = /* @__PURE__ */ new Set(["cross_referenced", "referenced"]);
|
|
144876
144984
|
const parsedEvents = events.flatMap((event) => {
|
|
144877
|
-
if (!("event" in event) ||
|
|
144878
|
-
|
|
144879
|
-
}
|
|
144985
|
+
if (!("event" in event) || typeof event.event !== "string") return [];
|
|
144986
|
+
if (!relevantEventTypes.has(event.event)) return [];
|
|
144880
144987
|
const baseEvent = {
|
|
144881
144988
|
event: event.event
|
|
144882
144989
|
};
|
|
@@ -145076,7 +145183,8 @@ function buildPrBodyWithFooter(ctx, body) {
|
|
|
145076
145183
|
const footer = buildPullfrogFooter({
|
|
145077
145184
|
triggeredBy: true,
|
|
145078
145185
|
workflowRun: ctx.runId ? { owner: ctx.repo.owner, repo: ctx.repo.name, runId: ctx.runId, jobId: ctx.jobId } : void 0,
|
|
145079
|
-
model: ctx.toolState.model
|
|
145186
|
+
model: ctx.toolState.model,
|
|
145187
|
+
fallbackFrom: ctx.toolState.modelFallback?.from
|
|
145080
145188
|
});
|
|
145081
145189
|
const bodyWithoutFooter = stripExistingFooter(fixDoubleEscapedString(body));
|
|
145082
145190
|
return `${bodyWithoutFooter}${footer}`;
|
|
@@ -145647,7 +145755,9 @@ function ListPullRequestReviewsTool(ctx) {
|
|
|
145647
145755
|
body: review.body,
|
|
145648
145756
|
state: review.state,
|
|
145649
145757
|
user: review.user?.login,
|
|
145650
|
-
submitted_at: review.submitted_at
|
|
145758
|
+
submitted_at: review.submitted_at,
|
|
145759
|
+
commit_id: review.commit_id,
|
|
145760
|
+
html_url: review.html_url
|
|
145651
145761
|
})),
|
|
145652
145762
|
count: reviews.length
|
|
145653
145763
|
};
|
|
@@ -145887,6 +145997,14 @@ function detectSandboxMethod() {
|
|
|
145887
145997
|
return "none";
|
|
145888
145998
|
}
|
|
145889
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(" ");
|
|
145890
146008
|
function spawnShell(params) {
|
|
145891
146009
|
const spawnOpts = { env: params.env, cwd: params.cwd, stdio: params.stdio, detached: true };
|
|
145892
146010
|
const sandboxMethod = detectSandboxMethod();
|
|
@@ -145899,7 +146017,14 @@ function spawnShell(params) {
|
|
|
145899
146017
|
if (sandboxMethod === "unshare") {
|
|
145900
146018
|
return spawn2(
|
|
145901
146019
|
"unshare",
|
|
145902
|
-
[
|
|
146020
|
+
[
|
|
146021
|
+
"--pid",
|
|
146022
|
+
"--fork",
|
|
146023
|
+
"--mount-proc",
|
|
146024
|
+
"bash",
|
|
146025
|
+
"-c",
|
|
146026
|
+
`${PROC_CLEANUP} ${SOCKET_CLEANUP} ${params.command}`
|
|
146027
|
+
],
|
|
145903
146028
|
spawnOpts
|
|
145904
146029
|
);
|
|
145905
146030
|
}
|
|
@@ -145925,7 +146050,7 @@ function spawnShell(params) {
|
|
|
145925
146050
|
"--mount-proc",
|
|
145926
146051
|
"bash",
|
|
145927
146052
|
"-c",
|
|
145928
|
-
`${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}'`
|
|
145929
146054
|
],
|
|
145930
146055
|
{ ...spawnOpts, env: {} }
|
|
145931
146056
|
);
|
|
@@ -146360,52 +146485,145 @@ Report findings clearly with file:line references and quoted evidence where poss
|
|
|
146360
146485
|
// modes.ts
|
|
146361
146486
|
var PR_SUMMARY_FORMAT = `### Default format
|
|
146362
146487
|
|
|
146363
|
-
|
|
146488
|
+
The body has at most three parts in this exact order:
|
|
146489
|
+
|
|
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.
|
|
146493
|
+
|
|
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.
|
|
146495
|
+
|
|
146496
|
+
## 1. Reviewed changes preamble
|
|
146364
146497
|
|
|
146365
|
-
|
|
146366
|
-
NOTE: use HTML bold <b>TL;DR</b>, NOT markdown bold **TL;DR**.
|
|
146498
|
+
Open with a single bolded inline lead-in followed immediately by the bullet list (no \`### Key changes\` heading, no \`<b>TL;DR</b>\`):
|
|
146367
146499
|
|
|
146368
|
-
|
|
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
|
+
\`\`\`
|
|
146369
146527
|
|
|
146370
|
-
|
|
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\`.
|
|
146371
146529
|
|
|
146372
|
-
|
|
146373
|
-
NOTE: the metadata line goes AFTER the bullet list, not before it.
|
|
146530
|
+
## 2. Cross-cutting issue sections (zero or more)
|
|
146374
146531
|
|
|
146375
|
-
|
|
146532
|
+
For each cross-cutting concern, one \`### \` section. Use this exact shape:
|
|
146376
146533
|
|
|
146377
|
-
|
|
146534
|
+
\`\`\`
|
|
146535
|
+
### {emoji} {short, descriptive title \u2014 what's wrong, not what to do}
|
|
146378
146536
|
|
|
146379
|
-
|
|
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.}
|
|
146380
146538
|
|
|
146381
|
-
>
|
|
146382
|
-
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.
|
|
146539
|
+
<details><summary>Technical details</summary>
|
|
146383
146540
|
|
|
146384
|
-
|
|
146541
|
+
\\\`\\\`\\\`\\\`markdown
|
|
146542
|
+
# {title repeated}
|
|
146385
146543
|
|
|
146386
|
-
|
|
146387
|
-
|
|
146388
|
-
|
|
146389
|
-
> </details>
|
|
146544
|
+
## Affected sites
|
|
146545
|
+
- {file path:line} \u2014 {what's wrong there}
|
|
146546
|
+
- ...
|
|
146390
146547
|
|
|
146391
|
-
|
|
146392
|
-
|
|
146548
|
+
## Required outcome
|
|
146549
|
+
- {what the fix needs to achieve, not how to achieve it}
|
|
146550
|
+
- ...
|
|
146393
146551
|
|
|
146394
|
-
|
|
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.}
|
|
146395
146554
|
|
|
146396
|
-
|
|
146397
|
-
|
|
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
|
+
\\\`\\\`\\\`\\\`
|
|
146398
146558
|
|
|
146399
|
-
|
|
146400
|
-
|
|
146401
|
-
|
|
146402
|
-
|
|
146403
|
-
|
|
146404
|
-
|
|
146405
|
-
|
|
146406
|
-
|
|
146407
|
-
-
|
|
146408
|
-
|
|
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.`;
|
|
146409
146627
|
function computeModes(agentId) {
|
|
146410
146628
|
const t = (toolName) => formatMcpToolRef(agentId, toolName);
|
|
146411
146629
|
return [
|
|
@@ -146447,7 +146665,7 @@ function computeModes(agentId) {
|
|
|
146447
146665
|
|
|
146448
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.
|
|
146449
146667
|
|
|
146450
|
-
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.
|
|
146451
146669
|
|
|
146452
146670
|
Delegation + research discipline (distilled from \`/anneal\` canonical \u2014 these are codified learnings from many review rounds, not theoretical best practices):
|
|
146453
146671
|
- Do NOT summarize what you implemented \u2014 that biases the subagent toward validating the shape of your solution rather than questioning it.
|
|
@@ -146456,7 +146674,7 @@ function computeModes(agentId) {
|
|
|
146456
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.
|
|
146457
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.
|
|
146458
146676
|
|
|
146459
|
-
|
|
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 "..."\`).
|
|
146460
146678
|
|
|
146461
146679
|
6. **finalize**:
|
|
146462
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)
|
|
@@ -146480,7 +146698,8 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
146480
146698
|
|
|
146481
146699
|
4. For each comment:
|
|
146482
146700
|
- understand the feedback
|
|
146483
|
-
-
|
|
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.
|
|
146484
146703
|
- if the request stands, make the code change using your native tools; otherwise reply explaining why
|
|
146485
146704
|
- record what was done (or why nothing was done)
|
|
146486
146705
|
|
|
@@ -146488,11 +146707,13 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
146488
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
|
|
146489
146708
|
- commit locally via shell (\`git add . && git commit -m "..."\`)
|
|
146490
146709
|
|
|
146491
|
-
6. Finalize:
|
|
146710
|
+
6. Finalize. Reply + resolve are paired write actions: do BOTH or NEITHER for each thread.
|
|
146492
146711
|
- confirm a clean working tree, then push via \`${t("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*)
|
|
146493
|
-
-
|
|
146494
|
-
-
|
|
146495
|
-
|
|
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`
|
|
146496
146717
|
},
|
|
146497
146718
|
// Review and IncrementalReview use a 0-or-2+ lens pattern. The default is
|
|
146498
146719
|
// 0 lenses (orchestrator handles the review solo). Multi-lens (2+
|
|
@@ -146509,9 +146730,12 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
146509
146730
|
// the Review/IncrementalReview lens fan-out where independence between
|
|
146510
146731
|
// perspectives is what's being purchased.
|
|
146511
146732
|
//
|
|
146512
|
-
//
|
|
146513
|
-
//
|
|
146514
|
-
//
|
|
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.
|
|
146515
146739
|
{
|
|
146516
146740
|
name: "Review",
|
|
146517
146741
|
description: "Review code, PRs, or implementations; provide feedback or suggestions; identify issues; or check code quality, style, and correctness",
|
|
@@ -146597,7 +146821,9 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
146597
146821
|
|
|
146598
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.
|
|
146599
146823
|
|
|
146600
|
-
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.
|
|
146601
146827
|
|
|
146602
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.
|
|
146603
146829
|
|
|
@@ -146605,12 +146831,12 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
146605
146831
|
|
|
146606
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.
|
|
146607
146833
|
|
|
146608
|
-
|
|
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:
|
|
146609
146835
|
|
|
146610
146836
|
- \`[!CAUTION]\` \u2014 large red banner. Reads as "this will break something."
|
|
146611
146837
|
- \`[!IMPORTANT]\` \u2014 large purple banner. Reads as "you need to look at this before merging."
|
|
146612
|
-
-
|
|
146613
|
-
-
|
|
146838
|
+
- \`> \u2139\uFE0F ...\` \u2014 informational blockquote. Reads as "minor suggestions, nothing blocking."
|
|
146839
|
+
- \`> \u2705 ...\` \u2014 green friendly blockquote. Reads as "no concerns, mergeable."
|
|
146614
146840
|
|
|
146615
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.
|
|
146616
146842
|
|
|
@@ -146619,25 +146845,25 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
146619
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):
|
|
146620
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\`.
|
|
146621
146847
|
- **minor suggestions only** (single-line nits, doc/comment polish, defer-able observations, "rough edges"):
|
|
146622
|
-
\`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.
|
|
146623
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):
|
|
146624
|
-
\`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.
|
|
146625
146851
|
- **no actionable issues**:
|
|
146626
|
-
\`approved: true\`. Body opens with
|
|
146852
|
+
\`approved: true\`. Body opens with \`> \u2705 No new issues found.\\n\\n\` followed by the PR summary.
|
|
146627
146853
|
|
|
146628
146854
|
${PR_SUMMARY_FORMAT}`
|
|
146629
146855
|
},
|
|
146630
|
-
// IncrementalReview shares Review's 0-or-2+ lens pattern
|
|
146631
|
-
//
|
|
146632
|
-
//
|
|
146633
|
-
//
|
|
146634
|
-
// subagents matches the canonical anneal
|
|
146635
|
-
// pre-existing failures — don't flag these"
|
|
146636
|
-
// regressions the new commits amplified.
|
|
146637
|
-
//
|
|
146638
|
-
//
|
|
146639
|
-
//
|
|
146640
|
-
//
|
|
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.
|
|
146641
146867
|
{
|
|
146642
146868
|
name: "IncrementalReview",
|
|
146643
146869
|
description: "Re-review a PR after new commits are pushed; focus on new changes since the last review",
|
|
@@ -146649,7 +146875,15 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
146649
146875
|
|
|
146650
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.
|
|
146651
146877
|
|
|
146652
|
-
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.
|
|
146653
146887
|
|
|
146654
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.**
|
|
146655
146889
|
|
|
@@ -146695,22 +146929,28 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
146695
146929
|
- do NOT pre-shape their output with a finding schema
|
|
146696
146930
|
- do NOT mention the other lenses (independence is the point)
|
|
146697
146931
|
|
|
146698
|
-
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.
|
|
146699
146935
|
|
|
146700
|
-
|
|
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.
|
|
146701
146939
|
|
|
146702
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.
|
|
146703
146941
|
|
|
146704
|
-
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.
|
|
146705
146943
|
|
|
146706
146944
|
Follow these rules:
|
|
146707
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.
|
|
146708
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.
|
|
146709
|
-
- 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 ...\`,
|
|
146710
|
-
- 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> ...\`,
|
|
146711
|
-
- 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
|
|
146712
|
-
- 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 \`>
|
|
146713
|
-
- 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}`
|
|
146714
146954
|
},
|
|
146715
146955
|
{
|
|
146716
146956
|
name: "Plan",
|
|
@@ -146725,7 +146965,7 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
146725
146965
|
|
|
146726
146966
|
3. Produce a structured, actionable plan with clear milestones.
|
|
146727
146967
|
|
|
146728
|
-
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.`
|
|
146729
146969
|
},
|
|
146730
146970
|
{
|
|
146731
146971
|
name: "Fix",
|
|
@@ -146817,6 +147057,7 @@ function initToolState(params) {
|
|
|
146817
147057
|
return {
|
|
146818
147058
|
progressComment: resolved,
|
|
146819
147059
|
hadProgressComment: !!resolved,
|
|
147060
|
+
prepushFailureCount: 0,
|
|
146820
147061
|
backgroundProcesses: /* @__PURE__ */ new Map(),
|
|
146821
147062
|
usageEntries: []
|
|
146822
147063
|
};
|
|
@@ -146916,6 +147157,17 @@ async function installFromNpmTarball(params) {
|
|
|
146916
147157
|
// utils/providerErrors.ts
|
|
146917
147158
|
var statusKey = `\\b(?:status[_ ]?code|http[_ ]?status|status)["']?\\s*[:=]\\s*["']?`;
|
|
146918
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" },
|
|
146919
147171
|
// auth patterns must come BEFORE rate-limit patterns. OpenRouter 401 error
|
|
146920
147172
|
// payloads carry `x-ratelimit-*` response headers in the dump, and the
|
|
146921
147173
|
// free-form rate-limit regex below would otherwise win on word-boundary
|
|
@@ -147042,11 +147294,25 @@ function addSkill(params) {
|
|
|
147042
147294
|
);
|
|
147043
147295
|
if (result.status === 0) {
|
|
147044
147296
|
log.success(`installed ${params.skill} skill (${params.agent})`);
|
|
147045
|
-
|
|
147046
|
-
const stderr = (result.stderr?.toString() || "").trim();
|
|
147047
|
-
const errorMsg = result.error ? result.error.message : stderr;
|
|
147048
|
-
log.info(`${params.skill} skill install failed: ${errorMsg}`);
|
|
147297
|
+
return;
|
|
147049
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")}`;
|
|
147050
147316
|
}
|
|
147051
147317
|
|
|
147052
147318
|
// utils/timer.ts
|
|
@@ -147143,7 +147409,7 @@ function buildUnsubmittedReviewPrompt(mode) {
|
|
|
147143
147409
|
return [
|
|
147144
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.`,
|
|
147145
147411
|
"",
|
|
147146
|
-
"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.",
|
|
147147
147413
|
"",
|
|
147148
147414
|
"do NOT stop again until `create_pull_request_review` has been called successfully."
|
|
147149
147415
|
].join("\n");
|
|
@@ -147189,6 +147455,11 @@ function buildPostRunPrompt(issues) {
|
|
|
147189
147455
|
if (issues.summaryStale) parts.push(buildSummaryStalePrompt(issues.summaryStale.filePath));
|
|
147190
147456
|
return parts.join("\n\n---\n\n");
|
|
147191
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
|
+
}
|
|
147192
147463
|
function buildLearningsReflectionPrompt(filePath) {
|
|
147193
147464
|
return [
|
|
147194
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?`,
|
|
@@ -147200,18 +147471,16 @@ function buildLearningsReflectionPrompt(filePath) {
|
|
|
147200
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.`,
|
|
147201
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.`,
|
|
147202
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
|
+
"",
|
|
147203
147476
|
`bullet hygiene:`,
|
|
147204
|
-
`- one fact per line starting with \`-
|
|
147205
|
-
`-
|
|
147206
|
-
`-
|
|
147207
|
-
|
|
147208
|
-
|
|
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.`,
|
|
147209
147482
|
"",
|
|
147210
|
-
`
|
|
147211
|
-
`- 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.`,
|
|
147212
|
-
`- 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.`,
|
|
147213
|
-
`- 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.`,
|
|
147214
|
-
`- 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.`,
|
|
147215
147484
|
"",
|
|
147216
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.`
|
|
147217
147486
|
].join("\n");
|
|
@@ -147431,7 +147700,7 @@ function stripProviderPrefix(specifier) {
|
|
|
147431
147700
|
function resolveEffort(_model) {
|
|
147432
147701
|
return "high";
|
|
147433
147702
|
}
|
|
147434
|
-
function
|
|
147703
|
+
function tailLines2(text, maxCodeUnits) {
|
|
147435
147704
|
if (text.length <= maxCodeUnits) return text;
|
|
147436
147705
|
const tail = text.slice(-maxCodeUnits);
|
|
147437
147706
|
const firstNewline = tail.indexOf("\n");
|
|
@@ -147495,6 +147764,7 @@ async function runClaude(params) {
|
|
|
147495
147764
|
}
|
|
147496
147765
|
} else if (block.type === "tool_use") {
|
|
147497
147766
|
const toolName = block.name || "unknown";
|
|
147767
|
+
suspendActivity();
|
|
147498
147768
|
if (params.onToolUse) {
|
|
147499
147769
|
params.onToolUse({
|
|
147500
147770
|
toolName,
|
|
@@ -147539,6 +147809,7 @@ async function runClaude(params) {
|
|
|
147539
147809
|
for (const block of content) {
|
|
147540
147810
|
if (typeof block === "string") continue;
|
|
147541
147811
|
if (block.type === "tool_result") {
|
|
147812
|
+
resumeActivity();
|
|
147542
147813
|
timerFor(label).markToolResult();
|
|
147543
147814
|
const outputContent = typeof block.content === "string" ? block.content : Array.isArray(block.content) ? block.content.map(
|
|
147544
147815
|
(entry) => typeof entry === "string" ? entry : typeof entry === "object" && entry !== null && "text" in entry ? String(entry.text) : JSON.stringify(entry)
|
|
@@ -147627,6 +147898,7 @@ async function runClaude(params) {
|
|
|
147627
147898
|
env: params.env,
|
|
147628
147899
|
activityTimeout: 3e5,
|
|
147629
147900
|
onActivityTimeout: params.onActivityTimeout,
|
|
147901
|
+
isPausedExternally: isActivitySuspended,
|
|
147630
147902
|
stdio: ["ignore", "pipe", "pipe"],
|
|
147631
147903
|
// run claude in its own process group so SIGKILL on activity timeout /
|
|
147632
147904
|
// outer cancellation reaches any subprocesses it spawns (rg, file
|
|
@@ -147720,7 +147992,7 @@ ${stderrContext}`);
|
|
|
147720
147992
|
const errorContext = lastProviderError ? ` (${lastProviderError})` : "";
|
|
147721
147993
|
const stdoutSnapshot = output.toString();
|
|
147722
147994
|
const stderrSnapshot = recentStderr.join("\n");
|
|
147723
|
-
const truncatedStdout = stdoutSnapshot ?
|
|
147995
|
+
const truncatedStdout = stdoutSnapshot ? tailLines2(stdoutSnapshot, 2048) : "";
|
|
147724
147996
|
const nonJsonStdoutSnapshot = recentNonJsonStdout.join("\n");
|
|
147725
147997
|
const errorMessage = lastResultError || stderrSnapshot || nonJsonStdoutSnapshot || truncatedStdout || `unknown error - no output from Claude CLI${errorContext}`;
|
|
147726
147998
|
log.error(
|
|
@@ -147782,6 +148054,7 @@ ${stderrContext}`
|
|
|
147782
148054
|
}
|
|
147783
148055
|
var MANAGED_SETTINGS_DIR = "/etc/claude-code";
|
|
147784
148056
|
var MANAGED_SETTINGS_PATH = `${MANAGED_SETTINGS_DIR}/managed-settings.json`;
|
|
148057
|
+
var CODEX_AUTH_DENY_PATH = "~/.local/share/opencode/auth.json";
|
|
147785
148058
|
var managedSettings = {
|
|
147786
148059
|
allowManagedPermissionRulesOnly: true,
|
|
147787
148060
|
allowManagedHooksOnly: true,
|
|
@@ -147794,12 +148067,16 @@ var managedSettings = {
|
|
|
147794
148067
|
"Edit(//proc/**)",
|
|
147795
148068
|
"Edit(//sys/**)",
|
|
147796
148069
|
"Glob(//proc/**)",
|
|
147797
|
-
"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})`
|
|
147798
148075
|
]
|
|
147799
148076
|
},
|
|
147800
148077
|
sandbox: {
|
|
147801
148078
|
filesystem: {
|
|
147802
|
-
denyRead: ["/proc", "/sys"]
|
|
148079
|
+
denyRead: ["/proc", "/sys", CODEX_AUTH_DENY_PATH]
|
|
147803
148080
|
}
|
|
147804
148081
|
}
|
|
147805
148082
|
};
|
|
@@ -147860,14 +148137,21 @@ var claude = agent({
|
|
|
147860
148137
|
if (model) {
|
|
147861
148138
|
baseArgs.push("--model", model);
|
|
147862
148139
|
}
|
|
148140
|
+
const repoDir = process.cwd();
|
|
147863
148141
|
const env2 = {
|
|
147864
148142
|
...process.env,
|
|
147865
|
-
...homeEnv
|
|
148143
|
+
...homeEnv,
|
|
148144
|
+
PWD: repoDir
|
|
147866
148145
|
};
|
|
147867
148146
|
if (isBedrockRoute) {
|
|
147868
148147
|
env2.CLAUDE_CODE_USE_BEDROCK = "1";
|
|
147869
148148
|
}
|
|
147870
|
-
|
|
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
|
+
}
|
|
147871
148155
|
log.info(`\xBB effort: ${effort}`);
|
|
147872
148156
|
log.debug(`\xBB starting Pullfrog (Claude Code): node ${baseArgs.join(" ")}`);
|
|
147873
148157
|
log.debug(`\xBB working directory: ${repoDir}`);
|
|
@@ -147887,7 +148171,7 @@ var claude = agent({
|
|
|
147887
148171
|
ctx,
|
|
147888
148172
|
initialResult: result,
|
|
147889
148173
|
initialUsage: result.usage,
|
|
147890
|
-
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,
|
|
147891
148175
|
canResume: (r) => Boolean(r.sessionId),
|
|
147892
148176
|
resume: async (c) => {
|
|
147893
148177
|
const sessionId = c.previousResult.sessionId;
|
|
@@ -147901,16 +148185,19 @@ var claude = agent({
|
|
|
147901
148185
|
}
|
|
147902
148186
|
});
|
|
147903
148187
|
|
|
147904
|
-
// agents/
|
|
147905
|
-
|
|
147906
|
-
import { mkdirSync as
|
|
147907
|
-
import { join as
|
|
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";
|
|
147908
148192
|
import { performance as performance7 } from "node:perf_hooks";
|
|
147909
148193
|
|
|
147910
148194
|
// utils/agentHangReport.ts
|
|
147911
148195
|
var MAX_STDERR_BYTES = 3e3;
|
|
147912
148196
|
function formatAgentHangBody(input) {
|
|
147913
148197
|
if (!input.diagnostic) return null;
|
|
148198
|
+
if (input.diagnostic.lastProviderError === "provider billing exhausted") {
|
|
148199
|
+
return formatBillingExhaustedBody(input.diagnostic);
|
|
148200
|
+
}
|
|
147914
148201
|
const verb = input.isHang ? "stalled" : "failed";
|
|
147915
148202
|
const cause = input.diagnostic.lastProviderError ? ` \u2014 likely cause: \`${input.diagnostic.lastProviderError}\`` : "";
|
|
147916
148203
|
const headline = `**${input.diagnostic.label} ${verb}**${cause}`;
|
|
@@ -147968,6 +148255,97 @@ function pickFence(content) {
|
|
|
147968
148255
|
}
|
|
147969
148256
|
return "`".repeat(Math.max(3, max + 1));
|
|
147970
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
|
|
148290
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "node:fs";
|
|
148291
|
+
import { homedir } from "node:os";
|
|
148292
|
+
import { join as join11 } from "node:path";
|
|
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
|
+
}
|
|
147971
148349
|
|
|
147972
148350
|
// agents/opencodePlugin.ts
|
|
147973
148351
|
var PULLFROG_BUS_EVENT_TYPE = "pullfrog_bus_event";
|
|
@@ -148050,6 +148428,9 @@ export default async function pullfrogEventsPlugin() {
|
|
|
148050
148428
|
}
|
|
148051
148429
|
`;
|
|
148052
148430
|
|
|
148431
|
+
// agents/opencodeShared.ts
|
|
148432
|
+
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
148433
|
+
|
|
148053
148434
|
// agents/subagentModels.ts
|
|
148054
148435
|
function deriveSubagentModels(orchestratorSpec) {
|
|
148055
148436
|
if (!orchestratorSpec) return { reviewer: void 0 };
|
|
@@ -148066,68 +148447,14 @@ function deriveSubagentModels(orchestratorSpec) {
|
|
|
148066
148447
|
return { reviewer: void 0 };
|
|
148067
148448
|
}
|
|
148068
148449
|
|
|
148069
|
-
// agents/
|
|
148070
|
-
|
|
148071
|
-
return
|
|
148072
|
-
|
|
148073
|
-
|
|
148074
|
-
|
|
148075
|
-
|
|
148076
|
-
|
|
148077
|
-
}
|
|
148078
|
-
var GEMINI_3_DIRECT_THINKING_LEVEL = "medium";
|
|
148079
|
-
var GEMINI_3_DIRECT_API_IDS = ["gemini-3.1-pro-preview", "gemini-3-flash-preview"];
|
|
148080
|
-
function buildSecurityConfig(ctx, model) {
|
|
148081
|
-
const config3 = {
|
|
148082
|
-
permission: {
|
|
148083
|
-
bash: "deny",
|
|
148084
|
-
edit: "allow",
|
|
148085
|
-
read: "allow",
|
|
148086
|
-
webfetch: "allow",
|
|
148087
|
-
external_directory: "allow",
|
|
148088
|
-
skill: "allow"
|
|
148089
|
-
},
|
|
148090
|
-
mcp: {
|
|
148091
|
-
[pullfrogMcpName]: { type: "remote", url: ctx.mcpServerUrl }
|
|
148092
|
-
},
|
|
148093
|
-
agent: (() => {
|
|
148094
|
-
const cfg = buildReviewerAgentConfig(model);
|
|
148095
|
-
const reviewerModel = cfg[REVIEWER_AGENT_NAME]?.model ?? "(inherit)";
|
|
148096
|
-
log.info(`\xBB subagent models: reviewfrog=${reviewerModel}`);
|
|
148097
|
-
return cfg;
|
|
148098
|
-
})(),
|
|
148099
|
-
// opt into opencode's experimental `batch` tool (added in
|
|
148100
|
-
// anomalyco/opencode PR #2983, opt-in via `experimental.batch_tool`). it
|
|
148101
|
-
// exposes a single `batch` tool that runs 1-25 independent tool calls
|
|
148102
|
-
// (read/grep/glob/bash/etc.) concurrently in one assistant turn, which
|
|
148103
|
-
// collapses the dominant grep→20×read pattern into a single round trip.
|
|
148104
|
-
// edits are explicitly disallowed inside the batch upstream. paired with
|
|
148105
|
-
// the "Parallel tool execution" guidance in utils/instructions.ts so the
|
|
148106
|
-
// model actually reaches for it. see wiki/prompt.md.
|
|
148107
|
-
experimental: { batch_tool: true },
|
|
148108
|
-
provider: {
|
|
148109
|
-
google: {
|
|
148110
|
-
models: Object.fromEntries(
|
|
148111
|
-
GEMINI_3_DIRECT_API_IDS.map((id) => [
|
|
148112
|
-
id,
|
|
148113
|
-
{
|
|
148114
|
-
options: {
|
|
148115
|
-
thinkingConfig: { thinkingLevel: GEMINI_3_DIRECT_THINKING_LEVEL }
|
|
148116
|
-
}
|
|
148117
|
-
}
|
|
148118
|
-
])
|
|
148119
|
-
)
|
|
148120
|
-
}
|
|
148121
|
-
}
|
|
148122
|
-
};
|
|
148123
|
-
if (model) {
|
|
148124
|
-
config3.model = model;
|
|
148125
|
-
const slashIndex = model.indexOf("/");
|
|
148126
|
-
if (slashIndex > 0) {
|
|
148127
|
-
config3.enabled_providers = [model.slice(0, slashIndex).toLowerCase()];
|
|
148128
|
-
}
|
|
148129
|
-
}
|
|
148130
|
-
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
|
+
);
|
|
148131
148458
|
}
|
|
148132
148459
|
function buildReviewerAgentConfig(orchestratorModel) {
|
|
148133
148460
|
const overrides = deriveSubagentModels(orchestratorModel);
|
|
@@ -148140,6 +148467,15 @@ function buildReviewerAgentConfig(orchestratorModel) {
|
|
|
148140
148467
|
}
|
|
148141
148468
|
};
|
|
148142
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.";
|
|
148143
148479
|
function getOpenCodeModels(cliPath) {
|
|
148144
148480
|
try {
|
|
148145
148481
|
const output = execFileSync4(cliPath, ["models"], {
|
|
@@ -148155,7 +148491,6 @@ function getOpenCodeModels(cliPath) {
|
|
|
148155
148491
|
return [];
|
|
148156
148492
|
}
|
|
148157
148493
|
}
|
|
148158
|
-
var AUTO_SELECT_WARNING = "select a model explicitly in the Pullfrog console (https://pullfrog.com/console) to avoid this.";
|
|
148159
148494
|
function autoSelectModel(cliPath) {
|
|
148160
148495
|
const availableModels = getOpenCodeModels(cliPath);
|
|
148161
148496
|
const availableSet = new Set(availableModels);
|
|
@@ -148176,6 +148511,58 @@ function autoSelectModel(cliPath) {
|
|
|
148176
148511
|
log.warning(`\xBB no model resolved. letting OpenCode auto-select. ${AUTO_SELECT_WARNING}`);
|
|
148177
148512
|
return void 0;
|
|
148178
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
|
+
}
|
|
148179
148566
|
async function runOpenCode(params) {
|
|
148180
148567
|
const startTime = performance7.now();
|
|
148181
148568
|
let eventCount = 0;
|
|
@@ -148184,9 +148571,10 @@ async function runOpenCode(params) {
|
|
|
148184
148571
|
let accumulatedCostUsd = 0;
|
|
148185
148572
|
let tokensLogged = false;
|
|
148186
148573
|
const toolCallTimings = /* @__PURE__ */ new Map();
|
|
148187
|
-
let
|
|
148188
|
-
|
|
148189
|
-
let
|
|
148574
|
+
let lastEventAt = performance7.now();
|
|
148575
|
+
const recentStderr = [];
|
|
148576
|
+
let lastProviderError = null;
|
|
148577
|
+
let agentErrorEvent = null;
|
|
148190
148578
|
const labeler = new SessionLabeler();
|
|
148191
148579
|
function eventLabel(event) {
|
|
148192
148580
|
const sid = event.sessionID ?? event.session_id;
|
|
@@ -148195,30 +148583,15 @@ async function runOpenCode(params) {
|
|
|
148195
148583
|
function withLabel(label, message) {
|
|
148196
148584
|
return label === ORCHESTRATOR_LABEL ? message : formatWithLabel(label, message);
|
|
148197
148585
|
}
|
|
148198
|
-
const thinkingTimers = /* @__PURE__ */ new Map();
|
|
148199
|
-
function timerFor(label) {
|
|
148200
|
-
let t = thinkingTimers.get(label);
|
|
148201
|
-
if (!t) {
|
|
148202
|
-
const formatLine = (line) => label === ORCHESTRATOR_LABEL ? line : formatWithLabel(label, line);
|
|
148203
|
-
t = new ThinkingTimer(formatLine);
|
|
148204
|
-
thinkingTimers.set(label, t);
|
|
148205
|
-
}
|
|
148206
|
-
return t;
|
|
148207
|
-
}
|
|
148208
148586
|
const taskDispatchByCallID = /* @__PURE__ */ new Map();
|
|
148209
|
-
|
|
148210
|
-
const knownNonTaskCallIDs = /* @__PURE__ */ new Set();
|
|
148211
|
-
function emitSubagentFinished(dispatch, status, output2, matchKind) {
|
|
148587
|
+
function emitSubagentFinished(dispatch, status, output2) {
|
|
148212
148588
|
const subagentDuration = performance7.now() - dispatch.startedAt;
|
|
148213
148589
|
const outputStr = typeof output2 === "string" ? output2 : "";
|
|
148214
148590
|
const outputPreview = outputStr.length > 120 ? `${outputStr.slice(0, 120)}\u2026` : outputStr;
|
|
148215
|
-
const matchSuffix = matchKind === "fifo" ? " [fifo-matched]" : "";
|
|
148216
148591
|
log.info(
|
|
148217
|
-
`\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, " ")}` : "")
|
|
148218
148593
|
);
|
|
148219
148594
|
taskDispatchByCallID.delete(dispatch.toolUseCallID);
|
|
148220
|
-
const idx = pendingTaskDispatches.indexOf(dispatch);
|
|
148221
|
-
if (idx >= 0) pendingTaskDispatches.splice(idx, 1);
|
|
148222
148595
|
}
|
|
148223
148596
|
function buildUsage() {
|
|
148224
148597
|
const totalInput = accumulatedTokens.input + accumulatedTokens.cacheRead + accumulatedTokens.cacheWrite;
|
|
@@ -148232,55 +148605,6 @@ async function runOpenCode(params) {
|
|
|
148232
148605
|
} : void 0;
|
|
148233
148606
|
}
|
|
148234
148607
|
const handlers2 = {
|
|
148235
|
-
init: (event) => {
|
|
148236
|
-
const label = labeler.labelFor(event.session_id ?? null);
|
|
148237
|
-
log.debug(
|
|
148238
|
-
withLabel(
|
|
148239
|
-
label,
|
|
148240
|
-
`\xBB ${params.label} init: session_id=${event.session_id || "unknown"}, model=${event.model || "unknown"}`
|
|
148241
|
-
)
|
|
148242
|
-
);
|
|
148243
|
-
log.debug(withLabel(label, `\xBB ${params.label} init event (full): ${JSON.stringify(event)}`));
|
|
148244
|
-
if (label === ORCHESTRATOR_LABEL) {
|
|
148245
|
-
finalOutput = "";
|
|
148246
|
-
accumulatedTokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
148247
|
-
accumulatedCostUsd = 0;
|
|
148248
|
-
tokensLogged = false;
|
|
148249
|
-
} else {
|
|
148250
|
-
log.info(`\xBB ${params.label} subagent init: ${label} (session ${event.session_id || "?"})`);
|
|
148251
|
-
}
|
|
148252
|
-
},
|
|
148253
|
-
message: (event) => {
|
|
148254
|
-
const label = eventLabel(event);
|
|
148255
|
-
if (event.role === "assistant" && event.content?.trim()) {
|
|
148256
|
-
const message = event.content.trim();
|
|
148257
|
-
if (event.delta) {
|
|
148258
|
-
log.debug(
|
|
148259
|
-
withLabel(
|
|
148260
|
-
label,
|
|
148261
|
-
`\xBB ${params.label} thinking: ${message.substring(0, 300)}${message.length > 300 ? "..." : ""}`
|
|
148262
|
-
)
|
|
148263
|
-
);
|
|
148264
|
-
} else {
|
|
148265
|
-
log.debug(
|
|
148266
|
-
withLabel(
|
|
148267
|
-
label,
|
|
148268
|
-
`\xBB ${params.label} message (${event.role}): ${message.substring(0, 100)}${message.length > 100 ? "..." : ""}`
|
|
148269
|
-
)
|
|
148270
|
-
);
|
|
148271
|
-
if (label === ORCHESTRATOR_LABEL) {
|
|
148272
|
-
finalOutput = message;
|
|
148273
|
-
}
|
|
148274
|
-
}
|
|
148275
|
-
} else if (event.role === "user") {
|
|
148276
|
-
log.debug(
|
|
148277
|
-
withLabel(
|
|
148278
|
-
label,
|
|
148279
|
-
`\xBB ${params.label} message (${event.role}): ${event.content?.substring(0, 100) || ""}${event.content && event.content.length > 100 ? "..." : ""}`
|
|
148280
|
-
)
|
|
148281
|
-
);
|
|
148282
|
-
}
|
|
148283
|
-
},
|
|
148284
148608
|
text: (event) => {
|
|
148285
148609
|
if (event.part?.text?.trim()) {
|
|
148286
148610
|
const message = event.part.text.trim();
|
|
@@ -148292,119 +148616,90 @@ async function runOpenCode(params) {
|
|
|
148292
148616
|
}
|
|
148293
148617
|
}
|
|
148294
148618
|
},
|
|
148295
|
-
|
|
148296
|
-
|
|
148297
|
-
|
|
148298
|
-
|
|
148299
|
-
|
|
148300
|
-
|
|
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: () => {
|
|
148301
148640
|
},
|
|
148302
|
-
step_finish:
|
|
148303
|
-
const
|
|
148304
|
-
|
|
148305
|
-
|
|
148306
|
-
accumulatedTokens.
|
|
148307
|
-
accumulatedTokens.
|
|
148308
|
-
accumulatedTokens.
|
|
148309
|
-
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;
|
|
148310
148648
|
}
|
|
148311
148649
|
if (typeof event.part?.cost === "number" && Number.isFinite(event.part.cost)) {
|
|
148312
148650
|
accumulatedCostUsd += event.part.cost;
|
|
148313
148651
|
}
|
|
148314
|
-
if (currentStepId === stepId) {
|
|
148315
|
-
currentStepId = null;
|
|
148316
|
-
currentStepType = null;
|
|
148317
|
-
}
|
|
148318
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
|
+
*/
|
|
148319
148658
|
tool_use: (event) => {
|
|
148320
148659
|
const toolName = event.part?.tool;
|
|
148321
148660
|
const toolId = event.part?.callID;
|
|
148661
|
+
const state = event.part?.state;
|
|
148322
148662
|
if (!toolName || !toolId) {
|
|
148323
148663
|
log.info(
|
|
148324
148664
|
`\xBB tool_use event missing toolName or toolId: ${JSON.stringify(event).substring(0, 500)}`
|
|
148325
148665
|
);
|
|
148326
148666
|
return;
|
|
148327
148667
|
}
|
|
148328
|
-
|
|
148329
|
-
|
|
148330
|
-
const taskInput = event.part?.state?.input ?? {};
|
|
148331
|
-
const dispatchedLabel = labeler.recordTaskDispatch(taskInput);
|
|
148332
|
-
const dispatch = {
|
|
148333
|
-
label: dispatchedLabel,
|
|
148334
|
-
startedAt: performance7.now(),
|
|
148335
|
-
toolUseCallID: toolId
|
|
148336
|
-
};
|
|
148337
|
-
taskDispatchByCallID.set(toolId, dispatch);
|
|
148338
|
-
pendingTaskDispatches.push(dispatch);
|
|
148339
|
-
log.info(
|
|
148340
|
-
`\xBB dispatching subagent: ${dispatchedLabel}` + (taskInput.subagent_type ? ` (subagent_type=${taskInput.subagent_type})` : "")
|
|
148341
|
-
);
|
|
148342
|
-
}
|
|
148343
|
-
} else {
|
|
148344
|
-
knownNonTaskCallIDs.add(toolId);
|
|
148345
|
-
}
|
|
148668
|
+
const status = state?.status;
|
|
148669
|
+
const isTerminal2 = status === "completed" || status === "error";
|
|
148346
148670
|
const label = eventLabel(event);
|
|
148347
|
-
if (
|
|
148348
|
-
|
|
148349
|
-
|
|
148350
|
-
|
|
148351
|
-
|
|
148352
|
-
|
|
148353
|
-
|
|
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
|
|
148354
148678
|
});
|
|
148679
|
+
log.info(
|
|
148680
|
+
`\xBB dispatching subagent: ${dispatchedLabel}` + (taskInput.subagent_type ? ` (subagent_type=${taskInput.subagent_type})` : "")
|
|
148681
|
+
);
|
|
148355
148682
|
}
|
|
148356
|
-
|
|
148357
|
-
|
|
148358
|
-
|
|
148359
|
-
log.info(withLabel(label, toolCallLine));
|
|
148360
|
-
if (event.part?.state?.status === "completed" && event.part.state.output) {
|
|
148361
|
-
log.debug(withLabel(label, ` output: ${event.part.state.output}`));
|
|
148362
|
-
}
|
|
148363
|
-
if (event.part?.state?.status === "error") {
|
|
148364
|
-
log.info(withLabel(label, `\xBB tool call failed: ${event.part.state.error}`));
|
|
148365
|
-
}
|
|
148366
|
-
if (toolName.includes("report_progress") && params.todoTracker) {
|
|
148367
|
-
log.debug("\xBB report_progress detected, disabling todo tracking");
|
|
148368
|
-
params.todoTracker.cancel();
|
|
148683
|
+
params.onToolUse?.({ toolName, input: state?.input });
|
|
148684
|
+
if (!toolCallTimings.has(toolId)) {
|
|
148685
|
+
toolCallTimings.set(toolId, performance7.now());
|
|
148369
148686
|
}
|
|
148370
|
-
|
|
148371
|
-
|
|
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}`));
|
|
148372
148692
|
}
|
|
148373
|
-
|
|
148374
|
-
|
|
148375
|
-
const toolId = event.part?.callID || event.tool_id;
|
|
148376
|
-
const state = event.part?.state;
|
|
148377
|
-
const status = state?.status ?? event.status ?? "unknown";
|
|
148378
|
-
const payload = state?.status === "completed" ? state.output : state?.status === "error" ? state.error : event.output;
|
|
148379
|
-
const label = eventLabel(event);
|
|
148380
|
-
timerFor(label).markToolResult();
|
|
148381
|
-
if (taskDispatchByCallID.size > 0 || pendingTaskDispatches.length > 0) {
|
|
148382
|
-
if (toolId && taskDispatchByCallID.has(toolId)) {
|
|
148383
|
-
const dispatch = taskDispatchByCallID.get(toolId);
|
|
148384
|
-
if (dispatch) emitSubagentFinished(dispatch, status, payload, "exact");
|
|
148385
|
-
} else {
|
|
148386
|
-
const callIDIsKnownNonTask = toolId ? knownNonTaskCallIDs.has(toolId) : false;
|
|
148387
|
-
if (!callIDIsKnownNonTask && pendingTaskDispatches.length > 0) {
|
|
148388
|
-
const dispatch = pendingTaskDispatches[0];
|
|
148389
|
-
emitSubagentFinished(dispatch, status, payload, "fifo");
|
|
148390
|
-
}
|
|
148391
|
-
}
|
|
148693
|
+
if (state?.status === "error") {
|
|
148694
|
+
log.info(withLabel(label, `\xBB tool call failed: ${state.error}`));
|
|
148392
148695
|
}
|
|
148393
|
-
if (
|
|
148696
|
+
if (isTerminal2) {
|
|
148697
|
+
const dispatch = toolName === "task" ? taskDispatchByCallID.get(toolId) : void 0;
|
|
148698
|
+
if (dispatch) emitSubagentFinished(dispatch, status, terminalPayload(state));
|
|
148394
148699
|
const toolStartTime = toolCallTimings.get(toolId);
|
|
148395
|
-
if (toolStartTime) {
|
|
148700
|
+
if (toolStartTime !== void 0) {
|
|
148396
148701
|
const toolDuration = performance7.now() - toolStartTime;
|
|
148397
148702
|
toolCallTimings.delete(toolId);
|
|
148398
|
-
const stepContext = currentStepId ? ` (step=${currentStepType || "unknown"})` : "";
|
|
148399
|
-
log.debug(
|
|
148400
|
-
withLabel(
|
|
148401
|
-
label,
|
|
148402
|
-
`\xBB ${params.label} tool_result${stepContext}: id=${toolId}, status=${status}, duration=${Math.round(toolDuration)}ms`
|
|
148403
|
-
)
|
|
148404
|
-
);
|
|
148405
|
-
if (payload) {
|
|
148406
|
-
log.debug(withLabel(label, ` output: ${payload}`));
|
|
148407
|
-
}
|
|
148408
148703
|
if (toolDuration > 5e3) {
|
|
148409
148704
|
log.info(
|
|
148410
148705
|
withLabel(
|
|
@@ -148415,10 +148710,12 @@ async function runOpenCode(params) {
|
|
|
148415
148710
|
}
|
|
148416
148711
|
}
|
|
148417
148712
|
}
|
|
148418
|
-
if (
|
|
148419
|
-
log.
|
|
148420
|
-
|
|
148421
|
-
|
|
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);
|
|
148422
148719
|
}
|
|
148423
148720
|
},
|
|
148424
148721
|
error: (event) => {
|
|
@@ -148427,23 +148724,18 @@ async function runOpenCode(params) {
|
|
|
148427
148724
|
const errorMessage = event.error?.data?.message || event.error?.name || JSON.stringify(event);
|
|
148428
148725
|
log.info(`\xBB ${params.label} error event: ${errorName}: ${errorMessage}`);
|
|
148429
148726
|
},
|
|
148430
|
-
|
|
148431
|
-
|
|
148432
|
-
|
|
148433
|
-
|
|
148434
|
-
|
|
148435
|
-
|
|
148436
|
-
|
|
148437
|
-
|
|
148438
|
-
|
|
148439
|
-
|
|
148440
|
-
|
|
148441
|
-
|
|
148442
|
-
logTokenTable({ ...accumulatedTokens, costUsd: accumulatedCostUsd });
|
|
148443
|
-
tokensLogged = true;
|
|
148444
|
-
}
|
|
148445
|
-
}
|
|
148446
|
-
},
|
|
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
|
+
*/
|
|
148447
148739
|
[PULLFROG_BUS_EVENT_TYPE]: async (event) => {
|
|
148448
148740
|
const busEvent = event.bus_event;
|
|
148449
148741
|
if (!busEvent || busEvent.type !== "message.part.updated") return;
|
|
@@ -148453,20 +148745,15 @@ async function runOpenCode(params) {
|
|
|
148453
148745
|
const partType = part.type;
|
|
148454
148746
|
if (partType === "tool") {
|
|
148455
148747
|
const status = part.state?.status;
|
|
148456
|
-
|
|
148457
|
-
|
|
148458
|
-
|
|
148459
|
-
const callID = partWithToolFields.callID;
|
|
148460
|
-
if (typeof callID === "string" && !taskDispatchByCallID.has(callID)) {
|
|
148461
|
-
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 ?? {};
|
|
148462
148751
|
const dispatchedLabel = labeler.recordTaskDispatch(taskInput);
|
|
148463
|
-
|
|
148752
|
+
taskDispatchByCallID.set(part.callID, {
|
|
148464
148753
|
label: dispatchedLabel,
|
|
148465
148754
|
startedAt: performance7.now(),
|
|
148466
|
-
toolUseCallID: callID
|
|
148467
|
-
};
|
|
148468
|
-
taskDispatchByCallID.set(callID, dispatch);
|
|
148469
|
-
pendingTaskDispatches.push(dispatch);
|
|
148755
|
+
toolUseCallID: part.callID
|
|
148756
|
+
});
|
|
148470
148757
|
log.info(
|
|
148471
148758
|
`\xBB dispatching subagent: ${dispatchedLabel}` + (taskInput.subagent_type ? ` (subagent_type=${taskInput.subagent_type})` : "")
|
|
148472
148759
|
);
|
|
@@ -148474,27 +148761,19 @@ async function runOpenCode(params) {
|
|
|
148474
148761
|
return;
|
|
148475
148762
|
}
|
|
148476
148763
|
if (status !== "completed" && status !== "error") return;
|
|
148477
|
-
await handlers2.tool_use({
|
|
148478
|
-
type: "tool_use",
|
|
148479
|
-
sessionID,
|
|
148480
|
-
part
|
|
148481
|
-
});
|
|
148764
|
+
await handlers2.tool_use({ type: "tool_use", sessionID, part });
|
|
148482
148765
|
return;
|
|
148483
148766
|
}
|
|
148484
148767
|
if (partType === "step-start" || partType === "step-finish") return;
|
|
148485
148768
|
if (partType === "text" && part.time?.end !== void 0) {
|
|
148486
|
-
|
|
148487
|
-
type: "text",
|
|
148488
|
-
sessionID,
|
|
148489
|
-
part
|
|
148490
|
-
});
|
|
148769
|
+
handlers2.text({ type: "text", sessionID, part });
|
|
148491
148770
|
return;
|
|
148492
148771
|
}
|
|
148772
|
+
if (partType === "reasoning" && part.time?.end !== void 0) {
|
|
148773
|
+
handlers2.reasoning({ type: "reasoning", sessionID, part });
|
|
148774
|
+
}
|
|
148493
148775
|
}
|
|
148494
148776
|
};
|
|
148495
|
-
const recentStderr = [];
|
|
148496
|
-
let lastProviderError = null;
|
|
148497
|
-
let agentErrorEvent = null;
|
|
148498
148777
|
const diagnostic = {
|
|
148499
148778
|
label: params.label,
|
|
148500
148779
|
recentStderr,
|
|
@@ -148552,15 +148831,15 @@ async function runOpenCode(params) {
|
|
|
148552
148831
|
eventCount++;
|
|
148553
148832
|
diagnostic.eventCount = eventCount;
|
|
148554
148833
|
log.debug(JSON.stringify(event, null, 2));
|
|
148555
|
-
const
|
|
148556
|
-
if (
|
|
148834
|
+
const idleMs = performance7.now() - lastEventAt;
|
|
148835
|
+
if (idleMs > 1e4) {
|
|
148557
148836
|
const activeToolCalls = toolCallTimings.size;
|
|
148558
148837
|
const toolCallInfo = activeToolCalls > 0 ? ` (waiting for ${activeToolCalls} tool call${activeToolCalls > 1 ? "s" : ""})` : ` (${params.label} may be processing internally - LLM calls, planning, etc.)`;
|
|
148559
148838
|
log.info(
|
|
148560
|
-
`\xBB no activity for ${(
|
|
148839
|
+
`\xBB no activity for ${(idleMs / 1e3).toFixed(1)}s${toolCallInfo} (${eventCount} events processed so far)`
|
|
148561
148840
|
);
|
|
148562
148841
|
}
|
|
148563
|
-
|
|
148842
|
+
lastEventAt = performance7.now();
|
|
148564
148843
|
const handler2 = handlers2[event.type];
|
|
148565
148844
|
if (!handler2) {
|
|
148566
148845
|
log.info(
|
|
@@ -148597,14 +148876,13 @@ async function runOpenCode(params) {
|
|
|
148597
148876
|
} else {
|
|
148598
148877
|
params.todoTracker?.cancel();
|
|
148599
148878
|
}
|
|
148600
|
-
if (
|
|
148601
|
-
for (const dispatch of
|
|
148879
|
+
if (taskDispatchByCallID.size > 0) {
|
|
148880
|
+
for (const dispatch of taskDispatchByCallID.values()) {
|
|
148602
148881
|
const elapsed = performance7.now() - dispatch.startedAt;
|
|
148603
148882
|
log.info(
|
|
148604
|
-
`\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`
|
|
148605
148884
|
);
|
|
148606
148885
|
}
|
|
148607
|
-
pendingTaskDispatches.length = 0;
|
|
148608
148886
|
taskDispatchByCallID.clear();
|
|
148609
148887
|
}
|
|
148610
148888
|
const duration4 = performance7.now() - startTime;
|
|
@@ -148687,22 +148965,22 @@ ${stderrContext}`
|
|
|
148687
148965
|
}
|
|
148688
148966
|
var opencode = agent({
|
|
148689
148967
|
name: "opencode",
|
|
148690
|
-
install:
|
|
148968
|
+
install: installCli,
|
|
148691
148969
|
run: async (ctx) => {
|
|
148692
|
-
const cliPath = await
|
|
148970
|
+
const cliPath = await installCli();
|
|
148693
148971
|
const rawModel = ctx.payload.proxyModel ?? ctx.resolvedModel ?? autoSelectModel(cliPath);
|
|
148694
148972
|
const bedrockModelId = process.env[BEDROCK_MODEL_ID_ENV]?.trim();
|
|
148695
148973
|
const isBedrockRoute = rawModel !== void 0 && bedrockModelId !== void 0 && bedrockModelId === rawModel;
|
|
148696
148974
|
const model = isBedrockRoute ? `amazon-bedrock/${rawModel}` : rawModel;
|
|
148697
148975
|
const homeEnv = {
|
|
148698
148976
|
HOME: ctx.tmpdir,
|
|
148699
|
-
XDG_CONFIG_HOME:
|
|
148977
|
+
XDG_CONFIG_HOME: join12(ctx.tmpdir, ".config")
|
|
148700
148978
|
};
|
|
148701
|
-
|
|
148702
|
-
const opencodePluginDir =
|
|
148703
|
-
|
|
148704
|
-
|
|
148705
|
-
|
|
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),
|
|
148706
148984
|
PULLFROG_OPENCODE_PLUGIN_SOURCE
|
|
148707
148985
|
);
|
|
148708
148986
|
const agentBrowserVersion = getDevDependencyVersion("agent-browser");
|
|
@@ -148713,18 +148991,32 @@ var opencode = agent({
|
|
|
148713
148991
|
agent: "opencode"
|
|
148714
148992
|
});
|
|
148715
148993
|
installBundledSkills({ home: homeEnv.HOME });
|
|
148716
|
-
const
|
|
148994
|
+
const codexAuth = installCodexAuth();
|
|
148995
|
+
const baseArgs = ["run", "--format", "json", "--print-logs", "--thinking"];
|
|
148717
148996
|
const permissionOverride = JSON.stringify({
|
|
148718
148997
|
external_directory: { "*": "deny", "/tmp/*": "allow" }
|
|
148719
148998
|
});
|
|
148999
|
+
const repoDir = process.cwd();
|
|
148720
149000
|
const env2 = {
|
|
148721
149001
|
...process.env,
|
|
148722
149002
|
...homeEnv,
|
|
149003
|
+
PWD: repoDir,
|
|
148723
149004
|
OPENCODE_CONFIG_CONTENT: buildSecurityConfig(ctx, model),
|
|
148724
149005
|
OPENCODE_PERMISSION: permissionOverride,
|
|
148725
149006
|
GOOGLE_GENERATIVE_AI_API_KEY: process.env.GOOGLE_GENERATIVE_AI_API_KEY || process.env.GEMINI_API_KEY
|
|
148726
149007
|
};
|
|
148727
|
-
|
|
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
|
+
}
|
|
148728
149020
|
log.debug(`\xBB starting Pullfrog (OpenCode): ${cliPath} ${baseArgs.join(" ")}`);
|
|
148729
149021
|
log.debug(`\xBB working directory: ${repoDir}`);
|
|
148730
149022
|
const runParams = {
|
|
@@ -148745,7 +149037,7 @@ var opencode = agent({
|
|
|
148745
149037
|
ctx,
|
|
148746
149038
|
initialResult: result,
|
|
148747
149039
|
initialUsage: result.usage,
|
|
148748
|
-
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,
|
|
148749
149041
|
resume: async (c) => runOpenCode({
|
|
148750
149042
|
...runParams,
|
|
148751
149043
|
args: [...baseArgs, "--continue", c.prompt]
|
|
@@ -148819,7 +149111,9 @@ function resolveAgent(ctx) {
|
|
|
148819
149111
|
}
|
|
148820
149112
|
|
|
148821
149113
|
// utils/apiKeys.ts
|
|
148822
|
-
var knownApiKeys = new Set(
|
|
149114
|
+
var knownApiKeys = new Set(
|
|
149115
|
+
Object.values(providers).flatMap((p) => [...p.envVars, ...p.managedCredentials ?? []])
|
|
149116
|
+
);
|
|
148823
149117
|
var MISSING_KEY_MARKER = "no API key found";
|
|
148824
149118
|
function buildMissingApiKeyError(params) {
|
|
148825
149119
|
const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
|
|
@@ -148848,6 +149142,11 @@ function hasEnvVar2(name) {
|
|
|
148848
149142
|
const value2 = process.env[name];
|
|
148849
149143
|
return typeof value2 === "string" && value2.length > 0;
|
|
148850
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
|
+
}
|
|
148851
149150
|
function validateBedrockSetup(params) {
|
|
148852
149151
|
const hasAuth = hasEnvVar2("AWS_BEARER_TOKEN_BEDROCK") || hasEnvVar2("AWS_ACCESS_KEY_ID") && hasEnvVar2("AWS_SECRET_ACCESS_KEY");
|
|
148853
149152
|
const missing = [];
|
|
@@ -148882,7 +149181,7 @@ function validateAgentApiKey(params) {
|
|
|
148882
149181
|
}
|
|
148883
149182
|
function isApiKeyAuthError(text) {
|
|
148884
149183
|
if (!text) return false;
|
|
148885
|
-
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);
|
|
148886
149185
|
}
|
|
148887
149186
|
function formatApiKeyErrorSummary(params) {
|
|
148888
149187
|
if (params.raw.includes(MISSING_KEY_MARKER)) {
|
|
@@ -148994,11 +149293,137 @@ async function fetchBodyHtml(ctx) {
|
|
|
148994
149293
|
}
|
|
148995
149294
|
}
|
|
148996
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
|
+
|
|
148997
149422
|
// utils/github.ts
|
|
148998
|
-
var
|
|
149423
|
+
var core3 = __toESM(require_core(), 1);
|
|
148999
149424
|
import { createSign } from "node:crypto";
|
|
149000
149425
|
import { rename, writeFile } from "node:fs/promises";
|
|
149001
|
-
import { dirname as dirname3, join as
|
|
149426
|
+
import { dirname as dirname3, join as join14 } from "node:path";
|
|
149002
149427
|
|
|
149003
149428
|
// node_modules/.pnpm/@octokit+plugin-throttling@11.0.3_@octokit+core@7.0.5/node_modules/@octokit/plugin-throttling/dist-bundle/index.js
|
|
149004
149429
|
var import_light = __toESM(require_light(), 1);
|
|
@@ -152657,7 +153082,7 @@ var TokenExchangeError = class extends Error {
|
|
|
152657
153082
|
}
|
|
152658
153083
|
};
|
|
152659
153084
|
async function acquireTokenViaOIDC(opts) {
|
|
152660
|
-
const oidcToken = await
|
|
153085
|
+
const oidcToken = await core3.getIDToken("pullfrog-api");
|
|
152661
153086
|
const repos = [...opts?.repos ?? []];
|
|
152662
153087
|
const targetRepo = process.env.GITHUB_REPOSITORY?.split("/")[1];
|
|
152663
153088
|
if (targetRepo) {
|
|
@@ -152815,9 +153240,13 @@ async function acquireNewToken(opts) {
|
|
|
152815
153240
|
return error49 instanceof Error && (error49.message.includes("timed out") || error49.message.includes("fetch failed") || error49.message.includes("ECONNRESET") || error49.message.includes("ETIMEDOUT"));
|
|
152816
153241
|
}
|
|
152817
153242
|
});
|
|
152818
|
-
} else {
|
|
152819
|
-
return await acquireTokenViaGitHubApp(opts);
|
|
152820
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);
|
|
152821
153250
|
}
|
|
152822
153251
|
function parseRepoContext() {
|
|
152823
153252
|
const githubRepo = process.env.GITHUB_REPOSITORY;
|
|
@@ -152852,7 +153281,7 @@ function getGitHubUsageSummary() {
|
|
|
152852
153281
|
}
|
|
152853
153282
|
async function writeGitHubUsageSummaryToFile(path3) {
|
|
152854
153283
|
const summary2 = getGitHubUsageSummary();
|
|
152855
|
-
const tmpPath =
|
|
153284
|
+
const tmpPath = join14(dirname3(path3), `.usage-summary-${process.pid}.tmp`);
|
|
152856
153285
|
await writeFile(tmpPath, JSON.stringify(summary2));
|
|
152857
153286
|
await rename(tmpPath, path3);
|
|
152858
153287
|
}
|
|
@@ -152902,253 +153331,6 @@ function createOctokit(token) {
|
|
|
152902
153331
|
return octokit;
|
|
152903
153332
|
}
|
|
152904
153333
|
|
|
152905
|
-
// utils/token.ts
|
|
152906
|
-
var core3 = __toESM(require_core(), 1);
|
|
152907
|
-
import assert2 from "node:assert/strict";
|
|
152908
|
-
var mcpTokenValue;
|
|
152909
|
-
function getJobToken() {
|
|
152910
|
-
const inputToken = core3.getInput("token");
|
|
152911
|
-
if (inputToken) {
|
|
152912
|
-
return inputToken;
|
|
152913
|
-
}
|
|
152914
|
-
const fallbackToken = process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
|
|
152915
|
-
if (fallbackToken) {
|
|
152916
|
-
return fallbackToken;
|
|
152917
|
-
}
|
|
152918
|
-
throw new Error("token input is required");
|
|
152919
|
-
}
|
|
152920
|
-
async function resolveTokens(params) {
|
|
152921
|
-
assert2(!mcpTokenValue, "tokens are already resolved");
|
|
152922
|
-
const externalToken = process.env.GH_TOKEN;
|
|
152923
|
-
if (externalToken) {
|
|
152924
|
-
mcpTokenValue = externalToken;
|
|
152925
|
-
if (isGitHubActions) {
|
|
152926
|
-
core3.setSecret(externalToken);
|
|
152927
|
-
}
|
|
152928
|
-
log.info("\xBB using external GH_TOKEN for both git and MCP");
|
|
152929
|
-
return {
|
|
152930
|
-
gitToken: externalToken,
|
|
152931
|
-
mcpToken: externalToken,
|
|
152932
|
-
async [Symbol.asyncDispose]() {
|
|
152933
|
-
mcpTokenValue = void 0;
|
|
152934
|
-
}
|
|
152935
|
-
};
|
|
152936
|
-
}
|
|
152937
|
-
const gitPermissions = params.push === "disabled" ? { contents: "read" } : { contents: "write", workflows: "write" };
|
|
152938
|
-
const gitToken = await acquireNewToken({ permissions: gitPermissions });
|
|
152939
|
-
if (isGitHubActions) {
|
|
152940
|
-
core3.setSecret(gitToken);
|
|
152941
|
-
}
|
|
152942
|
-
log.info(
|
|
152943
|
-
`\xBB acquired git token (${Object.entries(gitPermissions).map((e) => e.join(":")).join(", ")})`
|
|
152944
|
-
);
|
|
152945
|
-
const mcpPermissions = {
|
|
152946
|
-
contents: "write",
|
|
152947
|
-
pull_requests: "write",
|
|
152948
|
-
issues: "write",
|
|
152949
|
-
checks: "read",
|
|
152950
|
-
actions: "read"
|
|
152951
|
-
};
|
|
152952
|
-
const mcpToken = await acquireNewToken({ permissions: mcpPermissions });
|
|
152953
|
-
if (isGitHubActions) {
|
|
152954
|
-
core3.setSecret(mcpToken);
|
|
152955
|
-
}
|
|
152956
|
-
log.info(
|
|
152957
|
-
`\xBB acquired scoped MCP token (${Object.entries(mcpPermissions).map((e) => e.join(":")).join(", ")})`
|
|
152958
|
-
);
|
|
152959
|
-
mcpTokenValue = mcpToken;
|
|
152960
|
-
let disposingRef;
|
|
152961
|
-
const dispose = async () => {
|
|
152962
|
-
if (disposingRef) {
|
|
152963
|
-
return disposingRef.promise;
|
|
152964
|
-
}
|
|
152965
|
-
disposingRef = Promise.withResolvers();
|
|
152966
|
-
try {
|
|
152967
|
-
mcpTokenValue = void 0;
|
|
152968
|
-
await Promise.all([
|
|
152969
|
-
revokeGitHubInstallationToken(gitToken),
|
|
152970
|
-
revokeGitHubInstallationToken(mcpToken)
|
|
152971
|
-
]);
|
|
152972
|
-
} finally {
|
|
152973
|
-
removeSignalHandler();
|
|
152974
|
-
disposingRef.resolve();
|
|
152975
|
-
disposingRef = void 0;
|
|
152976
|
-
}
|
|
152977
|
-
};
|
|
152978
|
-
const removeSignalHandler = onExitSignal(dispose);
|
|
152979
|
-
return {
|
|
152980
|
-
gitToken,
|
|
152981
|
-
mcpToken,
|
|
152982
|
-
[Symbol.asyncDispose]: dispose
|
|
152983
|
-
};
|
|
152984
|
-
}
|
|
152985
|
-
function getGitHubInstallationToken() {
|
|
152986
|
-
assert2(mcpTokenValue, "tokens not set. call resolveTokens first.");
|
|
152987
|
-
return mcpTokenValue;
|
|
152988
|
-
}
|
|
152989
|
-
async function revokeGitHubInstallationToken(token) {
|
|
152990
|
-
const apiUrl = process.env.GITHUB_API_URL || "https://api.github.com";
|
|
152991
|
-
try {
|
|
152992
|
-
await fetch(`${apiUrl}/installation/token`, {
|
|
152993
|
-
method: "DELETE",
|
|
152994
|
-
headers: {
|
|
152995
|
-
Accept: "application/vnd.github+json",
|
|
152996
|
-
Authorization: `Bearer ${token}`,
|
|
152997
|
-
"X-GitHub-Api-Version": "2022-11-28"
|
|
152998
|
-
}
|
|
152999
|
-
});
|
|
153000
|
-
log.debug("\xBB installation token revoked");
|
|
153001
|
-
} catch (error49) {
|
|
153002
|
-
log.info(
|
|
153003
|
-
`Failed to revoke installation token: ${error49 instanceof Error ? error49.message : String(error49)}`
|
|
153004
|
-
);
|
|
153005
|
-
}
|
|
153006
|
-
}
|
|
153007
|
-
|
|
153008
|
-
// utils/errorReport.ts
|
|
153009
|
-
async function reportErrorToComment(ctx) {
|
|
153010
|
-
const formattedError = ctx.title ? `${ctx.title}
|
|
153011
|
-
|
|
153012
|
-
${ctx.error}` : ctx.error;
|
|
153013
|
-
const comment = ctx.toolState.progressComment;
|
|
153014
|
-
if (!comment) {
|
|
153015
|
-
return;
|
|
153016
|
-
}
|
|
153017
|
-
const repoContext = parseRepoContext();
|
|
153018
|
-
const octokit = createOctokit(getGitHubInstallationToken());
|
|
153019
|
-
const runId = process.env.GITHUB_RUN_ID ? Number.parseInt(process.env.GITHUB_RUN_ID, 10) : void 0;
|
|
153020
|
-
const customParts = [];
|
|
153021
|
-
if (runId) {
|
|
153022
|
-
const apiUrl = getApiUrl();
|
|
153023
|
-
customParts.push(
|
|
153024
|
-
`[Rerun failed job \u2794](${apiUrl}/trigger/${repoContext.owner}/${repoContext.name}/${runId}?action=rerun)`
|
|
153025
|
-
);
|
|
153026
|
-
}
|
|
153027
|
-
const footer = buildPullfrogFooter({
|
|
153028
|
-
triggeredBy: true,
|
|
153029
|
-
workflowRun: runId ? { owner: repoContext.owner, repo: repoContext.name, runId } : void 0,
|
|
153030
|
-
customParts,
|
|
153031
|
-
model: ctx.toolState.model
|
|
153032
|
-
});
|
|
153033
|
-
await updateProgressComment(
|
|
153034
|
-
{ octokit, owner: repoContext.owner, repo: repoContext.name },
|
|
153035
|
-
comment,
|
|
153036
|
-
`${formattedError}${footer}`
|
|
153037
|
-
);
|
|
153038
|
-
ctx.toolState.wasUpdated = true;
|
|
153039
|
-
}
|
|
153040
|
-
|
|
153041
|
-
// utils/gitAuthServer.ts
|
|
153042
|
-
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
153043
|
-
import { writeFileSync as writeFileSync9 } from "node:fs";
|
|
153044
|
-
import { createServer as createServer2 } from "node:http";
|
|
153045
|
-
import { join as join13 } from "node:path";
|
|
153046
|
-
var CODE_TTL_MS = 5 * 60 * 1e3;
|
|
153047
|
-
var TAMPER_WINDOW_MS = 6e4;
|
|
153048
|
-
function revokeGitHubToken(token) {
|
|
153049
|
-
fetch("https://api.github.com/installation/token", {
|
|
153050
|
-
method: "DELETE",
|
|
153051
|
-
headers: {
|
|
153052
|
-
Authorization: `Bearer ${token}`,
|
|
153053
|
-
Accept: "application/vnd.github+json",
|
|
153054
|
-
"User-Agent": "pullfrog"
|
|
153055
|
-
}
|
|
153056
|
-
}).then(
|
|
153057
|
-
(r) => log.info(`token revocation response: ${r.status}`),
|
|
153058
|
-
() => log.warning("token revocation request failed")
|
|
153059
|
-
);
|
|
153060
|
-
}
|
|
153061
|
-
async function startGitAuthServer(tmpdir3) {
|
|
153062
|
-
const codes = /* @__PURE__ */ new Map();
|
|
153063
|
-
const server = createServer2((req, res) => {
|
|
153064
|
-
if (req.method !== "GET") {
|
|
153065
|
-
res.writeHead(405).end();
|
|
153066
|
-
return;
|
|
153067
|
-
}
|
|
153068
|
-
const code = req.url?.slice(1);
|
|
153069
|
-
if (!code) {
|
|
153070
|
-
res.writeHead(400).end();
|
|
153071
|
-
return;
|
|
153072
|
-
}
|
|
153073
|
-
const entry = codes.get(code);
|
|
153074
|
-
if (!entry) {
|
|
153075
|
-
res.writeHead(404).end();
|
|
153076
|
-
return;
|
|
153077
|
-
}
|
|
153078
|
-
if (entry.state === "pending") {
|
|
153079
|
-
entry.state = "consumed";
|
|
153080
|
-
clearTimeout(entry.timeout);
|
|
153081
|
-
entry.timeout = setTimeout(() => codes.delete(code), TAMPER_WINDOW_MS);
|
|
153082
|
-
entry.timeout.unref();
|
|
153083
|
-
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
153084
|
-
res.end(entry.token);
|
|
153085
|
-
return;
|
|
153086
|
-
}
|
|
153087
|
-
log.info("askpass code used twice \u2014 revoking token");
|
|
153088
|
-
revokeGitHubToken(entry.token);
|
|
153089
|
-
clearTimeout(entry.timeout);
|
|
153090
|
-
codes.delete(code);
|
|
153091
|
-
res.writeHead(409, { "Content-Type": "text/plain" });
|
|
153092
|
-
res.end("compromised");
|
|
153093
|
-
});
|
|
153094
|
-
await new Promise((resolve3, reject) => {
|
|
153095
|
-
server.on("error", reject);
|
|
153096
|
-
server.listen(0, "127.0.0.1", () => resolve3());
|
|
153097
|
-
});
|
|
153098
|
-
const rawAddr = server.address();
|
|
153099
|
-
if (!rawAddr || typeof rawAddr === "string") {
|
|
153100
|
-
throw new Error("git auth server failed to bind");
|
|
153101
|
-
}
|
|
153102
|
-
const port = rawAddr.port;
|
|
153103
|
-
log.debug(`git auth server listening on 127.0.0.1:${port}`);
|
|
153104
|
-
function register4(token) {
|
|
153105
|
-
const code = randomUUID3();
|
|
153106
|
-
const timeout = setTimeout(() => {
|
|
153107
|
-
codes.delete(code);
|
|
153108
|
-
log.debug(`git auth code expired: ${code.slice(0, 8)}...`);
|
|
153109
|
-
}, CODE_TTL_MS);
|
|
153110
|
-
timeout.unref();
|
|
153111
|
-
codes.set(code, { token, state: "pending", timeout });
|
|
153112
|
-
return code;
|
|
153113
|
-
}
|
|
153114
|
-
function writeAskpassScript(code) {
|
|
153115
|
-
const scriptId = randomUUID3();
|
|
153116
|
-
const scriptName = `askpass-${scriptId}.js`;
|
|
153117
|
-
const scriptPath = join13(tmpdir3, scriptName);
|
|
153118
|
-
const content = [
|
|
153119
|
-
`#!/usr/bin/env node`,
|
|
153120
|
-
`var a=process.argv[2]||"";`,
|
|
153121
|
-
`if(/^Username/i.test(a)){process.stdout.write("x-access-token\\n")}`,
|
|
153122
|
-
`else{var h=require("http");`,
|
|
153123
|
-
`h.get("http://127.0.0.1:${port}/${code}",function(r){`,
|
|
153124
|
-
`if(r.statusCode===409){process.stderr.write("askpass-compromised\\n");process.exit(1)}`,
|
|
153125
|
-
`if(r.statusCode!==200){process.exit(1)}`,
|
|
153126
|
-
`var d="";r.on("data",function(c){d+=c});`,
|
|
153127
|
-
`r.on("end",function(){`,
|
|
153128
|
-
`process.stdout.write(d+"\\n");`,
|
|
153129
|
-
`try{require("fs").unlinkSync("${scriptPath.replace(/\\/g, "\\\\")}")}catch(e){}`,
|
|
153130
|
-
`})}).on("error",function(){process.exit(1)})}`
|
|
153131
|
-
].join("\n");
|
|
153132
|
-
writeFileSync9(scriptPath, content, { mode: 448 });
|
|
153133
|
-
return scriptPath;
|
|
153134
|
-
}
|
|
153135
|
-
async function close() {
|
|
153136
|
-
for (const entry of codes.values()) {
|
|
153137
|
-
clearTimeout(entry.timeout);
|
|
153138
|
-
}
|
|
153139
|
-
codes.clear();
|
|
153140
|
-
await new Promise((resolve3) => server.close(() => resolve3()));
|
|
153141
|
-
log.debug("git auth server closed");
|
|
153142
|
-
}
|
|
153143
|
-
return {
|
|
153144
|
-
port,
|
|
153145
|
-
register: register4,
|
|
153146
|
-
writeAskpassScript,
|
|
153147
|
-
close,
|
|
153148
|
-
[Symbol.asyncDispose]: close
|
|
153149
|
-
};
|
|
153150
|
-
}
|
|
153151
|
-
|
|
153152
153334
|
// utils/instructions.ts
|
|
153153
153335
|
import { execSync as execSync2 } from "node:child_process";
|
|
153154
153336
|
function buildRuntimeContext(ctx) {
|
|
@@ -153341,7 +153523,7 @@ Rules:
|
|
|
153341
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\`).
|
|
153342
153524
|
- Never add co-author trailers (e.g., "Co-authored-by" or "Co-Authored-By") to commit messages.
|
|
153343
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.
|
|
153344
|
-
- \`${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.
|
|
153345
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.
|
|
153346
153528
|
|
|
153347
153529
|
### GitHub
|
|
@@ -153369,11 +153551,9 @@ For maximum efficiency, whenever you need to perform multiple independent operat
|
|
|
153369
153551
|
- listing multiple directories
|
|
153370
153552
|
- inspecting multiple MCP tools or resources
|
|
153371
153553
|
|
|
153372
|
-
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.
|
|
153373
153555
|
|
|
153374
|
-
|
|
153375
|
-
|
|
153376
|
-
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.
|
|
153377
153557
|
|
|
153378
153558
|
### Command execution
|
|
153379
153559
|
|
|
@@ -153391,7 +153571,7 @@ When embedding images (e.g. uploaded screenshots) in comments or PR bodies, alwa
|
|
|
153391
153571
|
|
|
153392
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.
|
|
153393
153573
|
|
|
153394
|
-
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.
|
|
153395
153575
|
|
|
153396
153576
|
### If you get stuck
|
|
153397
153577
|
|
|
@@ -153430,8 +153610,8 @@ function renderLearningsToc(headings) {
|
|
|
153430
153610
|
}
|
|
153431
153611
|
function buildLearningsSection(ctx) {
|
|
153432
153612
|
if (!ctx.filePath) return "";
|
|
153433
|
-
const intro = `
|
|
153434
|
-
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.
|
|
153435
153615
|
|
|
153436
153616
|
${renderLearningsToc(ctx.headings)}`;
|
|
153437
153617
|
return `************* LEARNINGS *************
|
|
@@ -153501,18 +153681,10 @@ function resolveInstructions(ctx) {
|
|
|
153501
153681
|
|
|
153502
153682
|
// utils/learnings.ts
|
|
153503
153683
|
import { mkdir, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
153504
|
-
import { dirname as dirname4, join as
|
|
153505
|
-
|
|
153684
|
+
import { dirname as dirname4, join as join15 } from "node:path";
|
|
153685
|
+
|
|
153686
|
+
// utils/learningsTruncate.ts
|
|
153506
153687
|
var MAX_LEARNINGS_LENGTH = 1e5;
|
|
153507
|
-
function learningsFilePath(tmpdir3) {
|
|
153508
|
-
return join14(tmpdir3, LEARNINGS_FILE_NAME);
|
|
153509
|
-
}
|
|
153510
|
-
async function seedLearningsFile(params) {
|
|
153511
|
-
const path3 = learningsFilePath(params.tmpdir);
|
|
153512
|
-
await mkdir(dirname4(path3), { recursive: true });
|
|
153513
|
-
await writeFile2(path3, params.current ?? "", "utf8");
|
|
153514
|
-
return path3;
|
|
153515
|
-
}
|
|
153516
153688
|
var TRUNCATION_LINE_BOUNDARY_TOLERANCE = 4096;
|
|
153517
153689
|
function truncateAtLineBoundary(body, cap) {
|
|
153518
153690
|
if (body.length <= cap) return body;
|
|
@@ -153522,6 +153694,18 @@ function truncateAtLineBoundary(body, cap) {
|
|
|
153522
153694
|
if (cap - lastNewline > TRUNCATION_LINE_BOUNDARY_TOLERANCE) return head;
|
|
153523
153695
|
return head.slice(0, lastNewline);
|
|
153524
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
|
+
}
|
|
153525
153709
|
async function readLearningsFile(path3) {
|
|
153526
153710
|
let raw2;
|
|
153527
153711
|
try {
|
|
@@ -153531,6 +153715,45 @@ async function readLearningsFile(path3) {
|
|
|
153531
153715
|
}
|
|
153532
153716
|
return truncateAtLineBoundary(raw2.trim(), MAX_LEARNINGS_LENGTH);
|
|
153533
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
|
+
}
|
|
153534
153757
|
|
|
153535
153758
|
// utils/normalizeEnv.ts
|
|
153536
153759
|
var core4 = __toESM(require_core(), 1);
|
|
@@ -153590,8 +153813,63 @@ function normalizeEnv() {
|
|
|
153590
153813
|
}
|
|
153591
153814
|
}
|
|
153592
153815
|
|
|
153593
|
-
// utils/
|
|
153816
|
+
// utils/overrides.ts
|
|
153594
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);
|
|
153595
153873
|
import { isAbsolute as isAbsolute2, resolve as resolve2 } from "node:path";
|
|
153596
153874
|
|
|
153597
153875
|
// utils/versioning.ts
|
|
@@ -153655,7 +153933,7 @@ function resolveCwd(cwd) {
|
|
|
153655
153933
|
return workspace ? resolve2(workspace, cwd) : cwd;
|
|
153656
153934
|
}
|
|
153657
153935
|
function resolvePromptInput() {
|
|
153658
|
-
const prompt =
|
|
153936
|
+
const prompt = core6.getInput("prompt", { required: true });
|
|
153659
153937
|
let parsed2;
|
|
153660
153938
|
try {
|
|
153661
153939
|
parsed2 = JSON.parse(prompt);
|
|
@@ -153671,11 +153949,11 @@ function resolvePromptInput() {
|
|
|
153671
153949
|
}
|
|
153672
153950
|
function resolveNonPromptInputs() {
|
|
153673
153951
|
return Inputs.omit("prompt").assert({
|
|
153674
|
-
model:
|
|
153675
|
-
timeout:
|
|
153676
|
-
cwd:
|
|
153677
|
-
push:
|
|
153678
|
-
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
|
|
153679
153957
|
});
|
|
153680
153958
|
}
|
|
153681
153959
|
var isPullfrog = (actor) => {
|
|
@@ -153721,10 +153999,386 @@ function resolvePayload(resolvedPromptInput, repoSettings) {
|
|
|
153721
153999
|
proxyModel: void 0
|
|
153722
154000
|
};
|
|
153723
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
|
+
}
|
|
153724
154378
|
|
|
153725
154379
|
// utils/prSummary.ts
|
|
153726
154380
|
import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
|
|
153727
|
-
import { dirname as dirname5, join as
|
|
154381
|
+
import { dirname as dirname5, join as join16 } from "node:path";
|
|
153728
154382
|
var SUMMARY_FILE_NAME = "pullfrog-summary.md";
|
|
153729
154383
|
var SUMMARY_SCAFFOLD = `# PR summary
|
|
153730
154384
|
|
|
@@ -153734,7 +154388,7 @@ var SUMMARY_SCAFFOLD = `# PR summary
|
|
|
153734
154388
|
var MIN_SNAPSHOT_LENGTH = 60;
|
|
153735
154389
|
var MAX_SNAPSHOT_LENGTH = 32768;
|
|
153736
154390
|
function summaryFilePath(tmpdir3) {
|
|
153737
|
-
return
|
|
154391
|
+
return join16(tmpdir3, SUMMARY_FILE_NAME);
|
|
153738
154392
|
}
|
|
153739
154393
|
async function seedSummaryFile(params) {
|
|
153740
154394
|
const path3 = summaryFilePath(params.tmpdir);
|
|
@@ -153755,76 +154409,43 @@ async function readSummaryFile(path3) {
|
|
|
153755
154409
|
if (trimmed.length > MAX_SNAPSHOT_LENGTH) return trimmed.slice(0, MAX_SNAPSHOT_LENGTH);
|
|
153756
154410
|
return trimmed;
|
|
153757
154411
|
}
|
|
153758
|
-
|
|
153759
|
-
|
|
153760
|
-
var RE_REVIEW_PREAMBLE = "Incrementally re-review the new commits on this pull request. Use the IncrementalReview mode.";
|
|
153761
|
-
async function postReviewCleanup(ctx) {
|
|
153762
|
-
const review = ctx.toolState.review;
|
|
153763
|
-
if (!review) return;
|
|
153764
|
-
delete ctx.toolState.review;
|
|
153765
|
-
await bestEffort(() => reportReviewNodeId(ctx, { nodeId: review.nodeId }), "reportReviewNodeId");
|
|
153766
|
-
if (review.reviewedSha) {
|
|
153767
|
-
await bestEffort(
|
|
153768
|
-
() => dispatchFollowUpReReview(ctx, review.reviewedSha),
|
|
153769
|
-
"follow-up re-review dispatch"
|
|
153770
|
-
);
|
|
153771
|
-
}
|
|
153772
|
-
}
|
|
153773
|
-
async function bestEffort(fn2, label) {
|
|
154412
|
+
async function fetchPreviousSnapshot(ctx, prNumber) {
|
|
154413
|
+
if (!ctx.githubInstallationToken) return null;
|
|
153774
154414
|
try {
|
|
153775
|
-
await
|
|
153776
|
-
|
|
153777
|
-
|
|
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;
|
|
153778
154426
|
}
|
|
153779
154427
|
}
|
|
153780
|
-
async function
|
|
153781
|
-
const
|
|
153782
|
-
if (!
|
|
153783
|
-
|
|
153784
|
-
|
|
153785
|
-
|
|
153786
|
-
|
|
153787
|
-
|
|
153788
|
-
|
|
153789
|
-
if (pr.data.state !== "open") return;
|
|
153790
|
-
if (pr.data.draft) return;
|
|
153791
|
-
log.info(
|
|
153792
|
-
`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`
|
|
153793
|
-
);
|
|
153794
|
-
const event = {
|
|
153795
|
-
trigger: "pull_request_synchronize",
|
|
153796
|
-
issue_number: issueNumber,
|
|
153797
|
-
is_pr: true,
|
|
153798
|
-
title: pr.data.title,
|
|
153799
|
-
body: null,
|
|
153800
|
-
branch: pr.data.head.ref,
|
|
153801
|
-
before_sha: reviewedSha,
|
|
153802
|
-
silent: true
|
|
153803
|
-
};
|
|
153804
|
-
if (ctx.payload.event.authorPermission) {
|
|
153805
|
-
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;
|
|
153806
154437
|
}
|
|
153807
|
-
const
|
|
153808
|
-
|
|
153809
|
-
|
|
153810
|
-
|
|
153811
|
-
|
|
153812
|
-
|
|
153813
|
-
|
|
153814
|
-
}
|
|
153815
|
-
|
|
153816
|
-
owner: ctx.repo.owner,
|
|
153817
|
-
repo: ctx.repo.name,
|
|
153818
|
-
workflow_id: getCurrentWorkflowFilename(),
|
|
153819
|
-
ref: pr.data.base.repo.default_branch,
|
|
153820
|
-
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)}`);
|
|
153821
154447
|
});
|
|
153822
154448
|
}
|
|
153823
|
-
function getCurrentWorkflowFilename() {
|
|
153824
|
-
const ref = process.env.GITHUB_WORKFLOW_REF ?? "";
|
|
153825
|
-
const match3 = ref.match(/\/([^/]+)@/);
|
|
153826
|
-
return match3?.[1] ?? "pullfrog.yml";
|
|
153827
|
-
}
|
|
153828
154449
|
|
|
153829
154450
|
// utils/run.ts
|
|
153830
154451
|
async function handleAgentResult(ctx) {
|
|
@@ -153860,10 +154481,10 @@ async function handleAgentResult(ctx) {
|
|
|
153860
154481
|
};
|
|
153861
154482
|
}
|
|
153862
154483
|
|
|
154484
|
+
// utils/runContextData.ts
|
|
154485
|
+
var core9 = __toESM(require_core(), 1);
|
|
154486
|
+
|
|
153863
154487
|
// utils/runContext.ts
|
|
153864
|
-
function isInfraCovered(params) {
|
|
153865
|
-
return params.isOss || params.plan === "payg";
|
|
153866
|
-
}
|
|
153867
154488
|
var defaultSettings = {
|
|
153868
154489
|
model: null,
|
|
153869
154490
|
modes: [],
|
|
@@ -153933,13 +154554,12 @@ async function fetchRunContext(params) {
|
|
|
153933
154554
|
}
|
|
153934
154555
|
|
|
153935
154556
|
// utils/runContextData.ts
|
|
153936
|
-
var core6 = __toESM(require_core(), 1);
|
|
153937
154557
|
async function resolveRunContextData(params) {
|
|
153938
154558
|
log.info(`\xBB running Pullfrog v${package_default.version}...`);
|
|
153939
154559
|
const repoContext = parseRepoContext();
|
|
153940
154560
|
let oidcToken;
|
|
153941
154561
|
try {
|
|
153942
|
-
oidcToken = await
|
|
154562
|
+
oidcToken = await core9.getIDToken("pullfrog-api");
|
|
153943
154563
|
} catch {
|
|
153944
154564
|
}
|
|
153945
154565
|
const [repoResponse, runContext] = await Promise.all([
|
|
@@ -153961,13 +154581,240 @@ async function resolveRunContextData(params) {
|
|
|
153961
154581
|
};
|
|
153962
154582
|
}
|
|
153963
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
|
+
|
|
153964
154811
|
// utils/setup.ts
|
|
153965
154812
|
import { execFileSync as execFileSync5, execSync as execSync3 } from "node:child_process";
|
|
153966
154813
|
import { mkdtempSync } from "node:fs";
|
|
153967
154814
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
153968
|
-
import { join as
|
|
154815
|
+
import { join as join17 } from "node:path";
|
|
153969
154816
|
function createTempDirectory() {
|
|
153970
|
-
const sharedTempDir = mkdtempSync(
|
|
154817
|
+
const sharedTempDir = mkdtempSync(join17(tmpdir2(), "pullfrog-"));
|
|
153971
154818
|
process.env.PULLFROG_TEMP_DIR = sharedTempDir;
|
|
153972
154819
|
log.info(`\xBB created temp dir at ${sharedTempDir}`);
|
|
153973
154820
|
return sharedTempDir;
|
|
@@ -154071,25 +154918,6 @@ async function setupGit(params) {
|
|
|
154071
154918
|
log.info("\xBB git authentication configured");
|
|
154072
154919
|
}
|
|
154073
154920
|
|
|
154074
|
-
// utils/time.ts
|
|
154075
|
-
var TIMEOUT_DISABLED = "none";
|
|
154076
|
-
var TIME_STRING_REGEX = /^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/;
|
|
154077
|
-
function parseTimeString(input) {
|
|
154078
|
-
const match3 = input.match(TIME_STRING_REGEX);
|
|
154079
|
-
if (!match3 || !match3[1] && !match3[2] && !match3[3]) return null;
|
|
154080
|
-
const hours = parseInt(match3[1] || "0", 10);
|
|
154081
|
-
const minutes = parseInt(match3[2] || "0", 10);
|
|
154082
|
-
const seconds = parseInt(match3[3] || "0", 10);
|
|
154083
|
-
return (hours * 3600 + minutes * 60 + seconds) * 1e3;
|
|
154084
|
-
}
|
|
154085
|
-
var TIMEOUT_MAX_MS = 2147483647;
|
|
154086
|
-
function resolveTimeoutMs(input) {
|
|
154087
|
-
if (!input) return null;
|
|
154088
|
-
const parsed2 = parseTimeString(input);
|
|
154089
|
-
if (parsed2 === null || parsed2 <= 0 || parsed2 > TIMEOUT_MAX_MS) return null;
|
|
154090
|
-
return parsed2;
|
|
154091
|
-
}
|
|
154092
|
-
|
|
154093
154921
|
// utils/todoTracking.ts
|
|
154094
154922
|
function isValidTodoStatus(value2) {
|
|
154095
154923
|
return value2 === "pending" || value2 === "in_progress" || value2 === "completed" || value2 === "cancelled";
|
|
@@ -154226,305 +155054,42 @@ async function resolveRun(params) {
|
|
|
154226
155054
|
let jobId;
|
|
154227
155055
|
const jobName = process.env.GITHUB_JOB;
|
|
154228
155056
|
if (jobName && runId) {
|
|
154229
|
-
|
|
154230
|
-
|
|
154231
|
-
|
|
154232
|
-
|
|
154233
|
-
|
|
154234
|
-
|
|
154235
|
-
|
|
154236
|
-
|
|
154237
|
-
|
|
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}`);
|
|
154238
155071
|
}
|
|
154239
155072
|
}
|
|
154240
155073
|
return { runId, jobId };
|
|
154241
155074
|
}
|
|
154242
155075
|
|
|
154243
155076
|
// main.ts
|
|
154244
|
-
function resolveOutputSchema() {
|
|
154245
|
-
const raw2 = core7.getInput("output_schema");
|
|
154246
|
-
if (!raw2) return void 0;
|
|
154247
|
-
let parsed2;
|
|
154248
|
-
try {
|
|
154249
|
-
parsed2 = JSON.parse(raw2);
|
|
154250
|
-
} catch {
|
|
154251
|
-
throw new Error(`invalid output_schema: not valid JSON`);
|
|
154252
|
-
}
|
|
154253
|
-
if (!parsed2 || typeof parsed2 !== "object" || Array.isArray(parsed2)) {
|
|
154254
|
-
throw new Error(`invalid output_schema: must be a JSON object`);
|
|
154255
|
-
}
|
|
154256
|
-
log.info("\xBB structured output schema provided \u2014 output will be required");
|
|
154257
|
-
return parsed2;
|
|
154258
|
-
}
|
|
154259
|
-
function resolveTimeoutForLog(timeout) {
|
|
154260
|
-
if (!timeout) return "1h (default)";
|
|
154261
|
-
if (timeout === TIMEOUT_DISABLED) return "none (disabled)";
|
|
154262
|
-
return timeout;
|
|
154263
|
-
}
|
|
154264
|
-
function resolveModelForLog(ctx) {
|
|
154265
|
-
const envModel = process.env.PULLFROG_MODEL?.trim();
|
|
154266
|
-
if (envModel) return `${envModel} (override via PULLFROG_MODEL)`;
|
|
154267
|
-
if (ctx.payload.proxyModel) return `${ctx.payload.proxyModel} (proxy)`;
|
|
154268
|
-
if (ctx.resolvedModel && ctx.payload.model && ctx.payload.model !== ctx.resolvedModel) {
|
|
154269
|
-
return `${ctx.resolvedModel} (resolved from ${ctx.payload.model})`;
|
|
154270
|
-
}
|
|
154271
|
-
if (ctx.resolvedModel) return ctx.resolvedModel;
|
|
154272
|
-
if (ctx.payload.model) return `${ctx.payload.model} (unresolved)`;
|
|
154273
|
-
return "auto";
|
|
154274
|
-
}
|
|
154275
|
-
function resolveAgentForLog(ctx) {
|
|
154276
|
-
const envAgent = process.env.PULLFROG_AGENT?.trim();
|
|
154277
|
-
if (envAgent && envAgent === ctx.agentName) {
|
|
154278
|
-
return `${ctx.agentName} (override via PULLFROG_AGENT)`;
|
|
154279
|
-
}
|
|
154280
|
-
if (ctx.agentName === "claude" && ctx.resolvedModel) {
|
|
154281
|
-
return `${ctx.agentName} (auto-selected for ${ctx.resolvedModel})`;
|
|
154282
|
-
}
|
|
154283
|
-
return ctx.agentName;
|
|
154284
|
-
}
|
|
154285
|
-
var BillingError = class extends Error {
|
|
154286
|
-
code;
|
|
154287
|
-
declineCode;
|
|
154288
|
-
needsReauthentication;
|
|
154289
|
-
constructor(message, opts = {}) {
|
|
154290
|
-
super(message);
|
|
154291
|
-
this.name = "BillingError";
|
|
154292
|
-
this.code = opts.code ?? null;
|
|
154293
|
-
this.declineCode = opts.declineCode ?? null;
|
|
154294
|
-
this.needsReauthentication = opts.needsReauthentication ?? false;
|
|
154295
|
-
}
|
|
154296
|
-
};
|
|
154297
|
-
var TransientError = class extends Error {
|
|
154298
|
-
constructor(message) {
|
|
154299
|
-
super(message);
|
|
154300
|
-
this.name = "TransientError";
|
|
154301
|
-
}
|
|
154302
|
-
};
|
|
154303
|
-
function billingConsoleUrl(owner, anchor) {
|
|
154304
|
-
return `https://pullfrog.com/console/${encodeURIComponent(owner)}#${anchor}`;
|
|
154305
|
-
}
|
|
154306
|
-
function formatBillingErrorSummary(error49, owner) {
|
|
154307
|
-
if (error49.code === "router_requires_card") {
|
|
154308
|
-
return [
|
|
154309
|
-
"**Add a card to start using Pullfrog Router.**",
|
|
154310
|
-
"",
|
|
154311
|
-
"Router proxies OpenRouter at raw cost \u2014 no platform markup. Add a card and we'll auto-reload your wallet so runs keep flowing.",
|
|
154312
|
-
"",
|
|
154313
|
-
`[Add a card \u2192](${billingConsoleUrl(owner, "model-access")})`
|
|
154314
|
-
].join("\n");
|
|
154315
|
-
}
|
|
154316
|
-
if (error49.code === "router_balance_exhausted") {
|
|
154317
|
-
return [
|
|
154318
|
-
"**Your Pullfrog Router balance is exhausted.**",
|
|
154319
|
-
"",
|
|
154320
|
-
"You have a card on file but auto-reload is disabled, so runs paused once your balance went past the overdraft buffer.",
|
|
154321
|
-
"",
|
|
154322
|
-
`[Top up balance \u2192](${billingConsoleUrl(owner, "billing")}) \xB7 [Enable auto-reload \u2192](${billingConsoleUrl(owner, "model-access")})`
|
|
154323
|
-
].join("\n");
|
|
154324
|
-
}
|
|
154325
|
-
if (error49.code === "router_keylimit_exhausted") {
|
|
154326
|
-
return [
|
|
154327
|
-
"**This run was cut short \u2014 your Pullfrog Router balance ran out mid-run.**",
|
|
154328
|
-
"",
|
|
154329
|
-
"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.",
|
|
154330
|
-
"",
|
|
154331
|
-
`[Top up balance \u2192](${billingConsoleUrl(owner, "billing")}) \xB7 [Enable auto-reload \u2192](${billingConsoleUrl(owner, "model-access")})`
|
|
154332
|
-
].join("\n");
|
|
154333
|
-
}
|
|
154334
|
-
if (error49.needsReauthentication) {
|
|
154335
|
-
const code = error49.declineCode ?? "authentication_required";
|
|
154336
|
-
return [
|
|
154337
|
-
`**Your card issuer requires 3D Secure on every charge** (\`${code}\`).`,
|
|
154338
|
-
"",
|
|
154339
|
-
"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.",
|
|
154340
|
-
"",
|
|
154341
|
-
`[Top up balance \u2192](${billingConsoleUrl(owner, "billing")})`
|
|
154342
|
-
].join("\n");
|
|
154343
|
-
}
|
|
154344
|
-
if (error49.declineCode) {
|
|
154345
|
-
return [
|
|
154346
|
-
`**Your card was declined** (\`${error49.declineCode}\`).`,
|
|
154347
|
-
"",
|
|
154348
|
-
"Update your payment method and Pullfrog will retry on the next run.",
|
|
154349
|
-
"",
|
|
154350
|
-
`[Update payment method \u2192](${billingConsoleUrl(owner, "billing")})`
|
|
154351
|
-
].join("\n");
|
|
154352
|
-
}
|
|
154353
|
-
return [
|
|
154354
|
-
"**Your Pullfrog balance is empty.**",
|
|
154355
|
-
"",
|
|
154356
|
-
"Top up your balance or enable auto-reload to keep runs flowing.",
|
|
154357
|
-
"",
|
|
154358
|
-
`[Manage billing \u2192](${billingConsoleUrl(owner, "billing")})`
|
|
154359
|
-
].join("\n");
|
|
154360
|
-
}
|
|
154361
|
-
function formatTransientErrorSummary(error49, owner) {
|
|
154362
|
-
return [
|
|
154363
|
-
"**Pullfrog billing is temporarily unavailable.**",
|
|
154364
|
-
"",
|
|
154365
|
-
error49.message,
|
|
154366
|
-
"",
|
|
154367
|
-
`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")}).`
|
|
154368
|
-
].join("\n");
|
|
154369
|
-
}
|
|
154370
|
-
async function mintProxyKey(ctx) {
|
|
154371
|
-
try {
|
|
154372
|
-
const headers = await buildProxyTokenHeaders(ctx);
|
|
154373
|
-
if (!headers) return null;
|
|
154374
|
-
const response = await apiFetch({
|
|
154375
|
-
path: "/api/proxy-token",
|
|
154376
|
-
method: "POST",
|
|
154377
|
-
headers
|
|
154378
|
-
});
|
|
154379
|
-
if (response.status === 402) {
|
|
154380
|
-
const body = await response.json().catch(() => null);
|
|
154381
|
-
throw new BillingError(body?.error ?? "insufficient balance", {
|
|
154382
|
-
code: body?.code ?? null,
|
|
154383
|
-
declineCode: body?.declineCode ?? null,
|
|
154384
|
-
needsReauthentication: body?.needsReauthentication ?? false
|
|
154385
|
-
});
|
|
154386
|
-
}
|
|
154387
|
-
if (response.status === 503) {
|
|
154388
|
-
const body = await response.json().catch(() => null);
|
|
154389
|
-
throw new TransientError(
|
|
154390
|
-
body?.error ?? "billing service temporarily unavailable \u2014 retry shortly"
|
|
154391
|
-
);
|
|
154392
|
-
}
|
|
154393
|
-
if (!response.ok) {
|
|
154394
|
-
log.warning(`proxy key mint failed (${response.status})`);
|
|
154395
|
-
return null;
|
|
154396
|
-
}
|
|
154397
|
-
const data = await response.json();
|
|
154398
|
-
return data.key;
|
|
154399
|
-
} catch (error49) {
|
|
154400
|
-
if (error49 instanceof BillingError) throw error49;
|
|
154401
|
-
if (error49 instanceof TransientError) throw error49;
|
|
154402
|
-
log.warning(`proxy key mint error: ${error49 instanceof Error ? error49.message : String(error49)}`);
|
|
154403
|
-
return null;
|
|
154404
|
-
} finally {
|
|
154405
|
-
delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
154406
|
-
delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
154407
|
-
}
|
|
154408
|
-
}
|
|
154409
|
-
async function buildProxyTokenHeaders(ctx) {
|
|
154410
|
-
if (ctx.oidcCredentials) {
|
|
154411
|
-
process.env.ACTIONS_ID_TOKEN_REQUEST_URL = ctx.oidcCredentials.requestUrl;
|
|
154412
|
-
process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = ctx.oidcCredentials.requestToken;
|
|
154413
|
-
const oidcToken = await core7.getIDToken("pullfrog-api");
|
|
154414
|
-
delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
154415
|
-
delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
154416
|
-
return { Authorization: `Bearer ${oidcToken}` };
|
|
154417
|
-
}
|
|
154418
|
-
if (isLocalApiUrl()) {
|
|
154419
|
-
log.info(`\xBB proxy: dev bypass (x-dev-repo) for ${ctx.repo.owner}/${ctx.repo.name}`);
|
|
154420
|
-
return { "x-dev-repo": `${ctx.repo.owner}/${ctx.repo.name}` };
|
|
154421
|
-
}
|
|
154422
|
-
return null;
|
|
154423
|
-
}
|
|
154424
|
-
async function resolveProxyModel(ctx) {
|
|
154425
|
-
if (process.env.PULLFROG_MODEL?.trim()) return;
|
|
154426
|
-
const needsProxy = isInfraCovered({ isOss: ctx.oss, plan: ctx.plan }) && ctx.proxyModel;
|
|
154427
|
-
if (!needsProxy) return;
|
|
154428
|
-
if (!ctx.oidcCredentials && !isLocalApiUrl()) {
|
|
154429
|
-
log.warning("\xBB proxy requested but no OIDC credentials available \u2014 skipping");
|
|
154430
|
-
return;
|
|
154431
|
-
}
|
|
154432
|
-
const key = await mintProxyKey({ oidcCredentials: ctx.oidcCredentials, repo: ctx.repo });
|
|
154433
|
-
if (!key) return;
|
|
154434
|
-
process.env.OPENROUTER_API_KEY = key;
|
|
154435
|
-
core7.setSecret(key);
|
|
154436
|
-
ctx.payload.proxyModel = ctx.proxyModel;
|
|
154437
|
-
const label = ctx.oss ? "oss" : "router";
|
|
154438
|
-
log.info(`\xBB proxy: ${label} \u2192 ${ctx.proxyModel}`);
|
|
154439
|
-
}
|
|
154440
|
-
async function fetchPreviousSnapshot(ctx, prNumber) {
|
|
154441
|
-
if (!ctx.githubInstallationToken) return null;
|
|
154442
|
-
try {
|
|
154443
|
-
const response = await apiFetch({
|
|
154444
|
-
path: `/api/repo/${ctx.repo.owner}/${ctx.repo.name}/pr/${prNumber}/summary-comment`,
|
|
154445
|
-
method: "GET",
|
|
154446
|
-
headers: { authorization: `Bearer ${ctx.githubInstallationToken}` },
|
|
154447
|
-
signal: AbortSignal.timeout(1e4)
|
|
154448
|
-
});
|
|
154449
|
-
if (!response.ok) return null;
|
|
154450
|
-
const data = await response.json();
|
|
154451
|
-
return typeof data.snapshot === "string" && data.snapshot.length > 0 ? data.snapshot : null;
|
|
154452
|
-
} catch {
|
|
154453
|
-
return null;
|
|
154454
|
-
}
|
|
154455
|
-
}
|
|
154456
|
-
async function persistLearnings(ctx) {
|
|
154457
|
-
const filePath = ctx.toolState.learningsFilePath;
|
|
154458
|
-
if (!filePath) return;
|
|
154459
|
-
if (ctx.toolState.learningsPersistAttempted) return;
|
|
154460
|
-
ctx.toolState.learningsPersistAttempted = true;
|
|
154461
|
-
const current = await readLearningsFile(filePath);
|
|
154462
|
-
if (current === null) {
|
|
154463
|
-
log.debug(`learnings tmpfile missing or unreadable at ${filePath} \u2014 skipping persist`);
|
|
154464
|
-
return;
|
|
154465
|
-
}
|
|
154466
|
-
const seed = ctx.toolState.learningsSeed?.trim() ?? "";
|
|
154467
|
-
if (current === seed) {
|
|
154468
|
-
log.debug("learnings tmpfile unchanged from seed \u2014 skipping persist");
|
|
154469
|
-
return;
|
|
154470
|
-
}
|
|
154471
|
-
try {
|
|
154472
|
-
const response = await apiFetch({
|
|
154473
|
-
path: `/api/repo/${ctx.repo.owner}/${ctx.repo.name}/learnings`,
|
|
154474
|
-
method: "PATCH",
|
|
154475
|
-
headers: {
|
|
154476
|
-
authorization: `Bearer ${ctx.apiToken}`,
|
|
154477
|
-
"content-type": "application/json"
|
|
154478
|
-
},
|
|
154479
|
-
body: JSON.stringify({
|
|
154480
|
-
learnings: current,
|
|
154481
|
-
model: ctx.toolState.model
|
|
154482
|
-
}),
|
|
154483
|
-
signal: AbortSignal.timeout(1e4)
|
|
154484
|
-
});
|
|
154485
|
-
if (!response.ok) {
|
|
154486
|
-
const error49 = await response.text().catch(() => "(no body)");
|
|
154487
|
-
log.warning(`learnings persist failed (${response.status}): ${error49}`);
|
|
154488
|
-
return;
|
|
154489
|
-
}
|
|
154490
|
-
log.info("\xBB learnings updated");
|
|
154491
|
-
} catch (err) {
|
|
154492
|
-
log.warning(`learnings persist failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
154493
|
-
}
|
|
154494
|
-
}
|
|
154495
|
-
async function persistSummary(ctx) {
|
|
154496
|
-
const filePath = ctx.toolState.summaryFilePath;
|
|
154497
|
-
if (!filePath) return;
|
|
154498
|
-
if (ctx.toolState.summaryPersistAttempted) return;
|
|
154499
|
-
ctx.toolState.summaryPersistAttempted = true;
|
|
154500
|
-
const snapshot2 = await readSummaryFile(filePath);
|
|
154501
|
-
if (!snapshot2) {
|
|
154502
|
-
log.debug(`pr summary tmpfile missing or invalid at ${filePath} \u2014 skipping persist`);
|
|
154503
|
-
return;
|
|
154504
|
-
}
|
|
154505
|
-
const seed = ctx.toolState.summarySeed?.trim();
|
|
154506
|
-
if (seed !== void 0 && snapshot2 === seed) {
|
|
154507
|
-
log.warning(
|
|
154508
|
-
"\xBB pr summary tmpfile unchanged from seed \u2014 skipping persist (agent did not edit it)"
|
|
154509
|
-
);
|
|
154510
|
-
return;
|
|
154511
|
-
}
|
|
154512
|
-
await patchWorkflowRunFields(ctx, { summarySnapshot: snapshot2 }).catch((err) => {
|
|
154513
|
-
log.debug(`pr summary persist failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
154514
|
-
});
|
|
154515
|
-
}
|
|
154516
|
-
async function writeJobSummary(toolState, finalOutput) {
|
|
154517
|
-
const usageSummary = formatUsageSummary(toolState.usageEntries);
|
|
154518
|
-
const body = toolState.lastProgressBody || finalOutput;
|
|
154519
|
-
const summaryParts = [body, usageSummary].filter(Boolean);
|
|
154520
|
-
if (summaryParts.length > 0) {
|
|
154521
|
-
await writeSummary(summaryParts.join("\n\n"));
|
|
154522
|
-
}
|
|
154523
|
-
}
|
|
154524
155077
|
async function main() {
|
|
154525
155078
|
var _stack2 = [];
|
|
154526
155079
|
try {
|
|
154527
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
|
+
}
|
|
154528
155093
|
const usageSummaryPath = process.env.PULLFROG_USAGE_SUMMARY_PATH;
|
|
154529
155094
|
if (usageSummaryPath) {
|
|
154530
155095
|
onExitSignal(() => writeGitHubUsageSummaryToFile(usageSummaryPath));
|
|
@@ -154568,34 +155133,14 @@ async function main() {
|
|
|
154568
155133
|
delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
154569
155134
|
delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
154570
155135
|
}
|
|
154571
|
-
|
|
154572
|
-
|
|
154573
|
-
|
|
154574
|
-
|
|
154575
|
-
|
|
154576
|
-
|
|
154577
|
-
|
|
154578
|
-
|
|
154579
|
-
});
|
|
154580
|
-
} catch (error49) {
|
|
154581
|
-
if (error49 instanceof BillingError) {
|
|
154582
|
-
const summary2 = formatBillingErrorSummary(error49, runContext.repo.owner);
|
|
154583
|
-
await writeSummary(summary2).catch(() => {
|
|
154584
|
-
});
|
|
154585
|
-
await reportErrorToComment({ toolState, error: summary2 }).catch(() => {
|
|
154586
|
-
});
|
|
154587
|
-
throw error49;
|
|
154588
|
-
}
|
|
154589
|
-
if (error49 instanceof TransientError) {
|
|
154590
|
-
const summary2 = formatTransientErrorSummary(error49, runContext.repo.owner);
|
|
154591
|
-
await writeSummary(summary2).catch(() => {
|
|
154592
|
-
});
|
|
154593
|
-
await reportErrorToComment({ toolState, error: summary2 }).catch(() => {
|
|
154594
|
-
});
|
|
154595
|
-
throw error49;
|
|
154596
|
-
}
|
|
154597
|
-
throw error49;
|
|
154598
|
-
}
|
|
155136
|
+
await runProxyResolution({
|
|
155137
|
+
payload,
|
|
155138
|
+
oss: runContext.oss,
|
|
155139
|
+
proxyModel: runContext.proxyModel,
|
|
155140
|
+
oidcCredentials,
|
|
155141
|
+
repo: runContext.repo,
|
|
155142
|
+
toolState
|
|
155143
|
+
});
|
|
154599
155144
|
const octokit = createOctokit(tokenRef.mcpToken);
|
|
154600
155145
|
const runInfo = await resolveRun({ octokit });
|
|
154601
155146
|
let toolContext;
|
|
@@ -154622,12 +155167,24 @@ async function main() {
|
|
|
154622
155167
|
const tmpdir3 = createTempDirectory();
|
|
154623
155168
|
const gitAuthServer = __using(_stack, await startGitAuthServer(tmpdir3), true);
|
|
154624
155169
|
setGitAuthServer(gitAuthServer);
|
|
154625
|
-
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
|
+
}
|
|
154626
155183
|
const agent2 = resolveAgent({ model: resolvedModel });
|
|
154627
|
-
toolState.model = payload.proxyModel ?? resolvedModel ??
|
|
155184
|
+
toolState.model = payload.proxyModel ?? resolvedModel ?? effectiveSlug;
|
|
154628
155185
|
validateAgentApiKey({
|
|
154629
155186
|
agent: agent2,
|
|
154630
|
-
model: payload.proxyModel ?? resolvedModel ??
|
|
155187
|
+
model: payload.proxyModel ?? resolvedModel ?? effectiveSlug,
|
|
154631
155188
|
owner: runContext.repo.owner,
|
|
154632
155189
|
name: runContext.repo.name
|
|
154633
155190
|
});
|
|
@@ -154710,14 +155267,7 @@ async function main() {
|
|
|
154710
155267
|
onExitSignal(() => persistSummary(ctxForExit));
|
|
154711
155268
|
}
|
|
154712
155269
|
startInstallation(toolContext);
|
|
154713
|
-
|
|
154714
|
-
const agentForLog = resolveAgentForLog({ agentName: agent2.name, resolvedModel });
|
|
154715
|
-
const timeoutForLog = resolveTimeoutForLog(payload.timeout);
|
|
154716
|
-
log.info(`\xBB model: ${modelForLog}`);
|
|
154717
|
-
log.info(`\xBB agent: ${agentForLog}`);
|
|
154718
|
-
log.info(`\xBB push: ${payload.push}`);
|
|
154719
|
-
log.info(`\xBB shell: ${payload.shell}`);
|
|
154720
|
-
log.info(`\xBB timeout: ${timeoutForLog}`);
|
|
155270
|
+
logRunStartup({ payload, resolvedModel, agentName: agent2.name });
|
|
154721
155271
|
const instructions = resolveInstructions({
|
|
154722
155272
|
payload,
|
|
154723
155273
|
repo: runContext.repo,
|
|
@@ -154741,7 +155291,7 @@ ${instructions.user}` : null,
|
|
|
154741
155291
|
log.info(instructions.full);
|
|
154742
155292
|
});
|
|
154743
155293
|
if (agentId === "opencode") {
|
|
154744
|
-
const pluginDir =
|
|
155294
|
+
const pluginDir = join18(process.cwd(), ".opencode", "plugin");
|
|
154745
155295
|
const hasPlugins = existsSync7(pluginDir) && readdirSync(pluginDir).some((f) => /\.[jt]sx?$/.test(f));
|
|
154746
155296
|
if (hasPlugins && toolState.dependencyInstallation?.promise) {
|
|
154747
155297
|
log.info(
|
|
@@ -154801,6 +155351,7 @@ ${instructions.user}` : null,
|
|
|
154801
155351
|
todoTracker,
|
|
154802
155352
|
stopScript: runContext.repoSettings.stopScript,
|
|
154803
155353
|
toolState,
|
|
155354
|
+
apiToken: runContext.apiToken,
|
|
154804
155355
|
onActivityTimeout: onInnerActivityTimeout,
|
|
154805
155356
|
onToolUse: (event) => {
|
|
154806
155357
|
const wasTracked = recordDiffReadFromToolUse({
|
|
@@ -154850,42 +155401,7 @@ ${instructions.user}` : null,
|
|
|
154850
155401
|
"output_schema was provided but agent did not call set_output \u2014 structured output is required"
|
|
154851
155402
|
);
|
|
154852
155403
|
}
|
|
154853
|
-
|
|
154854
|
-
await postReviewCleanup(toolContext).catch((error49) => {
|
|
154855
|
-
log.debug(`post-review cleanup failed: ${error49}`);
|
|
154856
|
-
});
|
|
154857
|
-
}
|
|
154858
|
-
if (toolContext) {
|
|
154859
|
-
await persistSummary(toolContext);
|
|
154860
|
-
}
|
|
154861
|
-
if (toolContext) {
|
|
154862
|
-
await persistLearnings(toolContext);
|
|
154863
|
-
}
|
|
154864
|
-
if (!result.success && toolContext && toolState.progressComment) {
|
|
154865
|
-
const rawError = result.error || "agent run failed";
|
|
154866
|
-
const errorBody = isApiKeyAuthError(rawError) ? formatApiKeyErrorSummary({
|
|
154867
|
-
owner: runContext.repo.owner,
|
|
154868
|
-
name: runContext.repo.name,
|
|
154869
|
-
raw: rawError
|
|
154870
|
-
}) : rawError;
|
|
154871
|
-
await reportErrorToComment({ toolState, error: errorBody }).catch((error49) => {
|
|
154872
|
-
log.debug(`failure error report failed: ${error49}`);
|
|
154873
|
-
});
|
|
154874
|
-
}
|
|
154875
|
-
if (toolContext && result.success && toolState.progressComment && !toolState.finalSummaryWritten) {
|
|
154876
|
-
await deleteProgressComment(toolContext).catch((error49) => {
|
|
154877
|
-
log.debug(`stranded progress comment cleanup failed: ${error49}`);
|
|
154878
|
-
});
|
|
154879
|
-
}
|
|
154880
|
-
try {
|
|
154881
|
-
await writeJobSummary(toolState, result.output);
|
|
154882
|
-
} catch (error49) {
|
|
154883
|
-
log.debug(`job summary write failed: ${error49}`);
|
|
154884
|
-
}
|
|
154885
|
-
if (toolState.output) {
|
|
154886
|
-
log.info(`::pullfrog-output::${Buffer.from(toolState.output).toString("base64")}`);
|
|
154887
|
-
core7.setOutput("result", toolState.output);
|
|
154888
|
-
}
|
|
155404
|
+
await finalizeSuccessRun({ toolContext, toolState, result, repo: runContext.repo });
|
|
154889
155405
|
return await handleAgentResult({
|
|
154890
155406
|
result,
|
|
154891
155407
|
toolState,
|
|
@@ -154903,43 +155419,14 @@ ${instructions.user}` : null,
|
|
|
154903
155419
|
todoTracker?.cancel();
|
|
154904
155420
|
killTrackedChildren();
|
|
154905
155421
|
log.error(errorMessage);
|
|
154906
|
-
const
|
|
154907
|
-
|
|
154908
|
-
|
|
154909
|
-
|
|
154910
|
-
|
|
154911
|
-
|
|
154912
|
-
name: runContext.repo.name,
|
|
154913
|
-
raw: apiKeySource
|
|
154914
|
-
}) : null;
|
|
154915
|
-
try {
|
|
154916
|
-
const errorSummary = billingError ? formatBillingErrorSummary(billingError, runContext.repo.owner) : apiKeyErrorSummary ?? (hangBody ? `### \u274C Pullfrog failed
|
|
154917
|
-
|
|
154918
|
-
${hangBody}` : `### \u274C Pullfrog failed
|
|
154919
|
-
|
|
154920
|
-
\`\`\`
|
|
154921
|
-
${errorMessage}
|
|
154922
|
-
\`\`\``);
|
|
154923
|
-
const usageSummary = formatUsageSummary(toolState.usageEntries);
|
|
154924
|
-
const parts = [errorSummary, toolState.lastProgressBody, usageSummary].filter(Boolean);
|
|
154925
|
-
await writeSummary(parts.join("\n\n"));
|
|
154926
|
-
} catch {
|
|
154927
|
-
}
|
|
154928
|
-
try {
|
|
154929
|
-
const commentBody = billingError ? formatBillingErrorSummary(billingError, runContext.repo.owner) : apiKeyErrorSummary ?? hangBody ?? errorMessage;
|
|
154930
|
-
await reportErrorToComment({ toolState, error: commentBody });
|
|
154931
|
-
} catch {
|
|
154932
|
-
}
|
|
154933
|
-
if (toolContext) {
|
|
154934
|
-
await postReviewCleanup(toolContext).catch((error50) => {
|
|
154935
|
-
log.debug(`post-review cleanup failed: ${error50}`);
|
|
154936
|
-
});
|
|
154937
|
-
}
|
|
154938
|
-
if (toolContext) {
|
|
154939
|
-
await persistSummary(toolContext);
|
|
154940
|
-
}
|
|
155422
|
+
const rendered = renderRunError({
|
|
155423
|
+
errorMessage,
|
|
155424
|
+
repo: runContext.repo,
|
|
155425
|
+
agentDiagnostic: toolState.agentDiagnostic
|
|
155426
|
+
});
|
|
155427
|
+
await writeRunErrorOutputs({ rendered, toolState });
|
|
154941
155428
|
if (toolContext) {
|
|
154942
|
-
await
|
|
155429
|
+
await persistRunArtifacts(toolContext);
|
|
154943
155430
|
}
|
|
154944
155431
|
return {
|
|
154945
155432
|
success: false,
|