pullfrog 0.1.5 → 0.1.7
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/postRun.d.ts +21 -0
- package/dist/agents/sessionLabeler.d.ts +38 -18
- package/dist/agents/subagentModels.d.ts +19 -0
- package/dist/cli.mjs +678 -278
- package/dist/index.js +662 -264
- package/dist/internal.js +151 -59
- package/dist/models.d.ts +63 -3
- package/dist/utils/agent.d.ts +5 -2
- package/dist/utils/apiKeys.d.ts +18 -0
- package/dist/utils/instructions.d.ts +19 -0
- package/dist/utils/learnings.d.ts +20 -9
- package/dist/utils/normalizeEnv.d.ts +21 -1
- package/dist/utils/runContext.d.ts +16 -0
- package/dist/utils/subprocess.d.ts +40 -0
- package/dist/utils/timer.d.ts +11 -0
- package/package.json +1 -1
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 setSecret4(secret) {
|
|
19722
19722
|
(0, command_1.issueCommand)("add-mask", {}, secret);
|
|
19723
19723
|
}
|
|
19724
|
-
exports.setSecret =
|
|
19724
|
+
exports.setSecret = setSecret4;
|
|
19725
19725
|
function addPath(inputPath) {
|
|
19726
19726
|
const filePath = process.env["GITHUB_PATH"] || "";
|
|
19727
19727
|
if (filePath) {
|
|
@@ -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 core8 = [
|
|
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 = core8;
|
|
47751
47751
|
}
|
|
47752
47752
|
});
|
|
47753
47753
|
|
|
@@ -98924,7 +98924,7 @@ var require_fast_content_type_parse = __commonJS({
|
|
|
98924
98924
|
});
|
|
98925
98925
|
|
|
98926
98926
|
// main.ts
|
|
98927
|
-
var
|
|
98927
|
+
var core7 = __toESM(require_core(), 1);
|
|
98928
98928
|
import { existsSync as existsSync7, readdirSync } from "node:fs";
|
|
98929
98929
|
import { readFile as readFile4 } from "node:fs/promises";
|
|
98930
98930
|
import { join as join17 } from "node:path";
|
|
@@ -107744,7 +107744,8 @@ var providers = {
|
|
|
107744
107744
|
displayName: "Claude Opus",
|
|
107745
107745
|
resolve: "anthropic/claude-opus-4-7",
|
|
107746
107746
|
openRouterResolve: "openrouter/anthropic/claude-opus-4.7",
|
|
107747
|
-
preferred: true
|
|
107747
|
+
preferred: true,
|
|
107748
|
+
subagentModel: "claude-sonnet"
|
|
107748
107749
|
},
|
|
107749
107750
|
"claude-sonnet": {
|
|
107750
107751
|
displayName: "Claude Sonnet",
|
|
@@ -107766,12 +107767,23 @@ var providers = {
|
|
|
107766
107767
|
displayName: "GPT",
|
|
107767
107768
|
resolve: "openai/gpt-5.5",
|
|
107768
107769
|
openRouterResolve: "openrouter/openai/gpt-5.5",
|
|
107769
|
-
preferred: true
|
|
107770
|
+
preferred: true,
|
|
107771
|
+
subagentModel: "gpt-5.4"
|
|
107770
107772
|
},
|
|
107771
107773
|
"gpt-pro": {
|
|
107772
107774
|
displayName: "GPT Pro",
|
|
107773
107775
|
resolve: "openai/gpt-5.5-pro",
|
|
107774
|
-
openRouterResolve: "openrouter/openai/gpt-5.5-pro"
|
|
107776
|
+
openRouterResolve: "openrouter/openai/gpt-5.5-pro",
|
|
107777
|
+
subagentModel: "gpt"
|
|
107778
|
+
},
|
|
107779
|
+
// hidden subagent target — `gpt` lenses run against this. surfacing
|
|
107780
|
+
// it in the picker would just confuse users (it's the prior-flagship,
|
|
107781
|
+
// and they already have `gpt` and `gpt-mini` to choose from).
|
|
107782
|
+
"gpt-5.4": {
|
|
107783
|
+
displayName: "GPT 5.4",
|
|
107784
|
+
resolve: "openai/gpt-5.4",
|
|
107785
|
+
openRouterResolve: "openrouter/openai/gpt-5.4",
|
|
107786
|
+
hidden: true
|
|
107775
107787
|
},
|
|
107776
107788
|
"gpt-mini": {
|
|
107777
107789
|
displayName: "GPT Mini",
|
|
@@ -107809,7 +107821,8 @@ var providers = {
|
|
|
107809
107821
|
displayName: "Gemini Pro",
|
|
107810
107822
|
resolve: "google/gemini-3.1-pro-preview",
|
|
107811
107823
|
openRouterResolve: "openrouter/google/gemini-3.1-pro-preview",
|
|
107812
|
-
preferred: true
|
|
107824
|
+
preferred: true,
|
|
107825
|
+
subagentModel: "gemini-flash"
|
|
107813
107826
|
},
|
|
107814
107827
|
"gemini-flash": {
|
|
107815
107828
|
displayName: "Gemini Flash",
|
|
@@ -107897,7 +107910,8 @@ var providers = {
|
|
|
107897
107910
|
"claude-opus": {
|
|
107898
107911
|
displayName: "Claude Opus",
|
|
107899
107912
|
resolve: "opencode/claude-opus-4-7",
|
|
107900
|
-
openRouterResolve: "openrouter/anthropic/claude-opus-4.7"
|
|
107913
|
+
openRouterResolve: "openrouter/anthropic/claude-opus-4.7",
|
|
107914
|
+
subagentModel: "claude-sonnet"
|
|
107901
107915
|
},
|
|
107902
107916
|
"claude-sonnet": {
|
|
107903
107917
|
displayName: "Claude Sonnet",
|
|
@@ -107912,12 +107926,21 @@ var providers = {
|
|
|
107912
107926
|
gpt: {
|
|
107913
107927
|
displayName: "GPT",
|
|
107914
107928
|
resolve: "opencode/gpt-5.5",
|
|
107915
|
-
openRouterResolve: "openrouter/openai/gpt-5.5"
|
|
107929
|
+
openRouterResolve: "openrouter/openai/gpt-5.5",
|
|
107930
|
+
subagentModel: "gpt-5.4"
|
|
107916
107931
|
},
|
|
107917
107932
|
"gpt-pro": {
|
|
107918
107933
|
displayName: "GPT Pro",
|
|
107919
107934
|
resolve: "opencode/gpt-5.5-pro",
|
|
107920
|
-
openRouterResolve: "openrouter/openai/gpt-5.5-pro"
|
|
107935
|
+
openRouterResolve: "openrouter/openai/gpt-5.5-pro",
|
|
107936
|
+
subagentModel: "gpt"
|
|
107937
|
+
},
|
|
107938
|
+
// hidden subagent target — see openai provider above for context.
|
|
107939
|
+
"gpt-5.4": {
|
|
107940
|
+
displayName: "GPT 5.4",
|
|
107941
|
+
resolve: "opencode/gpt-5.4",
|
|
107942
|
+
openRouterResolve: "openrouter/openai/gpt-5.4",
|
|
107943
|
+
hidden: true
|
|
107921
107944
|
},
|
|
107922
107945
|
"gpt-mini": {
|
|
107923
107946
|
displayName: "GPT Mini",
|
|
@@ -107940,7 +107963,8 @@ var providers = {
|
|
|
107940
107963
|
"gemini-pro": {
|
|
107941
107964
|
displayName: "Gemini Pro",
|
|
107942
107965
|
resolve: "opencode/gemini-3.1-pro",
|
|
107943
|
-
openRouterResolve: "openrouter/google/gemini-3.1-pro-preview"
|
|
107966
|
+
openRouterResolve: "openrouter/google/gemini-3.1-pro-preview",
|
|
107967
|
+
subagentModel: "gemini-flash"
|
|
107944
107968
|
},
|
|
107945
107969
|
"gemini-flash": {
|
|
107946
107970
|
displayName: "Gemini Flash",
|
|
@@ -107972,6 +107996,20 @@ var providers = {
|
|
|
107972
107996
|
}
|
|
107973
107997
|
}
|
|
107974
107998
|
}),
|
|
107999
|
+
bedrock: provider({
|
|
108000
|
+
displayName: "Amazon Bedrock",
|
|
108001
|
+
envVars: ["AWS_BEARER_TOKEN_BEDROCK", "AWS_REGION", "BEDROCK_MODEL_ID"],
|
|
108002
|
+
models: {
|
|
108003
|
+
// single routing entry — the actual Bedrock model ID is read from
|
|
108004
|
+
// BEDROCK_MODEL_ID at run time. see ModelRouting docs for why we
|
|
108005
|
+
// don't catalog individual Bedrock models.
|
|
108006
|
+
byok: {
|
|
108007
|
+
displayName: "Amazon Bedrock",
|
|
108008
|
+
resolve: "bedrock",
|
|
108009
|
+
routing: "bedrock"
|
|
108010
|
+
}
|
|
108011
|
+
}
|
|
108012
|
+
}),
|
|
107975
108013
|
openrouter: provider({
|
|
107976
108014
|
displayName: "OpenRouter",
|
|
107977
108015
|
envVars: ["OPENROUTER_API_KEY"],
|
|
@@ -107980,7 +108018,8 @@ var providers = {
|
|
|
107980
108018
|
displayName: "Claude Opus",
|
|
107981
108019
|
resolve: "openrouter/anthropic/claude-opus-4.7",
|
|
107982
108020
|
openRouterResolve: "openrouter/anthropic/claude-opus-4.7",
|
|
107983
|
-
preferred: true
|
|
108021
|
+
preferred: true,
|
|
108022
|
+
subagentModel: "claude-sonnet"
|
|
107984
108023
|
},
|
|
107985
108024
|
"claude-sonnet": {
|
|
107986
108025
|
displayName: "Claude Sonnet",
|
|
@@ -107995,12 +108034,21 @@ var providers = {
|
|
|
107995
108034
|
gpt: {
|
|
107996
108035
|
displayName: "GPT",
|
|
107997
108036
|
resolve: "openrouter/openai/gpt-5.5",
|
|
107998
|
-
openRouterResolve: "openrouter/openai/gpt-5.5"
|
|
108037
|
+
openRouterResolve: "openrouter/openai/gpt-5.5",
|
|
108038
|
+
subagentModel: "gpt-5.4"
|
|
107999
108039
|
},
|
|
108000
108040
|
"gpt-pro": {
|
|
108001
108041
|
displayName: "GPT Pro",
|
|
108002
108042
|
resolve: "openrouter/openai/gpt-5.5-pro",
|
|
108003
|
-
openRouterResolve: "openrouter/openai/gpt-5.5-pro"
|
|
108043
|
+
openRouterResolve: "openrouter/openai/gpt-5.5-pro",
|
|
108044
|
+
subagentModel: "gpt"
|
|
108045
|
+
},
|
|
108046
|
+
// hidden subagent target — see openai provider above for context.
|
|
108047
|
+
"gpt-5.4": {
|
|
108048
|
+
displayName: "GPT 5.4",
|
|
108049
|
+
resolve: "openrouter/openai/gpt-5.4",
|
|
108050
|
+
openRouterResolve: "openrouter/openai/gpt-5.4",
|
|
108051
|
+
hidden: true
|
|
108004
108052
|
},
|
|
108005
108053
|
"gpt-mini": {
|
|
108006
108054
|
displayName: "GPT Mini",
|
|
@@ -108028,7 +108076,8 @@ var providers = {
|
|
|
108028
108076
|
"gemini-pro": {
|
|
108029
108077
|
displayName: "Gemini Pro",
|
|
108030
108078
|
resolve: "openrouter/google/gemini-3.1-pro-preview",
|
|
108031
|
-
openRouterResolve: "openrouter/google/gemini-3.1-pro-preview"
|
|
108079
|
+
openRouterResolve: "openrouter/google/gemini-3.1-pro-preview",
|
|
108080
|
+
subagentModel: "gemini-flash"
|
|
108032
108081
|
},
|
|
108033
108082
|
"gemini-flash": {
|
|
108034
108083
|
displayName: "Gemini Flash",
|
|
@@ -108097,7 +108146,13 @@ var modelAliases = Object.entries(providers).flatMap(
|
|
|
108097
108146
|
openRouterResolve: def.openRouterResolve,
|
|
108098
108147
|
preferred: def.preferred ?? false,
|
|
108099
108148
|
isFree: def.isFree ?? false,
|
|
108100
|
-
fallback: def.fallback
|
|
108149
|
+
fallback: def.fallback,
|
|
108150
|
+
routing: def.routing,
|
|
108151
|
+
// subagentModel is stored as an alias key local to the provider; expand
|
|
108152
|
+
// here to a fully-qualified slug so callers can look up the target alias
|
|
108153
|
+
// directly without re-deriving the provider.
|
|
108154
|
+
subagentModel: def.subagentModel ? `${providerKey}/${def.subagentModel}` : void 0,
|
|
108155
|
+
hidden: def.hidden ?? false
|
|
108101
108156
|
}))
|
|
108102
108157
|
);
|
|
108103
108158
|
var MAX_FALLBACK_DEPTH = 10;
|
|
@@ -108117,6 +108172,10 @@ function resolveDisplayAlias(slug2) {
|
|
|
108117
108172
|
function resolveCliModel(slug2) {
|
|
108118
108173
|
return resolveDisplayAlias(slug2)?.resolve;
|
|
108119
108174
|
}
|
|
108175
|
+
var BEDROCK_MODEL_ID_ENV = "BEDROCK_MODEL_ID";
|
|
108176
|
+
function isBedrockAnthropicId(bedrockModelId) {
|
|
108177
|
+
return bedrockModelId.toLowerCase().split(/[./:]/).includes("anthropic");
|
|
108178
|
+
}
|
|
108120
108179
|
|
|
108121
108180
|
// utils/buildPullfrogFooter.ts
|
|
108122
108181
|
var PULLFROG_DIVIDER = "<!-- PULLFROG_DIVIDER_DO_NOT_REMOVE_PLZ -->";
|
|
@@ -108964,7 +109023,7 @@ var Comment = type({
|
|
|
108964
109023
|
function CreateCommentTool(ctx) {
|
|
108965
109024
|
return tool({
|
|
108966
109025
|
name: "create_issue_comment",
|
|
108967
|
-
description: "Create a comment on a GitHub issue or PR. For progress/plan updates on the current run use report_progress instead. Use type: 'Plan' for plan comments.",
|
|
109026
|
+
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. Use type: 'Plan' for plan comments.",
|
|
108968
109027
|
parameters: Comment,
|
|
108969
109028
|
execute: execute(async ({ issueNumber, body, type: commentType }) => {
|
|
108970
109029
|
const bodyWithFooter = addFooter(ctx, body);
|
|
@@ -109132,7 +109191,7 @@ async function reportProgress(ctx, params) {
|
|
|
109132
109191
|
function ReportProgressTool(ctx) {
|
|
109133
109192
|
return tool({
|
|
109134
109193
|
name: "report_progress",
|
|
109135
|
-
description:
|
|
109194
|
+
description: 'Share progress on the associated GitHub issue/PR. The first call creates a comment; subsequent calls update it in place. Example: `report_progress({ body: "Implemented the auth check and added tests." })`. Call this at the end of every run with a brief final summary (1-3 sentences) unless the mode guidance instructs otherwise. The current task list is automatically appended in a collapsible section \u2014 do not restate individual steps.',
|
|
109136
109195
|
parameters: ReportProgress,
|
|
109137
109196
|
execute: execute(async (params) => {
|
|
109138
109197
|
let body = params.body;
|
|
@@ -109212,7 +109271,7 @@ function duplicateReplyDecision(params) {
|
|
|
109212
109271
|
function ReplyToReviewCommentTool(ctx) {
|
|
109213
109272
|
return tool({
|
|
109214
109273
|
name: "reply_to_review_comment",
|
|
109215
|
-
description:
|
|
109274
|
+
description: 'Reply to a PR review comment thread (NOT issue comments \u2014 this only works for inline review comments on PR diffs). Example: `reply_to_review_comment({ pull_number: 1234, comment_id: 567890, body: "Fixed by adding a null check." })`. Call exactly ONCE per parent comment you address in AddressReviews mode \u2014 duplicate calls with the same body are a no-op. Keep replies extremely brief (1 sentence max).',
|
|
109216
109275
|
parameters: ReplyToReviewComment,
|
|
109217
109276
|
execute: execute(async ({ pull_number, comment_id, body }) => {
|
|
109218
109277
|
const bodyWithFooter = addFooter(ctx, body);
|
|
@@ -109742,12 +109801,41 @@ function installSignalHandler() {
|
|
|
109742
109801
|
killTrackedChildren();
|
|
109743
109802
|
});
|
|
109744
109803
|
}
|
|
109804
|
+
var DEFAULT_MAX_RETAINED_BYTES = 8 * 1024 * 1024;
|
|
109805
|
+
var TailBuffer = class {
|
|
109806
|
+
// explicit field declarations rather than constructor parameter properties:
|
|
109807
|
+
// node's strip-only TS loader (used by action/test/run.ts in CI) rejects
|
|
109808
|
+
// `constructor(private readonly cap: number)` with ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX.
|
|
109809
|
+
cap;
|
|
109810
|
+
buffer = "";
|
|
109811
|
+
truncatedBytes = 0;
|
|
109812
|
+
constructor(cap) {
|
|
109813
|
+
this.cap = cap;
|
|
109814
|
+
}
|
|
109815
|
+
append(chunk) {
|
|
109816
|
+
if (this.cap <= 0) return;
|
|
109817
|
+
this.buffer += chunk;
|
|
109818
|
+
if (this.buffer.length > this.cap) {
|
|
109819
|
+
const drop = this.buffer.length - this.cap;
|
|
109820
|
+
this.truncatedBytes += drop;
|
|
109821
|
+
this.buffer = this.buffer.slice(drop);
|
|
109822
|
+
}
|
|
109823
|
+
}
|
|
109824
|
+
toString() {
|
|
109825
|
+
if (this.truncatedBytes === 0) return this.buffer;
|
|
109826
|
+
const mib = (this.truncatedBytes / 1024 / 1024).toFixed(1);
|
|
109827
|
+
return `... [${mib} MiB truncated by retain:tail cap] ...
|
|
109828
|
+
${this.buffer}`;
|
|
109829
|
+
}
|
|
109830
|
+
};
|
|
109745
109831
|
async function spawn(options) {
|
|
109746
109832
|
const activityTimeoutMs = options.activityTimeout ?? DEFAULT_ACTIVITY_TIMEOUT_MS;
|
|
109747
109833
|
installSignalHandler();
|
|
109748
109834
|
const startTime = performance3.now();
|
|
109749
|
-
|
|
109750
|
-
|
|
109835
|
+
const retain = options.retain ?? "tail";
|
|
109836
|
+
const cap = options.maxRetainedBytes ?? DEFAULT_MAX_RETAINED_BYTES;
|
|
109837
|
+
const stdoutBuffer = retain === "none" ? null : new TailBuffer(cap);
|
|
109838
|
+
const stderrBuffer = retain === "none" ? null : new TailBuffer(cap);
|
|
109751
109839
|
const killGroup = options.killGroup ?? false;
|
|
109752
109840
|
return new Promise((resolve3, reject) => {
|
|
109753
109841
|
const child = nodeSpawn(options.cmd, options.args, {
|
|
@@ -109821,17 +109909,29 @@ async function spawn(options) {
|
|
|
109821
109909
|
}
|
|
109822
109910
|
if (child.stdout) {
|
|
109823
109911
|
child.stdout.on("data", (data) => {
|
|
109824
|
-
|
|
109825
|
-
|
|
109826
|
-
|
|
109827
|
-
|
|
109912
|
+
try {
|
|
109913
|
+
updateActivity();
|
|
109914
|
+
const chunk = data.toString();
|
|
109915
|
+
stdoutBuffer?.append(chunk);
|
|
109916
|
+
options.onStdout?.(chunk);
|
|
109917
|
+
} catch (err) {
|
|
109918
|
+
log.debug(
|
|
109919
|
+
`spawn stdout handler threw: ${err instanceof Error ? err.message : String(err)}`
|
|
109920
|
+
);
|
|
109921
|
+
}
|
|
109828
109922
|
});
|
|
109829
109923
|
}
|
|
109830
109924
|
if (child.stderr) {
|
|
109831
109925
|
child.stderr.on("data", (data) => {
|
|
109832
|
-
|
|
109833
|
-
|
|
109834
|
-
|
|
109926
|
+
try {
|
|
109927
|
+
const chunk = data.toString();
|
|
109928
|
+
stderrBuffer?.append(chunk);
|
|
109929
|
+
options.onStderr?.(chunk);
|
|
109930
|
+
} catch (err) {
|
|
109931
|
+
log.debug(
|
|
109932
|
+
`spawn stderr handler threw: ${err instanceof Error ? err.message : String(err)}`
|
|
109933
|
+
);
|
|
109934
|
+
}
|
|
109835
109935
|
});
|
|
109836
109936
|
}
|
|
109837
109937
|
child.on("close", (exitCode, signal) => {
|
|
@@ -109858,7 +109958,7 @@ async function spawn(options) {
|
|
|
109858
109958
|
return;
|
|
109859
109959
|
}
|
|
109860
109960
|
let resolvedExitCode = exitCode ?? 0;
|
|
109861
|
-
let resolvedStderr = stderrBuffer;
|
|
109961
|
+
let resolvedStderr = stderrBuffer?.toString() ?? "";
|
|
109862
109962
|
if (exitCode === null && signal) {
|
|
109863
109963
|
const killMsg = `[spawn] ${options.cmd}: killed by signal ${signal}`;
|
|
109864
109964
|
resolvedStderr = resolvedStderr ? `${resolvedStderr}
|
|
@@ -109866,7 +109966,7 @@ ${killMsg}` : killMsg;
|
|
|
109866
109966
|
resolvedExitCode = 1;
|
|
109867
109967
|
}
|
|
109868
109968
|
resolve3({
|
|
109869
|
-
stdout: stdoutBuffer,
|
|
109969
|
+
stdout: stdoutBuffer?.toString() ?? "",
|
|
109870
109970
|
stderr: resolvedStderr,
|
|
109871
109971
|
exitCode: resolvedExitCode,
|
|
109872
109972
|
durationMs
|
|
@@ -109880,11 +109980,12 @@ ${killMsg}` : killMsg;
|
|
|
109880
109980
|
if (activityCheckIntervalId) clearInterval(activityCheckIntervalId);
|
|
109881
109981
|
const errMsg = `[spawn] ${options.cmd}: ${error49.message}`;
|
|
109882
109982
|
console.error(errMsg);
|
|
109883
|
-
|
|
109983
|
+
const existingStderr = stderrBuffer?.toString() ?? "";
|
|
109984
|
+
const finalStderr = existingStderr ? `${existingStderr}
|
|
109884
109985
|
${errMsg}` : errMsg;
|
|
109885
109986
|
resolve3({
|
|
109886
|
-
stdout: stdoutBuffer,
|
|
109887
|
-
stderr:
|
|
109987
|
+
stdout: stdoutBuffer?.toString() ?? "",
|
|
109988
|
+
stderr: finalStderr,
|
|
109888
109989
|
exitCode: 1,
|
|
109889
109990
|
durationMs
|
|
109890
109991
|
});
|
|
@@ -137793,7 +137894,7 @@ var require_core4 = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
137793
137894
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
137794
137895
|
const id_1 = require_id2();
|
|
137795
137896
|
const ref_1 = require_ref2();
|
|
137796
|
-
const
|
|
137897
|
+
const core8 = [
|
|
137797
137898
|
"$schema",
|
|
137798
137899
|
"$id",
|
|
137799
137900
|
"$defs",
|
|
@@ -137803,7 +137904,7 @@ var require_core4 = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
137803
137904
|
id_1.default,
|
|
137804
137905
|
ref_1.default
|
|
137805
137906
|
];
|
|
137806
|
-
exports.default =
|
|
137907
|
+
exports.default = core8;
|
|
137807
137908
|
}));
|
|
137808
137909
|
var require_limitNumber2 = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
137809
137910
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -142313,7 +142414,7 @@ var import_semver = __toESM(require_semver2(), 1);
|
|
|
142313
142414
|
// package.json
|
|
142314
142415
|
var package_default = {
|
|
142315
142416
|
name: "pullfrog",
|
|
142316
|
-
version: "0.1.
|
|
142417
|
+
version: "0.1.7",
|
|
142317
142418
|
type: "module",
|
|
142318
142419
|
bin: {
|
|
142319
142420
|
pullfrog: "dist/cli.mjs",
|
|
@@ -143169,7 +143270,7 @@ function PushBranchTool(ctx) {
|
|
|
143169
143270
|
const pushPermission = ctx.payload.push;
|
|
143170
143271
|
return tool({
|
|
143171
143272
|
name: "push_branch",
|
|
143172
|
-
description: "Push the current branch to the remote repository. Omit branchName to push the current branch (recommended). 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) before the network push \u2014 hook failure means tests/lint or similar in that script failed, not necessarily a Pullfrog timeout. Never force push unless explicitly requested. Pushes to the default branch are blocked in restricted mode.",
|
|
143273
|
+
description: "Push the current branch to the remote repository. Omit branchName to push the current branch (recommended). Example: `push_branch({})` to push the current branch. Example: `push_branch({ branchName: \"pr-1\" })` to push a specific local branch. If specifying branchName, use the LOCAL branch name (e.g., 'pr-1'), not the remote branch name. The correct remote and remote branch are determined automatically from branch config set by checkout_pr. Requires a clean working tree. Runs the repository prepush hook (if configured) before the network push \u2014 hook failure means tests/lint or similar in that script failed, not necessarily a Pullfrog timeout. 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.",
|
|
143173
143274
|
parameters: PushBranch,
|
|
143174
143275
|
execute: execute(async ({ branchName, force }) => {
|
|
143175
143276
|
if (pushPermission === "disabled") {
|
|
@@ -143308,7 +143409,7 @@ var Git = type({
|
|
|
143308
143409
|
function GitTool(ctx) {
|
|
143309
143410
|
return tool({
|
|
143310
143411
|
name: "git",
|
|
143311
|
-
description:
|
|
143412
|
+
description: 'Run a git subcommand. `command` is a single subcommand; flags and positional args go in `args`. Example: `git({ command: "log", args: ["--oneline", "-n", "20"] })`. Example: `git({ command: "diff", args: ["origin/main..HEAD"] })`. For push/fetch, use the dedicated MCP tools (push_branch, git_fetch). git pull is not available \u2014 use git_fetch then this tool with command \'merge\'.',
|
|
143312
143413
|
parameters: Git,
|
|
143313
143414
|
execute: execute(async (params) => {
|
|
143314
143415
|
const command = params.command;
|
|
@@ -143358,7 +143459,7 @@ var DEEPEN_RETRY_DEPTH = 1e3;
|
|
|
143358
143459
|
function GitFetchTool(ctx) {
|
|
143359
143460
|
return tool({
|
|
143360
143461
|
name: "git_fetch",
|
|
143361
|
-
description:
|
|
143462
|
+
description: 'Fetch refs from remote repository. Use this instead of git fetch directly. Example: `git_fetch({ ref: "main" })`. With depth: `git_fetch({ ref: "pull/1234/head", depth: 1 })`.',
|
|
143362
143463
|
parameters: GitFetch,
|
|
143363
143464
|
execute: execute(async (params) => {
|
|
143364
143465
|
rejectIfLeadingDash(params.ref, "ref");
|
|
@@ -143592,13 +143693,15 @@ var CreatePullRequestReview = type({
|
|
|
143592
143693
|
approved: type.boolean.describe(
|
|
143593
143694
|
"Set to true to submit as an approval. Use for both 'no issues found' and informational `> [!NOTE]` 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 `> [!IMPORTANT]` (recommended changes) and `> [!CAUTION]` (critical) reviews. Defaults to false (comment-only review). Rejections are not supported."
|
|
143594
143695
|
).optional(),
|
|
143595
|
-
commit_id: type.string.describe(
|
|
143696
|
+
commit_id: type.string.describe(
|
|
143697
|
+
"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."
|
|
143698
|
+
).optional(),
|
|
143596
143699
|
comments: type({
|
|
143597
143700
|
path: type.string.describe(
|
|
143598
143701
|
"The file path to comment on (relative to repo root). Must be a file that appears in the PR diff."
|
|
143599
143702
|
),
|
|
143600
143703
|
line: type.number.describe(
|
|
143601
|
-
"Line number to comment on. For multi-line ranges, this is the end line. Use NEW column from diff format."
|
|
143704
|
+
"Line number to comment on. For multi-line ranges, this is the end line. Use NEW column from diff format. Must sit inside a `@@` hunk in the PR diff \u2014 anchors on context-only or untouched lines are dropped silently (the rest of the review still posts; dropped entries are reported under `droppedComments` in the response)."
|
|
143602
143705
|
),
|
|
143603
143706
|
side: type.enumerated("LEFT", "RIGHT").describe(
|
|
143604
143707
|
"Side of the diff: LEFT (old code, lines starting with -) or RIGHT (new code, lines starting with + or unchanged). Defaults to RIGHT."
|
|
@@ -143608,7 +143711,7 @@ var CreatePullRequestReview = type({
|
|
|
143608
143711
|
"Full replacement code for the line range [start_line, line]. MUST preserve the exact indentation of the original code."
|
|
143609
143712
|
).optional(),
|
|
143610
143713
|
start_line: type.number.describe(
|
|
143611
|
-
"Start line for multi-line comment ranges. Omit for single-line comments. The range [start_line, line] defines which lines a suggestion replaces."
|
|
143714
|
+
"Start line for multi-line comment ranges. Omit for single-line comments. The range [start_line, line] defines which lines a suggestion replaces. Both `start_line` and `line` must sit inside the same `@@` hunk \u2014 a `start_line` outside the hunk causes the whole comment to be dropped even when `line` is valid. If you need to comment on context just above/below a hunk, shrink the range to a single line that is provably modified."
|
|
143612
143715
|
).optional()
|
|
143613
143716
|
}).array().describe(
|
|
143614
143717
|
"Inline comments on lines within diff hunks. Feedback about code outside the diff goes in 'body' instead."
|
|
@@ -143617,7 +143720,7 @@ var CreatePullRequestReview = type({
|
|
|
143617
143720
|
function CreatePullRequestReviewTool(ctx) {
|
|
143618
143721
|
return tool({
|
|
143619
143722
|
name: "create_pull_request_review",
|
|
143620
|
-
description: `Submit a review for an existing pull request. Each call creates a permanent, visible review on the PR \u2014 NEVER submit test or diagnostic reviews. Reviews with no body AND no comments are silently skipped (nothing to post). IMPORTANT: 95%+ of feedback should be in 'comments' array with file paths and line numbers. Only use 'body' for a 1-2 sentence summary with urgency and critical callouts. Use 'suggestion' to propose replacement code - MUST preserve exact indentation of original code. The first submission may error once with a one-time diff-coverage nudge listing unread TOC regions \u2014 retry with the same arguments and the pre-flight will not block again. Example replacing lines 42-44 (3 lines) with 5 lines: { path: 'src/api.ts', start_line: 42, line: 44, suggestion: ' const result = await fetch(url);\\n if (!result.ok) {\\n log.error(result.status);\\n throw new Error("request failed");\\n }' } CONSTRAINT: Inline comments can ONLY target files and lines that appear in the PR diff. Comments anchored outside a diff hunk are dropped automatically (with a note appended to the review body) \u2014 the rest of the review still posts.`,
|
|
143723
|
+
description: `Submit a review for an existing pull request. Example: \`create_pull_request_review({ pull_number: 1234, body: "LGTM", approved: true, comments: [{ path: "src/api.ts", line: 42, body: "nit: rename" }] })\`. Each call creates a permanent, visible review on the PR \u2014 NEVER submit test or diagnostic reviews. Reviews with no body AND no comments are silently skipped (nothing to post). IMPORTANT: 95%+ of feedback should be in 'comments' array with file paths and line numbers. Only use 'body' for a 1-2 sentence summary with urgency and critical callouts. Use 'suggestion' to propose replacement code - MUST preserve exact indentation of original code. The first submission may error once with a one-time diff-coverage nudge listing unread TOC regions \u2014 retry with the same arguments and the pre-flight will not block again. Example replacing lines 42-44 (3 lines) with 5 lines: { path: 'src/api.ts', start_line: 42, line: 44, suggestion: ' const result = await fetch(url);\\n if (!result.ok) {\\n log.error(result.status);\\n throw new Error("request failed");\\n }' } CONSTRAINT: Inline comments can ONLY target files and lines that appear in the PR diff. Comments anchored outside a diff hunk are dropped automatically (with a note appended to the review body) \u2014 the rest of the review still posts.`,
|
|
143621
143724
|
parameters: CreatePullRequestReview,
|
|
143622
143725
|
execute: execute(async ({ pull_number, body, approved, commit_id, comments = [] }) => {
|
|
143623
143726
|
if (body) body = fixDoubleEscapedString(body);
|
|
@@ -143846,7 +143949,7 @@ function runDiffCoveragePreflight(params) {
|
|
|
143846
143949
|
);
|
|
143847
143950
|
const unreadText = unread.map((entry) => `- ${entry.path} (${entry.unreadLines} lines, ${entry.ranges})`).join("\n");
|
|
143848
143951
|
throw new Error(
|
|
143849
|
-
`diff coverage pre-flight: some TOC regions were not read before review submission. this is a one-time nudge \u2014
|
|
143952
|
+
`diff coverage pre-flight: some TOC regions were not read before review submission. this is a one-time nudge \u2014 read the ranges below from ${coverageState.diffPath} on a best-effort basis, then call create_pull_request_review again. you are NOT obligated to read generated artifacts (lockfiles like pnpm-lock.yaml / package-lock.json / yarn.lock / Cargo.lock; codegen output like *.gen.*, *.pb.go, *.generated.*; snapshot/fixture dirs like __snapshots__/; migration metadata like drizzle/meta/, prisma migration SQL). if every unread region is generated, retry immediately without reading. this pre-flight will not block again in this review session.
|
|
143850
143953
|
|
|
143851
143954
|
unread TOC regions:
|
|
143852
143955
|
${unreadText}
|
|
@@ -144293,7 +144396,7 @@ async function checkoutPrBranch(pr, params) {
|
|
|
144293
144396
|
function CheckoutPrTool(ctx) {
|
|
144294
144397
|
return tool({
|
|
144295
144398
|
name: "checkout_pr",
|
|
144296
|
-
description: "Checkout a pull request branch locally. This fetches the PR branch and sets up push configuration for fork PRs. Returns diffPath pointing to the formatted diff file.",
|
|
144399
|
+
description: "Checkout a pull request branch locally. This fetches the PR branch and sets up push configuration for fork PRs. Returns diffPath pointing to the formatted diff file. Example: `checkout_pr({ pull_number: 1234 })`. Transient fetch timeouts are common \u2014 retry the same call up to a few times before treating the failure as terminal. If the error mentions `.git/shallow.lock: File exists` or `.git/index.lock: File exists`, that's a stale lock from a prior timed-out fetch \u2014 remove it via the shell tool (`rm -f .git/shallow.lock .git/index.lock`) and retry.",
|
|
144297
144400
|
parameters: CheckoutPr,
|
|
144298
144401
|
execute: execute(async ({ pull_number }) => {
|
|
144299
144402
|
const prResponse = await ctx.octokit.rest.pulls.get({
|
|
@@ -144604,7 +144707,7 @@ var CommitInfo = type({
|
|
|
144604
144707
|
function CommitInfoTool(ctx) {
|
|
144605
144708
|
return tool({
|
|
144606
144709
|
name: "get_commit_info",
|
|
144607
|
-
description:
|
|
144710
|
+
description: 'Retrieve commit metadata and diff via GitHub API. Use this instead of git show for reviewing commits - it works with shallow clones and shows the actual changes in the commit. Returns diffPath pointing to formatted diff file. Example: `get_commit_info({ sha: "2a6ab5d" })`.',
|
|
144608
144711
|
parameters: CommitInfo,
|
|
144609
144712
|
execute: execute(async ({ sha }) => {
|
|
144610
144713
|
const response = await ctx.octokit.rest.repos.getCommit({
|
|
@@ -144695,7 +144798,7 @@ var GetIssueComments = type({
|
|
|
144695
144798
|
function GetIssueCommentsTool(ctx) {
|
|
144696
144799
|
return tool({
|
|
144697
144800
|
name: "get_issue_comments",
|
|
144698
|
-
description: "Get all comments for a GitHub issue. Returns all comments including the issue body and all subsequent discussion comments.",
|
|
144801
|
+
description: "Get all comments for a GitHub issue. Returns all comments including the issue body and all subsequent discussion comments. Example: `get_issue_comments({ issue_number: 1234 })`.",
|
|
144699
144802
|
parameters: GetIssueComments,
|
|
144700
144803
|
execute: execute(async ({ issue_number }) => {
|
|
144701
144804
|
ctx.toolState.issueNumber = issue_number;
|
|
@@ -144796,7 +144899,7 @@ var IssueInfo = type({
|
|
|
144796
144899
|
function IssueInfoTool(ctx) {
|
|
144797
144900
|
return tool({
|
|
144798
144901
|
name: "get_issue",
|
|
144799
|
-
description: "Retrieve GitHub issue information by issue number",
|
|
144902
|
+
description: "Retrieve GitHub issue information by issue number. Example: `get_issue({ issue_number: 1234 })`.",
|
|
144800
144903
|
parameters: IssueInfo,
|
|
144801
144904
|
execute: execute(async ({ issue_number }) => {
|
|
144802
144905
|
const issue3 = await ctx.octokit.rest.issues.get({
|
|
@@ -145038,7 +145141,7 @@ var PullRequestInfo = type({
|
|
|
145038
145141
|
function PullRequestInfoTool(ctx) {
|
|
145039
145142
|
return tool({
|
|
145040
145143
|
name: "get_pull_request",
|
|
145041
|
-
description: "Retrieve PR metadata (title, body, state, branches, author, labels, linked issues). To checkout a PR branch locally, use checkout_pr instead.",
|
|
145144
|
+
description: "Retrieve PR metadata (title, body, state, branches, author, labels, linked issues). Example: `get_pull_request({ pull_number: 1234 })`. To checkout a PR branch locally, use checkout_pr instead.",
|
|
145042
145145
|
parameters: PullRequestInfo,
|
|
145043
145146
|
execute: execute(async ({ pull_number }) => {
|
|
145044
145147
|
const [restResponse, graphqlResponse] = await Promise.all([
|
|
@@ -145442,7 +145545,7 @@ async function getReviewData(input) {
|
|
|
145442
145545
|
function GetReviewCommentsTool(ctx) {
|
|
145443
145546
|
return tool({
|
|
145444
145547
|
name: "get_review_comments",
|
|
145445
|
-
description: "Get review comments for a pull request review with full thread context. Automatically filters to approved comments when applicable. Returns a TOC and commentsPath pointing to a markdown file with full comment details.",
|
|
145548
|
+
description: "Get review comments for a pull request review with full thread context. Example: `get_review_comments({ pull_number: 1234, review_id: 567890 })`. Automatically filters to approved comments when applicable. Returns a TOC and commentsPath pointing to a markdown file with full comment details.",
|
|
145446
145549
|
parameters: GetReviewComments,
|
|
145447
145550
|
execute: execute(async (params) => {
|
|
145448
145551
|
const approvedBy = ctx.payload.event.trigger === "fix_review" && ctx.payload.event.approved_only ? ctx.payload.triggerer : void 0;
|
|
@@ -145492,7 +145595,7 @@ var ListPullRequestReviews = type({
|
|
|
145492
145595
|
function ListPullRequestReviewsTool(ctx) {
|
|
145493
145596
|
return tool({
|
|
145494
145597
|
name: "list_pull_request_reviews",
|
|
145495
|
-
description: "List all reviews for a pull request. Returns all reviews including approvals, request changes, and comments.",
|
|
145598
|
+
description: "List all reviews for a pull request. Returns all reviews including approvals, request changes, and comments. Example: `list_pull_request_reviews({ pull_number: 1234 })`.",
|
|
145496
145599
|
parameters: ListPullRequestReviews,
|
|
145497
145600
|
execute: execute(async (params) => {
|
|
145498
145601
|
const reviews = await ctx.octokit.paginate(ctx.octokit.rest.pulls.listReviews, {
|
|
@@ -145642,7 +145745,7 @@ function SelectModeTool(ctx) {
|
|
|
145642
145745
|
const overrides = buildModeOverrides(t);
|
|
145643
145746
|
return tool({
|
|
145644
145747
|
name: "select_mode",
|
|
145645
|
-
description:
|
|
145748
|
+
description: 'Select a mode and receive step-by-step guidance on how to handle the task. Call this to understand the best workflow for the current mode. Example: `select_mode({ mode: "Review" })` or `select_mode({ mode: "Plan", issue_number: 1234 })`.',
|
|
145646
145749
|
parameters: SelectModeParams,
|
|
145647
145750
|
execute: execute(async (params) => {
|
|
145648
145751
|
if (ctx.toolState.selectedMode) {
|
|
@@ -145703,7 +145806,9 @@ import { setTimeout as sleep2 } from "node:timers/promises";
|
|
|
145703
145806
|
var ShellParams = type({
|
|
145704
145807
|
command: "string",
|
|
145705
145808
|
description: "string",
|
|
145706
|
-
"timeout?":
|
|
145809
|
+
"timeout?": type.number.describe(
|
|
145810
|
+
"Timeout in MILLISECONDS (not seconds). Default 30000 (30s), max 120000 (2m). e.g. timeout: 180000 for 3 minutes; timeout: 180 means 180ms and will kill the process almost immediately."
|
|
145811
|
+
),
|
|
145707
145812
|
"working_directory?": "string",
|
|
145708
145813
|
"background?": "boolean"
|
|
145709
145814
|
});
|
|
@@ -145822,6 +145927,8 @@ function ShellTool(ctx) {
|
|
|
145822
145927
|
name: "shell",
|
|
145823
145928
|
description: `Execute shell commands securely. Environment is filtered to remove API keys and secrets.
|
|
145824
145929
|
|
|
145930
|
+
Example: \`shell({ command: "pnpm test", description: "run the test suite" })\`.
|
|
145931
|
+
|
|
145825
145932
|
Use this tool to:
|
|
145826
145933
|
- Run shell commands (ls, cat, grep, find, etc.)
|
|
145827
145934
|
- Execute build tools (npm, pnpm, cargo, make, etc.)
|
|
@@ -146339,18 +146446,24 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
146339
146446
|
- resolve addressed threads via \`${t("resolve_review_thread")}\`
|
|
146340
146447
|
- call \`${t("report_progress")}\` with a brief summary (or the exact push error if push failed)`
|
|
146341
146448
|
},
|
|
146342
|
-
// Review and IncrementalReview use
|
|
146343
|
-
// (
|
|
146344
|
-
//
|
|
146345
|
-
//
|
|
146346
|
-
//
|
|
146347
|
-
//
|
|
146348
|
-
//
|
|
146349
|
-
//
|
|
146350
|
-
//
|
|
146351
|
-
//
|
|
146352
|
-
//
|
|
146353
|
-
//
|
|
146449
|
+
// Review and IncrementalReview use a 0-or-2+ lens pattern. The default is
|
|
146450
|
+
// 0 lenses (orchestrator handles the review solo). Multi-lens (2+
|
|
146451
|
+
// reviewfrog subagents in parallel) only fires for substantive PRs or
|
|
146452
|
+
// high-stakes-subsystem touches — and when it fires, ALL lenses must
|
|
146453
|
+
// dispatch in a single assistant turn or the parallelism win disappears.
|
|
146454
|
+
// We never dispatch exactly one lens: a single lens is just a worse,
|
|
146455
|
+
// slower version of doing the work yourself.
|
|
146456
|
+
//
|
|
146457
|
+
// Build mode self-review is a different problem shape: the orchestrator
|
|
146458
|
+
// wrote the code, so bias-mitigation comes from delegating to one
|
|
146459
|
+
// fresh-eyes subagent that doesn't share the implementation context. A
|
|
146460
|
+
// single subagent there is appropriate; the 0-or-2+ rule applies only to
|
|
146461
|
+
// the Review/IncrementalReview lens fan-out where independence between
|
|
146462
|
+
// perspectives is what's being purchased.
|
|
146463
|
+
//
|
|
146464
|
+
// Deliberate omission vs canonical /anneal: severity categorization in
|
|
146465
|
+
// the final message (the review body has its own CAUTION/IMPORTANT
|
|
146466
|
+
// framing instead of a severity table).
|
|
146354
146467
|
{
|
|
146355
146468
|
name: "Review",
|
|
146356
146469
|
description: "Review code, PRs, or implementations; provide feedback or suggestions; identify issues; or check code quality, style, and correctness",
|
|
@@ -146360,9 +146473,9 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
146360
146473
|
|
|
146361
146474
|
2. **checkout**: call \`${t("checkout_pr")}\` \u2014 this returns PR metadata and a \`diffPath\`. read the diff TOC end-to-end and treat its file line ranges as your coverage checklist.
|
|
146362
146475
|
|
|
146363
|
-
3. **triage**: orient yourself on the PR \u2014 identify *what kind of thing this is* (domain it touches, seams it crosses, external contracts it depends on, user-facing surfaces it changes).
|
|
146476
|
+
3. **triage**: orient yourself on the PR \u2014 identify *what kind of thing this is* (domain it touches, seams it crosses, external contracts it depends on, user-facing surfaces it changes). pull as much context as you need to render a confident, well-grounded review: read related files, grep for callers of changed symbols, check tests that exercise the touched paths, fetch related GitHub state. **you are the synthesizer** \u2014 never delegate understanding to subagents.
|
|
146364
146477
|
|
|
146365
|
-
if the PR is **genuinely trivial**, skip
|
|
146478
|
+
if the PR is **genuinely trivial**, skip the fan-out entirely and submit a \`No new issues found.\` review per step 7.
|
|
146366
146479
|
|
|
146367
146480
|
"Genuinely trivial" (skip):
|
|
146368
146481
|
- single-word doc typo, whitespace/format-only, comment-only across any number of files
|
|
@@ -146381,23 +146494,25 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
146381
146494
|
- any "typo fix" in user-facing copy that changes meaning ("approved" \u2192 "denied")
|
|
146382
146495
|
- mixed diffs where a semantic 1-liner is buried in whitespace/formatting changes
|
|
146383
146496
|
|
|
146384
|
-
|
|
146497
|
+
4. **lens decision \u2014 0 or 2+, NEVER 1**.
|
|
146385
146498
|
|
|
146386
|
-
|
|
146499
|
+
The default is **0 lenses**: handle the review yourself end-to-end. Most PRs land here.
|
|
146387
146500
|
|
|
146388
|
-
|
|
146389
|
-
-
|
|
146390
|
-
-
|
|
146391
|
-
-
|
|
146501
|
+
Dispatch **2+ \`${REVIEWER_AGENT_NAME}\` lenses in parallel** ONLY when ALL of the following are true:
|
|
146502
|
+
- the PR is substantive (>5 files changed AND >200 net lines), OR touches a high-stakes subsystem (auth, billing, payments, schema migration, webhooks, secrets, RBAC, multi-tenant isolation, cron/scheduling)
|
|
146503
|
+
- you can name 2+ distinct concrete failure modes that warrant independent lenses (one lens per failure mode; orthogonal, not overlapping)
|
|
146504
|
+
- parallel-orchestrated independent perspectives meaningfully outperform what you'd find solo
|
|
146392
146505
|
|
|
146393
|
-
|
|
146506
|
+
**NEVER dispatch exactly one lens.** A single lens is just a more expensive version of doing the work yourself with a worse model \u2014 it adds wall time and a context-handoff for no orthogonality benefit. Either you have at least two genuinely independent failure-mode hypotheses (dispatch all in one turn), or you don't (do the review yourself).
|
|
146507
|
+
|
|
146508
|
+
When you do go multi-lens, lens framings come in two flavors:
|
|
146394
146509
|
- **themed lenses** \u2014 a perspective applied across the whole diff (correctness, security, user-journey, performance, etc.).
|
|
146395
|
-
- **subsystem lenses** \u2014 a domain-scoped frame for high-stakes subsystems the PR touches (e.g. "the auth lens", "the billing lens", "the schema-migration lens").
|
|
146510
|
+
- **subsystem lenses** \u2014 a domain-scoped frame for high-stakes subsystems the PR touches (e.g. "the auth lens", "the billing lens", "the schema-migration lens"). **for high-stakes domains, lead with the subsystem lens rather than the generic themed equivalent** \u2014 "billing-subsystem" outperforms "correctness on billing code" because the framing primes the subagent to remember domain-specific failure modes (double-charges, refund races, currency rounding, dispute flows) the generic lens misses.
|
|
146396
146511
|
|
|
146397
146512
|
starter menu (combine, omit, or invent your own):
|
|
146398
146513
|
- **correctness & invariants** \u2014 bugs, races, error handling, edge cases, state-machine boundaries
|
|
146399
|
-
- **impact** \u2014
|
|
146400
|
-
- **research-validated assumptions** \u2014 third-party API contracts, SDK semantics, framework directives, version-gated behavior. the subagent must verify load-bearing claims via web search and quote source URLs.
|
|
146514
|
+
- **impact** \u2014 stale references in code/tests/docs/configs/UI after rename/remove
|
|
146515
|
+
- **research-validated assumptions** \u2014 third-party API contracts, SDK semantics, framework directives, version-gated behavior. **only pick when the PR's correctness depends on the contract behaving a specific way** \u2014 not when the API is merely used. The bar is "if the third-party contract differs from what the diff assumes, the PR is incorrect." When dispatched, the subagent must verify load-bearing claims via web search and quote source URLs.
|
|
146401
146516
|
- **security** \u2014 new endpoints, authZ, input validation, secrets handling, replay/CSRF/injection, cross-tenant isolation
|
|
146402
146517
|
- **user-journey** \u2014 UX-touching flows: walk through happy path and failure modes as a user
|
|
146403
146518
|
- **operational readiness** \u2014 observability, alerting, migrations (forward + rollback), feature flags, on-call burden
|
|
@@ -146407,26 +146522,36 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
146407
146522
|
- **holistic** \u2014 does the PR make sense as a whole? symmetric flows (delete for every create, rollback for every migration)?
|
|
146408
146523
|
- **subsystem lenses** (invent as the PR demands) \u2014 auth, billing, payments, schema migration, webhooks, secrets, RBAC, multi-tenant isolation, cron/scheduling, etc.
|
|
146409
146524
|
|
|
146410
|
-
|
|
146525
|
+
The only subagent type is \`${REVIEWER_AGENT_NAME}\` \u2014 used for lens judgment work ("is this safe / correct / well-tested?"), runs on a mid-tier model.
|
|
146526
|
+
|
|
146527
|
+
5. **fan out (only if step 4 said 2+ lenses)**: dispatch every \`${REVIEWER_AGENT_NAME}\` subagent for this run **IN A SINGLE ASSISTANT TURN, AS MULTIPLE PARALLEL TASK TOOL_USE BLOCKS IN ONE MESSAGE.**
|
|
146528
|
+
|
|
146529
|
+
\u26A0\uFE0F CRITICAL \u2014 PARALLELISM IS THE ONLY REASON LENSES EXIST. \u26A0\uFE0F
|
|
146530
|
+
The default tool-call behavior of Claude Code (and most agent runtimes) is **serial dispatch**: emit one Task call, await result, emit next, await, etc. This collapses your fan-out into a sequential review where each lens adds N \xD7 (orchestrator-think-time + lens-execution-time) to wall time. **YOU MUST OVERRIDE THIS DEFAULT.** Emit ALL of your Task tool_use blocks in the SAME assistant message, BEFORE you read ANY result from ANY of them. If you find yourself emitting one Task call, then thinking about the result, then emitting another \u2014 STOP and re-issue them all together. The whole point of going multi-lens is the wall-clock speedup from parallel execution; serial dispatch defeats it entirely.
|
|
146531
|
+
|
|
146532
|
+
\u2705 Right pattern: one assistant turn with N Task tool_use blocks \u2192 wait \u2192 N results arrive together \u2192 aggregate.
|
|
146533
|
+
\u274C Wrong pattern: turn 1 = Task(lens A) \u2192 turn 2 (after A's result) = Task(lens B) \u2192 turn 3 (after B's result) = Task(lens C). This is the failure mode. Do not do this.
|
|
146534
|
+
|
|
146535
|
+
You can also include your own \`read\` / \`grep\` / \`webfetch\` calls in the SAME turn as the parallel \`${REVIEWER_AGENT_NAME}\` dispatches \u2014 concurrent context-pulling on the orchestrator side runs in parallel with the lens fan-out and costs zero extra wall time.
|
|
146536
|
+
|
|
146537
|
+
if a subagent errors out, times out, or returns nothing usable, retry once with the same lens; if it still fails, proceed with partial coverage and note the missing lens in the review body \u2014 do not skip the fan-out entirely on a single subagent failure. each subagent gets:
|
|
146411
146538
|
- the diff path / target \u2014 reading the diff and the codebase is its job
|
|
146412
146539
|
- **only one lens** \u2014 never a multi-section "review for X, Y, and Z" prompt
|
|
146413
146540
|
- **a Task \`description\` set to the lens name** (e.g. \`"security"\`, \`"correctness"\`, \`"billing-subsystem"\`) \u2014 the harness reads this field to label the subagent's log lines so parallel runs can be told apart in CI output. without it, every subagent shows up as \`subagent#N\`.
|
|
146414
|
-
- the read-only contract restated in your dispatch instructions so the rule is present twice (the subagent's system prompt also enforces it). The test: would this call still be a no-op if reverted? If not (PR comments, branch pushes, issue updates, set_output, label changes, dependency installs, etc.), don't make it.
|
|
146415
146541
|
- if the lens touches external contracts, instruct the subagent to verify load-bearing claims via web search rather than trust training data, and to quote source URLs in its reasoning. action runs are non-interactive \u2014 there's no human in the loop to catch "I'm pretty sure Stripe does X."
|
|
146416
146542
|
- ask the subagent to report findings with file paths and NEW line numbers from the diff so you can anchor inline comments without re-reading the entire diff.
|
|
146417
146543
|
|
|
146418
146544
|
delegation discipline:
|
|
146419
|
-
- do NOT lens-review the diff yourself in parallel with the subagents (your job is dispatch + comment-drafting; doing the lens work yourself reintroduces the bias the fan-out avoids)
|
|
146420
146545
|
- do NOT summarize the PR for them (biases toward a validation frame)
|
|
146421
146546
|
- do NOT hand them a curated reading list (let them discover scope)
|
|
146422
146547
|
- do NOT pre-shape their output with a finding schema
|
|
146423
146548
|
- do NOT mention the other lenses (independence is the point \u2014 overlapping findings are a strong signal)
|
|
146424
146549
|
|
|
146425
|
-
|
|
146550
|
+
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.
|
|
146426
146551
|
|
|
146427
146552
|
for surviving findings, draft inline comments with NEW line numbers from the diff. every comment must be actionable, 2-3 sentences max. 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.
|
|
146428
146553
|
|
|
146429
|
-
|
|
146554
|
+
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.
|
|
146430
146555
|
|
|
146431
146556
|
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.
|
|
146432
146557
|
|
|
@@ -146454,10 +146579,10 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
146454
146579
|
|
|
146455
146580
|
${PR_SUMMARY_FORMAT}`
|
|
146456
146581
|
},
|
|
146457
|
-
// IncrementalReview shares Review's
|
|
146458
|
-
//
|
|
146459
|
-
//
|
|
146460
|
-
//
|
|
146582
|
+
// IncrementalReview shares Review's 0-or-2+ lens pattern but scopes the
|
|
146583
|
+
// target to the incremental diff. The "issues must be NEW since the last
|
|
146584
|
+
// Pullfrog review" filter lives at aggregation time (step 8), NOT in the
|
|
146585
|
+
// subagent prompt — pushing the filter into
|
|
146461
146586
|
// subagents matches the canonical anneal anti-pattern of "list known
|
|
146462
146587
|
// pre-existing failures — don't flag these" and suppresses signal on
|
|
146463
146588
|
// regressions the new commits amplified. The review body is just
|
|
@@ -146476,38 +146601,57 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
146476
146601
|
|
|
146477
146602
|
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.
|
|
146478
146603
|
|
|
146479
|
-
4. **prior feedback**: fetch previous reviews via \`${t("list_pull_request_reviews")}\`. for the most recent Pullfrog review, call \`${t("get_review_comments")}\` with the review ID to retrieve specific prior line-level feedback. you'll use this to filter your aggregation in step
|
|
146604
|
+
4. **prior feedback**: fetch previous reviews via \`${t("list_pull_request_reviews")}\`. for the most recent Pullfrog review, call \`${t("get_review_comments")}\` with the review ID to retrieve specific prior line-level feedback. you'll use this to filter your aggregation in step 8 \u2014 anything already flagged in a prior review and not changed by the new commits should not be re-raised. you do NOT need to render this in the review body; the rolling PR summary snapshot is the durable record of what's been addressed.
|
|
146480
146605
|
|
|
146481
|
-
5. **triage
|
|
146606
|
+
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.**
|
|
146482
146607
|
|
|
146483
|
-
if the incremental changes are **genuinely trivial**, skip the fan-out entirely and jump to step
|
|
146608
|
+
if the incremental changes are **genuinely trivial**, skip the fan-out entirely and jump to step 10's non-substantive path (do NOT submit a review).
|
|
146484
146609
|
|
|
146485
146610
|
"Genuinely trivial" (skip): formatting/comment tweaks, import reordering, lockfile regen, mechanical rename of import paths, whitespace-only.
|
|
146486
146611
|
"Looks trivial but isn't" (do NOT skip \u2014 same anti-patterns as Review mode): 1-line changes to SQL/regex/auth/billing/permissions/signature-verification code; flipping feature-flag defaults or retry/timeout constants; money/tax/HTTP-method/redirect changes; tightening or loosening a comparison operator; mixed diffs with a semantic line buried in formatting.
|
|
146487
146612
|
When unsure, treat as non-trivial.
|
|
146488
146613
|
|
|
146489
|
-
|
|
146614
|
+
6. **lens decision \u2014 0 or 2+, NEVER 1**.
|
|
146490
146615
|
|
|
146491
|
-
|
|
146492
|
-
|
|
146616
|
+
The default is **0 lenses**: handle the re-review yourself end-to-end. Most incremental reviews land here \u2014 especially thread-reply re-reviews where the user is asking "did you address X?" rather than "review the diff again."
|
|
146617
|
+
|
|
146618
|
+
Dispatch **2+ \`${REVIEWER_AGENT_NAME}\` lenses in parallel** ONLY when ALL of the following are true:
|
|
146619
|
+
- the incremental changes are substantive (>5 files changed AND >200 net new lines), OR touch a high-stakes subsystem (auth, billing, payments, schema migration, webhooks, secrets, RBAC, multi-tenant isolation, cron/scheduling)
|
|
146620
|
+
- you can name 2+ distinct concrete failure modes the new commits plausibly introduce that warrant independent lenses
|
|
146621
|
+
- parallel-orchestrated independent perspectives meaningfully outperform what you'd find solo
|
|
146622
|
+
|
|
146623
|
+
**NEVER dispatch exactly one lens.** Single-lens dispatch adds wall time and cost for no orthogonality benefit. Either go multi-lens (\u22652 in parallel) or do the re-review yourself.
|
|
146624
|
+
|
|
146625
|
+
Lens framing follows Review mode: themed lenses (correctness, security, etc.) and subsystem lenses (auth, billing, schema-migration, etc.) \u2014 for high-stakes domains lead with the subsystem lens.
|
|
146626
|
+
|
|
146627
|
+
7. **fan out (only if step 6 said 2+ lenses)**: dispatch every \`${REVIEWER_AGENT_NAME}\` subagent for this run **IN A SINGLE ASSISTANT TURN, AS MULTIPLE PARALLEL TASK TOOL_USE BLOCKS IN ONE MESSAGE.**
|
|
146628
|
+
|
|
146629
|
+
\u26A0\uFE0F CRITICAL \u2014 PARALLELISM IS THE ONLY REASON LENSES EXIST. \u26A0\uFE0F
|
|
146630
|
+
Default tool-call behavior is **serial dispatch**: emit one Task call, await result, emit next, await, etc. This collapses your fan-out into a sequential review where each lens adds N \xD7 (orchestrator-think-time + lens-execution-time) to wall time. **YOU MUST OVERRIDE THIS DEFAULT.** Emit ALL of your Task tool_use blocks in the SAME assistant message, BEFORE you read ANY result from ANY of them.
|
|
146631
|
+
|
|
146632
|
+
\u2705 Right pattern: one assistant turn with N Task tool_use blocks \u2192 wait \u2192 N results arrive together \u2192 aggregate.
|
|
146633
|
+
\u274C Wrong pattern: turn 1 = Task(lens A) \u2192 turn 2 (after A's result) = Task(lens B). This is the failure mode.
|
|
146634
|
+
|
|
146635
|
+
You can also include your own \`read\` / \`grep\` / \`webfetch\` calls in the SAME turn as the parallel \`${REVIEWER_AGENT_NAME}\` dispatches.
|
|
146636
|
+
|
|
146637
|
+
if a subagent errors out, times out, or returns nothing usable, retry once with the same lens; if it still fails, proceed with partial coverage and note the missing lens in the review body. each subagent gets:
|
|
146638
|
+
- the diff scope (incremental diff path if available, full diff otherwise). do NOT tell them to skip pre-existing issues \u2014 that suppresses regressions the new commits amplified; the "issues must be NEW" filter lives at aggregation time (step 8), not in the subagent prompt
|
|
146493
146639
|
- **only one lens** \u2014 never a multi-section "review for X, Y, and Z" prompt
|
|
146494
|
-
- **a Task \`description\` set to the lens name**
|
|
146495
|
-
- the
|
|
146496
|
-
- if the lens touches external contracts, instruct the subagent to verify load-bearing claims via web search and quote source URLs. action runs are non-interactive \u2014 there's no human to catch "I'm pretty sure Stripe does X."
|
|
146640
|
+
- **a Task \`description\` set to the lens name** \u2014 the harness reads this field to label log lines so parallel runs can be told apart.
|
|
146641
|
+
- if the lens touches external contracts, instruct the subagent to verify load-bearing claims via web search and quote source URLs.
|
|
146497
146642
|
- ask the subagent to report findings with file paths and NEW line numbers from the full PR diff so you can anchor inline comments.
|
|
146498
146643
|
|
|
146499
146644
|
delegation discipline:
|
|
146500
|
-
- do NOT lens-review the diff yourself in parallel with the subagents
|
|
146501
146645
|
- do NOT summarize the changes for them (biases toward validation frame)
|
|
146502
146646
|
- do NOT hand them a curated reading list (let them discover scope)
|
|
146503
146647
|
- do NOT pre-shape their output with a finding schema
|
|
146504
146648
|
- do NOT mention the other lenses (independence is the point)
|
|
146505
146649
|
|
|
146506
|
-
|
|
146650
|
+
8. **aggregate, draft, self-critique**: merge findings (yours + any subagent output if you went multi-lens); de-dup overlaps; trace each finding yourself. drop praise, style preferences, speculative/unverified claims, findings about pre-existing code unrelated to the new commits, anything not actionable, and anything that re-states prior review feedback (heuristic: if the finding's root cause lives in lines the *new commits* added or modified, it's in scope; otherwise drop). also drop **bloat-shaped findings** \u2014 proposed fixes that would add defensive checks for cases that can't happen, abstractions used once, comments restating obvious code, tests asserting tautologies, or "just-in-case" guards. subagents are fallible and bias toward recommending changes; the bar for an actionable inline comment is sound + correct + elegant. recommending a change that improves only one of the three (or degrades elegance to nominally improve correctness) makes the codebase worse, not better. To compute "lines the new commits added or modified": if \`incrementalDiffPath\` from step 2 is present, use it directly. Otherwise, take the prior Pullfrog review's \`commit_id\` (returned alongside each entry from \`${t("list_pull_request_reviews")}\` in step 4) and run \`git diff <prior-review-sha>..HEAD\` to isolate the lines added since that review. draft inline comments with NEW line numbers from the full PR diff \u2014 every comment must be actionable, 2-3 sentences max.
|
|
146507
146651
|
|
|
146508
|
-
|
|
146652
|
+
9. **build the review body** \u2014 a single "Reviewed changes" section: summarize at the logical-change level, not per-file. each bullet starts with a past-tense verb (e.g. \`- Extracted shared CLI runtime into a single module\`, \`- Renamed package to pullfrog\`). avoid file paths unless they add clarity. if the changes can be described in one sentence, use one sentence \u2014 no bullets needed. do NOT include a separate "Prior review feedback" checklist; 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 pull request instead of an incremental one \u2014 when this happens, you will need to determine what changes have happened since Pullfrog's most recent review.
|
|
146509
146653
|
|
|
146510
|
-
|
|
146654
|
+
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.
|
|
146511
146655
|
|
|
146512
146656
|
Same callout-intensity ladder as Review mode \u2014 \`[!CAUTION]\` (large red, "will break") \u2192 \`[!IMPORTANT]\` (large purple, "must address before merging") \u2192 \`[!NOTE]\` (small blue, "FYI") \u2192 no callout (plain text). And the 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.
|
|
146513
146657
|
|
|
@@ -146856,20 +147000,30 @@ var ThinkingTimer = class {
|
|
|
146856
147000
|
maximumFractionDigits: 1
|
|
146857
147001
|
});
|
|
146858
147002
|
lastToolResultTimestamp = null;
|
|
147003
|
+
formatLine;
|
|
147004
|
+
// node's native TS strip-only mode does not support parameter properties,
|
|
147005
|
+
// so the formatter is declared as a field and assigned in the body.
|
|
147006
|
+
constructor(formatLine = (l) => l) {
|
|
147007
|
+
this.formatLine = formatLine;
|
|
147008
|
+
}
|
|
146859
147009
|
markToolResult() {
|
|
146860
147010
|
this.lastToolResultTimestamp = performance5.now();
|
|
146861
|
-
log.debug(
|
|
147011
|
+
log.debug(
|
|
147012
|
+
this.formatLine(`\xBB thinking timer: markToolResult at ${this.lastToolResultTimestamp}`)
|
|
147013
|
+
);
|
|
146862
147014
|
}
|
|
146863
147015
|
markToolCall() {
|
|
146864
147016
|
const now = performance5.now();
|
|
146865
147017
|
log.debug(
|
|
146866
|
-
|
|
147018
|
+
this.formatLine(
|
|
147019
|
+
`\xBB thinking timer: markToolCall at ${now}, lastToolResult=${this.lastToolResultTimestamp}`
|
|
147020
|
+
)
|
|
146867
147021
|
);
|
|
146868
147022
|
if (this.lastToolResultTimestamp === null) return;
|
|
146869
147023
|
const elapsed = now - this.lastToolResultTimestamp;
|
|
146870
147024
|
if (elapsed < THINKING_THRESHOLD) return;
|
|
146871
147025
|
const seconds = elapsed / 1e3;
|
|
146872
|
-
log.info(`\xBB thought for ${this.durationFormatter.format(seconds)}`);
|
|
147026
|
+
log.info(this.formatLine(`\xBB thought for ${this.durationFormatter.format(seconds)}`));
|
|
146873
147027
|
}
|
|
146874
147028
|
};
|
|
146875
147029
|
|
|
@@ -146877,45 +147031,12 @@ var ThinkingTimer = class {
|
|
|
146877
147031
|
import { readFile } from "node:fs/promises";
|
|
146878
147032
|
function getUnsubmittedReview(toolState) {
|
|
146879
147033
|
const mode = toolState.selectedMode;
|
|
146880
|
-
if (mode !== "Review" && mode !== "IncrementalReview") return null;
|
|
146881
|
-
if (toolState.review || toolState.finalSummaryWritten) return null;
|
|
146882
147034
|
if (!toolState.hadProgressComment) return null;
|
|
146883
|
-
return
|
|
146884
|
-
|
|
146885
|
-
|
|
146886
|
-
function truncateHookOutput(raw2) {
|
|
146887
|
-
if (raw2.length <= MAX_HOOK_OUTPUT_CHARS) return raw2;
|
|
146888
|
-
return `...(truncated, showing last ${MAX_HOOK_OUTPUT_CHARS} chars)
|
|
146889
|
-
${raw2.slice(-MAX_HOOK_OUTPUT_CHARS)}`;
|
|
146890
|
-
}
|
|
146891
|
-
async function executeStopHook(script) {
|
|
146892
|
-
log.info("\xBB executing stop hook...");
|
|
146893
|
-
try {
|
|
146894
|
-
const result = await spawn({
|
|
146895
|
-
cmd: "bash",
|
|
146896
|
-
args: ["-c", script],
|
|
146897
|
-
env: process.env,
|
|
146898
|
-
timeout: LIFECYCLE_HOOK_TIMEOUT_MS,
|
|
146899
|
-
activityTimeout: 0,
|
|
146900
|
-
onStdout: (chunk) => process.stdout.write(chunk),
|
|
146901
|
-
onStderr: (chunk) => process.stderr.write(chunk)
|
|
146902
|
-
});
|
|
146903
|
-
if (result.exitCode === 0) {
|
|
146904
|
-
log.info("\xBB stop hook passed");
|
|
146905
|
-
return null;
|
|
146906
|
-
}
|
|
146907
|
-
const combined = [result.stderr.trim(), result.stdout.trim()].filter(Boolean).join("\n");
|
|
146908
|
-
const output = truncateHookOutput(combined);
|
|
146909
|
-
log.info(`\xBB stop hook failed with exit code ${result.exitCode}`);
|
|
146910
|
-
return { exitCode: result.exitCode, output };
|
|
146911
|
-
} catch (err) {
|
|
146912
|
-
const isTimeout = err instanceof SpawnTimeoutError && (err.code === SPAWN_TIMEOUT_CODE || err.code === SPAWN_ACTIVITY_TIMEOUT_CODE);
|
|
146913
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
146914
|
-
log.warning(
|
|
146915
|
-
`stop hook ${isTimeout ? "timed out" : "failed to spawn"}: ${msg} \u2014 skipping retry`
|
|
146916
|
-
);
|
|
146917
|
-
return null;
|
|
147035
|
+
if (mode === "Review") return toolState.review ? null : "Review";
|
|
147036
|
+
if (mode === "IncrementalReview") {
|
|
147037
|
+
return toolState.review || toolState.finalSummaryWritten ? null : "IncrementalReview";
|
|
146918
147038
|
}
|
|
147039
|
+
return null;
|
|
146919
147040
|
}
|
|
146920
147041
|
function buildStopHookPrompt(failure) {
|
|
146921
147042
|
return [
|
|
@@ -146965,10 +147086,6 @@ function buildUnsubmittedReviewPrompt(mode) {
|
|
|
146965
147086
|
}
|
|
146966
147087
|
async function collectPostRunIssues(ctx, options = {}) {
|
|
146967
147088
|
const issues = {};
|
|
146968
|
-
if (ctx.stopScript) {
|
|
146969
|
-
const failure = await executeStopHook(ctx.stopScript);
|
|
146970
|
-
if (failure) issues.stopHook = failure;
|
|
146971
|
-
}
|
|
146972
147089
|
const status = getGitStatus();
|
|
146973
147090
|
const mode = ctx.toolState.selectedMode;
|
|
146974
147091
|
if (status) {
|
|
@@ -147004,11 +147121,25 @@ function buildLearningsReflectionPrompt(filePath) {
|
|
|
147004
147121
|
"",
|
|
147005
147122
|
`the rolling learnings file is at \`${filePath}\`. read it first if you haven't already, then edit it in place using your native file tools. the server reads this file at end-of-run and persists any changes \u2014 there is no tool to call.`,
|
|
147006
147123
|
"",
|
|
147007
|
-
`
|
|
147008
|
-
`-
|
|
147009
|
-
`-
|
|
147010
|
-
`-
|
|
147011
|
-
|
|
147124
|
+
`structure:`,
|
|
147125
|
+
`- markdown hierarchy: \`## \` for top-level themes, \`### \` and deeper for sub-themes when a section grows. there is no fixed taxonomy \u2014 choose headings that fit THIS repo (e.g. for one repo \`## Migrations\` / \`## Local dev\` may make sense; for another, \`## API quirks\` / \`## Failure modes\`).`,
|
|
147126
|
+
`- **no section over ~300 lines.** when a section is approaching that, split it: introduce \`### \` subsections grouping related bullets, or hoist a coherent group into a new top-level \`## \` section. granular sections mean future runs read targeted line ranges instead of slurping the whole file. this is the most important hygiene rule on long-lived repos.`,
|
|
147127
|
+
`- if you find a flat unstructured list (legacy content from before this format), restructure it: read it, group related bullets, rewrite the file with \`## \` / \`### \` headings around them. don't preserve bad structure \u2014 fix it.`,
|
|
147128
|
+
"",
|
|
147129
|
+
`bullet hygiene:`,
|
|
147130
|
+
`- one fact per line starting with \`- \`. each bullet is ONE specific durable fact, not a paragraph or essay.`,
|
|
147131
|
+
`- aim for \u2264 240 chars per bullet. longer bullets are almost always mixing multiple facts that should be split, or burying the durable claim under PR-specific context that should be cut.`,
|
|
147132
|
+
`- only add bullets when the finding is high-confidence AND broadly useful AND will still be true in 3+ months. skip speculative, one-off, or "maybe" findings.`,
|
|
147133
|
+
`- prune bullets that are clearly wrong, no longer relevant, or low-signal. a focused, accurate file beats a long stale one. compressing two overlapping bullets into one tighter bullet counts as progress.`,
|
|
147134
|
+
`- deduplicate against existing entries (in any section) \u2014 if a bullet covers the same fact, update it in place instead of adding a duplicate.`,
|
|
147135
|
+
"",
|
|
147136
|
+
`do NOT add bullets for:`,
|
|
147137
|
+
`- pullfrog tool quirks (e.g. "\`shell\` timeout is in milliseconds", "\`git\` args must be a JSON array", "\`create_pull_request_review\` drops out-of-hunk comments", "\`push_branch\` may report timeout when push succeeded"). these are universal across repos and belong in the tool descriptions \u2014 flag the gap rather than hoarding the workaround per-repo.`,
|
|
147138
|
+
`- references to specific PR numbers, review IDs, commit SHAs, branch names, or person handles ("PR #595 introduced X", "flagged in review 12345", "as of commit abc123"). repo state changes; these decay into noise within weeks.`,
|
|
147139
|
+
`- dated assertions ("as of May 2026", "currently...", "for now..."). if a fact needs a date to be true, it isn't durable enough to belong here.`,
|
|
147140
|
+
`- play-by-play of what THIS run did. learnings are for the NEXT run, not a retrospective.`,
|
|
147141
|
+
"",
|
|
147142
|
+
`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.`
|
|
147012
147143
|
].join("\n");
|
|
147013
147144
|
}
|
|
147014
147145
|
async function runPostRunRetryLoop(params) {
|
|
@@ -147118,19 +147249,39 @@ function deriveLabelFromTaskInput(input) {
|
|
|
147118
147249
|
}
|
|
147119
147250
|
var SessionLabeler = class {
|
|
147120
147251
|
labels = /* @__PURE__ */ new Map();
|
|
147252
|
+
labelsByToolUseId = /* @__PURE__ */ new Map();
|
|
147121
147253
|
pendingLabels = [];
|
|
147122
147254
|
fallbackCounter = 0;
|
|
147123
|
-
|
|
147255
|
+
/**
|
|
147256
|
+
* Record a Task/Agent tool dispatch.
|
|
147257
|
+
*
|
|
147258
|
+
* @param input Task tool input — used to derive the lens label.
|
|
147259
|
+
* @param toolUseId Optional Agent tool_use id. When provided, future events
|
|
147260
|
+
* carrying `parent_tool_use_id === toolUseId` resolve
|
|
147261
|
+
* directly to this label without consuming the FIFO queue
|
|
147262
|
+
* (Claude path). Always also pushed to the FIFO queue so
|
|
147263
|
+
* the OpenCode path still works when toolUseId is absent.
|
|
147264
|
+
*/
|
|
147265
|
+
recordTaskDispatch(input, toolUseId) {
|
|
147124
147266
|
const label = deriveLabelFromTaskInput(input);
|
|
147125
147267
|
this.pendingLabels.push(label);
|
|
147268
|
+
if (toolUseId) this.labelsByToolUseId.set(toolUseId, label);
|
|
147126
147269
|
return label;
|
|
147127
147270
|
}
|
|
147128
147271
|
/**
|
|
147129
|
-
* Return a label for the given
|
|
147130
|
-
*
|
|
147131
|
-
*
|
|
147272
|
+
* Return a label for the given event.
|
|
147273
|
+
*
|
|
147274
|
+
* @param sessionID Session id from the event (OpenCode: per-session;
|
|
147275
|
+
* Claude: shared across orchestrator + subagents).
|
|
147276
|
+
* @param parentToolUseId Claude's `parent_tool_use_id` — non-null on
|
|
147277
|
+
* subagent messages. When set and known, takes
|
|
147278
|
+
* priority over the FIFO/sessionID path.
|
|
147132
147279
|
*/
|
|
147133
|
-
labelFor(sessionID) {
|
|
147280
|
+
labelFor(sessionID, parentToolUseId) {
|
|
147281
|
+
if (parentToolUseId) {
|
|
147282
|
+
const direct = this.labelsByToolUseId.get(parentToolUseId);
|
|
147283
|
+
if (direct) return direct;
|
|
147284
|
+
}
|
|
147134
147285
|
if (!sessionID) return ORCHESTRATOR_LABEL;
|
|
147135
147286
|
const existing = this.labels.get(sessionID);
|
|
147136
147287
|
if (existing) return existing;
|
|
@@ -147192,8 +147343,9 @@ function writeMcpConfig(ctx) {
|
|
|
147192
147343
|
function buildAgentsJson() {
|
|
147193
147344
|
const agents2 = {
|
|
147194
147345
|
[REVIEWER_AGENT_NAME]: {
|
|
147195
|
-
description: "Read-only review subagent for
|
|
147196
|
-
prompt: REVIEWER_SYSTEM_PROMPT
|
|
147346
|
+
description: "Read-only review subagent for lens-based code review (correctness, security, billing-subsystem, etc.). Reads only \u2014 no writes, no state-changing shell or MCP calls, no nested subagent dispatch.",
|
|
147347
|
+
prompt: REVIEWER_SYSTEM_PROMPT,
|
|
147348
|
+
model: "claude-sonnet-4-6"
|
|
147197
147349
|
}
|
|
147198
147350
|
};
|
|
147199
147351
|
return JSON.stringify(agents2);
|
|
@@ -147214,7 +147366,23 @@ function tailLines(text, maxCodeUnits) {
|
|
|
147214
147366
|
async function runClaude(params) {
|
|
147215
147367
|
const startTime = performance6.now();
|
|
147216
147368
|
let eventCount = 0;
|
|
147217
|
-
const
|
|
147369
|
+
const labeler = new SessionLabeler();
|
|
147370
|
+
function eventLabel(event) {
|
|
147371
|
+
return labeler.labelFor(event.session_id ?? null, event.parent_tool_use_id ?? null);
|
|
147372
|
+
}
|
|
147373
|
+
function withLabel(label, message) {
|
|
147374
|
+
return label === ORCHESTRATOR_LABEL ? message : formatWithLabel(label, message);
|
|
147375
|
+
}
|
|
147376
|
+
const thinkingTimers = /* @__PURE__ */ new Map();
|
|
147377
|
+
function timerFor(label) {
|
|
147378
|
+
let t = thinkingTimers.get(label);
|
|
147379
|
+
if (!t) {
|
|
147380
|
+
const formatLine = (line) => label === ORCHESTRATOR_LABEL ? line : formatWithLabel(label, line);
|
|
147381
|
+
t = new ThinkingTimer(formatLine);
|
|
147382
|
+
thinkingTimers.set(label, t);
|
|
147383
|
+
}
|
|
147384
|
+
return t;
|
|
147385
|
+
}
|
|
147218
147386
|
let finalOutput = "";
|
|
147219
147387
|
let sessionId;
|
|
147220
147388
|
let resultErrorSubtype = null;
|
|
@@ -147235,17 +147403,22 @@ async function runClaude(params) {
|
|
|
147235
147403
|
} : void 0;
|
|
147236
147404
|
}
|
|
147237
147405
|
const handlers2 = {
|
|
147238
|
-
system: (
|
|
147239
|
-
|
|
147406
|
+
system: (event) => {
|
|
147407
|
+
const label = eventLabel(event);
|
|
147408
|
+
log.debug(withLabel(label, `\xBB ${params.label} system event`));
|
|
147240
147409
|
},
|
|
147241
147410
|
assistant: (event) => {
|
|
147242
147411
|
const content = event.message?.content;
|
|
147243
147412
|
if (!content) return;
|
|
147413
|
+
const label = eventLabel(event);
|
|
147414
|
+
const boxTitle = label === ORCHESTRATOR_LABEL ? params.label : `${params.label} [${label}]`;
|
|
147244
147415
|
for (const block of content) {
|
|
147245
147416
|
if (block.type === "text" && block.text?.trim()) {
|
|
147246
147417
|
const message = block.text.trim();
|
|
147247
|
-
log.box(message, { title:
|
|
147248
|
-
|
|
147418
|
+
log.box(message, { title: boxTitle });
|
|
147419
|
+
if (label === ORCHESTRATOR_LABEL) {
|
|
147420
|
+
finalOutput = message;
|
|
147421
|
+
}
|
|
147249
147422
|
} else if (block.type === "tool_use") {
|
|
147250
147423
|
const toolName = block.name || "unknown";
|
|
147251
147424
|
if (params.onToolUse) {
|
|
@@ -147254,20 +147427,25 @@ async function runClaude(params) {
|
|
|
147254
147427
|
input: block.input
|
|
147255
147428
|
});
|
|
147256
147429
|
}
|
|
147257
|
-
|
|
147258
|
-
|
|
147259
|
-
|
|
147430
|
+
timerFor(label).markToolCall();
|
|
147431
|
+
const inputFormatted = formatJsonValue(block.input || {});
|
|
147432
|
+
const toolCallLine = inputFormatted !== "{}" ? `\xBB ${toolName}(${inputFormatted})` : `\xBB ${toolName}()`;
|
|
147433
|
+
log.info(withLabel(label, toolCallLine));
|
|
147434
|
+
if ((toolName === "Task" || toolName === "Agent") && block.input && typeof block.input === "object") {
|
|
147260
147435
|
const taskInput = block.input;
|
|
147261
|
-
const
|
|
147436
|
+
const dispatchedLabel = labeler.recordTaskDispatch(taskInput, block.id ?? null);
|
|
147262
147437
|
log.info(
|
|
147263
|
-
|
|
147438
|
+
withLabel(
|
|
147439
|
+
label,
|
|
147440
|
+
`\xBB dispatching subagent: ${dispatchedLabel}` + (taskInput.subagent_type ? ` (subagent_type=${taskInput.subagent_type})` : "")
|
|
147441
|
+
)
|
|
147264
147442
|
);
|
|
147265
147443
|
}
|
|
147266
147444
|
if (toolName.includes("report_progress") && params.todoTracker) {
|
|
147267
147445
|
log.debug("\xBB report_progress detected, disabling todo tracking");
|
|
147268
147446
|
params.todoTracker.cancel();
|
|
147269
147447
|
}
|
|
147270
|
-
if (toolName === "TodoWrite" && params.todoTracker?.enabled) {
|
|
147448
|
+
if (toolName === "TodoWrite" && params.todoTracker?.enabled && label === ORCHESTRATOR_LABEL) {
|
|
147271
147449
|
params.todoTracker.update(block.input);
|
|
147272
147450
|
}
|
|
147273
147451
|
}
|
|
@@ -147283,17 +147461,18 @@ async function runClaude(params) {
|
|
|
147283
147461
|
user: (event) => {
|
|
147284
147462
|
const content = event.message?.content;
|
|
147285
147463
|
if (!content) return;
|
|
147464
|
+
const label = eventLabel(event);
|
|
147286
147465
|
for (const block of content) {
|
|
147287
147466
|
if (typeof block === "string") continue;
|
|
147288
147467
|
if (block.type === "tool_result") {
|
|
147289
|
-
|
|
147468
|
+
timerFor(label).markToolResult();
|
|
147290
147469
|
const outputContent = typeof block.content === "string" ? block.content : Array.isArray(block.content) ? block.content.map(
|
|
147291
147470
|
(entry) => typeof entry === "string" ? entry : typeof entry === "object" && entry !== null && "text" in entry ? String(entry.text) : JSON.stringify(entry)
|
|
147292
147471
|
).join("\n") : String(block.content);
|
|
147293
147472
|
if (block.is_error) {
|
|
147294
|
-
log.info(`\xBB tool error: ${outputContent}`);
|
|
147473
|
+
log.info(withLabel(label, `\xBB tool error: ${outputContent}`));
|
|
147295
147474
|
} else {
|
|
147296
|
-
log.debug(`\xBB tool output: ${outputContent}`);
|
|
147475
|
+
log.debug(withLabel(label, `\xBB tool output: ${outputContent}`));
|
|
147297
147476
|
}
|
|
147298
147477
|
}
|
|
147299
147478
|
}
|
|
@@ -147362,8 +147541,9 @@ async function runClaude(params) {
|
|
|
147362
147541
|
}
|
|
147363
147542
|
};
|
|
147364
147543
|
const recentStderr = [];
|
|
147544
|
+
const recentNonJsonStdout = [];
|
|
147365
147545
|
let lastProviderError = null;
|
|
147366
|
-
|
|
147546
|
+
const output = new TailBuffer(DEFAULT_MAX_RETAINED_BYTES);
|
|
147367
147547
|
let stdoutBuffer = "";
|
|
147368
147548
|
try {
|
|
147369
147549
|
const result = await spawn({
|
|
@@ -147380,9 +147560,14 @@ async function runClaude(params) {
|
|
|
147380
147560
|
// there's no shim-orphan issue like opencode-ai/bin/opencode, but
|
|
147381
147561
|
// detached + killGroup is the right default for any agent runtime.
|
|
147382
147562
|
killGroup: true,
|
|
147563
|
+
// claude already drains every chunk via onStdout (NDJSON parsing) and
|
|
147564
|
+
// onStderr (recentStderr ring buffer). retaining a second copy in the
|
|
147565
|
+
// spawn wrapper would grow unbounded for long sessions and previously
|
|
147566
|
+
// crashed the wrapper with RangeError. see issue #680.
|
|
147567
|
+
retain: "none",
|
|
147383
147568
|
onStdout: async (chunk) => {
|
|
147384
147569
|
const text = chunk.toString();
|
|
147385
|
-
output
|
|
147570
|
+
output.append(text);
|
|
147386
147571
|
markActivity();
|
|
147387
147572
|
stdoutBuffer += text;
|
|
147388
147573
|
const lines = stdoutBuffer.split("\n");
|
|
@@ -147395,6 +147580,8 @@ async function runClaude(params) {
|
|
|
147395
147580
|
event = JSON.parse(trimmed);
|
|
147396
147581
|
} catch {
|
|
147397
147582
|
log.debug(`\xBB non-JSON stdout line: ${trimmed.substring(0, 200)}`);
|
|
147583
|
+
recentNonJsonStdout.push(trimmed);
|
|
147584
|
+
if (recentNonJsonStdout.length > MAX_STDERR_LINES) recentNonJsonStdout.shift();
|
|
147398
147585
|
continue;
|
|
147399
147586
|
}
|
|
147400
147587
|
eventCount++;
|
|
@@ -147457,16 +147644,19 @@ ${stderrContext}`);
|
|
|
147457
147644
|
const usage = buildUsage();
|
|
147458
147645
|
if (result.exitCode !== 0) {
|
|
147459
147646
|
const errorContext = lastProviderError ? ` (${lastProviderError})` : "";
|
|
147460
|
-
const
|
|
147461
|
-
const
|
|
147647
|
+
const stdoutSnapshot = output.toString();
|
|
147648
|
+
const stderrSnapshot = recentStderr.join("\n");
|
|
147649
|
+
const truncatedStdout = stdoutSnapshot ? tailLines(stdoutSnapshot, 2048) : "";
|
|
147650
|
+
const nonJsonStdoutSnapshot = recentNonJsonStdout.join("\n");
|
|
147651
|
+
const errorMessage = lastResultError || stderrSnapshot || nonJsonStdoutSnapshot || truncatedStdout || `unknown error - no output from Claude CLI${errorContext}`;
|
|
147462
147652
|
log.error(
|
|
147463
147653
|
`${params.label} exited with code ${result.exitCode}${errorContext}: ${errorMessage}`
|
|
147464
147654
|
);
|
|
147465
|
-
log.debug(`stdout: ${
|
|
147466
|
-
log.debug(`stderr: ${
|
|
147655
|
+
log.debug(`stdout: ${stdoutSnapshot.substring(0, 500)}`);
|
|
147656
|
+
log.debug(`stderr: ${stderrSnapshot.substring(0, 500)}`);
|
|
147467
147657
|
return {
|
|
147468
147658
|
success: false,
|
|
147469
|
-
output: finalOutput ||
|
|
147659
|
+
output: finalOutput || stdoutSnapshot,
|
|
147470
147660
|
error: errorMessage,
|
|
147471
147661
|
usage,
|
|
147472
147662
|
sessionId
|
|
@@ -147475,7 +147665,7 @@ ${stderrContext}`);
|
|
|
147475
147665
|
if (eventCount === 0 && lastProviderError) {
|
|
147476
147666
|
return {
|
|
147477
147667
|
success: false,
|
|
147478
|
-
output: finalOutput || output,
|
|
147668
|
+
output: finalOutput || output.toString(),
|
|
147479
147669
|
error: `provider error: ${lastProviderError}`,
|
|
147480
147670
|
usage,
|
|
147481
147671
|
sessionId
|
|
@@ -147484,13 +147674,13 @@ ${stderrContext}`);
|
|
|
147484
147674
|
if (resultErrorSubtype) {
|
|
147485
147675
|
return {
|
|
147486
147676
|
success: false,
|
|
147487
|
-
output: finalOutput || output,
|
|
147677
|
+
output: finalOutput || output.toString(),
|
|
147488
147678
|
error: lastResultError || `result subtype: ${resultErrorSubtype}`,
|
|
147489
147679
|
usage,
|
|
147490
147680
|
sessionId
|
|
147491
147681
|
};
|
|
147492
147682
|
}
|
|
147493
|
-
return { success: true, output: finalOutput || output, usage, sessionId };
|
|
147683
|
+
return { success: true, output: finalOutput || output.toString(), usage, sessionId };
|
|
147494
147684
|
} catch (error49) {
|
|
147495
147685
|
params.todoTracker?.cancel();
|
|
147496
147686
|
const duration4 = performance6.now() - startTime;
|
|
@@ -147509,7 +147699,7 @@ ${stderrContext}`
|
|
|
147509
147699
|
);
|
|
147510
147700
|
return {
|
|
147511
147701
|
success: false,
|
|
147512
|
-
output: finalOutput || output,
|
|
147702
|
+
output: finalOutput || output.toString(),
|
|
147513
147703
|
error: `${errorMessage} [${diagnosis}]`,
|
|
147514
147704
|
usage: buildUsage(),
|
|
147515
147705
|
sessionId
|
|
@@ -147559,7 +147749,9 @@ var claude = agent({
|
|
|
147559
147749
|
run: async (ctx) => {
|
|
147560
147750
|
const cliPath = await installClaudeCli();
|
|
147561
147751
|
const specifier = ctx.payload.proxyModel ?? ctx.resolvedModel;
|
|
147562
|
-
const
|
|
147752
|
+
const bedrockModelId = process.env[BEDROCK_MODEL_ID_ENV]?.trim();
|
|
147753
|
+
const isBedrockRoute = specifier !== void 0 && bedrockModelId !== void 0 && bedrockModelId === specifier && isBedrockAnthropicId(specifier);
|
|
147754
|
+
const model = !specifier ? void 0 : isBedrockRoute ? specifier : stripProviderPrefix(specifier);
|
|
147563
147755
|
const homeEnv = {
|
|
147564
147756
|
HOME: ctx.tmpdir,
|
|
147565
147757
|
XDG_CONFIG_HOME: join10(ctx.tmpdir, ".config")
|
|
@@ -147598,6 +147790,9 @@ var claude = agent({
|
|
|
147598
147790
|
...process.env,
|
|
147599
147791
|
...homeEnv
|
|
147600
147792
|
};
|
|
147793
|
+
if (isBedrockRoute) {
|
|
147794
|
+
env2.CLAUDE_CODE_USE_BEDROCK = "1";
|
|
147795
|
+
}
|
|
147601
147796
|
const repoDir = process.cwd();
|
|
147602
147797
|
log.info(`\xBB effort: ${effort}`);
|
|
147603
147798
|
log.debug(`\xBB starting Pullfrog (Claude Code): node ${baseArgs.join(" ")}`);
|
|
@@ -147719,6 +147914,22 @@ export default async function pullfrogEventsPlugin() {
|
|
|
147719
147914
|
}
|
|
147720
147915
|
`;
|
|
147721
147916
|
|
|
147917
|
+
// agents/subagentModels.ts
|
|
147918
|
+
function deriveSubagentModels(orchestratorSpec) {
|
|
147919
|
+
if (!orchestratorSpec) return { reviewer: void 0 };
|
|
147920
|
+
for (const source of modelAliases) {
|
|
147921
|
+
const matchedDirect = source.resolve === orchestratorSpec;
|
|
147922
|
+
const matchedOR = source.openRouterResolve === orchestratorSpec;
|
|
147923
|
+
if (!matchedDirect && !matchedOR) continue;
|
|
147924
|
+
if (!source.subagentModel) return { reviewer: void 0 };
|
|
147925
|
+
const target = modelAliases.find((a) => a.slug === source.subagentModel);
|
|
147926
|
+
if (!target) return { reviewer: void 0 };
|
|
147927
|
+
const reviewer = matchedOR ? target.openRouterResolve : target.resolve;
|
|
147928
|
+
return { reviewer };
|
|
147929
|
+
}
|
|
147930
|
+
return { reviewer: void 0 };
|
|
147931
|
+
}
|
|
147932
|
+
|
|
147722
147933
|
// agents/opencode.ts
|
|
147723
147934
|
async function installOpencodeCli() {
|
|
147724
147935
|
return await installFromNpmTarball({
|
|
@@ -147728,7 +147939,6 @@ async function installOpencodeCli() {
|
|
|
147728
147939
|
installDependencies: true
|
|
147729
147940
|
});
|
|
147730
147941
|
}
|
|
147731
|
-
var PULLFROG_OPENCODE_OUTPUT_LIMIT = 5e3;
|
|
147732
147942
|
var GEMINI_3_DIRECT_THINKING_LEVEL = "medium";
|
|
147733
147943
|
var GEMINI_3_DIRECT_API_IDS = ["gemini-3.1-pro-preview", "gemini-3-flash-preview"];
|
|
147734
147944
|
function buildSecurityConfig(ctx, model) {
|
|
@@ -147744,7 +147954,21 @@ function buildSecurityConfig(ctx, model) {
|
|
|
147744
147954
|
mcp: {
|
|
147745
147955
|
[pullfrogMcpName]: { type: "remote", url: ctx.mcpServerUrl }
|
|
147746
147956
|
},
|
|
147747
|
-
agent:
|
|
147957
|
+
agent: (() => {
|
|
147958
|
+
const cfg = buildReviewerAgentConfig(model);
|
|
147959
|
+
const reviewerModel = cfg[REVIEWER_AGENT_NAME]?.model ?? "(inherit)";
|
|
147960
|
+
log.info(`\xBB subagent models: reviewfrog=${reviewerModel}`);
|
|
147961
|
+
return cfg;
|
|
147962
|
+
})(),
|
|
147963
|
+
// opt into opencode's experimental `batch` tool (added in
|
|
147964
|
+
// anomalyco/opencode PR #2983, opt-in via `experimental.batch_tool`). it
|
|
147965
|
+
// exposes a single `batch` tool that runs 1-25 independent tool calls
|
|
147966
|
+
// (read/grep/glob/bash/etc.) concurrently in one assistant turn, which
|
|
147967
|
+
// collapses the dominant grep→20×read pattern into a single round trip.
|
|
147968
|
+
// edits are explicitly disallowed inside the batch upstream. paired with
|
|
147969
|
+
// the "Parallel tool execution" guidance in utils/instructions.ts so the
|
|
147970
|
+
// model actually reaches for it. see wiki/prompt.md.
|
|
147971
|
+
experimental: { batch_tool: true },
|
|
147748
147972
|
provider: {
|
|
147749
147973
|
google: {
|
|
147750
147974
|
models: Object.fromEntries(
|
|
@@ -147769,12 +147993,14 @@ function buildSecurityConfig(ctx, model) {
|
|
|
147769
147993
|
}
|
|
147770
147994
|
return JSON.stringify(config3);
|
|
147771
147995
|
}
|
|
147772
|
-
function buildReviewerAgentConfig() {
|
|
147996
|
+
function buildReviewerAgentConfig(orchestratorModel) {
|
|
147997
|
+
const overrides = deriveSubagentModels(orchestratorModel);
|
|
147773
147998
|
return {
|
|
147774
147999
|
[REVIEWER_AGENT_NAME]: {
|
|
147775
|
-
description: "Read-only review subagent for
|
|
148000
|
+
description: "Read-only review subagent for lens-based code review (correctness, security, billing-subsystem, etc.). Reads only \u2014 no writes, no state-changing shell or MCP calls, no nested subagent dispatch.",
|
|
147776
148001
|
mode: "subagent",
|
|
147777
|
-
prompt: REVIEWER_SYSTEM_PROMPT
|
|
148002
|
+
prompt: REVIEWER_SYSTEM_PROMPT,
|
|
148003
|
+
...overrides.reviewer !== void 0 ? { model: overrides.reviewer } : {}
|
|
147778
148004
|
}
|
|
147779
148005
|
};
|
|
147780
148006
|
}
|
|
@@ -147799,7 +148025,7 @@ function autoSelectModel(cliPath) {
|
|
|
147799
148025
|
const availableSet = new Set(availableModels);
|
|
147800
148026
|
if (availableSet.size > 0) {
|
|
147801
148027
|
log.debug(`\xBB opencode models (${availableSet.size}): ${availableModels.join(", ")}`);
|
|
147802
|
-
const match3 = modelAliases.find((a) => a.preferred && availableSet.has(a.resolve)) ?? modelAliases.find((a) => availableSet.has(a.resolve));
|
|
148028
|
+
const match3 = modelAliases.find((a) => !a.hidden && a.preferred && availableSet.has(a.resolve)) ?? modelAliases.find((a) => !a.hidden && availableSet.has(a.resolve));
|
|
147803
148029
|
if (match3) {
|
|
147804
148030
|
log.info(
|
|
147805
148031
|
`\xBB model: ${match3.resolve} (auto-selected${match3.preferred ? " \u2014 preferred" : ""} curated match)`
|
|
@@ -147817,7 +148043,6 @@ function autoSelectModel(cliPath) {
|
|
|
147817
148043
|
async function runOpenCode(params) {
|
|
147818
148044
|
const startTime = performance7.now();
|
|
147819
148045
|
let eventCount = 0;
|
|
147820
|
-
const thinkingTimer = new ThinkingTimer();
|
|
147821
148046
|
let finalOutput = "";
|
|
147822
148047
|
let accumulatedTokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
147823
148048
|
let accumulatedCostUsd = 0;
|
|
@@ -147834,6 +148059,16 @@ async function runOpenCode(params) {
|
|
|
147834
148059
|
function withLabel(label, message) {
|
|
147835
148060
|
return label === ORCHESTRATOR_LABEL ? message : formatWithLabel(label, message);
|
|
147836
148061
|
}
|
|
148062
|
+
const thinkingTimers = /* @__PURE__ */ new Map();
|
|
148063
|
+
function timerFor(label) {
|
|
148064
|
+
let t = thinkingTimers.get(label);
|
|
148065
|
+
if (!t) {
|
|
148066
|
+
const formatLine = (line) => label === ORCHESTRATOR_LABEL ? line : formatWithLabel(label, line);
|
|
148067
|
+
t = new ThinkingTimer(formatLine);
|
|
148068
|
+
thinkingTimers.set(label, t);
|
|
148069
|
+
}
|
|
148070
|
+
return t;
|
|
148071
|
+
}
|
|
147837
148072
|
const taskDispatchByCallID = /* @__PURE__ */ new Map();
|
|
147838
148073
|
const pendingTaskDispatches = [];
|
|
147839
148074
|
const knownNonTaskCallIDs = /* @__PURE__ */ new Set();
|
|
@@ -147982,7 +148217,7 @@ async function runOpenCode(params) {
|
|
|
147982
148217
|
input: event.part?.state?.input
|
|
147983
148218
|
});
|
|
147984
148219
|
}
|
|
147985
|
-
|
|
148220
|
+
timerFor(label).markToolCall();
|
|
147986
148221
|
const inputFormatted = formatJsonValue(event.part?.state?.input || {});
|
|
147987
148222
|
const toolCallLine = inputFormatted !== "{}" ? `\xBB ${toolName}(${inputFormatted})` : `\xBB ${toolName}()`;
|
|
147988
148223
|
log.info(withLabel(label, toolCallLine));
|
|
@@ -148006,7 +148241,7 @@ async function runOpenCode(params) {
|
|
|
148006
148241
|
const status = event.part?.state?.status || event.status || "unknown";
|
|
148007
148242
|
const output2 = event.part?.state?.output || event.output;
|
|
148008
148243
|
const label = eventLabel(event);
|
|
148009
|
-
|
|
148244
|
+
timerFor(label).markToolResult();
|
|
148010
148245
|
if (taskDispatchByCallID.size > 0 || pendingTaskDispatches.length > 0) {
|
|
148011
148246
|
if (toolId && taskDispatchByCallID.has(toolId)) {
|
|
148012
148247
|
const dispatch = taskDispatchByCallID.get(toolId);
|
|
@@ -148131,7 +148366,7 @@ async function runOpenCode(params) {
|
|
|
148131
148366
|
const recentStderr = [];
|
|
148132
148367
|
let lastProviderError = null;
|
|
148133
148368
|
let agentErrorEvent = null;
|
|
148134
|
-
|
|
148369
|
+
const output = new TailBuffer(DEFAULT_MAX_RETAINED_BYTES);
|
|
148135
148370
|
let stdoutBuffer = "";
|
|
148136
148371
|
try {
|
|
148137
148372
|
const result = await spawn({
|
|
@@ -148149,6 +148384,11 @@ async function runOpenCode(params) {
|
|
|
148149
148384
|
// never fires — producing zombie runs. detached + killGroup nukes the
|
|
148150
148385
|
// whole tree.
|
|
148151
148386
|
killGroup: true,
|
|
148387
|
+
// we already drain every chunk via onStdout/onStderr (NDJSON parsing
|
|
148388
|
+
// + recentStderr ring buffer). retaining a second copy in the spawn
|
|
148389
|
+
// wrapper would grow unbounded for multi-lens Reviews and previously
|
|
148390
|
+
// crashed the wrapper with RangeError at ~1 GiB. see issue #680.
|
|
148391
|
+
retain: "none",
|
|
148152
148392
|
// NB: we used to pass `isPausedExternally: isSubagentInFlight` to suspend
|
|
148153
148393
|
// the activity timer during subagent dispatches. unnecessary now that
|
|
148154
148394
|
// our injected plugin (action/agents/opencodePlugin.ts) re-emits
|
|
@@ -148158,7 +148398,7 @@ async function runOpenCode(params) {
|
|
|
148158
148398
|
// (~3.3 plugin events/sec during a typical subagent run).
|
|
148159
148399
|
onStdout: async (chunk) => {
|
|
148160
148400
|
const text = chunk.toString();
|
|
148161
|
-
output
|
|
148401
|
+
output.append(text);
|
|
148162
148402
|
markActivity();
|
|
148163
148403
|
stdoutBuffer += text;
|
|
148164
148404
|
const lines = stdoutBuffer.split("\n");
|
|
@@ -148247,18 +148487,25 @@ ${stderrContext}`);
|
|
|
148247
148487
|
const usage = buildUsage();
|
|
148248
148488
|
if (result.exitCode !== 0) {
|
|
148249
148489
|
const errorContext = lastProviderError ? ` (${lastProviderError})` : "";
|
|
148250
|
-
const
|
|
148490
|
+
const stdoutSnapshot = output.toString();
|
|
148491
|
+
const stderrSnapshot = recentStderr.join("\n");
|
|
148492
|
+
const errorMessage = stderrSnapshot || stdoutSnapshot || `unknown error - no output from OpenCode CLI${errorContext}`;
|
|
148251
148493
|
log.error(
|
|
148252
148494
|
`${params.label} exited with code ${result.exitCode}${errorContext}: ${errorMessage}`
|
|
148253
148495
|
);
|
|
148254
|
-
log.debug(`stdout: ${
|
|
148255
|
-
log.debug(`stderr: ${
|
|
148256
|
-
return {
|
|
148496
|
+
log.debug(`stdout: ${stdoutSnapshot.substring(0, 500)}`);
|
|
148497
|
+
log.debug(`stderr: ${stderrSnapshot.substring(0, 500)}`);
|
|
148498
|
+
return {
|
|
148499
|
+
success: false,
|
|
148500
|
+
output: finalOutput || stdoutSnapshot,
|
|
148501
|
+
error: errorMessage,
|
|
148502
|
+
usage
|
|
148503
|
+
};
|
|
148257
148504
|
}
|
|
148258
148505
|
if (eventCount === 0 && lastProviderError) {
|
|
148259
148506
|
return {
|
|
148260
148507
|
success: false,
|
|
148261
|
-
output: finalOutput || output,
|
|
148508
|
+
output: finalOutput || output.toString(),
|
|
148262
148509
|
error: `provider error: ${lastProviderError}`,
|
|
148263
148510
|
usage
|
|
148264
148511
|
};
|
|
@@ -148269,12 +148516,12 @@ ${stderrContext}`);
|
|
|
148269
148516
|
const errorMessage = errorEvent.error?.data?.message || errorEvent.error?.name || JSON.stringify(errorEvent);
|
|
148270
148517
|
return {
|
|
148271
148518
|
success: false,
|
|
148272
|
-
output: finalOutput || output,
|
|
148519
|
+
output: finalOutput || output.toString(),
|
|
148273
148520
|
error: `${errorName}: ${errorMessage}`,
|
|
148274
148521
|
usage
|
|
148275
148522
|
};
|
|
148276
148523
|
}
|
|
148277
|
-
return { success: true, output: finalOutput || output, usage };
|
|
148524
|
+
return { success: true, output: finalOutput || output.toString(), usage };
|
|
148278
148525
|
} catch (error49) {
|
|
148279
148526
|
params.todoTracker?.cancel();
|
|
148280
148527
|
const duration4 = performance7.now() - startTime;
|
|
@@ -148293,7 +148540,7 @@ ${stderrContext}`
|
|
|
148293
148540
|
);
|
|
148294
148541
|
return {
|
|
148295
148542
|
success: false,
|
|
148296
|
-
output: finalOutput || output,
|
|
148543
|
+
output: finalOutput || output.toString(),
|
|
148297
148544
|
error: `${errorMessage} [${diagnosis}]`,
|
|
148298
148545
|
usage: buildUsage()
|
|
148299
148546
|
};
|
|
@@ -148304,7 +148551,10 @@ var opencode = agent({
|
|
|
148304
148551
|
install: installOpencodeCli,
|
|
148305
148552
|
run: async (ctx) => {
|
|
148306
148553
|
const cliPath = await installOpencodeCli();
|
|
148307
|
-
const
|
|
148554
|
+
const rawModel = ctx.payload.proxyModel ?? ctx.resolvedModel ?? autoSelectModel(cliPath);
|
|
148555
|
+
const bedrockModelId = process.env[BEDROCK_MODEL_ID_ENV]?.trim();
|
|
148556
|
+
const isBedrockRoute = rawModel !== void 0 && bedrockModelId !== void 0 && bedrockModelId === rawModel;
|
|
148557
|
+
const model = isBedrockRoute ? `amazon-bedrock/${rawModel}` : rawModel;
|
|
148308
148558
|
const homeEnv = {
|
|
148309
148559
|
HOME: ctx.tmpdir,
|
|
148310
148560
|
XDG_CONFIG_HOME: join11(ctx.tmpdir, ".config")
|
|
@@ -148333,7 +148583,6 @@ var opencode = agent({
|
|
|
148333
148583
|
...homeEnv,
|
|
148334
148584
|
OPENCODE_CONFIG_CONTENT: buildSecurityConfig(ctx, model),
|
|
148335
148585
|
OPENCODE_PERMISSION: permissionOverride,
|
|
148336
|
-
OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX: PULLFROG_OPENCODE_OUTPUT_LIMIT.toString(),
|
|
148337
148586
|
GOOGLE_GENERATIVE_AI_API_KEY: process.env.GOOGLE_GENERATIVE_AI_API_KEY || process.env.GEMINI_API_KEY
|
|
148338
148587
|
};
|
|
148339
148588
|
const repoDir = process.cwd();
|
|
@@ -148376,13 +148625,29 @@ function hasEnvVar(name) {
|
|
|
148376
148625
|
function hasClaudeCodeAuth() {
|
|
148377
148626
|
return hasEnvVar("CLAUDE_CODE_OAUTH_TOKEN") || hasEnvVar("ANTHROPIC_API_KEY");
|
|
148378
148627
|
}
|
|
148628
|
+
function hasBedrockAuth() {
|
|
148629
|
+
return hasEnvVar("AWS_BEARER_TOKEN_BEDROCK") || hasEnvVar("AWS_ACCESS_KEY_ID") && hasEnvVar("AWS_SECRET_ACCESS_KEY");
|
|
148630
|
+
}
|
|
148631
|
+
function resolveSlug(slug2) {
|
|
148632
|
+
const alias = resolveDisplayAlias(slug2);
|
|
148633
|
+
if (alias?.routing === "bedrock") {
|
|
148634
|
+
const bedrockId = process.env[BEDROCK_MODEL_ID_ENV]?.trim();
|
|
148635
|
+
if (!bedrockId) {
|
|
148636
|
+
throw new Error(
|
|
148637
|
+
`${BEDROCK_MODEL_ID_ENV} env var is required when the model is set to "${slug2}". set it to an AWS Bedrock model ID (e.g. "us.anthropic.claude-opus-4-7", "amazon.nova-pro-v1:0"). see https://docs.pullfrog.com/bedrock for setup.`
|
|
148638
|
+
);
|
|
148639
|
+
}
|
|
148640
|
+
return bedrockId;
|
|
148641
|
+
}
|
|
148642
|
+
return resolveCliModel(slug2);
|
|
148643
|
+
}
|
|
148379
148644
|
function resolveModel(ctx) {
|
|
148380
148645
|
const envModel = process.env.PULLFROG_MODEL?.trim();
|
|
148381
148646
|
if (envModel) {
|
|
148382
|
-
return
|
|
148647
|
+
return resolveSlug(envModel) ?? envModel;
|
|
148383
148648
|
}
|
|
148384
148649
|
if (ctx.slug) {
|
|
148385
|
-
const resolved =
|
|
148650
|
+
const resolved = resolveSlug(ctx.slug);
|
|
148386
148651
|
if (resolved) {
|
|
148387
148652
|
return resolved;
|
|
148388
148653
|
}
|
|
@@ -148398,6 +148663,9 @@ function resolveAgent(ctx) {
|
|
|
148398
148663
|
}
|
|
148399
148664
|
log.warning(`\xBB unknown PULLFROG_AGENT="${envAgent}" \u2014 falling through to auto-select`);
|
|
148400
148665
|
}
|
|
148666
|
+
if (ctx.model && hasBedrockAuth() && process.env[BEDROCK_MODEL_ID_ENV]?.trim() === ctx.model) {
|
|
148667
|
+
return isBedrockAnthropicId(ctx.model) ? agents.claude : agents.opencode;
|
|
148668
|
+
}
|
|
148401
148669
|
if (ctx.model) {
|
|
148402
148670
|
try {
|
|
148403
148671
|
const provider2 = getModelProvider(ctx.model);
|
|
@@ -148412,31 +148680,56 @@ function resolveAgent(ctx) {
|
|
|
148412
148680
|
|
|
148413
148681
|
// utils/apiKeys.ts
|
|
148414
148682
|
var knownApiKeys = new Set(Object.values(providers).flatMap((p) => [...p.envVars]));
|
|
148683
|
+
var MISSING_KEY_MARKER = "no API key found";
|
|
148415
148684
|
function buildMissingApiKeyError(params) {
|
|
148416
|
-
const
|
|
148417
|
-
const settingsUrl = `${
|
|
148418
|
-
|
|
148419
|
-
|
|
148420
|
-
|
|
148685
|
+
const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
|
|
148686
|
+
const settingsUrl = `${getApiUrl()}/console/${params.owner}/${params.name}`;
|
|
148687
|
+
return [
|
|
148688
|
+
`**${MISSING_KEY_MARKER}** \u2014 Pullfrog needs at least one LLM provider API key (e.g. \`ANTHROPIC_API_KEY\`, \`OPENAI_API_KEY\`, \`GEMINI_API_KEY\`) configured as a GitHub Actions secret.`,
|
|
148689
|
+
"",
|
|
148690
|
+
`[Open repo secrets \u2192](${githubSecretsUrl}) \xB7 [Configure model \u2192](${settingsUrl}) \xB7 [Setup docs \u2192](https://docs.pullfrog.com/keys) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
|
|
148691
|
+
].join("\n");
|
|
148692
|
+
}
|
|
148693
|
+
function buildBedrockSetupError(params) {
|
|
148694
|
+
const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
|
|
148695
|
+
return `Bedrock model selected but required configuration is missing: ${params.missing.join(", ")}.
|
|
148421
148696
|
|
|
148422
|
-
|
|
148697
|
+
add the missing secret(s) to your GitHub repository at ${githubSecretsUrl}, then reference them in your workflow's \`env:\` block:
|
|
148423
148698
|
|
|
148424
|
-
|
|
148425
|
-
|
|
148426
|
-
|
|
148427
|
-
4. set the value to your API key
|
|
148428
|
-
5. click "Add secret"
|
|
148699
|
+
AWS_BEARER_TOKEN_BEDROCK: \${{ secrets.AWS_BEARER_TOKEN_BEDROCK }}
|
|
148700
|
+
AWS_REGION: \${{ secrets.AWS_REGION }}
|
|
148701
|
+
${BEDROCK_MODEL_ID_ENV}: \${{ secrets.${BEDROCK_MODEL_ID_ENV} }}
|
|
148429
148702
|
|
|
148430
|
-
|
|
148703
|
+
\`AWS_BEARER_TOKEN_BEDROCK\` may be substituted with \`AWS_ACCESS_KEY_ID\` + \`AWS_SECRET_ACCESS_KEY\` (and optional \`AWS_SESSION_TOKEN\`) if you prefer access keys.
|
|
148431
148704
|
|
|
148432
|
-
for full setup instructions, see https://docs.pullfrog.com/
|
|
148705
|
+
for full setup instructions, see https://docs.pullfrog.com/bedrock`;
|
|
148433
148706
|
}
|
|
148434
148707
|
function hasEnvVar2(name) {
|
|
148435
148708
|
const value2 = process.env[name];
|
|
148436
148709
|
return typeof value2 === "string" && value2.length > 0;
|
|
148437
148710
|
}
|
|
148711
|
+
function validateBedrockSetup(params) {
|
|
148712
|
+
const hasAuth = hasEnvVar2("AWS_BEARER_TOKEN_BEDROCK") || hasEnvVar2("AWS_ACCESS_KEY_ID") && hasEnvVar2("AWS_SECRET_ACCESS_KEY");
|
|
148713
|
+
const missing = [];
|
|
148714
|
+
if (!hasAuth)
|
|
148715
|
+
missing.push("AWS_BEARER_TOKEN_BEDROCK (or AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY)");
|
|
148716
|
+
if (!hasEnvVar2("AWS_REGION")) missing.push("AWS_REGION");
|
|
148717
|
+
if (!hasEnvVar2(BEDROCK_MODEL_ID_ENV)) missing.push(BEDROCK_MODEL_ID_ENV);
|
|
148718
|
+
if (missing.length > 0) {
|
|
148719
|
+
throw new Error(buildBedrockSetupError({ owner: params.owner, name: params.name, missing }));
|
|
148720
|
+
}
|
|
148721
|
+
}
|
|
148438
148722
|
function validateAgentApiKey(params) {
|
|
148439
148723
|
if (params.model) {
|
|
148724
|
+
const alias = resolveDisplayAlias(params.model);
|
|
148725
|
+
if (alias?.routing === "bedrock") {
|
|
148726
|
+
validateBedrockSetup({ owner: params.owner, name: params.name });
|
|
148727
|
+
return;
|
|
148728
|
+
}
|
|
148729
|
+
if (!params.model.includes("/")) {
|
|
148730
|
+
validateBedrockSetup({ owner: params.owner, name: params.name });
|
|
148731
|
+
return;
|
|
148732
|
+
}
|
|
148440
148733
|
const requiredVars = getModelEnvVars(params.model);
|
|
148441
148734
|
if (requiredVars.length === 0) return;
|
|
148442
148735
|
if (requiredVars.some((v) => hasEnvVar2(v))) return;
|
|
@@ -148447,6 +148740,22 @@ function validateAgentApiKey(params) {
|
|
|
148447
148740
|
throw new Error(buildMissingApiKeyError({ owner: params.owner, name: params.name }));
|
|
148448
148741
|
}
|
|
148449
148742
|
}
|
|
148743
|
+
function isApiKeyAuthError(text) {
|
|
148744
|
+
if (!text) return false;
|
|
148745
|
+
return text.includes(MISSING_KEY_MARKER) || /Invalid API key/i.test(text) || /\bUser not found\b/i.test(text) || /\bInvalid authentication\b/i.test(text);
|
|
148746
|
+
}
|
|
148747
|
+
function formatApiKeyErrorSummary(params) {
|
|
148748
|
+
if (params.raw.includes(MISSING_KEY_MARKER)) {
|
|
148749
|
+
return buildMissingApiKeyError({ owner: params.owner, name: params.name });
|
|
148750
|
+
}
|
|
148751
|
+
const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
|
|
148752
|
+
const settingsUrl = `${getApiUrl()}/console/${params.owner}/${params.name}`;
|
|
148753
|
+
return [
|
|
148754
|
+
`**Your LLM provider API key was rejected (401).** Rotate the key in your provider dashboard, then update the matching GitHub Actions secret.`,
|
|
148755
|
+
"",
|
|
148756
|
+
`[Update repo secret \u2192](${githubSecretsUrl}) \xB7 [Model settings \u2192](${settingsUrl}) \xB7 [Setup docs \u2192](https://docs.pullfrog.com/keys) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
|
|
148757
|
+
].join("\n");
|
|
148758
|
+
}
|
|
148450
148759
|
|
|
148451
148760
|
// utils/body.ts
|
|
148452
148761
|
var import_turndown = __toESM(require_turndown_cjs(), 1);
|
|
@@ -152199,6 +152508,14 @@ function isOIDCAvailable() {
|
|
|
152199
152508
|
process.env.ACTIONS_ID_TOKEN_REQUEST_URL && process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN
|
|
152200
152509
|
);
|
|
152201
152510
|
}
|
|
152511
|
+
var TokenExchangeError = class extends Error {
|
|
152512
|
+
status;
|
|
152513
|
+
constructor(status, message) {
|
|
152514
|
+
super(message);
|
|
152515
|
+
this.name = "TokenExchangeError";
|
|
152516
|
+
this.status = status;
|
|
152517
|
+
}
|
|
152518
|
+
};
|
|
152202
152519
|
async function acquireTokenViaOIDC(opts) {
|
|
152203
152520
|
const oidcToken = await core2.getIDToken("pullfrog-api");
|
|
152204
152521
|
const repos = [...opts?.repos ?? []];
|
|
@@ -152223,7 +152540,16 @@ async function acquireTokenViaOIDC(opts) {
|
|
|
152223
152540
|
});
|
|
152224
152541
|
clearTimeout(timeoutId);
|
|
152225
152542
|
if (!tokenResponse.ok) {
|
|
152226
|
-
|
|
152543
|
+
let serverMessage;
|
|
152544
|
+
try {
|
|
152545
|
+
const body = await tokenResponse.json();
|
|
152546
|
+
if (typeof body.error === "string") serverMessage = body.error;
|
|
152547
|
+
} catch {
|
|
152548
|
+
}
|
|
152549
|
+
throw new TokenExchangeError(
|
|
152550
|
+
tokenResponse.status,
|
|
152551
|
+
serverMessage ?? `Token exchange failed: ${tokenResponse.status} ${tokenResponse.statusText}`
|
|
152552
|
+
);
|
|
152227
152553
|
}
|
|
152228
152554
|
const tokenData = await tokenResponse.json();
|
|
152229
152555
|
return tokenData.token;
|
|
@@ -152344,7 +152670,10 @@ async function acquireNewToken(opts) {
|
|
|
152344
152670
|
if (isOIDCAvailable()) {
|
|
152345
152671
|
return await retry(() => acquireTokenViaOIDC(opts), {
|
|
152346
152672
|
label: "token exchange",
|
|
152347
|
-
shouldRetry: (error49) =>
|
|
152673
|
+
shouldRetry: (error49) => {
|
|
152674
|
+
if (error49 instanceof TokenExchangeError) return error49.status >= 500 || error49.status === 429;
|
|
152675
|
+
return error49 instanceof Error && (error49.message.includes("timed out") || error49.message.includes("fetch failed") || error49.message.includes("ECONNRESET") || error49.message.includes("ETIMEDOUT"));
|
|
152676
|
+
}
|
|
152348
152677
|
});
|
|
152349
152678
|
} else {
|
|
152350
152679
|
return await acquireTokenViaGitHubApp(opts);
|
|
@@ -152891,6 +153220,21 @@ ${getStandaloneModeInstructions(ctx.payload.event.trigger, t, ctx.outputSchema)}
|
|
|
152891
153220
|
|
|
152892
153221
|
Trust the tools \u2014 do not repeatedly verify file contents or git status after operations. If a tool reports success, proceed to the next step. Only verify if you encounter an actual error. Exception: right before \`${t("push_branch")}\`, ensure the working tree is clean \u2014 that tool rejects dirty trees, and tests you ran earlier often leave untracked output.
|
|
152893
153222
|
|
|
153223
|
+
### Parallel tool execution
|
|
153224
|
+
|
|
153225
|
+
For maximum efficiency, whenever you need to perform multiple independent operations, invoke all relevant tools simultaneously in a single assistant turn rather than sequentially. The dominant failure mode is grep \u2192 read \u2192 read \u2192 read \u2192 read across separate turns when one round trip would do. Always parallelize when calls are independent:
|
|
153226
|
+
- reading multiple files (especially after a grep returns candidates)
|
|
153227
|
+
- multiple greps with different patterns
|
|
153228
|
+
- glob + grep + read combos
|
|
153229
|
+
- listing multiple directories
|
|
153230
|
+
- inspecting multiple MCP tools or resources
|
|
153231
|
+
|
|
153232
|
+
Do NOT parallelize operations that depend on prior output (e.g. create a file then read it), or ordered stateful mutations. Edits are not parallelizable \u2014 sequence those normally.${ctx.agentId === "opencode" ? `
|
|
153233
|
+
|
|
153234
|
+
On OpenCode you also have a \`batch\` tool that bundles 1-25 independent calls into one wrapper call. Reach for it whenever you have >=2 independent calls. Native parallel tool_use and \`batch\` both achieve one round trip instead of N \u2014 use whichever your provider supports best.` : `
|
|
153235
|
+
|
|
153236
|
+
Emit multiple \`tool_use\` blocks in the same assistant message for independent calls \u2014 the runtime executes them concurrently. Do not wait for one tool result before issuing the next independent call.`}
|
|
153237
|
+
|
|
152894
153238
|
### Command execution
|
|
152895
153239
|
|
|
152896
153240
|
Never use \`sleep\` to wait for commands to complete. Commands run synchronously \u2014 when the shell tool returns, the command has finished.
|
|
@@ -152936,10 +153280,31 @@ function buildPromptContext(ctx) {
|
|
|
152936
153280
|
userQuoted: user ? user.split("\n").map((line) => `> ${line}`).join("\n") : ""
|
|
152937
153281
|
};
|
|
152938
153282
|
}
|
|
152939
|
-
function
|
|
152940
|
-
|
|
153283
|
+
function renderLearningsToc(headings) {
|
|
153284
|
+
if (headings.length === 0) return "";
|
|
153285
|
+
const rootDepth = Math.min(...headings.map((h) => h.depth));
|
|
153286
|
+
return headings.map((h) => {
|
|
153287
|
+
const indent2 = " ".repeat((h.depth - rootDepth) * 2);
|
|
153288
|
+
return `${indent2}- ${h.title} (L${h.startLine}-L${h.endLine})`;
|
|
153289
|
+
}).join("\n");
|
|
153290
|
+
}
|
|
153291
|
+
function buildLearningsSection(ctx) {
|
|
153292
|
+
if (!ctx.filePath) return "";
|
|
153293
|
+
const intro = `Repo-level learnings accumulated by previous agent runs live at \`${ctx.filePath}\`. Use this file as durable context (test commands, conventions, gotchas, architecture notes).`;
|
|
153294
|
+
const tocBody = ctx.headings.length === 0 ? "(no headings yet \u2014 file is empty or a flat list. read the whole file. during the post-run reflection turn, structure it with `## ` / `### ` headings so future runs can read targeted ranges.)" : `Read targeted line ranges via your native file tool \u2014 do NOT slurp the whole file. Each range starts at the section heading line, so reading the range gives you heading + body together.
|
|
153295
|
+
|
|
153296
|
+
${renderLearningsToc(ctx.headings)}`;
|
|
153297
|
+
return `************* LEARNINGS *************
|
|
152941
153298
|
|
|
152942
|
-
|
|
153299
|
+
${intro}
|
|
153300
|
+
|
|
153301
|
+
${tocBody}`;
|
|
153302
|
+
}
|
|
153303
|
+
function assembleFullPrompt(ctx) {
|
|
153304
|
+
const learningsSection = buildLearningsSection({
|
|
153305
|
+
filePath: ctx.learningsFilePath,
|
|
153306
|
+
headings: ctx.learningsHeadings
|
|
153307
|
+
});
|
|
152943
153308
|
const runtimeSection = `************* RUNTIME *************
|
|
152944
153309
|
|
|
152945
153310
|
${ctx.runtime}`;
|
|
@@ -152967,7 +153332,10 @@ function resolveInstructions(ctx) {
|
|
|
152967
153332
|
tocEntries.push({ label: "EVENT CONTEXT", description: "related PR/issue data" });
|
|
152968
153333
|
tocEntries.push({ label: "SYSTEM", description: "persona, security, tools, workflow rules" });
|
|
152969
153334
|
if (pctx.learningsFilePath)
|
|
152970
|
-
tocEntries.push({
|
|
153335
|
+
tocEntries.push({
|
|
153336
|
+
label: "LEARNINGS",
|
|
153337
|
+
description: "repo-specific knowledge file path + heading TOC"
|
|
153338
|
+
});
|
|
152971
153339
|
tocEntries.push({ label: "RUNTIME", description: "environment metadata" });
|
|
152972
153340
|
const toc = buildToc(tocEntries);
|
|
152973
153341
|
const full = assembleFullPrompt({
|
|
@@ -152977,6 +153345,7 @@ function resolveInstructions(ctx) {
|
|
|
152977
153345
|
eventContext,
|
|
152978
153346
|
system,
|
|
152979
153347
|
learningsFilePath: pctx.learningsFilePath,
|
|
153348
|
+
learningsHeadings: pctx.learningsHeadings,
|
|
152980
153349
|
runtime: pctx.runtime
|
|
152981
153350
|
});
|
|
152982
153351
|
const event = [pctx.eventTitle, pctx.eventMetadata].filter(Boolean).join("\n\n---\n\n");
|
|
@@ -152994,7 +153363,7 @@ function resolveInstructions(ctx) {
|
|
|
152994
153363
|
import { mkdir, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
152995
153364
|
import { dirname as dirname4, join as join14 } from "node:path";
|
|
152996
153365
|
var LEARNINGS_FILE_NAME = "pullfrog-learnings.md";
|
|
152997
|
-
var MAX_LEARNINGS_LENGTH =
|
|
153366
|
+
var MAX_LEARNINGS_LENGTH = 1e5;
|
|
152998
153367
|
function learningsFilePath(tmpdir3) {
|
|
152999
153368
|
return join14(tmpdir3, LEARNINGS_FILE_NAME);
|
|
153000
153369
|
}
|
|
@@ -153004,6 +153373,15 @@ async function seedLearningsFile(params) {
|
|
|
153004
153373
|
await writeFile2(path3, params.current ?? "", "utf8");
|
|
153005
153374
|
return path3;
|
|
153006
153375
|
}
|
|
153376
|
+
var TRUNCATION_LINE_BOUNDARY_TOLERANCE = 4096;
|
|
153377
|
+
function truncateAtLineBoundary(body, cap) {
|
|
153378
|
+
if (body.length <= cap) return body;
|
|
153379
|
+
const head = body.slice(0, cap);
|
|
153380
|
+
const lastNewline = head.lastIndexOf("\n");
|
|
153381
|
+
if (lastNewline <= 0) return head;
|
|
153382
|
+
if (cap - lastNewline > TRUNCATION_LINE_BOUNDARY_TOLERANCE) return head;
|
|
153383
|
+
return head.slice(0, lastNewline);
|
|
153384
|
+
}
|
|
153007
153385
|
async function readLearningsFile(path3) {
|
|
153008
153386
|
let raw2;
|
|
153009
153387
|
try {
|
|
@@ -153011,16 +153389,26 @@ async function readLearningsFile(path3) {
|
|
|
153011
153389
|
} catch {
|
|
153012
153390
|
return null;
|
|
153013
153391
|
}
|
|
153014
|
-
|
|
153015
|
-
if (trimmed.length > MAX_LEARNINGS_LENGTH) return trimmed.slice(0, MAX_LEARNINGS_LENGTH);
|
|
153016
|
-
return trimmed;
|
|
153392
|
+
return truncateAtLineBoundary(raw2.trim(), MAX_LEARNINGS_LENGTH);
|
|
153017
153393
|
}
|
|
153018
153394
|
|
|
153019
153395
|
// utils/normalizeEnv.ts
|
|
153020
|
-
|
|
153021
|
-
|
|
153022
|
-
|
|
153396
|
+
var core4 = __toESM(require_core(), 1);
|
|
153397
|
+
function sanitizeSecret(key, value2) {
|
|
153398
|
+
const trimmed = value2.trim();
|
|
153399
|
+
if (trimmed.length === 0) {
|
|
153400
|
+
log.warning(
|
|
153401
|
+
`\xBB ${key} is whitespace-only \u2014 leaving env var unchanged. check your secret value.`
|
|
153402
|
+
);
|
|
153403
|
+
return null;
|
|
153404
|
+
}
|
|
153405
|
+
if (trimmed !== value2) {
|
|
153406
|
+
log.warning(
|
|
153407
|
+
`\xBB stripped whitespace from ${key} (whitespace in secret values breaks GitHub Actions log masking)`
|
|
153408
|
+
);
|
|
153023
153409
|
}
|
|
153410
|
+
core4.setSecret(trimmed);
|
|
153411
|
+
return trimmed;
|
|
153024
153412
|
}
|
|
153025
153413
|
function normalizeEnv() {
|
|
153026
153414
|
const upperKeys = /* @__PURE__ */ new Map();
|
|
@@ -153031,11 +153419,6 @@ function normalizeEnv() {
|
|
|
153031
153419
|
upperKeys.set(upper2, existing);
|
|
153032
153420
|
}
|
|
153033
153421
|
for (const [upperKey, keys] of upperKeys) {
|
|
153034
|
-
if (isSensitiveEnvName(upperKey)) {
|
|
153035
|
-
for (const key of keys) {
|
|
153036
|
-
maskValue(process.env[key]);
|
|
153037
|
-
}
|
|
153038
|
-
}
|
|
153039
153422
|
if (keys.length === 1) {
|
|
153040
153423
|
const key = keys[0];
|
|
153041
153424
|
if (key !== upperKey) {
|
|
@@ -153058,10 +153441,17 @@ function normalizeEnv() {
|
|
|
153058
153441
|
}
|
|
153059
153442
|
process.env[upperKey] = preferredValue;
|
|
153060
153443
|
}
|
|
153444
|
+
for (const key of Object.keys(process.env)) {
|
|
153445
|
+
if (!isSensitiveEnvName(key)) continue;
|
|
153446
|
+
const value2 = process.env[key];
|
|
153447
|
+
if (typeof value2 !== "string" || value2.length === 0) continue;
|
|
153448
|
+
const sanitized = sanitizeSecret(key, value2);
|
|
153449
|
+
if (sanitized !== null) process.env[key] = sanitized;
|
|
153450
|
+
}
|
|
153061
153451
|
}
|
|
153062
153452
|
|
|
153063
153453
|
// utils/payload.ts
|
|
153064
|
-
var
|
|
153454
|
+
var core5 = __toESM(require_core(), 1);
|
|
153065
153455
|
import { isAbsolute as isAbsolute2, resolve as resolve2 } from "node:path";
|
|
153066
153456
|
|
|
153067
153457
|
// utils/versioning.ts
|
|
@@ -153125,7 +153515,7 @@ function resolveCwd(cwd) {
|
|
|
153125
153515
|
return workspace ? resolve2(workspace, cwd) : cwd;
|
|
153126
153516
|
}
|
|
153127
153517
|
function resolvePromptInput() {
|
|
153128
|
-
const prompt =
|
|
153518
|
+
const prompt = core5.getInput("prompt", { required: true });
|
|
153129
153519
|
let parsed2;
|
|
153130
153520
|
try {
|
|
153131
153521
|
parsed2 = JSON.parse(prompt);
|
|
@@ -153141,11 +153531,11 @@ function resolvePromptInput() {
|
|
|
153141
153531
|
}
|
|
153142
153532
|
function resolveNonPromptInputs() {
|
|
153143
153533
|
return Inputs.omit("prompt").assert({
|
|
153144
|
-
model:
|
|
153145
|
-
timeout:
|
|
153146
|
-
cwd:
|
|
153147
|
-
push:
|
|
153148
|
-
shell:
|
|
153534
|
+
model: core5.getInput("model") || void 0,
|
|
153535
|
+
timeout: core5.getInput("timeout") || void 0,
|
|
153536
|
+
cwd: core5.getInput("cwd") || void 0,
|
|
153537
|
+
push: core5.getInput("push") || void 0,
|
|
153538
|
+
shell: core5.getInput("shell") || void 0
|
|
153149
153539
|
});
|
|
153150
153540
|
}
|
|
153151
153541
|
var isPullfrog = (actor) => {
|
|
@@ -153346,6 +153736,7 @@ var defaultSettings = {
|
|
|
153346
153736
|
prApproveEnabled: false,
|
|
153347
153737
|
modeInstructions: {},
|
|
153348
153738
|
learnings: null,
|
|
153739
|
+
learningsHeadings: [],
|
|
153349
153740
|
envAllowlist: null
|
|
153350
153741
|
};
|
|
153351
153742
|
var defaultRunContext = {
|
|
@@ -153386,7 +153777,8 @@ async function fetchRunContext(params) {
|
|
|
153386
153777
|
setupScript: data.settings?.setupScript ?? null,
|
|
153387
153778
|
postCheckoutScript: data.settings?.postCheckoutScript ?? null,
|
|
153388
153779
|
prepushScript: data.settings?.prepushScript ?? null,
|
|
153389
|
-
stopScript: data.settings?.stopScript ?? null
|
|
153780
|
+
stopScript: data.settings?.stopScript ?? null,
|
|
153781
|
+
learningsHeadings: data.settings?.learningsHeadings ?? []
|
|
153390
153782
|
},
|
|
153391
153783
|
apiToken: data.apiToken,
|
|
153392
153784
|
oss: data.oss ?? false,
|
|
@@ -153401,13 +153793,13 @@ async function fetchRunContext(params) {
|
|
|
153401
153793
|
}
|
|
153402
153794
|
|
|
153403
153795
|
// utils/runContextData.ts
|
|
153404
|
-
var
|
|
153796
|
+
var core6 = __toESM(require_core(), 1);
|
|
153405
153797
|
async function resolveRunContextData(params) {
|
|
153406
153798
|
log.info(`\xBB running Pullfrog v${package_default.version}...`);
|
|
153407
153799
|
const repoContext = parseRepoContext();
|
|
153408
153800
|
let oidcToken;
|
|
153409
153801
|
try {
|
|
153410
|
-
oidcToken = await
|
|
153802
|
+
oidcToken = await core6.getIDToken("pullfrog-api");
|
|
153411
153803
|
} catch {
|
|
153412
153804
|
}
|
|
153413
153805
|
const [repoResponse, runContext] = await Promise.all([
|
|
@@ -153710,7 +154102,7 @@ async function resolveRun(params) {
|
|
|
153710
154102
|
|
|
153711
154103
|
// main.ts
|
|
153712
154104
|
function resolveOutputSchema() {
|
|
153713
|
-
const raw2 =
|
|
154105
|
+
const raw2 = core7.getInput("output_schema");
|
|
153714
154106
|
if (!raw2) return void 0;
|
|
153715
154107
|
let parsed2;
|
|
153716
154108
|
try {
|
|
@@ -153878,7 +154270,7 @@ async function buildProxyTokenHeaders(ctx) {
|
|
|
153878
154270
|
if (ctx.oidcCredentials) {
|
|
153879
154271
|
process.env.ACTIONS_ID_TOKEN_REQUEST_URL = ctx.oidcCredentials.requestUrl;
|
|
153880
154272
|
process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = ctx.oidcCredentials.requestToken;
|
|
153881
|
-
const oidcToken = await
|
|
154273
|
+
const oidcToken = await core7.getIDToken("pullfrog-api");
|
|
153882
154274
|
delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
153883
154275
|
delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
153884
154276
|
return { Authorization: `Bearer ${oidcToken}` };
|
|
@@ -153900,7 +154292,7 @@ async function resolveProxyModel(ctx) {
|
|
|
153900
154292
|
const key = await mintProxyKey({ oidcCredentials: ctx.oidcCredentials, repo: ctx.repo });
|
|
153901
154293
|
if (!key) return;
|
|
153902
154294
|
process.env.OPENROUTER_API_KEY = key;
|
|
153903
|
-
|
|
154295
|
+
core7.setSecret(key);
|
|
153904
154296
|
ctx.payload.proxyModel = ctx.proxyModel;
|
|
153905
154297
|
const label = ctx.oss ? "oss" : "router";
|
|
153906
154298
|
log.info(`\xBB proxy: ${label} \u2192 ${ctx.proxyModel}`);
|
|
@@ -154012,8 +154404,8 @@ async function main() {
|
|
|
154012
154404
|
if (runContext.dbSecrets) {
|
|
154013
154405
|
for (const [key, value2] of Object.entries(runContext.dbSecrets)) {
|
|
154014
154406
|
if (!process.env[key]) {
|
|
154015
|
-
|
|
154016
|
-
|
|
154407
|
+
const sanitized = sanitizeSecret(key, value2);
|
|
154408
|
+
if (sanitized !== null) process.env[key] = sanitized;
|
|
154017
154409
|
}
|
|
154018
154410
|
}
|
|
154019
154411
|
const count = Object.keys(runContext.dbSecrets).length;
|
|
@@ -154152,10 +154544,7 @@ async function main() {
|
|
|
154152
154544
|
current: runContext.repoSettings.learnings
|
|
154153
154545
|
});
|
|
154154
154546
|
toolState.learningsFilePath = learningsPath;
|
|
154155
|
-
|
|
154156
|
-
toolState.learningsSeed = await readFile4(learningsPath, "utf8");
|
|
154157
|
-
} catch {
|
|
154158
|
-
}
|
|
154547
|
+
toolState.learningsSeed = (runContext.repoSettings.learnings ?? "").trim();
|
|
154159
154548
|
log.info(
|
|
154160
154549
|
`\xBB learnings seeded at ${learningsPath} (existing=${runContext.repoSettings.learnings ? "yes" : "no"})`
|
|
154161
154550
|
);
|
|
@@ -154195,7 +154584,8 @@ async function main() {
|
|
|
154195
154584
|
modes: modes2,
|
|
154196
154585
|
agentId,
|
|
154197
154586
|
outputSchema,
|
|
154198
|
-
learningsFilePath: toolState.learningsFilePath ?? null
|
|
154587
|
+
learningsFilePath: toolState.learningsFilePath ?? null,
|
|
154588
|
+
learningsHeadings: runContext.repoSettings.learningsHeadings
|
|
154199
154589
|
});
|
|
154200
154590
|
const logParts = [
|
|
154201
154591
|
instructions.eventInstructions ? `EVENT-LEVEL INSTRUCTIONS:
|
|
@@ -154332,10 +154722,13 @@ ${instructions.user}` : null,
|
|
|
154332
154722
|
await persistLearnings(toolContext);
|
|
154333
154723
|
}
|
|
154334
154724
|
if (!result.success && toolContext && toolState.progressComment) {
|
|
154335
|
-
|
|
154336
|
-
|
|
154337
|
-
|
|
154338
|
-
|
|
154725
|
+
const rawError = result.error || "agent run failed";
|
|
154726
|
+
const errorBody = isApiKeyAuthError(rawError) ? formatApiKeyErrorSummary({
|
|
154727
|
+
owner: runContext.repo.owner,
|
|
154728
|
+
name: runContext.repo.name,
|
|
154729
|
+
raw: rawError
|
|
154730
|
+
}) : rawError;
|
|
154731
|
+
await reportErrorToComment({ toolState, error: errorBody }).catch((error49) => {
|
|
154339
154732
|
log.debug(`failure error report failed: ${error49}`);
|
|
154340
154733
|
});
|
|
154341
154734
|
}
|
|
@@ -154351,7 +154744,7 @@ ${instructions.user}` : null,
|
|
|
154351
154744
|
}
|
|
154352
154745
|
if (toolState.output) {
|
|
154353
154746
|
log.info(`::pullfrog-output::${Buffer.from(toolState.output).toString("base64")}`);
|
|
154354
|
-
|
|
154747
|
+
core7.setOutput("result", toolState.output);
|
|
154355
154748
|
}
|
|
154356
154749
|
return await handleAgentResult({
|
|
154357
154750
|
result,
|
|
@@ -154371,8 +154764,13 @@ ${instructions.user}` : null,
|
|
|
154371
154764
|
killTrackedChildren();
|
|
154372
154765
|
log.error(errorMessage);
|
|
154373
154766
|
const billingError = isRouterKeylimitExhaustedError(errorMessage) ? new BillingError(errorMessage, { code: "router_keylimit_exhausted" }) : null;
|
|
154767
|
+
const apiKeyErrorSummary = !billingError && isApiKeyAuthError(errorMessage) ? formatApiKeyErrorSummary({
|
|
154768
|
+
owner: runContext.repo.owner,
|
|
154769
|
+
name: runContext.repo.name,
|
|
154770
|
+
raw: errorMessage
|
|
154771
|
+
}) : null;
|
|
154374
154772
|
try {
|
|
154375
|
-
const errorSummary = billingError ? formatBillingErrorSummary(billingError, runContext.repo.owner) : `### \u274C Pullfrog failed
|
|
154773
|
+
const errorSummary = billingError ? formatBillingErrorSummary(billingError, runContext.repo.owner) : apiKeyErrorSummary ?? `### \u274C Pullfrog failed
|
|
154376
154774
|
|
|
154377
154775
|
\`\`\`
|
|
154378
154776
|
${errorMessage}
|
|
@@ -154383,7 +154781,7 @@ ${errorMessage}
|
|
|
154383
154781
|
} catch {
|
|
154384
154782
|
}
|
|
154385
154783
|
try {
|
|
154386
|
-
const commentBody = billingError ? formatBillingErrorSummary(billingError, runContext.repo.owner) : errorMessage;
|
|
154784
|
+
const commentBody = billingError ? formatBillingErrorSummary(billingError, runContext.repo.owner) : apiKeyErrorSummary ?? errorMessage;
|
|
154387
154785
|
await reportErrorToComment({ toolState, error: commentBody });
|
|
154388
154786
|
} catch {
|
|
154389
154787
|
}
|