pullfrog 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/postRun.d.ts +21 -0
- package/dist/agents/sessionLabeler.d.ts +38 -18
- package/dist/agents/subagentModels.d.ts +19 -0
- package/dist/cli.mjs +678 -278
- package/dist/index.js +662 -264
- package/dist/internal.js +151 -59
- package/dist/models.d.ts +63 -3
- package/dist/utils/agent.d.ts +5 -2
- package/dist/utils/apiKeys.d.ts +18 -0
- package/dist/utils/instructions.d.ts +19 -0
- package/dist/utils/learnings.d.ts +20 -9
- package/dist/utils/normalizeEnv.d.ts +21 -1
- package/dist/utils/runContext.d.ts +16 -0
- package/dist/utils/subprocess.d.ts +40 -0
- package/dist/utils/timer.d.ts +11 -0
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -19935,10 +19935,10 @@ var require_core = __commonJS({
|
|
|
19935
19935
|
(0, command_1.issueCommand)("set-env", { name }, convertedVal);
|
|
19936
19936
|
}
|
|
19937
19937
|
exports.exportVariable = exportVariable;
|
|
19938
|
-
function
|
|
19938
|
+
function setSecret5(secret) {
|
|
19939
19939
|
(0, command_1.issueCommand)("add-mask", {}, secret);
|
|
19940
19940
|
}
|
|
19941
|
-
exports.setSecret =
|
|
19941
|
+
exports.setSecret = setSecret5;
|
|
19942
19942
|
function addPath(inputPath) {
|
|
19943
19943
|
const filePath = process.env["GITHUB_PATH"] || "";
|
|
19944
19944
|
if (filePath) {
|
|
@@ -47954,7 +47954,7 @@ var require_core3 = __commonJS({
|
|
|
47954
47954
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47955
47955
|
var id_1 = require_id();
|
|
47956
47956
|
var ref_1 = require_ref();
|
|
47957
|
-
var
|
|
47957
|
+
var core9 = [
|
|
47958
47958
|
"$schema",
|
|
47959
47959
|
"$id",
|
|
47960
47960
|
"$defs",
|
|
@@ -47964,7 +47964,7 @@ var require_core3 = __commonJS({
|
|
|
47964
47964
|
id_1.default,
|
|
47965
47965
|
ref_1.default
|
|
47966
47966
|
];
|
|
47967
|
-
exports.default =
|
|
47967
|
+
exports.default = core9;
|
|
47968
47968
|
}
|
|
47969
47969
|
});
|
|
47970
47970
|
|
|
@@ -99202,12 +99202,12 @@ var import_picocolors2 = __toESM(require_picocolors(), 1);
|
|
|
99202
99202
|
import { basename as basename2 } from "node:path";
|
|
99203
99203
|
|
|
99204
99204
|
// commands/gha.ts
|
|
99205
|
-
var
|
|
99205
|
+
var core8 = __toESM(require_core(), 1);
|
|
99206
99206
|
var import_arg = __toESM(require_arg(), 1);
|
|
99207
99207
|
import { dirname as dirname6 } from "node:path";
|
|
99208
99208
|
|
|
99209
99209
|
// main.ts
|
|
99210
|
-
var
|
|
99210
|
+
var core7 = __toESM(require_core(), 1);
|
|
99211
99211
|
import { existsSync as existsSync7, readdirSync } from "node:fs";
|
|
99212
99212
|
import { readFile as readFile4 } from "node:fs/promises";
|
|
99213
99213
|
import { join as join17 } from "node:path";
|
|
@@ -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);
|
|
@@ -110025,12 +110084,41 @@ function installSignalHandler() {
|
|
|
110025
110084
|
killTrackedChildren();
|
|
110026
110085
|
});
|
|
110027
110086
|
}
|
|
110087
|
+
var DEFAULT_MAX_RETAINED_BYTES = 8 * 1024 * 1024;
|
|
110088
|
+
var TailBuffer = class {
|
|
110089
|
+
// explicit field declarations rather than constructor parameter properties:
|
|
110090
|
+
// node's strip-only TS loader (used by action/test/run.ts in CI) rejects
|
|
110091
|
+
// `constructor(private readonly cap: number)` with ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX.
|
|
110092
|
+
cap;
|
|
110093
|
+
buffer = "";
|
|
110094
|
+
truncatedBytes = 0;
|
|
110095
|
+
constructor(cap) {
|
|
110096
|
+
this.cap = cap;
|
|
110097
|
+
}
|
|
110098
|
+
append(chunk) {
|
|
110099
|
+
if (this.cap <= 0) return;
|
|
110100
|
+
this.buffer += chunk;
|
|
110101
|
+
if (this.buffer.length > this.cap) {
|
|
110102
|
+
const drop = this.buffer.length - this.cap;
|
|
110103
|
+
this.truncatedBytes += drop;
|
|
110104
|
+
this.buffer = this.buffer.slice(drop);
|
|
110105
|
+
}
|
|
110106
|
+
}
|
|
110107
|
+
toString() {
|
|
110108
|
+
if (this.truncatedBytes === 0) return this.buffer;
|
|
110109
|
+
const mib = (this.truncatedBytes / 1024 / 1024).toFixed(1);
|
|
110110
|
+
return `... [${mib} MiB truncated by retain:tail cap] ...
|
|
110111
|
+
${this.buffer}`;
|
|
110112
|
+
}
|
|
110113
|
+
};
|
|
110028
110114
|
async function spawn(options) {
|
|
110029
110115
|
const activityTimeoutMs = options.activityTimeout ?? DEFAULT_ACTIVITY_TIMEOUT_MS;
|
|
110030
110116
|
installSignalHandler();
|
|
110031
110117
|
const startTime = performance3.now();
|
|
110032
|
-
|
|
110033
|
-
|
|
110118
|
+
const retain = options.retain ?? "tail";
|
|
110119
|
+
const cap = options.maxRetainedBytes ?? DEFAULT_MAX_RETAINED_BYTES;
|
|
110120
|
+
const stdoutBuffer = retain === "none" ? null : new TailBuffer(cap);
|
|
110121
|
+
const stderrBuffer = retain === "none" ? null : new TailBuffer(cap);
|
|
110034
110122
|
const killGroup = options.killGroup ?? false;
|
|
110035
110123
|
return new Promise((resolve3, reject) => {
|
|
110036
110124
|
const child = nodeSpawn(options.cmd, options.args, {
|
|
@@ -110104,17 +110192,29 @@ async function spawn(options) {
|
|
|
110104
110192
|
}
|
|
110105
110193
|
if (child.stdout) {
|
|
110106
110194
|
child.stdout.on("data", (data) => {
|
|
110107
|
-
|
|
110108
|
-
|
|
110109
|
-
|
|
110110
|
-
|
|
110195
|
+
try {
|
|
110196
|
+
updateActivity();
|
|
110197
|
+
const chunk = data.toString();
|
|
110198
|
+
stdoutBuffer?.append(chunk);
|
|
110199
|
+
options.onStdout?.(chunk);
|
|
110200
|
+
} catch (err) {
|
|
110201
|
+
log.debug(
|
|
110202
|
+
`spawn stdout handler threw: ${err instanceof Error ? err.message : String(err)}`
|
|
110203
|
+
);
|
|
110204
|
+
}
|
|
110111
110205
|
});
|
|
110112
110206
|
}
|
|
110113
110207
|
if (child.stderr) {
|
|
110114
110208
|
child.stderr.on("data", (data) => {
|
|
110115
|
-
|
|
110116
|
-
|
|
110117
|
-
|
|
110209
|
+
try {
|
|
110210
|
+
const chunk = data.toString();
|
|
110211
|
+
stderrBuffer?.append(chunk);
|
|
110212
|
+
options.onStderr?.(chunk);
|
|
110213
|
+
} catch (err) {
|
|
110214
|
+
log.debug(
|
|
110215
|
+
`spawn stderr handler threw: ${err instanceof Error ? err.message : String(err)}`
|
|
110216
|
+
);
|
|
110217
|
+
}
|
|
110118
110218
|
});
|
|
110119
110219
|
}
|
|
110120
110220
|
child.on("close", (exitCode, signal) => {
|
|
@@ -110141,7 +110241,7 @@ async function spawn(options) {
|
|
|
110141
110241
|
return;
|
|
110142
110242
|
}
|
|
110143
110243
|
let resolvedExitCode = exitCode ?? 0;
|
|
110144
|
-
let resolvedStderr = stderrBuffer;
|
|
110244
|
+
let resolvedStderr = stderrBuffer?.toString() ?? "";
|
|
110145
110245
|
if (exitCode === null && signal) {
|
|
110146
110246
|
const killMsg = `[spawn] ${options.cmd}: killed by signal ${signal}`;
|
|
110147
110247
|
resolvedStderr = resolvedStderr ? `${resolvedStderr}
|
|
@@ -110149,7 +110249,7 @@ ${killMsg}` : killMsg;
|
|
|
110149
110249
|
resolvedExitCode = 1;
|
|
110150
110250
|
}
|
|
110151
110251
|
resolve3({
|
|
110152
|
-
stdout: stdoutBuffer,
|
|
110252
|
+
stdout: stdoutBuffer?.toString() ?? "",
|
|
110153
110253
|
stderr: resolvedStderr,
|
|
110154
110254
|
exitCode: resolvedExitCode,
|
|
110155
110255
|
durationMs
|
|
@@ -110163,11 +110263,12 @@ ${killMsg}` : killMsg;
|
|
|
110163
110263
|
if (activityCheckIntervalId) clearInterval(activityCheckIntervalId);
|
|
110164
110264
|
const errMsg = `[spawn] ${options.cmd}: ${error49.message}`;
|
|
110165
110265
|
console.error(errMsg);
|
|
110166
|
-
|
|
110266
|
+
const existingStderr = stderrBuffer?.toString() ?? "";
|
|
110267
|
+
const finalStderr = existingStderr ? `${existingStderr}
|
|
110167
110268
|
${errMsg}` : errMsg;
|
|
110168
110269
|
resolve3({
|
|
110169
|
-
stdout: stdoutBuffer,
|
|
110170
|
-
stderr:
|
|
110270
|
+
stdout: stdoutBuffer?.toString() ?? "",
|
|
110271
|
+
stderr: finalStderr,
|
|
110171
110272
|
exitCode: 1,
|
|
110172
110273
|
durationMs
|
|
110173
110274
|
});
|
|
@@ -138076,7 +138177,7 @@ var require_core4 = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
138076
138177
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
138077
138178
|
const id_1 = require_id2();
|
|
138078
138179
|
const ref_1 = require_ref2();
|
|
138079
|
-
const
|
|
138180
|
+
const core9 = [
|
|
138080
138181
|
"$schema",
|
|
138081
138182
|
"$id",
|
|
138082
138183
|
"$defs",
|
|
@@ -138086,7 +138187,7 @@ var require_core4 = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
138086
138187
|
id_1.default,
|
|
138087
138188
|
ref_1.default
|
|
138088
138189
|
];
|
|
138089
|
-
exports.default =
|
|
138190
|
+
exports.default = core9;
|
|
138090
138191
|
}));
|
|
138091
138192
|
var require_limitNumber2 = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
138092
138193
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -142596,7 +142697,7 @@ var import_semver = __toESM(require_semver2(), 1);
|
|
|
142596
142697
|
// package.json
|
|
142597
142698
|
var package_default = {
|
|
142598
142699
|
name: "pullfrog",
|
|
142599
|
-
version: "0.1.
|
|
142700
|
+
version: "0.1.7",
|
|
142600
142701
|
type: "module",
|
|
142601
142702
|
bin: {
|
|
142602
142703
|
pullfrog: "dist/cli.mjs",
|
|
@@ -143452,7 +143553,7 @@ function PushBranchTool(ctx) {
|
|
|
143452
143553
|
const pushPermission = ctx.payload.push;
|
|
143453
143554
|
return tool({
|
|
143454
143555
|
name: "push_branch",
|
|
143455
|
-
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.",
|
|
143556
|
+
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.",
|
|
143456
143557
|
parameters: PushBranch,
|
|
143457
143558
|
execute: execute(async ({ branchName, force }) => {
|
|
143458
143559
|
if (pushPermission === "disabled") {
|
|
@@ -143591,7 +143692,7 @@ var Git = type({
|
|
|
143591
143692
|
function GitTool(ctx) {
|
|
143592
143693
|
return tool({
|
|
143593
143694
|
name: "git",
|
|
143594
|
-
description:
|
|
143695
|
+
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\'.',
|
|
143595
143696
|
parameters: Git,
|
|
143596
143697
|
execute: execute(async (params) => {
|
|
143597
143698
|
const command = params.command;
|
|
@@ -143641,7 +143742,7 @@ var DEEPEN_RETRY_DEPTH = 1e3;
|
|
|
143641
143742
|
function GitFetchTool(ctx) {
|
|
143642
143743
|
return tool({
|
|
143643
143744
|
name: "git_fetch",
|
|
143644
|
-
description:
|
|
143745
|
+
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 })`.',
|
|
143645
143746
|
parameters: GitFetch,
|
|
143646
143747
|
execute: execute(async (params) => {
|
|
143647
143748
|
rejectIfLeadingDash(params.ref, "ref");
|
|
@@ -143875,13 +143976,15 @@ var CreatePullRequestReview = type({
|
|
|
143875
143976
|
approved: type.boolean.describe(
|
|
143876
143977
|
"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."
|
|
143877
143978
|
).optional(),
|
|
143878
|
-
commit_id: type.string.describe(
|
|
143979
|
+
commit_id: type.string.describe(
|
|
143980
|
+
"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."
|
|
143981
|
+
).optional(),
|
|
143879
143982
|
comments: type({
|
|
143880
143983
|
path: type.string.describe(
|
|
143881
143984
|
"The file path to comment on (relative to repo root). Must be a file that appears in the PR diff."
|
|
143882
143985
|
),
|
|
143883
143986
|
line: type.number.describe(
|
|
143884
|
-
"Line number to comment on. For multi-line ranges, this is the end line. Use NEW column from diff format."
|
|
143987
|
+
"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)."
|
|
143885
143988
|
),
|
|
143886
143989
|
side: type.enumerated("LEFT", "RIGHT").describe(
|
|
143887
143990
|
"Side of the diff: LEFT (old code, lines starting with -) or RIGHT (new code, lines starting with + or unchanged). Defaults to RIGHT."
|
|
@@ -143891,7 +143994,7 @@ var CreatePullRequestReview = type({
|
|
|
143891
143994
|
"Full replacement code for the line range [start_line, line]. MUST preserve the exact indentation of the original code."
|
|
143892
143995
|
).optional(),
|
|
143893
143996
|
start_line: type.number.describe(
|
|
143894
|
-
"Start line for multi-line comment ranges. Omit for single-line comments. The range [start_line, line] defines which lines a suggestion replaces."
|
|
143997
|
+
"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."
|
|
143895
143998
|
).optional()
|
|
143896
143999
|
}).array().describe(
|
|
143897
144000
|
"Inline comments on lines within diff hunks. Feedback about code outside the diff goes in 'body' instead."
|
|
@@ -143900,7 +144003,7 @@ var CreatePullRequestReview = type({
|
|
|
143900
144003
|
function CreatePullRequestReviewTool(ctx) {
|
|
143901
144004
|
return tool({
|
|
143902
144005
|
name: "create_pull_request_review",
|
|
143903
|
-
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.`,
|
|
144006
|
+
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.`,
|
|
143904
144007
|
parameters: CreatePullRequestReview,
|
|
143905
144008
|
execute: execute(async ({ pull_number, body, approved, commit_id, comments = [] }) => {
|
|
143906
144009
|
if (body) body = fixDoubleEscapedString(body);
|
|
@@ -144129,7 +144232,7 @@ function runDiffCoveragePreflight(params) {
|
|
|
144129
144232
|
);
|
|
144130
144233
|
const unreadText = unread.map((entry) => `- ${entry.path} (${entry.unreadLines} lines, ${entry.ranges})`).join("\n");
|
|
144131
144234
|
throw new Error(
|
|
144132
|
-
`diff coverage pre-flight: some TOC regions were not read before review submission. this is a one-time nudge \u2014
|
|
144235
|
+
`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.
|
|
144133
144236
|
|
|
144134
144237
|
unread TOC regions:
|
|
144135
144238
|
${unreadText}
|
|
@@ -144576,7 +144679,7 @@ async function checkoutPrBranch(pr, params) {
|
|
|
144576
144679
|
function CheckoutPrTool(ctx) {
|
|
144577
144680
|
return tool({
|
|
144578
144681
|
name: "checkout_pr",
|
|
144579
|
-
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.",
|
|
144682
|
+
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.",
|
|
144580
144683
|
parameters: CheckoutPr,
|
|
144581
144684
|
execute: execute(async ({ pull_number }) => {
|
|
144582
144685
|
const prResponse = await ctx.octokit.rest.pulls.get({
|
|
@@ -144887,7 +144990,7 @@ var CommitInfo = type({
|
|
|
144887
144990
|
function CommitInfoTool(ctx) {
|
|
144888
144991
|
return tool({
|
|
144889
144992
|
name: "get_commit_info",
|
|
144890
|
-
description:
|
|
144993
|
+
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" })`.',
|
|
144891
144994
|
parameters: CommitInfo,
|
|
144892
144995
|
execute: execute(async ({ sha }) => {
|
|
144893
144996
|
const response = await ctx.octokit.rest.repos.getCommit({
|
|
@@ -144978,7 +145081,7 @@ var GetIssueComments = type({
|
|
|
144978
145081
|
function GetIssueCommentsTool(ctx) {
|
|
144979
145082
|
return tool({
|
|
144980
145083
|
name: "get_issue_comments",
|
|
144981
|
-
description: "Get all comments for a GitHub issue. Returns all comments including the issue body and all subsequent discussion comments.",
|
|
145084
|
+
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 })`.",
|
|
144982
145085
|
parameters: GetIssueComments,
|
|
144983
145086
|
execute: execute(async ({ issue_number }) => {
|
|
144984
145087
|
ctx.toolState.issueNumber = issue_number;
|
|
@@ -145079,7 +145182,7 @@ var IssueInfo = type({
|
|
|
145079
145182
|
function IssueInfoTool(ctx) {
|
|
145080
145183
|
return tool({
|
|
145081
145184
|
name: "get_issue",
|
|
145082
|
-
description: "Retrieve GitHub issue information by issue number",
|
|
145185
|
+
description: "Retrieve GitHub issue information by issue number. Example: `get_issue({ issue_number: 1234 })`.",
|
|
145083
145186
|
parameters: IssueInfo,
|
|
145084
145187
|
execute: execute(async ({ issue_number }) => {
|
|
145085
145188
|
const issue3 = await ctx.octokit.rest.issues.get({
|
|
@@ -145321,7 +145424,7 @@ var PullRequestInfo = type({
|
|
|
145321
145424
|
function PullRequestInfoTool(ctx) {
|
|
145322
145425
|
return tool({
|
|
145323
145426
|
name: "get_pull_request",
|
|
145324
|
-
description: "Retrieve PR metadata (title, body, state, branches, author, labels, linked issues). To checkout a PR branch locally, use checkout_pr instead.",
|
|
145427
|
+
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.",
|
|
145325
145428
|
parameters: PullRequestInfo,
|
|
145326
145429
|
execute: execute(async ({ pull_number }) => {
|
|
145327
145430
|
const [restResponse, graphqlResponse] = await Promise.all([
|
|
@@ -145725,7 +145828,7 @@ async function getReviewData(input) {
|
|
|
145725
145828
|
function GetReviewCommentsTool(ctx) {
|
|
145726
145829
|
return tool({
|
|
145727
145830
|
name: "get_review_comments",
|
|
145728
|
-
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.",
|
|
145831
|
+
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.",
|
|
145729
145832
|
parameters: GetReviewComments,
|
|
145730
145833
|
execute: execute(async (params) => {
|
|
145731
145834
|
const approvedBy = ctx.payload.event.trigger === "fix_review" && ctx.payload.event.approved_only ? ctx.payload.triggerer : void 0;
|
|
@@ -145775,7 +145878,7 @@ var ListPullRequestReviews = type({
|
|
|
145775
145878
|
function ListPullRequestReviewsTool(ctx) {
|
|
145776
145879
|
return tool({
|
|
145777
145880
|
name: "list_pull_request_reviews",
|
|
145778
|
-
description: "List all reviews for a pull request. Returns all reviews including approvals, request changes, and comments.",
|
|
145881
|
+
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 })`.",
|
|
145779
145882
|
parameters: ListPullRequestReviews,
|
|
145780
145883
|
execute: execute(async (params) => {
|
|
145781
145884
|
const reviews = await ctx.octokit.paginate(ctx.octokit.rest.pulls.listReviews, {
|
|
@@ -145925,7 +146028,7 @@ function SelectModeTool(ctx) {
|
|
|
145925
146028
|
const overrides = buildModeOverrides(t2);
|
|
145926
146029
|
return tool({
|
|
145927
146030
|
name: "select_mode",
|
|
145928
|
-
description:
|
|
146031
|
+
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 })`.',
|
|
145929
146032
|
parameters: SelectModeParams,
|
|
145930
146033
|
execute: execute(async (params) => {
|
|
145931
146034
|
if (ctx.toolState.selectedMode) {
|
|
@@ -145986,7 +146089,9 @@ import { setTimeout as sleep2 } from "node:timers/promises";
|
|
|
145986
146089
|
var ShellParams = type({
|
|
145987
146090
|
command: "string",
|
|
145988
146091
|
description: "string",
|
|
145989
|
-
"timeout?":
|
|
146092
|
+
"timeout?": type.number.describe(
|
|
146093
|
+
"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."
|
|
146094
|
+
),
|
|
145990
146095
|
"working_directory?": "string",
|
|
145991
146096
|
"background?": "boolean"
|
|
145992
146097
|
});
|
|
@@ -146105,6 +146210,8 @@ function ShellTool(ctx) {
|
|
|
146105
146210
|
name: "shell",
|
|
146106
146211
|
description: `Execute shell commands securely. Environment is filtered to remove API keys and secrets.
|
|
146107
146212
|
|
|
146213
|
+
Example: \`shell({ command: "pnpm test", description: "run the test suite" })\`.
|
|
146214
|
+
|
|
146108
146215
|
Use this tool to:
|
|
146109
146216
|
- Run shell commands (ls, cat, grep, find, etc.)
|
|
146110
146217
|
- Execute build tools (npm, pnpm, cargo, make, etc.)
|
|
@@ -146622,18 +146729,24 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
146622
146729
|
- resolve addressed threads via \`${t2("resolve_review_thread")}\`
|
|
146623
146730
|
- call \`${t2("report_progress")}\` with a brief summary (or the exact push error if push failed)`
|
|
146624
146731
|
},
|
|
146625
|
-
// Review and IncrementalReview use
|
|
146626
|
-
// (
|
|
146627
|
-
//
|
|
146628
|
-
//
|
|
146629
|
-
//
|
|
146630
|
-
//
|
|
146631
|
-
//
|
|
146632
|
-
//
|
|
146633
|
-
//
|
|
146634
|
-
//
|
|
146635
|
-
//
|
|
146636
|
-
//
|
|
146732
|
+
// Review and IncrementalReview use a 0-or-2+ lens pattern. The default is
|
|
146733
|
+
// 0 lenses (orchestrator handles the review solo). Multi-lens (2+
|
|
146734
|
+
// reviewfrog subagents in parallel) only fires for substantive PRs or
|
|
146735
|
+
// high-stakes-subsystem touches — and when it fires, ALL lenses must
|
|
146736
|
+
// dispatch in a single assistant turn or the parallelism win disappears.
|
|
146737
|
+
// We never dispatch exactly one lens: a single lens is just a worse,
|
|
146738
|
+
// slower version of doing the work yourself.
|
|
146739
|
+
//
|
|
146740
|
+
// Build mode self-review is a different problem shape: the orchestrator
|
|
146741
|
+
// wrote the code, so bias-mitigation comes from delegating to one
|
|
146742
|
+
// fresh-eyes subagent that doesn't share the implementation context. A
|
|
146743
|
+
// single subagent there is appropriate; the 0-or-2+ rule applies only to
|
|
146744
|
+
// the Review/IncrementalReview lens fan-out where independence between
|
|
146745
|
+
// perspectives is what's being purchased.
|
|
146746
|
+
//
|
|
146747
|
+
// Deliberate omission vs canonical /anneal: severity categorization in
|
|
146748
|
+
// the final message (the review body has its own CAUTION/IMPORTANT
|
|
146749
|
+
// framing instead of a severity table).
|
|
146637
146750
|
{
|
|
146638
146751
|
name: "Review",
|
|
146639
146752
|
description: "Review code, PRs, or implementations; provide feedback or suggestions; identify issues; or check code quality, style, and correctness",
|
|
@@ -146643,9 +146756,9 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
146643
146756
|
|
|
146644
146757
|
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.
|
|
146645
146758
|
|
|
146646
|
-
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).
|
|
146759
|
+
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.
|
|
146647
146760
|
|
|
146648
|
-
if the PR is **genuinely trivial**, skip
|
|
146761
|
+
if the PR is **genuinely trivial**, skip the fan-out entirely and submit a \`No new issues found.\` review per step 7.
|
|
146649
146762
|
|
|
146650
146763
|
"Genuinely trivial" (skip):
|
|
146651
146764
|
- single-word doc typo, whitespace/format-only, comment-only across any number of files
|
|
@@ -146664,23 +146777,25 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
146664
146777
|
- any "typo fix" in user-facing copy that changes meaning ("approved" \u2192 "denied")
|
|
146665
146778
|
- mixed diffs where a semantic 1-liner is buried in whitespace/formatting changes
|
|
146666
146779
|
|
|
146667
|
-
|
|
146780
|
+
4. **lens decision \u2014 0 or 2+, NEVER 1**.
|
|
146781
|
+
|
|
146782
|
+
The default is **0 lenses**: handle the review yourself end-to-end. Most PRs land here.
|
|
146668
146783
|
|
|
146669
|
-
|
|
146784
|
+
Dispatch **2+ \`${REVIEWER_AGENT_NAME}\` lenses in parallel** ONLY when ALL of the following are true:
|
|
146785
|
+
- 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)
|
|
146786
|
+
- you can name 2+ distinct concrete failure modes that warrant independent lenses (one lens per failure mode; orthogonal, not overlapping)
|
|
146787
|
+
- parallel-orchestrated independent perspectives meaningfully outperform what you'd find solo
|
|
146670
146788
|
|
|
146671
|
-
|
|
146672
|
-
- **2\u20133 lenses (most PRs land here)** \u2014 new CRUD endpoint (correctness + security + test-integrity); new UI flow (user-journey + correctness); a single bug fix in a non-critical subsystem (correctness + test-integrity); design doc covering one domain (research-validated + correctness or holistic)
|
|
146673
|
-
- **4\u20135 lenses (high-stakes subsystem touches)** \u2014 any billing/payments change (billing-subsystem + correctness + security + operational-readiness); new auth flow (auth-subsystem + correctness + security + test-integrity); schema migration (schema-migration-subsystem + correctness + operational-readiness + impact); cross-subsystem PR that touches billing AND auth AND schema (one subsystem lens per domain + correctness)
|
|
146674
|
-
- **6+ lenses** \u2014 almost always a smell; you're either covering overlapping ground or this PR should have been split. push back via the review body rather than expanding lens count.
|
|
146789
|
+
**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).
|
|
146675
146790
|
|
|
146676
|
-
|
|
146791
|
+
When you do go multi-lens, lens framings come in two flavors:
|
|
146677
146792
|
- **themed lenses** \u2014 a perspective applied across the whole diff (correctness, security, user-journey, performance, etc.).
|
|
146678
|
-
- **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").
|
|
146793
|
+
- **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.
|
|
146679
146794
|
|
|
146680
146795
|
starter menu (combine, omit, or invent your own):
|
|
146681
146796
|
- **correctness & invariants** \u2014 bugs, races, error handling, edge cases, state-machine boundaries
|
|
146682
|
-
- **impact** \u2014
|
|
146683
|
-
- **research-validated assumptions** \u2014 third-party API contracts, SDK semantics, framework directives, version-gated behavior. the subagent must verify load-bearing claims via web search and quote source URLs.
|
|
146797
|
+
- **impact** \u2014 stale references in code/tests/docs/configs/UI after rename/remove
|
|
146798
|
+
- **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.
|
|
146684
146799
|
- **security** \u2014 new endpoints, authZ, input validation, secrets handling, replay/CSRF/injection, cross-tenant isolation
|
|
146685
146800
|
- **user-journey** \u2014 UX-touching flows: walk through happy path and failure modes as a user
|
|
146686
146801
|
- **operational readiness** \u2014 observability, alerting, migrations (forward + rollback), feature flags, on-call burden
|
|
@@ -146690,26 +146805,36 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
146690
146805
|
- **holistic** \u2014 does the PR make sense as a whole? symmetric flows (delete for every create, rollback for every migration)?
|
|
146691
146806
|
- **subsystem lenses** (invent as the PR demands) \u2014 auth, billing, payments, schema migration, webhooks, secrets, RBAC, multi-tenant isolation, cron/scheduling, etc.
|
|
146692
146807
|
|
|
146693
|
-
|
|
146808
|
+
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.
|
|
146809
|
+
|
|
146810
|
+
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.**
|
|
146811
|
+
|
|
146812
|
+
\u26A0\uFE0F CRITICAL \u2014 PARALLELISM IS THE ONLY REASON LENSES EXIST. \u26A0\uFE0F
|
|
146813
|
+
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.
|
|
146814
|
+
|
|
146815
|
+
\u2705 Right pattern: one assistant turn with N Task tool_use blocks \u2192 wait \u2192 N results arrive together \u2192 aggregate.
|
|
146816
|
+
\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.
|
|
146817
|
+
|
|
146818
|
+
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.
|
|
146819
|
+
|
|
146820
|
+
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:
|
|
146694
146821
|
- the diff path / target \u2014 reading the diff and the codebase is its job
|
|
146695
146822
|
- **only one lens** \u2014 never a multi-section "review for X, Y, and Z" prompt
|
|
146696
146823
|
- **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\`.
|
|
146697
|
-
- 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.
|
|
146698
146824
|
- 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."
|
|
146699
146825
|
- 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.
|
|
146700
146826
|
|
|
146701
146827
|
delegation discipline:
|
|
146702
|
-
- 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)
|
|
146703
146828
|
- do NOT summarize the PR for them (biases toward a validation frame)
|
|
146704
146829
|
- do NOT hand them a curated reading list (let them discover scope)
|
|
146705
146830
|
- do NOT pre-shape their output with a finding schema
|
|
146706
146831
|
- do NOT mention the other lenses (independence is the point \u2014 overlapping findings are a strong signal)
|
|
146707
146832
|
|
|
146708
|
-
|
|
146833
|
+
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.
|
|
146709
146834
|
|
|
146710
146835
|
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.
|
|
146711
146836
|
|
|
146712
|
-
|
|
146837
|
+
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.
|
|
146713
146838
|
|
|
146714
146839
|
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.
|
|
146715
146840
|
|
|
@@ -146737,10 +146862,10 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
146737
146862
|
|
|
146738
146863
|
${PR_SUMMARY_FORMAT}`
|
|
146739
146864
|
},
|
|
146740
|
-
// IncrementalReview shares Review's
|
|
146741
|
-
//
|
|
146742
|
-
//
|
|
146743
|
-
//
|
|
146865
|
+
// IncrementalReview shares Review's 0-or-2+ lens pattern but scopes the
|
|
146866
|
+
// target to the incremental diff. The "issues must be NEW since the last
|
|
146867
|
+
// Pullfrog review" filter lives at aggregation time (step 8), NOT in the
|
|
146868
|
+
// subagent prompt — pushing the filter into
|
|
146744
146869
|
// subagents matches the canonical anneal anti-pattern of "list known
|
|
146745
146870
|
// pre-existing failures — don't flag these" and suppresses signal on
|
|
146746
146871
|
// regressions the new commits amplified. The review body is just
|
|
@@ -146759,38 +146884,57 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
146759
146884
|
|
|
146760
146885
|
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.
|
|
146761
146886
|
|
|
146762
|
-
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
|
|
146887
|
+
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.
|
|
146763
146888
|
|
|
146764
|
-
5. **triage
|
|
146889
|
+
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.**
|
|
146765
146890
|
|
|
146766
|
-
if the incremental changes are **genuinely trivial**, skip the fan-out entirely and jump to step
|
|
146891
|
+
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).
|
|
146767
146892
|
|
|
146768
146893
|
"Genuinely trivial" (skip): formatting/comment tweaks, import reordering, lockfile regen, mechanical rename of import paths, whitespace-only.
|
|
146769
146894
|
"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.
|
|
146770
146895
|
When unsure, treat as non-trivial.
|
|
146771
146896
|
|
|
146772
|
-
|
|
146897
|
+
6. **lens decision \u2014 0 or 2+, NEVER 1**.
|
|
146898
|
+
|
|
146899
|
+
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."
|
|
146900
|
+
|
|
146901
|
+
Dispatch **2+ \`${REVIEWER_AGENT_NAME}\` lenses in parallel** ONLY when ALL of the following are true:
|
|
146902
|
+
- 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)
|
|
146903
|
+
- you can name 2+ distinct concrete failure modes the new commits plausibly introduce that warrant independent lenses
|
|
146904
|
+
- parallel-orchestrated independent perspectives meaningfully outperform what you'd find solo
|
|
146905
|
+
|
|
146906
|
+
**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.
|
|
146907
|
+
|
|
146908
|
+
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.
|
|
146909
|
+
|
|
146910
|
+
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.**
|
|
146773
146911
|
|
|
146774
|
-
|
|
146775
|
-
-
|
|
146912
|
+
\u26A0\uFE0F CRITICAL \u2014 PARALLELISM IS THE ONLY REASON LENSES EXIST. \u26A0\uFE0F
|
|
146913
|
+
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.
|
|
146914
|
+
|
|
146915
|
+
\u2705 Right pattern: one assistant turn with N Task tool_use blocks \u2192 wait \u2192 N results arrive together \u2192 aggregate.
|
|
146916
|
+
\u274C Wrong pattern: turn 1 = Task(lens A) \u2192 turn 2 (after A's result) = Task(lens B). This is the failure mode.
|
|
146917
|
+
|
|
146918
|
+
You can also include your own \`read\` / \`grep\` / \`webfetch\` calls in the SAME turn as the parallel \`${REVIEWER_AGENT_NAME}\` dispatches.
|
|
146919
|
+
|
|
146920
|
+
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:
|
|
146921
|
+
- 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
|
|
146776
146922
|
- **only one lens** \u2014 never a multi-section "review for X, Y, and Z" prompt
|
|
146777
|
-
- **a Task \`description\` set to the lens name**
|
|
146778
|
-
- the
|
|
146779
|
-
- 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."
|
|
146923
|
+
- **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.
|
|
146924
|
+
- if the lens touches external contracts, instruct the subagent to verify load-bearing claims via web search and quote source URLs.
|
|
146780
146925
|
- ask the subagent to report findings with file paths and NEW line numbers from the full PR diff so you can anchor inline comments.
|
|
146781
146926
|
|
|
146782
146927
|
delegation discipline:
|
|
146783
|
-
- do NOT lens-review the diff yourself in parallel with the subagents
|
|
146784
146928
|
- do NOT summarize the changes for them (biases toward validation frame)
|
|
146785
146929
|
- do NOT hand them a curated reading list (let them discover scope)
|
|
146786
146930
|
- do NOT pre-shape their output with a finding schema
|
|
146787
146931
|
- do NOT mention the other lenses (independence is the point)
|
|
146788
146932
|
|
|
146789
|
-
|
|
146933
|
+
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.
|
|
146790
146934
|
|
|
146791
|
-
|
|
146935
|
+
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.
|
|
146792
146936
|
|
|
146793
|
-
|
|
146937
|
+
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.
|
|
146794
146938
|
|
|
146795
146939
|
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.
|
|
146796
146940
|
|
|
@@ -147139,20 +147283,30 @@ var ThinkingTimer = class {
|
|
|
147139
147283
|
maximumFractionDigits: 1
|
|
147140
147284
|
});
|
|
147141
147285
|
lastToolResultTimestamp = null;
|
|
147286
|
+
formatLine;
|
|
147287
|
+
// node's native TS strip-only mode does not support parameter properties,
|
|
147288
|
+
// so the formatter is declared as a field and assigned in the body.
|
|
147289
|
+
constructor(formatLine = (l) => l) {
|
|
147290
|
+
this.formatLine = formatLine;
|
|
147291
|
+
}
|
|
147142
147292
|
markToolResult() {
|
|
147143
147293
|
this.lastToolResultTimestamp = performance5.now();
|
|
147144
|
-
log.debug(
|
|
147294
|
+
log.debug(
|
|
147295
|
+
this.formatLine(`\xBB thinking timer: markToolResult at ${this.lastToolResultTimestamp}`)
|
|
147296
|
+
);
|
|
147145
147297
|
}
|
|
147146
147298
|
markToolCall() {
|
|
147147
147299
|
const now = performance5.now();
|
|
147148
147300
|
log.debug(
|
|
147149
|
-
|
|
147301
|
+
this.formatLine(
|
|
147302
|
+
`\xBB thinking timer: markToolCall at ${now}, lastToolResult=${this.lastToolResultTimestamp}`
|
|
147303
|
+
)
|
|
147150
147304
|
);
|
|
147151
147305
|
if (this.lastToolResultTimestamp === null) return;
|
|
147152
147306
|
const elapsed = now - this.lastToolResultTimestamp;
|
|
147153
147307
|
if (elapsed < THINKING_THRESHOLD) return;
|
|
147154
147308
|
const seconds = elapsed / 1e3;
|
|
147155
|
-
log.info(`\xBB thought for ${this.durationFormatter.format(seconds)}`);
|
|
147309
|
+
log.info(this.formatLine(`\xBB thought for ${this.durationFormatter.format(seconds)}`));
|
|
147156
147310
|
}
|
|
147157
147311
|
};
|
|
147158
147312
|
|
|
@@ -147160,45 +147314,12 @@ var ThinkingTimer = class {
|
|
|
147160
147314
|
import { readFile } from "node:fs/promises";
|
|
147161
147315
|
function getUnsubmittedReview(toolState) {
|
|
147162
147316
|
const mode = toolState.selectedMode;
|
|
147163
|
-
if (mode !== "Review" && mode !== "IncrementalReview") return null;
|
|
147164
|
-
if (toolState.review || toolState.finalSummaryWritten) return null;
|
|
147165
147317
|
if (!toolState.hadProgressComment) return null;
|
|
147166
|
-
return
|
|
147167
|
-
|
|
147168
|
-
|
|
147169
|
-
function truncateHookOutput(raw2) {
|
|
147170
|
-
if (raw2.length <= MAX_HOOK_OUTPUT_CHARS) return raw2;
|
|
147171
|
-
return `...(truncated, showing last ${MAX_HOOK_OUTPUT_CHARS} chars)
|
|
147172
|
-
${raw2.slice(-MAX_HOOK_OUTPUT_CHARS)}`;
|
|
147173
|
-
}
|
|
147174
|
-
async function executeStopHook(script) {
|
|
147175
|
-
log.info("\xBB executing stop hook...");
|
|
147176
|
-
try {
|
|
147177
|
-
const result = await spawn({
|
|
147178
|
-
cmd: "bash",
|
|
147179
|
-
args: ["-c", script],
|
|
147180
|
-
env: process.env,
|
|
147181
|
-
timeout: LIFECYCLE_HOOK_TIMEOUT_MS,
|
|
147182
|
-
activityTimeout: 0,
|
|
147183
|
-
onStdout: (chunk) => process.stdout.write(chunk),
|
|
147184
|
-
onStderr: (chunk) => process.stderr.write(chunk)
|
|
147185
|
-
});
|
|
147186
|
-
if (result.exitCode === 0) {
|
|
147187
|
-
log.info("\xBB stop hook passed");
|
|
147188
|
-
return null;
|
|
147189
|
-
}
|
|
147190
|
-
const combined = [result.stderr.trim(), result.stdout.trim()].filter(Boolean).join("\n");
|
|
147191
|
-
const output = truncateHookOutput(combined);
|
|
147192
|
-
log.info(`\xBB stop hook failed with exit code ${result.exitCode}`);
|
|
147193
|
-
return { exitCode: result.exitCode, output };
|
|
147194
|
-
} catch (err) {
|
|
147195
|
-
const isTimeout = err instanceof SpawnTimeoutError && (err.code === SPAWN_TIMEOUT_CODE || err.code === SPAWN_ACTIVITY_TIMEOUT_CODE);
|
|
147196
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
147197
|
-
log.warning(
|
|
147198
|
-
`stop hook ${isTimeout ? "timed out" : "failed to spawn"}: ${msg} \u2014 skipping retry`
|
|
147199
|
-
);
|
|
147200
|
-
return null;
|
|
147318
|
+
if (mode === "Review") return toolState.review ? null : "Review";
|
|
147319
|
+
if (mode === "IncrementalReview") {
|
|
147320
|
+
return toolState.review || toolState.finalSummaryWritten ? null : "IncrementalReview";
|
|
147201
147321
|
}
|
|
147322
|
+
return null;
|
|
147202
147323
|
}
|
|
147203
147324
|
function buildStopHookPrompt(failure) {
|
|
147204
147325
|
return [
|
|
@@ -147248,10 +147369,6 @@ function buildUnsubmittedReviewPrompt(mode) {
|
|
|
147248
147369
|
}
|
|
147249
147370
|
async function collectPostRunIssues(ctx, options = {}) {
|
|
147250
147371
|
const issues = {};
|
|
147251
|
-
if (ctx.stopScript) {
|
|
147252
|
-
const failure = await executeStopHook(ctx.stopScript);
|
|
147253
|
-
if (failure) issues.stopHook = failure;
|
|
147254
|
-
}
|
|
147255
147372
|
const status = getGitStatus();
|
|
147256
147373
|
const mode = ctx.toolState.selectedMode;
|
|
147257
147374
|
if (status) {
|
|
@@ -147287,11 +147404,25 @@ function buildLearningsReflectionPrompt(filePath) {
|
|
|
147287
147404
|
"",
|
|
147288
147405
|
`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.`,
|
|
147289
147406
|
"",
|
|
147290
|
-
`
|
|
147291
|
-
`-
|
|
147292
|
-
`-
|
|
147293
|
-
`-
|
|
147294
|
-
|
|
147407
|
+
`structure:`,
|
|
147408
|
+
`- 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\`).`,
|
|
147409
|
+
`- **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.`,
|
|
147410
|
+
`- 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.`,
|
|
147411
|
+
"",
|
|
147412
|
+
`bullet hygiene:`,
|
|
147413
|
+
`- one fact per line starting with \`- \`. each bullet is ONE specific durable fact, not a paragraph or essay.`,
|
|
147414
|
+
`- 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.`,
|
|
147415
|
+
`- 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.`,
|
|
147416
|
+
`- 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.`,
|
|
147417
|
+
`- deduplicate against existing entries (in any section) \u2014 if a bullet covers the same fact, update it in place instead of adding a duplicate.`,
|
|
147418
|
+
"",
|
|
147419
|
+
`do NOT add bullets for:`,
|
|
147420
|
+
`- 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.`,
|
|
147421
|
+
`- 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.`,
|
|
147422
|
+
`- 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.`,
|
|
147423
|
+
`- play-by-play of what THIS run did. learnings are for the NEXT run, not a retrospective.`,
|
|
147424
|
+
"",
|
|
147425
|
+
`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.`
|
|
147295
147426
|
].join("\n");
|
|
147296
147427
|
}
|
|
147297
147428
|
async function runPostRunRetryLoop(params) {
|
|
@@ -147401,19 +147532,39 @@ function deriveLabelFromTaskInput(input) {
|
|
|
147401
147532
|
}
|
|
147402
147533
|
var SessionLabeler = class {
|
|
147403
147534
|
labels = /* @__PURE__ */ new Map();
|
|
147535
|
+
labelsByToolUseId = /* @__PURE__ */ new Map();
|
|
147404
147536
|
pendingLabels = [];
|
|
147405
147537
|
fallbackCounter = 0;
|
|
147406
|
-
|
|
147538
|
+
/**
|
|
147539
|
+
* Record a Task/Agent tool dispatch.
|
|
147540
|
+
*
|
|
147541
|
+
* @param input Task tool input — used to derive the lens label.
|
|
147542
|
+
* @param toolUseId Optional Agent tool_use id. When provided, future events
|
|
147543
|
+
* carrying `parent_tool_use_id === toolUseId` resolve
|
|
147544
|
+
* directly to this label without consuming the FIFO queue
|
|
147545
|
+
* (Claude path). Always also pushed to the FIFO queue so
|
|
147546
|
+
* the OpenCode path still works when toolUseId is absent.
|
|
147547
|
+
*/
|
|
147548
|
+
recordTaskDispatch(input, toolUseId) {
|
|
147407
147549
|
const label = deriveLabelFromTaskInput(input);
|
|
147408
147550
|
this.pendingLabels.push(label);
|
|
147551
|
+
if (toolUseId) this.labelsByToolUseId.set(toolUseId, label);
|
|
147409
147552
|
return label;
|
|
147410
147553
|
}
|
|
147411
147554
|
/**
|
|
147412
|
-
* Return a label for the given
|
|
147413
|
-
*
|
|
147414
|
-
*
|
|
147555
|
+
* Return a label for the given event.
|
|
147556
|
+
*
|
|
147557
|
+
* @param sessionID Session id from the event (OpenCode: per-session;
|
|
147558
|
+
* Claude: shared across orchestrator + subagents).
|
|
147559
|
+
* @param parentToolUseId Claude's `parent_tool_use_id` — non-null on
|
|
147560
|
+
* subagent messages. When set and known, takes
|
|
147561
|
+
* priority over the FIFO/sessionID path.
|
|
147415
147562
|
*/
|
|
147416
|
-
labelFor(sessionID) {
|
|
147563
|
+
labelFor(sessionID, parentToolUseId) {
|
|
147564
|
+
if (parentToolUseId) {
|
|
147565
|
+
const direct = this.labelsByToolUseId.get(parentToolUseId);
|
|
147566
|
+
if (direct) return direct;
|
|
147567
|
+
}
|
|
147417
147568
|
if (!sessionID) return ORCHESTRATOR_LABEL;
|
|
147418
147569
|
const existing = this.labels.get(sessionID);
|
|
147419
147570
|
if (existing) return existing;
|
|
@@ -147475,8 +147626,9 @@ function writeMcpConfig(ctx) {
|
|
|
147475
147626
|
function buildAgentsJson() {
|
|
147476
147627
|
const agents2 = {
|
|
147477
147628
|
[REVIEWER_AGENT_NAME]: {
|
|
147478
|
-
description: "Read-only review subagent for
|
|
147479
|
-
prompt: REVIEWER_SYSTEM_PROMPT
|
|
147629
|
+
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.",
|
|
147630
|
+
prompt: REVIEWER_SYSTEM_PROMPT,
|
|
147631
|
+
model: "claude-sonnet-4-6"
|
|
147480
147632
|
}
|
|
147481
147633
|
};
|
|
147482
147634
|
return JSON.stringify(agents2);
|
|
@@ -147497,7 +147649,23 @@ function tailLines(text, maxCodeUnits) {
|
|
|
147497
147649
|
async function runClaude(params) {
|
|
147498
147650
|
const startTime = performance6.now();
|
|
147499
147651
|
let eventCount = 0;
|
|
147500
|
-
const
|
|
147652
|
+
const labeler = new SessionLabeler();
|
|
147653
|
+
function eventLabel(event) {
|
|
147654
|
+
return labeler.labelFor(event.session_id ?? null, event.parent_tool_use_id ?? null);
|
|
147655
|
+
}
|
|
147656
|
+
function withLabel(label, message) {
|
|
147657
|
+
return label === ORCHESTRATOR_LABEL ? message : formatWithLabel(label, message);
|
|
147658
|
+
}
|
|
147659
|
+
const thinkingTimers = /* @__PURE__ */ new Map();
|
|
147660
|
+
function timerFor(label) {
|
|
147661
|
+
let t2 = thinkingTimers.get(label);
|
|
147662
|
+
if (!t2) {
|
|
147663
|
+
const formatLine = (line) => label === ORCHESTRATOR_LABEL ? line : formatWithLabel(label, line);
|
|
147664
|
+
t2 = new ThinkingTimer(formatLine);
|
|
147665
|
+
thinkingTimers.set(label, t2);
|
|
147666
|
+
}
|
|
147667
|
+
return t2;
|
|
147668
|
+
}
|
|
147501
147669
|
let finalOutput = "";
|
|
147502
147670
|
let sessionId;
|
|
147503
147671
|
let resultErrorSubtype = null;
|
|
@@ -147518,17 +147686,22 @@ async function runClaude(params) {
|
|
|
147518
147686
|
} : void 0;
|
|
147519
147687
|
}
|
|
147520
147688
|
const handlers2 = {
|
|
147521
|
-
system: (
|
|
147522
|
-
|
|
147689
|
+
system: (event) => {
|
|
147690
|
+
const label = eventLabel(event);
|
|
147691
|
+
log.debug(withLabel(label, `\xBB ${params.label} system event`));
|
|
147523
147692
|
},
|
|
147524
147693
|
assistant: (event) => {
|
|
147525
147694
|
const content = event.message?.content;
|
|
147526
147695
|
if (!content) return;
|
|
147696
|
+
const label = eventLabel(event);
|
|
147697
|
+
const boxTitle = label === ORCHESTRATOR_LABEL ? params.label : `${params.label} [${label}]`;
|
|
147527
147698
|
for (const block of content) {
|
|
147528
147699
|
if (block.type === "text" && block.text?.trim()) {
|
|
147529
147700
|
const message = block.text.trim();
|
|
147530
|
-
log.box(message, { title:
|
|
147531
|
-
|
|
147701
|
+
log.box(message, { title: boxTitle });
|
|
147702
|
+
if (label === ORCHESTRATOR_LABEL) {
|
|
147703
|
+
finalOutput = message;
|
|
147704
|
+
}
|
|
147532
147705
|
} else if (block.type === "tool_use") {
|
|
147533
147706
|
const toolName = block.name || "unknown";
|
|
147534
147707
|
if (params.onToolUse) {
|
|
@@ -147537,20 +147710,25 @@ async function runClaude(params) {
|
|
|
147537
147710
|
input: block.input
|
|
147538
147711
|
});
|
|
147539
147712
|
}
|
|
147540
|
-
|
|
147541
|
-
|
|
147542
|
-
|
|
147713
|
+
timerFor(label).markToolCall();
|
|
147714
|
+
const inputFormatted = formatJsonValue(block.input || {});
|
|
147715
|
+
const toolCallLine = inputFormatted !== "{}" ? `\xBB ${toolName}(${inputFormatted})` : `\xBB ${toolName}()`;
|
|
147716
|
+
log.info(withLabel(label, toolCallLine));
|
|
147717
|
+
if ((toolName === "Task" || toolName === "Agent") && block.input && typeof block.input === "object") {
|
|
147543
147718
|
const taskInput = block.input;
|
|
147544
|
-
const
|
|
147719
|
+
const dispatchedLabel = labeler.recordTaskDispatch(taskInput, block.id ?? null);
|
|
147545
147720
|
log.info(
|
|
147546
|
-
|
|
147721
|
+
withLabel(
|
|
147722
|
+
label,
|
|
147723
|
+
`\xBB dispatching subagent: ${dispatchedLabel}` + (taskInput.subagent_type ? ` (subagent_type=${taskInput.subagent_type})` : "")
|
|
147724
|
+
)
|
|
147547
147725
|
);
|
|
147548
147726
|
}
|
|
147549
147727
|
if (toolName.includes("report_progress") && params.todoTracker) {
|
|
147550
147728
|
log.debug("\xBB report_progress detected, disabling todo tracking");
|
|
147551
147729
|
params.todoTracker.cancel();
|
|
147552
147730
|
}
|
|
147553
|
-
if (toolName === "TodoWrite" && params.todoTracker?.enabled) {
|
|
147731
|
+
if (toolName === "TodoWrite" && params.todoTracker?.enabled && label === ORCHESTRATOR_LABEL) {
|
|
147554
147732
|
params.todoTracker.update(block.input);
|
|
147555
147733
|
}
|
|
147556
147734
|
}
|
|
@@ -147566,17 +147744,18 @@ async function runClaude(params) {
|
|
|
147566
147744
|
user: (event) => {
|
|
147567
147745
|
const content = event.message?.content;
|
|
147568
147746
|
if (!content) return;
|
|
147747
|
+
const label = eventLabel(event);
|
|
147569
147748
|
for (const block of content) {
|
|
147570
147749
|
if (typeof block === "string") continue;
|
|
147571
147750
|
if (block.type === "tool_result") {
|
|
147572
|
-
|
|
147751
|
+
timerFor(label).markToolResult();
|
|
147573
147752
|
const outputContent = typeof block.content === "string" ? block.content : Array.isArray(block.content) ? block.content.map(
|
|
147574
147753
|
(entry) => typeof entry === "string" ? entry : typeof entry === "object" && entry !== null && "text" in entry ? String(entry.text) : JSON.stringify(entry)
|
|
147575
147754
|
).join("\n") : String(block.content);
|
|
147576
147755
|
if (block.is_error) {
|
|
147577
|
-
log.info(`\xBB tool error: ${outputContent}`);
|
|
147756
|
+
log.info(withLabel(label, `\xBB tool error: ${outputContent}`));
|
|
147578
147757
|
} else {
|
|
147579
|
-
log.debug(`\xBB tool output: ${outputContent}`);
|
|
147758
|
+
log.debug(withLabel(label, `\xBB tool output: ${outputContent}`));
|
|
147580
147759
|
}
|
|
147581
147760
|
}
|
|
147582
147761
|
}
|
|
@@ -147645,8 +147824,9 @@ async function runClaude(params) {
|
|
|
147645
147824
|
}
|
|
147646
147825
|
};
|
|
147647
147826
|
const recentStderr = [];
|
|
147827
|
+
const recentNonJsonStdout = [];
|
|
147648
147828
|
let lastProviderError = null;
|
|
147649
|
-
|
|
147829
|
+
const output = new TailBuffer(DEFAULT_MAX_RETAINED_BYTES);
|
|
147650
147830
|
let stdoutBuffer = "";
|
|
147651
147831
|
try {
|
|
147652
147832
|
const result = await spawn({
|
|
@@ -147663,9 +147843,14 @@ async function runClaude(params) {
|
|
|
147663
147843
|
// there's no shim-orphan issue like opencode-ai/bin/opencode, but
|
|
147664
147844
|
// detached + killGroup is the right default for any agent runtime.
|
|
147665
147845
|
killGroup: true,
|
|
147846
|
+
// claude already drains every chunk via onStdout (NDJSON parsing) and
|
|
147847
|
+
// onStderr (recentStderr ring buffer). retaining a second copy in the
|
|
147848
|
+
// spawn wrapper would grow unbounded for long sessions and previously
|
|
147849
|
+
// crashed the wrapper with RangeError. see issue #680.
|
|
147850
|
+
retain: "none",
|
|
147666
147851
|
onStdout: async (chunk) => {
|
|
147667
147852
|
const text = chunk.toString();
|
|
147668
|
-
output
|
|
147853
|
+
output.append(text);
|
|
147669
147854
|
markActivity();
|
|
147670
147855
|
stdoutBuffer += text;
|
|
147671
147856
|
const lines = stdoutBuffer.split("\n");
|
|
@@ -147678,6 +147863,8 @@ async function runClaude(params) {
|
|
|
147678
147863
|
event = JSON.parse(trimmed);
|
|
147679
147864
|
} catch {
|
|
147680
147865
|
log.debug(`\xBB non-JSON stdout line: ${trimmed.substring(0, 200)}`);
|
|
147866
|
+
recentNonJsonStdout.push(trimmed);
|
|
147867
|
+
if (recentNonJsonStdout.length > MAX_STDERR_LINES) recentNonJsonStdout.shift();
|
|
147681
147868
|
continue;
|
|
147682
147869
|
}
|
|
147683
147870
|
eventCount++;
|
|
@@ -147740,16 +147927,19 @@ ${stderrContext}`);
|
|
|
147740
147927
|
const usage = buildUsage();
|
|
147741
147928
|
if (result.exitCode !== 0) {
|
|
147742
147929
|
const errorContext = lastProviderError ? ` (${lastProviderError})` : "";
|
|
147743
|
-
const
|
|
147744
|
-
const
|
|
147930
|
+
const stdoutSnapshot = output.toString();
|
|
147931
|
+
const stderrSnapshot = recentStderr.join("\n");
|
|
147932
|
+
const truncatedStdout = stdoutSnapshot ? tailLines(stdoutSnapshot, 2048) : "";
|
|
147933
|
+
const nonJsonStdoutSnapshot = recentNonJsonStdout.join("\n");
|
|
147934
|
+
const errorMessage = lastResultError || stderrSnapshot || nonJsonStdoutSnapshot || truncatedStdout || `unknown error - no output from Claude CLI${errorContext}`;
|
|
147745
147935
|
log.error(
|
|
147746
147936
|
`${params.label} exited with code ${result.exitCode}${errorContext}: ${errorMessage}`
|
|
147747
147937
|
);
|
|
147748
|
-
log.debug(`stdout: ${
|
|
147749
|
-
log.debug(`stderr: ${
|
|
147938
|
+
log.debug(`stdout: ${stdoutSnapshot.substring(0, 500)}`);
|
|
147939
|
+
log.debug(`stderr: ${stderrSnapshot.substring(0, 500)}`);
|
|
147750
147940
|
return {
|
|
147751
147941
|
success: false,
|
|
147752
|
-
output: finalOutput ||
|
|
147942
|
+
output: finalOutput || stdoutSnapshot,
|
|
147753
147943
|
error: errorMessage,
|
|
147754
147944
|
usage,
|
|
147755
147945
|
sessionId
|
|
@@ -147758,7 +147948,7 @@ ${stderrContext}`);
|
|
|
147758
147948
|
if (eventCount === 0 && lastProviderError) {
|
|
147759
147949
|
return {
|
|
147760
147950
|
success: false,
|
|
147761
|
-
output: finalOutput || output,
|
|
147951
|
+
output: finalOutput || output.toString(),
|
|
147762
147952
|
error: `provider error: ${lastProviderError}`,
|
|
147763
147953
|
usage,
|
|
147764
147954
|
sessionId
|
|
@@ -147767,13 +147957,13 @@ ${stderrContext}`);
|
|
|
147767
147957
|
if (resultErrorSubtype) {
|
|
147768
147958
|
return {
|
|
147769
147959
|
success: false,
|
|
147770
|
-
output: finalOutput || output,
|
|
147960
|
+
output: finalOutput || output.toString(),
|
|
147771
147961
|
error: lastResultError || `result subtype: ${resultErrorSubtype}`,
|
|
147772
147962
|
usage,
|
|
147773
147963
|
sessionId
|
|
147774
147964
|
};
|
|
147775
147965
|
}
|
|
147776
|
-
return { success: true, output: finalOutput || output, usage, sessionId };
|
|
147966
|
+
return { success: true, output: finalOutput || output.toString(), usage, sessionId };
|
|
147777
147967
|
} catch (error49) {
|
|
147778
147968
|
params.todoTracker?.cancel();
|
|
147779
147969
|
const duration4 = performance6.now() - startTime;
|
|
@@ -147792,7 +147982,7 @@ ${stderrContext}`
|
|
|
147792
147982
|
);
|
|
147793
147983
|
return {
|
|
147794
147984
|
success: false,
|
|
147795
|
-
output: finalOutput || output,
|
|
147985
|
+
output: finalOutput || output.toString(),
|
|
147796
147986
|
error: `${errorMessage} [${diagnosis}]`,
|
|
147797
147987
|
usage: buildUsage(),
|
|
147798
147988
|
sessionId
|
|
@@ -147842,7 +148032,9 @@ var claude = agent({
|
|
|
147842
148032
|
run: async (ctx) => {
|
|
147843
148033
|
const cliPath = await installClaudeCli();
|
|
147844
148034
|
const specifier = ctx.payload.proxyModel ?? ctx.resolvedModel;
|
|
147845
|
-
const
|
|
148035
|
+
const bedrockModelId = process.env[BEDROCK_MODEL_ID_ENV]?.trim();
|
|
148036
|
+
const isBedrockRoute = specifier !== void 0 && bedrockModelId !== void 0 && bedrockModelId === specifier && isBedrockAnthropicId(specifier);
|
|
148037
|
+
const model = !specifier ? void 0 : isBedrockRoute ? specifier : stripProviderPrefix(specifier);
|
|
147846
148038
|
const homeEnv = {
|
|
147847
148039
|
HOME: ctx.tmpdir,
|
|
147848
148040
|
XDG_CONFIG_HOME: join10(ctx.tmpdir, ".config")
|
|
@@ -147881,6 +148073,9 @@ var claude = agent({
|
|
|
147881
148073
|
...process.env,
|
|
147882
148074
|
...homeEnv
|
|
147883
148075
|
};
|
|
148076
|
+
if (isBedrockRoute) {
|
|
148077
|
+
env2.CLAUDE_CODE_USE_BEDROCK = "1";
|
|
148078
|
+
}
|
|
147884
148079
|
const repoDir = process.cwd();
|
|
147885
148080
|
log.info(`\xBB effort: ${effort}`);
|
|
147886
148081
|
log.debug(`\xBB starting Pullfrog (Claude Code): node ${baseArgs.join(" ")}`);
|
|
@@ -148002,6 +148197,22 @@ export default async function pullfrogEventsPlugin() {
|
|
|
148002
148197
|
}
|
|
148003
148198
|
`;
|
|
148004
148199
|
|
|
148200
|
+
// agents/subagentModels.ts
|
|
148201
|
+
function deriveSubagentModels(orchestratorSpec) {
|
|
148202
|
+
if (!orchestratorSpec) return { reviewer: void 0 };
|
|
148203
|
+
for (const source of modelAliases) {
|
|
148204
|
+
const matchedDirect = source.resolve === orchestratorSpec;
|
|
148205
|
+
const matchedOR = source.openRouterResolve === orchestratorSpec;
|
|
148206
|
+
if (!matchedDirect && !matchedOR) continue;
|
|
148207
|
+
if (!source.subagentModel) return { reviewer: void 0 };
|
|
148208
|
+
const target = modelAliases.find((a) => a.slug === source.subagentModel);
|
|
148209
|
+
if (!target) return { reviewer: void 0 };
|
|
148210
|
+
const reviewer = matchedOR ? target.openRouterResolve : target.resolve;
|
|
148211
|
+
return { reviewer };
|
|
148212
|
+
}
|
|
148213
|
+
return { reviewer: void 0 };
|
|
148214
|
+
}
|
|
148215
|
+
|
|
148005
148216
|
// agents/opencode.ts
|
|
148006
148217
|
async function installOpencodeCli() {
|
|
148007
148218
|
return await installFromNpmTarball({
|
|
@@ -148011,7 +148222,6 @@ async function installOpencodeCli() {
|
|
|
148011
148222
|
installDependencies: true
|
|
148012
148223
|
});
|
|
148013
148224
|
}
|
|
148014
|
-
var PULLFROG_OPENCODE_OUTPUT_LIMIT = 5e3;
|
|
148015
148225
|
var GEMINI_3_DIRECT_THINKING_LEVEL = "medium";
|
|
148016
148226
|
var GEMINI_3_DIRECT_API_IDS = ["gemini-3.1-pro-preview", "gemini-3-flash-preview"];
|
|
148017
148227
|
function buildSecurityConfig(ctx, model) {
|
|
@@ -148027,7 +148237,21 @@ function buildSecurityConfig(ctx, model) {
|
|
|
148027
148237
|
mcp: {
|
|
148028
148238
|
[pullfrogMcpName]: { type: "remote", url: ctx.mcpServerUrl }
|
|
148029
148239
|
},
|
|
148030
|
-
agent:
|
|
148240
|
+
agent: (() => {
|
|
148241
|
+
const cfg = buildReviewerAgentConfig(model);
|
|
148242
|
+
const reviewerModel = cfg[REVIEWER_AGENT_NAME]?.model ?? "(inherit)";
|
|
148243
|
+
log.info(`\xBB subagent models: reviewfrog=${reviewerModel}`);
|
|
148244
|
+
return cfg;
|
|
148245
|
+
})(),
|
|
148246
|
+
// opt into opencode's experimental `batch` tool (added in
|
|
148247
|
+
// anomalyco/opencode PR #2983, opt-in via `experimental.batch_tool`). it
|
|
148248
|
+
// exposes a single `batch` tool that runs 1-25 independent tool calls
|
|
148249
|
+
// (read/grep/glob/bash/etc.) concurrently in one assistant turn, which
|
|
148250
|
+
// collapses the dominant grep→20×read pattern into a single round trip.
|
|
148251
|
+
// edits are explicitly disallowed inside the batch upstream. paired with
|
|
148252
|
+
// the "Parallel tool execution" guidance in utils/instructions.ts so the
|
|
148253
|
+
// model actually reaches for it. see wiki/prompt.md.
|
|
148254
|
+
experimental: { batch_tool: true },
|
|
148031
148255
|
provider: {
|
|
148032
148256
|
google: {
|
|
148033
148257
|
models: Object.fromEntries(
|
|
@@ -148052,12 +148276,14 @@ function buildSecurityConfig(ctx, model) {
|
|
|
148052
148276
|
}
|
|
148053
148277
|
return JSON.stringify(config3);
|
|
148054
148278
|
}
|
|
148055
|
-
function buildReviewerAgentConfig() {
|
|
148279
|
+
function buildReviewerAgentConfig(orchestratorModel) {
|
|
148280
|
+
const overrides = deriveSubagentModels(orchestratorModel);
|
|
148056
148281
|
return {
|
|
148057
148282
|
[REVIEWER_AGENT_NAME]: {
|
|
148058
|
-
description: "Read-only review subagent for
|
|
148283
|
+
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.",
|
|
148059
148284
|
mode: "subagent",
|
|
148060
|
-
prompt: REVIEWER_SYSTEM_PROMPT
|
|
148285
|
+
prompt: REVIEWER_SYSTEM_PROMPT,
|
|
148286
|
+
...overrides.reviewer !== void 0 ? { model: overrides.reviewer } : {}
|
|
148061
148287
|
}
|
|
148062
148288
|
};
|
|
148063
148289
|
}
|
|
@@ -148082,7 +148308,7 @@ function autoSelectModel(cliPath) {
|
|
|
148082
148308
|
const availableSet = new Set(availableModels);
|
|
148083
148309
|
if (availableSet.size > 0) {
|
|
148084
148310
|
log.debug(`\xBB opencode models (${availableSet.size}): ${availableModels.join(", ")}`);
|
|
148085
|
-
const match3 = modelAliases.find((a) => a.preferred && availableSet.has(a.resolve)) ?? modelAliases.find((a) => availableSet.has(a.resolve));
|
|
148311
|
+
const match3 = modelAliases.find((a) => !a.hidden && a.preferred && availableSet.has(a.resolve)) ?? modelAliases.find((a) => !a.hidden && availableSet.has(a.resolve));
|
|
148086
148312
|
if (match3) {
|
|
148087
148313
|
log.info(
|
|
148088
148314
|
`\xBB model: ${match3.resolve} (auto-selected${match3.preferred ? " \u2014 preferred" : ""} curated match)`
|
|
@@ -148100,7 +148326,6 @@ function autoSelectModel(cliPath) {
|
|
|
148100
148326
|
async function runOpenCode(params) {
|
|
148101
148327
|
const startTime = performance7.now();
|
|
148102
148328
|
let eventCount = 0;
|
|
148103
|
-
const thinkingTimer = new ThinkingTimer();
|
|
148104
148329
|
let finalOutput = "";
|
|
148105
148330
|
let accumulatedTokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
148106
148331
|
let accumulatedCostUsd = 0;
|
|
@@ -148117,6 +148342,16 @@ async function runOpenCode(params) {
|
|
|
148117
148342
|
function withLabel(label, message) {
|
|
148118
148343
|
return label === ORCHESTRATOR_LABEL ? message : formatWithLabel(label, message);
|
|
148119
148344
|
}
|
|
148345
|
+
const thinkingTimers = /* @__PURE__ */ new Map();
|
|
148346
|
+
function timerFor(label) {
|
|
148347
|
+
let t2 = thinkingTimers.get(label);
|
|
148348
|
+
if (!t2) {
|
|
148349
|
+
const formatLine = (line) => label === ORCHESTRATOR_LABEL ? line : formatWithLabel(label, line);
|
|
148350
|
+
t2 = new ThinkingTimer(formatLine);
|
|
148351
|
+
thinkingTimers.set(label, t2);
|
|
148352
|
+
}
|
|
148353
|
+
return t2;
|
|
148354
|
+
}
|
|
148120
148355
|
const taskDispatchByCallID = /* @__PURE__ */ new Map();
|
|
148121
148356
|
const pendingTaskDispatches = [];
|
|
148122
148357
|
const knownNonTaskCallIDs = /* @__PURE__ */ new Set();
|
|
@@ -148265,7 +148500,7 @@ async function runOpenCode(params) {
|
|
|
148265
148500
|
input: event.part?.state?.input
|
|
148266
148501
|
});
|
|
148267
148502
|
}
|
|
148268
|
-
|
|
148503
|
+
timerFor(label).markToolCall();
|
|
148269
148504
|
const inputFormatted = formatJsonValue(event.part?.state?.input || {});
|
|
148270
148505
|
const toolCallLine = inputFormatted !== "{}" ? `\xBB ${toolName}(${inputFormatted})` : `\xBB ${toolName}()`;
|
|
148271
148506
|
log.info(withLabel(label, toolCallLine));
|
|
@@ -148289,7 +148524,7 @@ async function runOpenCode(params) {
|
|
|
148289
148524
|
const status = event.part?.state?.status || event.status || "unknown";
|
|
148290
148525
|
const output2 = event.part?.state?.output || event.output;
|
|
148291
148526
|
const label = eventLabel(event);
|
|
148292
|
-
|
|
148527
|
+
timerFor(label).markToolResult();
|
|
148293
148528
|
if (taskDispatchByCallID.size > 0 || pendingTaskDispatches.length > 0) {
|
|
148294
148529
|
if (toolId && taskDispatchByCallID.has(toolId)) {
|
|
148295
148530
|
const dispatch = taskDispatchByCallID.get(toolId);
|
|
@@ -148414,7 +148649,7 @@ async function runOpenCode(params) {
|
|
|
148414
148649
|
const recentStderr = [];
|
|
148415
148650
|
let lastProviderError = null;
|
|
148416
148651
|
let agentErrorEvent = null;
|
|
148417
|
-
|
|
148652
|
+
const output = new TailBuffer(DEFAULT_MAX_RETAINED_BYTES);
|
|
148418
148653
|
let stdoutBuffer = "";
|
|
148419
148654
|
try {
|
|
148420
148655
|
const result = await spawn({
|
|
@@ -148432,6 +148667,11 @@ async function runOpenCode(params) {
|
|
|
148432
148667
|
// never fires — producing zombie runs. detached + killGroup nukes the
|
|
148433
148668
|
// whole tree.
|
|
148434
148669
|
killGroup: true,
|
|
148670
|
+
// we already drain every chunk via onStdout/onStderr (NDJSON parsing
|
|
148671
|
+
// + recentStderr ring buffer). retaining a second copy in the spawn
|
|
148672
|
+
// wrapper would grow unbounded for multi-lens Reviews and previously
|
|
148673
|
+
// crashed the wrapper with RangeError at ~1 GiB. see issue #680.
|
|
148674
|
+
retain: "none",
|
|
148435
148675
|
// NB: we used to pass `isPausedExternally: isSubagentInFlight` to suspend
|
|
148436
148676
|
// the activity timer during subagent dispatches. unnecessary now that
|
|
148437
148677
|
// our injected plugin (action/agents/opencodePlugin.ts) re-emits
|
|
@@ -148441,7 +148681,7 @@ async function runOpenCode(params) {
|
|
|
148441
148681
|
// (~3.3 plugin events/sec during a typical subagent run).
|
|
148442
148682
|
onStdout: async (chunk) => {
|
|
148443
148683
|
const text = chunk.toString();
|
|
148444
|
-
output
|
|
148684
|
+
output.append(text);
|
|
148445
148685
|
markActivity();
|
|
148446
148686
|
stdoutBuffer += text;
|
|
148447
148687
|
const lines = stdoutBuffer.split("\n");
|
|
@@ -148530,18 +148770,25 @@ ${stderrContext}`);
|
|
|
148530
148770
|
const usage = buildUsage();
|
|
148531
148771
|
if (result.exitCode !== 0) {
|
|
148532
148772
|
const errorContext = lastProviderError ? ` (${lastProviderError})` : "";
|
|
148533
|
-
const
|
|
148773
|
+
const stdoutSnapshot = output.toString();
|
|
148774
|
+
const stderrSnapshot = recentStderr.join("\n");
|
|
148775
|
+
const errorMessage = stderrSnapshot || stdoutSnapshot || `unknown error - no output from OpenCode CLI${errorContext}`;
|
|
148534
148776
|
log.error(
|
|
148535
148777
|
`${params.label} exited with code ${result.exitCode}${errorContext}: ${errorMessage}`
|
|
148536
148778
|
);
|
|
148537
|
-
log.debug(`stdout: ${
|
|
148538
|
-
log.debug(`stderr: ${
|
|
148539
|
-
return {
|
|
148779
|
+
log.debug(`stdout: ${stdoutSnapshot.substring(0, 500)}`);
|
|
148780
|
+
log.debug(`stderr: ${stderrSnapshot.substring(0, 500)}`);
|
|
148781
|
+
return {
|
|
148782
|
+
success: false,
|
|
148783
|
+
output: finalOutput || stdoutSnapshot,
|
|
148784
|
+
error: errorMessage,
|
|
148785
|
+
usage
|
|
148786
|
+
};
|
|
148540
148787
|
}
|
|
148541
148788
|
if (eventCount === 0 && lastProviderError) {
|
|
148542
148789
|
return {
|
|
148543
148790
|
success: false,
|
|
148544
|
-
output: finalOutput || output,
|
|
148791
|
+
output: finalOutput || output.toString(),
|
|
148545
148792
|
error: `provider error: ${lastProviderError}`,
|
|
148546
148793
|
usage
|
|
148547
148794
|
};
|
|
@@ -148552,12 +148799,12 @@ ${stderrContext}`);
|
|
|
148552
148799
|
const errorMessage = errorEvent.error?.data?.message || errorEvent.error?.name || JSON.stringify(errorEvent);
|
|
148553
148800
|
return {
|
|
148554
148801
|
success: false,
|
|
148555
|
-
output: finalOutput || output,
|
|
148802
|
+
output: finalOutput || output.toString(),
|
|
148556
148803
|
error: `${errorName}: ${errorMessage}`,
|
|
148557
148804
|
usage
|
|
148558
148805
|
};
|
|
148559
148806
|
}
|
|
148560
|
-
return { success: true, output: finalOutput || output, usage };
|
|
148807
|
+
return { success: true, output: finalOutput || output.toString(), usage };
|
|
148561
148808
|
} catch (error49) {
|
|
148562
148809
|
params.todoTracker?.cancel();
|
|
148563
148810
|
const duration4 = performance7.now() - startTime;
|
|
@@ -148576,7 +148823,7 @@ ${stderrContext}`
|
|
|
148576
148823
|
);
|
|
148577
148824
|
return {
|
|
148578
148825
|
success: false,
|
|
148579
|
-
output: finalOutput || output,
|
|
148826
|
+
output: finalOutput || output.toString(),
|
|
148580
148827
|
error: `${errorMessage} [${diagnosis}]`,
|
|
148581
148828
|
usage: buildUsage()
|
|
148582
148829
|
};
|
|
@@ -148587,7 +148834,10 @@ var opencode = agent({
|
|
|
148587
148834
|
install: installOpencodeCli,
|
|
148588
148835
|
run: async (ctx) => {
|
|
148589
148836
|
const cliPath = await installOpencodeCli();
|
|
148590
|
-
const
|
|
148837
|
+
const rawModel = ctx.payload.proxyModel ?? ctx.resolvedModel ?? autoSelectModel(cliPath);
|
|
148838
|
+
const bedrockModelId = process.env[BEDROCK_MODEL_ID_ENV]?.trim();
|
|
148839
|
+
const isBedrockRoute = rawModel !== void 0 && bedrockModelId !== void 0 && bedrockModelId === rawModel;
|
|
148840
|
+
const model = isBedrockRoute ? `amazon-bedrock/${rawModel}` : rawModel;
|
|
148591
148841
|
const homeEnv = {
|
|
148592
148842
|
HOME: ctx.tmpdir,
|
|
148593
148843
|
XDG_CONFIG_HOME: join11(ctx.tmpdir, ".config")
|
|
@@ -148616,7 +148866,6 @@ var opencode = agent({
|
|
|
148616
148866
|
...homeEnv,
|
|
148617
148867
|
OPENCODE_CONFIG_CONTENT: buildSecurityConfig(ctx, model),
|
|
148618
148868
|
OPENCODE_PERMISSION: permissionOverride,
|
|
148619
|
-
OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX: PULLFROG_OPENCODE_OUTPUT_LIMIT.toString(),
|
|
148620
148869
|
GOOGLE_GENERATIVE_AI_API_KEY: process.env.GOOGLE_GENERATIVE_AI_API_KEY || process.env.GEMINI_API_KEY
|
|
148621
148870
|
};
|
|
148622
148871
|
const repoDir = process.cwd();
|
|
@@ -148659,13 +148908,29 @@ function hasEnvVar(name) {
|
|
|
148659
148908
|
function hasClaudeCodeAuth() {
|
|
148660
148909
|
return hasEnvVar("CLAUDE_CODE_OAUTH_TOKEN") || hasEnvVar("ANTHROPIC_API_KEY");
|
|
148661
148910
|
}
|
|
148911
|
+
function hasBedrockAuth() {
|
|
148912
|
+
return hasEnvVar("AWS_BEARER_TOKEN_BEDROCK") || hasEnvVar("AWS_ACCESS_KEY_ID") && hasEnvVar("AWS_SECRET_ACCESS_KEY");
|
|
148913
|
+
}
|
|
148914
|
+
function resolveSlug(slug2) {
|
|
148915
|
+
const alias = resolveDisplayAlias(slug2);
|
|
148916
|
+
if (alias?.routing === "bedrock") {
|
|
148917
|
+
const bedrockId = process.env[BEDROCK_MODEL_ID_ENV]?.trim();
|
|
148918
|
+
if (!bedrockId) {
|
|
148919
|
+
throw new Error(
|
|
148920
|
+
`${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.`
|
|
148921
|
+
);
|
|
148922
|
+
}
|
|
148923
|
+
return bedrockId;
|
|
148924
|
+
}
|
|
148925
|
+
return resolveCliModel(slug2);
|
|
148926
|
+
}
|
|
148662
148927
|
function resolveModel(ctx) {
|
|
148663
148928
|
const envModel = process.env.PULLFROG_MODEL?.trim();
|
|
148664
148929
|
if (envModel) {
|
|
148665
|
-
return
|
|
148930
|
+
return resolveSlug(envModel) ?? envModel;
|
|
148666
148931
|
}
|
|
148667
148932
|
if (ctx.slug) {
|
|
148668
|
-
const resolved =
|
|
148933
|
+
const resolved = resolveSlug(ctx.slug);
|
|
148669
148934
|
if (resolved) {
|
|
148670
148935
|
return resolved;
|
|
148671
148936
|
}
|
|
@@ -148681,6 +148946,9 @@ function resolveAgent(ctx) {
|
|
|
148681
148946
|
}
|
|
148682
148947
|
log.warning(`\xBB unknown PULLFROG_AGENT="${envAgent}" \u2014 falling through to auto-select`);
|
|
148683
148948
|
}
|
|
148949
|
+
if (ctx.model && hasBedrockAuth() && process.env[BEDROCK_MODEL_ID_ENV]?.trim() === ctx.model) {
|
|
148950
|
+
return isBedrockAnthropicId(ctx.model) ? agents.claude : agents.opencode;
|
|
148951
|
+
}
|
|
148684
148952
|
if (ctx.model) {
|
|
148685
148953
|
try {
|
|
148686
148954
|
const provider2 = getModelProvider(ctx.model);
|
|
@@ -148695,31 +148963,56 @@ function resolveAgent(ctx) {
|
|
|
148695
148963
|
|
|
148696
148964
|
// utils/apiKeys.ts
|
|
148697
148965
|
var knownApiKeys = new Set(Object.values(providers).flatMap((p2) => [...p2.envVars]));
|
|
148966
|
+
var MISSING_KEY_MARKER = "no API key found";
|
|
148698
148967
|
function buildMissingApiKeyError(params) {
|
|
148699
|
-
const
|
|
148700
|
-
const settingsUrl = `${
|
|
148701
|
-
|
|
148702
|
-
|
|
148703
|
-
|
|
148968
|
+
const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
|
|
148969
|
+
const settingsUrl = `${getApiUrl()}/console/${params.owner}/${params.name}`;
|
|
148970
|
+
return [
|
|
148971
|
+
`**${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.`,
|
|
148972
|
+
"",
|
|
148973
|
+
`[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)`
|
|
148974
|
+
].join("\n");
|
|
148975
|
+
}
|
|
148976
|
+
function buildBedrockSetupError(params) {
|
|
148977
|
+
const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
|
|
148978
|
+
return `Bedrock model selected but required configuration is missing: ${params.missing.join(", ")}.
|
|
148704
148979
|
|
|
148705
|
-
|
|
148980
|
+
add the missing secret(s) to your GitHub repository at ${githubSecretsUrl}, then reference them in your workflow's \`env:\` block:
|
|
148706
148981
|
|
|
148707
|
-
|
|
148708
|
-
|
|
148709
|
-
|
|
148710
|
-
4. set the value to your API key
|
|
148711
|
-
5. click "Add secret"
|
|
148982
|
+
AWS_BEARER_TOKEN_BEDROCK: \${{ secrets.AWS_BEARER_TOKEN_BEDROCK }}
|
|
148983
|
+
AWS_REGION: \${{ secrets.AWS_REGION }}
|
|
148984
|
+
${BEDROCK_MODEL_ID_ENV}: \${{ secrets.${BEDROCK_MODEL_ID_ENV} }}
|
|
148712
148985
|
|
|
148713
|
-
|
|
148986
|
+
\`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.
|
|
148714
148987
|
|
|
148715
|
-
for full setup instructions, see https://docs.pullfrog.com/
|
|
148988
|
+
for full setup instructions, see https://docs.pullfrog.com/bedrock`;
|
|
148716
148989
|
}
|
|
148717
148990
|
function hasEnvVar2(name) {
|
|
148718
148991
|
const value2 = process.env[name];
|
|
148719
148992
|
return typeof value2 === "string" && value2.length > 0;
|
|
148720
148993
|
}
|
|
148994
|
+
function validateBedrockSetup(params) {
|
|
148995
|
+
const hasAuth = hasEnvVar2("AWS_BEARER_TOKEN_BEDROCK") || hasEnvVar2("AWS_ACCESS_KEY_ID") && hasEnvVar2("AWS_SECRET_ACCESS_KEY");
|
|
148996
|
+
const missing = [];
|
|
148997
|
+
if (!hasAuth)
|
|
148998
|
+
missing.push("AWS_BEARER_TOKEN_BEDROCK (or AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY)");
|
|
148999
|
+
if (!hasEnvVar2("AWS_REGION")) missing.push("AWS_REGION");
|
|
149000
|
+
if (!hasEnvVar2(BEDROCK_MODEL_ID_ENV)) missing.push(BEDROCK_MODEL_ID_ENV);
|
|
149001
|
+
if (missing.length > 0) {
|
|
149002
|
+
throw new Error(buildBedrockSetupError({ owner: params.owner, name: params.name, missing }));
|
|
149003
|
+
}
|
|
149004
|
+
}
|
|
148721
149005
|
function validateAgentApiKey(params) {
|
|
148722
149006
|
if (params.model) {
|
|
149007
|
+
const alias = resolveDisplayAlias(params.model);
|
|
149008
|
+
if (alias?.routing === "bedrock") {
|
|
149009
|
+
validateBedrockSetup({ owner: params.owner, name: params.name });
|
|
149010
|
+
return;
|
|
149011
|
+
}
|
|
149012
|
+
if (!params.model.includes("/")) {
|
|
149013
|
+
validateBedrockSetup({ owner: params.owner, name: params.name });
|
|
149014
|
+
return;
|
|
149015
|
+
}
|
|
148723
149016
|
const requiredVars = getModelEnvVars(params.model);
|
|
148724
149017
|
if (requiredVars.length === 0) return;
|
|
148725
149018
|
if (requiredVars.some((v) => hasEnvVar2(v))) return;
|
|
@@ -148730,6 +149023,22 @@ function validateAgentApiKey(params) {
|
|
|
148730
149023
|
throw new Error(buildMissingApiKeyError({ owner: params.owner, name: params.name }));
|
|
148731
149024
|
}
|
|
148732
149025
|
}
|
|
149026
|
+
function isApiKeyAuthError(text) {
|
|
149027
|
+
if (!text) return false;
|
|
149028
|
+
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);
|
|
149029
|
+
}
|
|
149030
|
+
function formatApiKeyErrorSummary(params) {
|
|
149031
|
+
if (params.raw.includes(MISSING_KEY_MARKER)) {
|
|
149032
|
+
return buildMissingApiKeyError({ owner: params.owner, name: params.name });
|
|
149033
|
+
}
|
|
149034
|
+
const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
|
|
149035
|
+
const settingsUrl = `${getApiUrl()}/console/${params.owner}/${params.name}`;
|
|
149036
|
+
return [
|
|
149037
|
+
`**Your LLM provider API key was rejected (401).** Rotate the key in your provider dashboard, then update the matching GitHub Actions secret.`,
|
|
149038
|
+
"",
|
|
149039
|
+
`[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)`
|
|
149040
|
+
].join("\n");
|
|
149041
|
+
}
|
|
148733
149042
|
|
|
148734
149043
|
// utils/body.ts
|
|
148735
149044
|
var import_turndown = __toESM(require_turndown_cjs(), 1);
|
|
@@ -152482,6 +152791,14 @@ function isOIDCAvailable() {
|
|
|
152482
152791
|
process.env.ACTIONS_ID_TOKEN_REQUEST_URL && process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN
|
|
152483
152792
|
);
|
|
152484
152793
|
}
|
|
152794
|
+
var TokenExchangeError = class extends Error {
|
|
152795
|
+
status;
|
|
152796
|
+
constructor(status, message) {
|
|
152797
|
+
super(message);
|
|
152798
|
+
this.name = "TokenExchangeError";
|
|
152799
|
+
this.status = status;
|
|
152800
|
+
}
|
|
152801
|
+
};
|
|
152485
152802
|
async function acquireTokenViaOIDC(opts) {
|
|
152486
152803
|
const oidcToken = await core2.getIDToken("pullfrog-api");
|
|
152487
152804
|
const repos = [...opts?.repos ?? []];
|
|
@@ -152506,7 +152823,16 @@ async function acquireTokenViaOIDC(opts) {
|
|
|
152506
152823
|
});
|
|
152507
152824
|
clearTimeout(timeoutId);
|
|
152508
152825
|
if (!tokenResponse.ok) {
|
|
152509
|
-
|
|
152826
|
+
let serverMessage;
|
|
152827
|
+
try {
|
|
152828
|
+
const body = await tokenResponse.json();
|
|
152829
|
+
if (typeof body.error === "string") serverMessage = body.error;
|
|
152830
|
+
} catch {
|
|
152831
|
+
}
|
|
152832
|
+
throw new TokenExchangeError(
|
|
152833
|
+
tokenResponse.status,
|
|
152834
|
+
serverMessage ?? `Token exchange failed: ${tokenResponse.status} ${tokenResponse.statusText}`
|
|
152835
|
+
);
|
|
152510
152836
|
}
|
|
152511
152837
|
const tokenData = await tokenResponse.json();
|
|
152512
152838
|
return tokenData.token;
|
|
@@ -152627,7 +152953,10 @@ async function acquireNewToken(opts) {
|
|
|
152627
152953
|
if (isOIDCAvailable()) {
|
|
152628
152954
|
return await retry(() => acquireTokenViaOIDC(opts), {
|
|
152629
152955
|
label: "token exchange",
|
|
152630
|
-
shouldRetry: (error49) =>
|
|
152956
|
+
shouldRetry: (error49) => {
|
|
152957
|
+
if (error49 instanceof TokenExchangeError) return error49.status >= 500 || error49.status === 429;
|
|
152958
|
+
return error49 instanceof Error && (error49.message.includes("timed out") || error49.message.includes("fetch failed") || error49.message.includes("ECONNRESET") || error49.message.includes("ETIMEDOUT"));
|
|
152959
|
+
}
|
|
152631
152960
|
});
|
|
152632
152961
|
} else {
|
|
152633
152962
|
return await acquireTokenViaGitHubApp(opts);
|
|
@@ -153174,6 +153503,21 @@ ${getStandaloneModeInstructions(ctx.payload.event.trigger, t2, ctx.outputSchema)
|
|
|
153174
153503
|
|
|
153175
153504
|
Trust the tools \u2014 do not repeatedly verify file contents or git status after operations. If a tool reports success, proceed to the next step. Only verify if you encounter an actual error. Exception: right before \`${t2("push_branch")}\`, ensure the working tree is clean \u2014 that tool rejects dirty trees, and tests you ran earlier often leave untracked output.
|
|
153176
153505
|
|
|
153506
|
+
### Parallel tool execution
|
|
153507
|
+
|
|
153508
|
+
For maximum efficiency, whenever you need to perform multiple independent operations, invoke all relevant tools simultaneously in a single assistant turn rather than sequentially. The dominant failure mode is grep \u2192 read \u2192 read \u2192 read \u2192 read across separate turns when one round trip would do. Always parallelize when calls are independent:
|
|
153509
|
+
- reading multiple files (especially after a grep returns candidates)
|
|
153510
|
+
- multiple greps with different patterns
|
|
153511
|
+
- glob + grep + read combos
|
|
153512
|
+
- listing multiple directories
|
|
153513
|
+
- inspecting multiple MCP tools or resources
|
|
153514
|
+
|
|
153515
|
+
Do NOT parallelize operations that depend on prior output (e.g. create a file then read it), or ordered stateful mutations. Edits are not parallelizable \u2014 sequence those normally.${ctx.agentId === "opencode" ? `
|
|
153516
|
+
|
|
153517
|
+
On OpenCode you also have a \`batch\` tool that bundles 1-25 independent calls into one wrapper call. Reach for it whenever you have >=2 independent calls. Native parallel tool_use and \`batch\` both achieve one round trip instead of N \u2014 use whichever your provider supports best.` : `
|
|
153518
|
+
|
|
153519
|
+
Emit multiple \`tool_use\` blocks in the same assistant message for independent calls \u2014 the runtime executes them concurrently. Do not wait for one tool result before issuing the next independent call.`}
|
|
153520
|
+
|
|
153177
153521
|
### Command execution
|
|
153178
153522
|
|
|
153179
153523
|
Never use \`sleep\` to wait for commands to complete. Commands run synchronously \u2014 when the shell tool returns, the command has finished.
|
|
@@ -153219,10 +153563,31 @@ function buildPromptContext(ctx) {
|
|
|
153219
153563
|
userQuoted: user ? user.split("\n").map((line) => `> ${line}`).join("\n") : ""
|
|
153220
153564
|
};
|
|
153221
153565
|
}
|
|
153222
|
-
function
|
|
153223
|
-
|
|
153566
|
+
function renderLearningsToc(headings) {
|
|
153567
|
+
if (headings.length === 0) return "";
|
|
153568
|
+
const rootDepth = Math.min(...headings.map((h) => h.depth));
|
|
153569
|
+
return headings.map((h) => {
|
|
153570
|
+
const indent2 = " ".repeat((h.depth - rootDepth) * 2);
|
|
153571
|
+
return `${indent2}- ${h.title} (L${h.startLine}-L${h.endLine})`;
|
|
153572
|
+
}).join("\n");
|
|
153573
|
+
}
|
|
153574
|
+
function buildLearningsSection(ctx) {
|
|
153575
|
+
if (!ctx.filePath) return "";
|
|
153576
|
+
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).`;
|
|
153577
|
+
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.
|
|
153578
|
+
|
|
153579
|
+
${renderLearningsToc(ctx.headings)}`;
|
|
153580
|
+
return `************* LEARNINGS *************
|
|
153581
|
+
|
|
153582
|
+
${intro}
|
|
153224
153583
|
|
|
153225
|
-
|
|
153584
|
+
${tocBody}`;
|
|
153585
|
+
}
|
|
153586
|
+
function assembleFullPrompt(ctx) {
|
|
153587
|
+
const learningsSection = buildLearningsSection({
|
|
153588
|
+
filePath: ctx.learningsFilePath,
|
|
153589
|
+
headings: ctx.learningsHeadings
|
|
153590
|
+
});
|
|
153226
153591
|
const runtimeSection = `************* RUNTIME *************
|
|
153227
153592
|
|
|
153228
153593
|
${ctx.runtime}`;
|
|
@@ -153250,7 +153615,10 @@ function resolveInstructions(ctx) {
|
|
|
153250
153615
|
tocEntries.push({ label: "EVENT CONTEXT", description: "related PR/issue data" });
|
|
153251
153616
|
tocEntries.push({ label: "SYSTEM", description: "persona, security, tools, workflow rules" });
|
|
153252
153617
|
if (pctx.learningsFilePath)
|
|
153253
|
-
tocEntries.push({
|
|
153618
|
+
tocEntries.push({
|
|
153619
|
+
label: "LEARNINGS",
|
|
153620
|
+
description: "repo-specific knowledge file path + heading TOC"
|
|
153621
|
+
});
|
|
153254
153622
|
tocEntries.push({ label: "RUNTIME", description: "environment metadata" });
|
|
153255
153623
|
const toc = buildToc(tocEntries);
|
|
153256
153624
|
const full = assembleFullPrompt({
|
|
@@ -153260,6 +153628,7 @@ function resolveInstructions(ctx) {
|
|
|
153260
153628
|
eventContext,
|
|
153261
153629
|
system,
|
|
153262
153630
|
learningsFilePath: pctx.learningsFilePath,
|
|
153631
|
+
learningsHeadings: pctx.learningsHeadings,
|
|
153263
153632
|
runtime: pctx.runtime
|
|
153264
153633
|
});
|
|
153265
153634
|
const event = [pctx.eventTitle, pctx.eventMetadata].filter(Boolean).join("\n\n---\n\n");
|
|
@@ -153277,7 +153646,7 @@ function resolveInstructions(ctx) {
|
|
|
153277
153646
|
import { mkdir, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
153278
153647
|
import { dirname as dirname4, join as join14 } from "node:path";
|
|
153279
153648
|
var LEARNINGS_FILE_NAME = "pullfrog-learnings.md";
|
|
153280
|
-
var MAX_LEARNINGS_LENGTH =
|
|
153649
|
+
var MAX_LEARNINGS_LENGTH = 1e5;
|
|
153281
153650
|
function learningsFilePath(tmpdir3) {
|
|
153282
153651
|
return join14(tmpdir3, LEARNINGS_FILE_NAME);
|
|
153283
153652
|
}
|
|
@@ -153287,6 +153656,15 @@ async function seedLearningsFile(params) {
|
|
|
153287
153656
|
await writeFile2(path3, params.current ?? "", "utf8");
|
|
153288
153657
|
return path3;
|
|
153289
153658
|
}
|
|
153659
|
+
var TRUNCATION_LINE_BOUNDARY_TOLERANCE = 4096;
|
|
153660
|
+
function truncateAtLineBoundary(body, cap) {
|
|
153661
|
+
if (body.length <= cap) return body;
|
|
153662
|
+
const head = body.slice(0, cap);
|
|
153663
|
+
const lastNewline = head.lastIndexOf("\n");
|
|
153664
|
+
if (lastNewline <= 0) return head;
|
|
153665
|
+
if (cap - lastNewline > TRUNCATION_LINE_BOUNDARY_TOLERANCE) return head;
|
|
153666
|
+
return head.slice(0, lastNewline);
|
|
153667
|
+
}
|
|
153290
153668
|
async function readLearningsFile(path3) {
|
|
153291
153669
|
let raw2;
|
|
153292
153670
|
try {
|
|
@@ -153294,16 +153672,26 @@ async function readLearningsFile(path3) {
|
|
|
153294
153672
|
} catch {
|
|
153295
153673
|
return null;
|
|
153296
153674
|
}
|
|
153297
|
-
|
|
153298
|
-
if (trimmed.length > MAX_LEARNINGS_LENGTH) return trimmed.slice(0, MAX_LEARNINGS_LENGTH);
|
|
153299
|
-
return trimmed;
|
|
153675
|
+
return truncateAtLineBoundary(raw2.trim(), MAX_LEARNINGS_LENGTH);
|
|
153300
153676
|
}
|
|
153301
153677
|
|
|
153302
153678
|
// utils/normalizeEnv.ts
|
|
153303
|
-
|
|
153304
|
-
|
|
153305
|
-
|
|
153679
|
+
var core4 = __toESM(require_core(), 1);
|
|
153680
|
+
function sanitizeSecret(key, value2) {
|
|
153681
|
+
const trimmed = value2.trim();
|
|
153682
|
+
if (trimmed.length === 0) {
|
|
153683
|
+
log.warning(
|
|
153684
|
+
`\xBB ${key} is whitespace-only \u2014 leaving env var unchanged. check your secret value.`
|
|
153685
|
+
);
|
|
153686
|
+
return null;
|
|
153306
153687
|
}
|
|
153688
|
+
if (trimmed !== value2) {
|
|
153689
|
+
log.warning(
|
|
153690
|
+
`\xBB stripped whitespace from ${key} (whitespace in secret values breaks GitHub Actions log masking)`
|
|
153691
|
+
);
|
|
153692
|
+
}
|
|
153693
|
+
core4.setSecret(trimmed);
|
|
153694
|
+
return trimmed;
|
|
153307
153695
|
}
|
|
153308
153696
|
function normalizeEnv() {
|
|
153309
153697
|
const upperKeys = /* @__PURE__ */ new Map();
|
|
@@ -153314,11 +153702,6 @@ function normalizeEnv() {
|
|
|
153314
153702
|
upperKeys.set(upper2, existing);
|
|
153315
153703
|
}
|
|
153316
153704
|
for (const [upperKey, keys] of upperKeys) {
|
|
153317
|
-
if (isSensitiveEnvName(upperKey)) {
|
|
153318
|
-
for (const key of keys) {
|
|
153319
|
-
maskValue(process.env[key]);
|
|
153320
|
-
}
|
|
153321
|
-
}
|
|
153322
153705
|
if (keys.length === 1) {
|
|
153323
153706
|
const key = keys[0];
|
|
153324
153707
|
if (key !== upperKey) {
|
|
@@ -153341,10 +153724,17 @@ function normalizeEnv() {
|
|
|
153341
153724
|
}
|
|
153342
153725
|
process.env[upperKey] = preferredValue;
|
|
153343
153726
|
}
|
|
153727
|
+
for (const key of Object.keys(process.env)) {
|
|
153728
|
+
if (!isSensitiveEnvName(key)) continue;
|
|
153729
|
+
const value2 = process.env[key];
|
|
153730
|
+
if (typeof value2 !== "string" || value2.length === 0) continue;
|
|
153731
|
+
const sanitized = sanitizeSecret(key, value2);
|
|
153732
|
+
if (sanitized !== null) process.env[key] = sanitized;
|
|
153733
|
+
}
|
|
153344
153734
|
}
|
|
153345
153735
|
|
|
153346
153736
|
// utils/payload.ts
|
|
153347
|
-
var
|
|
153737
|
+
var core5 = __toESM(require_core(), 1);
|
|
153348
153738
|
import { isAbsolute as isAbsolute2, resolve as resolve2 } from "node:path";
|
|
153349
153739
|
|
|
153350
153740
|
// utils/versioning.ts
|
|
@@ -153408,7 +153798,7 @@ function resolveCwd(cwd) {
|
|
|
153408
153798
|
return workspace ? resolve2(workspace, cwd) : cwd;
|
|
153409
153799
|
}
|
|
153410
153800
|
function resolvePromptInput() {
|
|
153411
|
-
const prompt =
|
|
153801
|
+
const prompt = core5.getInput("prompt", { required: true });
|
|
153412
153802
|
let parsed2;
|
|
153413
153803
|
try {
|
|
153414
153804
|
parsed2 = JSON.parse(prompt);
|
|
@@ -153424,11 +153814,11 @@ function resolvePromptInput() {
|
|
|
153424
153814
|
}
|
|
153425
153815
|
function resolveNonPromptInputs() {
|
|
153426
153816
|
return Inputs.omit("prompt").assert({
|
|
153427
|
-
model:
|
|
153428
|
-
timeout:
|
|
153429
|
-
cwd:
|
|
153430
|
-
push:
|
|
153431
|
-
shell:
|
|
153817
|
+
model: core5.getInput("model") || void 0,
|
|
153818
|
+
timeout: core5.getInput("timeout") || void 0,
|
|
153819
|
+
cwd: core5.getInput("cwd") || void 0,
|
|
153820
|
+
push: core5.getInput("push") || void 0,
|
|
153821
|
+
shell: core5.getInput("shell") || void 0
|
|
153432
153822
|
});
|
|
153433
153823
|
}
|
|
153434
153824
|
var isPullfrog = (actor) => {
|
|
@@ -153629,6 +154019,7 @@ var defaultSettings = {
|
|
|
153629
154019
|
prApproveEnabled: false,
|
|
153630
154020
|
modeInstructions: {},
|
|
153631
154021
|
learnings: null,
|
|
154022
|
+
learningsHeadings: [],
|
|
153632
154023
|
envAllowlist: null
|
|
153633
154024
|
};
|
|
153634
154025
|
var defaultRunContext = {
|
|
@@ -153669,7 +154060,8 @@ async function fetchRunContext(params) {
|
|
|
153669
154060
|
setupScript: data.settings?.setupScript ?? null,
|
|
153670
154061
|
postCheckoutScript: data.settings?.postCheckoutScript ?? null,
|
|
153671
154062
|
prepushScript: data.settings?.prepushScript ?? null,
|
|
153672
|
-
stopScript: data.settings?.stopScript ?? null
|
|
154063
|
+
stopScript: data.settings?.stopScript ?? null,
|
|
154064
|
+
learningsHeadings: data.settings?.learningsHeadings ?? []
|
|
153673
154065
|
},
|
|
153674
154066
|
apiToken: data.apiToken,
|
|
153675
154067
|
oss: data.oss ?? false,
|
|
@@ -153684,13 +154076,13 @@ async function fetchRunContext(params) {
|
|
|
153684
154076
|
}
|
|
153685
154077
|
|
|
153686
154078
|
// utils/runContextData.ts
|
|
153687
|
-
var
|
|
154079
|
+
var core6 = __toESM(require_core(), 1);
|
|
153688
154080
|
async function resolveRunContextData(params) {
|
|
153689
154081
|
log.info(`\xBB running Pullfrog v${package_default.version}...`);
|
|
153690
154082
|
const repoContext = parseRepoContext();
|
|
153691
154083
|
let oidcToken;
|
|
153692
154084
|
try {
|
|
153693
|
-
oidcToken = await
|
|
154085
|
+
oidcToken = await core6.getIDToken("pullfrog-api");
|
|
153694
154086
|
} catch {
|
|
153695
154087
|
}
|
|
153696
154088
|
const [repoResponse, runContext] = await Promise.all([
|
|
@@ -153993,7 +154385,7 @@ async function resolveRun(params) {
|
|
|
153993
154385
|
|
|
153994
154386
|
// main.ts
|
|
153995
154387
|
function resolveOutputSchema() {
|
|
153996
|
-
const raw2 =
|
|
154388
|
+
const raw2 = core7.getInput("output_schema");
|
|
153997
154389
|
if (!raw2) return void 0;
|
|
153998
154390
|
let parsed2;
|
|
153999
154391
|
try {
|
|
@@ -154161,7 +154553,7 @@ async function buildProxyTokenHeaders(ctx) {
|
|
|
154161
154553
|
if (ctx.oidcCredentials) {
|
|
154162
154554
|
process.env.ACTIONS_ID_TOKEN_REQUEST_URL = ctx.oidcCredentials.requestUrl;
|
|
154163
154555
|
process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = ctx.oidcCredentials.requestToken;
|
|
154164
|
-
const oidcToken = await
|
|
154556
|
+
const oidcToken = await core7.getIDToken("pullfrog-api");
|
|
154165
154557
|
delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
154166
154558
|
delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
154167
154559
|
return { Authorization: `Bearer ${oidcToken}` };
|
|
@@ -154183,7 +154575,7 @@ async function resolveProxyModel(ctx) {
|
|
|
154183
154575
|
const key = await mintProxyKey({ oidcCredentials: ctx.oidcCredentials, repo: ctx.repo });
|
|
154184
154576
|
if (!key) return;
|
|
154185
154577
|
process.env.OPENROUTER_API_KEY = key;
|
|
154186
|
-
|
|
154578
|
+
core7.setSecret(key);
|
|
154187
154579
|
ctx.payload.proxyModel = ctx.proxyModel;
|
|
154188
154580
|
const label = ctx.oss ? "oss" : "router";
|
|
154189
154581
|
log.info(`\xBB proxy: ${label} \u2192 ${ctx.proxyModel}`);
|
|
@@ -154295,8 +154687,8 @@ async function main() {
|
|
|
154295
154687
|
if (runContext.dbSecrets) {
|
|
154296
154688
|
for (const [key, value2] of Object.entries(runContext.dbSecrets)) {
|
|
154297
154689
|
if (!process.env[key]) {
|
|
154298
|
-
|
|
154299
|
-
|
|
154690
|
+
const sanitized = sanitizeSecret(key, value2);
|
|
154691
|
+
if (sanitized !== null) process.env[key] = sanitized;
|
|
154300
154692
|
}
|
|
154301
154693
|
}
|
|
154302
154694
|
const count = Object.keys(runContext.dbSecrets).length;
|
|
@@ -154435,10 +154827,7 @@ async function main() {
|
|
|
154435
154827
|
current: runContext.repoSettings.learnings
|
|
154436
154828
|
});
|
|
154437
154829
|
toolState.learningsFilePath = learningsPath;
|
|
154438
|
-
|
|
154439
|
-
toolState.learningsSeed = await readFile4(learningsPath, "utf8");
|
|
154440
|
-
} catch {
|
|
154441
|
-
}
|
|
154830
|
+
toolState.learningsSeed = (runContext.repoSettings.learnings ?? "").trim();
|
|
154442
154831
|
log.info(
|
|
154443
154832
|
`\xBB learnings seeded at ${learningsPath} (existing=${runContext.repoSettings.learnings ? "yes" : "no"})`
|
|
154444
154833
|
);
|
|
@@ -154478,7 +154867,8 @@ async function main() {
|
|
|
154478
154867
|
modes: modes2,
|
|
154479
154868
|
agentId,
|
|
154480
154869
|
outputSchema,
|
|
154481
|
-
learningsFilePath: toolState.learningsFilePath ?? null
|
|
154870
|
+
learningsFilePath: toolState.learningsFilePath ?? null,
|
|
154871
|
+
learningsHeadings: runContext.repoSettings.learningsHeadings
|
|
154482
154872
|
});
|
|
154483
154873
|
const logParts = [
|
|
154484
154874
|
instructions.eventInstructions ? `EVENT-LEVEL INSTRUCTIONS:
|
|
@@ -154615,10 +155005,13 @@ ${instructions.user}` : null,
|
|
|
154615
155005
|
await persistLearnings(toolContext);
|
|
154616
155006
|
}
|
|
154617
155007
|
if (!result.success && toolContext && toolState.progressComment) {
|
|
154618
|
-
|
|
154619
|
-
|
|
154620
|
-
|
|
154621
|
-
|
|
155008
|
+
const rawError = result.error || "agent run failed";
|
|
155009
|
+
const errorBody = isApiKeyAuthError(rawError) ? formatApiKeyErrorSummary({
|
|
155010
|
+
owner: runContext.repo.owner,
|
|
155011
|
+
name: runContext.repo.name,
|
|
155012
|
+
raw: rawError
|
|
155013
|
+
}) : rawError;
|
|
155014
|
+
await reportErrorToComment({ toolState, error: errorBody }).catch((error49) => {
|
|
154622
155015
|
log.debug(`failure error report failed: ${error49}`);
|
|
154623
155016
|
});
|
|
154624
155017
|
}
|
|
@@ -154634,7 +155027,7 @@ ${instructions.user}` : null,
|
|
|
154634
155027
|
}
|
|
154635
155028
|
if (toolState.output) {
|
|
154636
155029
|
log.info(`::pullfrog-output::${Buffer.from(toolState.output).toString("base64")}`);
|
|
154637
|
-
|
|
155030
|
+
core7.setOutput("result", toolState.output);
|
|
154638
155031
|
}
|
|
154639
155032
|
return await handleAgentResult({
|
|
154640
155033
|
result,
|
|
@@ -154654,8 +155047,13 @@ ${instructions.user}` : null,
|
|
|
154654
155047
|
killTrackedChildren();
|
|
154655
155048
|
log.error(errorMessage);
|
|
154656
155049
|
const billingError = isRouterKeylimitExhaustedError(errorMessage) ? new BillingError(errorMessage, { code: "router_keylimit_exhausted" }) : null;
|
|
155050
|
+
const apiKeyErrorSummary = !billingError && isApiKeyAuthError(errorMessage) ? formatApiKeyErrorSummary({
|
|
155051
|
+
owner: runContext.repo.owner,
|
|
155052
|
+
name: runContext.repo.name,
|
|
155053
|
+
raw: errorMessage
|
|
155054
|
+
}) : null;
|
|
154657
155055
|
try {
|
|
154658
|
-
const errorSummary = billingError ? formatBillingErrorSummary(billingError, runContext.repo.owner) : `### \u274C Pullfrog failed
|
|
155056
|
+
const errorSummary = billingError ? formatBillingErrorSummary(billingError, runContext.repo.owner) : apiKeyErrorSummary ?? `### \u274C Pullfrog failed
|
|
154659
155057
|
|
|
154660
155058
|
\`\`\`
|
|
154661
155059
|
${errorMessage}
|
|
@@ -154666,7 +155064,7 @@ ${errorMessage}
|
|
|
154666
155064
|
} catch {
|
|
154667
155065
|
}
|
|
154668
155066
|
try {
|
|
154669
|
-
const commentBody = billingError ? formatBillingErrorSummary(billingError, runContext.repo.owner) : errorMessage;
|
|
155067
|
+
const commentBody = billingError ? formatBillingErrorSummary(billingError, runContext.repo.owner) : apiKeyErrorSummary ?? errorMessage;
|
|
154670
155068
|
await reportErrorToComment({ toolState, error: commentBody });
|
|
154671
155069
|
} catch {
|
|
154672
155070
|
}
|
|
@@ -154723,27 +155121,27 @@ async function runMain() {
|
|
|
154723
155121
|
}
|
|
154724
155122
|
} catch (error49) {
|
|
154725
155123
|
const errorMessage = error49 instanceof Error ? error49.message : "unknown error occurred";
|
|
154726
|
-
|
|
155124
|
+
core8.setFailed(`action failed: ${errorMessage}`);
|
|
154727
155125
|
}
|
|
154728
155126
|
}
|
|
154729
155127
|
async function tokenMain() {
|
|
154730
|
-
const reposInput =
|
|
155128
|
+
const reposInput = core8.getInput("repos");
|
|
154731
155129
|
const additionalRepos = reposInput ? reposInput.split(",").map((r) => r.trim()).filter(Boolean) : [];
|
|
154732
155130
|
const token = await acquireNewToken({ repos: additionalRepos });
|
|
154733
|
-
|
|
154734
|
-
|
|
154735
|
-
|
|
155131
|
+
core8.setSecret(token);
|
|
155132
|
+
core8.saveState(STATE_TOKEN, token);
|
|
155133
|
+
core8.setOutput("token", token);
|
|
154736
155134
|
const scope2 = additionalRepos.length ? `current repo + ${additionalRepos.join(", ")}` : "current repo only";
|
|
154737
|
-
|
|
155135
|
+
core8.info(`\xBB installation token acquired (${scope2})`);
|
|
154738
155136
|
}
|
|
154739
155137
|
async function tokenPost() {
|
|
154740
|
-
const token =
|
|
155138
|
+
const token = core8.getState(STATE_TOKEN);
|
|
154741
155139
|
if (!token) {
|
|
154742
|
-
|
|
155140
|
+
core8.debug("no token found in state, skipping revocation");
|
|
154743
155141
|
return;
|
|
154744
155142
|
}
|
|
154745
155143
|
await revokeGitHubInstallationToken(token);
|
|
154746
|
-
|
|
155144
|
+
core8.info("\xBB installation token revoked");
|
|
154747
155145
|
}
|
|
154748
155146
|
function printGhaUsage(params) {
|
|
154749
155147
|
params.stream(`usage: ${params.prog} gha [subcommand]
|
|
@@ -154859,7 +155257,7 @@ async function run(args2) {
|
|
|
154859
155257
|
}
|
|
154860
155258
|
} catch (error49) {
|
|
154861
155259
|
const message = error49 instanceof Error ? error49.message : String(error49);
|
|
154862
|
-
|
|
155260
|
+
core8.setFailed(message);
|
|
154863
155261
|
}
|
|
154864
155262
|
}
|
|
154865
155263
|
|
|
@@ -155809,8 +156207,10 @@ function link(text, url4) {
|
|
|
155809
156207
|
return `\x1B]8;;${url4}\x07${text}\x1B]8;;\x07`;
|
|
155810
156208
|
}
|
|
155811
156209
|
function buildProviders() {
|
|
155812
|
-
return Object.entries(providers).filter(([key]) => key !== "opencode" && key !== "openrouter").map(([key, config3]) => {
|
|
155813
|
-
const aliases = modelAliases.filter(
|
|
156210
|
+
return Object.entries(providers).filter(([key]) => key !== "opencode" && key !== "openrouter" && key !== "bedrock").map(([key, config3]) => {
|
|
156211
|
+
const aliases = modelAliases.filter(
|
|
156212
|
+
(a) => a.provider === key && !a.fallback && !a.routing && !a.hidden
|
|
156213
|
+
);
|
|
155814
156214
|
const recommended = aliases.find((a) => a.preferred);
|
|
155815
156215
|
const sorted = [...aliases].sort((a, b) => {
|
|
155816
156216
|
if (a.preferred && !b.preferred) return -1;
|
|
@@ -156523,7 +156923,7 @@ async function run2() {
|
|
|
156523
156923
|
}
|
|
156524
156924
|
|
|
156525
156925
|
// cli.ts
|
|
156526
|
-
var VERSION10 = "0.1.
|
|
156926
|
+
var VERSION10 = "0.1.7";
|
|
156527
156927
|
var bin = basename2(process.argv[1] || "");
|
|
156528
156928
|
var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
|
|
156529
156929
|
var rawArgs = process.argv.slice(2);
|