pullfrog 0.1.6 → 0.1.8
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/subagentModels.d.ts +19 -0
- package/dist/cli.mjs +753 -395
- package/dist/index.js +748 -392
- package/dist/internal.js +150 -60
- package/dist/mcp/shell.d.ts +5 -0
- package/dist/models.d.ts +63 -3
- package/dist/toolState.d.ts +2 -0
- package/dist/utils/agent.d.ts +5 -2
- package/dist/utils/agentHangReport.d.ts +38 -0
- package/dist/utils/apiKeys.d.ts +18 -0
- package/dist/utils/gitAuth.d.ts +27 -0
- package/dist/utils/instructions.d.ts +19 -0
- package/dist/utils/learnings.d.ts +20 -9
- package/dist/utils/providerErrors.d.ts +11 -0
- package/dist/utils/runContext.d.ts +16 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -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);
|
|
@@ -142355,7 +142414,7 @@ var import_semver = __toESM(require_semver2(), 1);
|
|
|
142355
142414
|
// package.json
|
|
142356
142415
|
var package_default = {
|
|
142357
142416
|
name: "pullfrog",
|
|
142358
|
-
version: "0.1.
|
|
142417
|
+
version: "0.1.8",
|
|
142359
142418
|
type: "module",
|
|
142360
142419
|
bin: {
|
|
142361
142420
|
pullfrog: "dist/cli.mjs",
|
|
@@ -142823,6 +142882,51 @@ function readNumber(params) {
|
|
|
142823
142882
|
import { execSync } from "node:child_process";
|
|
142824
142883
|
import { createHash } from "node:crypto";
|
|
142825
142884
|
import { readFileSync as readFileSync2, realpathSync, unlinkSync } from "node:fs";
|
|
142885
|
+
|
|
142886
|
+
// utils/shell.ts
|
|
142887
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
142888
|
+
function $(cmd, args2, options) {
|
|
142889
|
+
const encoding = options?.encoding ?? "utf-8";
|
|
142890
|
+
const env2 = resolveEnv(options?.env);
|
|
142891
|
+
const result = spawnSync2(cmd, args2, {
|
|
142892
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
142893
|
+
encoding,
|
|
142894
|
+
cwd: options?.cwd,
|
|
142895
|
+
env: env2
|
|
142896
|
+
});
|
|
142897
|
+
const stdout = result.stdout ?? "";
|
|
142898
|
+
const stderr = result.stderr ?? "";
|
|
142899
|
+
if (options?.log !== false) {
|
|
142900
|
+
const canWriteToStdout = process.stdout.isTTY === true;
|
|
142901
|
+
if (stdout) {
|
|
142902
|
+
if (canWriteToStdout) {
|
|
142903
|
+
process.stdout.write(stdout);
|
|
142904
|
+
} else {
|
|
142905
|
+
process.stderr.write(stdout);
|
|
142906
|
+
}
|
|
142907
|
+
}
|
|
142908
|
+
if (stderr) {
|
|
142909
|
+
process.stderr.write(stderr);
|
|
142910
|
+
}
|
|
142911
|
+
}
|
|
142912
|
+
if (result.status !== 0) {
|
|
142913
|
+
const errorResult = {
|
|
142914
|
+
status: result.status ?? -1,
|
|
142915
|
+
stdout,
|
|
142916
|
+
stderr
|
|
142917
|
+
};
|
|
142918
|
+
if (options?.onError) {
|
|
142919
|
+
options.onError(errorResult);
|
|
142920
|
+
return stdout.trim();
|
|
142921
|
+
}
|
|
142922
|
+
throw new Error(
|
|
142923
|
+
`Command failed with exit code ${errorResult.status}: ${stderr || "Unknown error"}`
|
|
142924
|
+
);
|
|
142925
|
+
}
|
|
142926
|
+
return stdout.trim();
|
|
142927
|
+
}
|
|
142928
|
+
|
|
142929
|
+
// utils/gitAuth.ts
|
|
142826
142930
|
var gitBinary;
|
|
142827
142931
|
function hashFile(path3) {
|
|
142828
142932
|
return createHash("sha256").update(readFileSync2(path3)).digest("hex");
|
|
@@ -142914,6 +143018,27 @@ ${stdout}` : stderr || stdout || "(no output)";
|
|
|
142914
143018
|
}
|
|
142915
143019
|
}
|
|
142916
143020
|
}
|
|
143021
|
+
var SHALLOW_UNREACHABLE_PATTERNS = [
|
|
143022
|
+
/Could not read [a-f0-9]{40,64}/,
|
|
143023
|
+
/remote did not send all necessary objects/
|
|
143024
|
+
];
|
|
143025
|
+
var DEEPEN_RETRY_DEPTH = 1e3;
|
|
143026
|
+
async function $gitFetchWithDeepen(args2, options, label) {
|
|
143027
|
+
try {
|
|
143028
|
+
return await $git("fetch", args2, options);
|
|
143029
|
+
} catch (err) {
|
|
143030
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
143031
|
+
const isShallowUnreachable = SHALLOW_UNREACHABLE_PATTERNS.some((p) => p.test(msg));
|
|
143032
|
+
if (!isShallowUnreachable) throw err;
|
|
143033
|
+
const isShallow = $("git", ["rev-parse", "--is-shallow-repository"], { log: false }).trim() === "true";
|
|
143034
|
+
if (!isShallow) throw err;
|
|
143035
|
+
log.info(
|
|
143036
|
+
`\xBB ${label ?? "git fetch"} hit shallow-unreachable error, retrying with --deepen=${DEEPEN_RETRY_DEPTH}`
|
|
143037
|
+
);
|
|
143038
|
+
const retryArgs = args2.filter((a) => !a.startsWith("--depth="));
|
|
143039
|
+
return await $git("fetch", [`--deepen=${DEEPEN_RETRY_DEPTH}`, ...retryArgs], options);
|
|
143040
|
+
}
|
|
143041
|
+
}
|
|
142917
143042
|
|
|
142918
143043
|
// lifecycle.ts
|
|
142919
143044
|
var LIFECYCLE_HOOK_TIMEOUT_MS = 6e5;
|
|
@@ -142955,49 +143080,6 @@ async function executeLifecycleHook(params) {
|
|
|
142955
143080
|
}
|
|
142956
143081
|
}
|
|
142957
143082
|
|
|
142958
|
-
// utils/shell.ts
|
|
142959
|
-
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
142960
|
-
function $(cmd, args2, options) {
|
|
142961
|
-
const encoding = options?.encoding ?? "utf-8";
|
|
142962
|
-
const env2 = resolveEnv(options?.env);
|
|
142963
|
-
const result = spawnSync2(cmd, args2, {
|
|
142964
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
142965
|
-
encoding,
|
|
142966
|
-
cwd: options?.cwd,
|
|
142967
|
-
env: env2
|
|
142968
|
-
});
|
|
142969
|
-
const stdout = result.stdout ?? "";
|
|
142970
|
-
const stderr = result.stderr ?? "";
|
|
142971
|
-
if (options?.log !== false) {
|
|
142972
|
-
const canWriteToStdout = process.stdout.isTTY === true;
|
|
142973
|
-
if (stdout) {
|
|
142974
|
-
if (canWriteToStdout) {
|
|
142975
|
-
process.stdout.write(stdout);
|
|
142976
|
-
} else {
|
|
142977
|
-
process.stderr.write(stdout);
|
|
142978
|
-
}
|
|
142979
|
-
}
|
|
142980
|
-
if (stderr) {
|
|
142981
|
-
process.stderr.write(stderr);
|
|
142982
|
-
}
|
|
142983
|
-
}
|
|
142984
|
-
if (result.status !== 0) {
|
|
142985
|
-
const errorResult = {
|
|
142986
|
-
status: result.status ?? -1,
|
|
142987
|
-
stdout,
|
|
142988
|
-
stderr
|
|
142989
|
-
};
|
|
142990
|
-
if (options?.onError) {
|
|
142991
|
-
options.onError(errorResult);
|
|
142992
|
-
return stdout.trim();
|
|
142993
|
-
}
|
|
142994
|
-
throw new Error(
|
|
142995
|
-
`Command failed with exit code ${errorResult.status}: ${stderr || "Unknown error"}`
|
|
142996
|
-
);
|
|
142997
|
-
}
|
|
142998
|
-
return stdout.trim();
|
|
142999
|
-
}
|
|
143000
|
-
|
|
143001
143083
|
// utils/rangeDiff.ts
|
|
143002
143084
|
function computeIncrementalDiff(params) {
|
|
143003
143085
|
try {
|
|
@@ -143211,7 +143293,7 @@ function PushBranchTool(ctx) {
|
|
|
143211
143293
|
const pushPermission = ctx.payload.push;
|
|
143212
143294
|
return tool({
|
|
143213
143295
|
name: "push_branch",
|
|
143214
|
-
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.",
|
|
143296
|
+
description: "Push the current branch to the remote repository. Omit branchName to push the current branch (recommended). Example: `push_branch({})` to push the current branch. Example: `push_branch({ branchName: \"pr-1\" })` to push a specific local branch. If specifying branchName, use the LOCAL branch name (e.g., 'pr-1'), not the remote branch name. The correct remote and remote branch are determined automatically from branch config set by checkout_pr. Requires a clean working tree. Runs the repository prepush hook (if configured) 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.",
|
|
143215
143297
|
parameters: PushBranch,
|
|
143216
143298
|
execute: execute(async ({ branchName, force }) => {
|
|
143217
143299
|
if (pushPermission === "disabled") {
|
|
@@ -143350,7 +143432,7 @@ var Git = type({
|
|
|
143350
143432
|
function GitTool(ctx) {
|
|
143351
143433
|
return tool({
|
|
143352
143434
|
name: "git",
|
|
143353
|
-
description:
|
|
143435
|
+
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\'.',
|
|
143354
143436
|
parameters: Git,
|
|
143355
143437
|
execute: execute(async (params) => {
|
|
143356
143438
|
const command = params.command;
|
|
@@ -143392,15 +143474,10 @@ var GitFetch = type({
|
|
|
143392
143474
|
ref: type.string.describe("Ref to fetch: branch name, tag, or 'pull/N/head' for PRs"),
|
|
143393
143475
|
depth: type.number.describe("Fetch depth (for shallow clones)").optional()
|
|
143394
143476
|
});
|
|
143395
|
-
var SHALLOW_UNREACHABLE_PATTERNS = [
|
|
143396
|
-
/Could not read [a-f0-9]{40,64}/,
|
|
143397
|
-
/remote did not send all necessary objects/
|
|
143398
|
-
];
|
|
143399
|
-
var DEEPEN_RETRY_DEPTH = 1e3;
|
|
143400
143477
|
function GitFetchTool(ctx) {
|
|
143401
143478
|
return tool({
|
|
143402
143479
|
name: "git_fetch",
|
|
143403
|
-
description:
|
|
143480
|
+
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 })`.',
|
|
143404
143481
|
parameters: GitFetch,
|
|
143405
143482
|
execute: execute(async (params) => {
|
|
143406
143483
|
rejectIfLeadingDash(params.ref, "ref");
|
|
@@ -143408,20 +143485,7 @@ function GitFetchTool(ctx) {
|
|
|
143408
143485
|
if (params.depth !== void 0) {
|
|
143409
143486
|
fetchArgs.push(`--depth=${params.depth}`);
|
|
143410
143487
|
}
|
|
143411
|
-
|
|
143412
|
-
await $git("fetch", fetchArgs, { token: ctx.gitToken });
|
|
143413
|
-
} catch (err) {
|
|
143414
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
143415
|
-
const isShallowUnreachable = SHALLOW_UNREACHABLE_PATTERNS.some((p) => p.test(msg));
|
|
143416
|
-
const isShallow = isShallowUnreachable && $("git", ["rev-parse", "--is-shallow-repository"], { log: false }).trim() === "true";
|
|
143417
|
-
if (!isShallow) throw err;
|
|
143418
|
-
log.info(
|
|
143419
|
-
`\xBB git_fetch hit shallow-unreachable error, retrying with --deepen=${DEEPEN_RETRY_DEPTH}`
|
|
143420
|
-
);
|
|
143421
|
-
await $git("fetch", [`--deepen=${DEEPEN_RETRY_DEPTH}`, "--no-tags", "origin", params.ref], {
|
|
143422
|
-
token: ctx.gitToken
|
|
143423
|
-
});
|
|
143424
|
-
}
|
|
143488
|
+
await $gitFetchWithDeepen(fetchArgs, { token: ctx.gitToken }, "git_fetch");
|
|
143425
143489
|
return { success: true, ref: params.ref };
|
|
143426
143490
|
})
|
|
143427
143491
|
});
|
|
@@ -143634,13 +143698,15 @@ var CreatePullRequestReview = type({
|
|
|
143634
143698
|
approved: type.boolean.describe(
|
|
143635
143699
|
"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."
|
|
143636
143700
|
).optional(),
|
|
143637
|
-
commit_id: type.string.describe(
|
|
143701
|
+
commit_id: type.string.describe(
|
|
143702
|
+
"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."
|
|
143703
|
+
).optional(),
|
|
143638
143704
|
comments: type({
|
|
143639
143705
|
path: type.string.describe(
|
|
143640
143706
|
"The file path to comment on (relative to repo root). Must be a file that appears in the PR diff."
|
|
143641
143707
|
),
|
|
143642
143708
|
line: type.number.describe(
|
|
143643
|
-
"Line number to comment on. For multi-line ranges, this is the end line. Use NEW column from diff format."
|
|
143709
|
+
"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)."
|
|
143644
143710
|
),
|
|
143645
143711
|
side: type.enumerated("LEFT", "RIGHT").describe(
|
|
143646
143712
|
"Side of the diff: LEFT (old code, lines starting with -) or RIGHT (new code, lines starting with + or unchanged). Defaults to RIGHT."
|
|
@@ -143650,7 +143716,7 @@ var CreatePullRequestReview = type({
|
|
|
143650
143716
|
"Full replacement code for the line range [start_line, line]. MUST preserve the exact indentation of the original code."
|
|
143651
143717
|
).optional(),
|
|
143652
143718
|
start_line: type.number.describe(
|
|
143653
|
-
"Start line for multi-line comment ranges. Omit for single-line comments. The range [start_line, line] defines which lines a suggestion replaces."
|
|
143719
|
+
"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."
|
|
143654
143720
|
).optional()
|
|
143655
143721
|
}).array().describe(
|
|
143656
143722
|
"Inline comments on lines within diff hunks. Feedback about code outside the diff goes in 'body' instead."
|
|
@@ -143659,7 +143725,7 @@ var CreatePullRequestReview = type({
|
|
|
143659
143725
|
function CreatePullRequestReviewTool(ctx) {
|
|
143660
143726
|
return tool({
|
|
143661
143727
|
name: "create_pull_request_review",
|
|
143662
|
-
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.`,
|
|
143728
|
+
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.`,
|
|
143663
143729
|
parameters: CreatePullRequestReview,
|
|
143664
143730
|
execute: execute(async ({ pull_number, body, approved, commit_id, comments = [] }) => {
|
|
143665
143731
|
if (body) body = fixDoubleEscapedString(body);
|
|
@@ -143888,7 +143954,7 @@ function runDiffCoveragePreflight(params) {
|
|
|
143888
143954
|
);
|
|
143889
143955
|
const unreadText = unread.map((entry) => `- ${entry.path} (${entry.unreadLines} lines, ${entry.ranges})`).join("\n");
|
|
143890
143956
|
throw new Error(
|
|
143891
|
-
`diff coverage pre-flight: some TOC regions were not read before review submission. this is a one-time nudge \u2014
|
|
143957
|
+
`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.
|
|
143892
143958
|
|
|
143893
143959
|
unread TOC regions:
|
|
143894
143960
|
${unreadText}
|
|
@@ -144143,10 +144209,10 @@ async function ensureBeforeShaReachable(params) {
|
|
|
144143
144209
|
sha: params.sha,
|
|
144144
144210
|
ref: tempBranch
|
|
144145
144211
|
}), true);
|
|
144146
|
-
await $
|
|
144147
|
-
"fetch",
|
|
144212
|
+
await $gitFetchWithDeepen(
|
|
144148
144213
|
["--no-tags", ...params.isShallow ? ["--depth=1"] : [], "origin", tempBranch],
|
|
144149
|
-
{ token: params.gitToken }
|
|
144214
|
+
{ token: params.gitToken },
|
|
144215
|
+
`before_sha temp branch ${tempBranch}`
|
|
144150
144216
|
);
|
|
144151
144217
|
log.debug(`\xBB fetched before_sha via temp branch ${tempBranch}`);
|
|
144152
144218
|
return true;
|
|
@@ -144222,16 +144288,22 @@ async function checkoutPrBranch(pr, params) {
|
|
|
144222
144288
|
toolState.checkoutSha = $("git", ["rev-parse", "HEAD"], { log: false }).trim();
|
|
144223
144289
|
const alreadyOnBranch = toolState.checkoutSha === pr.headSha;
|
|
144224
144290
|
log.debug(`\xBB fetching base branch (${pr.baseRef})...`);
|
|
144225
|
-
await $
|
|
144291
|
+
await $gitFetchWithDeepen(
|
|
144292
|
+
["--no-tags", "origin", pr.baseRef],
|
|
144293
|
+
{ token: gitToken },
|
|
144294
|
+
`base branch ${pr.baseRef}`
|
|
144295
|
+
);
|
|
144226
144296
|
if (!alreadyOnBranch) {
|
|
144227
144297
|
$("git", ["checkout", "-B", pr.baseRef, `origin/${pr.baseRef}`], { log: false });
|
|
144228
144298
|
log.debug(`\xBB fetching PR #${pr.number} (${localBranch})...`);
|
|
144229
144299
|
await retry(
|
|
144230
144300
|
async () => {
|
|
144231
144301
|
try {
|
|
144232
|
-
await $
|
|
144233
|
-
|
|
144234
|
-
|
|
144302
|
+
await $gitFetchWithDeepen(
|
|
144303
|
+
["--no-tags", "origin", `+pull/${pr.number}/head:${localBranch}`],
|
|
144304
|
+
{ token: gitToken },
|
|
144305
|
+
`PR #${pr.number}`
|
|
144306
|
+
);
|
|
144235
144307
|
} catch (e) {
|
|
144236
144308
|
const msg = e instanceof Error ? e.message : String(e);
|
|
144237
144309
|
if (PULL_REF_MISSING_PATTERN.test(msg)) {
|
|
@@ -144332,134 +144404,159 @@ async function checkoutPrBranch(pr, params) {
|
|
|
144332
144404
|
});
|
|
144333
144405
|
return { hookWarning: postCheckoutHook.warning };
|
|
144334
144406
|
}
|
|
144407
|
+
var inFlightCheckouts = /* @__PURE__ */ new Map();
|
|
144335
144408
|
function CheckoutPrTool(ctx) {
|
|
144336
|
-
|
|
144337
|
-
|
|
144338
|
-
|
|
144339
|
-
|
|
144340
|
-
|
|
144341
|
-
|
|
144342
|
-
|
|
144343
|
-
|
|
144344
|
-
|
|
144345
|
-
|
|
144346
|
-
|
|
144347
|
-
|
|
144348
|
-
|
|
144349
|
-
|
|
144350
|
-
|
|
144351
|
-
|
|
144352
|
-
|
|
144353
|
-
|
|
144354
|
-
|
|
144355
|
-
|
|
144356
|
-
|
|
144357
|
-
|
|
144358
|
-
|
|
144359
|
-
|
|
144360
|
-
|
|
144361
|
-
|
|
144362
|
-
|
|
144363
|
-
|
|
144364
|
-
|
|
144365
|
-
|
|
144366
|
-
|
|
144367
|
-
|
|
144409
|
+
const runCheckout = async (pull_number) => {
|
|
144410
|
+
const prResponse = await ctx.octokit.rest.pulls.get({
|
|
144411
|
+
owner: ctx.repo.owner,
|
|
144412
|
+
repo: ctx.repo.name,
|
|
144413
|
+
pull_number
|
|
144414
|
+
});
|
|
144415
|
+
const headRepo = prResponse.data.head.repo;
|
|
144416
|
+
if (!headRepo) {
|
|
144417
|
+
throw new Error(`PR #${pull_number} source repository was deleted`);
|
|
144418
|
+
}
|
|
144419
|
+
const pr = {
|
|
144420
|
+
number: pull_number,
|
|
144421
|
+
headSha: prResponse.data.head.sha,
|
|
144422
|
+
headRef: prResponse.data.head.ref,
|
|
144423
|
+
headRepoFullName: headRepo.full_name,
|
|
144424
|
+
baseRef: prResponse.data.base.ref,
|
|
144425
|
+
baseRepoFullName: prResponse.data.base.repo.full_name,
|
|
144426
|
+
maintainerCanModify: prResponse.data.maintainer_can_modify
|
|
144427
|
+
};
|
|
144428
|
+
const checkoutResult = await checkoutPrBranch(pr, {
|
|
144429
|
+
octokit: ctx.octokit,
|
|
144430
|
+
owner: ctx.repo.owner,
|
|
144431
|
+
name: ctx.repo.name,
|
|
144432
|
+
gitToken: ctx.gitToken,
|
|
144433
|
+
toolState: ctx.toolState,
|
|
144434
|
+
shell: ctx.payload.shell,
|
|
144435
|
+
postCheckoutScript: ctx.postCheckoutScript,
|
|
144436
|
+
beforeSha: ctx.toolState.beforeSha
|
|
144437
|
+
});
|
|
144438
|
+
const tempDir = process.env.PULLFROG_TEMP_DIR;
|
|
144439
|
+
if (!tempDir) {
|
|
144440
|
+
throw new Error(
|
|
144441
|
+
"PULLFROG_TEMP_DIR not set - checkout_pr must run in pullfrog action context"
|
|
144442
|
+
);
|
|
144443
|
+
}
|
|
144444
|
+
const headShort = ctx.toolState.checkoutSha.slice(0, 7);
|
|
144445
|
+
let incrementalDiffPath;
|
|
144446
|
+
if (ctx.toolState.beforeSha && ctx.toolState.checkoutSha) {
|
|
144447
|
+
const beforeShort = ctx.toolState.beforeSha.slice(0, 7);
|
|
144448
|
+
const incremental = computeIncrementalDiff({
|
|
144449
|
+
baseBranch: pr.baseRef,
|
|
144450
|
+
beforeSha: ctx.toolState.beforeSha,
|
|
144451
|
+
headSha: ctx.toolState.checkoutSha
|
|
144368
144452
|
});
|
|
144369
|
-
|
|
144370
|
-
|
|
144371
|
-
|
|
144372
|
-
|
|
144453
|
+
if (incremental) {
|
|
144454
|
+
incrementalDiffPath = join3(
|
|
144455
|
+
tempDir,
|
|
144456
|
+
`pr-${pull_number}-${beforeShort}-${headShort}-incremental.diff`
|
|
144457
|
+
);
|
|
144458
|
+
writeFileSync(incrementalDiffPath, incremental);
|
|
144459
|
+
log.info(
|
|
144460
|
+
`\xBB incremental diff computed (${incremental.length} bytes) \u2192 ${incrementalDiffPath}`
|
|
144373
144461
|
);
|
|
144374
144462
|
}
|
|
144375
|
-
|
|
144376
|
-
|
|
144377
|
-
|
|
144378
|
-
|
|
144379
|
-
const incremental = computeIncrementalDiff({
|
|
144380
|
-
baseBranch: pr.baseRef,
|
|
144381
|
-
beforeSha: ctx.toolState.beforeSha,
|
|
144382
|
-
headSha: ctx.toolState.checkoutSha
|
|
144383
|
-
});
|
|
144384
|
-
if (incremental) {
|
|
144385
|
-
incrementalDiffPath = join3(
|
|
144386
|
-
tempDir,
|
|
144387
|
-
`pr-${pull_number}-${beforeShort}-${headShort}-incremental.diff`
|
|
144388
|
-
);
|
|
144389
|
-
writeFileSync(incrementalDiffPath, incremental);
|
|
144390
|
-
log.info(
|
|
144391
|
-
`\xBB incremental diff computed (${incremental.length} bytes) \u2192 ${incrementalDiffPath}`
|
|
144392
|
-
);
|
|
144393
|
-
}
|
|
144394
|
-
}
|
|
144395
|
-
const formatResult = await fetchAndFormatPrDiff(ctx, pull_number);
|
|
144396
|
-
const diffPreview = formatResult.content.split("\n").slice(0, 100).join("\n");
|
|
144397
|
-
log.debug(`formatted diff preview (first 100 lines):
|
|
144463
|
+
}
|
|
144464
|
+
const formatResult = await fetchAndFormatPrDiff(ctx, pull_number);
|
|
144465
|
+
const diffPreview = formatResult.content.split("\n").slice(0, 100).join("\n");
|
|
144466
|
+
log.debug(`formatted diff preview (first 100 lines):
|
|
144398
144467
|
${diffPreview}`);
|
|
144399
|
-
|
|
144400
|
-
|
|
144401
|
-
|
|
144402
|
-
|
|
144403
|
-
|
|
144404
|
-
|
|
144405
|
-
|
|
144406
|
-
|
|
144468
|
+
const diffPath = join3(tempDir, `pr-${pull_number}-${headShort}.diff`);
|
|
144469
|
+
writeFileSync(diffPath, formatResult.content);
|
|
144470
|
+
log.debug(`wrote diff to ${diffPath} (${formatResult.content.length} bytes)`);
|
|
144471
|
+
ctx.toolState.diffCoverage = createDiffCoverageState({
|
|
144472
|
+
diffPath,
|
|
144473
|
+
totalLines: countLines({ content: formatResult.content }),
|
|
144474
|
+
toc: formatResult.toc,
|
|
144475
|
+
previous: ctx.toolState.diffCoverage
|
|
144476
|
+
});
|
|
144477
|
+
log.debug(
|
|
144478
|
+
`\xBB diff coverage initialized: diffPath=${diffPath}, totalLines=${ctx.toolState.diffCoverage.totalLines}, tocEntries=${ctx.toolState.diffCoverage.tocEntries.length}`
|
|
144479
|
+
);
|
|
144480
|
+
const cached4 = /* @__PURE__ */ new Map();
|
|
144481
|
+
for (const file2 of formatResult.files) {
|
|
144482
|
+
cached4.set(file2.filename, commentableLinesForFile(file2.patch));
|
|
144483
|
+
}
|
|
144484
|
+
ctx.toolState.commentableLinesByFile = cached4;
|
|
144485
|
+
ctx.toolState.commentableLinesPullNumber = pull_number;
|
|
144486
|
+
ctx.toolState.commentableLinesCheckoutSha = ctx.toolState.checkoutSha;
|
|
144487
|
+
const incrementalInstructions = incrementalDiffPath ? ` IMPORTANT: incrementalDiffPath contains ONLY the changes since the last reviewed version (computed via range-diff). you MUST read incrementalDiffPath FIRST to understand what changed, then use diffPath for full PR context. do NOT skip the incremental diff.` : "";
|
|
144488
|
+
const COMMIT_LOG_MAX = 200;
|
|
144489
|
+
const baseRange = `origin/${pr.baseRef}..HEAD`;
|
|
144490
|
+
let commitCount = 0;
|
|
144491
|
+
let commitLog = "";
|
|
144492
|
+
let commitLogUnavailable = false;
|
|
144493
|
+
try {
|
|
144494
|
+
commitCount = parseInt(
|
|
144495
|
+
$("git", ["rev-list", "--count", baseRange], { log: false }).trim() || "0",
|
|
144496
|
+
10
|
|
144497
|
+
);
|
|
144498
|
+
commitLog = $("git", ["log", "--oneline", `--max-count=${COMMIT_LOG_MAX}`, baseRange], {
|
|
144499
|
+
log: false
|
|
144407
144500
|
});
|
|
144501
|
+
} catch (err) {
|
|
144502
|
+
commitLogUnavailable = true;
|
|
144408
144503
|
log.debug(
|
|
144409
|
-
`\xBB
|
|
144504
|
+
`\xBB unable to compute commit metadata for ${baseRange}: ${err instanceof Error ? err.message : String(err)}`
|
|
144410
144505
|
);
|
|
144411
|
-
|
|
144412
|
-
|
|
144413
|
-
|
|
144414
|
-
|
|
144415
|
-
|
|
144416
|
-
|
|
144417
|
-
|
|
144418
|
-
|
|
144419
|
-
|
|
144420
|
-
|
|
144421
|
-
|
|
144422
|
-
|
|
144423
|
-
|
|
144506
|
+
}
|
|
144507
|
+
const commitLogTruncated = commitCount > COMMIT_LOG_MAX;
|
|
144508
|
+
const hookWarningInstructions = checkoutResult.hookWarning ? ` HOOK WARNING: the post-checkout lifecycle hook reported a non-fatal failure (see hookWarning). decide whether to retry based on the guidance in that field before proceeding.` : "";
|
|
144509
|
+
const commitLogInstructions = commitLogUnavailable ? ` NOTE: commit metadata is partial (base ref unreachable, likely a shallow fetch). commitCount/commitLog may be 0/empty or incomplete; treat them as "unknown" rather than "no commits", and use \`git log\` directly if you need the full history.` : commitLogTruncated ? ` NOTE: commitLog was capped at ${COMMIT_LOG_MAX} entries out of ${commitCount} commits; use \`git log\` directly if you need the full history.` : "";
|
|
144510
|
+
return {
|
|
144511
|
+
success: true,
|
|
144512
|
+
number: prResponse.data.number,
|
|
144513
|
+
title: prResponse.data.title,
|
|
144514
|
+
body: prResponse.data.body,
|
|
144515
|
+
base: pr.baseRef,
|
|
144516
|
+
localBranch: `pr-${pull_number}`,
|
|
144517
|
+
remoteBranch: `refs/heads/${pr.headRef}`,
|
|
144518
|
+
isFork: pr.headRepoFullName !== pr.baseRepoFullName,
|
|
144519
|
+
maintainerCanModify: pr.maintainerCanModify,
|
|
144520
|
+
url: prResponse.data.html_url,
|
|
144521
|
+
headRepo: pr.headRepoFullName,
|
|
144522
|
+
diffPath,
|
|
144523
|
+
incrementalDiffPath,
|
|
144524
|
+
toc: formatResult.toc,
|
|
144525
|
+
commitCount,
|
|
144526
|
+
commitLog,
|
|
144527
|
+
commitLogTruncated,
|
|
144528
|
+
commitLogUnavailable,
|
|
144529
|
+
hookWarning: checkoutResult.hookWarning,
|
|
144530
|
+
instructions: `the diff file at diffPath contains a table of contents (TOC) at the top listing every changed file with its line range. use the TOC line ranges as your checklist and read specific files from the diff instead of reading the entire file. for example, if the TOC says "src/foo.ts \u2192 lines 5-42", read lines 5-42 from diffPath to see that file's changes. review files selectively based on relevance rather than reading everything sequentially. to inspect the PR's changed files, use diffPath \u2014 do NOT run \`git diff <base>..<head>\` to re-derive what's already in diffPath. the formatted diff with line numbers is authoritative. \`git log\` and \`git diff --stat\` are fine for commit-range overview, and \`git diff\` / \`git diff --cached\` are fine for inspecting *your own* uncommitted changes \u2014 but PR review content MUST come from diffPath. before your review is submitted, a one-time coverage pre-flight may error listing unread TOC regions. retry the same create_pull_request_review call to proceed \u2014 optionally after reading the listed ranges. the pre-flight will not block again this session. the local branch is 'localBranch' (pr-{number}), not the remote branch name. when pushing, omit branchName to use the current branch. do not use remoteBranch as a local branch name.` + incrementalInstructions + hookWarningInstructions + commitLogInstructions
|
|
144531
|
+
};
|
|
144532
|
+
};
|
|
144533
|
+
return tool({
|
|
144534
|
+
name: "checkout_pr",
|
|
144535
|
+
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.",
|
|
144536
|
+
parameters: CheckoutPr,
|
|
144537
|
+
execute: execute(async ({ pull_number }) => {
|
|
144538
|
+
const inFlight = inFlightCheckouts.get(pull_number);
|
|
144539
|
+
if (inFlight) {
|
|
144540
|
+
log.info(`\xBB checkout_pr({pull_number:${pull_number}}) already in flight \u2014 sharing result`);
|
|
144541
|
+
return inFlight;
|
|
144542
|
+
}
|
|
144543
|
+
const current = ctx.toolState.issueNumber;
|
|
144544
|
+
if (current !== void 0 && current !== pull_number) {
|
|
144545
|
+
const dirty = $("git", ["status", "--porcelain"], { log: false }).trim();
|
|
144546
|
+
if (dirty) {
|
|
144547
|
+
throw new Error(
|
|
144548
|
+
`cannot checkout PR #${pull_number} while the working tree has uncommitted changes. commit, push, or discard them before switching. dirty paths:
|
|
144549
|
+
${dirty}`
|
|
144550
|
+
);
|
|
144551
|
+
}
|
|
144552
|
+
}
|
|
144553
|
+
const promise2 = runCheckout(pull_number);
|
|
144554
|
+
inFlightCheckouts.set(pull_number, promise2);
|
|
144424
144555
|
try {
|
|
144425
|
-
|
|
144426
|
-
|
|
144427
|
-
|
|
144428
|
-
);
|
|
144429
|
-
commitLog = $("git", ["log", "--oneline", `--max-count=${COMMIT_LOG_MAX}`, baseRange], {
|
|
144430
|
-
log: false
|
|
144431
|
-
});
|
|
144432
|
-
} catch (err) {
|
|
144433
|
-
commitLogUnavailable = true;
|
|
144434
|
-
log.debug(
|
|
144435
|
-
`\xBB unable to compute commit metadata for ${baseRange}: ${err instanceof Error ? err.message : String(err)}`
|
|
144436
|
-
);
|
|
144556
|
+
return await promise2;
|
|
144557
|
+
} finally {
|
|
144558
|
+
inFlightCheckouts.delete(pull_number);
|
|
144437
144559
|
}
|
|
144438
|
-
const commitLogTruncated = commitCount > COMMIT_LOG_MAX;
|
|
144439
|
-
const hookWarningInstructions = checkoutResult.hookWarning ? ` HOOK WARNING: the post-checkout lifecycle hook reported a non-fatal failure (see hookWarning). decide whether to retry based on the guidance in that field before proceeding.` : "";
|
|
144440
|
-
const commitLogInstructions = commitLogUnavailable ? ` NOTE: commit metadata is partial (base ref unreachable, likely a shallow fetch). commitCount/commitLog may be 0/empty or incomplete; treat them as "unknown" rather than "no commits", and use \`git log\` directly if you need the full history.` : commitLogTruncated ? ` NOTE: commitLog was capped at ${COMMIT_LOG_MAX} entries out of ${commitCount} commits; use \`git log\` directly if you need the full history.` : "";
|
|
144441
|
-
return {
|
|
144442
|
-
success: true,
|
|
144443
|
-
number: prResponse.data.number,
|
|
144444
|
-
title: prResponse.data.title,
|
|
144445
|
-
body: prResponse.data.body,
|
|
144446
|
-
base: pr.baseRef,
|
|
144447
|
-
localBranch: `pr-${pull_number}`,
|
|
144448
|
-
remoteBranch: `refs/heads/${pr.headRef}`,
|
|
144449
|
-
isFork: pr.headRepoFullName !== pr.baseRepoFullName,
|
|
144450
|
-
maintainerCanModify: pr.maintainerCanModify,
|
|
144451
|
-
url: prResponse.data.html_url,
|
|
144452
|
-
headRepo: pr.headRepoFullName,
|
|
144453
|
-
diffPath,
|
|
144454
|
-
incrementalDiffPath,
|
|
144455
|
-
toc: formatResult.toc,
|
|
144456
|
-
commitCount,
|
|
144457
|
-
commitLog,
|
|
144458
|
-
commitLogTruncated,
|
|
144459
|
-
commitLogUnavailable,
|
|
144460
|
-
hookWarning: checkoutResult.hookWarning,
|
|
144461
|
-
instructions: `the diff file at diffPath contains a table of contents (TOC) at the top listing every changed file with its line range. use the TOC line ranges as your checklist and read specific files from the diff instead of reading the entire file. for example, if the TOC says "src/foo.ts \u2192 lines 5-42", read lines 5-42 from diffPath to see that file's changes. review files selectively based on relevance rather than reading everything sequentially. to inspect the PR's changed files, use diffPath \u2014 do NOT run \`git diff <base>..<head>\` to re-derive what's already in diffPath. the formatted diff with line numbers is authoritative. \`git log\` and \`git diff --stat\` are fine for commit-range overview, and \`git diff\` / \`git diff --cached\` are fine for inspecting *your own* uncommitted changes \u2014 but PR review content MUST come from diffPath. before your review is submitted, a one-time coverage pre-flight may error listing unread TOC regions. retry the same create_pull_request_review call to proceed \u2014 optionally after reading the listed ranges. the pre-flight will not block again this session. the local branch is 'localBranch' (pr-{number}), not the remote branch name. when pushing, omit branchName to use the current branch. do not use remoteBranch as a local branch name.` + incrementalInstructions + hookWarningInstructions + commitLogInstructions
|
|
144462
|
-
};
|
|
144463
144560
|
})
|
|
144464
144561
|
});
|
|
144465
144562
|
}
|
|
@@ -144646,7 +144743,7 @@ var CommitInfo = type({
|
|
|
144646
144743
|
function CommitInfoTool(ctx) {
|
|
144647
144744
|
return tool({
|
|
144648
144745
|
name: "get_commit_info",
|
|
144649
|
-
description:
|
|
144746
|
+
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" })`.',
|
|
144650
144747
|
parameters: CommitInfo,
|
|
144651
144748
|
execute: execute(async ({ sha }) => {
|
|
144652
144749
|
const response = await ctx.octokit.rest.repos.getCommit({
|
|
@@ -144737,7 +144834,7 @@ var GetIssueComments = type({
|
|
|
144737
144834
|
function GetIssueCommentsTool(ctx) {
|
|
144738
144835
|
return tool({
|
|
144739
144836
|
name: "get_issue_comments",
|
|
144740
|
-
description: "Get all comments for a GitHub issue. Returns all comments including the issue body and all subsequent discussion comments.",
|
|
144837
|
+
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 })`.",
|
|
144741
144838
|
parameters: GetIssueComments,
|
|
144742
144839
|
execute: execute(async ({ issue_number }) => {
|
|
144743
144840
|
ctx.toolState.issueNumber = issue_number;
|
|
@@ -144838,7 +144935,7 @@ var IssueInfo = type({
|
|
|
144838
144935
|
function IssueInfoTool(ctx) {
|
|
144839
144936
|
return tool({
|
|
144840
144937
|
name: "get_issue",
|
|
144841
|
-
description: "Retrieve GitHub issue information by issue number",
|
|
144938
|
+
description: "Retrieve GitHub issue information by issue number. Example: `get_issue({ issue_number: 1234 })`.",
|
|
144842
144939
|
parameters: IssueInfo,
|
|
144843
144940
|
execute: execute(async ({ issue_number }) => {
|
|
144844
144941
|
const issue3 = await ctx.octokit.rest.issues.get({
|
|
@@ -145080,7 +145177,7 @@ var PullRequestInfo = type({
|
|
|
145080
145177
|
function PullRequestInfoTool(ctx) {
|
|
145081
145178
|
return tool({
|
|
145082
145179
|
name: "get_pull_request",
|
|
145083
|
-
description: "Retrieve PR metadata (title, body, state, branches, author, labels, linked issues). To checkout a PR branch locally, use checkout_pr instead.",
|
|
145180
|
+
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.",
|
|
145084
145181
|
parameters: PullRequestInfo,
|
|
145085
145182
|
execute: execute(async ({ pull_number }) => {
|
|
145086
145183
|
const [restResponse, graphqlResponse] = await Promise.all([
|
|
@@ -145484,7 +145581,7 @@ async function getReviewData(input) {
|
|
|
145484
145581
|
function GetReviewCommentsTool(ctx) {
|
|
145485
145582
|
return tool({
|
|
145486
145583
|
name: "get_review_comments",
|
|
145487
|
-
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.",
|
|
145584
|
+
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.",
|
|
145488
145585
|
parameters: GetReviewComments,
|
|
145489
145586
|
execute: execute(async (params) => {
|
|
145490
145587
|
const approvedBy = ctx.payload.event.trigger === "fix_review" && ctx.payload.event.approved_only ? ctx.payload.triggerer : void 0;
|
|
@@ -145534,7 +145631,7 @@ var ListPullRequestReviews = type({
|
|
|
145534
145631
|
function ListPullRequestReviewsTool(ctx) {
|
|
145535
145632
|
return tool({
|
|
145536
145633
|
name: "list_pull_request_reviews",
|
|
145537
|
-
description: "List all reviews for a pull request. Returns all reviews including approvals, request changes, and comments.",
|
|
145634
|
+
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 })`.",
|
|
145538
145635
|
parameters: ListPullRequestReviews,
|
|
145539
145636
|
execute: execute(async (params) => {
|
|
145540
145637
|
const reviews = await ctx.octokit.paginate(ctx.octokit.rest.pulls.listReviews, {
|
|
@@ -145684,7 +145781,7 @@ function SelectModeTool(ctx) {
|
|
|
145684
145781
|
const overrides = buildModeOverrides(t);
|
|
145685
145782
|
return tool({
|
|
145686
145783
|
name: "select_mode",
|
|
145687
|
-
description:
|
|
145784
|
+
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 })`.',
|
|
145688
145785
|
parameters: SelectModeParams,
|
|
145689
145786
|
execute: execute(async (params) => {
|
|
145690
145787
|
if (ctx.toolState.selectedMode) {
|
|
@@ -145745,7 +145842,9 @@ import { setTimeout as sleep2 } from "node:timers/promises";
|
|
|
145745
145842
|
var ShellParams = type({
|
|
145746
145843
|
command: "string",
|
|
145747
145844
|
description: "string",
|
|
145748
|
-
"timeout?":
|
|
145845
|
+
"timeout?": type.number.describe(
|
|
145846
|
+
"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."
|
|
145847
|
+
),
|
|
145749
145848
|
"working_directory?": "string",
|
|
145750
145849
|
"background?": "boolean"
|
|
145751
145850
|
});
|
|
@@ -145853,6 +145952,15 @@ function getTempDir() {
|
|
|
145853
145952
|
}
|
|
145854
145953
|
return tempDir;
|
|
145855
145954
|
}
|
|
145955
|
+
var MAX_OUTPUT_CHARS = 5e3;
|
|
145956
|
+
function capOutput(output) {
|
|
145957
|
+
if (output.length <= MAX_OUTPUT_CHARS) return output;
|
|
145958
|
+
const fullPath = join7(getTempDir(), `shell-${randomUUID2().slice(0, 8)}.log`);
|
|
145959
|
+
writeFileSync5(fullPath, output);
|
|
145960
|
+
const elided = output.length - MAX_OUTPUT_CHARS;
|
|
145961
|
+
return `... [${elided} chars truncated; full output saved to ${fullPath}] ...
|
|
145962
|
+
${output.slice(-MAX_OUTPUT_CHARS)}`;
|
|
145963
|
+
}
|
|
145856
145964
|
function isGitCommand(command) {
|
|
145857
145965
|
const trimmed = command.trim();
|
|
145858
145966
|
if (trimmed === "git" || trimmed.startsWith("git ")) return true;
|
|
@@ -145864,11 +145972,15 @@ function ShellTool(ctx) {
|
|
|
145864
145972
|
name: "shell",
|
|
145865
145973
|
description: `Execute shell commands securely. Environment is filtered to remove API keys and secrets.
|
|
145866
145974
|
|
|
145975
|
+
Example: \`shell({ command: "pnpm test", description: "run the test suite" })\`.
|
|
145976
|
+
|
|
145867
145977
|
Use this tool to:
|
|
145868
145978
|
- Run shell commands (ls, cat, grep, find, etc.)
|
|
145869
145979
|
- Execute build tools (npm, pnpm, cargo, make, etc.)
|
|
145870
145980
|
- Run tests and linters
|
|
145871
145981
|
|
|
145982
|
+
Output is capped at ${MAX_OUTPUT_CHARS} chars: if exceeded, only the tail is returned and the full body is saved to a tempfile (path included in the response). Re-read the tempfile with cat/tail/grep when you need more.
|
|
145983
|
+
|
|
145872
145984
|
Do NOT use this tool for git commands \u2014 use the dedicated git tools instead.`,
|
|
145873
145985
|
parameters: ShellParams,
|
|
145874
145986
|
execute: execute(async (params) => {
|
|
@@ -145959,12 +146071,13 @@ ${stderr}` : stderr : stdout;
|
|
|
145959
146071
|
output = output ? `${output}
|
|
145960
146072
|
[timed out after ${timeout}ms]` : `[timed out after ${timeout}ms]`;
|
|
145961
146073
|
const finalExitCode = exitCode ?? (timedOut ? 124 : -1);
|
|
146074
|
+
const trimmed = output.trim();
|
|
145962
146075
|
if (finalExitCode !== 0) {
|
|
145963
146076
|
log.info(`shell command failed with exit code ${finalExitCode}: ${params.command}`);
|
|
145964
|
-
if (
|
|
146077
|
+
if (trimmed) log.info(`output: ${trimmed}`);
|
|
145965
146078
|
}
|
|
145966
146079
|
return {
|
|
145967
|
-
output:
|
|
146080
|
+
output: capOutput(trimmed),
|
|
145968
146081
|
exit_code: finalExitCode,
|
|
145969
146082
|
timed_out: timedOut
|
|
145970
146083
|
};
|
|
@@ -146381,18 +146494,24 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
146381
146494
|
- resolve addressed threads via \`${t("resolve_review_thread")}\`
|
|
146382
146495
|
- call \`${t("report_progress")}\` with a brief summary (or the exact push error if push failed)`
|
|
146383
146496
|
},
|
|
146384
|
-
// Review and IncrementalReview use
|
|
146385
|
-
// (
|
|
146386
|
-
//
|
|
146387
|
-
//
|
|
146388
|
-
//
|
|
146389
|
-
//
|
|
146390
|
-
//
|
|
146391
|
-
//
|
|
146392
|
-
//
|
|
146393
|
-
//
|
|
146394
|
-
//
|
|
146395
|
-
//
|
|
146497
|
+
// Review and IncrementalReview use a 0-or-2+ lens pattern. The default is
|
|
146498
|
+
// 0 lenses (orchestrator handles the review solo). Multi-lens (2+
|
|
146499
|
+
// reviewfrog subagents in parallel) only fires for substantive PRs or
|
|
146500
|
+
// high-stakes-subsystem touches — and when it fires, ALL lenses must
|
|
146501
|
+
// dispatch in a single assistant turn or the parallelism win disappears.
|
|
146502
|
+
// We never dispatch exactly one lens: a single lens is just a worse,
|
|
146503
|
+
// slower version of doing the work yourself.
|
|
146504
|
+
//
|
|
146505
|
+
// Build mode self-review is a different problem shape: the orchestrator
|
|
146506
|
+
// wrote the code, so bias-mitigation comes from delegating to one
|
|
146507
|
+
// fresh-eyes subagent that doesn't share the implementation context. A
|
|
146508
|
+
// single subagent there is appropriate; the 0-or-2+ rule applies only to
|
|
146509
|
+
// the Review/IncrementalReview lens fan-out where independence between
|
|
146510
|
+
// perspectives is what's being purchased.
|
|
146511
|
+
//
|
|
146512
|
+
// Deliberate omission vs canonical /anneal: severity categorization in
|
|
146513
|
+
// the final message (the review body has its own CAUTION/IMPORTANT
|
|
146514
|
+
// framing instead of a severity table).
|
|
146396
146515
|
{
|
|
146397
146516
|
name: "Review",
|
|
146398
146517
|
description: "Review code, PRs, or implementations; provide feedback or suggestions; identify issues; or check code quality, style, and correctness",
|
|
@@ -146402,9 +146521,9 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
146402
146521
|
|
|
146403
146522
|
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.
|
|
146404
146523
|
|
|
146405
|
-
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).
|
|
146524
|
+
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.
|
|
146406
146525
|
|
|
146407
|
-
if the PR is **genuinely trivial**, skip
|
|
146526
|
+
if the PR is **genuinely trivial**, skip the fan-out entirely and submit a \`No new issues found.\` review per step 7.
|
|
146408
146527
|
|
|
146409
146528
|
"Genuinely trivial" (skip):
|
|
146410
146529
|
- single-word doc typo, whitespace/format-only, comment-only across any number of files
|
|
@@ -146423,25 +146542,25 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
146423
146542
|
- any "typo fix" in user-facing copy that changes meaning ("approved" \u2192 "denied")
|
|
146424
146543
|
- mixed diffs where a semantic 1-liner is buried in whitespace/formatting changes
|
|
146425
146544
|
|
|
146426
|
-
|
|
146545
|
+
4. **lens decision \u2014 0 or 2+, NEVER 1**.
|
|
146427
146546
|
|
|
146428
|
-
|
|
146547
|
+
The default is **0 lenses**: handle the review yourself end-to-end. Most PRs land here.
|
|
146429
146548
|
|
|
146430
|
-
|
|
146431
|
-
-
|
|
146432
|
-
-
|
|
146433
|
-
-
|
|
146549
|
+
Dispatch **2+ \`${REVIEWER_AGENT_NAME}\` lenses in parallel** ONLY when ALL of the following are true:
|
|
146550
|
+
- 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)
|
|
146551
|
+
- you can name 2+ distinct concrete failure modes that warrant independent lenses (one lens per failure mode; orthogonal, not overlapping)
|
|
146552
|
+
- parallel-orchestrated independent perspectives meaningfully outperform what you'd find solo
|
|
146434
146553
|
|
|
146435
|
-
**
|
|
146554
|
+
**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).
|
|
146436
146555
|
|
|
146437
|
-
|
|
146556
|
+
When you do go multi-lens, lens framings come in two flavors:
|
|
146438
146557
|
- **themed lenses** \u2014 a perspective applied across the whole diff (correctness, security, user-journey, performance, etc.).
|
|
146439
|
-
- **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").
|
|
146558
|
+
- **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.
|
|
146440
146559
|
|
|
146441
146560
|
starter menu (combine, omit, or invent your own):
|
|
146442
146561
|
- **correctness & invariants** \u2014 bugs, races, error handling, edge cases, state-machine boundaries
|
|
146443
|
-
- **impact** \u2014
|
|
146444
|
-
- **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.
|
|
146562
|
+
- **impact** \u2014 stale references in code/tests/docs/configs/UI after rename/remove
|
|
146563
|
+
- **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.
|
|
146445
146564
|
- **security** \u2014 new endpoints, authZ, input validation, secrets handling, replay/CSRF/injection, cross-tenant isolation
|
|
146446
146565
|
- **user-journey** \u2014 UX-touching flows: walk through happy path and failure modes as a user
|
|
146447
146566
|
- **operational readiness** \u2014 observability, alerting, migrations (forward + rollback), feature flags, on-call burden
|
|
@@ -146451,26 +146570,36 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
146451
146570
|
- **holistic** \u2014 does the PR make sense as a whole? symmetric flows (delete for every create, rollback for every migration)?
|
|
146452
146571
|
- **subsystem lenses** (invent as the PR demands) \u2014 auth, billing, payments, schema migration, webhooks, secrets, RBAC, multi-tenant isolation, cron/scheduling, etc.
|
|
146453
146572
|
|
|
146454
|
-
|
|
146573
|
+
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.
|
|
146574
|
+
|
|
146575
|
+
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.**
|
|
146576
|
+
|
|
146577
|
+
\u26A0\uFE0F CRITICAL \u2014 PARALLELISM IS THE ONLY REASON LENSES EXIST. \u26A0\uFE0F
|
|
146578
|
+
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.
|
|
146579
|
+
|
|
146580
|
+
\u2705 Right pattern: one assistant turn with N Task tool_use blocks \u2192 wait \u2192 N results arrive together \u2192 aggregate.
|
|
146581
|
+
\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.
|
|
146582
|
+
|
|
146583
|
+
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.
|
|
146584
|
+
|
|
146585
|
+
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:
|
|
146455
146586
|
- the diff path / target \u2014 reading the diff and the codebase is its job
|
|
146456
146587
|
- **only one lens** \u2014 never a multi-section "review for X, Y, and Z" prompt
|
|
146457
146588
|
- **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\`.
|
|
146458
|
-
- 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.
|
|
146459
146589
|
- 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."
|
|
146460
146590
|
- 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.
|
|
146461
146591
|
|
|
146462
146592
|
delegation discipline:
|
|
146463
|
-
- 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)
|
|
146464
146593
|
- do NOT summarize the PR for them (biases toward a validation frame)
|
|
146465
146594
|
- do NOT hand them a curated reading list (let them discover scope)
|
|
146466
146595
|
- do NOT pre-shape their output with a finding schema
|
|
146467
146596
|
- do NOT mention the other lenses (independence is the point \u2014 overlapping findings are a strong signal)
|
|
146468
146597
|
|
|
146469
|
-
|
|
146598
|
+
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.
|
|
146470
146599
|
|
|
146471
146600
|
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.
|
|
146472
146601
|
|
|
146473
|
-
|
|
146602
|
+
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.
|
|
146474
146603
|
|
|
146475
146604
|
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.
|
|
146476
146605
|
|
|
@@ -146498,10 +146627,10 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
146498
146627
|
|
|
146499
146628
|
${PR_SUMMARY_FORMAT}`
|
|
146500
146629
|
},
|
|
146501
|
-
// IncrementalReview shares Review's
|
|
146502
|
-
//
|
|
146503
|
-
//
|
|
146504
|
-
//
|
|
146630
|
+
// IncrementalReview shares Review's 0-or-2+ lens pattern but scopes the
|
|
146631
|
+
// target to the incremental diff. The "issues must be NEW since the last
|
|
146632
|
+
// Pullfrog review" filter lives at aggregation time (step 8), NOT in the
|
|
146633
|
+
// subagent prompt — pushing the filter into
|
|
146505
146634
|
// subagents matches the canonical anneal anti-pattern of "list known
|
|
146506
146635
|
// pre-existing failures — don't flag these" and suppresses signal on
|
|
146507
146636
|
// regressions the new commits amplified. The review body is just
|
|
@@ -146520,38 +146649,57 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
146520
146649
|
|
|
146521
146650
|
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.
|
|
146522
146651
|
|
|
146523
|
-
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
|
|
146652
|
+
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.
|
|
146524
146653
|
|
|
146525
|
-
5. **triage
|
|
146654
|
+
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.**
|
|
146526
146655
|
|
|
146527
|
-
if the incremental changes are **genuinely trivial**, skip the fan-out entirely and jump to step
|
|
146656
|
+
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).
|
|
146528
146657
|
|
|
146529
146658
|
"Genuinely trivial" (skip): formatting/comment tweaks, import reordering, lockfile regen, mechanical rename of import paths, whitespace-only.
|
|
146530
146659
|
"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.
|
|
146531
146660
|
When unsure, treat as non-trivial.
|
|
146532
146661
|
|
|
146533
|
-
|
|
146662
|
+
6. **lens decision \u2014 0 or 2+, NEVER 1**.
|
|
146663
|
+
|
|
146664
|
+
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."
|
|
146665
|
+
|
|
146666
|
+
Dispatch **2+ \`${REVIEWER_AGENT_NAME}\` lenses in parallel** ONLY when ALL of the following are true:
|
|
146667
|
+
- 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)
|
|
146668
|
+
- you can name 2+ distinct concrete failure modes the new commits plausibly introduce that warrant independent lenses
|
|
146669
|
+
- parallel-orchestrated independent perspectives meaningfully outperform what you'd find solo
|
|
146670
|
+
|
|
146671
|
+
**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.
|
|
146534
146672
|
|
|
146535
|
-
|
|
146536
|
-
|
|
146673
|
+
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.
|
|
146674
|
+
|
|
146675
|
+
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.**
|
|
146676
|
+
|
|
146677
|
+
\u26A0\uFE0F CRITICAL \u2014 PARALLELISM IS THE ONLY REASON LENSES EXIST. \u26A0\uFE0F
|
|
146678
|
+
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.
|
|
146679
|
+
|
|
146680
|
+
\u2705 Right pattern: one assistant turn with N Task tool_use blocks \u2192 wait \u2192 N results arrive together \u2192 aggregate.
|
|
146681
|
+
\u274C Wrong pattern: turn 1 = Task(lens A) \u2192 turn 2 (after A's result) = Task(lens B). This is the failure mode.
|
|
146682
|
+
|
|
146683
|
+
You can also include your own \`read\` / \`grep\` / \`webfetch\` calls in the SAME turn as the parallel \`${REVIEWER_AGENT_NAME}\` dispatches.
|
|
146684
|
+
|
|
146685
|
+
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:
|
|
146686
|
+
- 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
|
|
146537
146687
|
- **only one lens** \u2014 never a multi-section "review for X, Y, and Z" prompt
|
|
146538
|
-
- **a Task \`description\` set to the lens name**
|
|
146539
|
-
- the
|
|
146540
|
-
- 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."
|
|
146688
|
+
- **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.
|
|
146689
|
+
- if the lens touches external contracts, instruct the subagent to verify load-bearing claims via web search and quote source URLs.
|
|
146541
146690
|
- ask the subagent to report findings with file paths and NEW line numbers from the full PR diff so you can anchor inline comments.
|
|
146542
146691
|
|
|
146543
146692
|
delegation discipline:
|
|
146544
|
-
- do NOT lens-review the diff yourself in parallel with the subagents
|
|
146545
146693
|
- do NOT summarize the changes for them (biases toward validation frame)
|
|
146546
146694
|
- do NOT hand them a curated reading list (let them discover scope)
|
|
146547
146695
|
- do NOT pre-shape their output with a finding schema
|
|
146548
146696
|
- do NOT mention the other lenses (independence is the point)
|
|
146549
146697
|
|
|
146550
|
-
|
|
146698
|
+
8. **aggregate, draft, self-critique**: merge findings (yours + any subagent output if you went multi-lens); de-dup overlaps; trace each finding yourself. drop praise, style preferences, speculative/unverified claims, findings about pre-existing code unrelated to the new commits, anything not actionable, and anything that re-states prior review feedback (heuristic: if the finding's root cause lives in lines the *new commits* added or modified, it's in scope; otherwise drop). also drop **bloat-shaped findings** \u2014 proposed fixes that would add defensive checks for cases that can't happen, abstractions used once, comments restating obvious code, tests asserting tautologies, or "just-in-case" guards. subagents are fallible and bias toward recommending changes; the bar for an actionable inline comment is sound + correct + elegant. recommending a change that improves only one of the three (or degrades elegance to nominally improve correctness) makes the codebase worse, not better. To compute "lines the new commits added or modified": if \`incrementalDiffPath\` from step 2 is present, use it directly. Otherwise, take the prior Pullfrog review's \`commit_id\` (returned alongside each entry from \`${t("list_pull_request_reviews")}\` in step 4) and run \`git diff <prior-review-sha>..HEAD\` to isolate the lines added since that review. draft inline comments with NEW line numbers from the full PR diff \u2014 every comment must be actionable, 2-3 sentences max.
|
|
146551
146699
|
|
|
146552
|
-
|
|
146700
|
+
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.
|
|
146553
146701
|
|
|
146554
|
-
|
|
146702
|
+
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.
|
|
146555
146703
|
|
|
146556
146704
|
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.
|
|
146557
146705
|
|
|
@@ -146802,12 +146950,38 @@ var PROVIDER_ERROR_PATTERNS = [
|
|
|
146802
146950
|
// around `limit` rejects keys like `time_limit` or `field_limit`.
|
|
146803
146951
|
{ regex: /["']?\blimit\b["']?\s*:\s*0\b/, label: "zero quota" }
|
|
146804
146952
|
];
|
|
146805
|
-
|
|
146953
|
+
var EXCERPT_MAX_BYTES = 600;
|
|
146954
|
+
var LINES_BEFORE = 1;
|
|
146955
|
+
var LINES_AFTER = 2;
|
|
146956
|
+
function findProviderErrorMatch(text) {
|
|
146806
146957
|
for (const entry of PROVIDER_ERROR_PATTERNS) {
|
|
146807
|
-
|
|
146958
|
+
const m = entry.regex.exec(text);
|
|
146959
|
+
if (!m) continue;
|
|
146960
|
+
return { label: entry.label, excerpt: extractExcerpt(text, m.index) };
|
|
146808
146961
|
}
|
|
146809
146962
|
return null;
|
|
146810
146963
|
}
|
|
146964
|
+
function extractExcerpt(text, matchIndex) {
|
|
146965
|
+
const lineStart = text.lastIndexOf("\n", matchIndex - 1) + 1;
|
|
146966
|
+
const lineEndRaw = text.indexOf("\n", matchIndex);
|
|
146967
|
+
const lineEnd = lineEndRaw === -1 ? text.length : lineEndRaw;
|
|
146968
|
+
let start = lineStart;
|
|
146969
|
+
for (let i = 0; i < LINES_BEFORE && start > 0; i++) {
|
|
146970
|
+
const prev = text.lastIndexOf("\n", start - 2);
|
|
146971
|
+
start = prev < 0 ? 0 : prev + 1;
|
|
146972
|
+
}
|
|
146973
|
+
let end = lineEnd;
|
|
146974
|
+
for (let i = 0; i < LINES_AFTER && end < text.length; i++) {
|
|
146975
|
+
const next2 = text.indexOf("\n", end + 1);
|
|
146976
|
+
end = next2 < 0 ? text.length : next2;
|
|
146977
|
+
}
|
|
146978
|
+
let excerpt = text.slice(start, end);
|
|
146979
|
+
if (excerpt.length > EXCERPT_MAX_BYTES) {
|
|
146980
|
+
excerpt = text.slice(lineStart, lineEnd);
|
|
146981
|
+
if (excerpt.length > EXCERPT_MAX_BYTES) excerpt = excerpt.slice(0, EXCERPT_MAX_BYTES);
|
|
146982
|
+
}
|
|
146983
|
+
return excerpt.trim();
|
|
146984
|
+
}
|
|
146811
146985
|
var ROUTER_KEYLIMIT_EXHAUSTED_PATTERN = /requires more credits.*?fewer max_tokens|requested up to \d+ tokens.*?can only afford/is;
|
|
146812
146986
|
function isRouterKeylimitExhaustedError(text) {
|
|
146813
146987
|
return ROUTER_KEYLIMIT_EXHAUSTED_PATTERN.test(text);
|
|
@@ -146931,45 +147105,12 @@ var ThinkingTimer = class {
|
|
|
146931
147105
|
import { readFile } from "node:fs/promises";
|
|
146932
147106
|
function getUnsubmittedReview(toolState) {
|
|
146933
147107
|
const mode = toolState.selectedMode;
|
|
146934
|
-
if (mode !== "Review" && mode !== "IncrementalReview") return null;
|
|
146935
|
-
if (toolState.review || toolState.finalSummaryWritten) return null;
|
|
146936
147108
|
if (!toolState.hadProgressComment) return null;
|
|
146937
|
-
return
|
|
146938
|
-
|
|
146939
|
-
|
|
146940
|
-
function truncateHookOutput(raw2) {
|
|
146941
|
-
if (raw2.length <= MAX_HOOK_OUTPUT_CHARS) return raw2;
|
|
146942
|
-
return `...(truncated, showing last ${MAX_HOOK_OUTPUT_CHARS} chars)
|
|
146943
|
-
${raw2.slice(-MAX_HOOK_OUTPUT_CHARS)}`;
|
|
146944
|
-
}
|
|
146945
|
-
async function executeStopHook(script) {
|
|
146946
|
-
log.info("\xBB executing stop hook...");
|
|
146947
|
-
try {
|
|
146948
|
-
const result = await spawn({
|
|
146949
|
-
cmd: "bash",
|
|
146950
|
-
args: ["-c", script],
|
|
146951
|
-
env: process.env,
|
|
146952
|
-
timeout: LIFECYCLE_HOOK_TIMEOUT_MS,
|
|
146953
|
-
activityTimeout: 0,
|
|
146954
|
-
onStdout: (chunk) => process.stdout.write(chunk),
|
|
146955
|
-
onStderr: (chunk) => process.stderr.write(chunk)
|
|
146956
|
-
});
|
|
146957
|
-
if (result.exitCode === 0) {
|
|
146958
|
-
log.info("\xBB stop hook passed");
|
|
146959
|
-
return null;
|
|
146960
|
-
}
|
|
146961
|
-
const combined = [result.stderr.trim(), result.stdout.trim()].filter(Boolean).join("\n");
|
|
146962
|
-
const output = truncateHookOutput(combined);
|
|
146963
|
-
log.info(`\xBB stop hook failed with exit code ${result.exitCode}`);
|
|
146964
|
-
return { exitCode: result.exitCode, output };
|
|
146965
|
-
} catch (err) {
|
|
146966
|
-
const isTimeout = err instanceof SpawnTimeoutError && (err.code === SPAWN_TIMEOUT_CODE || err.code === SPAWN_ACTIVITY_TIMEOUT_CODE);
|
|
146967
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
146968
|
-
log.warning(
|
|
146969
|
-
`stop hook ${isTimeout ? "timed out" : "failed to spawn"}: ${msg} \u2014 skipping retry`
|
|
146970
|
-
);
|
|
146971
|
-
return null;
|
|
147109
|
+
if (mode === "Review") return toolState.review ? null : "Review";
|
|
147110
|
+
if (mode === "IncrementalReview") {
|
|
147111
|
+
return toolState.review || toolState.finalSummaryWritten ? null : "IncrementalReview";
|
|
146972
147112
|
}
|
|
147113
|
+
return null;
|
|
146973
147114
|
}
|
|
146974
147115
|
function buildStopHookPrompt(failure) {
|
|
146975
147116
|
return [
|
|
@@ -147019,10 +147160,6 @@ function buildUnsubmittedReviewPrompt(mode) {
|
|
|
147019
147160
|
}
|
|
147020
147161
|
async function collectPostRunIssues(ctx, options = {}) {
|
|
147021
147162
|
const issues = {};
|
|
147022
|
-
if (ctx.stopScript) {
|
|
147023
|
-
const failure = await executeStopHook(ctx.stopScript);
|
|
147024
|
-
if (failure) issues.stopHook = failure;
|
|
147025
|
-
}
|
|
147026
147163
|
const status = getGitStatus();
|
|
147027
147164
|
const mode = ctx.toolState.selectedMode;
|
|
147028
147165
|
if (status) {
|
|
@@ -147058,11 +147195,25 @@ function buildLearningsReflectionPrompt(filePath) {
|
|
|
147058
147195
|
"",
|
|
147059
147196
|
`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.`,
|
|
147060
147197
|
"",
|
|
147061
|
-
`
|
|
147062
|
-
`-
|
|
147063
|
-
`-
|
|
147064
|
-
`-
|
|
147065
|
-
|
|
147198
|
+
`structure:`,
|
|
147199
|
+
`- 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\`).`,
|
|
147200
|
+
`- **no section over ~300 lines.** when a section is approaching that, split it: introduce \`### \` subsections grouping related bullets, or hoist a coherent group into a new top-level \`## \` section. granular sections mean future runs read targeted line ranges instead of slurping the whole file. this is the most important hygiene rule on long-lived repos.`,
|
|
147201
|
+
`- if you find a flat unstructured list (legacy content from before this format), restructure it: read it, group related bullets, rewrite the file with \`## \` / \`### \` headings around them. don't preserve bad structure \u2014 fix it.`,
|
|
147202
|
+
"",
|
|
147203
|
+
`bullet hygiene:`,
|
|
147204
|
+
`- one fact per line starting with \`- \`. each bullet is ONE specific durable fact, not a paragraph or essay.`,
|
|
147205
|
+
`- 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.`,
|
|
147206
|
+
`- 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.`,
|
|
147207
|
+
`- 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.`,
|
|
147208
|
+
`- deduplicate against existing entries (in any section) \u2014 if a bullet covers the same fact, update it in place instead of adding a duplicate.`,
|
|
147209
|
+
"",
|
|
147210
|
+
`do NOT add bullets for:`,
|
|
147211
|
+
`- pullfrog tool quirks (e.g. "\`shell\` timeout is in milliseconds", "\`git\` args must be a JSON array", "\`create_pull_request_review\` drops out-of-hunk comments", "\`push_branch\` may report timeout when push succeeded"). these are universal across repos and belong in the tool descriptions \u2014 flag the gap rather than hoarding the workaround per-repo.`,
|
|
147212
|
+
`- references to specific PR numbers, review IDs, commit SHAs, branch names, or person handles ("PR #595 introduced X", "flagged in review 12345", "as of commit abc123"). repo state changes; these decay into noise within weeks.`,
|
|
147213
|
+
`- dated assertions ("as of May 2026", "currently...", "for now..."). if a fact needs a date to be true, it isn't durable enough to belong here.`,
|
|
147214
|
+
`- play-by-play of what THIS run did. learnings are for the NEXT run, not a retrospective.`,
|
|
147215
|
+
"",
|
|
147216
|
+
`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.`
|
|
147066
147217
|
].join("\n");
|
|
147067
147218
|
}
|
|
147068
147219
|
async function runPostRunRetryLoop(params) {
|
|
@@ -147266,8 +147417,9 @@ function writeMcpConfig(ctx) {
|
|
|
147266
147417
|
function buildAgentsJson() {
|
|
147267
147418
|
const agents2 = {
|
|
147268
147419
|
[REVIEWER_AGENT_NAME]: {
|
|
147269
|
-
description: "Read-only review subagent for
|
|
147270
|
-
prompt: REVIEWER_SYSTEM_PROMPT
|
|
147420
|
+
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.",
|
|
147421
|
+
prompt: REVIEWER_SYSTEM_PROMPT,
|
|
147422
|
+
model: "claude-sonnet-4-6"
|
|
147271
147423
|
}
|
|
147272
147424
|
};
|
|
147273
147425
|
return JSON.stringify(agents2);
|
|
@@ -147463,6 +147615,7 @@ async function runClaude(params) {
|
|
|
147463
147615
|
}
|
|
147464
147616
|
};
|
|
147465
147617
|
const recentStderr = [];
|
|
147618
|
+
const recentNonJsonStdout = [];
|
|
147466
147619
|
let lastProviderError = null;
|
|
147467
147620
|
const output = new TailBuffer(DEFAULT_MAX_RETAINED_BYTES);
|
|
147468
147621
|
let stdoutBuffer = "";
|
|
@@ -147501,6 +147654,8 @@ async function runClaude(params) {
|
|
|
147501
147654
|
event = JSON.parse(trimmed);
|
|
147502
147655
|
} catch {
|
|
147503
147656
|
log.debug(`\xBB non-JSON stdout line: ${trimmed.substring(0, 200)}`);
|
|
147657
|
+
recentNonJsonStdout.push(trimmed);
|
|
147658
|
+
if (recentNonJsonStdout.length > MAX_STDERR_LINES) recentNonJsonStdout.shift();
|
|
147504
147659
|
continue;
|
|
147505
147660
|
}
|
|
147506
147661
|
eventCount++;
|
|
@@ -147531,10 +147686,10 @@ async function runClaude(params) {
|
|
|
147531
147686
|
if (!trimmed) return;
|
|
147532
147687
|
recentStderr.push(trimmed);
|
|
147533
147688
|
if (recentStderr.length > MAX_STDERR_LINES) recentStderr.shift();
|
|
147534
|
-
const
|
|
147535
|
-
if (
|
|
147536
|
-
lastProviderError =
|
|
147537
|
-
log.info(`\xBB provider error detected (${
|
|
147689
|
+
const match3 = findProviderErrorMatch(trimmed);
|
|
147690
|
+
if (match3) {
|
|
147691
|
+
lastProviderError = match3.label;
|
|
147692
|
+
log.info(`\xBB provider error detected (${match3.label}): ${match3.excerpt}`);
|
|
147538
147693
|
} else {
|
|
147539
147694
|
log.debug(trimmed);
|
|
147540
147695
|
}
|
|
@@ -147566,7 +147721,8 @@ ${stderrContext}`);
|
|
|
147566
147721
|
const stdoutSnapshot = output.toString();
|
|
147567
147722
|
const stderrSnapshot = recentStderr.join("\n");
|
|
147568
147723
|
const truncatedStdout = stdoutSnapshot ? tailLines(stdoutSnapshot, 2048) : "";
|
|
147569
|
-
const
|
|
147724
|
+
const nonJsonStdoutSnapshot = recentNonJsonStdout.join("\n");
|
|
147725
|
+
const errorMessage = lastResultError || stderrSnapshot || nonJsonStdoutSnapshot || truncatedStdout || `unknown error - no output from Claude CLI${errorContext}`;
|
|
147570
147726
|
log.error(
|
|
147571
147727
|
`${params.label} exited with code ${result.exitCode}${errorContext}: ${errorMessage}`
|
|
147572
147728
|
);
|
|
@@ -147667,7 +147823,9 @@ var claude = agent({
|
|
|
147667
147823
|
run: async (ctx) => {
|
|
147668
147824
|
const cliPath = await installClaudeCli();
|
|
147669
147825
|
const specifier = ctx.payload.proxyModel ?? ctx.resolvedModel;
|
|
147670
|
-
const
|
|
147826
|
+
const bedrockModelId = process.env[BEDROCK_MODEL_ID_ENV]?.trim();
|
|
147827
|
+
const isBedrockRoute = specifier !== void 0 && bedrockModelId !== void 0 && bedrockModelId === specifier && isBedrockAnthropicId(specifier);
|
|
147828
|
+
const model = !specifier ? void 0 : isBedrockRoute ? specifier : stripProviderPrefix(specifier);
|
|
147671
147829
|
const homeEnv = {
|
|
147672
147830
|
HOME: ctx.tmpdir,
|
|
147673
147831
|
XDG_CONFIG_HOME: join10(ctx.tmpdir, ".config")
|
|
@@ -147706,6 +147864,9 @@ var claude = agent({
|
|
|
147706
147864
|
...process.env,
|
|
147707
147865
|
...homeEnv
|
|
147708
147866
|
};
|
|
147867
|
+
if (isBedrockRoute) {
|
|
147868
|
+
env2.CLAUDE_CODE_USE_BEDROCK = "1";
|
|
147869
|
+
}
|
|
147709
147870
|
const repoDir = process.cwd();
|
|
147710
147871
|
log.info(`\xBB effort: ${effort}`);
|
|
147711
147872
|
log.debug(`\xBB starting Pullfrog (Claude Code): node ${baseArgs.join(" ")}`);
|
|
@@ -147746,6 +147907,68 @@ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "node:f
|
|
|
147746
147907
|
import { join as join11 } from "node:path";
|
|
147747
147908
|
import { performance as performance7 } from "node:perf_hooks";
|
|
147748
147909
|
|
|
147910
|
+
// utils/agentHangReport.ts
|
|
147911
|
+
var MAX_STDERR_BYTES = 3e3;
|
|
147912
|
+
function formatAgentHangBody(input) {
|
|
147913
|
+
if (!input.diagnostic) return null;
|
|
147914
|
+
const verb = input.isHang ? "stalled" : "failed";
|
|
147915
|
+
const cause = input.diagnostic.lastProviderError ? ` \u2014 likely cause: \`${input.diagnostic.lastProviderError}\`` : "";
|
|
147916
|
+
const headline = `**${input.diagnostic.label} ${verb}**${cause}`;
|
|
147917
|
+
const explanation = formatExplanation({
|
|
147918
|
+
isHang: input.isHang,
|
|
147919
|
+
errorMessage: input.errorMessage
|
|
147920
|
+
});
|
|
147921
|
+
const parts = [headline, "", `${explanation} ${formatEventsPart(input.diagnostic)}`];
|
|
147922
|
+
const tail = renderStderrTail(input.diagnostic.recentStderr);
|
|
147923
|
+
if (tail) {
|
|
147924
|
+
const fence = pickFence(tail);
|
|
147925
|
+
parts.push(
|
|
147926
|
+
"",
|
|
147927
|
+
"<details><summary>Recent agent stderr</summary>",
|
|
147928
|
+
"",
|
|
147929
|
+
fence,
|
|
147930
|
+
tail,
|
|
147931
|
+
fence,
|
|
147932
|
+
"",
|
|
147933
|
+
"</details>"
|
|
147934
|
+
);
|
|
147935
|
+
}
|
|
147936
|
+
return parts.join("\n");
|
|
147937
|
+
}
|
|
147938
|
+
function formatExplanation(input) {
|
|
147939
|
+
if (!input.isHang) return `The agent exited unexpectedly: ${input.errorMessage}`;
|
|
147940
|
+
const idleSec = parseIdleSec(input.errorMessage);
|
|
147941
|
+
if (idleSec === void 0) {
|
|
147942
|
+
return "The agent stopped emitting events and was killed by the activity-timeout watchdog.";
|
|
147943
|
+
}
|
|
147944
|
+
return `The agent stopped emitting events for ${idleSec}s and was killed by the activity-timeout watchdog.`;
|
|
147945
|
+
}
|
|
147946
|
+
function parseIdleSec(message) {
|
|
147947
|
+
const match3 = /no output for (\d+)s/.exec(message);
|
|
147948
|
+
return match3 ? Number(match3[1]) : void 0;
|
|
147949
|
+
}
|
|
147950
|
+
function formatEventsPart(diagnostic) {
|
|
147951
|
+
if (diagnostic.eventCount > 0) {
|
|
147952
|
+
return `${diagnostic.eventCount} events were processed before the failure.`;
|
|
147953
|
+
}
|
|
147954
|
+
if (diagnostic.lastProviderError) return "No events were emitted before the failure.";
|
|
147955
|
+
return "No events were emitted \u2014 check whether the model provider is reachable.";
|
|
147956
|
+
}
|
|
147957
|
+
function renderStderrTail(lines) {
|
|
147958
|
+
if (lines.length === 0) return "";
|
|
147959
|
+
const joined = lines.join("\n");
|
|
147960
|
+
if (joined.length <= MAX_STDERR_BYTES) return joined;
|
|
147961
|
+
return `... (older lines truncated)
|
|
147962
|
+
${joined.slice(-MAX_STDERR_BYTES)}`;
|
|
147963
|
+
}
|
|
147964
|
+
function pickFence(content) {
|
|
147965
|
+
let max = 0;
|
|
147966
|
+
for (const match3 of content.matchAll(/`+/g)) {
|
|
147967
|
+
if (match3[0].length > max) max = match3[0].length;
|
|
147968
|
+
}
|
|
147969
|
+
return "`".repeat(Math.max(3, max + 1));
|
|
147970
|
+
}
|
|
147971
|
+
|
|
147749
147972
|
// agents/opencodePlugin.ts
|
|
147750
147973
|
var PULLFROG_BUS_EVENT_TYPE = "pullfrog_bus_event";
|
|
147751
147974
|
var PULLFROG_OPENCODE_PLUGIN_FILENAME = "pullfrog-events.ts";
|
|
@@ -147827,6 +148050,22 @@ export default async function pullfrogEventsPlugin() {
|
|
|
147827
148050
|
}
|
|
147828
148051
|
`;
|
|
147829
148052
|
|
|
148053
|
+
// agents/subagentModels.ts
|
|
148054
|
+
function deriveSubagentModels(orchestratorSpec) {
|
|
148055
|
+
if (!orchestratorSpec) return { reviewer: void 0 };
|
|
148056
|
+
for (const source of modelAliases) {
|
|
148057
|
+
const matchedDirect = source.resolve === orchestratorSpec;
|
|
148058
|
+
const matchedOR = source.openRouterResolve === orchestratorSpec;
|
|
148059
|
+
if (!matchedDirect && !matchedOR) continue;
|
|
148060
|
+
if (!source.subagentModel) return { reviewer: void 0 };
|
|
148061
|
+
const target = modelAliases.find((a) => a.slug === source.subagentModel);
|
|
148062
|
+
if (!target) return { reviewer: void 0 };
|
|
148063
|
+
const reviewer = matchedOR ? target.openRouterResolve : target.resolve;
|
|
148064
|
+
return { reviewer };
|
|
148065
|
+
}
|
|
148066
|
+
return { reviewer: void 0 };
|
|
148067
|
+
}
|
|
148068
|
+
|
|
147830
148069
|
// agents/opencode.ts
|
|
147831
148070
|
async function installOpencodeCli() {
|
|
147832
148071
|
return await installFromNpmTarball({
|
|
@@ -147836,7 +148075,6 @@ async function installOpencodeCli() {
|
|
|
147836
148075
|
installDependencies: true
|
|
147837
148076
|
});
|
|
147838
148077
|
}
|
|
147839
|
-
var PULLFROG_OPENCODE_OUTPUT_LIMIT = 5e3;
|
|
147840
148078
|
var GEMINI_3_DIRECT_THINKING_LEVEL = "medium";
|
|
147841
148079
|
var GEMINI_3_DIRECT_API_IDS = ["gemini-3.1-pro-preview", "gemini-3-flash-preview"];
|
|
147842
148080
|
function buildSecurityConfig(ctx, model) {
|
|
@@ -147852,7 +148090,12 @@ function buildSecurityConfig(ctx, model) {
|
|
|
147852
148090
|
mcp: {
|
|
147853
148091
|
[pullfrogMcpName]: { type: "remote", url: ctx.mcpServerUrl }
|
|
147854
148092
|
},
|
|
147855
|
-
agent:
|
|
148093
|
+
agent: (() => {
|
|
148094
|
+
const cfg = buildReviewerAgentConfig(model);
|
|
148095
|
+
const reviewerModel = cfg[REVIEWER_AGENT_NAME]?.model ?? "(inherit)";
|
|
148096
|
+
log.info(`\xBB subagent models: reviewfrog=${reviewerModel}`);
|
|
148097
|
+
return cfg;
|
|
148098
|
+
})(),
|
|
147856
148099
|
// opt into opencode's experimental `batch` tool (added in
|
|
147857
148100
|
// anomalyco/opencode PR #2983, opt-in via `experimental.batch_tool`). it
|
|
147858
148101
|
// exposes a single `batch` tool that runs 1-25 independent tool calls
|
|
@@ -147886,12 +148129,14 @@ function buildSecurityConfig(ctx, model) {
|
|
|
147886
148129
|
}
|
|
147887
148130
|
return JSON.stringify(config3);
|
|
147888
148131
|
}
|
|
147889
|
-
function buildReviewerAgentConfig() {
|
|
148132
|
+
function buildReviewerAgentConfig(orchestratorModel) {
|
|
148133
|
+
const overrides = deriveSubagentModels(orchestratorModel);
|
|
147890
148134
|
return {
|
|
147891
148135
|
[REVIEWER_AGENT_NAME]: {
|
|
147892
|
-
description: "Read-only review subagent for
|
|
148136
|
+
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.",
|
|
147893
148137
|
mode: "subagent",
|
|
147894
|
-
prompt: REVIEWER_SYSTEM_PROMPT
|
|
148138
|
+
prompt: REVIEWER_SYSTEM_PROMPT,
|
|
148139
|
+
...overrides.reviewer !== void 0 ? { model: overrides.reviewer } : {}
|
|
147895
148140
|
}
|
|
147896
148141
|
};
|
|
147897
148142
|
}
|
|
@@ -147916,7 +148161,7 @@ function autoSelectModel(cliPath) {
|
|
|
147916
148161
|
const availableSet = new Set(availableModels);
|
|
147917
148162
|
if (availableSet.size > 0) {
|
|
147918
148163
|
log.debug(`\xBB opencode models (${availableSet.size}): ${availableModels.join(", ")}`);
|
|
147919
|
-
const match3 = modelAliases.find((a) => a.preferred && availableSet.has(a.resolve)) ?? modelAliases.find((a) => availableSet.has(a.resolve));
|
|
148164
|
+
const match3 = modelAliases.find((a) => !a.hidden && a.preferred && availableSet.has(a.resolve)) ?? modelAliases.find((a) => !a.hidden && availableSet.has(a.resolve));
|
|
147920
148165
|
if (match3) {
|
|
147921
148166
|
log.info(
|
|
147922
148167
|
`\xBB model: ${match3.resolve} (auto-selected${match3.preferred ? " \u2014 preferred" : ""} curated match)`
|
|
@@ -148116,8 +148361,7 @@ async function runOpenCode(params) {
|
|
|
148116
148361
|
log.debug(withLabel(label, ` output: ${event.part.state.output}`));
|
|
148117
148362
|
}
|
|
148118
148363
|
if (event.part?.state?.status === "error") {
|
|
148119
|
-
|
|
148120
|
-
log.info(withLabel(label, `\xBB tool call failed: ${errorMsg}`));
|
|
148364
|
+
log.info(withLabel(label, `\xBB tool call failed: ${event.part.state.error}`));
|
|
148121
148365
|
}
|
|
148122
148366
|
if (toolName.includes("report_progress") && params.todoTracker) {
|
|
148123
148367
|
log.debug("\xBB report_progress detected, disabling todo tracking");
|
|
@@ -148129,19 +148373,20 @@ async function runOpenCode(params) {
|
|
|
148129
148373
|
},
|
|
148130
148374
|
tool_result: (event) => {
|
|
148131
148375
|
const toolId = event.part?.callID || event.tool_id;
|
|
148132
|
-
const
|
|
148133
|
-
const
|
|
148376
|
+
const state = event.part?.state;
|
|
148377
|
+
const status = state?.status ?? event.status ?? "unknown";
|
|
148378
|
+
const payload = state?.status === "completed" ? state.output : state?.status === "error" ? state.error : event.output;
|
|
148134
148379
|
const label = eventLabel(event);
|
|
148135
148380
|
timerFor(label).markToolResult();
|
|
148136
148381
|
if (taskDispatchByCallID.size > 0 || pendingTaskDispatches.length > 0) {
|
|
148137
148382
|
if (toolId && taskDispatchByCallID.has(toolId)) {
|
|
148138
148383
|
const dispatch = taskDispatchByCallID.get(toolId);
|
|
148139
|
-
if (dispatch) emitSubagentFinished(dispatch, status,
|
|
148384
|
+
if (dispatch) emitSubagentFinished(dispatch, status, payload, "exact");
|
|
148140
148385
|
} else {
|
|
148141
148386
|
const callIDIsKnownNonTask = toolId ? knownNonTaskCallIDs.has(toolId) : false;
|
|
148142
148387
|
if (!callIDIsKnownNonTask && pendingTaskDispatches.length > 0) {
|
|
148143
148388
|
const dispatch = pendingTaskDispatches[0];
|
|
148144
|
-
emitSubagentFinished(dispatch, status,
|
|
148389
|
+
emitSubagentFinished(dispatch, status, payload, "fifo");
|
|
148145
148390
|
}
|
|
148146
148391
|
}
|
|
148147
148392
|
}
|
|
@@ -148157,13 +148402,8 @@ async function runOpenCode(params) {
|
|
|
148157
148402
|
`\xBB ${params.label} tool_result${stepContext}: id=${toolId}, status=${status}, duration=${Math.round(toolDuration)}ms`
|
|
148158
148403
|
)
|
|
148159
148404
|
);
|
|
148160
|
-
if (
|
|
148161
|
-
log.debug(
|
|
148162
|
-
withLabel(
|
|
148163
|
-
label,
|
|
148164
|
-
` output: ${typeof output2 === "string" ? output2 : JSON.stringify(output2)}`
|
|
148165
|
-
)
|
|
148166
|
-
);
|
|
148405
|
+
if (payload) {
|
|
148406
|
+
log.debug(withLabel(label, ` output: ${payload}`));
|
|
148167
148407
|
}
|
|
148168
148408
|
if (toolDuration > 5e3) {
|
|
148169
148409
|
log.info(
|
|
@@ -148176,11 +148416,9 @@ async function runOpenCode(params) {
|
|
|
148176
148416
|
}
|
|
148177
148417
|
}
|
|
148178
148418
|
if (status === "error") {
|
|
148179
|
-
|
|
148180
|
-
|
|
148181
|
-
|
|
148182
|
-
const outputStr = typeof output2 === "string" ? output2 : JSON.stringify(output2);
|
|
148183
|
-
log.debug(withLabel(label, `tool output: ${outputStr}`));
|
|
148419
|
+
log.info(withLabel(label, `\xBB tool call failed: ${payload ?? "(no error message)"}`));
|
|
148420
|
+
} else if (payload) {
|
|
148421
|
+
log.debug(withLabel(label, `tool output: ${payload}`));
|
|
148184
148422
|
}
|
|
148185
148423
|
},
|
|
148186
148424
|
error: (event) => {
|
|
@@ -148257,6 +148495,13 @@ async function runOpenCode(params) {
|
|
|
148257
148495
|
const recentStderr = [];
|
|
148258
148496
|
let lastProviderError = null;
|
|
148259
148497
|
let agentErrorEvent = null;
|
|
148498
|
+
const diagnostic = {
|
|
148499
|
+
label: params.label,
|
|
148500
|
+
recentStderr,
|
|
148501
|
+
lastProviderError: void 0,
|
|
148502
|
+
eventCount: 0
|
|
148503
|
+
};
|
|
148504
|
+
params.toolState.agentDiagnostic = diagnostic;
|
|
148260
148505
|
const output = new TailBuffer(DEFAULT_MAX_RETAINED_BYTES);
|
|
148261
148506
|
let stdoutBuffer = "";
|
|
148262
148507
|
try {
|
|
@@ -148305,6 +148550,7 @@ async function runOpenCode(params) {
|
|
|
148305
148550
|
continue;
|
|
148306
148551
|
}
|
|
148307
148552
|
eventCount++;
|
|
148553
|
+
diagnostic.eventCount = eventCount;
|
|
148308
148554
|
log.debug(JSON.stringify(event, null, 2));
|
|
148309
148555
|
const timeSinceLastActivity = getIdleMs();
|
|
148310
148556
|
if (timeSinceLastActivity > 1e4) {
|
|
@@ -148336,10 +148582,11 @@ async function runOpenCode(params) {
|
|
|
148336
148582
|
if (!trimmed) return;
|
|
148337
148583
|
recentStderr.push(trimmed);
|
|
148338
148584
|
if (recentStderr.length > MAX_STDERR_LINES) recentStderr.shift();
|
|
148339
|
-
const
|
|
148340
|
-
if (
|
|
148341
|
-
lastProviderError =
|
|
148342
|
-
|
|
148585
|
+
const match3 = findProviderErrorMatch(trimmed);
|
|
148586
|
+
if (match3) {
|
|
148587
|
+
lastProviderError = match3.label;
|
|
148588
|
+
diagnostic.lastProviderError = match3.label;
|
|
148589
|
+
log.info(`\xBB provider error detected (${match3.label}): ${match3.excerpt}`);
|
|
148343
148590
|
} else {
|
|
148344
148591
|
log.debug(trimmed);
|
|
148345
148592
|
}
|
|
@@ -148429,10 +148676,11 @@ ${stderrContext}`);
|
|
|
148429
148676
|
`\xBB recent stderr (last ${Math.min(recentStderr.length, 10)} lines):
|
|
148430
148677
|
${stderrContext}`
|
|
148431
148678
|
);
|
|
148679
|
+
const body = formatAgentHangBody({ diagnostic, isHang: isActivityTimeout, errorMessage });
|
|
148432
148680
|
return {
|
|
148433
148681
|
success: false,
|
|
148434
148682
|
output: finalOutput || output.toString(),
|
|
148435
|
-
error: `${errorMessage} [${diagnosis}]`,
|
|
148683
|
+
error: body ?? `${errorMessage} [${diagnosis}]`,
|
|
148436
148684
|
usage: buildUsage()
|
|
148437
148685
|
};
|
|
148438
148686
|
}
|
|
@@ -148442,7 +148690,10 @@ var opencode = agent({
|
|
|
148442
148690
|
install: installOpencodeCli,
|
|
148443
148691
|
run: async (ctx) => {
|
|
148444
148692
|
const cliPath = await installOpencodeCli();
|
|
148445
|
-
const
|
|
148693
|
+
const rawModel = ctx.payload.proxyModel ?? ctx.resolvedModel ?? autoSelectModel(cliPath);
|
|
148694
|
+
const bedrockModelId = process.env[BEDROCK_MODEL_ID_ENV]?.trim();
|
|
148695
|
+
const isBedrockRoute = rawModel !== void 0 && bedrockModelId !== void 0 && bedrockModelId === rawModel;
|
|
148696
|
+
const model = isBedrockRoute ? `amazon-bedrock/${rawModel}` : rawModel;
|
|
148446
148697
|
const homeEnv = {
|
|
148447
148698
|
HOME: ctx.tmpdir,
|
|
148448
148699
|
XDG_CONFIG_HOME: join11(ctx.tmpdir, ".config")
|
|
@@ -148471,7 +148722,6 @@ var opencode = agent({
|
|
|
148471
148722
|
...homeEnv,
|
|
148472
148723
|
OPENCODE_CONFIG_CONTENT: buildSecurityConfig(ctx, model),
|
|
148473
148724
|
OPENCODE_PERMISSION: permissionOverride,
|
|
148474
|
-
OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX: PULLFROG_OPENCODE_OUTPUT_LIMIT.toString(),
|
|
148475
148725
|
GOOGLE_GENERATIVE_AI_API_KEY: process.env.GOOGLE_GENERATIVE_AI_API_KEY || process.env.GEMINI_API_KEY
|
|
148476
148726
|
};
|
|
148477
148727
|
const repoDir = process.cwd();
|
|
@@ -148482,6 +148732,7 @@ var opencode = agent({
|
|
|
148482
148732
|
cliPath,
|
|
148483
148733
|
cwd: repoDir,
|
|
148484
148734
|
env: env2,
|
|
148735
|
+
toolState: ctx.toolState,
|
|
148485
148736
|
todoTracker: ctx.todoTracker,
|
|
148486
148737
|
onActivityTimeout: ctx.onActivityTimeout,
|
|
148487
148738
|
onToolUse: ctx.onToolUse
|
|
@@ -148514,13 +148765,29 @@ function hasEnvVar(name) {
|
|
|
148514
148765
|
function hasClaudeCodeAuth() {
|
|
148515
148766
|
return hasEnvVar("CLAUDE_CODE_OAUTH_TOKEN") || hasEnvVar("ANTHROPIC_API_KEY");
|
|
148516
148767
|
}
|
|
148768
|
+
function hasBedrockAuth() {
|
|
148769
|
+
return hasEnvVar("AWS_BEARER_TOKEN_BEDROCK") || hasEnvVar("AWS_ACCESS_KEY_ID") && hasEnvVar("AWS_SECRET_ACCESS_KEY");
|
|
148770
|
+
}
|
|
148771
|
+
function resolveSlug(slug2) {
|
|
148772
|
+
const alias = resolveDisplayAlias(slug2);
|
|
148773
|
+
if (alias?.routing === "bedrock") {
|
|
148774
|
+
const bedrockId = process.env[BEDROCK_MODEL_ID_ENV]?.trim();
|
|
148775
|
+
if (!bedrockId) {
|
|
148776
|
+
throw new Error(
|
|
148777
|
+
`${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.`
|
|
148778
|
+
);
|
|
148779
|
+
}
|
|
148780
|
+
return bedrockId;
|
|
148781
|
+
}
|
|
148782
|
+
return resolveCliModel(slug2);
|
|
148783
|
+
}
|
|
148517
148784
|
function resolveModel(ctx) {
|
|
148518
148785
|
const envModel = process.env.PULLFROG_MODEL?.trim();
|
|
148519
148786
|
if (envModel) {
|
|
148520
|
-
return
|
|
148787
|
+
return resolveSlug(envModel) ?? envModel;
|
|
148521
148788
|
}
|
|
148522
148789
|
if (ctx.slug) {
|
|
148523
|
-
const resolved =
|
|
148790
|
+
const resolved = resolveSlug(ctx.slug);
|
|
148524
148791
|
if (resolved) {
|
|
148525
148792
|
return resolved;
|
|
148526
148793
|
}
|
|
@@ -148536,6 +148803,9 @@ function resolveAgent(ctx) {
|
|
|
148536
148803
|
}
|
|
148537
148804
|
log.warning(`\xBB unknown PULLFROG_AGENT="${envAgent}" \u2014 falling through to auto-select`);
|
|
148538
148805
|
}
|
|
148806
|
+
if (ctx.model && hasBedrockAuth() && process.env[BEDROCK_MODEL_ID_ENV]?.trim() === ctx.model) {
|
|
148807
|
+
return isBedrockAnthropicId(ctx.model) ? agents.claude : agents.opencode;
|
|
148808
|
+
}
|
|
148539
148809
|
if (ctx.model) {
|
|
148540
148810
|
try {
|
|
148541
148811
|
const provider2 = getModelProvider(ctx.model);
|
|
@@ -148550,31 +148820,56 @@ function resolveAgent(ctx) {
|
|
|
148550
148820
|
|
|
148551
148821
|
// utils/apiKeys.ts
|
|
148552
148822
|
var knownApiKeys = new Set(Object.values(providers).flatMap((p) => [...p.envVars]));
|
|
148823
|
+
var MISSING_KEY_MARKER = "no API key found";
|
|
148553
148824
|
function buildMissingApiKeyError(params) {
|
|
148554
|
-
const
|
|
148555
|
-
const settingsUrl = `${
|
|
148556
|
-
|
|
148557
|
-
|
|
148558
|
-
|
|
148825
|
+
const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
|
|
148826
|
+
const settingsUrl = `${getApiUrl()}/console/${params.owner}/${params.name}`;
|
|
148827
|
+
return [
|
|
148828
|
+
`**${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.`,
|
|
148829
|
+
"",
|
|
148830
|
+
`[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)`
|
|
148831
|
+
].join("\n");
|
|
148832
|
+
}
|
|
148833
|
+
function buildBedrockSetupError(params) {
|
|
148834
|
+
const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
|
|
148835
|
+
return `Bedrock model selected but required configuration is missing: ${params.missing.join(", ")}.
|
|
148559
148836
|
|
|
148560
|
-
|
|
148837
|
+
add the missing secret(s) to your GitHub repository at ${githubSecretsUrl}, then reference them in your workflow's \`env:\` block:
|
|
148561
148838
|
|
|
148562
|
-
|
|
148563
|
-
|
|
148564
|
-
|
|
148565
|
-
4. set the value to your API key
|
|
148566
|
-
5. click "Add secret"
|
|
148839
|
+
AWS_BEARER_TOKEN_BEDROCK: \${{ secrets.AWS_BEARER_TOKEN_BEDROCK }}
|
|
148840
|
+
AWS_REGION: \${{ secrets.AWS_REGION }}
|
|
148841
|
+
${BEDROCK_MODEL_ID_ENV}: \${{ secrets.${BEDROCK_MODEL_ID_ENV} }}
|
|
148567
148842
|
|
|
148568
|
-
|
|
148843
|
+
\`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.
|
|
148569
148844
|
|
|
148570
|
-
for full setup instructions, see https://docs.pullfrog.com/
|
|
148845
|
+
for full setup instructions, see https://docs.pullfrog.com/bedrock`;
|
|
148571
148846
|
}
|
|
148572
148847
|
function hasEnvVar2(name) {
|
|
148573
148848
|
const value2 = process.env[name];
|
|
148574
148849
|
return typeof value2 === "string" && value2.length > 0;
|
|
148575
148850
|
}
|
|
148851
|
+
function validateBedrockSetup(params) {
|
|
148852
|
+
const hasAuth = hasEnvVar2("AWS_BEARER_TOKEN_BEDROCK") || hasEnvVar2("AWS_ACCESS_KEY_ID") && hasEnvVar2("AWS_SECRET_ACCESS_KEY");
|
|
148853
|
+
const missing = [];
|
|
148854
|
+
if (!hasAuth)
|
|
148855
|
+
missing.push("AWS_BEARER_TOKEN_BEDROCK (or AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY)");
|
|
148856
|
+
if (!hasEnvVar2("AWS_REGION")) missing.push("AWS_REGION");
|
|
148857
|
+
if (!hasEnvVar2(BEDROCK_MODEL_ID_ENV)) missing.push(BEDROCK_MODEL_ID_ENV);
|
|
148858
|
+
if (missing.length > 0) {
|
|
148859
|
+
throw new Error(buildBedrockSetupError({ owner: params.owner, name: params.name, missing }));
|
|
148860
|
+
}
|
|
148861
|
+
}
|
|
148576
148862
|
function validateAgentApiKey(params) {
|
|
148577
148863
|
if (params.model) {
|
|
148864
|
+
const alias = resolveDisplayAlias(params.model);
|
|
148865
|
+
if (alias?.routing === "bedrock") {
|
|
148866
|
+
validateBedrockSetup({ owner: params.owner, name: params.name });
|
|
148867
|
+
return;
|
|
148868
|
+
}
|
|
148869
|
+
if (!params.model.includes("/")) {
|
|
148870
|
+
validateBedrockSetup({ owner: params.owner, name: params.name });
|
|
148871
|
+
return;
|
|
148872
|
+
}
|
|
148578
148873
|
const requiredVars = getModelEnvVars(params.model);
|
|
148579
148874
|
if (requiredVars.length === 0) return;
|
|
148580
148875
|
if (requiredVars.some((v) => hasEnvVar2(v))) return;
|
|
@@ -148585,6 +148880,22 @@ function validateAgentApiKey(params) {
|
|
|
148585
148880
|
throw new Error(buildMissingApiKeyError({ owner: params.owner, name: params.name }));
|
|
148586
148881
|
}
|
|
148587
148882
|
}
|
|
148883
|
+
function isApiKeyAuthError(text) {
|
|
148884
|
+
if (!text) return false;
|
|
148885
|
+
return text.includes(MISSING_KEY_MARKER) || /Invalid API key/i.test(text) || /\bUser not found\b/i.test(text) || /\bInvalid authentication\b/i.test(text);
|
|
148886
|
+
}
|
|
148887
|
+
function formatApiKeyErrorSummary(params) {
|
|
148888
|
+
if (params.raw.includes(MISSING_KEY_MARKER)) {
|
|
148889
|
+
return buildMissingApiKeyError({ owner: params.owner, name: params.name });
|
|
148890
|
+
}
|
|
148891
|
+
const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
|
|
148892
|
+
const settingsUrl = `${getApiUrl()}/console/${params.owner}/${params.name}`;
|
|
148893
|
+
return [
|
|
148894
|
+
`**Your LLM provider API key was rejected (401).** Rotate the key in your provider dashboard, then update the matching GitHub Actions secret.`,
|
|
148895
|
+
"",
|
|
148896
|
+
`[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)`
|
|
148897
|
+
].join("\n");
|
|
148898
|
+
}
|
|
148588
148899
|
|
|
148589
148900
|
// utils/body.ts
|
|
148590
148901
|
var import_turndown = __toESM(require_turndown_cjs(), 1);
|
|
@@ -153109,10 +153420,31 @@ function buildPromptContext(ctx) {
|
|
|
153109
153420
|
userQuoted: user ? user.split("\n").map((line) => `> ${line}`).join("\n") : ""
|
|
153110
153421
|
};
|
|
153111
153422
|
}
|
|
153112
|
-
function
|
|
153113
|
-
|
|
153423
|
+
function renderLearningsToc(headings) {
|
|
153424
|
+
if (headings.length === 0) return "";
|
|
153425
|
+
const rootDepth = Math.min(...headings.map((h) => h.depth));
|
|
153426
|
+
return headings.map((h) => {
|
|
153427
|
+
const indent2 = " ".repeat((h.depth - rootDepth) * 2);
|
|
153428
|
+
return `${indent2}- ${h.title} (L${h.startLine}-L${h.endLine})`;
|
|
153429
|
+
}).join("\n");
|
|
153430
|
+
}
|
|
153431
|
+
function buildLearningsSection(ctx) {
|
|
153432
|
+
if (!ctx.filePath) return "";
|
|
153433
|
+
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).`;
|
|
153434
|
+
const tocBody = ctx.headings.length === 0 ? "(no headings yet \u2014 file is empty or a flat list. read the whole file. during the post-run reflection turn, structure it with `## ` / `### ` headings so future runs can read targeted ranges.)" : `Read targeted line ranges via your native file tool \u2014 do NOT slurp the whole file. Each range starts at the section heading line, so reading the range gives you heading + body together.
|
|
153435
|
+
|
|
153436
|
+
${renderLearningsToc(ctx.headings)}`;
|
|
153437
|
+
return `************* LEARNINGS *************
|
|
153438
|
+
|
|
153439
|
+
${intro}
|
|
153114
153440
|
|
|
153115
|
-
|
|
153441
|
+
${tocBody}`;
|
|
153442
|
+
}
|
|
153443
|
+
function assembleFullPrompt(ctx) {
|
|
153444
|
+
const learningsSection = buildLearningsSection({
|
|
153445
|
+
filePath: ctx.learningsFilePath,
|
|
153446
|
+
headings: ctx.learningsHeadings
|
|
153447
|
+
});
|
|
153116
153448
|
const runtimeSection = `************* RUNTIME *************
|
|
153117
153449
|
|
|
153118
153450
|
${ctx.runtime}`;
|
|
@@ -153140,7 +153472,10 @@ function resolveInstructions(ctx) {
|
|
|
153140
153472
|
tocEntries.push({ label: "EVENT CONTEXT", description: "related PR/issue data" });
|
|
153141
153473
|
tocEntries.push({ label: "SYSTEM", description: "persona, security, tools, workflow rules" });
|
|
153142
153474
|
if (pctx.learningsFilePath)
|
|
153143
|
-
tocEntries.push({
|
|
153475
|
+
tocEntries.push({
|
|
153476
|
+
label: "LEARNINGS",
|
|
153477
|
+
description: "repo-specific knowledge file path + heading TOC"
|
|
153478
|
+
});
|
|
153144
153479
|
tocEntries.push({ label: "RUNTIME", description: "environment metadata" });
|
|
153145
153480
|
const toc = buildToc(tocEntries);
|
|
153146
153481
|
const full = assembleFullPrompt({
|
|
@@ -153150,6 +153485,7 @@ function resolveInstructions(ctx) {
|
|
|
153150
153485
|
eventContext,
|
|
153151
153486
|
system,
|
|
153152
153487
|
learningsFilePath: pctx.learningsFilePath,
|
|
153488
|
+
learningsHeadings: pctx.learningsHeadings,
|
|
153153
153489
|
runtime: pctx.runtime
|
|
153154
153490
|
});
|
|
153155
153491
|
const event = [pctx.eventTitle, pctx.eventMetadata].filter(Boolean).join("\n\n---\n\n");
|
|
@@ -153167,7 +153503,7 @@ function resolveInstructions(ctx) {
|
|
|
153167
153503
|
import { mkdir, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
153168
153504
|
import { dirname as dirname4, join as join14 } from "node:path";
|
|
153169
153505
|
var LEARNINGS_FILE_NAME = "pullfrog-learnings.md";
|
|
153170
|
-
var MAX_LEARNINGS_LENGTH =
|
|
153506
|
+
var MAX_LEARNINGS_LENGTH = 1e5;
|
|
153171
153507
|
function learningsFilePath(tmpdir3) {
|
|
153172
153508
|
return join14(tmpdir3, LEARNINGS_FILE_NAME);
|
|
153173
153509
|
}
|
|
@@ -153177,6 +153513,15 @@ async function seedLearningsFile(params) {
|
|
|
153177
153513
|
await writeFile2(path3, params.current ?? "", "utf8");
|
|
153178
153514
|
return path3;
|
|
153179
153515
|
}
|
|
153516
|
+
var TRUNCATION_LINE_BOUNDARY_TOLERANCE = 4096;
|
|
153517
|
+
function truncateAtLineBoundary(body, cap) {
|
|
153518
|
+
if (body.length <= cap) return body;
|
|
153519
|
+
const head = body.slice(0, cap);
|
|
153520
|
+
const lastNewline = head.lastIndexOf("\n");
|
|
153521
|
+
if (lastNewline <= 0) return head;
|
|
153522
|
+
if (cap - lastNewline > TRUNCATION_LINE_BOUNDARY_TOLERANCE) return head;
|
|
153523
|
+
return head.slice(0, lastNewline);
|
|
153524
|
+
}
|
|
153180
153525
|
async function readLearningsFile(path3) {
|
|
153181
153526
|
let raw2;
|
|
153182
153527
|
try {
|
|
@@ -153184,9 +153529,7 @@ async function readLearningsFile(path3) {
|
|
|
153184
153529
|
} catch {
|
|
153185
153530
|
return null;
|
|
153186
153531
|
}
|
|
153187
|
-
|
|
153188
|
-
if (trimmed.length > MAX_LEARNINGS_LENGTH) return trimmed.slice(0, MAX_LEARNINGS_LENGTH);
|
|
153189
|
-
return trimmed;
|
|
153532
|
+
return truncateAtLineBoundary(raw2.trim(), MAX_LEARNINGS_LENGTH);
|
|
153190
153533
|
}
|
|
153191
153534
|
|
|
153192
153535
|
// utils/normalizeEnv.ts
|
|
@@ -153533,6 +153876,7 @@ var defaultSettings = {
|
|
|
153533
153876
|
prApproveEnabled: false,
|
|
153534
153877
|
modeInstructions: {},
|
|
153535
153878
|
learnings: null,
|
|
153879
|
+
learningsHeadings: [],
|
|
153536
153880
|
envAllowlist: null
|
|
153537
153881
|
};
|
|
153538
153882
|
var defaultRunContext = {
|
|
@@ -153573,7 +153917,8 @@ async function fetchRunContext(params) {
|
|
|
153573
153917
|
setupScript: data.settings?.setupScript ?? null,
|
|
153574
153918
|
postCheckoutScript: data.settings?.postCheckoutScript ?? null,
|
|
153575
153919
|
prepushScript: data.settings?.prepushScript ?? null,
|
|
153576
|
-
stopScript: data.settings?.stopScript ?? null
|
|
153920
|
+
stopScript: data.settings?.stopScript ?? null,
|
|
153921
|
+
learningsHeadings: data.settings?.learningsHeadings ?? []
|
|
153577
153922
|
},
|
|
153578
153923
|
apiToken: data.apiToken,
|
|
153579
153924
|
oss: data.oss ?? false,
|
|
@@ -154339,10 +154684,7 @@ async function main() {
|
|
|
154339
154684
|
current: runContext.repoSettings.learnings
|
|
154340
154685
|
});
|
|
154341
154686
|
toolState.learningsFilePath = learningsPath;
|
|
154342
|
-
|
|
154343
|
-
toolState.learningsSeed = await readFile4(learningsPath, "utf8");
|
|
154344
|
-
} catch {
|
|
154345
|
-
}
|
|
154687
|
+
toolState.learningsSeed = (runContext.repoSettings.learnings ?? "").trim();
|
|
154346
154688
|
log.info(
|
|
154347
154689
|
`\xBB learnings seeded at ${learningsPath} (existing=${runContext.repoSettings.learnings ? "yes" : "no"})`
|
|
154348
154690
|
);
|
|
@@ -154382,7 +154724,8 @@ async function main() {
|
|
|
154382
154724
|
modes: modes2,
|
|
154383
154725
|
agentId,
|
|
154384
154726
|
outputSchema,
|
|
154385
|
-
learningsFilePath: toolState.learningsFilePath ?? null
|
|
154727
|
+
learningsFilePath: toolState.learningsFilePath ?? null,
|
|
154728
|
+
learningsHeadings: runContext.repoSettings.learningsHeadings
|
|
154386
154729
|
});
|
|
154387
154730
|
const logParts = [
|
|
154388
154731
|
instructions.eventInstructions ? `EVENT-LEVEL INSTRUCTIONS:
|
|
@@ -154519,10 +154862,13 @@ ${instructions.user}` : null,
|
|
|
154519
154862
|
await persistLearnings(toolContext);
|
|
154520
154863
|
}
|
|
154521
154864
|
if (!result.success && toolContext && toolState.progressComment) {
|
|
154522
|
-
|
|
154523
|
-
|
|
154524
|
-
|
|
154525
|
-
|
|
154865
|
+
const rawError = result.error || "agent run failed";
|
|
154866
|
+
const errorBody = isApiKeyAuthError(rawError) ? formatApiKeyErrorSummary({
|
|
154867
|
+
owner: runContext.repo.owner,
|
|
154868
|
+
name: runContext.repo.name,
|
|
154869
|
+
raw: rawError
|
|
154870
|
+
}) : rawError;
|
|
154871
|
+
await reportErrorToComment({ toolState, error: errorBody }).catch((error49) => {
|
|
154526
154872
|
log.debug(`failure error report failed: ${error49}`);
|
|
154527
154873
|
});
|
|
154528
154874
|
}
|
|
@@ -154558,19 +154904,29 @@ ${instructions.user}` : null,
|
|
|
154558
154904
|
killTrackedChildren();
|
|
154559
154905
|
log.error(errorMessage);
|
|
154560
154906
|
const billingError = isRouterKeylimitExhaustedError(errorMessage) ? new BillingError(errorMessage, { code: "router_keylimit_exhausted" }) : null;
|
|
154907
|
+
const isHang = errorMessage.startsWith("activity timeout") || errorMessage.startsWith("agent still pending");
|
|
154908
|
+
const hangBody = isHang ? formatAgentHangBody({ diagnostic: toolState.agentDiagnostic, isHang: true, errorMessage }) : null;
|
|
154909
|
+
const apiKeySource = hangBody ?? errorMessage;
|
|
154910
|
+
const apiKeyErrorSummary = !billingError && isApiKeyAuthError(apiKeySource) ? formatApiKeyErrorSummary({
|
|
154911
|
+
owner: runContext.repo.owner,
|
|
154912
|
+
name: runContext.repo.name,
|
|
154913
|
+
raw: apiKeySource
|
|
154914
|
+
}) : null;
|
|
154561
154915
|
try {
|
|
154562
|
-
const errorSummary = billingError ? formatBillingErrorSummary(billingError, runContext.repo.owner) : `### \u274C Pullfrog failed
|
|
154916
|
+
const errorSummary = billingError ? formatBillingErrorSummary(billingError, runContext.repo.owner) : apiKeyErrorSummary ?? (hangBody ? `### \u274C Pullfrog failed
|
|
154917
|
+
|
|
154918
|
+
${hangBody}` : `### \u274C Pullfrog failed
|
|
154563
154919
|
|
|
154564
154920
|
\`\`\`
|
|
154565
154921
|
${errorMessage}
|
|
154566
|
-
|
|
154922
|
+
\`\`\``);
|
|
154567
154923
|
const usageSummary = formatUsageSummary(toolState.usageEntries);
|
|
154568
154924
|
const parts = [errorSummary, toolState.lastProgressBody, usageSummary].filter(Boolean);
|
|
154569
154925
|
await writeSummary(parts.join("\n\n"));
|
|
154570
154926
|
} catch {
|
|
154571
154927
|
}
|
|
154572
154928
|
try {
|
|
154573
|
-
const commentBody = billingError ? formatBillingErrorSummary(billingError, runContext.repo.owner) : errorMessage;
|
|
154929
|
+
const commentBody = billingError ? formatBillingErrorSummary(billingError, runContext.repo.owner) : apiKeyErrorSummary ?? hangBody ?? errorMessage;
|
|
154574
154930
|
await reportErrorToComment({ toolState, error: commentBody });
|
|
154575
154931
|
} catch {
|
|
154576
154932
|
}
|