pullfrog 0.1.1 → 0.1.3
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/claude.d.ts +19 -0
- package/dist/agents/opencodePlugin.d.ts +60 -0
- package/dist/agents/postRun.d.ts +35 -30
- package/dist/agents/shared.d.ts +26 -13
- package/dist/cli.mjs +678 -241
- package/dist/index.js +675 -238
- package/dist/internal.js +89 -67
- package/dist/mcp/comment.d.ts +35 -0
- package/dist/mcp/review.d.ts +2 -4
- package/dist/mcp/server.d.ts +1 -68
- package/dist/modes.d.ts +10 -0
- package/dist/toolState.d.ts +109 -0
- package/dist/utils/apiUrl.d.ts +8 -0
- package/dist/utils/browser.d.ts +1 -1
- package/dist/utils/errorReport.d.ts +1 -1
- package/dist/utils/instructions.d.ts +4 -1
- package/dist/utils/learnings.d.ts +31 -0
- package/dist/utils/run.d.ts +1 -1
- package/dist/utils/setup.d.ts +1 -1
- package/dist/utils/subprocess.d.ts +0 -1
- package/package.json +1 -1
- package/dist/mcp/learnings.d.ts +0 -6
package/dist/cli.mjs
CHANGED
|
@@ -18415,7 +18415,7 @@ var require_summary = __commonJS({
|
|
|
18415
18415
|
exports.summary = exports.markdownSummary = exports.SUMMARY_DOCS_URL = exports.SUMMARY_ENV_VAR = void 0;
|
|
18416
18416
|
var os_1 = __require("os");
|
|
18417
18417
|
var fs_1 = __require("fs");
|
|
18418
|
-
var { access, appendFile, writeFile:
|
|
18418
|
+
var { access, appendFile, writeFile: writeFile4 } = fs_1.promises;
|
|
18419
18419
|
exports.SUMMARY_ENV_VAR = "GITHUB_STEP_SUMMARY";
|
|
18420
18420
|
exports.SUMMARY_DOCS_URL = "https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary";
|
|
18421
18421
|
var Summary = class {
|
|
@@ -18473,7 +18473,7 @@ var require_summary = __commonJS({
|
|
|
18473
18473
|
return __awaiter(this, void 0, void 0, function* () {
|
|
18474
18474
|
const overwrite = !!(options === null || options === void 0 ? void 0 : options.overwrite);
|
|
18475
18475
|
const filePath = yield this.filePath();
|
|
18476
|
-
const writeFunc = overwrite ?
|
|
18476
|
+
const writeFunc = overwrite ? writeFile4 : appendFile;
|
|
18477
18477
|
yield writeFunc(filePath, this._buffer, { encoding: "utf8" });
|
|
18478
18478
|
return this.emptyBuffer();
|
|
18479
18479
|
});
|
|
@@ -62879,8 +62879,8 @@ var require_snapshot_utils = __commonJS({
|
|
|
62879
62879
|
var require_snapshot_recorder = __commonJS({
|
|
62880
62880
|
"node_modules/.pnpm/undici@7.22.0/node_modules/undici/lib/mock/snapshot-recorder.js"(exports, module) {
|
|
62881
62881
|
"use strict";
|
|
62882
|
-
var { writeFile:
|
|
62883
|
-
var { dirname:
|
|
62882
|
+
var { writeFile: writeFile4, readFile: readFile5, mkdir: mkdir3 } = __require("node:fs/promises");
|
|
62883
|
+
var { dirname: dirname7, resolve: resolve3 } = __require("node:path");
|
|
62884
62884
|
var { setTimeout: setTimeout2, clearTimeout: clearTimeout2 } = __require("node:timers");
|
|
62885
62885
|
var { InvalidArgumentError, UndiciError } = require_errors4();
|
|
62886
62886
|
var { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = require_snapshot_utils();
|
|
@@ -63081,7 +63081,7 @@ var require_snapshot_recorder = __commonJS({
|
|
|
63081
63081
|
throw new InvalidArgumentError("Snapshot path is required");
|
|
63082
63082
|
}
|
|
63083
63083
|
try {
|
|
63084
|
-
const data = await
|
|
63084
|
+
const data = await readFile5(resolve3(path3), "utf8");
|
|
63085
63085
|
const parsed2 = JSON.parse(data);
|
|
63086
63086
|
if (Array.isArray(parsed2)) {
|
|
63087
63087
|
this.#snapshots.clear();
|
|
@@ -63111,12 +63111,12 @@ var require_snapshot_recorder = __commonJS({
|
|
|
63111
63111
|
throw new InvalidArgumentError("Snapshot path is required");
|
|
63112
63112
|
}
|
|
63113
63113
|
const resolvedPath = resolve3(path3);
|
|
63114
|
-
await
|
|
63114
|
+
await mkdir3(dirname7(resolvedPath), { recursive: true });
|
|
63115
63115
|
const data = Array.from(this.#snapshots.entries()).map(([hash2, snapshot2]) => ({
|
|
63116
63116
|
hash: hash2,
|
|
63117
63117
|
snapshot: snapshot2
|
|
63118
63118
|
}));
|
|
63119
|
-
await
|
|
63119
|
+
await writeFile4(resolvedPath, JSON.stringify(data, null, 2), { flush: true });
|
|
63120
63120
|
}
|
|
63121
63121
|
/**
|
|
63122
63122
|
* Clears all recorded snapshots
|
|
@@ -97692,14 +97692,14 @@ var require_turndown_cjs = __commonJS({
|
|
|
97692
97692
|
} else if (node2.nodeType === 1) {
|
|
97693
97693
|
replacement = replacementForNode.call(self2, node2);
|
|
97694
97694
|
}
|
|
97695
|
-
return
|
|
97695
|
+
return join18(output, replacement);
|
|
97696
97696
|
}, "");
|
|
97697
97697
|
}
|
|
97698
97698
|
function postProcess(output) {
|
|
97699
97699
|
var self2 = this;
|
|
97700
97700
|
this.rules.forEach(function(rule) {
|
|
97701
97701
|
if (typeof rule.append === "function") {
|
|
97702
|
-
output =
|
|
97702
|
+
output = join18(output, rule.append(self2.options));
|
|
97703
97703
|
}
|
|
97704
97704
|
});
|
|
97705
97705
|
return output.replace(/^[\t\r\n]+/, "").replace(/[\t\r\n\s]+$/, "");
|
|
@@ -97711,7 +97711,7 @@ var require_turndown_cjs = __commonJS({
|
|
|
97711
97711
|
if (whitespace.leading || whitespace.trailing) content = content.trim();
|
|
97712
97712
|
return whitespace.leading + rule.replacement(content, node2, this.options) + whitespace.trailing;
|
|
97713
97713
|
}
|
|
97714
|
-
function
|
|
97714
|
+
function join18(output, replacement) {
|
|
97715
97715
|
var s1 = trimTrailingNewlines(output);
|
|
97716
97716
|
var s2 = trimLeadingNewlines(replacement);
|
|
97717
97717
|
var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
|
|
@@ -99204,13 +99204,13 @@ import { basename as basename2 } from "node:path";
|
|
|
99204
99204
|
// commands/gha.ts
|
|
99205
99205
|
var core7 = __toESM(require_core(), 1);
|
|
99206
99206
|
var import_arg = __toESM(require_arg(), 1);
|
|
99207
|
-
import { dirname as
|
|
99207
|
+
import { dirname as dirname6 } from "node:path";
|
|
99208
99208
|
|
|
99209
99209
|
// main.ts
|
|
99210
99210
|
var core6 = __toESM(require_core(), 1);
|
|
99211
99211
|
import { existsSync as existsSync7, readdirSync } from "node:fs";
|
|
99212
|
-
import { readFile as
|
|
99213
|
-
import { join as
|
|
99212
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
99213
|
+
import { join as join17 } from "node:path";
|
|
99214
99214
|
|
|
99215
99215
|
// node_modules/.pnpm/@ark+util@0.56.0/node_modules/@ark/util/out/arrays.js
|
|
99216
99216
|
var liftArray = (data) => Array.isArray(data) ? data : [data];
|
|
@@ -107706,7 +107706,7 @@ function buildCommitPrompt(status) {
|
|
|
107706
107706
|
].join("\n");
|
|
107707
107707
|
}
|
|
107708
107708
|
function hasPostRunIssues(issues) {
|
|
107709
|
-
return issues.stopHook !== void 0 || issues.dirtyTree !== void 0 || issues.summaryStale !== void 0;
|
|
107709
|
+
return issues.stopHook !== void 0 || issues.dirtyTree !== void 0 || issues.summaryStale !== void 0 || issues.unsubmittedReview !== void 0;
|
|
107710
107710
|
}
|
|
107711
107711
|
var agent = (input) => {
|
|
107712
107712
|
return {
|
|
@@ -108006,6 +108006,13 @@ function getApiUrl() {
|
|
|
108006
108006
|
log.debug(`resolved API_URL: ${raw2}`);
|
|
108007
108007
|
return raw2;
|
|
108008
108008
|
}
|
|
108009
|
+
function isLocalApiUrl() {
|
|
108010
|
+
try {
|
|
108011
|
+
return isLocalUrl(new URL(getApiUrl()));
|
|
108012
|
+
} catch {
|
|
108013
|
+
return false;
|
|
108014
|
+
}
|
|
108015
|
+
}
|
|
108009
108016
|
|
|
108010
108017
|
// models.ts
|
|
108011
108018
|
function provider(config3) {
|
|
@@ -109244,6 +109251,7 @@ function CreateCommentTool(ctx) {
|
|
|
109244
109251
|
body: bodyWithFooter
|
|
109245
109252
|
});
|
|
109246
109253
|
ctx.toolState.wasUpdated = true;
|
|
109254
|
+
log.info(`\xBB created comment ${result.data.id}`);
|
|
109247
109255
|
if (commentType === "Plan") {
|
|
109248
109256
|
if (result.data.node_id) {
|
|
109249
109257
|
await patchWorkflowRunFields(ctx, { planCommentNodeId: result.data.node_id });
|
|
@@ -109257,6 +109265,7 @@ function CreateCommentTool(ctx) {
|
|
|
109257
109265
|
comment_id: result.data.id,
|
|
109258
109266
|
body: bodyWithPlanLink
|
|
109259
109267
|
});
|
|
109268
|
+
log.info(`\xBB updated comment ${updateResult.data.id}`);
|
|
109260
109269
|
return {
|
|
109261
109270
|
success: true,
|
|
109262
109271
|
commentId: updateResult.data.id,
|
|
@@ -109290,6 +109299,7 @@ function EditCommentTool(ctx) {
|
|
|
109290
109299
|
comment_id: commentId,
|
|
109291
109300
|
body: bodyWithFooter
|
|
109292
109301
|
});
|
|
109302
|
+
log.info(`\xBB updated comment ${result.data.id}`);
|
|
109293
109303
|
return {
|
|
109294
109304
|
success: true,
|
|
109295
109305
|
commentId: result.data.id,
|
|
@@ -109425,6 +109435,9 @@ ${collapsible}`;
|
|
|
109425
109435
|
message: "progress recorded (no GitHub comment created - this may occur for workflow_dispatch events or when there is no associated issue/PR)"
|
|
109426
109436
|
};
|
|
109427
109437
|
}
|
|
109438
|
+
if (result.commentId !== void 0) {
|
|
109439
|
+
log.info(`\xBB ${result.action} comment ${result.commentId}`);
|
|
109440
|
+
}
|
|
109428
109441
|
if (!params.target_plan_comment) {
|
|
109429
109442
|
ctx.toolState.finalSummaryWritten = true;
|
|
109430
109443
|
}
|
|
@@ -109461,13 +109474,38 @@ var ReplyToReviewComment = type({
|
|
|
109461
109474
|
"extremely brief reply (1 sentence max) explaining what was fixed, e.g. 'Fixed by renaming to X' or 'Added null check'"
|
|
109462
109475
|
)
|
|
109463
109476
|
});
|
|
109477
|
+
function duplicateReplyDecision(params) {
|
|
109478
|
+
const existing = params.existing;
|
|
109479
|
+
if (!existing) return null;
|
|
109480
|
+
if (existing.bodyWithFooter !== params.bodyWithFooter) return null;
|
|
109481
|
+
return {
|
|
109482
|
+
kind: "already-replied",
|
|
109483
|
+
commentId: existing.commentId,
|
|
109484
|
+
url: existing.url,
|
|
109485
|
+
reason: `reply ${existing.commentId} with identical body was already posted in this session; ignoring duplicate call`
|
|
109486
|
+
};
|
|
109487
|
+
}
|
|
109464
109488
|
function ReplyToReviewCommentTool(ctx) {
|
|
109465
109489
|
return tool({
|
|
109466
109490
|
name: "reply_to_review_comment",
|
|
109467
|
-
description: "Reply to a PR review comment thread (NOT issue comments \u2014 this only works for inline review comments on PR diffs). Call
|
|
109491
|
+
description: "Reply to a PR review comment thread (NOT issue comments \u2014 this only works for inline review comments on PR diffs). 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).",
|
|
109468
109492
|
parameters: ReplyToReviewComment,
|
|
109469
109493
|
execute: execute(async ({ pull_number, comment_id, body }) => {
|
|
109470
109494
|
const bodyWithFooter = addFooter(ctx, body);
|
|
109495
|
+
const dup = duplicateReplyDecision({
|
|
109496
|
+
existing: ctx.toolState.reviewReplies?.get(comment_id),
|
|
109497
|
+
bodyWithFooter
|
|
109498
|
+
});
|
|
109499
|
+
if (dup) {
|
|
109500
|
+
log.info(`skipping duplicate review reply: ${dup.reason}`);
|
|
109501
|
+
return {
|
|
109502
|
+
success: true,
|
|
109503
|
+
skipped: true,
|
|
109504
|
+
reason: dup.reason,
|
|
109505
|
+
commentId: dup.commentId,
|
|
109506
|
+
url: dup.url
|
|
109507
|
+
};
|
|
109508
|
+
}
|
|
109471
109509
|
const result = await ctx.octokit.rest.pulls.createReplyForReviewComment({
|
|
109472
109510
|
owner: ctx.repo.owner,
|
|
109473
109511
|
repo: ctx.repo.name,
|
|
@@ -109475,7 +109513,14 @@ function ReplyToReviewCommentTool(ctx) {
|
|
|
109475
109513
|
comment_id,
|
|
109476
109514
|
body: bodyWithFooter
|
|
109477
109515
|
});
|
|
109516
|
+
log.info(`\xBB created review comment ${result.data.id} (in reply to ${comment_id})`);
|
|
109478
109517
|
ctx.toolState.wasUpdated = true;
|
|
109518
|
+
ctx.toolState.reviewReplies ??= /* @__PURE__ */ new Map();
|
|
109519
|
+
ctx.toolState.reviewReplies.set(comment_id, {
|
|
109520
|
+
commentId: result.data.id,
|
|
109521
|
+
url: result.data.html_url,
|
|
109522
|
+
bodyWithFooter
|
|
109523
|
+
});
|
|
109479
109524
|
return {
|
|
109480
109525
|
success: true,
|
|
109481
109526
|
commentId: result.data.id,
|
|
@@ -110024,11 +110069,6 @@ async function spawn(options) {
|
|
|
110024
110069
|
`spawn activity timer: pid=${child.pid} cmd=${options.cmd} timeout=${activityTimeoutMs}ms`
|
|
110025
110070
|
);
|
|
110026
110071
|
activityCheckIntervalId = setInterval(() => {
|
|
110027
|
-
if (options.isPausedExternally?.()) {
|
|
110028
|
-
lastActivityTime = performance3.now();
|
|
110029
|
-
log.debug(`spawn activity check: pid=${child.pid} paused externally`);
|
|
110030
|
-
return;
|
|
110031
|
-
}
|
|
110032
110072
|
const idleMs = performance3.now() - lastActivityTime;
|
|
110033
110073
|
log.debug(
|
|
110034
110074
|
`spawn activity check: pid=${child.pid} idle=${Math.round(idleMs)}ms / ${activityTimeoutMs}ms`
|
|
@@ -110227,13 +110267,13 @@ var installNodeDependencies = {
|
|
|
110227
110267
|
};
|
|
110228
110268
|
}
|
|
110229
110269
|
}
|
|
110230
|
-
const resolved = resolveCommand(agent2, "frozen", [])
|
|
110270
|
+
const resolved = resolveCommand(agent2, "frozen", []);
|
|
110231
110271
|
if (!resolved) {
|
|
110232
110272
|
return {
|
|
110233
110273
|
language: "node",
|
|
110234
110274
|
packageManager,
|
|
110235
110275
|
dependenciesInstalled: false,
|
|
110236
|
-
issues: [`no install command
|
|
110276
|
+
issues: [`no frozen-install command available for ${agent2}`]
|
|
110237
110277
|
};
|
|
110238
110278
|
}
|
|
110239
110279
|
if (options.ignoreScripts) {
|
|
@@ -142549,7 +142589,7 @@ var import_semver = __toESM(require_semver2(), 1);
|
|
|
142549
142589
|
// package.json
|
|
142550
142590
|
var package_default = {
|
|
142551
142591
|
name: "pullfrog",
|
|
142552
|
-
version: "0.1.
|
|
142592
|
+
version: "0.1.3",
|
|
142553
142593
|
type: "module",
|
|
142554
142594
|
bin: {
|
|
142555
142595
|
pullfrog: "dist/cli.mjs",
|
|
@@ -143493,6 +143533,10 @@ ${integrateStep}
|
|
|
143493
143533
|
if (!pushed) {
|
|
143494
143534
|
throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
|
|
143495
143535
|
}
|
|
143536
|
+
const pushedSha = $("git", ["rev-parse", "HEAD"], { log: false }).trim();
|
|
143537
|
+
log.info(
|
|
143538
|
+
`\xBB pushed branch ${branch} to ${pushDest.remoteName}/${pushDest.remoteBranch} (sha ${pushedSha})`
|
|
143539
|
+
);
|
|
143496
143540
|
return {
|
|
143497
143541
|
success: true,
|
|
143498
143542
|
branch,
|
|
@@ -143641,6 +143685,7 @@ function DeleteBranchTool(ctx) {
|
|
|
143641
143685
|
await $git("push", ["origin", "--delete", `refs/heads/${params.branchName}`], {
|
|
143642
143686
|
token: ctx.gitToken
|
|
143643
143687
|
});
|
|
143688
|
+
log.info(`\xBB deleted branch ${params.branchName}`);
|
|
143644
143689
|
return { success: true, deleted: params.branchName };
|
|
143645
143690
|
})
|
|
143646
143691
|
});
|
|
@@ -143666,6 +143711,7 @@ function PushTagsTool(ctx) {
|
|
|
143666
143711
|
await $git("push", pushArgs, {
|
|
143667
143712
|
token: ctx.gitToken
|
|
143668
143713
|
});
|
|
143714
|
+
log.info(`\xBB pushed tag ${params.tag}`);
|
|
143669
143715
|
return { success: true, tag: params.tag };
|
|
143670
143716
|
})
|
|
143671
143717
|
});
|
|
@@ -143820,7 +143866,7 @@ var CreatePullRequestReview = type({
|
|
|
143820
143866
|
"1-2 sentence high-level summary with urgency level, critical callouts, and feedback about code outside the diff. Specific feedback on diff lines goes in 'comments' array."
|
|
143821
143867
|
).optional(),
|
|
143822
143868
|
approved: type.boolean.describe(
|
|
143823
|
-
"Set to true to submit as an approval.
|
|
143869
|
+
"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."
|
|
143824
143870
|
).optional(),
|
|
143825
143871
|
commit_id: type.string.describe("Optional SHA of the commit being reviewed. Defaults to latest.").optional(),
|
|
143826
143872
|
comments: type({
|
|
@@ -143990,6 +144036,7 @@ function CreatePullRequestReviewTool(ctx) {
|
|
|
143990
144036
|
}
|
|
143991
144037
|
const reviewId = result.data.id;
|
|
143992
144038
|
const reviewNodeId = result.data.node_id;
|
|
144039
|
+
log.info(`\xBB created review ${reviewId} on pull request #${pull_number}`);
|
|
143993
144040
|
const actuallyReviewedSha = ctx.toolState.checkoutSha ?? params.commit_id;
|
|
143994
144041
|
ctx.toolState.review = {
|
|
143995
144042
|
id: reviewId,
|
|
@@ -144349,6 +144396,8 @@ async function ensureBeforeShaReachable(params) {
|
|
|
144349
144396
|
}
|
|
144350
144397
|
}
|
|
144351
144398
|
var STALE_LOCK_AGE_MS = 3e4;
|
|
144399
|
+
var PULL_REF_RETRY_DELAYS_MS = [2e3, 5e3, 1e4];
|
|
144400
|
+
var PULL_REF_MISSING_PATTERN = /couldn't find remote ref pull\/\d+\/head/i;
|
|
144352
144401
|
var GIT_LOCK_PATHS = [
|
|
144353
144402
|
".git/shallow.lock",
|
|
144354
144403
|
".git/index.lock",
|
|
@@ -144374,6 +144423,27 @@ function cleanupStaleGitLocks() {
|
|
|
144374
144423
|
}
|
|
144375
144424
|
}
|
|
144376
144425
|
}
|
|
144426
|
+
async function isPullRequestStillDispatchable(args2) {
|
|
144427
|
+
try {
|
|
144428
|
+
const { data } = await args2.octokit.rest.pulls.get({
|
|
144429
|
+
owner: args2.owner,
|
|
144430
|
+
repo: args2.repo,
|
|
144431
|
+
pull_number: args2.pr.number
|
|
144432
|
+
});
|
|
144433
|
+
if (data.state !== "open") return false;
|
|
144434
|
+
if (data.head.sha !== args2.pr.headSha) return false;
|
|
144435
|
+
return true;
|
|
144436
|
+
} catch {
|
|
144437
|
+
return true;
|
|
144438
|
+
}
|
|
144439
|
+
}
|
|
144440
|
+
async function abortIfPullRequestMoved(args2) {
|
|
144441
|
+
const stillValid = await isPullRequestStillDispatchable(args2);
|
|
144442
|
+
if (stillValid) return;
|
|
144443
|
+
throw new Error(
|
|
144444
|
+
`PR #${args2.pr.number} is no longer in the state it was at dispatch (likely closed, merged, or force-pushed between webhook fire and run start). aborting checkout \u2014 re-trigger the run if this PR is still active.`
|
|
144445
|
+
);
|
|
144446
|
+
}
|
|
144377
144447
|
async function checkoutPrBranch(pr, params) {
|
|
144378
144448
|
const { octokit, owner, name, gitToken, toolState, beforeSha } = params;
|
|
144379
144449
|
log.info(`\xBB checking out PR #${pr.number}...`);
|
|
@@ -144390,9 +144460,26 @@ async function checkoutPrBranch(pr, params) {
|
|
|
144390
144460
|
if (!alreadyOnBranch) {
|
|
144391
144461
|
$("git", ["checkout", "-B", pr.baseRef, `origin/${pr.baseRef}`], { log: false });
|
|
144392
144462
|
log.debug(`\xBB fetching PR #${pr.number} (${localBranch})...`);
|
|
144393
|
-
await
|
|
144394
|
-
|
|
144395
|
-
|
|
144463
|
+
await retry(
|
|
144464
|
+
async () => {
|
|
144465
|
+
try {
|
|
144466
|
+
await $git("fetch", ["--no-tags", "origin", `+pull/${pr.number}/head:${localBranch}`], {
|
|
144467
|
+
token: gitToken
|
|
144468
|
+
});
|
|
144469
|
+
} catch (e) {
|
|
144470
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
144471
|
+
if (PULL_REF_MISSING_PATTERN.test(msg)) {
|
|
144472
|
+
await abortIfPullRequestMoved({ octokit, owner, repo: name, pr });
|
|
144473
|
+
}
|
|
144474
|
+
throw e;
|
|
144475
|
+
}
|
|
144476
|
+
},
|
|
144477
|
+
{
|
|
144478
|
+
delaysMs: PULL_REF_RETRY_DELAYS_MS,
|
|
144479
|
+
label: `pull/${pr.number}/head fetch`,
|
|
144480
|
+
shouldRetry: (e) => PULL_REF_MISSING_PATTERN.test(e instanceof Error ? e.message : String(e))
|
|
144481
|
+
}
|
|
144482
|
+
);
|
|
144396
144483
|
$("git", ["checkout", localBranch], { log: false });
|
|
144397
144484
|
log.debug(`\xBB checked out PR #${pr.number}`);
|
|
144398
144485
|
toolState.checkoutSha = $("git", ["rev-parse", "HEAD"], { log: false }).trim();
|
|
@@ -144854,6 +144941,7 @@ function IssueTool(ctx) {
|
|
|
144854
144941
|
labels: params.labels ?? [],
|
|
144855
144942
|
assignees: params.assignees ?? []
|
|
144856
144943
|
});
|
|
144944
|
+
log.info(`\xBB created issue #${result.data.number} (id ${result.data.id})`);
|
|
144857
144945
|
const nodeId = result.data.node_id;
|
|
144858
144946
|
if (typeof nodeId === "string" && nodeId.length > 0) {
|
|
144859
144947
|
await patchWorkflowRunFields(ctx, {
|
|
@@ -145045,6 +145133,7 @@ function AddLabelsTool(ctx) {
|
|
|
145045
145133
|
issue_number,
|
|
145046
145134
|
labels
|
|
145047
145135
|
});
|
|
145136
|
+
log.info(`\xBB added labels [${labels.join(", ")}] to issue #${issue_number}`);
|
|
145048
145137
|
return {
|
|
145049
145138
|
success: true,
|
|
145050
145139
|
labels: result.data.map((label) => label.name)
|
|
@@ -145053,40 +145142,6 @@ function AddLabelsTool(ctx) {
|
|
|
145053
145142
|
});
|
|
145054
145143
|
}
|
|
145055
145144
|
|
|
145056
|
-
// mcp/learnings.ts
|
|
145057
|
-
var UpdateLearningsParams = type({
|
|
145058
|
-
learnings: type.string.describe(
|
|
145059
|
-
"the FULL merged learnings as a flat bullet list. each line starts with `- `. one discrete, actionable fact per bullet. combine existing bullets from the prompt with your new discoveries. deduplicate \u2014 if an existing bullet covers the same fact, update it in place rather than adding a new one. drop bullets that are clearly wrong or no longer relevant to the current codebase. keep the list focused and concise."
|
|
145060
|
-
)
|
|
145061
|
-
});
|
|
145062
|
-
function UpdateLearningsTool(ctx) {
|
|
145063
|
-
return tool({
|
|
145064
|
-
name: "update_learnings",
|
|
145065
|
-
description: "persist operational learnings about this repository (setup steps, test commands, key conventions, patterns). ONLY call this when you have high confidence the information is correct and broadly useful for future runs \u2014 not for one-off findings or uncertain observations. format: flat bullet list (`- ` per line, one fact per bullet). pass the FULL merged list \u2014 combine existing learnings from the prompt with new discoveries. deduplicate, and drop bullets that are clearly wrong or no longer relevant to the current codebase.",
|
|
145066
|
-
parameters: UpdateLearningsParams,
|
|
145067
|
-
execute: execute(async (params) => {
|
|
145068
|
-
const response = await apiFetch({
|
|
145069
|
-
path: `/api/repo/${ctx.repo.owner}/${ctx.repo.name}/learnings`,
|
|
145070
|
-
method: "PATCH",
|
|
145071
|
-
headers: {
|
|
145072
|
-
authorization: `Bearer ${ctx.apiToken}`,
|
|
145073
|
-
"content-type": "application/json"
|
|
145074
|
-
},
|
|
145075
|
-
body: JSON.stringify({
|
|
145076
|
-
learnings: params.learnings,
|
|
145077
|
-
model: ctx.toolState.model
|
|
145078
|
-
}),
|
|
145079
|
-
signal: AbortSignal.timeout(1e4)
|
|
145080
|
-
});
|
|
145081
|
-
if (!response.ok) {
|
|
145082
|
-
const error49 = await response.text();
|
|
145083
|
-
throw new Error(`failed to update learnings: ${error49}`);
|
|
145084
|
-
}
|
|
145085
|
-
return { success: true };
|
|
145086
|
-
})
|
|
145087
|
-
});
|
|
145088
|
-
}
|
|
145089
|
-
|
|
145090
145145
|
// mcp/output.ts
|
|
145091
145146
|
var import_ajv3 = __toESM(require_ajv(), 1);
|
|
145092
145147
|
var SetOutputParams = type({
|
|
@@ -145180,6 +145235,7 @@ function UpdatePullRequestBodyTool(ctx) {
|
|
|
145180
145235
|
pull_number: params.pull_number,
|
|
145181
145236
|
body: bodyWithFooter
|
|
145182
145237
|
});
|
|
145238
|
+
log.info(`\xBB updated pull request #${result.data.number}`);
|
|
145183
145239
|
ctx.toolState.wasUpdated = true;
|
|
145184
145240
|
return {
|
|
145185
145241
|
success: true,
|
|
@@ -145207,6 +145263,7 @@ function CreatePullRequestTool(ctx) {
|
|
|
145207
145263
|
base: params.base,
|
|
145208
145264
|
draft: params.draft ?? false
|
|
145209
145265
|
});
|
|
145266
|
+
log.info(`\xBB created pull request #${result.data.number} (id ${result.data.id})`);
|
|
145210
145267
|
const reviewer = ctx.payload.triggerer;
|
|
145211
145268
|
if (reviewer) {
|
|
145212
145269
|
try {
|
|
@@ -145758,7 +145815,7 @@ function ResolveReviewThreadTool(ctx) {
|
|
|
145758
145815
|
threadId: params.thread_id
|
|
145759
145816
|
});
|
|
145760
145817
|
const thread = response.resolveReviewThread.thread;
|
|
145761
|
-
log.
|
|
145818
|
+
log.info(`\xBB resolved review thread ${thread.id}`);
|
|
145762
145819
|
return {
|
|
145763
145820
|
thread_id: thread.id,
|
|
145764
145821
|
is_resolved: thread.isResolved,
|
|
@@ -145799,13 +145856,14 @@ function buildModeOverrides(t2) {
|
|
|
145799
145856
|
|
|
145800
145857
|
An existing plan comment was found for this issue. Update that comment with the revised plan \u2014 do not create a new plan comment.
|
|
145801
145858
|
|
|
145802
|
-
1.
|
|
145803
|
-
2.
|
|
145859
|
+
1. **task list**: create your task list for this run as your first action.
|
|
145860
|
+
2. Use \`previousPlanBody\` from this response as the plan to revise; do not call \`get_issue\` or \`get_issue_comments\`.
|
|
145861
|
+
3. Revise the plan based on the user's request:
|
|
145804
145862
|
- incorporate the current plan (\`previousPlanBody\`) and the user's revision request
|
|
145805
145863
|
- gather relevant codebase context (file paths, architecture notes from AGENTS.md)
|
|
145806
145864
|
- produce a structured plan with clear milestones
|
|
145807
|
-
|
|
145808
|
-
|
|
145865
|
+
4. Call \`${t2("report_progress")}\` with the full revised plan text and \`{ target_plan_comment: true }\` so it updates the existing plan comment (not the progress comment).
|
|
145866
|
+
5. Then post a short note to the progress comment (e.g. "Plan has been updated in the comment above.") via \`${t2("report_progress")}\` so it is not left as "Leaping...".`
|
|
145809
145867
|
};
|
|
145810
145868
|
}
|
|
145811
145869
|
var modeInstructionParent = {
|
|
@@ -146230,24 +146288,13 @@ function UploadFileTool(ctx) {
|
|
|
146230
146288
|
if (!uploadResponse.ok) {
|
|
146231
146289
|
throw new Error(`failed to upload file: ${uploadResponse.statusText}`);
|
|
146232
146290
|
}
|
|
146291
|
+
log.info(`\xBB uploaded file ${publicUrl}`);
|
|
146233
146292
|
return { success: true, publicUrl, filename, contentLength, contentType };
|
|
146234
146293
|
})
|
|
146235
146294
|
});
|
|
146236
146295
|
}
|
|
146237
146296
|
|
|
146238
146297
|
// mcp/server.ts
|
|
146239
|
-
function initToolState(params) {
|
|
146240
|
-
const resolved = parseProgressComment(params.progressComment);
|
|
146241
|
-
if (resolved) {
|
|
146242
|
-
log.info(`\xBB using pre-created progress comment: ${resolved.id} (${resolved.type})`);
|
|
146243
|
-
}
|
|
146244
|
-
return {
|
|
146245
|
-
progressComment: resolved,
|
|
146246
|
-
hadProgressComment: !!resolved,
|
|
146247
|
-
backgroundProcesses: /* @__PURE__ */ new Map(),
|
|
146248
|
-
usageEntries: []
|
|
146249
|
-
};
|
|
146250
|
-
}
|
|
146251
146298
|
var mcpPortStart = 3764;
|
|
146252
146299
|
var mcpPortAttempts = 100;
|
|
146253
146300
|
var mcpHost = "127.0.0.1";
|
|
@@ -146323,8 +146370,7 @@ function buildOrchestratorTools(ctx, outputSchema) {
|
|
|
146323
146370
|
PushTagsTool(ctx),
|
|
146324
146371
|
DeleteBranchTool(ctx),
|
|
146325
146372
|
CreatePullRequestTool(ctx),
|
|
146326
|
-
UpdatePullRequestBodyTool(ctx)
|
|
146327
|
-
UpdateLearningsTool(ctx)
|
|
146373
|
+
UpdatePullRequestBodyTool(ctx)
|
|
146328
146374
|
];
|
|
146329
146375
|
}
|
|
146330
146376
|
async function tryStartMcpServer(ctx, tools, port) {
|
|
@@ -146481,9 +146527,6 @@ Rules:
|
|
|
146481
146527
|
- Do NOT include a changelog section \u2014 the key changes list serves this purpose
|
|
146482
146528
|
- Focus on *intent*, not *what* \u2014 the diff already shows what changed
|
|
146483
146529
|
- Get the file count and commit count from the checkout_pr metadata, not by counting manually`;
|
|
146484
|
-
function learningsStep(t2, n) {
|
|
146485
|
-
return `${n}. **learnings** (only if high confidence): if you discovered something about repo setup, test commands, conventions, or patterns that you are confident is correct and would reliably help future runs, call \`${t2("update_learnings")}\` to persist it. skip this step if you are unsure or the finding is speculative/one-off. format as a flat bullet list (\`- \` per line, one fact per bullet). merge with existing learnings from the prompt \u2014 pass the FULL merged list. deduplicate, and drop bullets that are clearly wrong or no longer relevant to the current codebase.`;
|
|
146486
|
-
}
|
|
146487
146530
|
function computeModes(agentId) {
|
|
146488
146531
|
const t2 = (toolName) => formatMcpToolRef(agentId, toolName);
|
|
146489
146532
|
return [
|
|
@@ -146492,18 +146535,20 @@ function computeModes(agentId) {
|
|
|
146492
146535
|
description: "Implement, build, create, or develop code changes; make specific changes to files or features; execute a plan; or handle tasks with specific implementation details",
|
|
146493
146536
|
prompt: `### Checklist
|
|
146494
146537
|
|
|
146495
|
-
1. **
|
|
146538
|
+
1. **task list**: create your task list for this run as your first action.
|
|
146496
146539
|
|
|
146497
|
-
2. **
|
|
146540
|
+
2. **plan** (optional, for complex tasks): analyze requirements, read AGENTS.md and relevant code, produce a step-by-step implementation plan.
|
|
146541
|
+
|
|
146542
|
+
3. **setup**: checkout or create the branch:
|
|
146498
146543
|
- **PR event, modifying the existing PR**: call \`${t2("checkout_pr")}\`
|
|
146499
146544
|
- **new branch**: use \`${t2("git")}\` to create a branch (\`git checkout -b pullfrog/branch-name\`)
|
|
146500
146545
|
|
|
146501
|
-
|
|
146546
|
+
4. **build**: implement changes using your native file and shell tools:
|
|
146502
146547
|
- follow the plan (if you ran a plan phase)
|
|
146503
146548
|
- plan your approach before writing code: identify which files need to change, key design decisions, and edge cases. for non-trivial changes, consider whether there's a more elegant approach.
|
|
146504
146549
|
- run relevant tests/lints before committing
|
|
146505
146550
|
|
|
146506
|
-
|
|
146551
|
+
5. **self-review**: judgment call \u2014 does YOUR diff warrant a fresh-eyes pass?
|
|
146507
146552
|
|
|
146508
146553
|
Skip self-review (commit directly) when the diff is **genuinely trivial**:
|
|
146509
146554
|
- doc typos, comment-only edits, whitespace/format-only, import reordering
|
|
@@ -146534,13 +146579,11 @@ function computeModes(agentId) {
|
|
|
146534
146579
|
|
|
146535
146580
|
Review the findings, address valid points, and discard nitpicks or false positives. The reviewer is fallible \u2014 it biases toward *recommending additions* (defensive checks for impossible cases, extra logging, new abstractions used once, comments restating code, tests asserting tautologies, "just-in-case" guards). For each finding, ask: would applying it leave the code more sound, correct, AND elegant? Two-out-of-three is usually a signal to look harder for a fix that gets all three before settling for one that trades elegance for correctness. Reject bloat-shaped findings without applying them, and after applying the rest re-read your diff and be discerning about what *you just changed*: if any fix turned out to be bloat in context, revert it. The goal is code that is sound and correct *while remaining elegant*; the smallest diff that fixes the real defect almost always wins. Then verify only intended changes are present, no debug artifacts or commented-out code remain, no unrelated files were modified. Commit locally via shell (\`git add . && git commit -m "..."\`).
|
|
146536
146581
|
|
|
146537
|
-
|
|
146582
|
+
6. **finalize**:
|
|
146538
146583
|
- confirm a clean working tree, then push via \`${t2("push_branch")}\` (see *SYSTEM* Git rules if this fails \u2014 prepush errors are usually the repo's tests/lint, not infra timeouts)
|
|
146539
146584
|
- create a PR via \`${t2("create_pull_request")}\`
|
|
146540
146585
|
- call \`${t2("report_progress")}\` with the PR link or the exact error if push/PR failed
|
|
146541
146586
|
|
|
146542
|
-
${learningsStep(t2, 6)}
|
|
146543
|
-
|
|
146544
146587
|
### Notes
|
|
146545
146588
|
|
|
146546
146589
|
For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
@@ -146550,27 +146593,27 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
146550
146593
|
description: "Address PR review feedback; respond to reviewer comments; make requested changes to an existing PR",
|
|
146551
146594
|
prompt: `### Checklist
|
|
146552
146595
|
|
|
146553
|
-
1.
|
|
146596
|
+
1. **task list**: create your task list for this run as your first action.
|
|
146554
146597
|
|
|
146555
|
-
2.
|
|
146598
|
+
2. Checkout the PR branch via \`${t2("checkout_pr")}\`.
|
|
146556
146599
|
|
|
146557
|
-
3.
|
|
146600
|
+
3. Fetch review comments via \`${t2("get_review_comments")}\`.
|
|
146601
|
+
|
|
146602
|
+
4. For each comment:
|
|
146558
146603
|
- understand the feedback
|
|
146559
146604
|
- evaluate whether applying it would leave the code more **sound, correct, AND elegant**. reviewers are fallible and bias toward *recommending additions* (defensive checks for impossible cases, extra abstractions, comments restating obvious code, tests asserting tautologies, "just-in-case" guards). if a request would add bloat \u2014 ceremony without commensurate correctness benefit \u2014 push back in your reply rather than mechanically applying it. two-out-of-three is usually a signal to look harder for a fix that gets all three before settling.
|
|
146560
146605
|
- if the request stands, make the code change using your native tools; otherwise reply explaining why
|
|
146561
146606
|
- record what was done (or why nothing was done)
|
|
146562
146607
|
|
|
146563
|
-
|
|
146608
|
+
5. Quality check:
|
|
146564
146609
|
- test changes, then review the diff before committing \u2014 verify only intended changes are present, no debug artifacts remain, no fix turned out to be bloat in context (revert any that did), and the changes are clean enough that a senior engineer would approve without hesitation
|
|
146565
146610
|
- commit locally via shell (\`git add . && git commit -m "..."\`)
|
|
146566
146611
|
|
|
146567
|
-
|
|
146612
|
+
6. Finalize:
|
|
146568
146613
|
- confirm a clean working tree, then push via \`${t2("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*)
|
|
146569
|
-
- reply to each comment using \`${t2("reply_to_review_comment")}\`
|
|
146614
|
+
- reply to each comment **exactly once** using \`${t2("reply_to_review_comment")}\` \u2014 do not re-emit the same call (the runtime dedupes identical bodies and the second call is wasted)
|
|
146570
146615
|
- resolve addressed threads via \`${t2("resolve_review_thread")}\`
|
|
146571
|
-
- call \`${t2("report_progress")}\` with a brief summary (or the exact push error if push failed)
|
|
146572
|
-
|
|
146573
|
-
${learningsStep(t2, 6)}`
|
|
146616
|
+
- call \`${t2("report_progress")}\` with a brief summary (or the exact push error if push failed)`
|
|
146574
146617
|
},
|
|
146575
146618
|
// Review and IncrementalReview use the multi-lens orchestrator pattern
|
|
146576
146619
|
// (canonical source: .claude/commands/anneal.md). The orchestrator does
|
|
@@ -146589,11 +146632,13 @@ ${learningsStep(t2, 6)}`
|
|
|
146589
146632
|
description: "Review code, PRs, or implementations; provide feedback or suggestions; identify issues; or check code quality, style, and correctness",
|
|
146590
146633
|
prompt: `### Checklist
|
|
146591
146634
|
|
|
146592
|
-
1. **
|
|
146635
|
+
1. **task list**: create your task list for this run as your first action.
|
|
146593
146636
|
|
|
146594
|
-
2. **
|
|
146637
|
+
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.
|
|
146595
146638
|
|
|
146596
|
-
|
|
146639
|
+
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). orientation only \u2014 defer specific defect-hunting to the subagents; pre-reviewing biases the lenses you pick. use \`${t2("get_pull_request")}\` and other read-only GitHub tools for additional context if needed.
|
|
146640
|
+
|
|
146641
|
+
if the PR is **genuinely trivial**, skip steps 4\u20135 entirely and submit a \`No new issues found.\` review per step 6. there's no value in dispatching even one lens for a typo.
|
|
146597
146642
|
|
|
146598
146643
|
"Genuinely trivial" (skip):
|
|
146599
146644
|
- single-word doc typo, whitespace/format-only, comment-only across any number of files
|
|
@@ -146638,7 +146683,7 @@ ${learningsStep(t2, 6)}`
|
|
|
146638
146683
|
- **holistic** \u2014 does the PR make sense as a whole? symmetric flows (delete for every create, rollback for every migration)?
|
|
146639
146684
|
- **subsystem lenses** (invent as the PR demands) \u2014 auth, billing, payments, schema migration, webhooks, secrets, RBAC, multi-tenant isolation, cron/scheduling, etc.
|
|
146640
146685
|
|
|
146641
|
-
|
|
146686
|
+
4. **fan out**: dispatch one \`${REVIEWER_AGENT_NAME}\` subagent per lens \u2014 its baked-in system prompt enforces the non-mutative + non-recursive contract (read-only file/search/web tools and read-only MCP queries; no writes, shell side effects, state-changing MCP calls, or nested subagent dispatch). when picking 2+ lenses, dispatch them in a **single assistant turn with multiple parallel subagent calls**; issuing one and awaiting reply before the next collapses the fan-out into a serial review. 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 step 4 entirely on a single subagent failure. each subagent gets:
|
|
146642
146687
|
- the diff path / target \u2014 reading the diff and the codebase is its job
|
|
146643
146688
|
- **only one lens** \u2014 never a multi-section "review for X, Y, and Z" prompt
|
|
146644
146689
|
- **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\`.
|
|
@@ -146653,20 +146698,33 @@ ${learningsStep(t2, 6)}`
|
|
|
146653
146698
|
- do NOT pre-shape their output with a finding schema
|
|
146654
146699
|
- do NOT mention the other lenses (independence is the point \u2014 overlapping findings are a strong signal)
|
|
146655
146700
|
|
|
146656
|
-
|
|
146701
|
+
5. **aggregate & draft**: 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.
|
|
146657
146702
|
|
|
146658
146703
|
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.
|
|
146659
146704
|
|
|
146660
|
-
|
|
146705
|
+
6. **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.
|
|
146661
146706
|
|
|
146662
146707
|
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.
|
|
146663
146708
|
|
|
146664
146709
|
The review body is structured as: \`[optional alert blockquote]\` \u2192 \`[PR summary using the default format below]\`. Inline comments are passed via the \`comments\` parameter, not in the body.
|
|
146665
146710
|
|
|
146666
|
-
|
|
146711
|
+
GitHub alert blockquotes render at four visual intensities \u2014 the callout is what the author sees first, so pick the one that matches what you want them to do:
|
|
146712
|
+
|
|
146713
|
+
- \`[!CAUTION]\` \u2014 large red banner. Reads as "this will break something."
|
|
146714
|
+
- \`[!IMPORTANT]\` \u2014 large purple banner. Reads as "you need to look at this before merging."
|
|
146715
|
+
- \`[!NOTE]\` \u2014 small blue inline callout. Reads as "FYI, here's something worth noting."
|
|
146716
|
+
- no callout \u2014 plain text. Reads as routine review output.
|
|
146717
|
+
|
|
146718
|
+
Two reinforcing levers: callout intensity (above) and \`approved\` (which gates the footer Fix-button affordance \u2014 Fix renders 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. Pick the tier the author's actual next action justifies.
|
|
146719
|
+
|
|
146720
|
+
- **critical issues** (blocks merge \u2014 bugs, security, data loss, broken core flows):
|
|
146667
146721
|
\`approved: false\`. Body opens with \`> [!CAUTION]\\n> This PR introduces ...\`, followed by the PR summary. Include all inline comments via \`comments\`.
|
|
146668
|
-
- **
|
|
146669
|
-
\`approved: false\`. Body opens with \`> [!IMPORTANT]\\n>
|
|
146722
|
+
- **must-address non-critical findings** (real consequences if shipped \u2014 incorrect behavior in non-critical paths, missing validation on user input, regressions the author should fix before merge):
|
|
146723
|
+
\`approved: false\`. Body opens with \`> [!IMPORTANT]\\n> ...\`, followed by the PR summary. Reserve this tier for findings with concrete fallout \u2014 do NOT use \`[!IMPORTANT]\` for nits, style preferences, or "consider also" suggestions. Include all inline comments via \`comments\`.
|
|
146724
|
+
- **minor suggestions only** (single-line nits, doc/comment polish, defer-able observations, "rough edges"):
|
|
146725
|
+
\`approved: false\`. NO alert blockquote. Body opens directly with the PR summary. Include all inline comments via \`comments\`.
|
|
146726
|
+
- **informational observations** (mergeable as-is, nothing actionable \u2014 e.g. prior feedback addressed cleanly, surfacing a minor stale doc reference, calling out something noteworthy without recommending a change):
|
|
146727
|
+
\`approved: true\`. Body opens with \`> [!NOTE]\\n> ...\`, followed by the PR summary. Do NOT include inline \`comments\` \u2014 \`[!NOTE]\` signals "no action needed", which contradicts an actionable anchor; if a point is concrete enough to anchor to a line, downgrade the whole review to "minor suggestions only" (\`approved: false\`) instead.
|
|
146670
146728
|
- **no actionable issues**:
|
|
146671
146729
|
\`approved: true\`. Body opens with \`No new issues found.\` followed by the PR summary.
|
|
146672
146730
|
|
|
@@ -146675,7 +146733,7 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
146675
146733
|
// IncrementalReview shares Review's multi-lens orchestrator pattern but
|
|
146676
146734
|
// scopes the target to the incremental diff. The "issues must be NEW
|
|
146677
146735
|
// since the last Pullfrog review" filter lives at aggregation time
|
|
146678
|
-
// (step
|
|
146736
|
+
// (step 6), NOT in the subagent prompt — pushing the filter into
|
|
146679
146737
|
// subagents matches the canonical anneal anti-pattern of "list known
|
|
146680
146738
|
// pre-existing failures — don't flag these" and suppresses signal on
|
|
146681
146739
|
// regressions the new commits amplified. The review body is just
|
|
@@ -146688,15 +146746,17 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
146688
146746
|
description: "Re-review a PR after new commits are pushed; focus on new changes since the last review",
|
|
146689
146747
|
prompt: `### Checklist
|
|
146690
146748
|
|
|
146691
|
-
1. **
|
|
146749
|
+
1. **task list**: create your task list for this run as your first action.
|
|
146750
|
+
|
|
146751
|
+
2. **checkout**: call \`${t2("checkout_pr")}\` \u2014 this returns PR metadata, \`diffPath\` (full diff), and \`incrementalDiffPath\` (changes since last reviewed version, if available). read the diff TOC first and use its line ranges as your coverage checklist.
|
|
146692
146752
|
|
|
146693
|
-
|
|
146753
|
+
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.
|
|
146694
146754
|
|
|
146695
|
-
|
|
146755
|
+
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 6 \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.
|
|
146696
146756
|
|
|
146697
|
-
|
|
146757
|
+
5. **triage & fan out**: orient on the *incremental* changes \u2014 domain, seams, external contracts, user-facing surfaces.
|
|
146698
146758
|
|
|
146699
|
-
if the incremental changes are **genuinely trivial**, skip the fan-out entirely and jump to step
|
|
146759
|
+
if the incremental changes are **genuinely trivial**, skip the fan-out entirely and jump to step 8's non-substantive path (do NOT submit a review).
|
|
146700
146760
|
|
|
146701
146761
|
"Genuinely trivial" (skip): formatting/comment tweaks, import reordering, lockfile regen, mechanical rename of import paths, whitespace-only.
|
|
146702
146762
|
"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.
|
|
@@ -146704,8 +146764,8 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
146704
146764
|
|
|
146705
146765
|
otherwise pick lenses by where the new commits concentrate risk \u2014 **there's no fixed count**, same calibration as Review mode (1 lens for pure refactor / isolated fix; 2\u20133 for typical features; 4\u20135 for high-stakes subsystem touches; 6+ is a smell). lens framing follows Review mode: themed lenses (correctness & invariants, impact when new commits remove/rename/deprecate things, research-validated assumptions, security, user-journey, operational readiness, integration & cross-cutting, test integrity, performance, holistic) and subsystem lenses (auth, billing, schema migration, etc.) \u2014 for high-stakes domains lead with the subsystem lens rather than the generic themed equivalent.
|
|
146706
146766
|
|
|
146707
|
-
dispatch one \`${REVIEWER_AGENT_NAME}\` subagent per lens \u2014 its baked-in system prompt enforces the non-mutative + non-recursive contract (read-only file/search/web tools and read-only MCP queries; no writes, shell side effects, state-changing MCP calls, or nested subagent dispatch). dispatch them in a **single assistant turn with multiple parallel subagent calls** (serial dispatch collapses the fan-out). 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 step
|
|
146708
|
-
- 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
|
|
146767
|
+
dispatch one \`${REVIEWER_AGENT_NAME}\` subagent per lens \u2014 its baked-in system prompt enforces the non-mutative + non-recursive contract (read-only file/search/web tools and read-only MCP queries; no writes, shell side effects, state-changing MCP calls, or nested subagent dispatch). dispatch them in a **single assistant turn with multiple parallel subagent calls** (serial dispatch collapses the fan-out). 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 step 5 entirely on a single subagent failure. each subagent gets:
|
|
146768
|
+
- 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 6), not in the subagent prompt
|
|
146709
146769
|
- **only one lens** \u2014 never a multi-section "review for X, Y, and Z" prompt
|
|
146710
146770
|
- **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\`.
|
|
146711
146771
|
- 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.
|
|
@@ -146719,15 +146779,21 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
146719
146779
|
- do NOT pre-shape their output with a finding schema
|
|
146720
146780
|
- do NOT mention the other lenses (independence is the point)
|
|
146721
146781
|
|
|
146722
|
-
|
|
146782
|
+
6. **aggregate, draft, self-critique**: merge findings; 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.
|
|
146723
146783
|
|
|
146724
|
-
|
|
146784
|
+
7. **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.
|
|
146725
146785
|
|
|
146726
|
-
|
|
146786
|
+
8. 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.
|
|
146787
|
+
|
|
146788
|
+
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.
|
|
146789
|
+
|
|
146790
|
+
Follow these rules:
|
|
146727
146791
|
- 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.
|
|
146728
|
-
- IF NO NEW ISSUES, NON-SUBSTANTIVE CHANGES ONLY (trivial formatting, import reordering, comment tweaks): do NOT submit a review.
|
|
146729
|
-
- ELSE IF NEW CRITICAL ISSUES (blocks merge): call \`${t2("create_pull_request_review")}\` with \`approved: false\`, all comments, and the review body. body opens with
|
|
146730
|
-
- ELSE IF NEW
|
|
146792
|
+
- IF NO NEW ISSUES, NON-SUBSTANTIVE CHANGES ONLY (trivial formatting, import reordering, comment tweaks): do NOT submit a review. Instead call \`${t2("report_progress")}\` with a 1-2 sentence note explaining no review was warranted (e.g. "No new issues. Changes since last review are formatting-only."). this leaves a visible signal that the run completed.
|
|
146793
|
+
- ELSE IF NEW CRITICAL ISSUES (blocks merge \u2014 bugs, security, data loss, broken core flows): call \`${t2("create_pull_request_review")}\` with \`approved: false\`, all comments, and the review body. body opens with \`> [!CAUTION]\\n> This PR introduces ...\`, then the Reviewed-changes summary.
|
|
146794
|
+
- ELSE IF NEW MUST-ADDRESS NON-CRITICAL FINDINGS (real consequences if shipped \u2014 incorrect behavior, missing validation, regressions the author should fix before merge): call \`${t2("create_pull_request_review")}\` with \`approved: false\`, all comments, and the review body. body opens with \`> [!IMPORTANT]\\n> ...\`, then the Reviewed-changes summary. Do NOT use this tier for nits, style preferences, or "consider also" suggestions.
|
|
146795
|
+
- ELSE IF NEW MINOR SUGGESTIONS ONLY (single-line nits, doc/comment polish, defer-able observations, "rough edges"): call \`${t2("create_pull_request_review")}\` with \`approved: false\`, all comments, and the review body. body opens directly with \`Reviewed the following changes:\\n\` (NO alert blockquote), then the Reviewed-changes summary.
|
|
146796
|
+
- ELSE IF INFORMATIONAL OBSERVATIONS (mergeable as-is, but worth surfacing \u2014 e.g. prior feedback addressed cleanly with one minor stale doc reference, or a noteworthy positive observation): call \`${t2("create_pull_request_review")}\` with \`approved: true\`, NO inline comments, and the review body. body opens with \`> [!NOTE]\\n> ...\` alert, then the Reviewed-changes summary. If a point is concrete enough to anchor to a line, downgrade the whole review to "minor suggestions only" (\`approved: false\`) instead \u2014 \`[!NOTE]\` and inline comments don't mix.
|
|
146731
146797
|
- ELSE IF NO NEW ISSUES, SUBSTANTIVE CHANGES (new functionality, behavior changes, or fixes to prior review feedback): call \`${t2("create_pull_request_review")}\` to create a PR review. If all previous reviews have been properly addressed and no new issues were discovered, you can set \`approved: true\`. body opens with \`No new issues. Reviewed the following changes:\\n\`, then the Reviewed-changes summary.`
|
|
146732
146798
|
},
|
|
146733
146799
|
{
|
|
@@ -146735,61 +146801,63 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
146735
146801
|
description: "Create plans, break down tasks, outline steps, analyze requirements, understand scope of work, or provide task breakdowns",
|
|
146736
146802
|
prompt: `### Checklist
|
|
146737
146803
|
|
|
146738
|
-
1.
|
|
146804
|
+
1. **task list**: create your task list for this run as your first action.
|
|
146805
|
+
|
|
146806
|
+
2. Analyze the task and gather context:
|
|
146739
146807
|
- read AGENTS.md and relevant codebase files
|
|
146740
146808
|
- understand the architecture and constraints
|
|
146741
146809
|
|
|
146742
|
-
|
|
146743
|
-
|
|
146744
|
-
3. Call \`${t2("report_progress")}\` with the plan.
|
|
146810
|
+
3. Produce a structured, actionable plan with clear milestones.
|
|
146745
146811
|
|
|
146746
|
-
|
|
146812
|
+
4. Call \`${t2("report_progress")}\` with the plan.`
|
|
146747
146813
|
},
|
|
146748
146814
|
{
|
|
146749
146815
|
name: "Fix",
|
|
146750
146816
|
description: "Fix CI failures; debug failing tests or builds; investigate and resolve check suite failures",
|
|
146751
146817
|
prompt: `### Checklist
|
|
146752
146818
|
|
|
146753
|
-
1.
|
|
146819
|
+
1. **task list**: create your task list for this run as your first action.
|
|
146754
146820
|
|
|
146755
|
-
2.
|
|
146821
|
+
2. Checkout the PR branch via \`${t2("checkout_pr")}\`.
|
|
146756
146822
|
|
|
146757
|
-
3.
|
|
146823
|
+
3. Fetch check suite logs via \`${t2("get_check_suite_logs")}\`.
|
|
146758
146824
|
|
|
146759
|
-
4.
|
|
146825
|
+
4. **CRITICAL**: verify the failure was INTRODUCED BY THIS PR before fixing. If unrelated, abort and report.
|
|
146826
|
+
|
|
146827
|
+
5. Diagnose and fix:
|
|
146760
146828
|
- read the workflow file, reproduce locally with the EXACT same commands CI runs
|
|
146761
146829
|
- fix the issue using your native file and shell tools
|
|
146762
146830
|
- verify the fix by re-running the exact CI command
|
|
146763
146831
|
- review the diff before committing \u2014 verify only the fix is present, no debug artifacts, no unrelated changes. the fix should be clean enough that a senior engineer would approve without hesitation.
|
|
146764
146832
|
- commit locally via shell (\`git add . && git commit -m "..."\`)
|
|
146765
146833
|
|
|
146766
|
-
|
|
146834
|
+
6. Finalize:
|
|
146767
146835
|
- confirm a clean working tree, then push via \`${t2("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*)
|
|
146768
|
-
- call \`${t2("report_progress")}\` with the diagnosis and fix summary (or the exact push error if push failed)
|
|
146769
|
-
|
|
146770
|
-
${learningsStep(t2, 6)}`
|
|
146836
|
+
- call \`${t2("report_progress")}\` with the diagnosis and fix summary (or the exact push error if push failed)`
|
|
146771
146837
|
},
|
|
146772
146838
|
{
|
|
146773
146839
|
name: "ResolveConflicts",
|
|
146774
146840
|
description: "Resolve merge conflicts in a PR branch against the base branch",
|
|
146775
146841
|
prompt: `### Checklist
|
|
146776
146842
|
|
|
146777
|
-
1. **
|
|
146843
|
+
1. **task list**: create your task list for this run as your first action.
|
|
146844
|
+
|
|
146845
|
+
2. **Setup**:
|
|
146778
146846
|
- Call \`${t2("checkout_pr")}\` to get the PR branch.
|
|
146779
146847
|
- Call \`${t2("get_pull_request")}\` to identify the base branch (e.g., 'main').
|
|
146780
146848
|
- Call \`${t2("git_fetch")}\` to fetch the base branch.
|
|
146781
146849
|
|
|
146782
|
-
|
|
146850
|
+
3. **Merge Attempt**:
|
|
146783
146851
|
- Run \`git merge origin/<base_branch>\` via shell.
|
|
146784
|
-
- If it succeeds automatically, confirm a clean working tree, push via \`${t2("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*), and call \`${t2("report_progress")}\` with a brief success note or the exact push error if push failed \u2014 **then stop; do not run steps
|
|
146785
|
-
- If it fails (conflicts), resolve them manually (continue to steps
|
|
146852
|
+
- If it succeeds automatically, confirm a clean working tree, push via \`${t2("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*), and call \`${t2("report_progress")}\` with a brief success note or the exact push error if push failed \u2014 **then stop; do not run steps 4\u20135.**
|
|
146853
|
+
- If it fails (conflicts), resolve them manually (continue to steps 4\u20135).
|
|
146786
146854
|
|
|
146787
|
-
|
|
146855
|
+
4. **Resolve Conflicts**:
|
|
146788
146856
|
- Run \`git status\` or parse the merge output to find the list of conflicting files.
|
|
146789
146857
|
- For each conflicting file: read it, find the conflict markers (\`<<<<<<<\`, \`=======\`, \`>>>>>>>\`), understand the code context, and rewrite the file with the correct resolution. Remove all markers.
|
|
146790
146858
|
- Verify the file syntax is correct after resolution.
|
|
146791
146859
|
|
|
146792
|
-
|
|
146860
|
+
5. **Finalize**:
|
|
146793
146861
|
- Run a final verification (build/test) to ensure the resolution works.
|
|
146794
146862
|
- \`git add . && git commit -m "resolve merge conflicts"\`
|
|
146795
146863
|
- confirm a clean working tree, then push via \`${t2("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*)
|
|
@@ -146800,24 +146868,43 @@ ${learningsStep(t2, 6)}`
|
|
|
146800
146868
|
description: "General-purpose tasks that don't fit other modes: answering questions, adding comments, labeling, running ad-hoc commands, or any direct request",
|
|
146801
146869
|
prompt: `### Checklist
|
|
146802
146870
|
|
|
146803
|
-
1.
|
|
146871
|
+
1. **task list**: create your task list for this run as your first action.
|
|
146872
|
+
|
|
146873
|
+
2. Analyze the task. For simple operations (labeling, commenting, answering questions, running a single command), handle directly.
|
|
146804
146874
|
|
|
146805
|
-
|
|
146875
|
+
3. For substantial work \u2014 code changes across multiple files, multi-step investigations:
|
|
146806
146876
|
- plan your approach before starting
|
|
146807
146877
|
- use native file and shell tools for local operations
|
|
146808
146878
|
- use ${pullfrogMcpName} MCP tools for GitHub/git operations
|
|
146809
146879
|
- if code changes are needed: review your own diff before committing \u2014 verify only intended changes are present, no debug artifacts remain, and the changes are clean enough that a senior engineer would approve without hesitation
|
|
146810
146880
|
|
|
146811
|
-
|
|
146881
|
+
4. Finalize:
|
|
146812
146882
|
- if code changes were made, push to a pull request (new or existing) using \`${t2("push_branch")}\` and \`${t2("create_pull_request")}\` as needed. \`git status\` must be clean before you finish (see *SYSTEM* Git rules if push fails).
|
|
146813
146883
|
- call \`${t2("report_progress")}\` once with results \u2014 include exact tool errors if push or PR creation failed
|
|
146814
|
-
- if the task involved labeling, commenting, or other GitHub operations, perform those directly
|
|
146815
|
-
|
|
146816
|
-
${learningsStep(t2, 4)}`
|
|
146884
|
+
- if the task involved labeling, commenting, or other GitHub operations, perform those directly`
|
|
146817
146885
|
}
|
|
146818
146886
|
];
|
|
146819
146887
|
}
|
|
146820
146888
|
var modes = computeModes("opencode");
|
|
146889
|
+
var NON_COMMITTING_MODES = /* @__PURE__ */ new Set([
|
|
146890
|
+
"Review",
|
|
146891
|
+
"IncrementalReview",
|
|
146892
|
+
"Plan"
|
|
146893
|
+
]);
|
|
146894
|
+
|
|
146895
|
+
// toolState.ts
|
|
146896
|
+
function initToolState(params) {
|
|
146897
|
+
const resolved = parseProgressComment(params.progressComment);
|
|
146898
|
+
if (resolved) {
|
|
146899
|
+
log.info(`\xBB using pre-created progress comment: ${resolved.id} (${resolved.type})`);
|
|
146900
|
+
}
|
|
146901
|
+
return {
|
|
146902
|
+
progressComment: resolved,
|
|
146903
|
+
hadProgressComment: !!resolved,
|
|
146904
|
+
backgroundProcesses: /* @__PURE__ */ new Map(),
|
|
146905
|
+
usageEntries: []
|
|
146906
|
+
};
|
|
146907
|
+
}
|
|
146821
146908
|
|
|
146822
146909
|
// agents/claude.ts
|
|
146823
146910
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
@@ -146913,6 +147000,17 @@ async function installFromNpmTarball(params) {
|
|
|
146913
147000
|
// utils/providerErrors.ts
|
|
146914
147001
|
var statusKey = `\\b(?:status[_ ]?code|http[_ ]?status|status)["']?\\s*[:=]\\s*["']?`;
|
|
146915
147002
|
var PROVIDER_ERROR_PATTERNS = [
|
|
147003
|
+
// auth patterns must come BEFORE rate-limit patterns. OpenRouter 401 error
|
|
147004
|
+
// payloads carry `x-ratelimit-*` response headers in the dump, and the
|
|
147005
|
+
// free-form rate-limit regex below would otherwise win on word-boundary
|
|
147006
|
+
// matches inside header names. canonical 401 messages: OpenRouter returns
|
|
147007
|
+
// `{"error":{"message":"User not found","code":401}}` for disabled or
|
|
147008
|
+
// invalid keys (https://openai.luzhipeng.com/docs/api/reference/errors-and-debugging).
|
|
147009
|
+
{ regex: new RegExp(`${statusKey}401\\b`, "i"), label: "auth error (401)" },
|
|
147010
|
+
{ regex: new RegExp(`${statusKey}403\\b`, "i"), label: "auth error (403)" },
|
|
147011
|
+
{ regex: /\bUser not found\b/i, label: "auth error (invalid/disabled key)" },
|
|
147012
|
+
{ regex: /\bInvalid authentication\b/i, label: "auth error (invalid credentials)" },
|
|
147013
|
+
{ regex: /\bNo auth credentials found\b/i, label: "auth error (missing credentials)" },
|
|
146916
147014
|
{ regex: new RegExp(`${statusKey}429\\b`, "i"), label: "rate limited (429)" },
|
|
146917
147015
|
{ regex: new RegExp(`${statusKey}500\\b`, "i"), label: "provider 500 error" },
|
|
146918
147016
|
{ regex: new RegExp(`${statusKey}503\\b`, "i"), label: "provider unavailable (503)" },
|
|
@@ -146976,7 +147074,7 @@ function installBundledSkills(params) {
|
|
|
146976
147074
|
writeFileSync6(join9(skillDir, "SKILL.md"), content);
|
|
146977
147075
|
}
|
|
146978
147076
|
}
|
|
146979
|
-
log.
|
|
147077
|
+
log.success(`installed bundled skills: ${BUNDLED_SKILL_NAMES.join(", ")}`);
|
|
146980
147078
|
}
|
|
146981
147079
|
function addSkill(params) {
|
|
146982
147080
|
const result = spawnSync5(
|
|
@@ -147001,7 +147099,7 @@ function addSkill(params) {
|
|
|
147001
147099
|
}
|
|
147002
147100
|
);
|
|
147003
147101
|
if (result.status === 0) {
|
|
147004
|
-
log.
|
|
147102
|
+
log.success(`installed ${params.skill} skill (${params.agent})`);
|
|
147005
147103
|
} else {
|
|
147006
147104
|
const stderr = (result.stderr?.toString() || "").trim();
|
|
147007
147105
|
const errorMsg = result.error ? result.error.message : stderr;
|
|
@@ -147053,6 +147151,13 @@ var ThinkingTimer = class {
|
|
|
147053
147151
|
|
|
147054
147152
|
// agents/postRun.ts
|
|
147055
147153
|
import { readFile } from "node:fs/promises";
|
|
147154
|
+
function getUnsubmittedReview(toolState) {
|
|
147155
|
+
const mode = toolState.selectedMode;
|
|
147156
|
+
if (mode !== "Review" && mode !== "IncrementalReview") return null;
|
|
147157
|
+
if (toolState.review || toolState.finalSummaryWritten) return null;
|
|
147158
|
+
if (!toolState.hadProgressComment) return null;
|
|
147159
|
+
return mode;
|
|
147160
|
+
}
|
|
147056
147161
|
var MAX_HOOK_OUTPUT_CHARS = 4096;
|
|
147057
147162
|
function truncateHookOutput(raw2) {
|
|
147058
147163
|
if (raw2.length <= MAX_HOOK_OUTPUT_CHARS) return raw2;
|
|
@@ -147114,39 +147219,72 @@ function buildSummaryStalePrompt(filePath) {
|
|
|
147114
147219
|
"if the diff is genuinely too small or noisy to warrant rewriting (e.g. a one-line typo fix, a comment tweak, a formatting-only change), it's fine to leave the structure as-is \u2014 but at minimum confirm you considered it by appending one line to the appropriate section noting the run. silence is not an option; the snapshot is what the next review run reads as context."
|
|
147115
147220
|
].join("\n");
|
|
147116
147221
|
}
|
|
147117
|
-
|
|
147222
|
+
function buildUnsubmittedReviewPrompt(mode) {
|
|
147223
|
+
if (mode === "Review") {
|
|
147224
|
+
return [
|
|
147225
|
+
`MISSING REVIEW OUTPUT \u2014 you selected Review mode but stopped without calling \`create_pull_request_review\`. the user has no visible signal that this run produced anything; the progress comment will be deleted on exit and no review will appear on the PR.`,
|
|
147226
|
+
"",
|
|
147227
|
+
"call `create_pull_request_review` now with your aggregated review (body + inline comments). pick the tier per the mode prompt \u2014 Review mode has no no-submit exit, so even informational `> [!NOTE]` reviews and `No new issues found.` reviews must be submitted (both use `approved: true`). the first call may error once with a diff-coverage nudge \u2014 retry the same call to proceed.",
|
|
147228
|
+
"",
|
|
147229
|
+
"do NOT stop again until `create_pull_request_review` has been called successfully."
|
|
147230
|
+
].join("\n");
|
|
147231
|
+
}
|
|
147232
|
+
return [
|
|
147233
|
+
`MISSING REVIEW OUTPUT \u2014 you selected IncrementalReview mode but stopped without calling \`create_pull_request_review\` or \`report_progress\`. the user has no visible signal that this run produced anything; the progress comment will be deleted on exit and no review will appear on the PR.`,
|
|
147234
|
+
"",
|
|
147235
|
+
"do exactly one of:",
|
|
147236
|
+
"- if you have findings: call `create_pull_request_review` now with your aggregated review (body + inline comments). the first call may error once with a diff-coverage nudge \u2014 retry the same call to proceed.",
|
|
147237
|
+
"- if there are genuinely no actionable findings since the last review (e.g. only formatting / comment / lockfile changes): call `report_progress` with a 1-2 sentence summary explaining that no review was warranted.",
|
|
147238
|
+
"",
|
|
147239
|
+
"do NOT stop again until one of those tools has been called successfully."
|
|
147240
|
+
].join("\n");
|
|
147241
|
+
}
|
|
147242
|
+
async function collectPostRunIssues(ctx, options = {}) {
|
|
147118
147243
|
const issues = {};
|
|
147119
|
-
if (
|
|
147120
|
-
const failure = await executeStopHook(
|
|
147244
|
+
if (ctx.stopScript) {
|
|
147245
|
+
const failure = await executeStopHook(ctx.stopScript);
|
|
147121
147246
|
if (failure) issues.stopHook = failure;
|
|
147122
147247
|
}
|
|
147123
147248
|
const status = getGitStatus();
|
|
147124
|
-
|
|
147125
|
-
if (
|
|
147126
|
-
|
|
147127
|
-
|
|
147249
|
+
const mode = ctx.toolState.selectedMode;
|
|
147250
|
+
if (status) {
|
|
147251
|
+
if (mode && NON_COMMITTING_MODES.has(mode)) {
|
|
147252
|
+
log.info(`\xBB dirty-tree gate suppressed: mode \`${mode}\` does not commit`);
|
|
147253
|
+
} else {
|
|
147254
|
+
issues.dirtyTree = status;
|
|
147255
|
+
}
|
|
147128
147256
|
}
|
|
147257
|
+
const summaryFilePath2 = ctx.toolState.summaryFilePath;
|
|
147258
|
+
const summarySeed = ctx.toolState.summarySeed;
|
|
147259
|
+
if (!options.skipSummaryStale && summaryFilePath2 && summarySeed !== void 0) {
|
|
147260
|
+
const stale = await isSummaryUnchanged(summaryFilePath2, summarySeed);
|
|
147261
|
+
if (stale) issues.summaryStale = { filePath: summaryFilePath2 };
|
|
147262
|
+
}
|
|
147263
|
+
const unsubmittedMode = getUnsubmittedReview(ctx.toolState);
|
|
147264
|
+
if (unsubmittedMode) issues.unsubmittedReview = unsubmittedMode;
|
|
147129
147265
|
return issues;
|
|
147130
147266
|
}
|
|
147131
147267
|
function buildPostRunPrompt(issues) {
|
|
147132
147268
|
const parts = [];
|
|
147133
147269
|
if (issues.stopHook) parts.push(buildStopHookPrompt(issues.stopHook));
|
|
147270
|
+
if (issues.unsubmittedReview) {
|
|
147271
|
+
parts.push(buildUnsubmittedReviewPrompt(issues.unsubmittedReview));
|
|
147272
|
+
}
|
|
147134
147273
|
if (issues.dirtyTree) parts.push(buildCommitPrompt(issues.dirtyTree));
|
|
147135
147274
|
if (issues.summaryStale) parts.push(buildSummaryStalePrompt(issues.summaryStale.filePath));
|
|
147136
147275
|
return parts.join("\n\n---\n\n");
|
|
147137
147276
|
}
|
|
147138
|
-
function buildLearningsReflectionPrompt(
|
|
147139
|
-
const t2 = (name) => formatMcpToolRef(agentId, name);
|
|
147277
|
+
function buildLearningsReflectionPrompt(filePath) {
|
|
147140
147278
|
return [
|
|
147141
|
-
`REFLECTION \u2014 before you finish, think back over this task: did you discover anything about this repo's setup, test commands, conventions, or patterns that
|
|
147279
|
+
`REFLECTION \u2014 before you finish, think back over this task: did you discover anything about this repo's setup, test commands, conventions, or patterns that is high-confidence and would reliably help future runs?`,
|
|
147142
147280
|
"",
|
|
147143
|
-
`
|
|
147281
|
+
`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.`,
|
|
147144
147282
|
"",
|
|
147145
|
-
`
|
|
147146
|
-
`- only
|
|
147147
|
-
`-
|
|
147148
|
-
`-
|
|
147149
|
-
`- if you
|
|
147283
|
+
`keep the file healthy:`,
|
|
147284
|
+
`- only add bullets when the finding is high-confidence AND broadly useful. skip speculative, one-off, or "maybe" findings.`,
|
|
147285
|
+
`- prune bullets that are clearly wrong, no longer relevant, or low-signal (rarely useful). a focused, accurate file beats a long stale one.`,
|
|
147286
|
+
`- format: flat bullet list, one fact per line starting with \`- \`. deduplicate against existing entries \u2014 if a bullet covers the same fact, update it in place instead of adding a duplicate.`,
|
|
147287
|
+
`- leave the file alone if you have nothing substantively new to add and the existing entries still look healthy. silence is a valid outcome \u2014 just reply "done" and stop.`
|
|
147150
147288
|
].join("\n");
|
|
147151
147289
|
}
|
|
147152
147290
|
async function runPostRunRetryLoop(params) {
|
|
@@ -147158,10 +147296,8 @@ async function runPostRunRetryLoop(params) {
|
|
|
147158
147296
|
let summaryStaleNudged = false;
|
|
147159
147297
|
while (gateResumeCount < MAX_POST_RUN_RETRIES) {
|
|
147160
147298
|
if (!result.success) break;
|
|
147161
|
-
const issues = await collectPostRunIssues({
|
|
147162
|
-
|
|
147163
|
-
summaryFilePath: summaryStaleNudged ? void 0 : params.summaryFilePath,
|
|
147164
|
-
summarySeed: summaryStaleNudged ? void 0 : params.summarySeed
|
|
147299
|
+
const issues = await collectPostRunIssues(params.ctx, {
|
|
147300
|
+
skipSummaryStale: summaryStaleNudged
|
|
147165
147301
|
});
|
|
147166
147302
|
if (issues.summaryStale) summaryStaleNudged = true;
|
|
147167
147303
|
finalIssues = issues;
|
|
@@ -147209,7 +147345,7 @@ async function runPostRunRetryLoop(params) {
|
|
|
147209
147345
|
gateResumeCount++;
|
|
147210
147346
|
}
|
|
147211
147347
|
if (gateResumeCount > 0 && result.success && hasPostRunIssues(finalIssues)) {
|
|
147212
|
-
finalIssues = await collectPostRunIssues({
|
|
147348
|
+
finalIssues = await collectPostRunIssues(params.ctx, { skipSummaryStale: true });
|
|
147213
147349
|
}
|
|
147214
147350
|
if (result.success && finalIssues.stopHook) {
|
|
147215
147351
|
const retryNote = gateResumeCount > 0 ? ` after ${gateResumeCount} retry ${gateResumeCount === 1 ? "attempt" : "attempts"}` : "";
|
|
@@ -147220,6 +147356,16 @@ async function runPostRunRetryLoop(params) {
|
|
|
147220
147356
|
usage: aggregatedUsage
|
|
147221
147357
|
};
|
|
147222
147358
|
}
|
|
147359
|
+
if (result.success && finalIssues.unsubmittedReview) {
|
|
147360
|
+
const retryNote = gateResumeCount > 0 ? ` after ${gateResumeCount} retry ${gateResumeCount === 1 ? "attempt" : "attempts"}` : "";
|
|
147361
|
+
const expected = finalIssues.unsubmittedReview === "Review" ? "create_pull_request_review" : "create_pull_request_review or report_progress";
|
|
147362
|
+
return {
|
|
147363
|
+
...result,
|
|
147364
|
+
success: false,
|
|
147365
|
+
error: `${finalIssues.unsubmittedReview} mode finished without calling ${expected}${retryNote}`,
|
|
147366
|
+
usage: aggregatedUsage
|
|
147367
|
+
};
|
|
147368
|
+
}
|
|
147223
147369
|
return { ...result, usage: aggregatedUsage };
|
|
147224
147370
|
}
|
|
147225
147371
|
|
|
@@ -147336,6 +147482,12 @@ function resolveEffort(model) {
|
|
|
147336
147482
|
if (model?.includes("opus")) return "max";
|
|
147337
147483
|
return "high";
|
|
147338
147484
|
}
|
|
147485
|
+
function tailLines(text, maxCodeUnits) {
|
|
147486
|
+
if (text.length <= maxCodeUnits) return text;
|
|
147487
|
+
const tail = text.slice(-maxCodeUnits);
|
|
147488
|
+
const firstNewline = tail.indexOf("\n");
|
|
147489
|
+
return firstNewline > 0 && firstNewline < tail.length - 1 ? tail.slice(firstNewline + 1) : tail;
|
|
147490
|
+
}
|
|
147339
147491
|
async function runClaude(params) {
|
|
147340
147492
|
const startTime = performance6.now();
|
|
147341
147493
|
let eventCount = 0;
|
|
@@ -147343,6 +147495,8 @@ async function runClaude(params) {
|
|
|
147343
147495
|
let finalOutput = "";
|
|
147344
147496
|
let sessionId;
|
|
147345
147497
|
let resultErrorSubtype = null;
|
|
147498
|
+
let lastResultError = null;
|
|
147499
|
+
let syntheticStopFailure = false;
|
|
147346
147500
|
let accumulatedTokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
147347
147501
|
let accumulatedCostUsd = 0;
|
|
147348
147502
|
let tokensLogged = false;
|
|
@@ -147425,6 +147579,16 @@ async function runClaude(params) {
|
|
|
147425
147579
|
if (event.session_id) sessionId = event.session_id;
|
|
147426
147580
|
const subtype = event.subtype || "unknown";
|
|
147427
147581
|
const numTurns = event.num_turns || 0;
|
|
147582
|
+
if (event.is_error === true && subtype === "success") {
|
|
147583
|
+
const apiStatus = event.api_error_status;
|
|
147584
|
+
lastResultError = event.result?.trim() || `claude reported is_error=true with no result text (api_error_status=${apiStatus ?? "unknown"})`;
|
|
147585
|
+
resultErrorSubtype = subtype;
|
|
147586
|
+
syntheticStopFailure = true;
|
|
147587
|
+
log.info(
|
|
147588
|
+
`\xBB ${params.label} result error: subtype=${subtype}, api_error_status=${apiStatus ?? "unknown"}, message=${lastResultError}`
|
|
147589
|
+
);
|
|
147590
|
+
return;
|
|
147591
|
+
}
|
|
147428
147592
|
if (subtype === "success") {
|
|
147429
147593
|
const usage = event.usage;
|
|
147430
147594
|
const inputTokens = usage?.input_tokens || 0;
|
|
@@ -147447,12 +147611,15 @@ async function runClaude(params) {
|
|
|
147447
147611
|
}
|
|
147448
147612
|
} else if (subtype === "error_max_turns") {
|
|
147449
147613
|
resultErrorSubtype = subtype;
|
|
147614
|
+
lastResultError = event.errors?.join("\n").trim() || null;
|
|
147450
147615
|
log.info(`\xBB ${params.label} max turns reached: ${JSON.stringify(event)}`);
|
|
147451
147616
|
} else if (subtype === "error_during_execution") {
|
|
147452
147617
|
resultErrorSubtype = subtype;
|
|
147618
|
+
lastResultError = event.errors?.join("\n").trim() || null;
|
|
147453
147619
|
log.info(`\xBB ${params.label} execution error: ${JSON.stringify(event)}`);
|
|
147454
147620
|
} else if (subtype.startsWith("error")) {
|
|
147455
147621
|
resultErrorSubtype = subtype;
|
|
147622
|
+
lastResultError = event.errors?.join("\n").trim() || null;
|
|
147456
147623
|
log.info(`\xBB ${params.label} result: subtype=${subtype}, data=${JSON.stringify(event)}`);
|
|
147457
147624
|
} else {
|
|
147458
147625
|
log.info(`\xBB ${params.label} result: subtype=${subtype}, data=${JSON.stringify(event)}`);
|
|
@@ -147560,14 +147727,15 @@ async function runClaude(params) {
|
|
|
147560
147727
|
if (stderrContext) log.info(`\xBB last stderr output:
|
|
147561
147728
|
${stderrContext}`);
|
|
147562
147729
|
}
|
|
147563
|
-
if (!tokensLogged && (accumulatedTokens.input > 0 || accumulatedTokens.output > 0 || accumulatedTokens.cacheRead > 0 || accumulatedTokens.cacheWrite > 0)) {
|
|
147730
|
+
if (!tokensLogged && !syntheticStopFailure && (accumulatedTokens.input > 0 || accumulatedTokens.output > 0 || accumulatedTokens.cacheRead > 0 || accumulatedTokens.cacheWrite > 0)) {
|
|
147564
147731
|
logTokenTable({ ...accumulatedTokens, costUsd: accumulatedCostUsd });
|
|
147565
147732
|
tokensLogged = true;
|
|
147566
147733
|
}
|
|
147567
147734
|
const usage = buildUsage();
|
|
147568
147735
|
if (result.exitCode !== 0) {
|
|
147569
147736
|
const errorContext = lastProviderError ? ` (${lastProviderError})` : "";
|
|
147570
|
-
const
|
|
147737
|
+
const truncatedStdout = result.stdout ? tailLines(result.stdout, 2048) : "";
|
|
147738
|
+
const errorMessage = lastResultError || result.stderr || truncatedStdout || `unknown error - no output from Claude CLI${errorContext}`;
|
|
147571
147739
|
log.error(
|
|
147572
147740
|
`${params.label} exited with code ${result.exitCode}${errorContext}: ${errorMessage}`
|
|
147573
147741
|
);
|
|
@@ -147594,7 +147762,7 @@ ${stderrContext}`);
|
|
|
147594
147762
|
return {
|
|
147595
147763
|
success: false,
|
|
147596
147764
|
output: finalOutput || output,
|
|
147597
|
-
error: `result subtype: ${resultErrorSubtype}`,
|
|
147765
|
+
error: lastResultError || `result subtype: ${resultErrorSubtype}`,
|
|
147598
147766
|
usage,
|
|
147599
147767
|
sessionId
|
|
147600
147768
|
};
|
|
@@ -147724,12 +147892,10 @@ var claude = agent({
|
|
|
147724
147892
|
args: [...baseArgs, "-p", ctx.instructions.full]
|
|
147725
147893
|
});
|
|
147726
147894
|
return runPostRunRetryLoop({
|
|
147895
|
+
ctx,
|
|
147727
147896
|
initialResult: result,
|
|
147728
147897
|
initialUsage: result.usage,
|
|
147729
|
-
|
|
147730
|
-
summaryFilePath: ctx.summaryFilePath,
|
|
147731
|
-
summarySeed: ctx.summarySeed,
|
|
147732
|
-
reflectionPrompt: buildLearningsReflectionPrompt("claude"),
|
|
147898
|
+
reflectionPrompt: ctx.toolState.learningsFilePath ? buildLearningsReflectionPrompt(ctx.toolState.learningsFilePath) : void 0,
|
|
147733
147899
|
canResume: (r) => Boolean(r.sessionId),
|
|
147734
147900
|
resume: async (c2) => {
|
|
147735
147901
|
const sessionId = c2.previousResult.sessionId;
|
|
@@ -147745,9 +147911,92 @@ var claude = agent({
|
|
|
147745
147911
|
|
|
147746
147912
|
// agents/opencode.ts
|
|
147747
147913
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
147748
|
-
import { mkdirSync as mkdirSync5 } from "node:fs";
|
|
147914
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "node:fs";
|
|
147749
147915
|
import { join as join11 } from "node:path";
|
|
147750
147916
|
import { performance as performance7 } from "node:perf_hooks";
|
|
147917
|
+
|
|
147918
|
+
// agents/opencodePlugin.ts
|
|
147919
|
+
var PULLFROG_BUS_EVENT_TYPE = "pullfrog_bus_event";
|
|
147920
|
+
var PULLFROG_OPENCODE_PLUGIN_FILENAME = "pullfrog-events.ts";
|
|
147921
|
+
var PULLFROG_OPENCODE_PLUGIN_SOURCE = `// AUTOGENERATED by Pullfrog. do not edit; it'll be overwritten on the next run.
|
|
147922
|
+
// surfaces opencode subagent activity that the CLI's run-loop discards. see
|
|
147923
|
+
// action/agents/opencodePlugin.ts in pullfrog/app for why this exists. lives
|
|
147924
|
+
// inside the per-run tmpdir (XDG_CONFIG_HOME/opencode/plugin/), never inside
|
|
147925
|
+
// the user's working tree.
|
|
147926
|
+
|
|
147927
|
+
const PULLFROG_BUS_EVENT_TYPE = ${JSON.stringify(PULLFROG_BUS_EVENT_TYPE)};
|
|
147928
|
+
|
|
147929
|
+
// the first sessionID we see on a message.part.updated event is the
|
|
147930
|
+
// orchestrator \u2014 opencode's run command creates exactly one top-level session
|
|
147931
|
+
// before any subagent is dispatched, and the user-prompt text part fires
|
|
147932
|
+
// before the first task tool_use. we lock that sessionID in here and use it
|
|
147933
|
+
// to filter: the orchestrator's events are already streamed by the CLI's
|
|
147934
|
+
// run-loop, so we only forward (a) all subagent events, and (b) the
|
|
147935
|
+
// orchestrator's task tool dispatches at status="running". the CLI only
|
|
147936
|
+
// emits task tool_use at status=completed (after the subagent finishes), so
|
|
147937
|
+
// without the early announce the parent's labeler binds subagent sessions
|
|
147938
|
+
// before recordTaskDispatch fires and the lens label is lost.
|
|
147939
|
+
let orchestratorSessionID: string | undefined;
|
|
147940
|
+
|
|
147941
|
+
function isOrchestratorTaskDispatch(part: {
|
|
147942
|
+
type?: string;
|
|
147943
|
+
tool?: string;
|
|
147944
|
+
state?: { status?: string };
|
|
147945
|
+
}): boolean {
|
|
147946
|
+
if (part.type !== "tool") return false;
|
|
147947
|
+
if (part.tool !== "task") return false;
|
|
147948
|
+
// only forward at status="running" (not "pending"). at pending the
|
|
147949
|
+
// state.input is still {} \u2014 the orchestrator has emitted the part shell
|
|
147950
|
+
// but the LLM hasn't filled in description/subagent_type/prompt yet. by
|
|
147951
|
+
// running, input is populated and recordTaskDispatch can derive the lens
|
|
147952
|
+
// label correctly.
|
|
147953
|
+
return part.state?.status === "running";
|
|
147954
|
+
}
|
|
147955
|
+
|
|
147956
|
+
export default async function pullfrogEventsPlugin() {
|
|
147957
|
+
return {
|
|
147958
|
+
event: async (input: {
|
|
147959
|
+
event: {
|
|
147960
|
+
type: string;
|
|
147961
|
+
properties?: {
|
|
147962
|
+
part?: {
|
|
147963
|
+
sessionID?: string;
|
|
147964
|
+
type?: string;
|
|
147965
|
+
tool?: string;
|
|
147966
|
+
state?: { status?: string };
|
|
147967
|
+
};
|
|
147968
|
+
};
|
|
147969
|
+
};
|
|
147970
|
+
}) => {
|
|
147971
|
+
const event = input.event;
|
|
147972
|
+
if (!event || typeof event !== "object") return;
|
|
147973
|
+
if (event.type !== "message.part.updated") return;
|
|
147974
|
+
const part = event.properties?.part;
|
|
147975
|
+
const sessionID = part?.sessionID;
|
|
147976
|
+
if (typeof sessionID !== "string" || sessionID.length === 0) return;
|
|
147977
|
+
if (orchestratorSessionID === undefined) orchestratorSessionID = sessionID;
|
|
147978
|
+
|
|
147979
|
+
if (sessionID === orchestratorSessionID) {
|
|
147980
|
+
// skip orchestrator events EXCEPT early task dispatches.
|
|
147981
|
+
if (!part || !isOrchestratorTaskDispatch(part)) return;
|
|
147982
|
+
}
|
|
147983
|
+
|
|
147984
|
+
try {
|
|
147985
|
+
const line = JSON.stringify({
|
|
147986
|
+
type: PULLFROG_BUS_EVENT_TYPE,
|
|
147987
|
+
bus_event: event,
|
|
147988
|
+
});
|
|
147989
|
+
process.stdout.write(line + "\\n");
|
|
147990
|
+
} catch {
|
|
147991
|
+
// a circular reference or BigInt etc. would throw; swallow rather
|
|
147992
|
+
// than letting a single bad event take down the plugin.
|
|
147993
|
+
}
|
|
147994
|
+
},
|
|
147995
|
+
};
|
|
147996
|
+
}
|
|
147997
|
+
`;
|
|
147998
|
+
|
|
147999
|
+
// agents/opencode.ts
|
|
147751
148000
|
async function installOpencodeCli() {
|
|
147752
148001
|
return await installFromNpmTarball({
|
|
147753
148002
|
packageName: "opencode-ai",
|
|
@@ -147757,6 +148006,8 @@ async function installOpencodeCli() {
|
|
|
147757
148006
|
});
|
|
147758
148007
|
}
|
|
147759
148008
|
var PULLFROG_OPENCODE_OUTPUT_LIMIT = 5e3;
|
|
148009
|
+
var GEMINI_3_DIRECT_THINKING_LEVEL = "medium";
|
|
148010
|
+
var GEMINI_3_DIRECT_API_IDS = ["gemini-3.1-pro-preview", "gemini-3-flash-preview"];
|
|
147760
148011
|
function buildSecurityConfig(ctx, model) {
|
|
147761
148012
|
const config3 = {
|
|
147762
148013
|
permission: {
|
|
@@ -147770,7 +148021,21 @@ function buildSecurityConfig(ctx, model) {
|
|
|
147770
148021
|
mcp: {
|
|
147771
148022
|
[pullfrogMcpName]: { type: "remote", url: ctx.mcpServerUrl }
|
|
147772
148023
|
},
|
|
147773
|
-
agent: buildReviewerAgentConfig()
|
|
148024
|
+
agent: buildReviewerAgentConfig(),
|
|
148025
|
+
provider: {
|
|
148026
|
+
google: {
|
|
148027
|
+
models: Object.fromEntries(
|
|
148028
|
+
GEMINI_3_DIRECT_API_IDS.map((id) => [
|
|
148029
|
+
id,
|
|
148030
|
+
{
|
|
148031
|
+
options: {
|
|
148032
|
+
thinkingConfig: { thinkingLevel: GEMINI_3_DIRECT_THINKING_LEVEL }
|
|
148033
|
+
}
|
|
148034
|
+
}
|
|
148035
|
+
])
|
|
148036
|
+
)
|
|
148037
|
+
}
|
|
148038
|
+
}
|
|
147774
148039
|
};
|
|
147775
148040
|
if (model) {
|
|
147776
148041
|
config3.model = model;
|
|
@@ -147849,9 +148114,6 @@ async function runOpenCode(params) {
|
|
|
147849
148114
|
const taskDispatchByCallID = /* @__PURE__ */ new Map();
|
|
147850
148115
|
const pendingTaskDispatches = [];
|
|
147851
148116
|
const knownNonTaskCallIDs = /* @__PURE__ */ new Set();
|
|
147852
|
-
function isSubagentInFlight() {
|
|
147853
|
-
return taskDispatchByCallID.size > 0 || pendingTaskDispatches.length > 0;
|
|
147854
|
-
}
|
|
147855
148117
|
function emitSubagentFinished(dispatch, status, output2, matchKind) {
|
|
147856
148118
|
const subagentDuration = performance7.now() - dispatch.startedAt;
|
|
147857
148119
|
const outputStr = typeof output2 === "string" ? output2 : "";
|
|
@@ -147970,18 +148232,20 @@ async function runOpenCode(params) {
|
|
|
147970
148232
|
return;
|
|
147971
148233
|
}
|
|
147972
148234
|
if (toolName === "task") {
|
|
147973
|
-
|
|
147974
|
-
|
|
147975
|
-
|
|
147976
|
-
|
|
147977
|
-
|
|
147978
|
-
|
|
147979
|
-
|
|
147980
|
-
|
|
147981
|
-
|
|
147982
|
-
|
|
147983
|
-
|
|
147984
|
-
|
|
148235
|
+
if (!taskDispatchByCallID.has(toolId)) {
|
|
148236
|
+
const taskInput = event.part?.state?.input ?? {};
|
|
148237
|
+
const dispatchedLabel = labeler.recordTaskDispatch(taskInput);
|
|
148238
|
+
const dispatch = {
|
|
148239
|
+
label: dispatchedLabel,
|
|
148240
|
+
startedAt: performance7.now(),
|
|
148241
|
+
toolUseCallID: toolId
|
|
148242
|
+
};
|
|
148243
|
+
taskDispatchByCallID.set(toolId, dispatch);
|
|
148244
|
+
pendingTaskDispatches.push(dispatch);
|
|
148245
|
+
log.info(
|
|
148246
|
+
`\xBB dispatching subagent: ${dispatchedLabel}` + (taskInput.subagent_type ? ` (subagent_type=${taskInput.subagent_type})` : "")
|
|
148247
|
+
);
|
|
148248
|
+
}
|
|
147985
148249
|
} else {
|
|
147986
148250
|
knownNonTaskCallIDs.add(toolId);
|
|
147987
148251
|
}
|
|
@@ -148002,6 +148266,10 @@ async function runOpenCode(params) {
|
|
|
148002
148266
|
if (event.part?.state?.status === "completed" && event.part.state.output) {
|
|
148003
148267
|
log.debug(withLabel(label, ` output: ${event.part.state.output}`));
|
|
148004
148268
|
}
|
|
148269
|
+
if (event.part?.state?.status === "error") {
|
|
148270
|
+
const errorMsg = event.part.state.output ?? "(no error message)";
|
|
148271
|
+
log.info(withLabel(label, `\xBB tool call failed: ${errorMsg}`));
|
|
148272
|
+
}
|
|
148005
148273
|
if (toolName.includes("report_progress") && params.todoTracker) {
|
|
148006
148274
|
log.debug("\xBB report_progress detected, disabling todo tracking");
|
|
148007
148275
|
params.todoTracker.cancel();
|
|
@@ -148088,6 +148356,53 @@ async function runOpenCode(params) {
|
|
|
148088
148356
|
tokensLogged = true;
|
|
148089
148357
|
}
|
|
148090
148358
|
}
|
|
148359
|
+
},
|
|
148360
|
+
[PULLFROG_BUS_EVENT_TYPE]: async (event) => {
|
|
148361
|
+
const busEvent = event.bus_event;
|
|
148362
|
+
if (!busEvent || busEvent.type !== "message.part.updated") return;
|
|
148363
|
+
const part = busEvent.properties?.part;
|
|
148364
|
+
if (!part || typeof part.sessionID !== "string") return;
|
|
148365
|
+
const sessionID = part.sessionID;
|
|
148366
|
+
const partType = part.type;
|
|
148367
|
+
if (partType === "tool") {
|
|
148368
|
+
const status = part.state?.status;
|
|
148369
|
+
const partWithToolFields = part;
|
|
148370
|
+
const isOrchestratorTaskDispatch = partWithToolFields.tool === "task" && status === "running";
|
|
148371
|
+
if (isOrchestratorTaskDispatch) {
|
|
148372
|
+
const callID = partWithToolFields.callID;
|
|
148373
|
+
if (typeof callID === "string" && !taskDispatchByCallID.has(callID)) {
|
|
148374
|
+
const taskInput = partWithToolFields.state?.input ?? {};
|
|
148375
|
+
const dispatchedLabel = labeler.recordTaskDispatch(taskInput);
|
|
148376
|
+
const dispatch = {
|
|
148377
|
+
label: dispatchedLabel,
|
|
148378
|
+
startedAt: performance7.now(),
|
|
148379
|
+
toolUseCallID: callID
|
|
148380
|
+
};
|
|
148381
|
+
taskDispatchByCallID.set(callID, dispatch);
|
|
148382
|
+
pendingTaskDispatches.push(dispatch);
|
|
148383
|
+
log.info(
|
|
148384
|
+
`\xBB dispatching subagent: ${dispatchedLabel}` + (taskInput.subagent_type ? ` (subagent_type=${taskInput.subagent_type})` : "")
|
|
148385
|
+
);
|
|
148386
|
+
}
|
|
148387
|
+
return;
|
|
148388
|
+
}
|
|
148389
|
+
if (status !== "completed" && status !== "error") return;
|
|
148390
|
+
await handlers2.tool_use({
|
|
148391
|
+
type: "tool_use",
|
|
148392
|
+
sessionID,
|
|
148393
|
+
part
|
|
148394
|
+
});
|
|
148395
|
+
return;
|
|
148396
|
+
}
|
|
148397
|
+
if (partType === "step-start" || partType === "step-finish") return;
|
|
148398
|
+
if (partType === "text" && part.time?.end !== void 0) {
|
|
148399
|
+
await handlers2.text({
|
|
148400
|
+
type: "text",
|
|
148401
|
+
sessionID,
|
|
148402
|
+
part
|
|
148403
|
+
});
|
|
148404
|
+
return;
|
|
148405
|
+
}
|
|
148091
148406
|
}
|
|
148092
148407
|
};
|
|
148093
148408
|
const recentStderr = [];
|
|
@@ -148111,13 +148426,13 @@ async function runOpenCode(params) {
|
|
|
148111
148426
|
// never fires — producing zombie runs. detached + killGroup nukes the
|
|
148112
148427
|
// whole tree.
|
|
148113
148428
|
killGroup: true,
|
|
148114
|
-
//
|
|
148115
|
-
//
|
|
148116
|
-
//
|
|
148117
|
-
//
|
|
148118
|
-
//
|
|
148119
|
-
//
|
|
148120
|
-
|
|
148429
|
+
// NB: we used to pass `isPausedExternally: isSubagentInFlight` to suspend
|
|
148430
|
+
// the activity timer during subagent dispatches. unnecessary now that
|
|
148431
|
+
// our injected plugin (action/agents/opencodePlugin.ts) re-emits
|
|
148432
|
+
// subagent `message.part.updated` events on opencode's stdout — those
|
|
148433
|
+
// arrive at child.stdout here, fire updateActivity(), and reset
|
|
148434
|
+
// lastActivityTime naturally. verified empirically in PR #634
|
|
148435
|
+
// (~3.3 plugin events/sec during a typical subagent run).
|
|
148121
148436
|
onStdout: async (chunk) => {
|
|
148122
148437
|
const text = chunk.toString();
|
|
148123
148438
|
output += text;
|
|
@@ -148272,6 +148587,12 @@ var opencode = agent({
|
|
|
148272
148587
|
XDG_CONFIG_HOME: join11(ctx.tmpdir, ".config")
|
|
148273
148588
|
};
|
|
148274
148589
|
mkdirSync5(join11(homeEnv.XDG_CONFIG_HOME, "opencode"), { recursive: true });
|
|
148590
|
+
const opencodePluginDir = join11(homeEnv.XDG_CONFIG_HOME, "opencode", "plugin");
|
|
148591
|
+
mkdirSync5(opencodePluginDir, { recursive: true });
|
|
148592
|
+
writeFileSync8(
|
|
148593
|
+
join11(opencodePluginDir, PULLFROG_OPENCODE_PLUGIN_FILENAME),
|
|
148594
|
+
PULLFROG_OPENCODE_PLUGIN_SOURCE
|
|
148595
|
+
);
|
|
148275
148596
|
const agentBrowserVersion = getDevDependencyVersion("agent-browser");
|
|
148276
148597
|
addSkill({
|
|
148277
148598
|
ref: `vercel-labs/agent-browser@v${agentBrowserVersion}`,
|
|
@@ -148309,12 +148630,10 @@ var opencode = agent({
|
|
|
148309
148630
|
args: [...baseArgs, ctx.instructions.full]
|
|
148310
148631
|
});
|
|
148311
148632
|
return runPostRunRetryLoop({
|
|
148633
|
+
ctx,
|
|
148312
148634
|
initialResult: result,
|
|
148313
148635
|
initialUsage: result.usage,
|
|
148314
|
-
|
|
148315
|
-
summaryFilePath: ctx.summaryFilePath,
|
|
148316
|
-
summarySeed: ctx.summarySeed,
|
|
148317
|
-
reflectionPrompt: buildLearningsReflectionPrompt("opencode"),
|
|
148636
|
+
reflectionPrompt: ctx.toolState.learningsFilePath ? buildLearningsReflectionPrompt(ctx.toolState.learningsFilePath) : void 0,
|
|
148318
148637
|
resume: async (c2) => runOpenCode({
|
|
148319
148638
|
...runParams,
|
|
148320
148639
|
args: [...baseArgs, "--continue", c2.prompt]
|
|
@@ -152240,8 +152559,10 @@ var checkRepositoryAccess = async (token, repoOwner, repoName) => {
|
|
|
152240
152559
|
const response = await githubRequest("/installation/repositories", {
|
|
152241
152560
|
headers: { Authorization: `token ${token}` }
|
|
152242
152561
|
});
|
|
152562
|
+
const ownerLower = repoOwner.toLowerCase();
|
|
152563
|
+
const nameLower = repoName.toLowerCase();
|
|
152243
152564
|
return response.repositories.some(
|
|
152244
|
-
(repo) => repo.owner.login ===
|
|
152565
|
+
(repo) => repo.owner.login.toLowerCase() === ownerLower && repo.name.toLowerCase() === nameLower
|
|
152245
152566
|
);
|
|
152246
152567
|
} catch {
|
|
152247
152568
|
return false;
|
|
@@ -152527,7 +152848,7 @@ ${ctx.error}` : ctx.error;
|
|
|
152527
152848
|
|
|
152528
152849
|
// utils/gitAuthServer.ts
|
|
152529
152850
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
152530
|
-
import { writeFileSync as
|
|
152851
|
+
import { writeFileSync as writeFileSync9 } from "node:fs";
|
|
152531
152852
|
import { createServer as createServer2 } from "node:http";
|
|
152532
152853
|
import { join as join13 } from "node:path";
|
|
152533
152854
|
var CODE_TTL_MS = 5 * 60 * 1e3;
|
|
@@ -152616,7 +152937,7 @@ async function startGitAuthServer(tmpdir3) {
|
|
|
152616
152937
|
`try{require("fs").unlinkSync("${scriptPath.replace(/\\/g, "\\\\")}")}catch(e){}`,
|
|
152617
152938
|
`})}).on("error",function(){process.exit(1)})}`
|
|
152618
152939
|
].join("\n");
|
|
152619
|
-
|
|
152940
|
+
writeFileSync9(scriptPath, content, { mode: 448 });
|
|
152620
152941
|
return scriptPath;
|
|
152621
152942
|
}
|
|
152622
152943
|
async function close() {
|
|
@@ -152890,9 +153211,9 @@ function buildPromptContext(ctx) {
|
|
|
152890
153211
|
};
|
|
152891
153212
|
}
|
|
152892
153213
|
function assembleFullPrompt(ctx) {
|
|
152893
|
-
const learningsSection = ctx.
|
|
153214
|
+
const learningsSection = ctx.learningsFilePath ? `************* LEARNINGS *************
|
|
152894
153215
|
|
|
152895
|
-
|
|
153216
|
+
Repo-level learnings accumulated by previous agent runs live at \`${ctx.learningsFilePath}\`. Read this file early and let the entries inform your approach (test commands, conventions, gotchas, etc.). The file may be empty if no learnings have been collected yet.` : "";
|
|
152896
153217
|
const runtimeSection = `************* RUNTIME *************
|
|
152897
153218
|
|
|
152898
153219
|
${ctx.runtime}`;
|
|
@@ -152919,8 +153240,8 @@ function resolveInstructions(ctx) {
|
|
|
152919
153240
|
if (eventContext)
|
|
152920
153241
|
tocEntries.push({ label: "EVENT CONTEXT", description: "related PR/issue data" });
|
|
152921
153242
|
tocEntries.push({ label: "SYSTEM", description: "persona, security, tools, workflow rules" });
|
|
152922
|
-
if (pctx.
|
|
152923
|
-
tocEntries.push({ label: "LEARNINGS", description: "repo-specific knowledge" });
|
|
153243
|
+
if (pctx.learningsFilePath)
|
|
153244
|
+
tocEntries.push({ label: "LEARNINGS", description: "repo-specific knowledge file path" });
|
|
152924
153245
|
tocEntries.push({ label: "RUNTIME", description: "environment metadata" });
|
|
152925
153246
|
const toc = buildToc(tocEntries);
|
|
152926
153247
|
const full = assembleFullPrompt({
|
|
@@ -152929,7 +153250,7 @@ function resolveInstructions(ctx) {
|
|
|
152929
153250
|
procedure,
|
|
152930
153251
|
eventContext,
|
|
152931
153252
|
system,
|
|
152932
|
-
|
|
153253
|
+
learningsFilePath: pctx.learningsFilePath,
|
|
152933
153254
|
runtime: pctx.runtime
|
|
152934
153255
|
});
|
|
152935
153256
|
const event = [pctx.eventTitle, pctx.eventMetadata].filter(Boolean).join("\n\n---\n\n");
|
|
@@ -152943,6 +153264,32 @@ function resolveInstructions(ctx) {
|
|
|
152943
153264
|
};
|
|
152944
153265
|
}
|
|
152945
153266
|
|
|
153267
|
+
// utils/learnings.ts
|
|
153268
|
+
import { mkdir, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
153269
|
+
import { dirname as dirname4, join as join14 } from "node:path";
|
|
153270
|
+
var LEARNINGS_FILE_NAME = "pullfrog-learnings.md";
|
|
153271
|
+
var MAX_LEARNINGS_LENGTH = 1e4;
|
|
153272
|
+
function learningsFilePath(tmpdir3) {
|
|
153273
|
+
return join14(tmpdir3, LEARNINGS_FILE_NAME);
|
|
153274
|
+
}
|
|
153275
|
+
async function seedLearningsFile(params) {
|
|
153276
|
+
const path3 = learningsFilePath(params.tmpdir);
|
|
153277
|
+
await mkdir(dirname4(path3), { recursive: true });
|
|
153278
|
+
await writeFile2(path3, params.current ?? "", "utf8");
|
|
153279
|
+
return path3;
|
|
153280
|
+
}
|
|
153281
|
+
async function readLearningsFile(path3) {
|
|
153282
|
+
let raw2;
|
|
153283
|
+
try {
|
|
153284
|
+
raw2 = await readFile2(path3, "utf8");
|
|
153285
|
+
} catch {
|
|
153286
|
+
return null;
|
|
153287
|
+
}
|
|
153288
|
+
const trimmed = raw2.trim();
|
|
153289
|
+
if (trimmed.length > MAX_LEARNINGS_LENGTH) return trimmed.slice(0, MAX_LEARNINGS_LENGTH);
|
|
153290
|
+
return trimmed;
|
|
153291
|
+
}
|
|
153292
|
+
|
|
152946
153293
|
// utils/normalizeEnv.ts
|
|
152947
153294
|
function maskValue(value2) {
|
|
152948
153295
|
if (value2 && typeof value2 === "string" && value2.trim().length > 0) {
|
|
@@ -153118,8 +153465,8 @@ function resolvePayload(resolvedPromptInput, repoSettings) {
|
|
|
153118
153465
|
}
|
|
153119
153466
|
|
|
153120
153467
|
// utils/prSummary.ts
|
|
153121
|
-
import { mkdir, readFile as
|
|
153122
|
-
import { dirname as
|
|
153468
|
+
import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
|
|
153469
|
+
import { dirname as dirname5, join as join15 } from "node:path";
|
|
153123
153470
|
var SUMMARY_FILE_NAME = "pullfrog-summary.md";
|
|
153124
153471
|
var SUMMARY_SCAFFOLD = `# PR summary
|
|
153125
153472
|
|
|
@@ -153129,19 +153476,19 @@ var SUMMARY_SCAFFOLD = `# PR summary
|
|
|
153129
153476
|
var MIN_SNAPSHOT_LENGTH = 60;
|
|
153130
153477
|
var MAX_SNAPSHOT_LENGTH = 32768;
|
|
153131
153478
|
function summaryFilePath(tmpdir3) {
|
|
153132
|
-
return
|
|
153479
|
+
return join15(tmpdir3, SUMMARY_FILE_NAME);
|
|
153133
153480
|
}
|
|
153134
153481
|
async function seedSummaryFile(params) {
|
|
153135
153482
|
const path3 = summaryFilePath(params.tmpdir);
|
|
153136
|
-
await
|
|
153483
|
+
await mkdir2(dirname5(path3), { recursive: true });
|
|
153137
153484
|
const seed = params.previousSnapshot && params.previousSnapshot.trim().length >= MIN_SNAPSHOT_LENGTH ? params.previousSnapshot : SUMMARY_SCAFFOLD;
|
|
153138
|
-
await
|
|
153485
|
+
await writeFile3(path3, seed, "utf8");
|
|
153139
153486
|
return path3;
|
|
153140
153487
|
}
|
|
153141
153488
|
async function readSummaryFile(path3) {
|
|
153142
153489
|
let raw2;
|
|
153143
153490
|
try {
|
|
153144
|
-
raw2 = await
|
|
153491
|
+
raw2 = await readFile3(path3, "utf8");
|
|
153145
153492
|
} catch {
|
|
153146
153493
|
return null;
|
|
153147
153494
|
}
|
|
@@ -153359,9 +153706,9 @@ async function resolveRunContextData(params) {
|
|
|
153359
153706
|
import { execFileSync as execFileSync5, execSync as execSync3 } from "node:child_process";
|
|
153360
153707
|
import { mkdtempSync } from "node:fs";
|
|
153361
153708
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
153362
|
-
import { join as
|
|
153709
|
+
import { join as join16 } from "node:path";
|
|
153363
153710
|
function createTempDirectory() {
|
|
153364
|
-
const sharedTempDir = mkdtempSync(
|
|
153711
|
+
const sharedTempDir = mkdtempSync(join16(tmpdir2(), "pullfrog-"));
|
|
153365
153712
|
process.env.PULLFROG_TEMP_DIR = sharedTempDir;
|
|
153366
153713
|
log.info(`\xBB created temp dir at ${sharedTempDir}`);
|
|
153367
153714
|
return sharedTempDir;
|
|
@@ -153763,15 +154110,12 @@ function formatTransientErrorSummary(error49, owner) {
|
|
|
153763
154110
|
}
|
|
153764
154111
|
async function mintProxyKey(ctx) {
|
|
153765
154112
|
try {
|
|
153766
|
-
|
|
153767
|
-
|
|
153768
|
-
const oidcToken = await core6.getIDToken("pullfrog-api");
|
|
153769
|
-
delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
153770
|
-
delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
154113
|
+
const headers = await buildProxyTokenHeaders(ctx);
|
|
154114
|
+
if (!headers) return null;
|
|
153771
154115
|
const response = await apiFetch({
|
|
153772
154116
|
path: "/api/proxy-token",
|
|
153773
154117
|
method: "POST",
|
|
153774
|
-
headers
|
|
154118
|
+
headers
|
|
153775
154119
|
});
|
|
153776
154120
|
if (response.status === 402) {
|
|
153777
154121
|
const body = await response.json().catch(() => null);
|
|
@@ -153803,15 +154147,30 @@ async function mintProxyKey(ctx) {
|
|
|
153803
154147
|
delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
153804
154148
|
}
|
|
153805
154149
|
}
|
|
154150
|
+
async function buildProxyTokenHeaders(ctx) {
|
|
154151
|
+
if (ctx.oidcCredentials) {
|
|
154152
|
+
process.env.ACTIONS_ID_TOKEN_REQUEST_URL = ctx.oidcCredentials.requestUrl;
|
|
154153
|
+
process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = ctx.oidcCredentials.requestToken;
|
|
154154
|
+
const oidcToken = await core6.getIDToken("pullfrog-api");
|
|
154155
|
+
delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
154156
|
+
delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
154157
|
+
return { Authorization: `Bearer ${oidcToken}` };
|
|
154158
|
+
}
|
|
154159
|
+
if (isLocalApiUrl()) {
|
|
154160
|
+
log.info(`\xBB proxy: dev bypass (x-dev-repo) for ${ctx.repo.owner}/${ctx.repo.name}`);
|
|
154161
|
+
return { "x-dev-repo": `${ctx.repo.owner}/${ctx.repo.name}` };
|
|
154162
|
+
}
|
|
154163
|
+
return null;
|
|
154164
|
+
}
|
|
153806
154165
|
async function resolveProxyModel(ctx) {
|
|
153807
154166
|
if (process.env.PULLFROG_MODEL?.trim()) return;
|
|
153808
154167
|
const needsProxy = isInfraCovered({ isOss: ctx.oss, plan: ctx.plan }) && ctx.proxyModel;
|
|
153809
154168
|
if (!needsProxy) return;
|
|
153810
|
-
if (!ctx.oidcCredentials) {
|
|
154169
|
+
if (!ctx.oidcCredentials && !isLocalApiUrl()) {
|
|
153811
154170
|
log.warning("\xBB proxy requested but no OIDC credentials available \u2014 skipping");
|
|
153812
154171
|
return;
|
|
153813
154172
|
}
|
|
153814
|
-
const key = await mintProxyKey({ oidcCredentials: ctx.oidcCredentials });
|
|
154173
|
+
const key = await mintProxyKey({ oidcCredentials: ctx.oidcCredentials, repo: ctx.repo });
|
|
153815
154174
|
if (!key) return;
|
|
153816
154175
|
process.env.OPENROUTER_API_KEY = key;
|
|
153817
154176
|
core6.setSecret(key);
|
|
@@ -153835,6 +154194,45 @@ async function fetchPreviousSnapshot(ctx, prNumber) {
|
|
|
153835
154194
|
return null;
|
|
153836
154195
|
}
|
|
153837
154196
|
}
|
|
154197
|
+
async function persistLearnings(ctx) {
|
|
154198
|
+
const filePath = ctx.toolState.learningsFilePath;
|
|
154199
|
+
if (!filePath) return;
|
|
154200
|
+
if (ctx.toolState.learningsPersistAttempted) return;
|
|
154201
|
+
ctx.toolState.learningsPersistAttempted = true;
|
|
154202
|
+
const current = await readLearningsFile(filePath);
|
|
154203
|
+
if (current === null) {
|
|
154204
|
+
log.debug(`learnings tmpfile missing or unreadable at ${filePath} \u2014 skipping persist`);
|
|
154205
|
+
return;
|
|
154206
|
+
}
|
|
154207
|
+
const seed = ctx.toolState.learningsSeed?.trim() ?? "";
|
|
154208
|
+
if (current === seed) {
|
|
154209
|
+
log.debug("learnings tmpfile unchanged from seed \u2014 skipping persist");
|
|
154210
|
+
return;
|
|
154211
|
+
}
|
|
154212
|
+
try {
|
|
154213
|
+
const response = await apiFetch({
|
|
154214
|
+
path: `/api/repo/${ctx.repo.owner}/${ctx.repo.name}/learnings`,
|
|
154215
|
+
method: "PATCH",
|
|
154216
|
+
headers: {
|
|
154217
|
+
authorization: `Bearer ${ctx.apiToken}`,
|
|
154218
|
+
"content-type": "application/json"
|
|
154219
|
+
},
|
|
154220
|
+
body: JSON.stringify({
|
|
154221
|
+
learnings: current,
|
|
154222
|
+
model: ctx.toolState.model
|
|
154223
|
+
}),
|
|
154224
|
+
signal: AbortSignal.timeout(1e4)
|
|
154225
|
+
});
|
|
154226
|
+
if (!response.ok) {
|
|
154227
|
+
const error49 = await response.text().catch(() => "(no body)");
|
|
154228
|
+
log.debug(`learnings persist failed (${response.status}): ${error49}`);
|
|
154229
|
+
return;
|
|
154230
|
+
}
|
|
154231
|
+
log.info("\xBB learnings updated");
|
|
154232
|
+
} catch (err) {
|
|
154233
|
+
log.debug(`learnings persist failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
154234
|
+
}
|
|
154235
|
+
}
|
|
153838
154236
|
async function persistSummary(ctx) {
|
|
153839
154237
|
const filePath = ctx.toolState.summaryFilePath;
|
|
153840
154238
|
if (!filePath) return;
|
|
@@ -153856,9 +154254,10 @@ async function persistSummary(ctx) {
|
|
|
153856
154254
|
log.debug(`pr summary persist failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
153857
154255
|
});
|
|
153858
154256
|
}
|
|
153859
|
-
async function writeJobSummary(toolState) {
|
|
154257
|
+
async function writeJobSummary(toolState, finalOutput) {
|
|
153860
154258
|
const usageSummary = formatUsageSummary(toolState.usageEntries);
|
|
153861
|
-
const
|
|
154259
|
+
const body = toolState.lastProgressBody || finalOutput;
|
|
154260
|
+
const summaryParts = [body, usageSummary].filter(Boolean);
|
|
153862
154261
|
if (summaryParts.length > 0) {
|
|
153863
154262
|
await writeSummary(summaryParts.join("\n\n"));
|
|
153864
154263
|
}
|
|
@@ -153916,7 +154315,8 @@ async function main() {
|
|
|
153916
154315
|
oss: runContext.oss,
|
|
153917
154316
|
plan: runContext.plan,
|
|
153918
154317
|
proxyModel: runContext.proxyModel,
|
|
153919
|
-
oidcCredentials
|
|
154318
|
+
oidcCredentials,
|
|
154319
|
+
repo: runContext.repo
|
|
153920
154320
|
});
|
|
153921
154321
|
} catch (error49) {
|
|
153922
154322
|
if (error49 instanceof BillingError) {
|
|
@@ -154019,12 +154419,32 @@ async function main() {
|
|
|
154019
154419
|
toolContext.mcpServerUrl = mcpHttpServer.url;
|
|
154020
154420
|
log.info(`\xBB MCP server started at ${mcpHttpServer.url}`);
|
|
154021
154421
|
timer.checkpoint("mcpServer");
|
|
154422
|
+
try {
|
|
154423
|
+
const learningsPath = await seedLearningsFile({
|
|
154424
|
+
tmpdir: tmpdir3,
|
|
154425
|
+
current: runContext.repoSettings.learnings
|
|
154426
|
+
});
|
|
154427
|
+
toolState.learningsFilePath = learningsPath;
|
|
154428
|
+
try {
|
|
154429
|
+
toolState.learningsSeed = await readFile4(learningsPath, "utf8");
|
|
154430
|
+
} catch {
|
|
154431
|
+
}
|
|
154432
|
+
log.info(
|
|
154433
|
+
`\xBB learnings seeded at ${learningsPath} (existing=${runContext.repoSettings.learnings ? "yes" : "no"})`
|
|
154434
|
+
);
|
|
154435
|
+
const ctxForExit = toolContext;
|
|
154436
|
+
onExitSignal(() => persistLearnings(ctxForExit));
|
|
154437
|
+
} catch (err) {
|
|
154438
|
+
log.warning(
|
|
154439
|
+
`\xBB learnings seed failed: ${err instanceof Error ? err.message : String(err)} \u2014 continuing without learnings file`
|
|
154440
|
+
);
|
|
154441
|
+
}
|
|
154022
154442
|
if (payload.generateSummary && payload.event.is_pr && payload.event.issue_number) {
|
|
154023
154443
|
const previousSnapshot = await fetchPreviousSnapshot(toolContext, payload.event.issue_number);
|
|
154024
154444
|
const filePath = await seedSummaryFile({ tmpdir: tmpdir3, previousSnapshot });
|
|
154025
154445
|
toolState.summaryFilePath = filePath;
|
|
154026
154446
|
try {
|
|
154027
|
-
toolState.summarySeed = await
|
|
154447
|
+
toolState.summarySeed = await readFile4(filePath, "utf8");
|
|
154028
154448
|
} catch {
|
|
154029
154449
|
}
|
|
154030
154450
|
log.info(
|
|
@@ -154048,7 +154468,7 @@ async function main() {
|
|
|
154048
154468
|
modes: modes2,
|
|
154049
154469
|
agentId,
|
|
154050
154470
|
outputSchema,
|
|
154051
|
-
|
|
154471
|
+
learningsFilePath: toolState.learningsFilePath ?? null
|
|
154052
154472
|
});
|
|
154053
154473
|
const logParts = [
|
|
154054
154474
|
instructions.eventInstructions ? `EVENT-LEVEL INSTRUCTIONS:
|
|
@@ -154064,7 +154484,7 @@ ${instructions.user}` : null,
|
|
|
154064
154484
|
log.info(instructions.full);
|
|
154065
154485
|
});
|
|
154066
154486
|
if (agentId === "opencode") {
|
|
154067
|
-
const pluginDir =
|
|
154487
|
+
const pluginDir = join17(process.cwd(), ".opencode", "plugin");
|
|
154068
154488
|
const hasPlugins = existsSync7(pluginDir) && readdirSync(pluginDir).some((f) => /\.[jt]sx?$/.test(f));
|
|
154069
154489
|
if (hasPlugins && toolState.dependencyInstallation?.promise) {
|
|
154070
154490
|
log.info(
|
|
@@ -154123,8 +154543,7 @@ ${instructions.user}` : null,
|
|
|
154123
154543
|
instructions,
|
|
154124
154544
|
todoTracker,
|
|
154125
154545
|
stopScript: runContext.repoSettings.stopScript,
|
|
154126
|
-
|
|
154127
|
-
summarySeed: toolState.summarySeed,
|
|
154546
|
+
toolState,
|
|
154128
154547
|
onActivityTimeout: onInnerActivityTimeout,
|
|
154129
154548
|
onToolUse: (event) => {
|
|
154130
154549
|
const wasTracked = recordDiffReadFromToolUse({
|
|
@@ -154182,12 +154601,27 @@ ${instructions.user}` : null,
|
|
|
154182
154601
|
if (toolContext) {
|
|
154183
154602
|
await persistSummary(toolContext);
|
|
154184
154603
|
}
|
|
154185
|
-
if (toolContext
|
|
154604
|
+
if (toolContext) {
|
|
154605
|
+
await persistLearnings(toolContext);
|
|
154606
|
+
}
|
|
154607
|
+
if (!result.success && toolContext && toolState.progressComment) {
|
|
154608
|
+
await reportErrorToComment({
|
|
154609
|
+
toolState,
|
|
154610
|
+
error: result.error || "agent run failed"
|
|
154611
|
+
}).catch((error49) => {
|
|
154612
|
+
log.debug(`failure error report failed: ${error49}`);
|
|
154613
|
+
});
|
|
154614
|
+
}
|
|
154615
|
+
if (toolContext && result.success && toolState.progressComment && !toolState.finalSummaryWritten) {
|
|
154186
154616
|
await deleteProgressComment(toolContext).catch((error49) => {
|
|
154187
154617
|
log.debug(`stranded progress comment cleanup failed: ${error49}`);
|
|
154188
154618
|
});
|
|
154189
154619
|
}
|
|
154190
|
-
|
|
154620
|
+
try {
|
|
154621
|
+
await writeJobSummary(toolState, result.output);
|
|
154622
|
+
} catch (error49) {
|
|
154623
|
+
log.debug(`job summary write failed: ${error49}`);
|
|
154624
|
+
}
|
|
154191
154625
|
if (toolState.output) {
|
|
154192
154626
|
log.info(`::pullfrog-output::${Buffer.from(toolState.output).toString("base64")}`);
|
|
154193
154627
|
core6.setOutput("result", toolState.output);
|
|
@@ -154234,6 +154668,9 @@ ${errorMessage}
|
|
|
154234
154668
|
if (toolContext) {
|
|
154235
154669
|
await persistSummary(toolContext);
|
|
154236
154670
|
}
|
|
154671
|
+
if (toolContext) {
|
|
154672
|
+
await persistLearnings(toolContext);
|
|
154673
|
+
}
|
|
154237
154674
|
return {
|
|
154238
154675
|
success: false,
|
|
154239
154676
|
error: errorMessage
|
|
@@ -154266,7 +154703,7 @@ ${errorMessage}
|
|
|
154266
154703
|
}
|
|
154267
154704
|
|
|
154268
154705
|
// commands/gha.ts
|
|
154269
|
-
process.env.PATH = `${
|
|
154706
|
+
process.env.PATH = `${dirname6(process.execPath)}:${process.env.PATH}`;
|
|
154270
154707
|
var STATE_TOKEN = "token";
|
|
154271
154708
|
async function runMain() {
|
|
154272
154709
|
try {
|
|
@@ -156076,7 +156513,7 @@ async function run2() {
|
|
|
156076
156513
|
}
|
|
156077
156514
|
|
|
156078
156515
|
// cli.ts
|
|
156079
|
-
var VERSION10 = "0.1.
|
|
156516
|
+
var VERSION10 = "0.1.3";
|
|
156080
156517
|
var bin = basename2(process.argv[1] || "");
|
|
156081
156518
|
var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
|
|
156082
156519
|
var rawArgs = process.argv.slice(2);
|