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/cli.mjs
CHANGED
|
@@ -108027,7 +108027,8 @@ var providers = {
|
|
|
108027
108027
|
displayName: "Claude Opus",
|
|
108028
108028
|
resolve: "anthropic/claude-opus-4-7",
|
|
108029
108029
|
openRouterResolve: "openrouter/anthropic/claude-opus-4.7",
|
|
108030
|
-
preferred: true
|
|
108030
|
+
preferred: true,
|
|
108031
|
+
subagentModel: "claude-sonnet"
|
|
108031
108032
|
},
|
|
108032
108033
|
"claude-sonnet": {
|
|
108033
108034
|
displayName: "Claude Sonnet",
|
|
@@ -108049,12 +108050,23 @@ var providers = {
|
|
|
108049
108050
|
displayName: "GPT",
|
|
108050
108051
|
resolve: "openai/gpt-5.5",
|
|
108051
108052
|
openRouterResolve: "openrouter/openai/gpt-5.5",
|
|
108052
|
-
preferred: true
|
|
108053
|
+
preferred: true,
|
|
108054
|
+
subagentModel: "gpt-5.4"
|
|
108053
108055
|
},
|
|
108054
108056
|
"gpt-pro": {
|
|
108055
108057
|
displayName: "GPT Pro",
|
|
108056
108058
|
resolve: "openai/gpt-5.5-pro",
|
|
108057
|
-
openRouterResolve: "openrouter/openai/gpt-5.5-pro"
|
|
108059
|
+
openRouterResolve: "openrouter/openai/gpt-5.5-pro",
|
|
108060
|
+
subagentModel: "gpt"
|
|
108061
|
+
},
|
|
108062
|
+
// hidden subagent target — `gpt` lenses run against this. surfacing
|
|
108063
|
+
// it in the picker would just confuse users (it's the prior-flagship,
|
|
108064
|
+
// and they already have `gpt` and `gpt-mini` to choose from).
|
|
108065
|
+
"gpt-5.4": {
|
|
108066
|
+
displayName: "GPT 5.4",
|
|
108067
|
+
resolve: "openai/gpt-5.4",
|
|
108068
|
+
openRouterResolve: "openrouter/openai/gpt-5.4",
|
|
108069
|
+
hidden: true
|
|
108058
108070
|
},
|
|
108059
108071
|
"gpt-mini": {
|
|
108060
108072
|
displayName: "GPT Mini",
|
|
@@ -108092,7 +108104,8 @@ var providers = {
|
|
|
108092
108104
|
displayName: "Gemini Pro",
|
|
108093
108105
|
resolve: "google/gemini-3.1-pro-preview",
|
|
108094
108106
|
openRouterResolve: "openrouter/google/gemini-3.1-pro-preview",
|
|
108095
|
-
preferred: true
|
|
108107
|
+
preferred: true,
|
|
108108
|
+
subagentModel: "gemini-flash"
|
|
108096
108109
|
},
|
|
108097
108110
|
"gemini-flash": {
|
|
108098
108111
|
displayName: "Gemini Flash",
|
|
@@ -108180,7 +108193,8 @@ var providers = {
|
|
|
108180
108193
|
"claude-opus": {
|
|
108181
108194
|
displayName: "Claude Opus",
|
|
108182
108195
|
resolve: "opencode/claude-opus-4-7",
|
|
108183
|
-
openRouterResolve: "openrouter/anthropic/claude-opus-4.7"
|
|
108196
|
+
openRouterResolve: "openrouter/anthropic/claude-opus-4.7",
|
|
108197
|
+
subagentModel: "claude-sonnet"
|
|
108184
108198
|
},
|
|
108185
108199
|
"claude-sonnet": {
|
|
108186
108200
|
displayName: "Claude Sonnet",
|
|
@@ -108195,12 +108209,21 @@ var providers = {
|
|
|
108195
108209
|
gpt: {
|
|
108196
108210
|
displayName: "GPT",
|
|
108197
108211
|
resolve: "opencode/gpt-5.5",
|
|
108198
|
-
openRouterResolve: "openrouter/openai/gpt-5.5"
|
|
108212
|
+
openRouterResolve: "openrouter/openai/gpt-5.5",
|
|
108213
|
+
subagentModel: "gpt-5.4"
|
|
108199
108214
|
},
|
|
108200
108215
|
"gpt-pro": {
|
|
108201
108216
|
displayName: "GPT Pro",
|
|
108202
108217
|
resolve: "opencode/gpt-5.5-pro",
|
|
108203
|
-
openRouterResolve: "openrouter/openai/gpt-5.5-pro"
|
|
108218
|
+
openRouterResolve: "openrouter/openai/gpt-5.5-pro",
|
|
108219
|
+
subagentModel: "gpt"
|
|
108220
|
+
},
|
|
108221
|
+
// hidden subagent target — see openai provider above for context.
|
|
108222
|
+
"gpt-5.4": {
|
|
108223
|
+
displayName: "GPT 5.4",
|
|
108224
|
+
resolve: "opencode/gpt-5.4",
|
|
108225
|
+
openRouterResolve: "openrouter/openai/gpt-5.4",
|
|
108226
|
+
hidden: true
|
|
108204
108227
|
},
|
|
108205
108228
|
"gpt-mini": {
|
|
108206
108229
|
displayName: "GPT Mini",
|
|
@@ -108223,7 +108246,8 @@ var providers = {
|
|
|
108223
108246
|
"gemini-pro": {
|
|
108224
108247
|
displayName: "Gemini Pro",
|
|
108225
108248
|
resolve: "opencode/gemini-3.1-pro",
|
|
108226
|
-
openRouterResolve: "openrouter/google/gemini-3.1-pro-preview"
|
|
108249
|
+
openRouterResolve: "openrouter/google/gemini-3.1-pro-preview",
|
|
108250
|
+
subagentModel: "gemini-flash"
|
|
108227
108251
|
},
|
|
108228
108252
|
"gemini-flash": {
|
|
108229
108253
|
displayName: "Gemini Flash",
|
|
@@ -108255,6 +108279,20 @@ var providers = {
|
|
|
108255
108279
|
}
|
|
108256
108280
|
}
|
|
108257
108281
|
}),
|
|
108282
|
+
bedrock: provider({
|
|
108283
|
+
displayName: "Amazon Bedrock",
|
|
108284
|
+
envVars: ["AWS_BEARER_TOKEN_BEDROCK", "AWS_REGION", "BEDROCK_MODEL_ID"],
|
|
108285
|
+
models: {
|
|
108286
|
+
// single routing entry — the actual Bedrock model ID is read from
|
|
108287
|
+
// BEDROCK_MODEL_ID at run time. see ModelRouting docs for why we
|
|
108288
|
+
// don't catalog individual Bedrock models.
|
|
108289
|
+
byok: {
|
|
108290
|
+
displayName: "Amazon Bedrock",
|
|
108291
|
+
resolve: "bedrock",
|
|
108292
|
+
routing: "bedrock"
|
|
108293
|
+
}
|
|
108294
|
+
}
|
|
108295
|
+
}),
|
|
108258
108296
|
openrouter: provider({
|
|
108259
108297
|
displayName: "OpenRouter",
|
|
108260
108298
|
envVars: ["OPENROUTER_API_KEY"],
|
|
@@ -108263,7 +108301,8 @@ var providers = {
|
|
|
108263
108301
|
displayName: "Claude Opus",
|
|
108264
108302
|
resolve: "openrouter/anthropic/claude-opus-4.7",
|
|
108265
108303
|
openRouterResolve: "openrouter/anthropic/claude-opus-4.7",
|
|
108266
|
-
preferred: true
|
|
108304
|
+
preferred: true,
|
|
108305
|
+
subagentModel: "claude-sonnet"
|
|
108267
108306
|
},
|
|
108268
108307
|
"claude-sonnet": {
|
|
108269
108308
|
displayName: "Claude Sonnet",
|
|
@@ -108278,12 +108317,21 @@ var providers = {
|
|
|
108278
108317
|
gpt: {
|
|
108279
108318
|
displayName: "GPT",
|
|
108280
108319
|
resolve: "openrouter/openai/gpt-5.5",
|
|
108281
|
-
openRouterResolve: "openrouter/openai/gpt-5.5"
|
|
108320
|
+
openRouterResolve: "openrouter/openai/gpt-5.5",
|
|
108321
|
+
subagentModel: "gpt-5.4"
|
|
108282
108322
|
},
|
|
108283
108323
|
"gpt-pro": {
|
|
108284
108324
|
displayName: "GPT Pro",
|
|
108285
108325
|
resolve: "openrouter/openai/gpt-5.5-pro",
|
|
108286
|
-
openRouterResolve: "openrouter/openai/gpt-5.5-pro"
|
|
108326
|
+
openRouterResolve: "openrouter/openai/gpt-5.5-pro",
|
|
108327
|
+
subagentModel: "gpt"
|
|
108328
|
+
},
|
|
108329
|
+
// hidden subagent target — see openai provider above for context.
|
|
108330
|
+
"gpt-5.4": {
|
|
108331
|
+
displayName: "GPT 5.4",
|
|
108332
|
+
resolve: "openrouter/openai/gpt-5.4",
|
|
108333
|
+
openRouterResolve: "openrouter/openai/gpt-5.4",
|
|
108334
|
+
hidden: true
|
|
108287
108335
|
},
|
|
108288
108336
|
"gpt-mini": {
|
|
108289
108337
|
displayName: "GPT Mini",
|
|
@@ -108311,7 +108359,8 @@ var providers = {
|
|
|
108311
108359
|
"gemini-pro": {
|
|
108312
108360
|
displayName: "Gemini Pro",
|
|
108313
108361
|
resolve: "openrouter/google/gemini-3.1-pro-preview",
|
|
108314
|
-
openRouterResolve: "openrouter/google/gemini-3.1-pro-preview"
|
|
108362
|
+
openRouterResolve: "openrouter/google/gemini-3.1-pro-preview",
|
|
108363
|
+
subagentModel: "gemini-flash"
|
|
108315
108364
|
},
|
|
108316
108365
|
"gemini-flash": {
|
|
108317
108366
|
displayName: "Gemini Flash",
|
|
@@ -108380,7 +108429,13 @@ var modelAliases = Object.entries(providers).flatMap(
|
|
|
108380
108429
|
openRouterResolve: def.openRouterResolve,
|
|
108381
108430
|
preferred: def.preferred ?? false,
|
|
108382
108431
|
isFree: def.isFree ?? false,
|
|
108383
|
-
fallback: def.fallback
|
|
108432
|
+
fallback: def.fallback,
|
|
108433
|
+
routing: def.routing,
|
|
108434
|
+
// subagentModel is stored as an alias key local to the provider; expand
|
|
108435
|
+
// here to a fully-qualified slug so callers can look up the target alias
|
|
108436
|
+
// directly without re-deriving the provider.
|
|
108437
|
+
subagentModel: def.subagentModel ? `${providerKey}/${def.subagentModel}` : void 0,
|
|
108438
|
+
hidden: def.hidden ?? false
|
|
108384
108439
|
}))
|
|
108385
108440
|
);
|
|
108386
108441
|
var MAX_FALLBACK_DEPTH = 10;
|
|
@@ -108400,6 +108455,10 @@ function resolveDisplayAlias(slug2) {
|
|
|
108400
108455
|
function resolveCliModel(slug2) {
|
|
108401
108456
|
return resolveDisplayAlias(slug2)?.resolve;
|
|
108402
108457
|
}
|
|
108458
|
+
var BEDROCK_MODEL_ID_ENV = "BEDROCK_MODEL_ID";
|
|
108459
|
+
function isBedrockAnthropicId(bedrockModelId) {
|
|
108460
|
+
return bedrockModelId.toLowerCase().split(/[./:]/).includes("anthropic");
|
|
108461
|
+
}
|
|
108403
108462
|
|
|
108404
108463
|
// utils/buildPullfrogFooter.ts
|
|
108405
108464
|
var PULLFROG_DIVIDER = "<!-- PULLFROG_DIVIDER_DO_NOT_REMOVE_PLZ -->";
|
|
@@ -109247,7 +109306,7 @@ var Comment = type({
|
|
|
109247
109306
|
function CreateCommentTool(ctx) {
|
|
109248
109307
|
return tool({
|
|
109249
109308
|
name: "create_issue_comment",
|
|
109250
|
-
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.",
|
|
109309
|
+
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.",
|
|
109251
109310
|
parameters: Comment,
|
|
109252
109311
|
execute: execute(async ({ issueNumber, body, type: commentType }) => {
|
|
109253
109312
|
const bodyWithFooter = addFooter(ctx, body);
|
|
@@ -109415,7 +109474,7 @@ async function reportProgress(ctx, params) {
|
|
|
109415
109474
|
function ReportProgressTool(ctx) {
|
|
109416
109475
|
return tool({
|
|
109417
109476
|
name: "report_progress",
|
|
109418
|
-
description:
|
|
109477
|
+
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.',
|
|
109419
109478
|
parameters: ReportProgress,
|
|
109420
109479
|
execute: execute(async (params) => {
|
|
109421
109480
|
let body = params.body;
|
|
@@ -109495,7 +109554,7 @@ function duplicateReplyDecision(params) {
|
|
|
109495
109554
|
function ReplyToReviewCommentTool(ctx) {
|
|
109496
109555
|
return tool({
|
|
109497
109556
|
name: "reply_to_review_comment",
|
|
109498
|
-
description:
|
|
109557
|
+
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).',
|
|
109499
109558
|
parameters: ReplyToReviewComment,
|
|
109500
109559
|
execute: execute(async ({ pull_number, comment_id, body }) => {
|
|
109501
109560
|
const bodyWithFooter = addFooter(ctx, body);
|
|
@@ -142638,7 +142697,7 @@ var import_semver = __toESM(require_semver2(), 1);
|
|
|
142638
142697
|
// package.json
|
|
142639
142698
|
var package_default = {
|
|
142640
142699
|
name: "pullfrog",
|
|
142641
|
-
version: "0.1.
|
|
142700
|
+
version: "0.1.8",
|
|
142642
142701
|
type: "module",
|
|
142643
142702
|
bin: {
|
|
142644
142703
|
pullfrog: "dist/cli.mjs",
|
|
@@ -143106,6 +143165,51 @@ function readNumber(params) {
|
|
|
143106
143165
|
import { execSync } from "node:child_process";
|
|
143107
143166
|
import { createHash } from "node:crypto";
|
|
143108
143167
|
import { readFileSync as readFileSync2, realpathSync, unlinkSync } from "node:fs";
|
|
143168
|
+
|
|
143169
|
+
// utils/shell.ts
|
|
143170
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
143171
|
+
function $(cmd, args2, options) {
|
|
143172
|
+
const encoding = options?.encoding ?? "utf-8";
|
|
143173
|
+
const env2 = resolveEnv(options?.env);
|
|
143174
|
+
const result = spawnSync2(cmd, args2, {
|
|
143175
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
143176
|
+
encoding,
|
|
143177
|
+
cwd: options?.cwd,
|
|
143178
|
+
env: env2
|
|
143179
|
+
});
|
|
143180
|
+
const stdout = result.stdout ?? "";
|
|
143181
|
+
const stderr = result.stderr ?? "";
|
|
143182
|
+
if (options?.log !== false) {
|
|
143183
|
+
const canWriteToStdout = process.stdout.isTTY === true;
|
|
143184
|
+
if (stdout) {
|
|
143185
|
+
if (canWriteToStdout) {
|
|
143186
|
+
process.stdout.write(stdout);
|
|
143187
|
+
} else {
|
|
143188
|
+
process.stderr.write(stdout);
|
|
143189
|
+
}
|
|
143190
|
+
}
|
|
143191
|
+
if (stderr) {
|
|
143192
|
+
process.stderr.write(stderr);
|
|
143193
|
+
}
|
|
143194
|
+
}
|
|
143195
|
+
if (result.status !== 0) {
|
|
143196
|
+
const errorResult = {
|
|
143197
|
+
status: result.status ?? -1,
|
|
143198
|
+
stdout,
|
|
143199
|
+
stderr
|
|
143200
|
+
};
|
|
143201
|
+
if (options?.onError) {
|
|
143202
|
+
options.onError(errorResult);
|
|
143203
|
+
return stdout.trim();
|
|
143204
|
+
}
|
|
143205
|
+
throw new Error(
|
|
143206
|
+
`Command failed with exit code ${errorResult.status}: ${stderr || "Unknown error"}`
|
|
143207
|
+
);
|
|
143208
|
+
}
|
|
143209
|
+
return stdout.trim();
|
|
143210
|
+
}
|
|
143211
|
+
|
|
143212
|
+
// utils/gitAuth.ts
|
|
143109
143213
|
var gitBinary;
|
|
143110
143214
|
function hashFile(path3) {
|
|
143111
143215
|
return createHash("sha256").update(readFileSync2(path3)).digest("hex");
|
|
@@ -143197,6 +143301,27 @@ ${stdout}` : stderr || stdout || "(no output)";
|
|
|
143197
143301
|
}
|
|
143198
143302
|
}
|
|
143199
143303
|
}
|
|
143304
|
+
var SHALLOW_UNREACHABLE_PATTERNS = [
|
|
143305
|
+
/Could not read [a-f0-9]{40,64}/,
|
|
143306
|
+
/remote did not send all necessary objects/
|
|
143307
|
+
];
|
|
143308
|
+
var DEEPEN_RETRY_DEPTH = 1e3;
|
|
143309
|
+
async function $gitFetchWithDeepen(args2, options, label) {
|
|
143310
|
+
try {
|
|
143311
|
+
return await $git("fetch", args2, options);
|
|
143312
|
+
} catch (err) {
|
|
143313
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
143314
|
+
const isShallowUnreachable = SHALLOW_UNREACHABLE_PATTERNS.some((p2) => p2.test(msg));
|
|
143315
|
+
if (!isShallowUnreachable) throw err;
|
|
143316
|
+
const isShallow = $("git", ["rev-parse", "--is-shallow-repository"], { log: false }).trim() === "true";
|
|
143317
|
+
if (!isShallow) throw err;
|
|
143318
|
+
log.info(
|
|
143319
|
+
`\xBB ${label ?? "git fetch"} hit shallow-unreachable error, retrying with --deepen=${DEEPEN_RETRY_DEPTH}`
|
|
143320
|
+
);
|
|
143321
|
+
const retryArgs = args2.filter((a) => !a.startsWith("--depth="));
|
|
143322
|
+
return await $git("fetch", [`--deepen=${DEEPEN_RETRY_DEPTH}`, ...retryArgs], options);
|
|
143323
|
+
}
|
|
143324
|
+
}
|
|
143200
143325
|
|
|
143201
143326
|
// lifecycle.ts
|
|
143202
143327
|
var LIFECYCLE_HOOK_TIMEOUT_MS = 6e5;
|
|
@@ -143238,49 +143363,6 @@ async function executeLifecycleHook(params) {
|
|
|
143238
143363
|
}
|
|
143239
143364
|
}
|
|
143240
143365
|
|
|
143241
|
-
// utils/shell.ts
|
|
143242
|
-
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
143243
|
-
function $(cmd, args2, options) {
|
|
143244
|
-
const encoding = options?.encoding ?? "utf-8";
|
|
143245
|
-
const env2 = resolveEnv(options?.env);
|
|
143246
|
-
const result = spawnSync2(cmd, args2, {
|
|
143247
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
143248
|
-
encoding,
|
|
143249
|
-
cwd: options?.cwd,
|
|
143250
|
-
env: env2
|
|
143251
|
-
});
|
|
143252
|
-
const stdout = result.stdout ?? "";
|
|
143253
|
-
const stderr = result.stderr ?? "";
|
|
143254
|
-
if (options?.log !== false) {
|
|
143255
|
-
const canWriteToStdout = process.stdout.isTTY === true;
|
|
143256
|
-
if (stdout) {
|
|
143257
|
-
if (canWriteToStdout) {
|
|
143258
|
-
process.stdout.write(stdout);
|
|
143259
|
-
} else {
|
|
143260
|
-
process.stderr.write(stdout);
|
|
143261
|
-
}
|
|
143262
|
-
}
|
|
143263
|
-
if (stderr) {
|
|
143264
|
-
process.stderr.write(stderr);
|
|
143265
|
-
}
|
|
143266
|
-
}
|
|
143267
|
-
if (result.status !== 0) {
|
|
143268
|
-
const errorResult = {
|
|
143269
|
-
status: result.status ?? -1,
|
|
143270
|
-
stdout,
|
|
143271
|
-
stderr
|
|
143272
|
-
};
|
|
143273
|
-
if (options?.onError) {
|
|
143274
|
-
options.onError(errorResult);
|
|
143275
|
-
return stdout.trim();
|
|
143276
|
-
}
|
|
143277
|
-
throw new Error(
|
|
143278
|
-
`Command failed with exit code ${errorResult.status}: ${stderr || "Unknown error"}`
|
|
143279
|
-
);
|
|
143280
|
-
}
|
|
143281
|
-
return stdout.trim();
|
|
143282
|
-
}
|
|
143283
|
-
|
|
143284
143366
|
// utils/rangeDiff.ts
|
|
143285
143367
|
function computeIncrementalDiff(params) {
|
|
143286
143368
|
try {
|
|
@@ -143494,7 +143576,7 @@ function PushBranchTool(ctx) {
|
|
|
143494
143576
|
const pushPermission = ctx.payload.push;
|
|
143495
143577
|
return tool({
|
|
143496
143578
|
name: "push_branch",
|
|
143497
|
-
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.",
|
|
143579
|
+
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.",
|
|
143498
143580
|
parameters: PushBranch,
|
|
143499
143581
|
execute: execute(async ({ branchName, force }) => {
|
|
143500
143582
|
if (pushPermission === "disabled") {
|
|
@@ -143633,7 +143715,7 @@ var Git = type({
|
|
|
143633
143715
|
function GitTool(ctx) {
|
|
143634
143716
|
return tool({
|
|
143635
143717
|
name: "git",
|
|
143636
|
-
description:
|
|
143718
|
+
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\'.',
|
|
143637
143719
|
parameters: Git,
|
|
143638
143720
|
execute: execute(async (params) => {
|
|
143639
143721
|
const command = params.command;
|
|
@@ -143675,15 +143757,10 @@ var GitFetch = type({
|
|
|
143675
143757
|
ref: type.string.describe("Ref to fetch: branch name, tag, or 'pull/N/head' for PRs"),
|
|
143676
143758
|
depth: type.number.describe("Fetch depth (for shallow clones)").optional()
|
|
143677
143759
|
});
|
|
143678
|
-
var SHALLOW_UNREACHABLE_PATTERNS = [
|
|
143679
|
-
/Could not read [a-f0-9]{40,64}/,
|
|
143680
|
-
/remote did not send all necessary objects/
|
|
143681
|
-
];
|
|
143682
|
-
var DEEPEN_RETRY_DEPTH = 1e3;
|
|
143683
143760
|
function GitFetchTool(ctx) {
|
|
143684
143761
|
return tool({
|
|
143685
143762
|
name: "git_fetch",
|
|
143686
|
-
description:
|
|
143763
|
+
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 })`.',
|
|
143687
143764
|
parameters: GitFetch,
|
|
143688
143765
|
execute: execute(async (params) => {
|
|
143689
143766
|
rejectIfLeadingDash(params.ref, "ref");
|
|
@@ -143691,20 +143768,7 @@ function GitFetchTool(ctx) {
|
|
|
143691
143768
|
if (params.depth !== void 0) {
|
|
143692
143769
|
fetchArgs.push(`--depth=${params.depth}`);
|
|
143693
143770
|
}
|
|
143694
|
-
|
|
143695
|
-
await $git("fetch", fetchArgs, { token: ctx.gitToken });
|
|
143696
|
-
} catch (err) {
|
|
143697
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
143698
|
-
const isShallowUnreachable = SHALLOW_UNREACHABLE_PATTERNS.some((p2) => p2.test(msg));
|
|
143699
|
-
const isShallow = isShallowUnreachable && $("git", ["rev-parse", "--is-shallow-repository"], { log: false }).trim() === "true";
|
|
143700
|
-
if (!isShallow) throw err;
|
|
143701
|
-
log.info(
|
|
143702
|
-
`\xBB git_fetch hit shallow-unreachable error, retrying with --deepen=${DEEPEN_RETRY_DEPTH}`
|
|
143703
|
-
);
|
|
143704
|
-
await $git("fetch", [`--deepen=${DEEPEN_RETRY_DEPTH}`, "--no-tags", "origin", params.ref], {
|
|
143705
|
-
token: ctx.gitToken
|
|
143706
|
-
});
|
|
143707
|
-
}
|
|
143771
|
+
await $gitFetchWithDeepen(fetchArgs, { token: ctx.gitToken }, "git_fetch");
|
|
143708
143772
|
return { success: true, ref: params.ref };
|
|
143709
143773
|
})
|
|
143710
143774
|
});
|
|
@@ -143917,13 +143981,15 @@ var CreatePullRequestReview = type({
|
|
|
143917
143981
|
approved: type.boolean.describe(
|
|
143918
143982
|
"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."
|
|
143919
143983
|
).optional(),
|
|
143920
|
-
commit_id: type.string.describe(
|
|
143984
|
+
commit_id: type.string.describe(
|
|
143985
|
+
"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."
|
|
143986
|
+
).optional(),
|
|
143921
143987
|
comments: type({
|
|
143922
143988
|
path: type.string.describe(
|
|
143923
143989
|
"The file path to comment on (relative to repo root). Must be a file that appears in the PR diff."
|
|
143924
143990
|
),
|
|
143925
143991
|
line: type.number.describe(
|
|
143926
|
-
"Line number to comment on. For multi-line ranges, this is the end line. Use NEW column from diff format."
|
|
143992
|
+
"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)."
|
|
143927
143993
|
),
|
|
143928
143994
|
side: type.enumerated("LEFT", "RIGHT").describe(
|
|
143929
143995
|
"Side of the diff: LEFT (old code, lines starting with -) or RIGHT (new code, lines starting with + or unchanged). Defaults to RIGHT."
|
|
@@ -143933,7 +143999,7 @@ var CreatePullRequestReview = type({
|
|
|
143933
143999
|
"Full replacement code for the line range [start_line, line]. MUST preserve the exact indentation of the original code."
|
|
143934
144000
|
).optional(),
|
|
143935
144001
|
start_line: type.number.describe(
|
|
143936
|
-
"Start line for multi-line comment ranges. Omit for single-line comments. The range [start_line, line] defines which lines a suggestion replaces."
|
|
144002
|
+
"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."
|
|
143937
144003
|
).optional()
|
|
143938
144004
|
}).array().describe(
|
|
143939
144005
|
"Inline comments on lines within diff hunks. Feedback about code outside the diff goes in 'body' instead."
|
|
@@ -143942,7 +144008,7 @@ var CreatePullRequestReview = type({
|
|
|
143942
144008
|
function CreatePullRequestReviewTool(ctx) {
|
|
143943
144009
|
return tool({
|
|
143944
144010
|
name: "create_pull_request_review",
|
|
143945
|
-
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.`,
|
|
144011
|
+
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.`,
|
|
143946
144012
|
parameters: CreatePullRequestReview,
|
|
143947
144013
|
execute: execute(async ({ pull_number, body, approved, commit_id, comments = [] }) => {
|
|
143948
144014
|
if (body) body = fixDoubleEscapedString(body);
|
|
@@ -144171,7 +144237,7 @@ function runDiffCoveragePreflight(params) {
|
|
|
144171
144237
|
);
|
|
144172
144238
|
const unreadText = unread.map((entry) => `- ${entry.path} (${entry.unreadLines} lines, ${entry.ranges})`).join("\n");
|
|
144173
144239
|
throw new Error(
|
|
144174
|
-
`diff coverage pre-flight: some TOC regions were not read before review submission. this is a one-time nudge \u2014
|
|
144240
|
+
`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.
|
|
144175
144241
|
|
|
144176
144242
|
unread TOC regions:
|
|
144177
144243
|
${unreadText}
|
|
@@ -144426,10 +144492,10 @@ async function ensureBeforeShaReachable(params) {
|
|
|
144426
144492
|
sha: params.sha,
|
|
144427
144493
|
ref: tempBranch
|
|
144428
144494
|
}), true);
|
|
144429
|
-
await $
|
|
144430
|
-
"fetch",
|
|
144495
|
+
await $gitFetchWithDeepen(
|
|
144431
144496
|
["--no-tags", ...params.isShallow ? ["--depth=1"] : [], "origin", tempBranch],
|
|
144432
|
-
{ token: params.gitToken }
|
|
144497
|
+
{ token: params.gitToken },
|
|
144498
|
+
`before_sha temp branch ${tempBranch}`
|
|
144433
144499
|
);
|
|
144434
144500
|
log.debug(`\xBB fetched before_sha via temp branch ${tempBranch}`);
|
|
144435
144501
|
return true;
|
|
@@ -144505,16 +144571,22 @@ async function checkoutPrBranch(pr, params) {
|
|
|
144505
144571
|
toolState.checkoutSha = $("git", ["rev-parse", "HEAD"], { log: false }).trim();
|
|
144506
144572
|
const alreadyOnBranch = toolState.checkoutSha === pr.headSha;
|
|
144507
144573
|
log.debug(`\xBB fetching base branch (${pr.baseRef})...`);
|
|
144508
|
-
await $
|
|
144574
|
+
await $gitFetchWithDeepen(
|
|
144575
|
+
["--no-tags", "origin", pr.baseRef],
|
|
144576
|
+
{ token: gitToken },
|
|
144577
|
+
`base branch ${pr.baseRef}`
|
|
144578
|
+
);
|
|
144509
144579
|
if (!alreadyOnBranch) {
|
|
144510
144580
|
$("git", ["checkout", "-B", pr.baseRef, `origin/${pr.baseRef}`], { log: false });
|
|
144511
144581
|
log.debug(`\xBB fetching PR #${pr.number} (${localBranch})...`);
|
|
144512
144582
|
await retry(
|
|
144513
144583
|
async () => {
|
|
144514
144584
|
try {
|
|
144515
|
-
await $
|
|
144516
|
-
|
|
144517
|
-
|
|
144585
|
+
await $gitFetchWithDeepen(
|
|
144586
|
+
["--no-tags", "origin", `+pull/${pr.number}/head:${localBranch}`],
|
|
144587
|
+
{ token: gitToken },
|
|
144588
|
+
`PR #${pr.number}`
|
|
144589
|
+
);
|
|
144518
144590
|
} catch (e) {
|
|
144519
144591
|
const msg = e instanceof Error ? e.message : String(e);
|
|
144520
144592
|
if (PULL_REF_MISSING_PATTERN.test(msg)) {
|
|
@@ -144615,134 +144687,159 @@ async function checkoutPrBranch(pr, params) {
|
|
|
144615
144687
|
});
|
|
144616
144688
|
return { hookWarning: postCheckoutHook.warning };
|
|
144617
144689
|
}
|
|
144690
|
+
var inFlightCheckouts = /* @__PURE__ */ new Map();
|
|
144618
144691
|
function CheckoutPrTool(ctx) {
|
|
144619
|
-
|
|
144620
|
-
|
|
144621
|
-
|
|
144622
|
-
|
|
144623
|
-
|
|
144624
|
-
|
|
144625
|
-
|
|
144626
|
-
|
|
144627
|
-
|
|
144628
|
-
|
|
144629
|
-
|
|
144630
|
-
|
|
144631
|
-
|
|
144632
|
-
|
|
144633
|
-
|
|
144634
|
-
|
|
144635
|
-
|
|
144636
|
-
|
|
144637
|
-
|
|
144638
|
-
|
|
144639
|
-
|
|
144640
|
-
|
|
144641
|
-
|
|
144642
|
-
|
|
144643
|
-
|
|
144644
|
-
|
|
144645
|
-
|
|
144646
|
-
|
|
144647
|
-
|
|
144648
|
-
|
|
144649
|
-
|
|
144650
|
-
|
|
144692
|
+
const runCheckout = async (pull_number) => {
|
|
144693
|
+
const prResponse = await ctx.octokit.rest.pulls.get({
|
|
144694
|
+
owner: ctx.repo.owner,
|
|
144695
|
+
repo: ctx.repo.name,
|
|
144696
|
+
pull_number
|
|
144697
|
+
});
|
|
144698
|
+
const headRepo = prResponse.data.head.repo;
|
|
144699
|
+
if (!headRepo) {
|
|
144700
|
+
throw new Error(`PR #${pull_number} source repository was deleted`);
|
|
144701
|
+
}
|
|
144702
|
+
const pr = {
|
|
144703
|
+
number: pull_number,
|
|
144704
|
+
headSha: prResponse.data.head.sha,
|
|
144705
|
+
headRef: prResponse.data.head.ref,
|
|
144706
|
+
headRepoFullName: headRepo.full_name,
|
|
144707
|
+
baseRef: prResponse.data.base.ref,
|
|
144708
|
+
baseRepoFullName: prResponse.data.base.repo.full_name,
|
|
144709
|
+
maintainerCanModify: prResponse.data.maintainer_can_modify
|
|
144710
|
+
};
|
|
144711
|
+
const checkoutResult = await checkoutPrBranch(pr, {
|
|
144712
|
+
octokit: ctx.octokit,
|
|
144713
|
+
owner: ctx.repo.owner,
|
|
144714
|
+
name: ctx.repo.name,
|
|
144715
|
+
gitToken: ctx.gitToken,
|
|
144716
|
+
toolState: ctx.toolState,
|
|
144717
|
+
shell: ctx.payload.shell,
|
|
144718
|
+
postCheckoutScript: ctx.postCheckoutScript,
|
|
144719
|
+
beforeSha: ctx.toolState.beforeSha
|
|
144720
|
+
});
|
|
144721
|
+
const tempDir = process.env.PULLFROG_TEMP_DIR;
|
|
144722
|
+
if (!tempDir) {
|
|
144723
|
+
throw new Error(
|
|
144724
|
+
"PULLFROG_TEMP_DIR not set - checkout_pr must run in pullfrog action context"
|
|
144725
|
+
);
|
|
144726
|
+
}
|
|
144727
|
+
const headShort = ctx.toolState.checkoutSha.slice(0, 7);
|
|
144728
|
+
let incrementalDiffPath;
|
|
144729
|
+
if (ctx.toolState.beforeSha && ctx.toolState.checkoutSha) {
|
|
144730
|
+
const beforeShort = ctx.toolState.beforeSha.slice(0, 7);
|
|
144731
|
+
const incremental = computeIncrementalDiff({
|
|
144732
|
+
baseBranch: pr.baseRef,
|
|
144733
|
+
beforeSha: ctx.toolState.beforeSha,
|
|
144734
|
+
headSha: ctx.toolState.checkoutSha
|
|
144651
144735
|
});
|
|
144652
|
-
|
|
144653
|
-
|
|
144654
|
-
|
|
144655
|
-
|
|
144736
|
+
if (incremental) {
|
|
144737
|
+
incrementalDiffPath = join3(
|
|
144738
|
+
tempDir,
|
|
144739
|
+
`pr-${pull_number}-${beforeShort}-${headShort}-incremental.diff`
|
|
144740
|
+
);
|
|
144741
|
+
writeFileSync(incrementalDiffPath, incremental);
|
|
144742
|
+
log.info(
|
|
144743
|
+
`\xBB incremental diff computed (${incremental.length} bytes) \u2192 ${incrementalDiffPath}`
|
|
144656
144744
|
);
|
|
144657
144745
|
}
|
|
144658
|
-
|
|
144659
|
-
|
|
144660
|
-
|
|
144661
|
-
|
|
144662
|
-
const incremental = computeIncrementalDiff({
|
|
144663
|
-
baseBranch: pr.baseRef,
|
|
144664
|
-
beforeSha: ctx.toolState.beforeSha,
|
|
144665
|
-
headSha: ctx.toolState.checkoutSha
|
|
144666
|
-
});
|
|
144667
|
-
if (incremental) {
|
|
144668
|
-
incrementalDiffPath = join3(
|
|
144669
|
-
tempDir,
|
|
144670
|
-
`pr-${pull_number}-${beforeShort}-${headShort}-incremental.diff`
|
|
144671
|
-
);
|
|
144672
|
-
writeFileSync(incrementalDiffPath, incremental);
|
|
144673
|
-
log.info(
|
|
144674
|
-
`\xBB incremental diff computed (${incremental.length} bytes) \u2192 ${incrementalDiffPath}`
|
|
144675
|
-
);
|
|
144676
|
-
}
|
|
144677
|
-
}
|
|
144678
|
-
const formatResult = await fetchAndFormatPrDiff(ctx, pull_number);
|
|
144679
|
-
const diffPreview = formatResult.content.split("\n").slice(0, 100).join("\n");
|
|
144680
|
-
log.debug(`formatted diff preview (first 100 lines):
|
|
144746
|
+
}
|
|
144747
|
+
const formatResult = await fetchAndFormatPrDiff(ctx, pull_number);
|
|
144748
|
+
const diffPreview = formatResult.content.split("\n").slice(0, 100).join("\n");
|
|
144749
|
+
log.debug(`formatted diff preview (first 100 lines):
|
|
144681
144750
|
${diffPreview}`);
|
|
144682
|
-
|
|
144683
|
-
|
|
144684
|
-
|
|
144685
|
-
|
|
144686
|
-
|
|
144687
|
-
|
|
144688
|
-
|
|
144689
|
-
|
|
144751
|
+
const diffPath = join3(tempDir, `pr-${pull_number}-${headShort}.diff`);
|
|
144752
|
+
writeFileSync(diffPath, formatResult.content);
|
|
144753
|
+
log.debug(`wrote diff to ${diffPath} (${formatResult.content.length} bytes)`);
|
|
144754
|
+
ctx.toolState.diffCoverage = createDiffCoverageState({
|
|
144755
|
+
diffPath,
|
|
144756
|
+
totalLines: countLines({ content: formatResult.content }),
|
|
144757
|
+
toc: formatResult.toc,
|
|
144758
|
+
previous: ctx.toolState.diffCoverage
|
|
144759
|
+
});
|
|
144760
|
+
log.debug(
|
|
144761
|
+
`\xBB diff coverage initialized: diffPath=${diffPath}, totalLines=${ctx.toolState.diffCoverage.totalLines}, tocEntries=${ctx.toolState.diffCoverage.tocEntries.length}`
|
|
144762
|
+
);
|
|
144763
|
+
const cached4 = /* @__PURE__ */ new Map();
|
|
144764
|
+
for (const file2 of formatResult.files) {
|
|
144765
|
+
cached4.set(file2.filename, commentableLinesForFile(file2.patch));
|
|
144766
|
+
}
|
|
144767
|
+
ctx.toolState.commentableLinesByFile = cached4;
|
|
144768
|
+
ctx.toolState.commentableLinesPullNumber = pull_number;
|
|
144769
|
+
ctx.toolState.commentableLinesCheckoutSha = ctx.toolState.checkoutSha;
|
|
144770
|
+
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.` : "";
|
|
144771
|
+
const COMMIT_LOG_MAX = 200;
|
|
144772
|
+
const baseRange = `origin/${pr.baseRef}..HEAD`;
|
|
144773
|
+
let commitCount = 0;
|
|
144774
|
+
let commitLog = "";
|
|
144775
|
+
let commitLogUnavailable = false;
|
|
144776
|
+
try {
|
|
144777
|
+
commitCount = parseInt(
|
|
144778
|
+
$("git", ["rev-list", "--count", baseRange], { log: false }).trim() || "0",
|
|
144779
|
+
10
|
|
144780
|
+
);
|
|
144781
|
+
commitLog = $("git", ["log", "--oneline", `--max-count=${COMMIT_LOG_MAX}`, baseRange], {
|
|
144782
|
+
log: false
|
|
144690
144783
|
});
|
|
144784
|
+
} catch (err) {
|
|
144785
|
+
commitLogUnavailable = true;
|
|
144691
144786
|
log.debug(
|
|
144692
|
-
`\xBB
|
|
144787
|
+
`\xBB unable to compute commit metadata for ${baseRange}: ${err instanceof Error ? err.message : String(err)}`
|
|
144693
144788
|
);
|
|
144694
|
-
|
|
144695
|
-
|
|
144696
|
-
|
|
144697
|
-
|
|
144698
|
-
|
|
144699
|
-
|
|
144700
|
-
|
|
144701
|
-
|
|
144702
|
-
|
|
144703
|
-
|
|
144704
|
-
|
|
144705
|
-
|
|
144706
|
-
|
|
144789
|
+
}
|
|
144790
|
+
const commitLogTruncated = commitCount > COMMIT_LOG_MAX;
|
|
144791
|
+
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.` : "";
|
|
144792
|
+
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.` : "";
|
|
144793
|
+
return {
|
|
144794
|
+
success: true,
|
|
144795
|
+
number: prResponse.data.number,
|
|
144796
|
+
title: prResponse.data.title,
|
|
144797
|
+
body: prResponse.data.body,
|
|
144798
|
+
base: pr.baseRef,
|
|
144799
|
+
localBranch: `pr-${pull_number}`,
|
|
144800
|
+
remoteBranch: `refs/heads/${pr.headRef}`,
|
|
144801
|
+
isFork: pr.headRepoFullName !== pr.baseRepoFullName,
|
|
144802
|
+
maintainerCanModify: pr.maintainerCanModify,
|
|
144803
|
+
url: prResponse.data.html_url,
|
|
144804
|
+
headRepo: pr.headRepoFullName,
|
|
144805
|
+
diffPath,
|
|
144806
|
+
incrementalDiffPath,
|
|
144807
|
+
toc: formatResult.toc,
|
|
144808
|
+
commitCount,
|
|
144809
|
+
commitLog,
|
|
144810
|
+
commitLogTruncated,
|
|
144811
|
+
commitLogUnavailable,
|
|
144812
|
+
hookWarning: checkoutResult.hookWarning,
|
|
144813
|
+
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
|
|
144814
|
+
};
|
|
144815
|
+
};
|
|
144816
|
+
return tool({
|
|
144817
|
+
name: "checkout_pr",
|
|
144818
|
+
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.",
|
|
144819
|
+
parameters: CheckoutPr,
|
|
144820
|
+
execute: execute(async ({ pull_number }) => {
|
|
144821
|
+
const inFlight = inFlightCheckouts.get(pull_number);
|
|
144822
|
+
if (inFlight) {
|
|
144823
|
+
log.info(`\xBB checkout_pr({pull_number:${pull_number}}) already in flight \u2014 sharing result`);
|
|
144824
|
+
return inFlight;
|
|
144825
|
+
}
|
|
144826
|
+
const current = ctx.toolState.issueNumber;
|
|
144827
|
+
if (current !== void 0 && current !== pull_number) {
|
|
144828
|
+
const dirty = $("git", ["status", "--porcelain"], { log: false }).trim();
|
|
144829
|
+
if (dirty) {
|
|
144830
|
+
throw new Error(
|
|
144831
|
+
`cannot checkout PR #${pull_number} while the working tree has uncommitted changes. commit, push, or discard them before switching. dirty paths:
|
|
144832
|
+
${dirty}`
|
|
144833
|
+
);
|
|
144834
|
+
}
|
|
144835
|
+
}
|
|
144836
|
+
const promise2 = runCheckout(pull_number);
|
|
144837
|
+
inFlightCheckouts.set(pull_number, promise2);
|
|
144707
144838
|
try {
|
|
144708
|
-
|
|
144709
|
-
|
|
144710
|
-
|
|
144711
|
-
);
|
|
144712
|
-
commitLog = $("git", ["log", "--oneline", `--max-count=${COMMIT_LOG_MAX}`, baseRange], {
|
|
144713
|
-
log: false
|
|
144714
|
-
});
|
|
144715
|
-
} catch (err) {
|
|
144716
|
-
commitLogUnavailable = true;
|
|
144717
|
-
log.debug(
|
|
144718
|
-
`\xBB unable to compute commit metadata for ${baseRange}: ${err instanceof Error ? err.message : String(err)}`
|
|
144719
|
-
);
|
|
144839
|
+
return await promise2;
|
|
144840
|
+
} finally {
|
|
144841
|
+
inFlightCheckouts.delete(pull_number);
|
|
144720
144842
|
}
|
|
144721
|
-
const commitLogTruncated = commitCount > COMMIT_LOG_MAX;
|
|
144722
|
-
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.` : "";
|
|
144723
|
-
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.` : "";
|
|
144724
|
-
return {
|
|
144725
|
-
success: true,
|
|
144726
|
-
number: prResponse.data.number,
|
|
144727
|
-
title: prResponse.data.title,
|
|
144728
|
-
body: prResponse.data.body,
|
|
144729
|
-
base: pr.baseRef,
|
|
144730
|
-
localBranch: `pr-${pull_number}`,
|
|
144731
|
-
remoteBranch: `refs/heads/${pr.headRef}`,
|
|
144732
|
-
isFork: pr.headRepoFullName !== pr.baseRepoFullName,
|
|
144733
|
-
maintainerCanModify: pr.maintainerCanModify,
|
|
144734
|
-
url: prResponse.data.html_url,
|
|
144735
|
-
headRepo: pr.headRepoFullName,
|
|
144736
|
-
diffPath,
|
|
144737
|
-
incrementalDiffPath,
|
|
144738
|
-
toc: formatResult.toc,
|
|
144739
|
-
commitCount,
|
|
144740
|
-
commitLog,
|
|
144741
|
-
commitLogTruncated,
|
|
144742
|
-
commitLogUnavailable,
|
|
144743
|
-
hookWarning: checkoutResult.hookWarning,
|
|
144744
|
-
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
|
|
144745
|
-
};
|
|
144746
144843
|
})
|
|
144747
144844
|
});
|
|
144748
144845
|
}
|
|
@@ -144929,7 +145026,7 @@ var CommitInfo = type({
|
|
|
144929
145026
|
function CommitInfoTool(ctx) {
|
|
144930
145027
|
return tool({
|
|
144931
145028
|
name: "get_commit_info",
|
|
144932
|
-
description:
|
|
145029
|
+
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" })`.',
|
|
144933
145030
|
parameters: CommitInfo,
|
|
144934
145031
|
execute: execute(async ({ sha }) => {
|
|
144935
145032
|
const response = await ctx.octokit.rest.repos.getCommit({
|
|
@@ -145020,7 +145117,7 @@ var GetIssueComments = type({
|
|
|
145020
145117
|
function GetIssueCommentsTool(ctx) {
|
|
145021
145118
|
return tool({
|
|
145022
145119
|
name: "get_issue_comments",
|
|
145023
|
-
description: "Get all comments for a GitHub issue. Returns all comments including the issue body and all subsequent discussion comments.",
|
|
145120
|
+
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 })`.",
|
|
145024
145121
|
parameters: GetIssueComments,
|
|
145025
145122
|
execute: execute(async ({ issue_number }) => {
|
|
145026
145123
|
ctx.toolState.issueNumber = issue_number;
|
|
@@ -145121,7 +145218,7 @@ var IssueInfo = type({
|
|
|
145121
145218
|
function IssueInfoTool(ctx) {
|
|
145122
145219
|
return tool({
|
|
145123
145220
|
name: "get_issue",
|
|
145124
|
-
description: "Retrieve GitHub issue information by issue number",
|
|
145221
|
+
description: "Retrieve GitHub issue information by issue number. Example: `get_issue({ issue_number: 1234 })`.",
|
|
145125
145222
|
parameters: IssueInfo,
|
|
145126
145223
|
execute: execute(async ({ issue_number }) => {
|
|
145127
145224
|
const issue3 = await ctx.octokit.rest.issues.get({
|
|
@@ -145363,7 +145460,7 @@ var PullRequestInfo = type({
|
|
|
145363
145460
|
function PullRequestInfoTool(ctx) {
|
|
145364
145461
|
return tool({
|
|
145365
145462
|
name: "get_pull_request",
|
|
145366
|
-
description: "Retrieve PR metadata (title, body, state, branches, author, labels, linked issues). To checkout a PR branch locally, use checkout_pr instead.",
|
|
145463
|
+
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.",
|
|
145367
145464
|
parameters: PullRequestInfo,
|
|
145368
145465
|
execute: execute(async ({ pull_number }) => {
|
|
145369
145466
|
const [restResponse, graphqlResponse] = await Promise.all([
|
|
@@ -145767,7 +145864,7 @@ async function getReviewData(input) {
|
|
|
145767
145864
|
function GetReviewCommentsTool(ctx) {
|
|
145768
145865
|
return tool({
|
|
145769
145866
|
name: "get_review_comments",
|
|
145770
|
-
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.",
|
|
145867
|
+
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.",
|
|
145771
145868
|
parameters: GetReviewComments,
|
|
145772
145869
|
execute: execute(async (params) => {
|
|
145773
145870
|
const approvedBy = ctx.payload.event.trigger === "fix_review" && ctx.payload.event.approved_only ? ctx.payload.triggerer : void 0;
|
|
@@ -145817,7 +145914,7 @@ var ListPullRequestReviews = type({
|
|
|
145817
145914
|
function ListPullRequestReviewsTool(ctx) {
|
|
145818
145915
|
return tool({
|
|
145819
145916
|
name: "list_pull_request_reviews",
|
|
145820
|
-
description: "List all reviews for a pull request. Returns all reviews including approvals, request changes, and comments.",
|
|
145917
|
+
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 })`.",
|
|
145821
145918
|
parameters: ListPullRequestReviews,
|
|
145822
145919
|
execute: execute(async (params) => {
|
|
145823
145920
|
const reviews = await ctx.octokit.paginate(ctx.octokit.rest.pulls.listReviews, {
|
|
@@ -145967,7 +146064,7 @@ function SelectModeTool(ctx) {
|
|
|
145967
146064
|
const overrides = buildModeOverrides(t2);
|
|
145968
146065
|
return tool({
|
|
145969
146066
|
name: "select_mode",
|
|
145970
|
-
description:
|
|
146067
|
+
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 })`.',
|
|
145971
146068
|
parameters: SelectModeParams,
|
|
145972
146069
|
execute: execute(async (params) => {
|
|
145973
146070
|
if (ctx.toolState.selectedMode) {
|
|
@@ -146028,7 +146125,9 @@ import { setTimeout as sleep2 } from "node:timers/promises";
|
|
|
146028
146125
|
var ShellParams = type({
|
|
146029
146126
|
command: "string",
|
|
146030
146127
|
description: "string",
|
|
146031
|
-
"timeout?":
|
|
146128
|
+
"timeout?": type.number.describe(
|
|
146129
|
+
"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."
|
|
146130
|
+
),
|
|
146032
146131
|
"working_directory?": "string",
|
|
146033
146132
|
"background?": "boolean"
|
|
146034
146133
|
});
|
|
@@ -146136,6 +146235,15 @@ function getTempDir() {
|
|
|
146136
146235
|
}
|
|
146137
146236
|
return tempDir;
|
|
146138
146237
|
}
|
|
146238
|
+
var MAX_OUTPUT_CHARS = 5e3;
|
|
146239
|
+
function capOutput(output) {
|
|
146240
|
+
if (output.length <= MAX_OUTPUT_CHARS) return output;
|
|
146241
|
+
const fullPath = join7(getTempDir(), `shell-${randomUUID2().slice(0, 8)}.log`);
|
|
146242
|
+
writeFileSync5(fullPath, output);
|
|
146243
|
+
const elided = output.length - MAX_OUTPUT_CHARS;
|
|
146244
|
+
return `... [${elided} chars truncated; full output saved to ${fullPath}] ...
|
|
146245
|
+
${output.slice(-MAX_OUTPUT_CHARS)}`;
|
|
146246
|
+
}
|
|
146139
146247
|
function isGitCommand(command) {
|
|
146140
146248
|
const trimmed = command.trim();
|
|
146141
146249
|
if (trimmed === "git" || trimmed.startsWith("git ")) return true;
|
|
@@ -146147,11 +146255,15 @@ function ShellTool(ctx) {
|
|
|
146147
146255
|
name: "shell",
|
|
146148
146256
|
description: `Execute shell commands securely. Environment is filtered to remove API keys and secrets.
|
|
146149
146257
|
|
|
146258
|
+
Example: \`shell({ command: "pnpm test", description: "run the test suite" })\`.
|
|
146259
|
+
|
|
146150
146260
|
Use this tool to:
|
|
146151
146261
|
- Run shell commands (ls, cat, grep, find, etc.)
|
|
146152
146262
|
- Execute build tools (npm, pnpm, cargo, make, etc.)
|
|
146153
146263
|
- Run tests and linters
|
|
146154
146264
|
|
|
146265
|
+
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.
|
|
146266
|
+
|
|
146155
146267
|
Do NOT use this tool for git commands \u2014 use the dedicated git tools instead.`,
|
|
146156
146268
|
parameters: ShellParams,
|
|
146157
146269
|
execute: execute(async (params) => {
|
|
@@ -146242,12 +146354,13 @@ ${stderr}` : stderr : stdout;
|
|
|
146242
146354
|
output = output ? `${output}
|
|
146243
146355
|
[timed out after ${timeout}ms]` : `[timed out after ${timeout}ms]`;
|
|
146244
146356
|
const finalExitCode = exitCode ?? (timedOut ? 124 : -1);
|
|
146357
|
+
const trimmed = output.trim();
|
|
146245
146358
|
if (finalExitCode !== 0) {
|
|
146246
146359
|
log.info(`shell command failed with exit code ${finalExitCode}: ${params.command}`);
|
|
146247
|
-
if (
|
|
146360
|
+
if (trimmed) log.info(`output: ${trimmed}`);
|
|
146248
146361
|
}
|
|
146249
146362
|
return {
|
|
146250
|
-
output:
|
|
146363
|
+
output: capOutput(trimmed),
|
|
146251
146364
|
exit_code: finalExitCode,
|
|
146252
146365
|
timed_out: timedOut
|
|
146253
146366
|
};
|
|
@@ -146664,18 +146777,24 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
146664
146777
|
- resolve addressed threads via \`${t2("resolve_review_thread")}\`
|
|
146665
146778
|
- call \`${t2("report_progress")}\` with a brief summary (or the exact push error if push failed)`
|
|
146666
146779
|
},
|
|
146667
|
-
// Review and IncrementalReview use
|
|
146668
|
-
// (
|
|
146669
|
-
//
|
|
146670
|
-
//
|
|
146671
|
-
//
|
|
146672
|
-
//
|
|
146673
|
-
//
|
|
146674
|
-
//
|
|
146675
|
-
//
|
|
146676
|
-
//
|
|
146677
|
-
//
|
|
146678
|
-
//
|
|
146780
|
+
// Review and IncrementalReview use a 0-or-2+ lens pattern. The default is
|
|
146781
|
+
// 0 lenses (orchestrator handles the review solo). Multi-lens (2+
|
|
146782
|
+
// reviewfrog subagents in parallel) only fires for substantive PRs or
|
|
146783
|
+
// high-stakes-subsystem touches — and when it fires, ALL lenses must
|
|
146784
|
+
// dispatch in a single assistant turn or the parallelism win disappears.
|
|
146785
|
+
// We never dispatch exactly one lens: a single lens is just a worse,
|
|
146786
|
+
// slower version of doing the work yourself.
|
|
146787
|
+
//
|
|
146788
|
+
// Build mode self-review is a different problem shape: the orchestrator
|
|
146789
|
+
// wrote the code, so bias-mitigation comes from delegating to one
|
|
146790
|
+
// fresh-eyes subagent that doesn't share the implementation context. A
|
|
146791
|
+
// single subagent there is appropriate; the 0-or-2+ rule applies only to
|
|
146792
|
+
// the Review/IncrementalReview lens fan-out where independence between
|
|
146793
|
+
// perspectives is what's being purchased.
|
|
146794
|
+
//
|
|
146795
|
+
// Deliberate omission vs canonical /anneal: severity categorization in
|
|
146796
|
+
// the final message (the review body has its own CAUTION/IMPORTANT
|
|
146797
|
+
// framing instead of a severity table).
|
|
146679
146798
|
{
|
|
146680
146799
|
name: "Review",
|
|
146681
146800
|
description: "Review code, PRs, or implementations; provide feedback or suggestions; identify issues; or check code quality, style, and correctness",
|
|
@@ -146685,9 +146804,9 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
146685
146804
|
|
|
146686
146805
|
2. **checkout**: call \`${t2("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.
|
|
146687
146806
|
|
|
146688
|
-
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).
|
|
146807
|
+
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.
|
|
146689
146808
|
|
|
146690
|
-
if the PR is **genuinely trivial**, skip
|
|
146809
|
+
if the PR is **genuinely trivial**, skip the fan-out entirely and submit a \`No new issues found.\` review per step 7.
|
|
146691
146810
|
|
|
146692
146811
|
"Genuinely trivial" (skip):
|
|
146693
146812
|
- single-word doc typo, whitespace/format-only, comment-only across any number of files
|
|
@@ -146706,25 +146825,25 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
146706
146825
|
- any "typo fix" in user-facing copy that changes meaning ("approved" \u2192 "denied")
|
|
146707
146826
|
- mixed diffs where a semantic 1-liner is buried in whitespace/formatting changes
|
|
146708
146827
|
|
|
146709
|
-
|
|
146828
|
+
4. **lens decision \u2014 0 or 2+, NEVER 1**.
|
|
146710
146829
|
|
|
146711
|
-
|
|
146830
|
+
The default is **0 lenses**: handle the review yourself end-to-end. Most PRs land here.
|
|
146712
146831
|
|
|
146713
|
-
|
|
146714
|
-
-
|
|
146715
|
-
-
|
|
146716
|
-
-
|
|
146832
|
+
Dispatch **2+ \`${REVIEWER_AGENT_NAME}\` lenses in parallel** ONLY when ALL of the following are true:
|
|
146833
|
+
- 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)
|
|
146834
|
+
- you can name 2+ distinct concrete failure modes that warrant independent lenses (one lens per failure mode; orthogonal, not overlapping)
|
|
146835
|
+
- parallel-orchestrated independent perspectives meaningfully outperform what you'd find solo
|
|
146717
146836
|
|
|
146718
|
-
**
|
|
146837
|
+
**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).
|
|
146719
146838
|
|
|
146720
|
-
|
|
146839
|
+
When you do go multi-lens, lens framings come in two flavors:
|
|
146721
146840
|
- **themed lenses** \u2014 a perspective applied across the whole diff (correctness, security, user-journey, performance, etc.).
|
|
146722
|
-
- **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").
|
|
146841
|
+
- **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.
|
|
146723
146842
|
|
|
146724
146843
|
starter menu (combine, omit, or invent your own):
|
|
146725
146844
|
- **correctness & invariants** \u2014 bugs, races, error handling, edge cases, state-machine boundaries
|
|
146726
|
-
- **impact** \u2014
|
|
146727
|
-
- **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.
|
|
146845
|
+
- **impact** \u2014 stale references in code/tests/docs/configs/UI after rename/remove
|
|
146846
|
+
- **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.
|
|
146728
146847
|
- **security** \u2014 new endpoints, authZ, input validation, secrets handling, replay/CSRF/injection, cross-tenant isolation
|
|
146729
146848
|
- **user-journey** \u2014 UX-touching flows: walk through happy path and failure modes as a user
|
|
146730
146849
|
- **operational readiness** \u2014 observability, alerting, migrations (forward + rollback), feature flags, on-call burden
|
|
@@ -146734,26 +146853,36 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
146734
146853
|
- **holistic** \u2014 does the PR make sense as a whole? symmetric flows (delete for every create, rollback for every migration)?
|
|
146735
146854
|
- **subsystem lenses** (invent as the PR demands) \u2014 auth, billing, payments, schema migration, webhooks, secrets, RBAC, multi-tenant isolation, cron/scheduling, etc.
|
|
146736
146855
|
|
|
146737
|
-
|
|
146856
|
+
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.
|
|
146857
|
+
|
|
146858
|
+
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.**
|
|
146859
|
+
|
|
146860
|
+
\u26A0\uFE0F CRITICAL \u2014 PARALLELISM IS THE ONLY REASON LENSES EXIST. \u26A0\uFE0F
|
|
146861
|
+
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.
|
|
146862
|
+
|
|
146863
|
+
\u2705 Right pattern: one assistant turn with N Task tool_use blocks \u2192 wait \u2192 N results arrive together \u2192 aggregate.
|
|
146864
|
+
\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.
|
|
146865
|
+
|
|
146866
|
+
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.
|
|
146867
|
+
|
|
146868
|
+
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:
|
|
146738
146869
|
- the diff path / target \u2014 reading the diff and the codebase is its job
|
|
146739
146870
|
- **only one lens** \u2014 never a multi-section "review for X, Y, and Z" prompt
|
|
146740
146871
|
- **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\`.
|
|
146741
|
-
- 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.
|
|
146742
146872
|
- 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."
|
|
146743
146873
|
- 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.
|
|
146744
146874
|
|
|
146745
146875
|
delegation discipline:
|
|
146746
|
-
- 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)
|
|
146747
146876
|
- do NOT summarize the PR for them (biases toward a validation frame)
|
|
146748
146877
|
- do NOT hand them a curated reading list (let them discover scope)
|
|
146749
146878
|
- do NOT pre-shape their output with a finding schema
|
|
146750
146879
|
- do NOT mention the other lenses (independence is the point \u2014 overlapping findings are a strong signal)
|
|
146751
146880
|
|
|
146752
|
-
|
|
146881
|
+
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.
|
|
146753
146882
|
|
|
146754
146883
|
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.
|
|
146755
146884
|
|
|
146756
|
-
|
|
146885
|
+
7. **submit**: ALWAYS submit exactly one review via \`${t2("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.
|
|
146757
146886
|
|
|
146758
146887
|
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.
|
|
146759
146888
|
|
|
@@ -146781,10 +146910,10 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
146781
146910
|
|
|
146782
146911
|
${PR_SUMMARY_FORMAT}`
|
|
146783
146912
|
},
|
|
146784
|
-
// IncrementalReview shares Review's
|
|
146785
|
-
//
|
|
146786
|
-
//
|
|
146787
|
-
//
|
|
146913
|
+
// IncrementalReview shares Review's 0-or-2+ lens pattern but scopes the
|
|
146914
|
+
// target to the incremental diff. The "issues must be NEW since the last
|
|
146915
|
+
// Pullfrog review" filter lives at aggregation time (step 8), NOT in the
|
|
146916
|
+
// subagent prompt — pushing the filter into
|
|
146788
146917
|
// subagents matches the canonical anneal anti-pattern of "list known
|
|
146789
146918
|
// pre-existing failures — don't flag these" and suppresses signal on
|
|
146790
146919
|
// regressions the new commits amplified. The review body is just
|
|
@@ -146803,38 +146932,57 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
146803
146932
|
|
|
146804
146933
|
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.
|
|
146805
146934
|
|
|
146806
|
-
4. **prior feedback**: fetch previous reviews via \`${t2("list_pull_request_reviews")}\`. for the most recent Pullfrog review, call \`${t2("get_review_comments")}\` with the review ID to retrieve specific prior line-level feedback. you'll use this to filter your aggregation in step
|
|
146935
|
+
4. **prior feedback**: fetch previous reviews via \`${t2("list_pull_request_reviews")}\`. for the most recent Pullfrog review, call \`${t2("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.
|
|
146807
146936
|
|
|
146808
|
-
5. **triage
|
|
146937
|
+
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.**
|
|
146809
146938
|
|
|
146810
|
-
if the incremental changes are **genuinely trivial**, skip the fan-out entirely and jump to step
|
|
146939
|
+
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).
|
|
146811
146940
|
|
|
146812
146941
|
"Genuinely trivial" (skip): formatting/comment tweaks, import reordering, lockfile regen, mechanical rename of import paths, whitespace-only.
|
|
146813
146942
|
"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.
|
|
146814
146943
|
When unsure, treat as non-trivial.
|
|
146815
146944
|
|
|
146816
|
-
|
|
146945
|
+
6. **lens decision \u2014 0 or 2+, NEVER 1**.
|
|
146946
|
+
|
|
146947
|
+
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."
|
|
146948
|
+
|
|
146949
|
+
Dispatch **2+ \`${REVIEWER_AGENT_NAME}\` lenses in parallel** ONLY when ALL of the following are true:
|
|
146950
|
+
- 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)
|
|
146951
|
+
- you can name 2+ distinct concrete failure modes the new commits plausibly introduce that warrant independent lenses
|
|
146952
|
+
- parallel-orchestrated independent perspectives meaningfully outperform what you'd find solo
|
|
146953
|
+
|
|
146954
|
+
**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.
|
|
146955
|
+
|
|
146956
|
+
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.
|
|
146817
146957
|
|
|
146818
|
-
|
|
146819
|
-
|
|
146958
|
+
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.**
|
|
146959
|
+
|
|
146960
|
+
\u26A0\uFE0F CRITICAL \u2014 PARALLELISM IS THE ONLY REASON LENSES EXIST. \u26A0\uFE0F
|
|
146961
|
+
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.
|
|
146962
|
+
|
|
146963
|
+
\u2705 Right pattern: one assistant turn with N Task tool_use blocks \u2192 wait \u2192 N results arrive together \u2192 aggregate.
|
|
146964
|
+
\u274C Wrong pattern: turn 1 = Task(lens A) \u2192 turn 2 (after A's result) = Task(lens B). This is the failure mode.
|
|
146965
|
+
|
|
146966
|
+
You can also include your own \`read\` / \`grep\` / \`webfetch\` calls in the SAME turn as the parallel \`${REVIEWER_AGENT_NAME}\` dispatches.
|
|
146967
|
+
|
|
146968
|
+
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:
|
|
146969
|
+
- 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
|
|
146820
146970
|
- **only one lens** \u2014 never a multi-section "review for X, Y, and Z" prompt
|
|
146821
|
-
- **a Task \`description\` set to the lens name**
|
|
146822
|
-
- the
|
|
146823
|
-
- 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."
|
|
146971
|
+
- **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.
|
|
146972
|
+
- if the lens touches external contracts, instruct the subagent to verify load-bearing claims via web search and quote source URLs.
|
|
146824
146973
|
- ask the subagent to report findings with file paths and NEW line numbers from the full PR diff so you can anchor inline comments.
|
|
146825
146974
|
|
|
146826
146975
|
delegation discipline:
|
|
146827
|
-
- do NOT lens-review the diff yourself in parallel with the subagents
|
|
146828
146976
|
- do NOT summarize the changes for them (biases toward validation frame)
|
|
146829
146977
|
- do NOT hand them a curated reading list (let them discover scope)
|
|
146830
146978
|
- do NOT pre-shape their output with a finding schema
|
|
146831
146979
|
- do NOT mention the other lenses (independence is the point)
|
|
146832
146980
|
|
|
146833
|
-
|
|
146981
|
+
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 \`${t2("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.
|
|
146834
146982
|
|
|
146835
|
-
|
|
146983
|
+
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.
|
|
146836
146984
|
|
|
146837
|
-
|
|
146985
|
+
10. Submit \u2014 every run must end with EXACTLY ONE of \`${t2("create_pull_request_review")}\` (substantive review) or \`${t2("report_progress")}\` (no-review acknowledgement). do NOT call \`create_issue_comment\` for review output.
|
|
146838
146986
|
|
|
146839
146987
|
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.
|
|
146840
146988
|
|
|
@@ -147085,12 +147233,38 @@ var PROVIDER_ERROR_PATTERNS = [
|
|
|
147085
147233
|
// around `limit` rejects keys like `time_limit` or `field_limit`.
|
|
147086
147234
|
{ regex: /["']?\blimit\b["']?\s*:\s*0\b/, label: "zero quota" }
|
|
147087
147235
|
];
|
|
147088
|
-
|
|
147236
|
+
var EXCERPT_MAX_BYTES = 600;
|
|
147237
|
+
var LINES_BEFORE = 1;
|
|
147238
|
+
var LINES_AFTER = 2;
|
|
147239
|
+
function findProviderErrorMatch(text) {
|
|
147089
147240
|
for (const entry of PROVIDER_ERROR_PATTERNS) {
|
|
147090
|
-
|
|
147241
|
+
const m = entry.regex.exec(text);
|
|
147242
|
+
if (!m) continue;
|
|
147243
|
+
return { label: entry.label, excerpt: extractExcerpt(text, m.index) };
|
|
147091
147244
|
}
|
|
147092
147245
|
return null;
|
|
147093
147246
|
}
|
|
147247
|
+
function extractExcerpt(text, matchIndex) {
|
|
147248
|
+
const lineStart = text.lastIndexOf("\n", matchIndex - 1) + 1;
|
|
147249
|
+
const lineEndRaw = text.indexOf("\n", matchIndex);
|
|
147250
|
+
const lineEnd = lineEndRaw === -1 ? text.length : lineEndRaw;
|
|
147251
|
+
let start = lineStart;
|
|
147252
|
+
for (let i = 0; i < LINES_BEFORE && start > 0; i++) {
|
|
147253
|
+
const prev = text.lastIndexOf("\n", start - 2);
|
|
147254
|
+
start = prev < 0 ? 0 : prev + 1;
|
|
147255
|
+
}
|
|
147256
|
+
let end = lineEnd;
|
|
147257
|
+
for (let i = 0; i < LINES_AFTER && end < text.length; i++) {
|
|
147258
|
+
const next2 = text.indexOf("\n", end + 1);
|
|
147259
|
+
end = next2 < 0 ? text.length : next2;
|
|
147260
|
+
}
|
|
147261
|
+
let excerpt = text.slice(start, end);
|
|
147262
|
+
if (excerpt.length > EXCERPT_MAX_BYTES) {
|
|
147263
|
+
excerpt = text.slice(lineStart, lineEnd);
|
|
147264
|
+
if (excerpt.length > EXCERPT_MAX_BYTES) excerpt = excerpt.slice(0, EXCERPT_MAX_BYTES);
|
|
147265
|
+
}
|
|
147266
|
+
return excerpt.trim();
|
|
147267
|
+
}
|
|
147094
147268
|
var ROUTER_KEYLIMIT_EXHAUSTED_PATTERN = /requires more credits.*?fewer max_tokens|requested up to \d+ tokens.*?can only afford/is;
|
|
147095
147269
|
function isRouterKeylimitExhaustedError(text) {
|
|
147096
147270
|
return ROUTER_KEYLIMIT_EXHAUSTED_PATTERN.test(text);
|
|
@@ -147214,45 +147388,12 @@ var ThinkingTimer = class {
|
|
|
147214
147388
|
import { readFile } from "node:fs/promises";
|
|
147215
147389
|
function getUnsubmittedReview(toolState) {
|
|
147216
147390
|
const mode = toolState.selectedMode;
|
|
147217
|
-
if (mode !== "Review" && mode !== "IncrementalReview") return null;
|
|
147218
|
-
if (toolState.review || toolState.finalSummaryWritten) return null;
|
|
147219
147391
|
if (!toolState.hadProgressComment) return null;
|
|
147220
|
-
return
|
|
147221
|
-
|
|
147222
|
-
|
|
147223
|
-
function truncateHookOutput(raw2) {
|
|
147224
|
-
if (raw2.length <= MAX_HOOK_OUTPUT_CHARS) return raw2;
|
|
147225
|
-
return `...(truncated, showing last ${MAX_HOOK_OUTPUT_CHARS} chars)
|
|
147226
|
-
${raw2.slice(-MAX_HOOK_OUTPUT_CHARS)}`;
|
|
147227
|
-
}
|
|
147228
|
-
async function executeStopHook(script) {
|
|
147229
|
-
log.info("\xBB executing stop hook...");
|
|
147230
|
-
try {
|
|
147231
|
-
const result = await spawn({
|
|
147232
|
-
cmd: "bash",
|
|
147233
|
-
args: ["-c", script],
|
|
147234
|
-
env: process.env,
|
|
147235
|
-
timeout: LIFECYCLE_HOOK_TIMEOUT_MS,
|
|
147236
|
-
activityTimeout: 0,
|
|
147237
|
-
onStdout: (chunk) => process.stdout.write(chunk),
|
|
147238
|
-
onStderr: (chunk) => process.stderr.write(chunk)
|
|
147239
|
-
});
|
|
147240
|
-
if (result.exitCode === 0) {
|
|
147241
|
-
log.info("\xBB stop hook passed");
|
|
147242
|
-
return null;
|
|
147243
|
-
}
|
|
147244
|
-
const combined = [result.stderr.trim(), result.stdout.trim()].filter(Boolean).join("\n");
|
|
147245
|
-
const output = truncateHookOutput(combined);
|
|
147246
|
-
log.info(`\xBB stop hook failed with exit code ${result.exitCode}`);
|
|
147247
|
-
return { exitCode: result.exitCode, output };
|
|
147248
|
-
} catch (err) {
|
|
147249
|
-
const isTimeout = err instanceof SpawnTimeoutError && (err.code === SPAWN_TIMEOUT_CODE || err.code === SPAWN_ACTIVITY_TIMEOUT_CODE);
|
|
147250
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
147251
|
-
log.warning(
|
|
147252
|
-
`stop hook ${isTimeout ? "timed out" : "failed to spawn"}: ${msg} \u2014 skipping retry`
|
|
147253
|
-
);
|
|
147254
|
-
return null;
|
|
147392
|
+
if (mode === "Review") return toolState.review ? null : "Review";
|
|
147393
|
+
if (mode === "IncrementalReview") {
|
|
147394
|
+
return toolState.review || toolState.finalSummaryWritten ? null : "IncrementalReview";
|
|
147255
147395
|
}
|
|
147396
|
+
return null;
|
|
147256
147397
|
}
|
|
147257
147398
|
function buildStopHookPrompt(failure) {
|
|
147258
147399
|
return [
|
|
@@ -147302,10 +147443,6 @@ function buildUnsubmittedReviewPrompt(mode) {
|
|
|
147302
147443
|
}
|
|
147303
147444
|
async function collectPostRunIssues(ctx, options = {}) {
|
|
147304
147445
|
const issues = {};
|
|
147305
|
-
if (ctx.stopScript) {
|
|
147306
|
-
const failure = await executeStopHook(ctx.stopScript);
|
|
147307
|
-
if (failure) issues.stopHook = failure;
|
|
147308
|
-
}
|
|
147309
147446
|
const status = getGitStatus();
|
|
147310
147447
|
const mode = ctx.toolState.selectedMode;
|
|
147311
147448
|
if (status) {
|
|
@@ -147341,11 +147478,25 @@ function buildLearningsReflectionPrompt(filePath) {
|
|
|
147341
147478
|
"",
|
|
147342
147479
|
`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.`,
|
|
147343
147480
|
"",
|
|
147344
|
-
`
|
|
147345
|
-
`-
|
|
147346
|
-
`-
|
|
147347
|
-
`-
|
|
147348
|
-
|
|
147481
|
+
`structure:`,
|
|
147482
|
+
`- 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\`).`,
|
|
147483
|
+
`- **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.`,
|
|
147484
|
+
`- 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.`,
|
|
147485
|
+
"",
|
|
147486
|
+
`bullet hygiene:`,
|
|
147487
|
+
`- one fact per line starting with \`- \`. each bullet is ONE specific durable fact, not a paragraph or essay.`,
|
|
147488
|
+
`- 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.`,
|
|
147489
|
+
`- 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.`,
|
|
147490
|
+
`- 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.`,
|
|
147491
|
+
`- deduplicate against existing entries (in any section) \u2014 if a bullet covers the same fact, update it in place instead of adding a duplicate.`,
|
|
147492
|
+
"",
|
|
147493
|
+
`do NOT add bullets for:`,
|
|
147494
|
+
`- 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.`,
|
|
147495
|
+
`- 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.`,
|
|
147496
|
+
`- 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.`,
|
|
147497
|
+
`- play-by-play of what THIS run did. learnings are for the NEXT run, not a retrospective.`,
|
|
147498
|
+
"",
|
|
147499
|
+
`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.`
|
|
147349
147500
|
].join("\n");
|
|
147350
147501
|
}
|
|
147351
147502
|
async function runPostRunRetryLoop(params) {
|
|
@@ -147549,8 +147700,9 @@ function writeMcpConfig(ctx) {
|
|
|
147549
147700
|
function buildAgentsJson() {
|
|
147550
147701
|
const agents2 = {
|
|
147551
147702
|
[REVIEWER_AGENT_NAME]: {
|
|
147552
|
-
description: "Read-only review subagent for
|
|
147553
|
-
prompt: REVIEWER_SYSTEM_PROMPT
|
|
147703
|
+
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.",
|
|
147704
|
+
prompt: REVIEWER_SYSTEM_PROMPT,
|
|
147705
|
+
model: "claude-sonnet-4-6"
|
|
147554
147706
|
}
|
|
147555
147707
|
};
|
|
147556
147708
|
return JSON.stringify(agents2);
|
|
@@ -147746,6 +147898,7 @@ async function runClaude(params) {
|
|
|
147746
147898
|
}
|
|
147747
147899
|
};
|
|
147748
147900
|
const recentStderr = [];
|
|
147901
|
+
const recentNonJsonStdout = [];
|
|
147749
147902
|
let lastProviderError = null;
|
|
147750
147903
|
const output = new TailBuffer(DEFAULT_MAX_RETAINED_BYTES);
|
|
147751
147904
|
let stdoutBuffer = "";
|
|
@@ -147784,6 +147937,8 @@ async function runClaude(params) {
|
|
|
147784
147937
|
event = JSON.parse(trimmed);
|
|
147785
147938
|
} catch {
|
|
147786
147939
|
log.debug(`\xBB non-JSON stdout line: ${trimmed.substring(0, 200)}`);
|
|
147940
|
+
recentNonJsonStdout.push(trimmed);
|
|
147941
|
+
if (recentNonJsonStdout.length > MAX_STDERR_LINES) recentNonJsonStdout.shift();
|
|
147787
147942
|
continue;
|
|
147788
147943
|
}
|
|
147789
147944
|
eventCount++;
|
|
@@ -147814,10 +147969,10 @@ async function runClaude(params) {
|
|
|
147814
147969
|
if (!trimmed) return;
|
|
147815
147970
|
recentStderr.push(trimmed);
|
|
147816
147971
|
if (recentStderr.length > MAX_STDERR_LINES) recentStderr.shift();
|
|
147817
|
-
const
|
|
147818
|
-
if (
|
|
147819
|
-
lastProviderError =
|
|
147820
|
-
log.info(`\xBB provider error detected (${
|
|
147972
|
+
const match3 = findProviderErrorMatch(trimmed);
|
|
147973
|
+
if (match3) {
|
|
147974
|
+
lastProviderError = match3.label;
|
|
147975
|
+
log.info(`\xBB provider error detected (${match3.label}): ${match3.excerpt}`);
|
|
147821
147976
|
} else {
|
|
147822
147977
|
log.debug(trimmed);
|
|
147823
147978
|
}
|
|
@@ -147849,7 +148004,8 @@ ${stderrContext}`);
|
|
|
147849
148004
|
const stdoutSnapshot = output.toString();
|
|
147850
148005
|
const stderrSnapshot = recentStderr.join("\n");
|
|
147851
148006
|
const truncatedStdout = stdoutSnapshot ? tailLines(stdoutSnapshot, 2048) : "";
|
|
147852
|
-
const
|
|
148007
|
+
const nonJsonStdoutSnapshot = recentNonJsonStdout.join("\n");
|
|
148008
|
+
const errorMessage = lastResultError || stderrSnapshot || nonJsonStdoutSnapshot || truncatedStdout || `unknown error - no output from Claude CLI${errorContext}`;
|
|
147853
148009
|
log.error(
|
|
147854
148010
|
`${params.label} exited with code ${result.exitCode}${errorContext}: ${errorMessage}`
|
|
147855
148011
|
);
|
|
@@ -147950,7 +148106,9 @@ var claude = agent({
|
|
|
147950
148106
|
run: async (ctx) => {
|
|
147951
148107
|
const cliPath = await installClaudeCli();
|
|
147952
148108
|
const specifier = ctx.payload.proxyModel ?? ctx.resolvedModel;
|
|
147953
|
-
const
|
|
148109
|
+
const bedrockModelId = process.env[BEDROCK_MODEL_ID_ENV]?.trim();
|
|
148110
|
+
const isBedrockRoute = specifier !== void 0 && bedrockModelId !== void 0 && bedrockModelId === specifier && isBedrockAnthropicId(specifier);
|
|
148111
|
+
const model = !specifier ? void 0 : isBedrockRoute ? specifier : stripProviderPrefix(specifier);
|
|
147954
148112
|
const homeEnv = {
|
|
147955
148113
|
HOME: ctx.tmpdir,
|
|
147956
148114
|
XDG_CONFIG_HOME: join10(ctx.tmpdir, ".config")
|
|
@@ -147989,6 +148147,9 @@ var claude = agent({
|
|
|
147989
148147
|
...process.env,
|
|
147990
148148
|
...homeEnv
|
|
147991
148149
|
};
|
|
148150
|
+
if (isBedrockRoute) {
|
|
148151
|
+
env2.CLAUDE_CODE_USE_BEDROCK = "1";
|
|
148152
|
+
}
|
|
147992
148153
|
const repoDir = process.cwd();
|
|
147993
148154
|
log.info(`\xBB effort: ${effort}`);
|
|
147994
148155
|
log.debug(`\xBB starting Pullfrog (Claude Code): node ${baseArgs.join(" ")}`);
|
|
@@ -148029,6 +148190,68 @@ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "node:f
|
|
|
148029
148190
|
import { join as join11 } from "node:path";
|
|
148030
148191
|
import { performance as performance7 } from "node:perf_hooks";
|
|
148031
148192
|
|
|
148193
|
+
// utils/agentHangReport.ts
|
|
148194
|
+
var MAX_STDERR_BYTES = 3e3;
|
|
148195
|
+
function formatAgentHangBody(input) {
|
|
148196
|
+
if (!input.diagnostic) return null;
|
|
148197
|
+
const verb = input.isHang ? "stalled" : "failed";
|
|
148198
|
+
const cause = input.diagnostic.lastProviderError ? ` \u2014 likely cause: \`${input.diagnostic.lastProviderError}\`` : "";
|
|
148199
|
+
const headline = `**${input.diagnostic.label} ${verb}**${cause}`;
|
|
148200
|
+
const explanation = formatExplanation({
|
|
148201
|
+
isHang: input.isHang,
|
|
148202
|
+
errorMessage: input.errorMessage
|
|
148203
|
+
});
|
|
148204
|
+
const parts = [headline, "", `${explanation} ${formatEventsPart(input.diagnostic)}`];
|
|
148205
|
+
const tail = renderStderrTail(input.diagnostic.recentStderr);
|
|
148206
|
+
if (tail) {
|
|
148207
|
+
const fence = pickFence(tail);
|
|
148208
|
+
parts.push(
|
|
148209
|
+
"",
|
|
148210
|
+
"<details><summary>Recent agent stderr</summary>",
|
|
148211
|
+
"",
|
|
148212
|
+
fence,
|
|
148213
|
+
tail,
|
|
148214
|
+
fence,
|
|
148215
|
+
"",
|
|
148216
|
+
"</details>"
|
|
148217
|
+
);
|
|
148218
|
+
}
|
|
148219
|
+
return parts.join("\n");
|
|
148220
|
+
}
|
|
148221
|
+
function formatExplanation(input) {
|
|
148222
|
+
if (!input.isHang) return `The agent exited unexpectedly: ${input.errorMessage}`;
|
|
148223
|
+
const idleSec = parseIdleSec(input.errorMessage);
|
|
148224
|
+
if (idleSec === void 0) {
|
|
148225
|
+
return "The agent stopped emitting events and was killed by the activity-timeout watchdog.";
|
|
148226
|
+
}
|
|
148227
|
+
return `The agent stopped emitting events for ${idleSec}s and was killed by the activity-timeout watchdog.`;
|
|
148228
|
+
}
|
|
148229
|
+
function parseIdleSec(message) {
|
|
148230
|
+
const match3 = /no output for (\d+)s/.exec(message);
|
|
148231
|
+
return match3 ? Number(match3[1]) : void 0;
|
|
148232
|
+
}
|
|
148233
|
+
function formatEventsPart(diagnostic) {
|
|
148234
|
+
if (diagnostic.eventCount > 0) {
|
|
148235
|
+
return `${diagnostic.eventCount} events were processed before the failure.`;
|
|
148236
|
+
}
|
|
148237
|
+
if (diagnostic.lastProviderError) return "No events were emitted before the failure.";
|
|
148238
|
+
return "No events were emitted \u2014 check whether the model provider is reachable.";
|
|
148239
|
+
}
|
|
148240
|
+
function renderStderrTail(lines) {
|
|
148241
|
+
if (lines.length === 0) return "";
|
|
148242
|
+
const joined = lines.join("\n");
|
|
148243
|
+
if (joined.length <= MAX_STDERR_BYTES) return joined;
|
|
148244
|
+
return `... (older lines truncated)
|
|
148245
|
+
${joined.slice(-MAX_STDERR_BYTES)}`;
|
|
148246
|
+
}
|
|
148247
|
+
function pickFence(content) {
|
|
148248
|
+
let max = 0;
|
|
148249
|
+
for (const match3 of content.matchAll(/`+/g)) {
|
|
148250
|
+
if (match3[0].length > max) max = match3[0].length;
|
|
148251
|
+
}
|
|
148252
|
+
return "`".repeat(Math.max(3, max + 1));
|
|
148253
|
+
}
|
|
148254
|
+
|
|
148032
148255
|
// agents/opencodePlugin.ts
|
|
148033
148256
|
var PULLFROG_BUS_EVENT_TYPE = "pullfrog_bus_event";
|
|
148034
148257
|
var PULLFROG_OPENCODE_PLUGIN_FILENAME = "pullfrog-events.ts";
|
|
@@ -148110,6 +148333,22 @@ export default async function pullfrogEventsPlugin() {
|
|
|
148110
148333
|
}
|
|
148111
148334
|
`;
|
|
148112
148335
|
|
|
148336
|
+
// agents/subagentModels.ts
|
|
148337
|
+
function deriveSubagentModels(orchestratorSpec) {
|
|
148338
|
+
if (!orchestratorSpec) return { reviewer: void 0 };
|
|
148339
|
+
for (const source of modelAliases) {
|
|
148340
|
+
const matchedDirect = source.resolve === orchestratorSpec;
|
|
148341
|
+
const matchedOR = source.openRouterResolve === orchestratorSpec;
|
|
148342
|
+
if (!matchedDirect && !matchedOR) continue;
|
|
148343
|
+
if (!source.subagentModel) return { reviewer: void 0 };
|
|
148344
|
+
const target = modelAliases.find((a) => a.slug === source.subagentModel);
|
|
148345
|
+
if (!target) return { reviewer: void 0 };
|
|
148346
|
+
const reviewer = matchedOR ? target.openRouterResolve : target.resolve;
|
|
148347
|
+
return { reviewer };
|
|
148348
|
+
}
|
|
148349
|
+
return { reviewer: void 0 };
|
|
148350
|
+
}
|
|
148351
|
+
|
|
148113
148352
|
// agents/opencode.ts
|
|
148114
148353
|
async function installOpencodeCli() {
|
|
148115
148354
|
return await installFromNpmTarball({
|
|
@@ -148119,7 +148358,6 @@ async function installOpencodeCli() {
|
|
|
148119
148358
|
installDependencies: true
|
|
148120
148359
|
});
|
|
148121
148360
|
}
|
|
148122
|
-
var PULLFROG_OPENCODE_OUTPUT_LIMIT = 5e3;
|
|
148123
148361
|
var GEMINI_3_DIRECT_THINKING_LEVEL = "medium";
|
|
148124
148362
|
var GEMINI_3_DIRECT_API_IDS = ["gemini-3.1-pro-preview", "gemini-3-flash-preview"];
|
|
148125
148363
|
function buildSecurityConfig(ctx, model) {
|
|
@@ -148135,7 +148373,12 @@ function buildSecurityConfig(ctx, model) {
|
|
|
148135
148373
|
mcp: {
|
|
148136
148374
|
[pullfrogMcpName]: { type: "remote", url: ctx.mcpServerUrl }
|
|
148137
148375
|
},
|
|
148138
|
-
agent:
|
|
148376
|
+
agent: (() => {
|
|
148377
|
+
const cfg = buildReviewerAgentConfig(model);
|
|
148378
|
+
const reviewerModel = cfg[REVIEWER_AGENT_NAME]?.model ?? "(inherit)";
|
|
148379
|
+
log.info(`\xBB subagent models: reviewfrog=${reviewerModel}`);
|
|
148380
|
+
return cfg;
|
|
148381
|
+
})(),
|
|
148139
148382
|
// opt into opencode's experimental `batch` tool (added in
|
|
148140
148383
|
// anomalyco/opencode PR #2983, opt-in via `experimental.batch_tool`). it
|
|
148141
148384
|
// exposes a single `batch` tool that runs 1-25 independent tool calls
|
|
@@ -148169,12 +148412,14 @@ function buildSecurityConfig(ctx, model) {
|
|
|
148169
148412
|
}
|
|
148170
148413
|
return JSON.stringify(config3);
|
|
148171
148414
|
}
|
|
148172
|
-
function buildReviewerAgentConfig() {
|
|
148415
|
+
function buildReviewerAgentConfig(orchestratorModel) {
|
|
148416
|
+
const overrides = deriveSubagentModels(orchestratorModel);
|
|
148173
148417
|
return {
|
|
148174
148418
|
[REVIEWER_AGENT_NAME]: {
|
|
148175
|
-
description: "Read-only review subagent for
|
|
148419
|
+
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.",
|
|
148176
148420
|
mode: "subagent",
|
|
148177
|
-
prompt: REVIEWER_SYSTEM_PROMPT
|
|
148421
|
+
prompt: REVIEWER_SYSTEM_PROMPT,
|
|
148422
|
+
...overrides.reviewer !== void 0 ? { model: overrides.reviewer } : {}
|
|
148178
148423
|
}
|
|
148179
148424
|
};
|
|
148180
148425
|
}
|
|
@@ -148199,7 +148444,7 @@ function autoSelectModel(cliPath) {
|
|
|
148199
148444
|
const availableSet = new Set(availableModels);
|
|
148200
148445
|
if (availableSet.size > 0) {
|
|
148201
148446
|
log.debug(`\xBB opencode models (${availableSet.size}): ${availableModels.join(", ")}`);
|
|
148202
|
-
const match3 = modelAliases.find((a) => a.preferred && availableSet.has(a.resolve)) ?? modelAliases.find((a) => availableSet.has(a.resolve));
|
|
148447
|
+
const match3 = modelAliases.find((a) => !a.hidden && a.preferred && availableSet.has(a.resolve)) ?? modelAliases.find((a) => !a.hidden && availableSet.has(a.resolve));
|
|
148203
148448
|
if (match3) {
|
|
148204
148449
|
log.info(
|
|
148205
148450
|
`\xBB model: ${match3.resolve} (auto-selected${match3.preferred ? " \u2014 preferred" : ""} curated match)`
|
|
@@ -148399,8 +148644,7 @@ async function runOpenCode(params) {
|
|
|
148399
148644
|
log.debug(withLabel(label, ` output: ${event.part.state.output}`));
|
|
148400
148645
|
}
|
|
148401
148646
|
if (event.part?.state?.status === "error") {
|
|
148402
|
-
|
|
148403
|
-
log.info(withLabel(label, `\xBB tool call failed: ${errorMsg}`));
|
|
148647
|
+
log.info(withLabel(label, `\xBB tool call failed: ${event.part.state.error}`));
|
|
148404
148648
|
}
|
|
148405
148649
|
if (toolName.includes("report_progress") && params.todoTracker) {
|
|
148406
148650
|
log.debug("\xBB report_progress detected, disabling todo tracking");
|
|
@@ -148412,19 +148656,20 @@ async function runOpenCode(params) {
|
|
|
148412
148656
|
},
|
|
148413
148657
|
tool_result: (event) => {
|
|
148414
148658
|
const toolId = event.part?.callID || event.tool_id;
|
|
148415
|
-
const
|
|
148416
|
-
const
|
|
148659
|
+
const state = event.part?.state;
|
|
148660
|
+
const status = state?.status ?? event.status ?? "unknown";
|
|
148661
|
+
const payload = state?.status === "completed" ? state.output : state?.status === "error" ? state.error : event.output;
|
|
148417
148662
|
const label = eventLabel(event);
|
|
148418
148663
|
timerFor(label).markToolResult();
|
|
148419
148664
|
if (taskDispatchByCallID.size > 0 || pendingTaskDispatches.length > 0) {
|
|
148420
148665
|
if (toolId && taskDispatchByCallID.has(toolId)) {
|
|
148421
148666
|
const dispatch = taskDispatchByCallID.get(toolId);
|
|
148422
|
-
if (dispatch) emitSubagentFinished(dispatch, status,
|
|
148667
|
+
if (dispatch) emitSubagentFinished(dispatch, status, payload, "exact");
|
|
148423
148668
|
} else {
|
|
148424
148669
|
const callIDIsKnownNonTask = toolId ? knownNonTaskCallIDs.has(toolId) : false;
|
|
148425
148670
|
if (!callIDIsKnownNonTask && pendingTaskDispatches.length > 0) {
|
|
148426
148671
|
const dispatch = pendingTaskDispatches[0];
|
|
148427
|
-
emitSubagentFinished(dispatch, status,
|
|
148672
|
+
emitSubagentFinished(dispatch, status, payload, "fifo");
|
|
148428
148673
|
}
|
|
148429
148674
|
}
|
|
148430
148675
|
}
|
|
@@ -148440,13 +148685,8 @@ async function runOpenCode(params) {
|
|
|
148440
148685
|
`\xBB ${params.label} tool_result${stepContext}: id=${toolId}, status=${status}, duration=${Math.round(toolDuration)}ms`
|
|
148441
148686
|
)
|
|
148442
148687
|
);
|
|
148443
|
-
if (
|
|
148444
|
-
log.debug(
|
|
148445
|
-
withLabel(
|
|
148446
|
-
label,
|
|
148447
|
-
` output: ${typeof output2 === "string" ? output2 : JSON.stringify(output2)}`
|
|
148448
|
-
)
|
|
148449
|
-
);
|
|
148688
|
+
if (payload) {
|
|
148689
|
+
log.debug(withLabel(label, ` output: ${payload}`));
|
|
148450
148690
|
}
|
|
148451
148691
|
if (toolDuration > 5e3) {
|
|
148452
148692
|
log.info(
|
|
@@ -148459,11 +148699,9 @@ async function runOpenCode(params) {
|
|
|
148459
148699
|
}
|
|
148460
148700
|
}
|
|
148461
148701
|
if (status === "error") {
|
|
148462
|
-
|
|
148463
|
-
|
|
148464
|
-
|
|
148465
|
-
const outputStr = typeof output2 === "string" ? output2 : JSON.stringify(output2);
|
|
148466
|
-
log.debug(withLabel(label, `tool output: ${outputStr}`));
|
|
148702
|
+
log.info(withLabel(label, `\xBB tool call failed: ${payload ?? "(no error message)"}`));
|
|
148703
|
+
} else if (payload) {
|
|
148704
|
+
log.debug(withLabel(label, `tool output: ${payload}`));
|
|
148467
148705
|
}
|
|
148468
148706
|
},
|
|
148469
148707
|
error: (event) => {
|
|
@@ -148540,6 +148778,13 @@ async function runOpenCode(params) {
|
|
|
148540
148778
|
const recentStderr = [];
|
|
148541
148779
|
let lastProviderError = null;
|
|
148542
148780
|
let agentErrorEvent = null;
|
|
148781
|
+
const diagnostic = {
|
|
148782
|
+
label: params.label,
|
|
148783
|
+
recentStderr,
|
|
148784
|
+
lastProviderError: void 0,
|
|
148785
|
+
eventCount: 0
|
|
148786
|
+
};
|
|
148787
|
+
params.toolState.agentDiagnostic = diagnostic;
|
|
148543
148788
|
const output = new TailBuffer(DEFAULT_MAX_RETAINED_BYTES);
|
|
148544
148789
|
let stdoutBuffer = "";
|
|
148545
148790
|
try {
|
|
@@ -148588,6 +148833,7 @@ async function runOpenCode(params) {
|
|
|
148588
148833
|
continue;
|
|
148589
148834
|
}
|
|
148590
148835
|
eventCount++;
|
|
148836
|
+
diagnostic.eventCount = eventCount;
|
|
148591
148837
|
log.debug(JSON.stringify(event, null, 2));
|
|
148592
148838
|
const timeSinceLastActivity = getIdleMs();
|
|
148593
148839
|
if (timeSinceLastActivity > 1e4) {
|
|
@@ -148619,10 +148865,11 @@ async function runOpenCode(params) {
|
|
|
148619
148865
|
if (!trimmed) return;
|
|
148620
148866
|
recentStderr.push(trimmed);
|
|
148621
148867
|
if (recentStderr.length > MAX_STDERR_LINES) recentStderr.shift();
|
|
148622
|
-
const
|
|
148623
|
-
if (
|
|
148624
|
-
lastProviderError =
|
|
148625
|
-
|
|
148868
|
+
const match3 = findProviderErrorMatch(trimmed);
|
|
148869
|
+
if (match3) {
|
|
148870
|
+
lastProviderError = match3.label;
|
|
148871
|
+
diagnostic.lastProviderError = match3.label;
|
|
148872
|
+
log.info(`\xBB provider error detected (${match3.label}): ${match3.excerpt}`);
|
|
148626
148873
|
} else {
|
|
148627
148874
|
log.debug(trimmed);
|
|
148628
148875
|
}
|
|
@@ -148712,10 +148959,11 @@ ${stderrContext}`);
|
|
|
148712
148959
|
`\xBB recent stderr (last ${Math.min(recentStderr.length, 10)} lines):
|
|
148713
148960
|
${stderrContext}`
|
|
148714
148961
|
);
|
|
148962
|
+
const body = formatAgentHangBody({ diagnostic, isHang: isActivityTimeout, errorMessage });
|
|
148715
148963
|
return {
|
|
148716
148964
|
success: false,
|
|
148717
148965
|
output: finalOutput || output.toString(),
|
|
148718
|
-
error: `${errorMessage} [${diagnosis}]`,
|
|
148966
|
+
error: body ?? `${errorMessage} [${diagnosis}]`,
|
|
148719
148967
|
usage: buildUsage()
|
|
148720
148968
|
};
|
|
148721
148969
|
}
|
|
@@ -148725,7 +148973,10 @@ var opencode = agent({
|
|
|
148725
148973
|
install: installOpencodeCli,
|
|
148726
148974
|
run: async (ctx) => {
|
|
148727
148975
|
const cliPath = await installOpencodeCli();
|
|
148728
|
-
const
|
|
148976
|
+
const rawModel = ctx.payload.proxyModel ?? ctx.resolvedModel ?? autoSelectModel(cliPath);
|
|
148977
|
+
const bedrockModelId = process.env[BEDROCK_MODEL_ID_ENV]?.trim();
|
|
148978
|
+
const isBedrockRoute = rawModel !== void 0 && bedrockModelId !== void 0 && bedrockModelId === rawModel;
|
|
148979
|
+
const model = isBedrockRoute ? `amazon-bedrock/${rawModel}` : rawModel;
|
|
148729
148980
|
const homeEnv = {
|
|
148730
148981
|
HOME: ctx.tmpdir,
|
|
148731
148982
|
XDG_CONFIG_HOME: join11(ctx.tmpdir, ".config")
|
|
@@ -148754,7 +149005,6 @@ var opencode = agent({
|
|
|
148754
149005
|
...homeEnv,
|
|
148755
149006
|
OPENCODE_CONFIG_CONTENT: buildSecurityConfig(ctx, model),
|
|
148756
149007
|
OPENCODE_PERMISSION: permissionOverride,
|
|
148757
|
-
OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX: PULLFROG_OPENCODE_OUTPUT_LIMIT.toString(),
|
|
148758
149008
|
GOOGLE_GENERATIVE_AI_API_KEY: process.env.GOOGLE_GENERATIVE_AI_API_KEY || process.env.GEMINI_API_KEY
|
|
148759
149009
|
};
|
|
148760
149010
|
const repoDir = process.cwd();
|
|
@@ -148765,6 +149015,7 @@ var opencode = agent({
|
|
|
148765
149015
|
cliPath,
|
|
148766
149016
|
cwd: repoDir,
|
|
148767
149017
|
env: env2,
|
|
149018
|
+
toolState: ctx.toolState,
|
|
148768
149019
|
todoTracker: ctx.todoTracker,
|
|
148769
149020
|
onActivityTimeout: ctx.onActivityTimeout,
|
|
148770
149021
|
onToolUse: ctx.onToolUse
|
|
@@ -148797,13 +149048,29 @@ function hasEnvVar(name) {
|
|
|
148797
149048
|
function hasClaudeCodeAuth() {
|
|
148798
149049
|
return hasEnvVar("CLAUDE_CODE_OAUTH_TOKEN") || hasEnvVar("ANTHROPIC_API_KEY");
|
|
148799
149050
|
}
|
|
149051
|
+
function hasBedrockAuth() {
|
|
149052
|
+
return hasEnvVar("AWS_BEARER_TOKEN_BEDROCK") || hasEnvVar("AWS_ACCESS_KEY_ID") && hasEnvVar("AWS_SECRET_ACCESS_KEY");
|
|
149053
|
+
}
|
|
149054
|
+
function resolveSlug(slug2) {
|
|
149055
|
+
const alias = resolveDisplayAlias(slug2);
|
|
149056
|
+
if (alias?.routing === "bedrock") {
|
|
149057
|
+
const bedrockId = process.env[BEDROCK_MODEL_ID_ENV]?.trim();
|
|
149058
|
+
if (!bedrockId) {
|
|
149059
|
+
throw new Error(
|
|
149060
|
+
`${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.`
|
|
149061
|
+
);
|
|
149062
|
+
}
|
|
149063
|
+
return bedrockId;
|
|
149064
|
+
}
|
|
149065
|
+
return resolveCliModel(slug2);
|
|
149066
|
+
}
|
|
148800
149067
|
function resolveModel(ctx) {
|
|
148801
149068
|
const envModel = process.env.PULLFROG_MODEL?.trim();
|
|
148802
149069
|
if (envModel) {
|
|
148803
|
-
return
|
|
149070
|
+
return resolveSlug(envModel) ?? envModel;
|
|
148804
149071
|
}
|
|
148805
149072
|
if (ctx.slug) {
|
|
148806
|
-
const resolved =
|
|
149073
|
+
const resolved = resolveSlug(ctx.slug);
|
|
148807
149074
|
if (resolved) {
|
|
148808
149075
|
return resolved;
|
|
148809
149076
|
}
|
|
@@ -148819,6 +149086,9 @@ function resolveAgent(ctx) {
|
|
|
148819
149086
|
}
|
|
148820
149087
|
log.warning(`\xBB unknown PULLFROG_AGENT="${envAgent}" \u2014 falling through to auto-select`);
|
|
148821
149088
|
}
|
|
149089
|
+
if (ctx.model && hasBedrockAuth() && process.env[BEDROCK_MODEL_ID_ENV]?.trim() === ctx.model) {
|
|
149090
|
+
return isBedrockAnthropicId(ctx.model) ? agents.claude : agents.opencode;
|
|
149091
|
+
}
|
|
148822
149092
|
if (ctx.model) {
|
|
148823
149093
|
try {
|
|
148824
149094
|
const provider2 = getModelProvider(ctx.model);
|
|
@@ -148833,31 +149103,56 @@ function resolveAgent(ctx) {
|
|
|
148833
149103
|
|
|
148834
149104
|
// utils/apiKeys.ts
|
|
148835
149105
|
var knownApiKeys = new Set(Object.values(providers).flatMap((p2) => [...p2.envVars]));
|
|
149106
|
+
var MISSING_KEY_MARKER = "no API key found";
|
|
148836
149107
|
function buildMissingApiKeyError(params) {
|
|
148837
|
-
const
|
|
148838
|
-
const settingsUrl = `${
|
|
148839
|
-
|
|
148840
|
-
|
|
148841
|
-
|
|
149108
|
+
const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
|
|
149109
|
+
const settingsUrl = `${getApiUrl()}/console/${params.owner}/${params.name}`;
|
|
149110
|
+
return [
|
|
149111
|
+
`**${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.`,
|
|
149112
|
+
"",
|
|
149113
|
+
`[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)`
|
|
149114
|
+
].join("\n");
|
|
149115
|
+
}
|
|
149116
|
+
function buildBedrockSetupError(params) {
|
|
149117
|
+
const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
|
|
149118
|
+
return `Bedrock model selected but required configuration is missing: ${params.missing.join(", ")}.
|
|
148842
149119
|
|
|
148843
|
-
|
|
149120
|
+
add the missing secret(s) to your GitHub repository at ${githubSecretsUrl}, then reference them in your workflow's \`env:\` block:
|
|
148844
149121
|
|
|
148845
|
-
|
|
148846
|
-
|
|
148847
|
-
|
|
148848
|
-
4. set the value to your API key
|
|
148849
|
-
5. click "Add secret"
|
|
149122
|
+
AWS_BEARER_TOKEN_BEDROCK: \${{ secrets.AWS_BEARER_TOKEN_BEDROCK }}
|
|
149123
|
+
AWS_REGION: \${{ secrets.AWS_REGION }}
|
|
149124
|
+
${BEDROCK_MODEL_ID_ENV}: \${{ secrets.${BEDROCK_MODEL_ID_ENV} }}
|
|
148850
149125
|
|
|
148851
|
-
|
|
149126
|
+
\`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.
|
|
148852
149127
|
|
|
148853
|
-
for full setup instructions, see https://docs.pullfrog.com/
|
|
149128
|
+
for full setup instructions, see https://docs.pullfrog.com/bedrock`;
|
|
148854
149129
|
}
|
|
148855
149130
|
function hasEnvVar2(name) {
|
|
148856
149131
|
const value2 = process.env[name];
|
|
148857
149132
|
return typeof value2 === "string" && value2.length > 0;
|
|
148858
149133
|
}
|
|
149134
|
+
function validateBedrockSetup(params) {
|
|
149135
|
+
const hasAuth = hasEnvVar2("AWS_BEARER_TOKEN_BEDROCK") || hasEnvVar2("AWS_ACCESS_KEY_ID") && hasEnvVar2("AWS_SECRET_ACCESS_KEY");
|
|
149136
|
+
const missing = [];
|
|
149137
|
+
if (!hasAuth)
|
|
149138
|
+
missing.push("AWS_BEARER_TOKEN_BEDROCK (or AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY)");
|
|
149139
|
+
if (!hasEnvVar2("AWS_REGION")) missing.push("AWS_REGION");
|
|
149140
|
+
if (!hasEnvVar2(BEDROCK_MODEL_ID_ENV)) missing.push(BEDROCK_MODEL_ID_ENV);
|
|
149141
|
+
if (missing.length > 0) {
|
|
149142
|
+
throw new Error(buildBedrockSetupError({ owner: params.owner, name: params.name, missing }));
|
|
149143
|
+
}
|
|
149144
|
+
}
|
|
148859
149145
|
function validateAgentApiKey(params) {
|
|
148860
149146
|
if (params.model) {
|
|
149147
|
+
const alias = resolveDisplayAlias(params.model);
|
|
149148
|
+
if (alias?.routing === "bedrock") {
|
|
149149
|
+
validateBedrockSetup({ owner: params.owner, name: params.name });
|
|
149150
|
+
return;
|
|
149151
|
+
}
|
|
149152
|
+
if (!params.model.includes("/")) {
|
|
149153
|
+
validateBedrockSetup({ owner: params.owner, name: params.name });
|
|
149154
|
+
return;
|
|
149155
|
+
}
|
|
148861
149156
|
const requiredVars = getModelEnvVars(params.model);
|
|
148862
149157
|
if (requiredVars.length === 0) return;
|
|
148863
149158
|
if (requiredVars.some((v) => hasEnvVar2(v))) return;
|
|
@@ -148868,6 +149163,22 @@ function validateAgentApiKey(params) {
|
|
|
148868
149163
|
throw new Error(buildMissingApiKeyError({ owner: params.owner, name: params.name }));
|
|
148869
149164
|
}
|
|
148870
149165
|
}
|
|
149166
|
+
function isApiKeyAuthError(text) {
|
|
149167
|
+
if (!text) return false;
|
|
149168
|
+
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);
|
|
149169
|
+
}
|
|
149170
|
+
function formatApiKeyErrorSummary(params) {
|
|
149171
|
+
if (params.raw.includes(MISSING_KEY_MARKER)) {
|
|
149172
|
+
return buildMissingApiKeyError({ owner: params.owner, name: params.name });
|
|
149173
|
+
}
|
|
149174
|
+
const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
|
|
149175
|
+
const settingsUrl = `${getApiUrl()}/console/${params.owner}/${params.name}`;
|
|
149176
|
+
return [
|
|
149177
|
+
`**Your LLM provider API key was rejected (401).** Rotate the key in your provider dashboard, then update the matching GitHub Actions secret.`,
|
|
149178
|
+
"",
|
|
149179
|
+
`[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)`
|
|
149180
|
+
].join("\n");
|
|
149181
|
+
}
|
|
148871
149182
|
|
|
148872
149183
|
// utils/body.ts
|
|
148873
149184
|
var import_turndown = __toESM(require_turndown_cjs(), 1);
|
|
@@ -153392,10 +153703,31 @@ function buildPromptContext(ctx) {
|
|
|
153392
153703
|
userQuoted: user ? user.split("\n").map((line) => `> ${line}`).join("\n") : ""
|
|
153393
153704
|
};
|
|
153394
153705
|
}
|
|
153395
|
-
function
|
|
153396
|
-
|
|
153706
|
+
function renderLearningsToc(headings) {
|
|
153707
|
+
if (headings.length === 0) return "";
|
|
153708
|
+
const rootDepth = Math.min(...headings.map((h) => h.depth));
|
|
153709
|
+
return headings.map((h) => {
|
|
153710
|
+
const indent2 = " ".repeat((h.depth - rootDepth) * 2);
|
|
153711
|
+
return `${indent2}- ${h.title} (L${h.startLine}-L${h.endLine})`;
|
|
153712
|
+
}).join("\n");
|
|
153713
|
+
}
|
|
153714
|
+
function buildLearningsSection(ctx) {
|
|
153715
|
+
if (!ctx.filePath) return "";
|
|
153716
|
+
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).`;
|
|
153717
|
+
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.
|
|
153718
|
+
|
|
153719
|
+
${renderLearningsToc(ctx.headings)}`;
|
|
153720
|
+
return `************* LEARNINGS *************
|
|
153397
153721
|
|
|
153398
|
-
|
|
153722
|
+
${intro}
|
|
153723
|
+
|
|
153724
|
+
${tocBody}`;
|
|
153725
|
+
}
|
|
153726
|
+
function assembleFullPrompt(ctx) {
|
|
153727
|
+
const learningsSection = buildLearningsSection({
|
|
153728
|
+
filePath: ctx.learningsFilePath,
|
|
153729
|
+
headings: ctx.learningsHeadings
|
|
153730
|
+
});
|
|
153399
153731
|
const runtimeSection = `************* RUNTIME *************
|
|
153400
153732
|
|
|
153401
153733
|
${ctx.runtime}`;
|
|
@@ -153423,7 +153755,10 @@ function resolveInstructions(ctx) {
|
|
|
153423
153755
|
tocEntries.push({ label: "EVENT CONTEXT", description: "related PR/issue data" });
|
|
153424
153756
|
tocEntries.push({ label: "SYSTEM", description: "persona, security, tools, workflow rules" });
|
|
153425
153757
|
if (pctx.learningsFilePath)
|
|
153426
|
-
tocEntries.push({
|
|
153758
|
+
tocEntries.push({
|
|
153759
|
+
label: "LEARNINGS",
|
|
153760
|
+
description: "repo-specific knowledge file path + heading TOC"
|
|
153761
|
+
});
|
|
153427
153762
|
tocEntries.push({ label: "RUNTIME", description: "environment metadata" });
|
|
153428
153763
|
const toc = buildToc(tocEntries);
|
|
153429
153764
|
const full = assembleFullPrompt({
|
|
@@ -153433,6 +153768,7 @@ function resolveInstructions(ctx) {
|
|
|
153433
153768
|
eventContext,
|
|
153434
153769
|
system,
|
|
153435
153770
|
learningsFilePath: pctx.learningsFilePath,
|
|
153771
|
+
learningsHeadings: pctx.learningsHeadings,
|
|
153436
153772
|
runtime: pctx.runtime
|
|
153437
153773
|
});
|
|
153438
153774
|
const event = [pctx.eventTitle, pctx.eventMetadata].filter(Boolean).join("\n\n---\n\n");
|
|
@@ -153450,7 +153786,7 @@ function resolveInstructions(ctx) {
|
|
|
153450
153786
|
import { mkdir, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
153451
153787
|
import { dirname as dirname4, join as join14 } from "node:path";
|
|
153452
153788
|
var LEARNINGS_FILE_NAME = "pullfrog-learnings.md";
|
|
153453
|
-
var MAX_LEARNINGS_LENGTH =
|
|
153789
|
+
var MAX_LEARNINGS_LENGTH = 1e5;
|
|
153454
153790
|
function learningsFilePath(tmpdir3) {
|
|
153455
153791
|
return join14(tmpdir3, LEARNINGS_FILE_NAME);
|
|
153456
153792
|
}
|
|
@@ -153460,6 +153796,15 @@ async function seedLearningsFile(params) {
|
|
|
153460
153796
|
await writeFile2(path3, params.current ?? "", "utf8");
|
|
153461
153797
|
return path3;
|
|
153462
153798
|
}
|
|
153799
|
+
var TRUNCATION_LINE_BOUNDARY_TOLERANCE = 4096;
|
|
153800
|
+
function truncateAtLineBoundary(body, cap) {
|
|
153801
|
+
if (body.length <= cap) return body;
|
|
153802
|
+
const head = body.slice(0, cap);
|
|
153803
|
+
const lastNewline = head.lastIndexOf("\n");
|
|
153804
|
+
if (lastNewline <= 0) return head;
|
|
153805
|
+
if (cap - lastNewline > TRUNCATION_LINE_BOUNDARY_TOLERANCE) return head;
|
|
153806
|
+
return head.slice(0, lastNewline);
|
|
153807
|
+
}
|
|
153463
153808
|
async function readLearningsFile(path3) {
|
|
153464
153809
|
let raw2;
|
|
153465
153810
|
try {
|
|
@@ -153467,9 +153812,7 @@ async function readLearningsFile(path3) {
|
|
|
153467
153812
|
} catch {
|
|
153468
153813
|
return null;
|
|
153469
153814
|
}
|
|
153470
|
-
|
|
153471
|
-
if (trimmed.length > MAX_LEARNINGS_LENGTH) return trimmed.slice(0, MAX_LEARNINGS_LENGTH);
|
|
153472
|
-
return trimmed;
|
|
153815
|
+
return truncateAtLineBoundary(raw2.trim(), MAX_LEARNINGS_LENGTH);
|
|
153473
153816
|
}
|
|
153474
153817
|
|
|
153475
153818
|
// utils/normalizeEnv.ts
|
|
@@ -153816,6 +154159,7 @@ var defaultSettings = {
|
|
|
153816
154159
|
prApproveEnabled: false,
|
|
153817
154160
|
modeInstructions: {},
|
|
153818
154161
|
learnings: null,
|
|
154162
|
+
learningsHeadings: [],
|
|
153819
154163
|
envAllowlist: null
|
|
153820
154164
|
};
|
|
153821
154165
|
var defaultRunContext = {
|
|
@@ -153856,7 +154200,8 @@ async function fetchRunContext(params) {
|
|
|
153856
154200
|
setupScript: data.settings?.setupScript ?? null,
|
|
153857
154201
|
postCheckoutScript: data.settings?.postCheckoutScript ?? null,
|
|
153858
154202
|
prepushScript: data.settings?.prepushScript ?? null,
|
|
153859
|
-
stopScript: data.settings?.stopScript ?? null
|
|
154203
|
+
stopScript: data.settings?.stopScript ?? null,
|
|
154204
|
+
learningsHeadings: data.settings?.learningsHeadings ?? []
|
|
153860
154205
|
},
|
|
153861
154206
|
apiToken: data.apiToken,
|
|
153862
154207
|
oss: data.oss ?? false,
|
|
@@ -154622,10 +154967,7 @@ async function main() {
|
|
|
154622
154967
|
current: runContext.repoSettings.learnings
|
|
154623
154968
|
});
|
|
154624
154969
|
toolState.learningsFilePath = learningsPath;
|
|
154625
|
-
|
|
154626
|
-
toolState.learningsSeed = await readFile4(learningsPath, "utf8");
|
|
154627
|
-
} catch {
|
|
154628
|
-
}
|
|
154970
|
+
toolState.learningsSeed = (runContext.repoSettings.learnings ?? "").trim();
|
|
154629
154971
|
log.info(
|
|
154630
154972
|
`\xBB learnings seeded at ${learningsPath} (existing=${runContext.repoSettings.learnings ? "yes" : "no"})`
|
|
154631
154973
|
);
|
|
@@ -154665,7 +155007,8 @@ async function main() {
|
|
|
154665
155007
|
modes: modes2,
|
|
154666
155008
|
agentId,
|
|
154667
155009
|
outputSchema,
|
|
154668
|
-
learningsFilePath: toolState.learningsFilePath ?? null
|
|
155010
|
+
learningsFilePath: toolState.learningsFilePath ?? null,
|
|
155011
|
+
learningsHeadings: runContext.repoSettings.learningsHeadings
|
|
154669
155012
|
});
|
|
154670
155013
|
const logParts = [
|
|
154671
155014
|
instructions.eventInstructions ? `EVENT-LEVEL INSTRUCTIONS:
|
|
@@ -154802,10 +155145,13 @@ ${instructions.user}` : null,
|
|
|
154802
155145
|
await persistLearnings(toolContext);
|
|
154803
155146
|
}
|
|
154804
155147
|
if (!result.success && toolContext && toolState.progressComment) {
|
|
154805
|
-
|
|
154806
|
-
|
|
154807
|
-
|
|
154808
|
-
|
|
155148
|
+
const rawError = result.error || "agent run failed";
|
|
155149
|
+
const errorBody = isApiKeyAuthError(rawError) ? formatApiKeyErrorSummary({
|
|
155150
|
+
owner: runContext.repo.owner,
|
|
155151
|
+
name: runContext.repo.name,
|
|
155152
|
+
raw: rawError
|
|
155153
|
+
}) : rawError;
|
|
155154
|
+
await reportErrorToComment({ toolState, error: errorBody }).catch((error49) => {
|
|
154809
155155
|
log.debug(`failure error report failed: ${error49}`);
|
|
154810
155156
|
});
|
|
154811
155157
|
}
|
|
@@ -154841,19 +155187,29 @@ ${instructions.user}` : null,
|
|
|
154841
155187
|
killTrackedChildren();
|
|
154842
155188
|
log.error(errorMessage);
|
|
154843
155189
|
const billingError = isRouterKeylimitExhaustedError(errorMessage) ? new BillingError(errorMessage, { code: "router_keylimit_exhausted" }) : null;
|
|
155190
|
+
const isHang = errorMessage.startsWith("activity timeout") || errorMessage.startsWith("agent still pending");
|
|
155191
|
+
const hangBody = isHang ? formatAgentHangBody({ diagnostic: toolState.agentDiagnostic, isHang: true, errorMessage }) : null;
|
|
155192
|
+
const apiKeySource = hangBody ?? errorMessage;
|
|
155193
|
+
const apiKeyErrorSummary = !billingError && isApiKeyAuthError(apiKeySource) ? formatApiKeyErrorSummary({
|
|
155194
|
+
owner: runContext.repo.owner,
|
|
155195
|
+
name: runContext.repo.name,
|
|
155196
|
+
raw: apiKeySource
|
|
155197
|
+
}) : null;
|
|
154844
155198
|
try {
|
|
154845
|
-
const errorSummary = billingError ? formatBillingErrorSummary(billingError, runContext.repo.owner) : `### \u274C Pullfrog failed
|
|
155199
|
+
const errorSummary = billingError ? formatBillingErrorSummary(billingError, runContext.repo.owner) : apiKeyErrorSummary ?? (hangBody ? `### \u274C Pullfrog failed
|
|
155200
|
+
|
|
155201
|
+
${hangBody}` : `### \u274C Pullfrog failed
|
|
154846
155202
|
|
|
154847
155203
|
\`\`\`
|
|
154848
155204
|
${errorMessage}
|
|
154849
|
-
|
|
155205
|
+
\`\`\``);
|
|
154850
155206
|
const usageSummary = formatUsageSummary(toolState.usageEntries);
|
|
154851
155207
|
const parts = [errorSummary, toolState.lastProgressBody, usageSummary].filter(Boolean);
|
|
154852
155208
|
await writeSummary(parts.join("\n\n"));
|
|
154853
155209
|
} catch {
|
|
154854
155210
|
}
|
|
154855
155211
|
try {
|
|
154856
|
-
const commentBody = billingError ? formatBillingErrorSummary(billingError, runContext.repo.owner) : errorMessage;
|
|
155212
|
+
const commentBody = billingError ? formatBillingErrorSummary(billingError, runContext.repo.owner) : apiKeyErrorSummary ?? hangBody ?? errorMessage;
|
|
154857
155213
|
await reportErrorToComment({ toolState, error: commentBody });
|
|
154858
155214
|
} catch {
|
|
154859
155215
|
}
|
|
@@ -155996,8 +156352,10 @@ function link(text, url4) {
|
|
|
155996
156352
|
return `\x1B]8;;${url4}\x07${text}\x1B]8;;\x07`;
|
|
155997
156353
|
}
|
|
155998
156354
|
function buildProviders() {
|
|
155999
|
-
return Object.entries(providers).filter(([key]) => key !== "opencode" && key !== "openrouter").map(([key, config3]) => {
|
|
156000
|
-
const aliases = modelAliases.filter(
|
|
156355
|
+
return Object.entries(providers).filter(([key]) => key !== "opencode" && key !== "openrouter" && key !== "bedrock").map(([key, config3]) => {
|
|
156356
|
+
const aliases = modelAliases.filter(
|
|
156357
|
+
(a) => a.provider === key && !a.fallback && !a.routing && !a.hidden
|
|
156358
|
+
);
|
|
156001
156359
|
const recommended = aliases.find((a) => a.preferred);
|
|
156002
156360
|
const sorted = [...aliases].sort((a, b) => {
|
|
156003
156361
|
if (a.preferred && !b.preferred) return -1;
|
|
@@ -156710,7 +157068,7 @@ async function run2() {
|
|
|
156710
157068
|
}
|
|
156711
157069
|
|
|
156712
157070
|
// cli.ts
|
|
156713
|
-
var VERSION10 = "0.1.
|
|
157071
|
+
var VERSION10 = "0.1.8";
|
|
156714
157072
|
var bin = basename2(process.argv[1] || "");
|
|
156715
157073
|
var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
|
|
156716
157074
|
var rawArgs = process.argv.slice(2);
|