pullfrog 0.0.205 → 0.1.1
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/cli.mjs +113 -20
- package/dist/index.js +112 -19
- package/dist/internal.js +5 -1
- package/dist/mcp/review.d.ts +13 -0
- package/dist/utils/patchWorkflowRunFields.d.ts +1 -1
- package/dist/utils/providerErrors.d.ts +1 -0
- package/dist/utils/retry.d.ts +6 -0
- package/dist/utils/subprocess.d.ts +2 -0
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -108399,7 +108399,11 @@ function resolveCliModel(slug2) {
|
|
|
108399
108399
|
var PULLFROG_DIVIDER = "<!-- PULLFROG_DIVIDER_DO_NOT_REMOVE_PLZ -->";
|
|
108400
108400
|
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>`;
|
|
108401
108401
|
function formatModelLabel(slug2) {
|
|
108402
|
-
const alias = resolveDisplayAlias(slug2)
|
|
108402
|
+
const alias = resolveDisplayAlias(slug2) ?? // reverse-lookup: when the caller passes an effective model (proxy or
|
|
108403
|
+
// resolved target like "openrouter/anthropic/claude-opus-4.7") instead of
|
|
108404
|
+
// a stored alias slug, find the alias whose resolve target matches so we
|
|
108405
|
+
// still render a friendly display name.
|
|
108406
|
+
modelAliases.find((a) => a.resolve === slug2 || a.openRouterResolve === slug2);
|
|
108403
108407
|
if (!alias) return `\`${slug2}\``;
|
|
108404
108408
|
return alias.isFree ? `\`${alias.displayName}\` (free)` : `\`${alias.displayName}\``;
|
|
108405
108409
|
}
|
|
@@ -108474,10 +108478,13 @@ var defaultShouldRetry = (error49) => {
|
|
|
108474
108478
|
return error49.name === "AbortError" || error49.message.includes("fetch failed") || error49.message.includes("ECONNRESET") || error49.message.includes("ETIMEDOUT");
|
|
108475
108479
|
};
|
|
108476
108480
|
async function retry(fn2, options = {}) {
|
|
108477
|
-
const maxAttempts = options.maxAttempts ?? 3;
|
|
108478
|
-
const delayMs = options.delayMs ?? 1e3;
|
|
108479
108481
|
const shouldRetry = options.shouldRetry ?? defaultShouldRetry;
|
|
108480
108482
|
const label = options.label ?? "operation";
|
|
108483
|
+
const delays = options.delaysMs ? Array.from(options.delaysMs) : Array.from(
|
|
108484
|
+
{ length: (options.maxAttempts ?? 3) - 1 },
|
|
108485
|
+
(_2, i) => (options.delayMs ?? 1e3) * (i + 1)
|
|
108486
|
+
);
|
|
108487
|
+
const maxAttempts = delays.length + 1;
|
|
108481
108488
|
let lastError;
|
|
108482
108489
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
108483
108490
|
try {
|
|
@@ -108487,7 +108494,7 @@ async function retry(fn2, options = {}) {
|
|
|
108487
108494
|
if (attempt === maxAttempts || !shouldRetry(error49)) {
|
|
108488
108495
|
throw error49;
|
|
108489
108496
|
}
|
|
108490
|
-
const delay2 =
|
|
108497
|
+
const delay2 = delays[attempt - 1];
|
|
108491
108498
|
log.info(`\xBB ${label} failed (attempt ${attempt}/${maxAttempts}), retrying in ${delay2}ms...`);
|
|
108492
108499
|
await sleep(delay2);
|
|
108493
108500
|
}
|
|
@@ -108501,7 +108508,6 @@ var STRING_KEYS = [
|
|
|
108501
108508
|
"issueNodeId",
|
|
108502
108509
|
"reviewNodeId",
|
|
108503
108510
|
"planCommentNodeId",
|
|
108504
|
-
"summaryCommentNodeId",
|
|
108505
108511
|
"summarySnapshot"
|
|
108506
108512
|
];
|
|
108507
108513
|
var NUMBER_KEYS = [
|
|
@@ -109973,6 +109979,7 @@ async function spawn(options) {
|
|
|
109973
109979
|
const startTime = performance3.now();
|
|
109974
109980
|
let stdoutBuffer = "";
|
|
109975
109981
|
let stderrBuffer = "";
|
|
109982
|
+
const killGroup = options.killGroup ?? false;
|
|
109976
109983
|
return new Promise((resolve3, reject) => {
|
|
109977
109984
|
const child = nodeSpawn(options.cmd, options.args, {
|
|
109978
109985
|
env: options.env || {
|
|
@@ -109980,9 +109987,20 @@ async function spawn(options) {
|
|
|
109980
109987
|
HOME: process.env.HOME || ""
|
|
109981
109988
|
},
|
|
109982
109989
|
stdio: options.stdio || ["pipe", "pipe", "pipe"],
|
|
109983
|
-
cwd: options.cwd || process.cwd()
|
|
109990
|
+
cwd: options.cwd || process.cwd(),
|
|
109991
|
+
detached: killGroup
|
|
109984
109992
|
});
|
|
109985
|
-
|
|
109993
|
+
const killSelf = (signal) => {
|
|
109994
|
+
if (killGroup && child.pid) {
|
|
109995
|
+
try {
|
|
109996
|
+
process.kill(-child.pid, signal);
|
|
109997
|
+
return;
|
|
109998
|
+
} catch {
|
|
109999
|
+
}
|
|
110000
|
+
}
|
|
110001
|
+
child.kill(signal);
|
|
110002
|
+
};
|
|
110003
|
+
trackChild({ child, killGroup });
|
|
109986
110004
|
let timeoutId;
|
|
109987
110005
|
let sigkillEscalatorId;
|
|
109988
110006
|
let activityCheckIntervalId;
|
|
@@ -109993,10 +110011,10 @@ async function spawn(options) {
|
|
|
109993
110011
|
if (options.timeout) {
|
|
109994
110012
|
timeoutId = setTimeout(() => {
|
|
109995
110013
|
isTimedOut = true;
|
|
109996
|
-
|
|
110014
|
+
killSelf("SIGTERM");
|
|
109997
110015
|
sigkillEscalatorId = setTimeout(() => {
|
|
109998
110016
|
if (!child.killed) {
|
|
109999
|
-
|
|
110017
|
+
killSelf("SIGKILL");
|
|
110000
110018
|
}
|
|
110001
110019
|
}, 5e3);
|
|
110002
110020
|
}, options.timeout);
|
|
@@ -110006,6 +110024,11 @@ async function spawn(options) {
|
|
|
110006
110024
|
`spawn activity timer: pid=${child.pid} cmd=${options.cmd} timeout=${activityTimeoutMs}ms`
|
|
110007
110025
|
);
|
|
110008
110026
|
activityCheckIntervalId = setInterval(() => {
|
|
110027
|
+
if (options.isPausedExternally?.()) {
|
|
110028
|
+
lastActivityTime = performance3.now();
|
|
110029
|
+
log.debug(`spawn activity check: pid=${child.pid} paused externally`);
|
|
110030
|
+
return;
|
|
110031
|
+
}
|
|
110009
110032
|
const idleMs = performance3.now() - lastActivityTime;
|
|
110010
110033
|
log.debug(
|
|
110011
110034
|
`spawn activity check: pid=${child.pid} idle=${Math.round(idleMs)}ms / ${activityTimeoutMs}ms`
|
|
@@ -110015,9 +110038,9 @@ async function spawn(options) {
|
|
|
110015
110038
|
killedAtIdleMs = idleMs;
|
|
110016
110039
|
const idleSec = Math.round(idleMs / 1e3);
|
|
110017
110040
|
log.info(
|
|
110018
|
-
`no output for ${idleSec}s from pid=${child.pid} (${options.cmd}), killing process`
|
|
110041
|
+
`no output for ${idleSec}s from pid=${child.pid} (${options.cmd}), killing process${killGroup ? " group" : ""}`
|
|
110019
110042
|
);
|
|
110020
|
-
|
|
110043
|
+
killSelf("SIGKILL");
|
|
110021
110044
|
clearInterval(activityCheckIntervalId);
|
|
110022
110045
|
try {
|
|
110023
110046
|
options.onActivityTimeout?.();
|
|
@@ -142526,7 +142549,7 @@ var import_semver = __toESM(require_semver2(), 1);
|
|
|
142526
142549
|
// package.json
|
|
142527
142550
|
var package_default = {
|
|
142528
142551
|
name: "pullfrog",
|
|
142529
|
-
version: "0.
|
|
142552
|
+
version: "0.1.1",
|
|
142530
142553
|
type: "module",
|
|
142531
142554
|
bin: {
|
|
142532
142555
|
pullfrog: "dist/cli.mjs",
|
|
@@ -143654,6 +143677,12 @@ function getHttpStatus(err) {
|
|
|
143654
143677
|
const status = err.status;
|
|
143655
143678
|
return typeof status === "number" ? status : void 0;
|
|
143656
143679
|
}
|
|
143680
|
+
function isTransientReviewError(err) {
|
|
143681
|
+
if (getHttpStatus(err) !== 422) return false;
|
|
143682
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
143683
|
+
return /internal error occurred, please try again/i.test(msg);
|
|
143684
|
+
}
|
|
143685
|
+
var TRANSIENT_REVIEW_RETRY_DELAYS_MS = [1e3, 3e3];
|
|
143657
143686
|
function commentableLinesForFile(patch) {
|
|
143658
143687
|
const right = /* @__PURE__ */ new Set();
|
|
143659
143688
|
const left = /* @__PURE__ */ new Set();
|
|
@@ -143921,12 +143950,26 @@ function CreatePullRequestReviewTool(ctx) {
|
|
|
143921
143950
|
}
|
|
143922
143951
|
let result;
|
|
143923
143952
|
try {
|
|
143924
|
-
result =
|
|
143925
|
-
body,
|
|
143926
|
-
|
|
143927
|
-
|
|
143928
|
-
|
|
143953
|
+
result = await retry(
|
|
143954
|
+
() => body ? createAndSubmitWithFooter(ctx, params, {
|
|
143955
|
+
body,
|
|
143956
|
+
approved: approved ?? false,
|
|
143957
|
+
hasComments: (params.comments?.length ?? 0) > 0
|
|
143958
|
+
}) : createReviewWithStrandedRecovery(ctx, params),
|
|
143959
|
+
{
|
|
143960
|
+
delaysMs: TRANSIENT_REVIEW_RETRY_DELAYS_MS,
|
|
143961
|
+
shouldRetry: isTransientReviewError,
|
|
143962
|
+
label: "review submission"
|
|
143963
|
+
}
|
|
143964
|
+
);
|
|
143929
143965
|
} catch (err) {
|
|
143966
|
+
if (isTransientReviewError(err)) {
|
|
143967
|
+
const rawMsg2 = err instanceof Error ? err.message : String(err);
|
|
143968
|
+
throw new Error(
|
|
143969
|
+
`GitHub returned a transient 422 "internal error" on the reviews endpoint after ${TRANSIENT_REVIEW_RETRY_DELAYS_MS.length + 1} attempts. This is a GitHub-side issue, not a problem with your review content. Do NOT modify or drop inline comments \u2014 their content is not the cause. Wait ~30 seconds and call this tool once more with the SAME arguments. If it still fails, submit a body-only review (move all inline feedback into \`body\` as text) so nothing is lost. GitHub said: ${rawMsg2}`,
|
|
143970
|
+
{ cause: err }
|
|
143971
|
+
);
|
|
143972
|
+
}
|
|
143930
143973
|
if (getHttpStatus(err) !== 422 || !params.comments?.length) throw err;
|
|
143931
143974
|
const details = params.comments.map((c2) => {
|
|
143932
143975
|
const line = c2.line ?? 0;
|
|
@@ -146899,6 +146942,10 @@ function detectProviderError(text) {
|
|
|
146899
146942
|
}
|
|
146900
146943
|
return null;
|
|
146901
146944
|
}
|
|
146945
|
+
var ROUTER_KEYLIMIT_EXHAUSTED_PATTERN = /requires more credits.*?fewer max_tokens|requested up to \d+ tokens.*?can only afford/is;
|
|
146946
|
+
function isRouterKeylimitExhaustedError(text) {
|
|
146947
|
+
return ROUTER_KEYLIMIT_EXHAUSTED_PATTERN.test(text);
|
|
146948
|
+
}
|
|
146902
146949
|
|
|
146903
146950
|
// utils/skills.ts
|
|
146904
146951
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
@@ -147437,6 +147484,12 @@ async function runClaude(params) {
|
|
|
147437
147484
|
activityTimeout: 3e5,
|
|
147438
147485
|
onActivityTimeout: params.onActivityTimeout,
|
|
147439
147486
|
stdio: ["ignore", "pipe", "pipe"],
|
|
147487
|
+
// run claude in its own process group so SIGKILL on activity timeout /
|
|
147488
|
+
// outer cancellation reaches any subprocesses it spawns (rg, file
|
|
147489
|
+
// watchers, mcp transports, etc). claude itself is a node bundle so
|
|
147490
|
+
// there's no shim-orphan issue like opencode-ai/bin/opencode, but
|
|
147491
|
+
// detached + killGroup is the right default for any agent runtime.
|
|
147492
|
+
killGroup: true,
|
|
147440
147493
|
onStdout: async (chunk) => {
|
|
147441
147494
|
const text = chunk.toString();
|
|
147442
147495
|
output += text;
|
|
@@ -147703,6 +147756,7 @@ async function installOpencodeCli() {
|
|
|
147703
147756
|
installDependencies: true
|
|
147704
147757
|
});
|
|
147705
147758
|
}
|
|
147759
|
+
var PULLFROG_OPENCODE_OUTPUT_LIMIT = 5e3;
|
|
147706
147760
|
function buildSecurityConfig(ctx, model) {
|
|
147707
147761
|
const config3 = {
|
|
147708
147762
|
permission: {
|
|
@@ -147795,6 +147849,9 @@ async function runOpenCode(params) {
|
|
|
147795
147849
|
const taskDispatchByCallID = /* @__PURE__ */ new Map();
|
|
147796
147850
|
const pendingTaskDispatches = [];
|
|
147797
147851
|
const knownNonTaskCallIDs = /* @__PURE__ */ new Set();
|
|
147852
|
+
function isSubagentInFlight() {
|
|
147853
|
+
return taskDispatchByCallID.size > 0 || pendingTaskDispatches.length > 0;
|
|
147854
|
+
}
|
|
147798
147855
|
function emitSubagentFinished(dispatch, status, output2, matchKind) {
|
|
147799
147856
|
const subagentDuration = performance7.now() - dispatch.startedAt;
|
|
147800
147857
|
const outputStr = typeof output2 === "string" ? output2 : "";
|
|
@@ -148047,6 +148104,20 @@ async function runOpenCode(params) {
|
|
|
148047
148104
|
activityTimeout: 3e5,
|
|
148048
148105
|
onActivityTimeout: params.onActivityTimeout,
|
|
148049
148106
|
stdio: ["ignore", "pipe", "pipe"],
|
|
148107
|
+
// node_modules/opencode-ai/bin/opencode is a Node shim that spawnSyncs
|
|
148108
|
+
// the native opencode-<plat>-<arch> binary with stdio:"inherit". without
|
|
148109
|
+
// a process-group kill, SIGKILL hits only the shim, the native binary
|
|
148110
|
+
// is reparented to PID 1, holds our stdout pipe open, and `child.close`
|
|
148111
|
+
// never fires — producing zombie runs. detached + killGroup nukes the
|
|
148112
|
+
// whole tree.
|
|
148113
|
+
killGroup: true,
|
|
148114
|
+
// suspend the inner activity timer while a `task` subagent is in flight.
|
|
148115
|
+
// opencode's task tool encapsulates subagent execution in-process — the
|
|
148116
|
+
// subagent's internal events don't surface on the parent NDJSON stream,
|
|
148117
|
+
// so without this the 5min timeout would falsely fire mid-subagent.
|
|
148118
|
+
// suspend/resume is preferable to a heartbeat because there's no race
|
|
148119
|
+
// between a periodic tick and a subagent finishing between ticks.
|
|
148120
|
+
isPausedExternally: isSubagentInFlight,
|
|
148050
148121
|
onStdout: async (chunk) => {
|
|
148051
148122
|
const text = chunk.toString();
|
|
148052
148123
|
output += text;
|
|
@@ -148218,6 +148289,7 @@ var opencode = agent({
|
|
|
148218
148289
|
...homeEnv,
|
|
148219
148290
|
OPENCODE_CONFIG_CONTENT: buildSecurityConfig(ctx, model),
|
|
148220
148291
|
OPENCODE_PERMISSION: permissionOverride,
|
|
148292
|
+
OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX: PULLFROG_OPENCODE_OUTPUT_LIMIT.toString(),
|
|
148221
148293
|
GOOGLE_GENERATIVE_AI_API_KEY: process.env.GOOGLE_GENERATIVE_AI_API_KEY || process.env.GEMINI_API_KEY
|
|
148222
148294
|
};
|
|
148223
148295
|
const repoDir = process.cwd();
|
|
@@ -153635,6 +153707,24 @@ function formatBillingErrorSummary(error49, owner) {
|
|
|
153635
153707
|
`[Add a card \u2192](${billingConsoleUrl(owner, "model-access")})`
|
|
153636
153708
|
].join("\n");
|
|
153637
153709
|
}
|
|
153710
|
+
if (error49.code === "router_balance_exhausted") {
|
|
153711
|
+
return [
|
|
153712
|
+
"**Your Pullfrog Router balance is exhausted.**",
|
|
153713
|
+
"",
|
|
153714
|
+
"You have a card on file but auto-reload is disabled, so runs paused once your balance went past the overdraft buffer.",
|
|
153715
|
+
"",
|
|
153716
|
+
`[Top up balance \u2192](${billingConsoleUrl(owner, "billing")}) \xB7 [Enable auto-reload \u2192](${billingConsoleUrl(owner, "model-access")})`
|
|
153717
|
+
].join("\n");
|
|
153718
|
+
}
|
|
153719
|
+
if (error49.code === "router_keylimit_exhausted") {
|
|
153720
|
+
return [
|
|
153721
|
+
"**This run was cut short \u2014 your Pullfrog Router balance ran out mid-run.**",
|
|
153722
|
+
"",
|
|
153723
|
+
"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.",
|
|
153724
|
+
"",
|
|
153725
|
+
`[Top up balance \u2192](${billingConsoleUrl(owner, "billing")}) \xB7 [Enable auto-reload \u2192](${billingConsoleUrl(owner, "model-access")})`
|
|
153726
|
+
].join("\n");
|
|
153727
|
+
}
|
|
153638
153728
|
if (error49.needsReauthentication) {
|
|
153639
153729
|
const code = error49.declineCode ?? "authentication_required";
|
|
153640
153730
|
return [
|
|
@@ -153875,6 +153965,7 @@ async function main() {
|
|
|
153875
153965
|
setGitAuthServer(gitAuthServer);
|
|
153876
153966
|
const resolvedModel = payload.proxyModel ? void 0 : resolveModel({ slug: payload.model });
|
|
153877
153967
|
const agent2 = resolveAgent({ model: resolvedModel });
|
|
153968
|
+
toolState.model = payload.proxyModel ?? resolvedModel ?? payload.model;
|
|
153878
153969
|
validateAgentApiKey({
|
|
153879
153970
|
agent: agent2,
|
|
153880
153971
|
model: payload.proxyModel ?? resolvedModel ?? payload.model,
|
|
@@ -154118,8 +154209,9 @@ ${instructions.user}` : null,
|
|
|
154118
154209
|
todoTracker?.cancel();
|
|
154119
154210
|
killTrackedChildren();
|
|
154120
154211
|
log.error(errorMessage);
|
|
154212
|
+
const billingError = isRouterKeylimitExhaustedError(errorMessage) ? new BillingError(errorMessage, { code: "router_keylimit_exhausted" }) : null;
|
|
154121
154213
|
try {
|
|
154122
|
-
const errorSummary = `### \u274C Pullfrog failed
|
|
154214
|
+
const errorSummary = billingError ? formatBillingErrorSummary(billingError, runContext.repo.owner) : `### \u274C Pullfrog failed
|
|
154123
154215
|
|
|
154124
154216
|
\`\`\`
|
|
154125
154217
|
${errorMessage}
|
|
@@ -154130,7 +154222,8 @@ ${errorMessage}
|
|
|
154130
154222
|
} catch {
|
|
154131
154223
|
}
|
|
154132
154224
|
try {
|
|
154133
|
-
|
|
154225
|
+
const commentBody = billingError ? formatBillingErrorSummary(billingError, runContext.repo.owner) : errorMessage;
|
|
154226
|
+
await reportErrorToComment({ toolState, error: commentBody });
|
|
154134
154227
|
} catch {
|
|
154135
154228
|
}
|
|
154136
154229
|
if (toolContext) {
|
|
@@ -155983,7 +156076,7 @@ async function run2() {
|
|
|
155983
156076
|
}
|
|
155984
156077
|
|
|
155985
156078
|
// cli.ts
|
|
155986
|
-
var VERSION10 = "0.
|
|
156079
|
+
var VERSION10 = "0.1.1";
|
|
155987
156080
|
var bin = basename2(process.argv[1] || "");
|
|
155988
156081
|
var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
|
|
155989
156082
|
var rawArgs = process.argv.slice(2);
|
package/dist/index.js
CHANGED
|
@@ -108116,7 +108116,11 @@ function resolveCliModel(slug2) {
|
|
|
108116
108116
|
var PULLFROG_DIVIDER = "<!-- PULLFROG_DIVIDER_DO_NOT_REMOVE_PLZ -->";
|
|
108117
108117
|
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>`;
|
|
108118
108118
|
function formatModelLabel(slug2) {
|
|
108119
|
-
const alias = resolveDisplayAlias(slug2)
|
|
108119
|
+
const alias = resolveDisplayAlias(slug2) ?? // reverse-lookup: when the caller passes an effective model (proxy or
|
|
108120
|
+
// resolved target like "openrouter/anthropic/claude-opus-4.7") instead of
|
|
108121
|
+
// a stored alias slug, find the alias whose resolve target matches so we
|
|
108122
|
+
// still render a friendly display name.
|
|
108123
|
+
modelAliases.find((a) => a.resolve === slug2 || a.openRouterResolve === slug2);
|
|
108120
108124
|
if (!alias) return `\`${slug2}\``;
|
|
108121
108125
|
return alias.isFree ? `\`${alias.displayName}\` (free)` : `\`${alias.displayName}\``;
|
|
108122
108126
|
}
|
|
@@ -108191,10 +108195,13 @@ var defaultShouldRetry = (error49) => {
|
|
|
108191
108195
|
return error49.name === "AbortError" || error49.message.includes("fetch failed") || error49.message.includes("ECONNRESET") || error49.message.includes("ETIMEDOUT");
|
|
108192
108196
|
};
|
|
108193
108197
|
async function retry(fn2, options = {}) {
|
|
108194
|
-
const maxAttempts = options.maxAttempts ?? 3;
|
|
108195
|
-
const delayMs = options.delayMs ?? 1e3;
|
|
108196
108198
|
const shouldRetry = options.shouldRetry ?? defaultShouldRetry;
|
|
108197
108199
|
const label = options.label ?? "operation";
|
|
108200
|
+
const delays = options.delaysMs ? Array.from(options.delaysMs) : Array.from(
|
|
108201
|
+
{ length: (options.maxAttempts ?? 3) - 1 },
|
|
108202
|
+
(_, i) => (options.delayMs ?? 1e3) * (i + 1)
|
|
108203
|
+
);
|
|
108204
|
+
const maxAttempts = delays.length + 1;
|
|
108198
108205
|
let lastError;
|
|
108199
108206
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
108200
108207
|
try {
|
|
@@ -108204,7 +108211,7 @@ async function retry(fn2, options = {}) {
|
|
|
108204
108211
|
if (attempt === maxAttempts || !shouldRetry(error49)) {
|
|
108205
108212
|
throw error49;
|
|
108206
108213
|
}
|
|
108207
|
-
const delay2 =
|
|
108214
|
+
const delay2 = delays[attempt - 1];
|
|
108208
108215
|
log.info(`\xBB ${label} failed (attempt ${attempt}/${maxAttempts}), retrying in ${delay2}ms...`);
|
|
108209
108216
|
await sleep(delay2);
|
|
108210
108217
|
}
|
|
@@ -108218,7 +108225,6 @@ var STRING_KEYS = [
|
|
|
108218
108225
|
"issueNodeId",
|
|
108219
108226
|
"reviewNodeId",
|
|
108220
108227
|
"planCommentNodeId",
|
|
108221
|
-
"summaryCommentNodeId",
|
|
108222
108228
|
"summarySnapshot"
|
|
108223
108229
|
];
|
|
108224
108230
|
var NUMBER_KEYS = [
|
|
@@ -109690,6 +109696,7 @@ async function spawn(options) {
|
|
|
109690
109696
|
const startTime = performance3.now();
|
|
109691
109697
|
let stdoutBuffer = "";
|
|
109692
109698
|
let stderrBuffer = "";
|
|
109699
|
+
const killGroup = options.killGroup ?? false;
|
|
109693
109700
|
return new Promise((resolve3, reject) => {
|
|
109694
109701
|
const child = nodeSpawn(options.cmd, options.args, {
|
|
109695
109702
|
env: options.env || {
|
|
@@ -109697,9 +109704,20 @@ async function spawn(options) {
|
|
|
109697
109704
|
HOME: process.env.HOME || ""
|
|
109698
109705
|
},
|
|
109699
109706
|
stdio: options.stdio || ["pipe", "pipe", "pipe"],
|
|
109700
|
-
cwd: options.cwd || process.cwd()
|
|
109707
|
+
cwd: options.cwd || process.cwd(),
|
|
109708
|
+
detached: killGroup
|
|
109701
109709
|
});
|
|
109702
|
-
|
|
109710
|
+
const killSelf = (signal) => {
|
|
109711
|
+
if (killGroup && child.pid) {
|
|
109712
|
+
try {
|
|
109713
|
+
process.kill(-child.pid, signal);
|
|
109714
|
+
return;
|
|
109715
|
+
} catch {
|
|
109716
|
+
}
|
|
109717
|
+
}
|
|
109718
|
+
child.kill(signal);
|
|
109719
|
+
};
|
|
109720
|
+
trackChild({ child, killGroup });
|
|
109703
109721
|
let timeoutId;
|
|
109704
109722
|
let sigkillEscalatorId;
|
|
109705
109723
|
let activityCheckIntervalId;
|
|
@@ -109710,10 +109728,10 @@ async function spawn(options) {
|
|
|
109710
109728
|
if (options.timeout) {
|
|
109711
109729
|
timeoutId = setTimeout(() => {
|
|
109712
109730
|
isTimedOut = true;
|
|
109713
|
-
|
|
109731
|
+
killSelf("SIGTERM");
|
|
109714
109732
|
sigkillEscalatorId = setTimeout(() => {
|
|
109715
109733
|
if (!child.killed) {
|
|
109716
|
-
|
|
109734
|
+
killSelf("SIGKILL");
|
|
109717
109735
|
}
|
|
109718
109736
|
}, 5e3);
|
|
109719
109737
|
}, options.timeout);
|
|
@@ -109723,6 +109741,11 @@ async function spawn(options) {
|
|
|
109723
109741
|
`spawn activity timer: pid=${child.pid} cmd=${options.cmd} timeout=${activityTimeoutMs}ms`
|
|
109724
109742
|
);
|
|
109725
109743
|
activityCheckIntervalId = setInterval(() => {
|
|
109744
|
+
if (options.isPausedExternally?.()) {
|
|
109745
|
+
lastActivityTime = performance3.now();
|
|
109746
|
+
log.debug(`spawn activity check: pid=${child.pid} paused externally`);
|
|
109747
|
+
return;
|
|
109748
|
+
}
|
|
109726
109749
|
const idleMs = performance3.now() - lastActivityTime;
|
|
109727
109750
|
log.debug(
|
|
109728
109751
|
`spawn activity check: pid=${child.pid} idle=${Math.round(idleMs)}ms / ${activityTimeoutMs}ms`
|
|
@@ -109732,9 +109755,9 @@ async function spawn(options) {
|
|
|
109732
109755
|
killedAtIdleMs = idleMs;
|
|
109733
109756
|
const idleSec = Math.round(idleMs / 1e3);
|
|
109734
109757
|
log.info(
|
|
109735
|
-
`no output for ${idleSec}s from pid=${child.pid} (${options.cmd}), killing process`
|
|
109758
|
+
`no output for ${idleSec}s from pid=${child.pid} (${options.cmd}), killing process${killGroup ? " group" : ""}`
|
|
109736
109759
|
);
|
|
109737
|
-
|
|
109760
|
+
killSelf("SIGKILL");
|
|
109738
109761
|
clearInterval(activityCheckIntervalId);
|
|
109739
109762
|
try {
|
|
109740
109763
|
options.onActivityTimeout?.();
|
|
@@ -142243,7 +142266,7 @@ var import_semver = __toESM(require_semver2(), 1);
|
|
|
142243
142266
|
// package.json
|
|
142244
142267
|
var package_default = {
|
|
142245
142268
|
name: "pullfrog",
|
|
142246
|
-
version: "0.
|
|
142269
|
+
version: "0.1.1",
|
|
142247
142270
|
type: "module",
|
|
142248
142271
|
bin: {
|
|
142249
142272
|
pullfrog: "dist/cli.mjs",
|
|
@@ -143371,6 +143394,12 @@ function getHttpStatus(err) {
|
|
|
143371
143394
|
const status = err.status;
|
|
143372
143395
|
return typeof status === "number" ? status : void 0;
|
|
143373
143396
|
}
|
|
143397
|
+
function isTransientReviewError(err) {
|
|
143398
|
+
if (getHttpStatus(err) !== 422) return false;
|
|
143399
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
143400
|
+
return /internal error occurred, please try again/i.test(msg);
|
|
143401
|
+
}
|
|
143402
|
+
var TRANSIENT_REVIEW_RETRY_DELAYS_MS = [1e3, 3e3];
|
|
143374
143403
|
function commentableLinesForFile(patch) {
|
|
143375
143404
|
const right = /* @__PURE__ */ new Set();
|
|
143376
143405
|
const left = /* @__PURE__ */ new Set();
|
|
@@ -143638,12 +143667,26 @@ function CreatePullRequestReviewTool(ctx) {
|
|
|
143638
143667
|
}
|
|
143639
143668
|
let result;
|
|
143640
143669
|
try {
|
|
143641
|
-
result =
|
|
143642
|
-
body,
|
|
143643
|
-
|
|
143644
|
-
|
|
143645
|
-
|
|
143670
|
+
result = await retry(
|
|
143671
|
+
() => body ? createAndSubmitWithFooter(ctx, params, {
|
|
143672
|
+
body,
|
|
143673
|
+
approved: approved ?? false,
|
|
143674
|
+
hasComments: (params.comments?.length ?? 0) > 0
|
|
143675
|
+
}) : createReviewWithStrandedRecovery(ctx, params),
|
|
143676
|
+
{
|
|
143677
|
+
delaysMs: TRANSIENT_REVIEW_RETRY_DELAYS_MS,
|
|
143678
|
+
shouldRetry: isTransientReviewError,
|
|
143679
|
+
label: "review submission"
|
|
143680
|
+
}
|
|
143681
|
+
);
|
|
143646
143682
|
} catch (err) {
|
|
143683
|
+
if (isTransientReviewError(err)) {
|
|
143684
|
+
const rawMsg2 = err instanceof Error ? err.message : String(err);
|
|
143685
|
+
throw new Error(
|
|
143686
|
+
`GitHub returned a transient 422 "internal error" on the reviews endpoint after ${TRANSIENT_REVIEW_RETRY_DELAYS_MS.length + 1} attempts. This is a GitHub-side issue, not a problem with your review content. Do NOT modify or drop inline comments \u2014 their content is not the cause. Wait ~30 seconds and call this tool once more with the SAME arguments. If it still fails, submit a body-only review (move all inline feedback into \`body\` as text) so nothing is lost. GitHub said: ${rawMsg2}`,
|
|
143687
|
+
{ cause: err }
|
|
143688
|
+
);
|
|
143689
|
+
}
|
|
143647
143690
|
if (getHttpStatus(err) !== 422 || !params.comments?.length) throw err;
|
|
143648
143691
|
const details = params.comments.map((c) => {
|
|
143649
143692
|
const line = c.line ?? 0;
|
|
@@ -146616,6 +146659,10 @@ function detectProviderError(text) {
|
|
|
146616
146659
|
}
|
|
146617
146660
|
return null;
|
|
146618
146661
|
}
|
|
146662
|
+
var ROUTER_KEYLIMIT_EXHAUSTED_PATTERN = /requires more credits.*?fewer max_tokens|requested up to \d+ tokens.*?can only afford/is;
|
|
146663
|
+
function isRouterKeylimitExhaustedError(text) {
|
|
146664
|
+
return ROUTER_KEYLIMIT_EXHAUSTED_PATTERN.test(text);
|
|
146665
|
+
}
|
|
146619
146666
|
|
|
146620
146667
|
// utils/skills.ts
|
|
146621
146668
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
@@ -147154,6 +147201,12 @@ async function runClaude(params) {
|
|
|
147154
147201
|
activityTimeout: 3e5,
|
|
147155
147202
|
onActivityTimeout: params.onActivityTimeout,
|
|
147156
147203
|
stdio: ["ignore", "pipe", "pipe"],
|
|
147204
|
+
// run claude in its own process group so SIGKILL on activity timeout /
|
|
147205
|
+
// outer cancellation reaches any subprocesses it spawns (rg, file
|
|
147206
|
+
// watchers, mcp transports, etc). claude itself is a node bundle so
|
|
147207
|
+
// there's no shim-orphan issue like opencode-ai/bin/opencode, but
|
|
147208
|
+
// detached + killGroup is the right default for any agent runtime.
|
|
147209
|
+
killGroup: true,
|
|
147157
147210
|
onStdout: async (chunk) => {
|
|
147158
147211
|
const text = chunk.toString();
|
|
147159
147212
|
output += text;
|
|
@@ -147420,6 +147473,7 @@ async function installOpencodeCli() {
|
|
|
147420
147473
|
installDependencies: true
|
|
147421
147474
|
});
|
|
147422
147475
|
}
|
|
147476
|
+
var PULLFROG_OPENCODE_OUTPUT_LIMIT = 5e3;
|
|
147423
147477
|
function buildSecurityConfig(ctx, model) {
|
|
147424
147478
|
const config3 = {
|
|
147425
147479
|
permission: {
|
|
@@ -147512,6 +147566,9 @@ async function runOpenCode(params) {
|
|
|
147512
147566
|
const taskDispatchByCallID = /* @__PURE__ */ new Map();
|
|
147513
147567
|
const pendingTaskDispatches = [];
|
|
147514
147568
|
const knownNonTaskCallIDs = /* @__PURE__ */ new Set();
|
|
147569
|
+
function isSubagentInFlight() {
|
|
147570
|
+
return taskDispatchByCallID.size > 0 || pendingTaskDispatches.length > 0;
|
|
147571
|
+
}
|
|
147515
147572
|
function emitSubagentFinished(dispatch, status, output2, matchKind) {
|
|
147516
147573
|
const subagentDuration = performance7.now() - dispatch.startedAt;
|
|
147517
147574
|
const outputStr = typeof output2 === "string" ? output2 : "";
|
|
@@ -147764,6 +147821,20 @@ async function runOpenCode(params) {
|
|
|
147764
147821
|
activityTimeout: 3e5,
|
|
147765
147822
|
onActivityTimeout: params.onActivityTimeout,
|
|
147766
147823
|
stdio: ["ignore", "pipe", "pipe"],
|
|
147824
|
+
// node_modules/opencode-ai/bin/opencode is a Node shim that spawnSyncs
|
|
147825
|
+
// the native opencode-<plat>-<arch> binary with stdio:"inherit". without
|
|
147826
|
+
// a process-group kill, SIGKILL hits only the shim, the native binary
|
|
147827
|
+
// is reparented to PID 1, holds our stdout pipe open, and `child.close`
|
|
147828
|
+
// never fires — producing zombie runs. detached + killGroup nukes the
|
|
147829
|
+
// whole tree.
|
|
147830
|
+
killGroup: true,
|
|
147831
|
+
// suspend the inner activity timer while a `task` subagent is in flight.
|
|
147832
|
+
// opencode's task tool encapsulates subagent execution in-process — the
|
|
147833
|
+
// subagent's internal events don't surface on the parent NDJSON stream,
|
|
147834
|
+
// so without this the 5min timeout would falsely fire mid-subagent.
|
|
147835
|
+
// suspend/resume is preferable to a heartbeat because there's no race
|
|
147836
|
+
// between a periodic tick and a subagent finishing between ticks.
|
|
147837
|
+
isPausedExternally: isSubagentInFlight,
|
|
147767
147838
|
onStdout: async (chunk) => {
|
|
147768
147839
|
const text = chunk.toString();
|
|
147769
147840
|
output += text;
|
|
@@ -147935,6 +148006,7 @@ var opencode = agent({
|
|
|
147935
148006
|
...homeEnv,
|
|
147936
148007
|
OPENCODE_CONFIG_CONTENT: buildSecurityConfig(ctx, model),
|
|
147937
148008
|
OPENCODE_PERMISSION: permissionOverride,
|
|
148009
|
+
OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX: PULLFROG_OPENCODE_OUTPUT_LIMIT.toString(),
|
|
147938
148010
|
GOOGLE_GENERATIVE_AI_API_KEY: process.env.GOOGLE_GENERATIVE_AI_API_KEY || process.env.GEMINI_API_KEY
|
|
147939
148011
|
};
|
|
147940
148012
|
const repoDir = process.cwd();
|
|
@@ -153352,6 +153424,24 @@ function formatBillingErrorSummary(error49, owner) {
|
|
|
153352
153424
|
`[Add a card \u2192](${billingConsoleUrl(owner, "model-access")})`
|
|
153353
153425
|
].join("\n");
|
|
153354
153426
|
}
|
|
153427
|
+
if (error49.code === "router_balance_exhausted") {
|
|
153428
|
+
return [
|
|
153429
|
+
"**Your Pullfrog Router balance is exhausted.**",
|
|
153430
|
+
"",
|
|
153431
|
+
"You have a card on file but auto-reload is disabled, so runs paused once your balance went past the overdraft buffer.",
|
|
153432
|
+
"",
|
|
153433
|
+
`[Top up balance \u2192](${billingConsoleUrl(owner, "billing")}) \xB7 [Enable auto-reload \u2192](${billingConsoleUrl(owner, "model-access")})`
|
|
153434
|
+
].join("\n");
|
|
153435
|
+
}
|
|
153436
|
+
if (error49.code === "router_keylimit_exhausted") {
|
|
153437
|
+
return [
|
|
153438
|
+
"**This run was cut short \u2014 your Pullfrog Router balance ran out mid-run.**",
|
|
153439
|
+
"",
|
|
153440
|
+
"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.",
|
|
153441
|
+
"",
|
|
153442
|
+
`[Top up balance \u2192](${billingConsoleUrl(owner, "billing")}) \xB7 [Enable auto-reload \u2192](${billingConsoleUrl(owner, "model-access")})`
|
|
153443
|
+
].join("\n");
|
|
153444
|
+
}
|
|
153355
153445
|
if (error49.needsReauthentication) {
|
|
153356
153446
|
const code = error49.declineCode ?? "authentication_required";
|
|
153357
153447
|
return [
|
|
@@ -153592,6 +153682,7 @@ async function main() {
|
|
|
153592
153682
|
setGitAuthServer(gitAuthServer);
|
|
153593
153683
|
const resolvedModel = payload.proxyModel ? void 0 : resolveModel({ slug: payload.model });
|
|
153594
153684
|
const agent2 = resolveAgent({ model: resolvedModel });
|
|
153685
|
+
toolState.model = payload.proxyModel ?? resolvedModel ?? payload.model;
|
|
153595
153686
|
validateAgentApiKey({
|
|
153596
153687
|
agent: agent2,
|
|
153597
153688
|
model: payload.proxyModel ?? resolvedModel ?? payload.model,
|
|
@@ -153835,8 +153926,9 @@ ${instructions.user}` : null,
|
|
|
153835
153926
|
todoTracker?.cancel();
|
|
153836
153927
|
killTrackedChildren();
|
|
153837
153928
|
log.error(errorMessage);
|
|
153929
|
+
const billingError = isRouterKeylimitExhaustedError(errorMessage) ? new BillingError(errorMessage, { code: "router_keylimit_exhausted" }) : null;
|
|
153838
153930
|
try {
|
|
153839
|
-
const errorSummary = `### \u274C Pullfrog failed
|
|
153931
|
+
const errorSummary = billingError ? formatBillingErrorSummary(billingError, runContext.repo.owner) : `### \u274C Pullfrog failed
|
|
153840
153932
|
|
|
153841
153933
|
\`\`\`
|
|
153842
153934
|
${errorMessage}
|
|
@@ -153847,7 +153939,8 @@ ${errorMessage}
|
|
|
153847
153939
|
} catch {
|
|
153848
153940
|
}
|
|
153849
153941
|
try {
|
|
153850
|
-
|
|
153942
|
+
const commentBody = billingError ? formatBillingErrorSummary(billingError, runContext.repo.owner) : errorMessage;
|
|
153943
|
+
await reportErrorToComment({ toolState, error: commentBody });
|
|
153851
153944
|
} catch {
|
|
153852
153945
|
}
|
|
153853
153946
|
if (toolContext) {
|
package/dist/internal.js
CHANGED
|
@@ -805,7 +805,11 @@ var modes = computeModes("opencode");
|
|
|
805
805
|
var PULLFROG_DIVIDER = "<!-- PULLFROG_DIVIDER_DO_NOT_REMOVE_PLZ -->";
|
|
806
806
|
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>`;
|
|
807
807
|
function formatModelLabel(slug) {
|
|
808
|
-
const alias = resolveDisplayAlias(slug)
|
|
808
|
+
const alias = resolveDisplayAlias(slug) ?? // reverse-lookup: when the caller passes an effective model (proxy or
|
|
809
|
+
// resolved target like "openrouter/anthropic/claude-opus-4.7") instead of
|
|
810
|
+
// a stored alias slug, find the alias whose resolve target matches so we
|
|
811
|
+
// still render a friendly display name.
|
|
812
|
+
modelAliases.find((a) => a.resolve === slug || a.openRouterResolve === slug);
|
|
809
813
|
if (!alias) return `\`${slug}\``;
|
|
810
814
|
return alias.isFree ? `\`${alias.displayName}\` (free)` : `\`${alias.displayName}\``;
|
|
811
815
|
}
|
package/dist/mcp/review.d.ts
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import type { RestEndpointMethodTypes } from "@octokit/rest";
|
|
2
2
|
import type { ToolContext } from "./server.ts";
|
|
3
|
+
/**
|
|
4
|
+
* detect GitHub's generic server-side 422 ("An internal error occurred,
|
|
5
|
+
* please try again.") that sometimes fires on `POST /pulls/{n}/reviews`.
|
|
6
|
+
*
|
|
7
|
+
* the body is stable across occurrences and distinct from every other 422
|
|
8
|
+
* cause we care about (anchor validation, body length, malformed suggestion
|
|
9
|
+
* blocks) — those all cite the specific problem. treating this as a
|
|
10
|
+
* transient server error unlocks bounded in-tool retry instead of surfacing
|
|
11
|
+
* it to the agent with the generic "likely causes (1)(2)(3)" prompt, which
|
|
12
|
+
* induces whack-a-mole comment dropping on content that was never the issue.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isTransientReviewError(err: unknown): boolean;
|
|
15
|
+
export declare const TRANSIENT_REVIEW_RETRY_DELAYS_MS: number[];
|
|
3
16
|
export type CommentableLines = {
|
|
4
17
|
RIGHT: Set<number>;
|
|
5
18
|
LEFT: Set<number>;
|
|
@@ -5,7 +5,7 @@ import type { ToolContext } from "../mcp/server.ts";
|
|
|
5
5
|
* are created during the run. Strings only (GraphQL node IDs).
|
|
6
6
|
* Keep in sync with `STRING_FIELDS` in `app/api/workflow-run/[runId]/route.ts`.
|
|
7
7
|
*/
|
|
8
|
-
export type WorkflowRunArtifactPatchKey = "prNodeId" | "issueNodeId" | "reviewNodeId" | "planCommentNodeId" | "
|
|
8
|
+
export type WorkflowRunArtifactPatchKey = "prNodeId" | "issueNodeId" | "reviewNodeId" | "planCommentNodeId" | "summarySnapshot";
|
|
9
9
|
/**
|
|
10
10
|
* Usage fields — aggregated across all agent calls and PATCHed once at
|
|
11
11
|
* end-of-run. Token counts are Int4 on the DB side (ample for any realistic
|
package/dist/utils/retry.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
export type RetryOptions = {
|
|
2
2
|
maxAttempts?: number;
|
|
3
3
|
delayMs?: number;
|
|
4
|
+
/**
|
|
5
|
+
* explicit delay schedule — one entry per retry (length N ⇒ N+1 attempts).
|
|
6
|
+
* when set, overrides `maxAttempts` and `delayMs`. e.g. `[1_000, 3_000]`
|
|
7
|
+
* means up to 3 attempts, sleeping 1s before retry 2 and 3s before retry 3.
|
|
8
|
+
*/
|
|
9
|
+
delaysMs?: readonly number[];
|
|
4
10
|
shouldRetry?: (error: unknown) => boolean;
|
|
5
11
|
label?: string;
|
|
6
12
|
};
|
|
@@ -26,6 +26,8 @@ export interface SpawnOptions {
|
|
|
26
26
|
stdio?: ("pipe" | "ignore" | "inherit")[];
|
|
27
27
|
onStdout?: (chunk: string) => void;
|
|
28
28
|
onStderr?: (chunk: string) => void;
|
|
29
|
+
killGroup?: boolean;
|
|
30
|
+
isPausedExternally?: () => boolean;
|
|
29
31
|
}
|
|
30
32
|
export interface SpawnResult {
|
|
31
33
|
stdout: string;
|