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/index.js
CHANGED
|
@@ -18198,7 +18198,7 @@ var require_summary = __commonJS({
|
|
|
18198
18198
|
exports.summary = exports.markdownSummary = exports.SUMMARY_DOCS_URL = exports.SUMMARY_ENV_VAR = void 0;
|
|
18199
18199
|
var os_1 = __require("os");
|
|
18200
18200
|
var fs_1 = __require("fs");
|
|
18201
|
-
var { access, appendFile, writeFile:
|
|
18201
|
+
var { access, appendFile, writeFile: writeFile4 } = fs_1.promises;
|
|
18202
18202
|
exports.SUMMARY_ENV_VAR = "GITHUB_STEP_SUMMARY";
|
|
18203
18203
|
exports.SUMMARY_DOCS_URL = "https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary";
|
|
18204
18204
|
var Summary = class {
|
|
@@ -18256,7 +18256,7 @@ var require_summary = __commonJS({
|
|
|
18256
18256
|
return __awaiter(this, void 0, void 0, function* () {
|
|
18257
18257
|
const overwrite = !!(options === null || options === void 0 ? void 0 : options.overwrite);
|
|
18258
18258
|
const filePath = yield this.filePath();
|
|
18259
|
-
const writeFunc = overwrite ?
|
|
18259
|
+
const writeFunc = overwrite ? writeFile4 : appendFile;
|
|
18260
18260
|
yield writeFunc(filePath, this._buffer, { encoding: "utf8" });
|
|
18261
18261
|
return this.emptyBuffer();
|
|
18262
18262
|
});
|
|
@@ -62662,8 +62662,8 @@ var require_snapshot_utils = __commonJS({
|
|
|
62662
62662
|
var require_snapshot_recorder = __commonJS({
|
|
62663
62663
|
"node_modules/.pnpm/undici@7.22.0/node_modules/undici/lib/mock/snapshot-recorder.js"(exports, module) {
|
|
62664
62664
|
"use strict";
|
|
62665
|
-
var { writeFile:
|
|
62666
|
-
var { dirname:
|
|
62665
|
+
var { writeFile: writeFile4, readFile: readFile5, mkdir: mkdir3 } = __require("node:fs/promises");
|
|
62666
|
+
var { dirname: dirname6, resolve: resolve3 } = __require("node:path");
|
|
62667
62667
|
var { setTimeout: setTimeout2, clearTimeout: clearTimeout2 } = __require("node:timers");
|
|
62668
62668
|
var { InvalidArgumentError, UndiciError } = require_errors4();
|
|
62669
62669
|
var { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = require_snapshot_utils();
|
|
@@ -62864,7 +62864,7 @@ var require_snapshot_recorder = __commonJS({
|
|
|
62864
62864
|
throw new InvalidArgumentError("Snapshot path is required");
|
|
62865
62865
|
}
|
|
62866
62866
|
try {
|
|
62867
|
-
const data = await
|
|
62867
|
+
const data = await readFile5(resolve3(path3), "utf8");
|
|
62868
62868
|
const parsed2 = JSON.parse(data);
|
|
62869
62869
|
if (Array.isArray(parsed2)) {
|
|
62870
62870
|
this.#snapshots.clear();
|
|
@@ -62894,12 +62894,12 @@ var require_snapshot_recorder = __commonJS({
|
|
|
62894
62894
|
throw new InvalidArgumentError("Snapshot path is required");
|
|
62895
62895
|
}
|
|
62896
62896
|
const resolvedPath = resolve3(path3);
|
|
62897
|
-
await
|
|
62897
|
+
await mkdir3(dirname6(resolvedPath), { recursive: true });
|
|
62898
62898
|
const data = Array.from(this.#snapshots.entries()).map(([hash2, snapshot2]) => ({
|
|
62899
62899
|
hash: hash2,
|
|
62900
62900
|
snapshot: snapshot2
|
|
62901
62901
|
}));
|
|
62902
|
-
await
|
|
62902
|
+
await writeFile4(resolvedPath, JSON.stringify(data, null, 2), { flush: true });
|
|
62903
62903
|
}
|
|
62904
62904
|
/**
|
|
62905
62905
|
* Clears all recorded snapshots
|
|
@@ -97475,14 +97475,14 @@ var require_turndown_cjs = __commonJS({
|
|
|
97475
97475
|
} else if (node2.nodeType === 1) {
|
|
97476
97476
|
replacement = replacementForNode.call(self2, node2);
|
|
97477
97477
|
}
|
|
97478
|
-
return
|
|
97478
|
+
return join18(output, replacement);
|
|
97479
97479
|
}, "");
|
|
97480
97480
|
}
|
|
97481
97481
|
function postProcess(output) {
|
|
97482
97482
|
var self2 = this;
|
|
97483
97483
|
this.rules.forEach(function(rule) {
|
|
97484
97484
|
if (typeof rule.append === "function") {
|
|
97485
|
-
output =
|
|
97485
|
+
output = join18(output, rule.append(self2.options));
|
|
97486
97486
|
}
|
|
97487
97487
|
});
|
|
97488
97488
|
return output.replace(/^[\t\r\n]+/, "").replace(/[\t\r\n\s]+$/, "");
|
|
@@ -97494,7 +97494,7 @@ var require_turndown_cjs = __commonJS({
|
|
|
97494
97494
|
if (whitespace.leading || whitespace.trailing) content = content.trim();
|
|
97495
97495
|
return whitespace.leading + rule.replacement(content, node2, this.options) + whitespace.trailing;
|
|
97496
97496
|
}
|
|
97497
|
-
function
|
|
97497
|
+
function join18(output, replacement) {
|
|
97498
97498
|
var s1 = trimTrailingNewlines(output);
|
|
97499
97499
|
var s2 = trimLeadingNewlines(replacement);
|
|
97500
97500
|
var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
|
|
@@ -98926,8 +98926,8 @@ var require_fast_content_type_parse = __commonJS({
|
|
|
98926
98926
|
// main.ts
|
|
98927
98927
|
var core6 = __toESM(require_core(), 1);
|
|
98928
98928
|
import { existsSync as existsSync7, readdirSync } from "node:fs";
|
|
98929
|
-
import { readFile as
|
|
98930
|
-
import { join as
|
|
98929
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
98930
|
+
import { join as join17 } from "node:path";
|
|
98931
98931
|
|
|
98932
98932
|
// node_modules/.pnpm/@ark+util@0.56.0/node_modules/@ark/util/out/arrays.js
|
|
98933
98933
|
var liftArray = (data) => Array.isArray(data) ? data : [data];
|
|
@@ -107423,7 +107423,7 @@ function buildCommitPrompt(status) {
|
|
|
107423
107423
|
].join("\n");
|
|
107424
107424
|
}
|
|
107425
107425
|
function hasPostRunIssues(issues) {
|
|
107426
|
-
return issues.stopHook !== void 0 || issues.dirtyTree !== void 0 || issues.summaryStale !== void 0;
|
|
107426
|
+
return issues.stopHook !== void 0 || issues.dirtyTree !== void 0 || issues.summaryStale !== void 0 || issues.unsubmittedReview !== void 0;
|
|
107427
107427
|
}
|
|
107428
107428
|
var agent = (input) => {
|
|
107429
107429
|
return {
|
|
@@ -107723,6 +107723,13 @@ function getApiUrl() {
|
|
|
107723
107723
|
log.debug(`resolved API_URL: ${raw2}`);
|
|
107724
107724
|
return raw2;
|
|
107725
107725
|
}
|
|
107726
|
+
function isLocalApiUrl() {
|
|
107727
|
+
try {
|
|
107728
|
+
return isLocalUrl(new URL(getApiUrl()));
|
|
107729
|
+
} catch {
|
|
107730
|
+
return false;
|
|
107731
|
+
}
|
|
107732
|
+
}
|
|
107726
107733
|
|
|
107727
107734
|
// models.ts
|
|
107728
107735
|
function provider(config3) {
|
|
@@ -108961,6 +108968,7 @@ function CreateCommentTool(ctx) {
|
|
|
108961
108968
|
body: bodyWithFooter
|
|
108962
108969
|
});
|
|
108963
108970
|
ctx.toolState.wasUpdated = true;
|
|
108971
|
+
log.info(`\xBB created comment ${result.data.id}`);
|
|
108964
108972
|
if (commentType === "Plan") {
|
|
108965
108973
|
if (result.data.node_id) {
|
|
108966
108974
|
await patchWorkflowRunFields(ctx, { planCommentNodeId: result.data.node_id });
|
|
@@ -108974,6 +108982,7 @@ function CreateCommentTool(ctx) {
|
|
|
108974
108982
|
comment_id: result.data.id,
|
|
108975
108983
|
body: bodyWithPlanLink
|
|
108976
108984
|
});
|
|
108985
|
+
log.info(`\xBB updated comment ${updateResult.data.id}`);
|
|
108977
108986
|
return {
|
|
108978
108987
|
success: true,
|
|
108979
108988
|
commentId: updateResult.data.id,
|
|
@@ -109007,6 +109016,7 @@ function EditCommentTool(ctx) {
|
|
|
109007
109016
|
comment_id: commentId,
|
|
109008
109017
|
body: bodyWithFooter
|
|
109009
109018
|
});
|
|
109019
|
+
log.info(`\xBB updated comment ${result.data.id}`);
|
|
109010
109020
|
return {
|
|
109011
109021
|
success: true,
|
|
109012
109022
|
commentId: result.data.id,
|
|
@@ -109142,6 +109152,9 @@ ${collapsible}`;
|
|
|
109142
109152
|
message: "progress recorded (no GitHub comment created - this may occur for workflow_dispatch events or when there is no associated issue/PR)"
|
|
109143
109153
|
};
|
|
109144
109154
|
}
|
|
109155
|
+
if (result.commentId !== void 0) {
|
|
109156
|
+
log.info(`\xBB ${result.action} comment ${result.commentId}`);
|
|
109157
|
+
}
|
|
109145
109158
|
if (!params.target_plan_comment) {
|
|
109146
109159
|
ctx.toolState.finalSummaryWritten = true;
|
|
109147
109160
|
}
|
|
@@ -109178,13 +109191,38 @@ var ReplyToReviewComment = type({
|
|
|
109178
109191
|
"extremely brief reply (1 sentence max) explaining what was fixed, e.g. 'Fixed by renaming to X' or 'Added null check'"
|
|
109179
109192
|
)
|
|
109180
109193
|
});
|
|
109194
|
+
function duplicateReplyDecision(params) {
|
|
109195
|
+
const existing = params.existing;
|
|
109196
|
+
if (!existing) return null;
|
|
109197
|
+
if (existing.bodyWithFooter !== params.bodyWithFooter) return null;
|
|
109198
|
+
return {
|
|
109199
|
+
kind: "already-replied",
|
|
109200
|
+
commentId: existing.commentId,
|
|
109201
|
+
url: existing.url,
|
|
109202
|
+
reason: `reply ${existing.commentId} with identical body was already posted in this session; ignoring duplicate call`
|
|
109203
|
+
};
|
|
109204
|
+
}
|
|
109181
109205
|
function ReplyToReviewCommentTool(ctx) {
|
|
109182
109206
|
return tool({
|
|
109183
109207
|
name: "reply_to_review_comment",
|
|
109184
|
-
description: "Reply to a PR review comment thread (NOT issue comments \u2014 this only works for inline review comments on PR diffs). Call
|
|
109208
|
+
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).",
|
|
109185
109209
|
parameters: ReplyToReviewComment,
|
|
109186
109210
|
execute: execute(async ({ pull_number, comment_id, body }) => {
|
|
109187
109211
|
const bodyWithFooter = addFooter(ctx, body);
|
|
109212
|
+
const dup = duplicateReplyDecision({
|
|
109213
|
+
existing: ctx.toolState.reviewReplies?.get(comment_id),
|
|
109214
|
+
bodyWithFooter
|
|
109215
|
+
});
|
|
109216
|
+
if (dup) {
|
|
109217
|
+
log.info(`skipping duplicate review reply: ${dup.reason}`);
|
|
109218
|
+
return {
|
|
109219
|
+
success: true,
|
|
109220
|
+
skipped: true,
|
|
109221
|
+
reason: dup.reason,
|
|
109222
|
+
commentId: dup.commentId,
|
|
109223
|
+
url: dup.url
|
|
109224
|
+
};
|
|
109225
|
+
}
|
|
109188
109226
|
const result = await ctx.octokit.rest.pulls.createReplyForReviewComment({
|
|
109189
109227
|
owner: ctx.repo.owner,
|
|
109190
109228
|
repo: ctx.repo.name,
|
|
@@ -109192,7 +109230,14 @@ function ReplyToReviewCommentTool(ctx) {
|
|
|
109192
109230
|
comment_id,
|
|
109193
109231
|
body: bodyWithFooter
|
|
109194
109232
|
});
|
|
109233
|
+
log.info(`\xBB created review comment ${result.data.id} (in reply to ${comment_id})`);
|
|
109195
109234
|
ctx.toolState.wasUpdated = true;
|
|
109235
|
+
ctx.toolState.reviewReplies ??= /* @__PURE__ */ new Map();
|
|
109236
|
+
ctx.toolState.reviewReplies.set(comment_id, {
|
|
109237
|
+
commentId: result.data.id,
|
|
109238
|
+
url: result.data.html_url,
|
|
109239
|
+
bodyWithFooter
|
|
109240
|
+
});
|
|
109196
109241
|
return {
|
|
109197
109242
|
success: true,
|
|
109198
109243
|
commentId: result.data.id,
|
|
@@ -109741,11 +109786,6 @@ async function spawn(options) {
|
|
|
109741
109786
|
`spawn activity timer: pid=${child.pid} cmd=${options.cmd} timeout=${activityTimeoutMs}ms`
|
|
109742
109787
|
);
|
|
109743
109788
|
activityCheckIntervalId = setInterval(() => {
|
|
109744
|
-
if (options.isPausedExternally?.()) {
|
|
109745
|
-
lastActivityTime = performance3.now();
|
|
109746
|
-
log.debug(`spawn activity check: pid=${child.pid} paused externally`);
|
|
109747
|
-
return;
|
|
109748
|
-
}
|
|
109749
109789
|
const idleMs = performance3.now() - lastActivityTime;
|
|
109750
109790
|
log.debug(
|
|
109751
109791
|
`spawn activity check: pid=${child.pid} idle=${Math.round(idleMs)}ms / ${activityTimeoutMs}ms`
|
|
@@ -109944,13 +109984,13 @@ var installNodeDependencies = {
|
|
|
109944
109984
|
};
|
|
109945
109985
|
}
|
|
109946
109986
|
}
|
|
109947
|
-
const resolved = resolveCommand(agent2, "frozen", [])
|
|
109987
|
+
const resolved = resolveCommand(agent2, "frozen", []);
|
|
109948
109988
|
if (!resolved) {
|
|
109949
109989
|
return {
|
|
109950
109990
|
language: "node",
|
|
109951
109991
|
packageManager,
|
|
109952
109992
|
dependenciesInstalled: false,
|
|
109953
|
-
issues: [`no install command
|
|
109993
|
+
issues: [`no frozen-install command available for ${agent2}`]
|
|
109954
109994
|
};
|
|
109955
109995
|
}
|
|
109956
109996
|
if (options.ignoreScripts) {
|
|
@@ -142266,7 +142306,7 @@ var import_semver = __toESM(require_semver2(), 1);
|
|
|
142266
142306
|
// package.json
|
|
142267
142307
|
var package_default = {
|
|
142268
142308
|
name: "pullfrog",
|
|
142269
|
-
version: "0.1.
|
|
142309
|
+
version: "0.1.3",
|
|
142270
142310
|
type: "module",
|
|
142271
142311
|
bin: {
|
|
142272
142312
|
pullfrog: "dist/cli.mjs",
|
|
@@ -143210,6 +143250,10 @@ ${integrateStep}
|
|
|
143210
143250
|
if (!pushed) {
|
|
143211
143251
|
throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
|
|
143212
143252
|
}
|
|
143253
|
+
const pushedSha = $("git", ["rev-parse", "HEAD"], { log: false }).trim();
|
|
143254
|
+
log.info(
|
|
143255
|
+
`\xBB pushed branch ${branch} to ${pushDest.remoteName}/${pushDest.remoteBranch} (sha ${pushedSha})`
|
|
143256
|
+
);
|
|
143213
143257
|
return {
|
|
143214
143258
|
success: true,
|
|
143215
143259
|
branch,
|
|
@@ -143358,6 +143402,7 @@ function DeleteBranchTool(ctx) {
|
|
|
143358
143402
|
await $git("push", ["origin", "--delete", `refs/heads/${params.branchName}`], {
|
|
143359
143403
|
token: ctx.gitToken
|
|
143360
143404
|
});
|
|
143405
|
+
log.info(`\xBB deleted branch ${params.branchName}`);
|
|
143361
143406
|
return { success: true, deleted: params.branchName };
|
|
143362
143407
|
})
|
|
143363
143408
|
});
|
|
@@ -143383,6 +143428,7 @@ function PushTagsTool(ctx) {
|
|
|
143383
143428
|
await $git("push", pushArgs, {
|
|
143384
143429
|
token: ctx.gitToken
|
|
143385
143430
|
});
|
|
143431
|
+
log.info(`\xBB pushed tag ${params.tag}`);
|
|
143386
143432
|
return { success: true, tag: params.tag };
|
|
143387
143433
|
})
|
|
143388
143434
|
});
|
|
@@ -143537,7 +143583,7 @@ var CreatePullRequestReview = type({
|
|
|
143537
143583
|
"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."
|
|
143538
143584
|
).optional(),
|
|
143539
143585
|
approved: type.boolean.describe(
|
|
143540
|
-
"Set to true to submit as an approval.
|
|
143586
|
+
"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."
|
|
143541
143587
|
).optional(),
|
|
143542
143588
|
commit_id: type.string.describe("Optional SHA of the commit being reviewed. Defaults to latest.").optional(),
|
|
143543
143589
|
comments: type({
|
|
@@ -143707,6 +143753,7 @@ function CreatePullRequestReviewTool(ctx) {
|
|
|
143707
143753
|
}
|
|
143708
143754
|
const reviewId = result.data.id;
|
|
143709
143755
|
const reviewNodeId = result.data.node_id;
|
|
143756
|
+
log.info(`\xBB created review ${reviewId} on pull request #${pull_number}`);
|
|
143710
143757
|
const actuallyReviewedSha = ctx.toolState.checkoutSha ?? params.commit_id;
|
|
143711
143758
|
ctx.toolState.review = {
|
|
143712
143759
|
id: reviewId,
|
|
@@ -144066,6 +144113,8 @@ async function ensureBeforeShaReachable(params) {
|
|
|
144066
144113
|
}
|
|
144067
144114
|
}
|
|
144068
144115
|
var STALE_LOCK_AGE_MS = 3e4;
|
|
144116
|
+
var PULL_REF_RETRY_DELAYS_MS = [2e3, 5e3, 1e4];
|
|
144117
|
+
var PULL_REF_MISSING_PATTERN = /couldn't find remote ref pull\/\d+\/head/i;
|
|
144069
144118
|
var GIT_LOCK_PATHS = [
|
|
144070
144119
|
".git/shallow.lock",
|
|
144071
144120
|
".git/index.lock",
|
|
@@ -144091,6 +144140,27 @@ function cleanupStaleGitLocks() {
|
|
|
144091
144140
|
}
|
|
144092
144141
|
}
|
|
144093
144142
|
}
|
|
144143
|
+
async function isPullRequestStillDispatchable(args2) {
|
|
144144
|
+
try {
|
|
144145
|
+
const { data } = await args2.octokit.rest.pulls.get({
|
|
144146
|
+
owner: args2.owner,
|
|
144147
|
+
repo: args2.repo,
|
|
144148
|
+
pull_number: args2.pr.number
|
|
144149
|
+
});
|
|
144150
|
+
if (data.state !== "open") return false;
|
|
144151
|
+
if (data.head.sha !== args2.pr.headSha) return false;
|
|
144152
|
+
return true;
|
|
144153
|
+
} catch {
|
|
144154
|
+
return true;
|
|
144155
|
+
}
|
|
144156
|
+
}
|
|
144157
|
+
async function abortIfPullRequestMoved(args2) {
|
|
144158
|
+
const stillValid = await isPullRequestStillDispatchable(args2);
|
|
144159
|
+
if (stillValid) return;
|
|
144160
|
+
throw new Error(
|
|
144161
|
+
`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.`
|
|
144162
|
+
);
|
|
144163
|
+
}
|
|
144094
144164
|
async function checkoutPrBranch(pr, params) {
|
|
144095
144165
|
const { octokit, owner, name, gitToken, toolState, beforeSha } = params;
|
|
144096
144166
|
log.info(`\xBB checking out PR #${pr.number}...`);
|
|
@@ -144107,9 +144177,26 @@ async function checkoutPrBranch(pr, params) {
|
|
|
144107
144177
|
if (!alreadyOnBranch) {
|
|
144108
144178
|
$("git", ["checkout", "-B", pr.baseRef, `origin/${pr.baseRef}`], { log: false });
|
|
144109
144179
|
log.debug(`\xBB fetching PR #${pr.number} (${localBranch})...`);
|
|
144110
|
-
await
|
|
144111
|
-
|
|
144112
|
-
|
|
144180
|
+
await retry(
|
|
144181
|
+
async () => {
|
|
144182
|
+
try {
|
|
144183
|
+
await $git("fetch", ["--no-tags", "origin", `+pull/${pr.number}/head:${localBranch}`], {
|
|
144184
|
+
token: gitToken
|
|
144185
|
+
});
|
|
144186
|
+
} catch (e) {
|
|
144187
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
144188
|
+
if (PULL_REF_MISSING_PATTERN.test(msg)) {
|
|
144189
|
+
await abortIfPullRequestMoved({ octokit, owner, repo: name, pr });
|
|
144190
|
+
}
|
|
144191
|
+
throw e;
|
|
144192
|
+
}
|
|
144193
|
+
},
|
|
144194
|
+
{
|
|
144195
|
+
delaysMs: PULL_REF_RETRY_DELAYS_MS,
|
|
144196
|
+
label: `pull/${pr.number}/head fetch`,
|
|
144197
|
+
shouldRetry: (e) => PULL_REF_MISSING_PATTERN.test(e instanceof Error ? e.message : String(e))
|
|
144198
|
+
}
|
|
144199
|
+
);
|
|
144113
144200
|
$("git", ["checkout", localBranch], { log: false });
|
|
144114
144201
|
log.debug(`\xBB checked out PR #${pr.number}`);
|
|
144115
144202
|
toolState.checkoutSha = $("git", ["rev-parse", "HEAD"], { log: false }).trim();
|
|
@@ -144571,6 +144658,7 @@ function IssueTool(ctx) {
|
|
|
144571
144658
|
labels: params.labels ?? [],
|
|
144572
144659
|
assignees: params.assignees ?? []
|
|
144573
144660
|
});
|
|
144661
|
+
log.info(`\xBB created issue #${result.data.number} (id ${result.data.id})`);
|
|
144574
144662
|
const nodeId = result.data.node_id;
|
|
144575
144663
|
if (typeof nodeId === "string" && nodeId.length > 0) {
|
|
144576
144664
|
await patchWorkflowRunFields(ctx, {
|
|
@@ -144762,6 +144850,7 @@ function AddLabelsTool(ctx) {
|
|
|
144762
144850
|
issue_number,
|
|
144763
144851
|
labels
|
|
144764
144852
|
});
|
|
144853
|
+
log.info(`\xBB added labels [${labels.join(", ")}] to issue #${issue_number}`);
|
|
144765
144854
|
return {
|
|
144766
144855
|
success: true,
|
|
144767
144856
|
labels: result.data.map((label) => label.name)
|
|
@@ -144770,40 +144859,6 @@ function AddLabelsTool(ctx) {
|
|
|
144770
144859
|
});
|
|
144771
144860
|
}
|
|
144772
144861
|
|
|
144773
|
-
// mcp/learnings.ts
|
|
144774
|
-
var UpdateLearningsParams = type({
|
|
144775
|
-
learnings: type.string.describe(
|
|
144776
|
-
"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."
|
|
144777
|
-
)
|
|
144778
|
-
});
|
|
144779
|
-
function UpdateLearningsTool(ctx) {
|
|
144780
|
-
return tool({
|
|
144781
|
-
name: "update_learnings",
|
|
144782
|
-
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.",
|
|
144783
|
-
parameters: UpdateLearningsParams,
|
|
144784
|
-
execute: execute(async (params) => {
|
|
144785
|
-
const response = await apiFetch({
|
|
144786
|
-
path: `/api/repo/${ctx.repo.owner}/${ctx.repo.name}/learnings`,
|
|
144787
|
-
method: "PATCH",
|
|
144788
|
-
headers: {
|
|
144789
|
-
authorization: `Bearer ${ctx.apiToken}`,
|
|
144790
|
-
"content-type": "application/json"
|
|
144791
|
-
},
|
|
144792
|
-
body: JSON.stringify({
|
|
144793
|
-
learnings: params.learnings,
|
|
144794
|
-
model: ctx.toolState.model
|
|
144795
|
-
}),
|
|
144796
|
-
signal: AbortSignal.timeout(1e4)
|
|
144797
|
-
});
|
|
144798
|
-
if (!response.ok) {
|
|
144799
|
-
const error49 = await response.text();
|
|
144800
|
-
throw new Error(`failed to update learnings: ${error49}`);
|
|
144801
|
-
}
|
|
144802
|
-
return { success: true };
|
|
144803
|
-
})
|
|
144804
|
-
});
|
|
144805
|
-
}
|
|
144806
|
-
|
|
144807
144862
|
// mcp/output.ts
|
|
144808
144863
|
var import_ajv3 = __toESM(require_ajv(), 1);
|
|
144809
144864
|
var SetOutputParams = type({
|
|
@@ -144897,6 +144952,7 @@ function UpdatePullRequestBodyTool(ctx) {
|
|
|
144897
144952
|
pull_number: params.pull_number,
|
|
144898
144953
|
body: bodyWithFooter
|
|
144899
144954
|
});
|
|
144955
|
+
log.info(`\xBB updated pull request #${result.data.number}`);
|
|
144900
144956
|
ctx.toolState.wasUpdated = true;
|
|
144901
144957
|
return {
|
|
144902
144958
|
success: true,
|
|
@@ -144924,6 +144980,7 @@ function CreatePullRequestTool(ctx) {
|
|
|
144924
144980
|
base: params.base,
|
|
144925
144981
|
draft: params.draft ?? false
|
|
144926
144982
|
});
|
|
144983
|
+
log.info(`\xBB created pull request #${result.data.number} (id ${result.data.id})`);
|
|
144927
144984
|
const reviewer = ctx.payload.triggerer;
|
|
144928
144985
|
if (reviewer) {
|
|
144929
144986
|
try {
|
|
@@ -145475,7 +145532,7 @@ function ResolveReviewThreadTool(ctx) {
|
|
|
145475
145532
|
threadId: params.thread_id
|
|
145476
145533
|
});
|
|
145477
145534
|
const thread = response.resolveReviewThread.thread;
|
|
145478
|
-
log.
|
|
145535
|
+
log.info(`\xBB resolved review thread ${thread.id}`);
|
|
145479
145536
|
return {
|
|
145480
145537
|
thread_id: thread.id,
|
|
145481
145538
|
is_resolved: thread.isResolved,
|
|
@@ -145516,13 +145573,14 @@ function buildModeOverrides(t) {
|
|
|
145516
145573
|
|
|
145517
145574
|
An existing plan comment was found for this issue. Update that comment with the revised plan \u2014 do not create a new plan comment.
|
|
145518
145575
|
|
|
145519
|
-
1.
|
|
145520
|
-
2.
|
|
145576
|
+
1. **task list**: create your task list for this run as your first action.
|
|
145577
|
+
2. Use \`previousPlanBody\` from this response as the plan to revise; do not call \`get_issue\` or \`get_issue_comments\`.
|
|
145578
|
+
3. Revise the plan based on the user's request:
|
|
145521
145579
|
- incorporate the current plan (\`previousPlanBody\`) and the user's revision request
|
|
145522
145580
|
- gather relevant codebase context (file paths, architecture notes from AGENTS.md)
|
|
145523
145581
|
- produce a structured plan with clear milestones
|
|
145524
|
-
|
|
145525
|
-
|
|
145582
|
+
4. Call \`${t("report_progress")}\` with the full revised plan text and \`{ target_plan_comment: true }\` so it updates the existing plan comment (not the progress comment).
|
|
145583
|
+
5. Then post a short note to the progress comment (e.g. "Plan has been updated in the comment above.") via \`${t("report_progress")}\` so it is not left as "Leaping...".`
|
|
145526
145584
|
};
|
|
145527
145585
|
}
|
|
145528
145586
|
var modeInstructionParent = {
|
|
@@ -145947,24 +146005,13 @@ function UploadFileTool(ctx) {
|
|
|
145947
146005
|
if (!uploadResponse.ok) {
|
|
145948
146006
|
throw new Error(`failed to upload file: ${uploadResponse.statusText}`);
|
|
145949
146007
|
}
|
|
146008
|
+
log.info(`\xBB uploaded file ${publicUrl}`);
|
|
145950
146009
|
return { success: true, publicUrl, filename, contentLength, contentType };
|
|
145951
146010
|
})
|
|
145952
146011
|
});
|
|
145953
146012
|
}
|
|
145954
146013
|
|
|
145955
146014
|
// mcp/server.ts
|
|
145956
|
-
function initToolState(params) {
|
|
145957
|
-
const resolved = parseProgressComment(params.progressComment);
|
|
145958
|
-
if (resolved) {
|
|
145959
|
-
log.info(`\xBB using pre-created progress comment: ${resolved.id} (${resolved.type})`);
|
|
145960
|
-
}
|
|
145961
|
-
return {
|
|
145962
|
-
progressComment: resolved,
|
|
145963
|
-
hadProgressComment: !!resolved,
|
|
145964
|
-
backgroundProcesses: /* @__PURE__ */ new Map(),
|
|
145965
|
-
usageEntries: []
|
|
145966
|
-
};
|
|
145967
|
-
}
|
|
145968
146015
|
var mcpPortStart = 3764;
|
|
145969
146016
|
var mcpPortAttempts = 100;
|
|
145970
146017
|
var mcpHost = "127.0.0.1";
|
|
@@ -146040,8 +146087,7 @@ function buildOrchestratorTools(ctx, outputSchema) {
|
|
|
146040
146087
|
PushTagsTool(ctx),
|
|
146041
146088
|
DeleteBranchTool(ctx),
|
|
146042
146089
|
CreatePullRequestTool(ctx),
|
|
146043
|
-
UpdatePullRequestBodyTool(ctx)
|
|
146044
|
-
UpdateLearningsTool(ctx)
|
|
146090
|
+
UpdatePullRequestBodyTool(ctx)
|
|
146045
146091
|
];
|
|
146046
146092
|
}
|
|
146047
146093
|
async function tryStartMcpServer(ctx, tools, port) {
|
|
@@ -146198,9 +146244,6 @@ Rules:
|
|
|
146198
146244
|
- Do NOT include a changelog section \u2014 the key changes list serves this purpose
|
|
146199
146245
|
- Focus on *intent*, not *what* \u2014 the diff already shows what changed
|
|
146200
146246
|
- Get the file count and commit count from the checkout_pr metadata, not by counting manually`;
|
|
146201
|
-
function learningsStep(t, n) {
|
|
146202
|
-
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 \`${t("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.`;
|
|
146203
|
-
}
|
|
146204
146247
|
function computeModes(agentId) {
|
|
146205
146248
|
const t = (toolName) => formatMcpToolRef(agentId, toolName);
|
|
146206
146249
|
return [
|
|
@@ -146209,18 +146252,20 @@ function computeModes(agentId) {
|
|
|
146209
146252
|
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",
|
|
146210
146253
|
prompt: `### Checklist
|
|
146211
146254
|
|
|
146212
|
-
1. **
|
|
146255
|
+
1. **task list**: create your task list for this run as your first action.
|
|
146213
146256
|
|
|
146214
|
-
2. **
|
|
146257
|
+
2. **plan** (optional, for complex tasks): analyze requirements, read AGENTS.md and relevant code, produce a step-by-step implementation plan.
|
|
146258
|
+
|
|
146259
|
+
3. **setup**: checkout or create the branch:
|
|
146215
146260
|
- **PR event, modifying the existing PR**: call \`${t("checkout_pr")}\`
|
|
146216
146261
|
- **new branch**: use \`${t("git")}\` to create a branch (\`git checkout -b pullfrog/branch-name\`)
|
|
146217
146262
|
|
|
146218
|
-
|
|
146263
|
+
4. **build**: implement changes using your native file and shell tools:
|
|
146219
146264
|
- follow the plan (if you ran a plan phase)
|
|
146220
146265
|
- 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.
|
|
146221
146266
|
- run relevant tests/lints before committing
|
|
146222
146267
|
|
|
146223
|
-
|
|
146268
|
+
5. **self-review**: judgment call \u2014 does YOUR diff warrant a fresh-eyes pass?
|
|
146224
146269
|
|
|
146225
146270
|
Skip self-review (commit directly) when the diff is **genuinely trivial**:
|
|
146226
146271
|
- doc typos, comment-only edits, whitespace/format-only, import reordering
|
|
@@ -146251,13 +146296,11 @@ function computeModes(agentId) {
|
|
|
146251
146296
|
|
|
146252
146297
|
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 "..."\`).
|
|
146253
146298
|
|
|
146254
|
-
|
|
146299
|
+
6. **finalize**:
|
|
146255
146300
|
- confirm a clean working tree, then push via \`${t("push_branch")}\` (see *SYSTEM* Git rules if this fails \u2014 prepush errors are usually the repo's tests/lint, not infra timeouts)
|
|
146256
146301
|
- create a PR via \`${t("create_pull_request")}\`
|
|
146257
146302
|
- call \`${t("report_progress")}\` with the PR link or the exact error if push/PR failed
|
|
146258
146303
|
|
|
146259
|
-
${learningsStep(t, 6)}
|
|
146260
|
-
|
|
146261
146304
|
### Notes
|
|
146262
146305
|
|
|
146263
146306
|
For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
@@ -146267,27 +146310,27 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
146267
146310
|
description: "Address PR review feedback; respond to reviewer comments; make requested changes to an existing PR",
|
|
146268
146311
|
prompt: `### Checklist
|
|
146269
146312
|
|
|
146270
|
-
1.
|
|
146313
|
+
1. **task list**: create your task list for this run as your first action.
|
|
146271
146314
|
|
|
146272
|
-
2.
|
|
146315
|
+
2. Checkout the PR branch via \`${t("checkout_pr")}\`.
|
|
146273
146316
|
|
|
146274
|
-
3.
|
|
146317
|
+
3. Fetch review comments via \`${t("get_review_comments")}\`.
|
|
146318
|
+
|
|
146319
|
+
4. For each comment:
|
|
146275
146320
|
- understand the feedback
|
|
146276
146321
|
- 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.
|
|
146277
146322
|
- if the request stands, make the code change using your native tools; otherwise reply explaining why
|
|
146278
146323
|
- record what was done (or why nothing was done)
|
|
146279
146324
|
|
|
146280
|
-
|
|
146325
|
+
5. Quality check:
|
|
146281
146326
|
- 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
|
|
146282
146327
|
- commit locally via shell (\`git add . && git commit -m "..."\`)
|
|
146283
146328
|
|
|
146284
|
-
|
|
146329
|
+
6. Finalize:
|
|
146285
146330
|
- confirm a clean working tree, then push via \`${t("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*)
|
|
146286
|
-
- reply to each comment using \`${t("reply_to_review_comment")}\`
|
|
146331
|
+
- reply to each comment **exactly once** using \`${t("reply_to_review_comment")}\` \u2014 do not re-emit the same call (the runtime dedupes identical bodies and the second call is wasted)
|
|
146287
146332
|
- resolve addressed threads via \`${t("resolve_review_thread")}\`
|
|
146288
|
-
- call \`${t("report_progress")}\` with a brief summary (or the exact push error if push failed)
|
|
146289
|
-
|
|
146290
|
-
${learningsStep(t, 6)}`
|
|
146333
|
+
- call \`${t("report_progress")}\` with a brief summary (or the exact push error if push failed)`
|
|
146291
146334
|
},
|
|
146292
146335
|
// Review and IncrementalReview use the multi-lens orchestrator pattern
|
|
146293
146336
|
// (canonical source: .claude/commands/anneal.md). The orchestrator does
|
|
@@ -146306,11 +146349,13 @@ ${learningsStep(t, 6)}`
|
|
|
146306
146349
|
description: "Review code, PRs, or implementations; provide feedback or suggestions; identify issues; or check code quality, style, and correctness",
|
|
146307
146350
|
prompt: `### Checklist
|
|
146308
146351
|
|
|
146309
|
-
1. **
|
|
146352
|
+
1. **task list**: create your task list for this run as your first action.
|
|
146310
146353
|
|
|
146311
|
-
2. **
|
|
146354
|
+
2. **checkout**: call \`${t("checkout_pr")}\` \u2014 this returns PR metadata and a \`diffPath\`. read the diff TOC end-to-end and treat its file line ranges as your coverage checklist.
|
|
146312
146355
|
|
|
146313
|
-
|
|
146356
|
+
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 \`${t("get_pull_request")}\` and other read-only GitHub tools for additional context if needed.
|
|
146357
|
+
|
|
146358
|
+
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.
|
|
146314
146359
|
|
|
146315
146360
|
"Genuinely trivial" (skip):
|
|
146316
146361
|
- single-word doc typo, whitespace/format-only, comment-only across any number of files
|
|
@@ -146355,7 +146400,7 @@ ${learningsStep(t, 6)}`
|
|
|
146355
146400
|
- **holistic** \u2014 does the PR make sense as a whole? symmetric flows (delete for every create, rollback for every migration)?
|
|
146356
146401
|
- **subsystem lenses** (invent as the PR demands) \u2014 auth, billing, payments, schema migration, webhooks, secrets, RBAC, multi-tenant isolation, cron/scheduling, etc.
|
|
146357
146402
|
|
|
146358
|
-
|
|
146403
|
+
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:
|
|
146359
146404
|
- the diff path / target \u2014 reading the diff and the codebase is its job
|
|
146360
146405
|
- **only one lens** \u2014 never a multi-section "review for X, Y, and Z" prompt
|
|
146361
146406
|
- **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\`.
|
|
@@ -146370,20 +146415,33 @@ ${learningsStep(t, 6)}`
|
|
|
146370
146415
|
- do NOT pre-shape their output with a finding schema
|
|
146371
146416
|
- do NOT mention the other lenses (independence is the point \u2014 overlapping findings are a strong signal)
|
|
146372
146417
|
|
|
146373
|
-
|
|
146418
|
+
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.
|
|
146374
146419
|
|
|
146375
146420
|
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.
|
|
146376
146421
|
|
|
146377
|
-
|
|
146422
|
+
6. **submit**: ALWAYS submit exactly one review via \`${t("create_pull_request_review")}\`. Do NOT call \`report_progress\` \u2014 the review is the final record and the progress comment will be cleaned up automatically.
|
|
146378
146423
|
|
|
146379
146424
|
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.
|
|
146380
146425
|
|
|
146381
146426
|
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.
|
|
146382
146427
|
|
|
146383
|
-
|
|
146428
|
+
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:
|
|
146429
|
+
|
|
146430
|
+
- \`[!CAUTION]\` \u2014 large red banner. Reads as "this will break something."
|
|
146431
|
+
- \`[!IMPORTANT]\` \u2014 large purple banner. Reads as "you need to look at this before merging."
|
|
146432
|
+
- \`[!NOTE]\` \u2014 small blue inline callout. Reads as "FYI, here's something worth noting."
|
|
146433
|
+
- no callout \u2014 plain text. Reads as routine review output.
|
|
146434
|
+
|
|
146435
|
+
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.
|
|
146436
|
+
|
|
146437
|
+
- **critical issues** (blocks merge \u2014 bugs, security, data loss, broken core flows):
|
|
146384
146438
|
\`approved: false\`. Body opens with \`> [!CAUTION]\\n> This PR introduces ...\`, followed by the PR summary. Include all inline comments via \`comments\`.
|
|
146385
|
-
- **
|
|
146386
|
-
\`approved: false\`. Body opens with \`> [!IMPORTANT]\\n>
|
|
146439
|
+
- **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):
|
|
146440
|
+
\`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\`.
|
|
146441
|
+
- **minor suggestions only** (single-line nits, doc/comment polish, defer-able observations, "rough edges"):
|
|
146442
|
+
\`approved: false\`. NO alert blockquote. Body opens directly with the PR summary. Include all inline comments via \`comments\`.
|
|
146443
|
+
- **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):
|
|
146444
|
+
\`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.
|
|
146387
146445
|
- **no actionable issues**:
|
|
146388
146446
|
\`approved: true\`. Body opens with \`No new issues found.\` followed by the PR summary.
|
|
146389
146447
|
|
|
@@ -146392,7 +146450,7 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
146392
146450
|
// IncrementalReview shares Review's multi-lens orchestrator pattern but
|
|
146393
146451
|
// scopes the target to the incremental diff. The "issues must be NEW
|
|
146394
146452
|
// since the last Pullfrog review" filter lives at aggregation time
|
|
146395
|
-
// (step
|
|
146453
|
+
// (step 6), NOT in the subagent prompt — pushing the filter into
|
|
146396
146454
|
// subagents matches the canonical anneal anti-pattern of "list known
|
|
146397
146455
|
// pre-existing failures — don't flag these" and suppresses signal on
|
|
146398
146456
|
// regressions the new commits amplified. The review body is just
|
|
@@ -146405,15 +146463,17 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
146405
146463
|
description: "Re-review a PR after new commits are pushed; focus on new changes since the last review",
|
|
146406
146464
|
prompt: `### Checklist
|
|
146407
146465
|
|
|
146408
|
-
1. **
|
|
146466
|
+
1. **task list**: create your task list for this run as your first action.
|
|
146467
|
+
|
|
146468
|
+
2. **checkout**: call \`${t("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.
|
|
146409
146469
|
|
|
146410
|
-
|
|
146470
|
+
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.
|
|
146411
146471
|
|
|
146412
|
-
|
|
146472
|
+
4. **prior feedback**: fetch previous reviews via \`${t("list_pull_request_reviews")}\`. for the most recent Pullfrog review, call \`${t("get_review_comments")}\` with the review ID to retrieve specific prior line-level feedback. you'll use this to filter your aggregation in step 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.
|
|
146413
146473
|
|
|
146414
|
-
|
|
146474
|
+
5. **triage & fan out**: orient on the *incremental* changes \u2014 domain, seams, external contracts, user-facing surfaces.
|
|
146415
146475
|
|
|
146416
|
-
if the incremental changes are **genuinely trivial**, skip the fan-out entirely and jump to step
|
|
146476
|
+
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).
|
|
146417
146477
|
|
|
146418
146478
|
"Genuinely trivial" (skip): formatting/comment tweaks, import reordering, lockfile regen, mechanical rename of import paths, whitespace-only.
|
|
146419
146479
|
"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.
|
|
@@ -146421,8 +146481,8 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
146421
146481
|
|
|
146422
146482
|
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.
|
|
146423
146483
|
|
|
146424
|
-
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
|
|
146425
|
-
- 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
|
|
146484
|
+
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:
|
|
146485
|
+
- 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
|
|
146426
146486
|
- **only one lens** \u2014 never a multi-section "review for X, Y, and Z" prompt
|
|
146427
146487
|
- **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\`.
|
|
146428
146488
|
- 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.
|
|
@@ -146436,15 +146496,21 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
146436
146496
|
- do NOT pre-shape their output with a finding schema
|
|
146437
146497
|
- do NOT mention the other lenses (independence is the point)
|
|
146438
146498
|
|
|
146439
|
-
|
|
146499
|
+
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 \`${t("list_pull_request_reviews")}\` in step 4) and run \`git diff <prior-review-sha>..HEAD\` to isolate the lines added since that review. draft inline comments with NEW line numbers from the full PR diff \u2014 every comment must be actionable, 2-3 sentences max.
|
|
146440
146500
|
|
|
146441
|
-
|
|
146501
|
+
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.
|
|
146442
146502
|
|
|
146443
|
-
|
|
146503
|
+
8. Submit \u2014 every run must end with EXACTLY ONE of \`${t("create_pull_request_review")}\` (substantive review) or \`${t("report_progress")}\` (no-review acknowledgement). do NOT call \`create_issue_comment\` for review output.
|
|
146504
|
+
|
|
146505
|
+
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.
|
|
146506
|
+
|
|
146507
|
+
Follow these rules:
|
|
146444
146508
|
- 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.
|
|
146445
|
-
- IF NO NEW ISSUES, NON-SUBSTANTIVE CHANGES ONLY (trivial formatting, import reordering, comment tweaks): do NOT submit a review.
|
|
146446
|
-
- ELSE IF NEW CRITICAL ISSUES (blocks merge): call \`${t("create_pull_request_review")}\` with \`approved: false\`, all comments, and the review body. body opens with
|
|
146447
|
-
- ELSE IF NEW
|
|
146509
|
+
- IF NO NEW ISSUES, NON-SUBSTANTIVE CHANGES ONLY (trivial formatting, import reordering, comment tweaks): do NOT submit a review. Instead call \`${t("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.
|
|
146510
|
+
- ELSE IF NEW CRITICAL ISSUES (blocks merge \u2014 bugs, security, data loss, broken core flows): call \`${t("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.
|
|
146511
|
+
- 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 \`${t("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.
|
|
146512
|
+
- ELSE IF NEW MINOR SUGGESTIONS ONLY (single-line nits, doc/comment polish, defer-able observations, "rough edges"): call \`${t("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.
|
|
146513
|
+
- 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 \`${t("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.
|
|
146448
146514
|
- ELSE IF NO NEW ISSUES, SUBSTANTIVE CHANGES (new functionality, behavior changes, or fixes to prior review feedback): call \`${t("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.`
|
|
146449
146515
|
},
|
|
146450
146516
|
{
|
|
@@ -146452,61 +146518,63 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
146452
146518
|
description: "Create plans, break down tasks, outline steps, analyze requirements, understand scope of work, or provide task breakdowns",
|
|
146453
146519
|
prompt: `### Checklist
|
|
146454
146520
|
|
|
146455
|
-
1.
|
|
146521
|
+
1. **task list**: create your task list for this run as your first action.
|
|
146522
|
+
|
|
146523
|
+
2. Analyze the task and gather context:
|
|
146456
146524
|
- read AGENTS.md and relevant codebase files
|
|
146457
146525
|
- understand the architecture and constraints
|
|
146458
146526
|
|
|
146459
|
-
|
|
146460
|
-
|
|
146461
|
-
3. Call \`${t("report_progress")}\` with the plan.
|
|
146527
|
+
3. Produce a structured, actionable plan with clear milestones.
|
|
146462
146528
|
|
|
146463
|
-
|
|
146529
|
+
4. Call \`${t("report_progress")}\` with the plan.`
|
|
146464
146530
|
},
|
|
146465
146531
|
{
|
|
146466
146532
|
name: "Fix",
|
|
146467
146533
|
description: "Fix CI failures; debug failing tests or builds; investigate and resolve check suite failures",
|
|
146468
146534
|
prompt: `### Checklist
|
|
146469
146535
|
|
|
146470
|
-
1.
|
|
146536
|
+
1. **task list**: create your task list for this run as your first action.
|
|
146471
146537
|
|
|
146472
|
-
2.
|
|
146538
|
+
2. Checkout the PR branch via \`${t("checkout_pr")}\`.
|
|
146473
146539
|
|
|
146474
|
-
3.
|
|
146540
|
+
3. Fetch check suite logs via \`${t("get_check_suite_logs")}\`.
|
|
146475
146541
|
|
|
146476
|
-
4.
|
|
146542
|
+
4. **CRITICAL**: verify the failure was INTRODUCED BY THIS PR before fixing. If unrelated, abort and report.
|
|
146543
|
+
|
|
146544
|
+
5. Diagnose and fix:
|
|
146477
146545
|
- read the workflow file, reproduce locally with the EXACT same commands CI runs
|
|
146478
146546
|
- fix the issue using your native file and shell tools
|
|
146479
146547
|
- verify the fix by re-running the exact CI command
|
|
146480
146548
|
- 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.
|
|
146481
146549
|
- commit locally via shell (\`git add . && git commit -m "..."\`)
|
|
146482
146550
|
|
|
146483
|
-
|
|
146551
|
+
6. Finalize:
|
|
146484
146552
|
- confirm a clean working tree, then push via \`${t("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*)
|
|
146485
|
-
- call \`${t("report_progress")}\` with the diagnosis and fix summary (or the exact push error if push failed)
|
|
146486
|
-
|
|
146487
|
-
${learningsStep(t, 6)}`
|
|
146553
|
+
- call \`${t("report_progress")}\` with the diagnosis and fix summary (or the exact push error if push failed)`
|
|
146488
146554
|
},
|
|
146489
146555
|
{
|
|
146490
146556
|
name: "ResolveConflicts",
|
|
146491
146557
|
description: "Resolve merge conflicts in a PR branch against the base branch",
|
|
146492
146558
|
prompt: `### Checklist
|
|
146493
146559
|
|
|
146494
|
-
1. **
|
|
146560
|
+
1. **task list**: create your task list for this run as your first action.
|
|
146561
|
+
|
|
146562
|
+
2. **Setup**:
|
|
146495
146563
|
- Call \`${t("checkout_pr")}\` to get the PR branch.
|
|
146496
146564
|
- Call \`${t("get_pull_request")}\` to identify the base branch (e.g., 'main').
|
|
146497
146565
|
- Call \`${t("git_fetch")}\` to fetch the base branch.
|
|
146498
146566
|
|
|
146499
|
-
|
|
146567
|
+
3. **Merge Attempt**:
|
|
146500
146568
|
- Run \`git merge origin/<base_branch>\` via shell.
|
|
146501
|
-
- If it succeeds automatically, confirm a clean working tree, push via \`${t("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*), and call \`${t("report_progress")}\` with a brief success note or the exact push error if push failed \u2014 **then stop; do not run steps
|
|
146502
|
-
- If it fails (conflicts), resolve them manually (continue to steps
|
|
146569
|
+
- If it succeeds automatically, confirm a clean working tree, push via \`${t("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*), and call \`${t("report_progress")}\` with a brief success note or the exact push error if push failed \u2014 **then stop; do not run steps 4\u20135.**
|
|
146570
|
+
- If it fails (conflicts), resolve them manually (continue to steps 4\u20135).
|
|
146503
146571
|
|
|
146504
|
-
|
|
146572
|
+
4. **Resolve Conflicts**:
|
|
146505
146573
|
- Run \`git status\` or parse the merge output to find the list of conflicting files.
|
|
146506
146574
|
- 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.
|
|
146507
146575
|
- Verify the file syntax is correct after resolution.
|
|
146508
146576
|
|
|
146509
|
-
|
|
146577
|
+
5. **Finalize**:
|
|
146510
146578
|
- Run a final verification (build/test) to ensure the resolution works.
|
|
146511
146579
|
- \`git add . && git commit -m "resolve merge conflicts"\`
|
|
146512
146580
|
- confirm a clean working tree, then push via \`${t("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*)
|
|
@@ -146517,24 +146585,43 @@ ${learningsStep(t, 6)}`
|
|
|
146517
146585
|
description: "General-purpose tasks that don't fit other modes: answering questions, adding comments, labeling, running ad-hoc commands, or any direct request",
|
|
146518
146586
|
prompt: `### Checklist
|
|
146519
146587
|
|
|
146520
|
-
1.
|
|
146588
|
+
1. **task list**: create your task list for this run as your first action.
|
|
146589
|
+
|
|
146590
|
+
2. Analyze the task. For simple operations (labeling, commenting, answering questions, running a single command), handle directly.
|
|
146521
146591
|
|
|
146522
|
-
|
|
146592
|
+
3. For substantial work \u2014 code changes across multiple files, multi-step investigations:
|
|
146523
146593
|
- plan your approach before starting
|
|
146524
146594
|
- use native file and shell tools for local operations
|
|
146525
146595
|
- use ${pullfrogMcpName} MCP tools for GitHub/git operations
|
|
146526
146596
|
- 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
|
|
146527
146597
|
|
|
146528
|
-
|
|
146598
|
+
4. Finalize:
|
|
146529
146599
|
- if code changes were made, push to a pull request (new or existing) using \`${t("push_branch")}\` and \`${t("create_pull_request")}\` as needed. \`git status\` must be clean before you finish (see *SYSTEM* Git rules if push fails).
|
|
146530
146600
|
- call \`${t("report_progress")}\` once with results \u2014 include exact tool errors if push or PR creation failed
|
|
146531
|
-
- if the task involved labeling, commenting, or other GitHub operations, perform those directly
|
|
146532
|
-
|
|
146533
|
-
${learningsStep(t, 4)}`
|
|
146601
|
+
- if the task involved labeling, commenting, or other GitHub operations, perform those directly`
|
|
146534
146602
|
}
|
|
146535
146603
|
];
|
|
146536
146604
|
}
|
|
146537
146605
|
var modes = computeModes("opencode");
|
|
146606
|
+
var NON_COMMITTING_MODES = /* @__PURE__ */ new Set([
|
|
146607
|
+
"Review",
|
|
146608
|
+
"IncrementalReview",
|
|
146609
|
+
"Plan"
|
|
146610
|
+
]);
|
|
146611
|
+
|
|
146612
|
+
// toolState.ts
|
|
146613
|
+
function initToolState(params) {
|
|
146614
|
+
const resolved = parseProgressComment(params.progressComment);
|
|
146615
|
+
if (resolved) {
|
|
146616
|
+
log.info(`\xBB using pre-created progress comment: ${resolved.id} (${resolved.type})`);
|
|
146617
|
+
}
|
|
146618
|
+
return {
|
|
146619
|
+
progressComment: resolved,
|
|
146620
|
+
hadProgressComment: !!resolved,
|
|
146621
|
+
backgroundProcesses: /* @__PURE__ */ new Map(),
|
|
146622
|
+
usageEntries: []
|
|
146623
|
+
};
|
|
146624
|
+
}
|
|
146538
146625
|
|
|
146539
146626
|
// agents/claude.ts
|
|
146540
146627
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
@@ -146630,6 +146717,17 @@ async function installFromNpmTarball(params) {
|
|
|
146630
146717
|
// utils/providerErrors.ts
|
|
146631
146718
|
var statusKey = `\\b(?:status[_ ]?code|http[_ ]?status|status)["']?\\s*[:=]\\s*["']?`;
|
|
146632
146719
|
var PROVIDER_ERROR_PATTERNS = [
|
|
146720
|
+
// auth patterns must come BEFORE rate-limit patterns. OpenRouter 401 error
|
|
146721
|
+
// payloads carry `x-ratelimit-*` response headers in the dump, and the
|
|
146722
|
+
// free-form rate-limit regex below would otherwise win on word-boundary
|
|
146723
|
+
// matches inside header names. canonical 401 messages: OpenRouter returns
|
|
146724
|
+
// `{"error":{"message":"User not found","code":401}}` for disabled or
|
|
146725
|
+
// invalid keys (https://openai.luzhipeng.com/docs/api/reference/errors-and-debugging).
|
|
146726
|
+
{ regex: new RegExp(`${statusKey}401\\b`, "i"), label: "auth error (401)" },
|
|
146727
|
+
{ regex: new RegExp(`${statusKey}403\\b`, "i"), label: "auth error (403)" },
|
|
146728
|
+
{ regex: /\bUser not found\b/i, label: "auth error (invalid/disabled key)" },
|
|
146729
|
+
{ regex: /\bInvalid authentication\b/i, label: "auth error (invalid credentials)" },
|
|
146730
|
+
{ regex: /\bNo auth credentials found\b/i, label: "auth error (missing credentials)" },
|
|
146633
146731
|
{ regex: new RegExp(`${statusKey}429\\b`, "i"), label: "rate limited (429)" },
|
|
146634
146732
|
{ regex: new RegExp(`${statusKey}500\\b`, "i"), label: "provider 500 error" },
|
|
146635
146733
|
{ regex: new RegExp(`${statusKey}503\\b`, "i"), label: "provider unavailable (503)" },
|
|
@@ -146693,7 +146791,7 @@ function installBundledSkills(params) {
|
|
|
146693
146791
|
writeFileSync6(join9(skillDir, "SKILL.md"), content);
|
|
146694
146792
|
}
|
|
146695
146793
|
}
|
|
146696
|
-
log.
|
|
146794
|
+
log.success(`installed bundled skills: ${BUNDLED_SKILL_NAMES.join(", ")}`);
|
|
146697
146795
|
}
|
|
146698
146796
|
function addSkill(params) {
|
|
146699
146797
|
const result = spawnSync5(
|
|
@@ -146718,7 +146816,7 @@ function addSkill(params) {
|
|
|
146718
146816
|
}
|
|
146719
146817
|
);
|
|
146720
146818
|
if (result.status === 0) {
|
|
146721
|
-
log.
|
|
146819
|
+
log.success(`installed ${params.skill} skill (${params.agent})`);
|
|
146722
146820
|
} else {
|
|
146723
146821
|
const stderr = (result.stderr?.toString() || "").trim();
|
|
146724
146822
|
const errorMsg = result.error ? result.error.message : stderr;
|
|
@@ -146770,6 +146868,13 @@ var ThinkingTimer = class {
|
|
|
146770
146868
|
|
|
146771
146869
|
// agents/postRun.ts
|
|
146772
146870
|
import { readFile } from "node:fs/promises";
|
|
146871
|
+
function getUnsubmittedReview(toolState) {
|
|
146872
|
+
const mode = toolState.selectedMode;
|
|
146873
|
+
if (mode !== "Review" && mode !== "IncrementalReview") return null;
|
|
146874
|
+
if (toolState.review || toolState.finalSummaryWritten) return null;
|
|
146875
|
+
if (!toolState.hadProgressComment) return null;
|
|
146876
|
+
return mode;
|
|
146877
|
+
}
|
|
146773
146878
|
var MAX_HOOK_OUTPUT_CHARS = 4096;
|
|
146774
146879
|
function truncateHookOutput(raw2) {
|
|
146775
146880
|
if (raw2.length <= MAX_HOOK_OUTPUT_CHARS) return raw2;
|
|
@@ -146831,39 +146936,72 @@ function buildSummaryStalePrompt(filePath) {
|
|
|
146831
146936
|
"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."
|
|
146832
146937
|
].join("\n");
|
|
146833
146938
|
}
|
|
146834
|
-
|
|
146939
|
+
function buildUnsubmittedReviewPrompt(mode) {
|
|
146940
|
+
if (mode === "Review") {
|
|
146941
|
+
return [
|
|
146942
|
+
`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.`,
|
|
146943
|
+
"",
|
|
146944
|
+
"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.",
|
|
146945
|
+
"",
|
|
146946
|
+
"do NOT stop again until `create_pull_request_review` has been called successfully."
|
|
146947
|
+
].join("\n");
|
|
146948
|
+
}
|
|
146949
|
+
return [
|
|
146950
|
+
`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.`,
|
|
146951
|
+
"",
|
|
146952
|
+
"do exactly one of:",
|
|
146953
|
+
"- 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.",
|
|
146954
|
+
"- 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.",
|
|
146955
|
+
"",
|
|
146956
|
+
"do NOT stop again until one of those tools has been called successfully."
|
|
146957
|
+
].join("\n");
|
|
146958
|
+
}
|
|
146959
|
+
async function collectPostRunIssues(ctx, options = {}) {
|
|
146835
146960
|
const issues = {};
|
|
146836
|
-
if (
|
|
146837
|
-
const failure = await executeStopHook(
|
|
146961
|
+
if (ctx.stopScript) {
|
|
146962
|
+
const failure = await executeStopHook(ctx.stopScript);
|
|
146838
146963
|
if (failure) issues.stopHook = failure;
|
|
146839
146964
|
}
|
|
146840
146965
|
const status = getGitStatus();
|
|
146841
|
-
|
|
146842
|
-
if (
|
|
146843
|
-
|
|
146844
|
-
|
|
146966
|
+
const mode = ctx.toolState.selectedMode;
|
|
146967
|
+
if (status) {
|
|
146968
|
+
if (mode && NON_COMMITTING_MODES.has(mode)) {
|
|
146969
|
+
log.info(`\xBB dirty-tree gate suppressed: mode \`${mode}\` does not commit`);
|
|
146970
|
+
} else {
|
|
146971
|
+
issues.dirtyTree = status;
|
|
146972
|
+
}
|
|
146845
146973
|
}
|
|
146974
|
+
const summaryFilePath2 = ctx.toolState.summaryFilePath;
|
|
146975
|
+
const summarySeed = ctx.toolState.summarySeed;
|
|
146976
|
+
if (!options.skipSummaryStale && summaryFilePath2 && summarySeed !== void 0) {
|
|
146977
|
+
const stale = await isSummaryUnchanged(summaryFilePath2, summarySeed);
|
|
146978
|
+
if (stale) issues.summaryStale = { filePath: summaryFilePath2 };
|
|
146979
|
+
}
|
|
146980
|
+
const unsubmittedMode = getUnsubmittedReview(ctx.toolState);
|
|
146981
|
+
if (unsubmittedMode) issues.unsubmittedReview = unsubmittedMode;
|
|
146846
146982
|
return issues;
|
|
146847
146983
|
}
|
|
146848
146984
|
function buildPostRunPrompt(issues) {
|
|
146849
146985
|
const parts = [];
|
|
146850
146986
|
if (issues.stopHook) parts.push(buildStopHookPrompt(issues.stopHook));
|
|
146987
|
+
if (issues.unsubmittedReview) {
|
|
146988
|
+
parts.push(buildUnsubmittedReviewPrompt(issues.unsubmittedReview));
|
|
146989
|
+
}
|
|
146851
146990
|
if (issues.dirtyTree) parts.push(buildCommitPrompt(issues.dirtyTree));
|
|
146852
146991
|
if (issues.summaryStale) parts.push(buildSummaryStalePrompt(issues.summaryStale.filePath));
|
|
146853
146992
|
return parts.join("\n\n---\n\n");
|
|
146854
146993
|
}
|
|
146855
|
-
function buildLearningsReflectionPrompt(
|
|
146856
|
-
const t = (name) => formatMcpToolRef(agentId, name);
|
|
146994
|
+
function buildLearningsReflectionPrompt(filePath) {
|
|
146857
146995
|
return [
|
|
146858
|
-
`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
|
|
146996
|
+
`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?`,
|
|
146859
146997
|
"",
|
|
146860
|
-
`
|
|
146998
|
+
`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.`,
|
|
146861
146999
|
"",
|
|
146862
|
-
`
|
|
146863
|
-
`- only
|
|
146864
|
-
`-
|
|
146865
|
-
`-
|
|
146866
|
-
`- if you
|
|
147000
|
+
`keep the file healthy:`,
|
|
147001
|
+
`- only add bullets when the finding is high-confidence AND broadly useful. skip speculative, one-off, or "maybe" findings.`,
|
|
147002
|
+
`- prune bullets that are clearly wrong, no longer relevant, or low-signal (rarely useful). a focused, accurate file beats a long stale one.`,
|
|
147003
|
+
`- 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.`,
|
|
147004
|
+
`- 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.`
|
|
146867
147005
|
].join("\n");
|
|
146868
147006
|
}
|
|
146869
147007
|
async function runPostRunRetryLoop(params) {
|
|
@@ -146875,10 +147013,8 @@ async function runPostRunRetryLoop(params) {
|
|
|
146875
147013
|
let summaryStaleNudged = false;
|
|
146876
147014
|
while (gateResumeCount < MAX_POST_RUN_RETRIES) {
|
|
146877
147015
|
if (!result.success) break;
|
|
146878
|
-
const issues = await collectPostRunIssues({
|
|
146879
|
-
|
|
146880
|
-
summaryFilePath: summaryStaleNudged ? void 0 : params.summaryFilePath,
|
|
146881
|
-
summarySeed: summaryStaleNudged ? void 0 : params.summarySeed
|
|
147016
|
+
const issues = await collectPostRunIssues(params.ctx, {
|
|
147017
|
+
skipSummaryStale: summaryStaleNudged
|
|
146882
147018
|
});
|
|
146883
147019
|
if (issues.summaryStale) summaryStaleNudged = true;
|
|
146884
147020
|
finalIssues = issues;
|
|
@@ -146926,7 +147062,7 @@ async function runPostRunRetryLoop(params) {
|
|
|
146926
147062
|
gateResumeCount++;
|
|
146927
147063
|
}
|
|
146928
147064
|
if (gateResumeCount > 0 && result.success && hasPostRunIssues(finalIssues)) {
|
|
146929
|
-
finalIssues = await collectPostRunIssues({
|
|
147065
|
+
finalIssues = await collectPostRunIssues(params.ctx, { skipSummaryStale: true });
|
|
146930
147066
|
}
|
|
146931
147067
|
if (result.success && finalIssues.stopHook) {
|
|
146932
147068
|
const retryNote = gateResumeCount > 0 ? ` after ${gateResumeCount} retry ${gateResumeCount === 1 ? "attempt" : "attempts"}` : "";
|
|
@@ -146937,6 +147073,16 @@ async function runPostRunRetryLoop(params) {
|
|
|
146937
147073
|
usage: aggregatedUsage
|
|
146938
147074
|
};
|
|
146939
147075
|
}
|
|
147076
|
+
if (result.success && finalIssues.unsubmittedReview) {
|
|
147077
|
+
const retryNote = gateResumeCount > 0 ? ` after ${gateResumeCount} retry ${gateResumeCount === 1 ? "attempt" : "attempts"}` : "";
|
|
147078
|
+
const expected = finalIssues.unsubmittedReview === "Review" ? "create_pull_request_review" : "create_pull_request_review or report_progress";
|
|
147079
|
+
return {
|
|
147080
|
+
...result,
|
|
147081
|
+
success: false,
|
|
147082
|
+
error: `${finalIssues.unsubmittedReview} mode finished without calling ${expected}${retryNote}`,
|
|
147083
|
+
usage: aggregatedUsage
|
|
147084
|
+
};
|
|
147085
|
+
}
|
|
146940
147086
|
return { ...result, usage: aggregatedUsage };
|
|
146941
147087
|
}
|
|
146942
147088
|
|
|
@@ -147053,6 +147199,12 @@ function resolveEffort(model) {
|
|
|
147053
147199
|
if (model?.includes("opus")) return "max";
|
|
147054
147200
|
return "high";
|
|
147055
147201
|
}
|
|
147202
|
+
function tailLines(text, maxCodeUnits) {
|
|
147203
|
+
if (text.length <= maxCodeUnits) return text;
|
|
147204
|
+
const tail = text.slice(-maxCodeUnits);
|
|
147205
|
+
const firstNewline = tail.indexOf("\n");
|
|
147206
|
+
return firstNewline > 0 && firstNewline < tail.length - 1 ? tail.slice(firstNewline + 1) : tail;
|
|
147207
|
+
}
|
|
147056
147208
|
async function runClaude(params) {
|
|
147057
147209
|
const startTime = performance6.now();
|
|
147058
147210
|
let eventCount = 0;
|
|
@@ -147060,6 +147212,8 @@ async function runClaude(params) {
|
|
|
147060
147212
|
let finalOutput = "";
|
|
147061
147213
|
let sessionId;
|
|
147062
147214
|
let resultErrorSubtype = null;
|
|
147215
|
+
let lastResultError = null;
|
|
147216
|
+
let syntheticStopFailure = false;
|
|
147063
147217
|
let accumulatedTokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
147064
147218
|
let accumulatedCostUsd = 0;
|
|
147065
147219
|
let tokensLogged = false;
|
|
@@ -147142,6 +147296,16 @@ async function runClaude(params) {
|
|
|
147142
147296
|
if (event.session_id) sessionId = event.session_id;
|
|
147143
147297
|
const subtype = event.subtype || "unknown";
|
|
147144
147298
|
const numTurns = event.num_turns || 0;
|
|
147299
|
+
if (event.is_error === true && subtype === "success") {
|
|
147300
|
+
const apiStatus = event.api_error_status;
|
|
147301
|
+
lastResultError = event.result?.trim() || `claude reported is_error=true with no result text (api_error_status=${apiStatus ?? "unknown"})`;
|
|
147302
|
+
resultErrorSubtype = subtype;
|
|
147303
|
+
syntheticStopFailure = true;
|
|
147304
|
+
log.info(
|
|
147305
|
+
`\xBB ${params.label} result error: subtype=${subtype}, api_error_status=${apiStatus ?? "unknown"}, message=${lastResultError}`
|
|
147306
|
+
);
|
|
147307
|
+
return;
|
|
147308
|
+
}
|
|
147145
147309
|
if (subtype === "success") {
|
|
147146
147310
|
const usage = event.usage;
|
|
147147
147311
|
const inputTokens = usage?.input_tokens || 0;
|
|
@@ -147164,12 +147328,15 @@ async function runClaude(params) {
|
|
|
147164
147328
|
}
|
|
147165
147329
|
} else if (subtype === "error_max_turns") {
|
|
147166
147330
|
resultErrorSubtype = subtype;
|
|
147331
|
+
lastResultError = event.errors?.join("\n").trim() || null;
|
|
147167
147332
|
log.info(`\xBB ${params.label} max turns reached: ${JSON.stringify(event)}`);
|
|
147168
147333
|
} else if (subtype === "error_during_execution") {
|
|
147169
147334
|
resultErrorSubtype = subtype;
|
|
147335
|
+
lastResultError = event.errors?.join("\n").trim() || null;
|
|
147170
147336
|
log.info(`\xBB ${params.label} execution error: ${JSON.stringify(event)}`);
|
|
147171
147337
|
} else if (subtype.startsWith("error")) {
|
|
147172
147338
|
resultErrorSubtype = subtype;
|
|
147339
|
+
lastResultError = event.errors?.join("\n").trim() || null;
|
|
147173
147340
|
log.info(`\xBB ${params.label} result: subtype=${subtype}, data=${JSON.stringify(event)}`);
|
|
147174
147341
|
} else {
|
|
147175
147342
|
log.info(`\xBB ${params.label} result: subtype=${subtype}, data=${JSON.stringify(event)}`);
|
|
@@ -147277,14 +147444,15 @@ async function runClaude(params) {
|
|
|
147277
147444
|
if (stderrContext) log.info(`\xBB last stderr output:
|
|
147278
147445
|
${stderrContext}`);
|
|
147279
147446
|
}
|
|
147280
|
-
if (!tokensLogged && (accumulatedTokens.input > 0 || accumulatedTokens.output > 0 || accumulatedTokens.cacheRead > 0 || accumulatedTokens.cacheWrite > 0)) {
|
|
147447
|
+
if (!tokensLogged && !syntheticStopFailure && (accumulatedTokens.input > 0 || accumulatedTokens.output > 0 || accumulatedTokens.cacheRead > 0 || accumulatedTokens.cacheWrite > 0)) {
|
|
147281
147448
|
logTokenTable({ ...accumulatedTokens, costUsd: accumulatedCostUsd });
|
|
147282
147449
|
tokensLogged = true;
|
|
147283
147450
|
}
|
|
147284
147451
|
const usage = buildUsage();
|
|
147285
147452
|
if (result.exitCode !== 0) {
|
|
147286
147453
|
const errorContext = lastProviderError ? ` (${lastProviderError})` : "";
|
|
147287
|
-
const
|
|
147454
|
+
const truncatedStdout = result.stdout ? tailLines(result.stdout, 2048) : "";
|
|
147455
|
+
const errorMessage = lastResultError || result.stderr || truncatedStdout || `unknown error - no output from Claude CLI${errorContext}`;
|
|
147288
147456
|
log.error(
|
|
147289
147457
|
`${params.label} exited with code ${result.exitCode}${errorContext}: ${errorMessage}`
|
|
147290
147458
|
);
|
|
@@ -147311,7 +147479,7 @@ ${stderrContext}`);
|
|
|
147311
147479
|
return {
|
|
147312
147480
|
success: false,
|
|
147313
147481
|
output: finalOutput || output,
|
|
147314
|
-
error: `result subtype: ${resultErrorSubtype}`,
|
|
147482
|
+
error: lastResultError || `result subtype: ${resultErrorSubtype}`,
|
|
147315
147483
|
usage,
|
|
147316
147484
|
sessionId
|
|
147317
147485
|
};
|
|
@@ -147441,12 +147609,10 @@ var claude = agent({
|
|
|
147441
147609
|
args: [...baseArgs, "-p", ctx.instructions.full]
|
|
147442
147610
|
});
|
|
147443
147611
|
return runPostRunRetryLoop({
|
|
147612
|
+
ctx,
|
|
147444
147613
|
initialResult: result,
|
|
147445
147614
|
initialUsage: result.usage,
|
|
147446
|
-
|
|
147447
|
-
summaryFilePath: ctx.summaryFilePath,
|
|
147448
|
-
summarySeed: ctx.summarySeed,
|
|
147449
|
-
reflectionPrompt: buildLearningsReflectionPrompt("claude"),
|
|
147615
|
+
reflectionPrompt: ctx.toolState.learningsFilePath ? buildLearningsReflectionPrompt(ctx.toolState.learningsFilePath) : void 0,
|
|
147450
147616
|
canResume: (r) => Boolean(r.sessionId),
|
|
147451
147617
|
resume: async (c) => {
|
|
147452
147618
|
const sessionId = c.previousResult.sessionId;
|
|
@@ -147462,9 +147628,92 @@ var claude = agent({
|
|
|
147462
147628
|
|
|
147463
147629
|
// agents/opencode.ts
|
|
147464
147630
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
147465
|
-
import { mkdirSync as mkdirSync5 } from "node:fs";
|
|
147631
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "node:fs";
|
|
147466
147632
|
import { join as join11 } from "node:path";
|
|
147467
147633
|
import { performance as performance7 } from "node:perf_hooks";
|
|
147634
|
+
|
|
147635
|
+
// agents/opencodePlugin.ts
|
|
147636
|
+
var PULLFROG_BUS_EVENT_TYPE = "pullfrog_bus_event";
|
|
147637
|
+
var PULLFROG_OPENCODE_PLUGIN_FILENAME = "pullfrog-events.ts";
|
|
147638
|
+
var PULLFROG_OPENCODE_PLUGIN_SOURCE = `// AUTOGENERATED by Pullfrog. do not edit; it'll be overwritten on the next run.
|
|
147639
|
+
// surfaces opencode subagent activity that the CLI's run-loop discards. see
|
|
147640
|
+
// action/agents/opencodePlugin.ts in pullfrog/app for why this exists. lives
|
|
147641
|
+
// inside the per-run tmpdir (XDG_CONFIG_HOME/opencode/plugin/), never inside
|
|
147642
|
+
// the user's working tree.
|
|
147643
|
+
|
|
147644
|
+
const PULLFROG_BUS_EVENT_TYPE = ${JSON.stringify(PULLFROG_BUS_EVENT_TYPE)};
|
|
147645
|
+
|
|
147646
|
+
// the first sessionID we see on a message.part.updated event is the
|
|
147647
|
+
// orchestrator \u2014 opencode's run command creates exactly one top-level session
|
|
147648
|
+
// before any subagent is dispatched, and the user-prompt text part fires
|
|
147649
|
+
// before the first task tool_use. we lock that sessionID in here and use it
|
|
147650
|
+
// to filter: the orchestrator's events are already streamed by the CLI's
|
|
147651
|
+
// run-loop, so we only forward (a) all subagent events, and (b) the
|
|
147652
|
+
// orchestrator's task tool dispatches at status="running". the CLI only
|
|
147653
|
+
// emits task tool_use at status=completed (after the subagent finishes), so
|
|
147654
|
+
// without the early announce the parent's labeler binds subagent sessions
|
|
147655
|
+
// before recordTaskDispatch fires and the lens label is lost.
|
|
147656
|
+
let orchestratorSessionID: string | undefined;
|
|
147657
|
+
|
|
147658
|
+
function isOrchestratorTaskDispatch(part: {
|
|
147659
|
+
type?: string;
|
|
147660
|
+
tool?: string;
|
|
147661
|
+
state?: { status?: string };
|
|
147662
|
+
}): boolean {
|
|
147663
|
+
if (part.type !== "tool") return false;
|
|
147664
|
+
if (part.tool !== "task") return false;
|
|
147665
|
+
// only forward at status="running" (not "pending"). at pending the
|
|
147666
|
+
// state.input is still {} \u2014 the orchestrator has emitted the part shell
|
|
147667
|
+
// but the LLM hasn't filled in description/subagent_type/prompt yet. by
|
|
147668
|
+
// running, input is populated and recordTaskDispatch can derive the lens
|
|
147669
|
+
// label correctly.
|
|
147670
|
+
return part.state?.status === "running";
|
|
147671
|
+
}
|
|
147672
|
+
|
|
147673
|
+
export default async function pullfrogEventsPlugin() {
|
|
147674
|
+
return {
|
|
147675
|
+
event: async (input: {
|
|
147676
|
+
event: {
|
|
147677
|
+
type: string;
|
|
147678
|
+
properties?: {
|
|
147679
|
+
part?: {
|
|
147680
|
+
sessionID?: string;
|
|
147681
|
+
type?: string;
|
|
147682
|
+
tool?: string;
|
|
147683
|
+
state?: { status?: string };
|
|
147684
|
+
};
|
|
147685
|
+
};
|
|
147686
|
+
};
|
|
147687
|
+
}) => {
|
|
147688
|
+
const event = input.event;
|
|
147689
|
+
if (!event || typeof event !== "object") return;
|
|
147690
|
+
if (event.type !== "message.part.updated") return;
|
|
147691
|
+
const part = event.properties?.part;
|
|
147692
|
+
const sessionID = part?.sessionID;
|
|
147693
|
+
if (typeof sessionID !== "string" || sessionID.length === 0) return;
|
|
147694
|
+
if (orchestratorSessionID === undefined) orchestratorSessionID = sessionID;
|
|
147695
|
+
|
|
147696
|
+
if (sessionID === orchestratorSessionID) {
|
|
147697
|
+
// skip orchestrator events EXCEPT early task dispatches.
|
|
147698
|
+
if (!part || !isOrchestratorTaskDispatch(part)) return;
|
|
147699
|
+
}
|
|
147700
|
+
|
|
147701
|
+
try {
|
|
147702
|
+
const line = JSON.stringify({
|
|
147703
|
+
type: PULLFROG_BUS_EVENT_TYPE,
|
|
147704
|
+
bus_event: event,
|
|
147705
|
+
});
|
|
147706
|
+
process.stdout.write(line + "\\n");
|
|
147707
|
+
} catch {
|
|
147708
|
+
// a circular reference or BigInt etc. would throw; swallow rather
|
|
147709
|
+
// than letting a single bad event take down the plugin.
|
|
147710
|
+
}
|
|
147711
|
+
},
|
|
147712
|
+
};
|
|
147713
|
+
}
|
|
147714
|
+
`;
|
|
147715
|
+
|
|
147716
|
+
// agents/opencode.ts
|
|
147468
147717
|
async function installOpencodeCli() {
|
|
147469
147718
|
return await installFromNpmTarball({
|
|
147470
147719
|
packageName: "opencode-ai",
|
|
@@ -147474,6 +147723,8 @@ async function installOpencodeCli() {
|
|
|
147474
147723
|
});
|
|
147475
147724
|
}
|
|
147476
147725
|
var PULLFROG_OPENCODE_OUTPUT_LIMIT = 5e3;
|
|
147726
|
+
var GEMINI_3_DIRECT_THINKING_LEVEL = "medium";
|
|
147727
|
+
var GEMINI_3_DIRECT_API_IDS = ["gemini-3.1-pro-preview", "gemini-3-flash-preview"];
|
|
147477
147728
|
function buildSecurityConfig(ctx, model) {
|
|
147478
147729
|
const config3 = {
|
|
147479
147730
|
permission: {
|
|
@@ -147487,7 +147738,21 @@ function buildSecurityConfig(ctx, model) {
|
|
|
147487
147738
|
mcp: {
|
|
147488
147739
|
[pullfrogMcpName]: { type: "remote", url: ctx.mcpServerUrl }
|
|
147489
147740
|
},
|
|
147490
|
-
agent: buildReviewerAgentConfig()
|
|
147741
|
+
agent: buildReviewerAgentConfig(),
|
|
147742
|
+
provider: {
|
|
147743
|
+
google: {
|
|
147744
|
+
models: Object.fromEntries(
|
|
147745
|
+
GEMINI_3_DIRECT_API_IDS.map((id) => [
|
|
147746
|
+
id,
|
|
147747
|
+
{
|
|
147748
|
+
options: {
|
|
147749
|
+
thinkingConfig: { thinkingLevel: GEMINI_3_DIRECT_THINKING_LEVEL }
|
|
147750
|
+
}
|
|
147751
|
+
}
|
|
147752
|
+
])
|
|
147753
|
+
)
|
|
147754
|
+
}
|
|
147755
|
+
}
|
|
147491
147756
|
};
|
|
147492
147757
|
if (model) {
|
|
147493
147758
|
config3.model = model;
|
|
@@ -147566,9 +147831,6 @@ async function runOpenCode(params) {
|
|
|
147566
147831
|
const taskDispatchByCallID = /* @__PURE__ */ new Map();
|
|
147567
147832
|
const pendingTaskDispatches = [];
|
|
147568
147833
|
const knownNonTaskCallIDs = /* @__PURE__ */ new Set();
|
|
147569
|
-
function isSubagentInFlight() {
|
|
147570
|
-
return taskDispatchByCallID.size > 0 || pendingTaskDispatches.length > 0;
|
|
147571
|
-
}
|
|
147572
147834
|
function emitSubagentFinished(dispatch, status, output2, matchKind) {
|
|
147573
147835
|
const subagentDuration = performance7.now() - dispatch.startedAt;
|
|
147574
147836
|
const outputStr = typeof output2 === "string" ? output2 : "";
|
|
@@ -147687,18 +147949,20 @@ async function runOpenCode(params) {
|
|
|
147687
147949
|
return;
|
|
147688
147950
|
}
|
|
147689
147951
|
if (toolName === "task") {
|
|
147690
|
-
|
|
147691
|
-
|
|
147692
|
-
|
|
147693
|
-
|
|
147694
|
-
|
|
147695
|
-
|
|
147696
|
-
|
|
147697
|
-
|
|
147698
|
-
|
|
147699
|
-
|
|
147700
|
-
|
|
147701
|
-
|
|
147952
|
+
if (!taskDispatchByCallID.has(toolId)) {
|
|
147953
|
+
const taskInput = event.part?.state?.input ?? {};
|
|
147954
|
+
const dispatchedLabel = labeler.recordTaskDispatch(taskInput);
|
|
147955
|
+
const dispatch = {
|
|
147956
|
+
label: dispatchedLabel,
|
|
147957
|
+
startedAt: performance7.now(),
|
|
147958
|
+
toolUseCallID: toolId
|
|
147959
|
+
};
|
|
147960
|
+
taskDispatchByCallID.set(toolId, dispatch);
|
|
147961
|
+
pendingTaskDispatches.push(dispatch);
|
|
147962
|
+
log.info(
|
|
147963
|
+
`\xBB dispatching subagent: ${dispatchedLabel}` + (taskInput.subagent_type ? ` (subagent_type=${taskInput.subagent_type})` : "")
|
|
147964
|
+
);
|
|
147965
|
+
}
|
|
147702
147966
|
} else {
|
|
147703
147967
|
knownNonTaskCallIDs.add(toolId);
|
|
147704
147968
|
}
|
|
@@ -147719,6 +147983,10 @@ async function runOpenCode(params) {
|
|
|
147719
147983
|
if (event.part?.state?.status === "completed" && event.part.state.output) {
|
|
147720
147984
|
log.debug(withLabel(label, ` output: ${event.part.state.output}`));
|
|
147721
147985
|
}
|
|
147986
|
+
if (event.part?.state?.status === "error") {
|
|
147987
|
+
const errorMsg = event.part.state.output ?? "(no error message)";
|
|
147988
|
+
log.info(withLabel(label, `\xBB tool call failed: ${errorMsg}`));
|
|
147989
|
+
}
|
|
147722
147990
|
if (toolName.includes("report_progress") && params.todoTracker) {
|
|
147723
147991
|
log.debug("\xBB report_progress detected, disabling todo tracking");
|
|
147724
147992
|
params.todoTracker.cancel();
|
|
@@ -147805,6 +148073,53 @@ async function runOpenCode(params) {
|
|
|
147805
148073
|
tokensLogged = true;
|
|
147806
148074
|
}
|
|
147807
148075
|
}
|
|
148076
|
+
},
|
|
148077
|
+
[PULLFROG_BUS_EVENT_TYPE]: async (event) => {
|
|
148078
|
+
const busEvent = event.bus_event;
|
|
148079
|
+
if (!busEvent || busEvent.type !== "message.part.updated") return;
|
|
148080
|
+
const part = busEvent.properties?.part;
|
|
148081
|
+
if (!part || typeof part.sessionID !== "string") return;
|
|
148082
|
+
const sessionID = part.sessionID;
|
|
148083
|
+
const partType = part.type;
|
|
148084
|
+
if (partType === "tool") {
|
|
148085
|
+
const status = part.state?.status;
|
|
148086
|
+
const partWithToolFields = part;
|
|
148087
|
+
const isOrchestratorTaskDispatch = partWithToolFields.tool === "task" && status === "running";
|
|
148088
|
+
if (isOrchestratorTaskDispatch) {
|
|
148089
|
+
const callID = partWithToolFields.callID;
|
|
148090
|
+
if (typeof callID === "string" && !taskDispatchByCallID.has(callID)) {
|
|
148091
|
+
const taskInput = partWithToolFields.state?.input ?? {};
|
|
148092
|
+
const dispatchedLabel = labeler.recordTaskDispatch(taskInput);
|
|
148093
|
+
const dispatch = {
|
|
148094
|
+
label: dispatchedLabel,
|
|
148095
|
+
startedAt: performance7.now(),
|
|
148096
|
+
toolUseCallID: callID
|
|
148097
|
+
};
|
|
148098
|
+
taskDispatchByCallID.set(callID, dispatch);
|
|
148099
|
+
pendingTaskDispatches.push(dispatch);
|
|
148100
|
+
log.info(
|
|
148101
|
+
`\xBB dispatching subagent: ${dispatchedLabel}` + (taskInput.subagent_type ? ` (subagent_type=${taskInput.subagent_type})` : "")
|
|
148102
|
+
);
|
|
148103
|
+
}
|
|
148104
|
+
return;
|
|
148105
|
+
}
|
|
148106
|
+
if (status !== "completed" && status !== "error") return;
|
|
148107
|
+
await handlers2.tool_use({
|
|
148108
|
+
type: "tool_use",
|
|
148109
|
+
sessionID,
|
|
148110
|
+
part
|
|
148111
|
+
});
|
|
148112
|
+
return;
|
|
148113
|
+
}
|
|
148114
|
+
if (partType === "step-start" || partType === "step-finish") return;
|
|
148115
|
+
if (partType === "text" && part.time?.end !== void 0) {
|
|
148116
|
+
await handlers2.text({
|
|
148117
|
+
type: "text",
|
|
148118
|
+
sessionID,
|
|
148119
|
+
part
|
|
148120
|
+
});
|
|
148121
|
+
return;
|
|
148122
|
+
}
|
|
147808
148123
|
}
|
|
147809
148124
|
};
|
|
147810
148125
|
const recentStderr = [];
|
|
@@ -147828,13 +148143,13 @@ async function runOpenCode(params) {
|
|
|
147828
148143
|
// never fires — producing zombie runs. detached + killGroup nukes the
|
|
147829
148144
|
// whole tree.
|
|
147830
148145
|
killGroup: true,
|
|
147831
|
-
//
|
|
147832
|
-
//
|
|
147833
|
-
//
|
|
147834
|
-
//
|
|
147835
|
-
//
|
|
147836
|
-
//
|
|
147837
|
-
|
|
148146
|
+
// NB: we used to pass `isPausedExternally: isSubagentInFlight` to suspend
|
|
148147
|
+
// the activity timer during subagent dispatches. unnecessary now that
|
|
148148
|
+
// our injected plugin (action/agents/opencodePlugin.ts) re-emits
|
|
148149
|
+
// subagent `message.part.updated` events on opencode's stdout — those
|
|
148150
|
+
// arrive at child.stdout here, fire updateActivity(), and reset
|
|
148151
|
+
// lastActivityTime naturally. verified empirically in PR #634
|
|
148152
|
+
// (~3.3 plugin events/sec during a typical subagent run).
|
|
147838
148153
|
onStdout: async (chunk) => {
|
|
147839
148154
|
const text = chunk.toString();
|
|
147840
148155
|
output += text;
|
|
@@ -147989,6 +148304,12 @@ var opencode = agent({
|
|
|
147989
148304
|
XDG_CONFIG_HOME: join11(ctx.tmpdir, ".config")
|
|
147990
148305
|
};
|
|
147991
148306
|
mkdirSync5(join11(homeEnv.XDG_CONFIG_HOME, "opencode"), { recursive: true });
|
|
148307
|
+
const opencodePluginDir = join11(homeEnv.XDG_CONFIG_HOME, "opencode", "plugin");
|
|
148308
|
+
mkdirSync5(opencodePluginDir, { recursive: true });
|
|
148309
|
+
writeFileSync8(
|
|
148310
|
+
join11(opencodePluginDir, PULLFROG_OPENCODE_PLUGIN_FILENAME),
|
|
148311
|
+
PULLFROG_OPENCODE_PLUGIN_SOURCE
|
|
148312
|
+
);
|
|
147992
148313
|
const agentBrowserVersion = getDevDependencyVersion("agent-browser");
|
|
147993
148314
|
addSkill({
|
|
147994
148315
|
ref: `vercel-labs/agent-browser@v${agentBrowserVersion}`,
|
|
@@ -148026,12 +148347,10 @@ var opencode = agent({
|
|
|
148026
148347
|
args: [...baseArgs, ctx.instructions.full]
|
|
148027
148348
|
});
|
|
148028
148349
|
return runPostRunRetryLoop({
|
|
148350
|
+
ctx,
|
|
148029
148351
|
initialResult: result,
|
|
148030
148352
|
initialUsage: result.usage,
|
|
148031
|
-
|
|
148032
|
-
summaryFilePath: ctx.summaryFilePath,
|
|
148033
|
-
summarySeed: ctx.summarySeed,
|
|
148034
|
-
reflectionPrompt: buildLearningsReflectionPrompt("opencode"),
|
|
148353
|
+
reflectionPrompt: ctx.toolState.learningsFilePath ? buildLearningsReflectionPrompt(ctx.toolState.learningsFilePath) : void 0,
|
|
148035
148354
|
resume: async (c) => runOpenCode({
|
|
148036
148355
|
...runParams,
|
|
148037
148356
|
args: [...baseArgs, "--continue", c.prompt]
|
|
@@ -151957,8 +152276,10 @@ var checkRepositoryAccess = async (token, repoOwner, repoName) => {
|
|
|
151957
152276
|
const response = await githubRequest("/installation/repositories", {
|
|
151958
152277
|
headers: { Authorization: `token ${token}` }
|
|
151959
152278
|
});
|
|
152279
|
+
const ownerLower = repoOwner.toLowerCase();
|
|
152280
|
+
const nameLower = repoName.toLowerCase();
|
|
151960
152281
|
return response.repositories.some(
|
|
151961
|
-
(repo) => repo.owner.login ===
|
|
152282
|
+
(repo) => repo.owner.login.toLowerCase() === ownerLower && repo.name.toLowerCase() === nameLower
|
|
151962
152283
|
);
|
|
151963
152284
|
} catch {
|
|
151964
152285
|
return false;
|
|
@@ -152244,7 +152565,7 @@ ${ctx.error}` : ctx.error;
|
|
|
152244
152565
|
|
|
152245
152566
|
// utils/gitAuthServer.ts
|
|
152246
152567
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
152247
|
-
import { writeFileSync as
|
|
152568
|
+
import { writeFileSync as writeFileSync9 } from "node:fs";
|
|
152248
152569
|
import { createServer as createServer2 } from "node:http";
|
|
152249
152570
|
import { join as join13 } from "node:path";
|
|
152250
152571
|
var CODE_TTL_MS = 5 * 60 * 1e3;
|
|
@@ -152333,7 +152654,7 @@ async function startGitAuthServer(tmpdir3) {
|
|
|
152333
152654
|
`try{require("fs").unlinkSync("${scriptPath.replace(/\\/g, "\\\\")}")}catch(e){}`,
|
|
152334
152655
|
`})}).on("error",function(){process.exit(1)})}`
|
|
152335
152656
|
].join("\n");
|
|
152336
|
-
|
|
152657
|
+
writeFileSync9(scriptPath, content, { mode: 448 });
|
|
152337
152658
|
return scriptPath;
|
|
152338
152659
|
}
|
|
152339
152660
|
async function close() {
|
|
@@ -152607,9 +152928,9 @@ function buildPromptContext(ctx) {
|
|
|
152607
152928
|
};
|
|
152608
152929
|
}
|
|
152609
152930
|
function assembleFullPrompt(ctx) {
|
|
152610
|
-
const learningsSection = ctx.
|
|
152931
|
+
const learningsSection = ctx.learningsFilePath ? `************* LEARNINGS *************
|
|
152611
152932
|
|
|
152612
|
-
|
|
152933
|
+
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.` : "";
|
|
152613
152934
|
const runtimeSection = `************* RUNTIME *************
|
|
152614
152935
|
|
|
152615
152936
|
${ctx.runtime}`;
|
|
@@ -152636,8 +152957,8 @@ function resolveInstructions(ctx) {
|
|
|
152636
152957
|
if (eventContext)
|
|
152637
152958
|
tocEntries.push({ label: "EVENT CONTEXT", description: "related PR/issue data" });
|
|
152638
152959
|
tocEntries.push({ label: "SYSTEM", description: "persona, security, tools, workflow rules" });
|
|
152639
|
-
if (pctx.
|
|
152640
|
-
tocEntries.push({ label: "LEARNINGS", description: "repo-specific knowledge" });
|
|
152960
|
+
if (pctx.learningsFilePath)
|
|
152961
|
+
tocEntries.push({ label: "LEARNINGS", description: "repo-specific knowledge file path" });
|
|
152641
152962
|
tocEntries.push({ label: "RUNTIME", description: "environment metadata" });
|
|
152642
152963
|
const toc = buildToc(tocEntries);
|
|
152643
152964
|
const full = assembleFullPrompt({
|
|
@@ -152646,7 +152967,7 @@ function resolveInstructions(ctx) {
|
|
|
152646
152967
|
procedure,
|
|
152647
152968
|
eventContext,
|
|
152648
152969
|
system,
|
|
152649
|
-
|
|
152970
|
+
learningsFilePath: pctx.learningsFilePath,
|
|
152650
152971
|
runtime: pctx.runtime
|
|
152651
152972
|
});
|
|
152652
152973
|
const event = [pctx.eventTitle, pctx.eventMetadata].filter(Boolean).join("\n\n---\n\n");
|
|
@@ -152660,6 +152981,32 @@ function resolveInstructions(ctx) {
|
|
|
152660
152981
|
};
|
|
152661
152982
|
}
|
|
152662
152983
|
|
|
152984
|
+
// utils/learnings.ts
|
|
152985
|
+
import { mkdir, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
152986
|
+
import { dirname as dirname4, join as join14 } from "node:path";
|
|
152987
|
+
var LEARNINGS_FILE_NAME = "pullfrog-learnings.md";
|
|
152988
|
+
var MAX_LEARNINGS_LENGTH = 1e4;
|
|
152989
|
+
function learningsFilePath(tmpdir3) {
|
|
152990
|
+
return join14(tmpdir3, LEARNINGS_FILE_NAME);
|
|
152991
|
+
}
|
|
152992
|
+
async function seedLearningsFile(params) {
|
|
152993
|
+
const path3 = learningsFilePath(params.tmpdir);
|
|
152994
|
+
await mkdir(dirname4(path3), { recursive: true });
|
|
152995
|
+
await writeFile2(path3, params.current ?? "", "utf8");
|
|
152996
|
+
return path3;
|
|
152997
|
+
}
|
|
152998
|
+
async function readLearningsFile(path3) {
|
|
152999
|
+
let raw2;
|
|
153000
|
+
try {
|
|
153001
|
+
raw2 = await readFile2(path3, "utf8");
|
|
153002
|
+
} catch {
|
|
153003
|
+
return null;
|
|
153004
|
+
}
|
|
153005
|
+
const trimmed = raw2.trim();
|
|
153006
|
+
if (trimmed.length > MAX_LEARNINGS_LENGTH) return trimmed.slice(0, MAX_LEARNINGS_LENGTH);
|
|
153007
|
+
return trimmed;
|
|
153008
|
+
}
|
|
153009
|
+
|
|
152663
153010
|
// utils/normalizeEnv.ts
|
|
152664
153011
|
function maskValue(value2) {
|
|
152665
153012
|
if (value2 && typeof value2 === "string" && value2.trim().length > 0) {
|
|
@@ -152835,8 +153182,8 @@ function resolvePayload(resolvedPromptInput, repoSettings) {
|
|
|
152835
153182
|
}
|
|
152836
153183
|
|
|
152837
153184
|
// utils/prSummary.ts
|
|
152838
|
-
import { mkdir, readFile as
|
|
152839
|
-
import { dirname as
|
|
153185
|
+
import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
|
|
153186
|
+
import { dirname as dirname5, join as join15 } from "node:path";
|
|
152840
153187
|
var SUMMARY_FILE_NAME = "pullfrog-summary.md";
|
|
152841
153188
|
var SUMMARY_SCAFFOLD = `# PR summary
|
|
152842
153189
|
|
|
@@ -152846,19 +153193,19 @@ var SUMMARY_SCAFFOLD = `# PR summary
|
|
|
152846
153193
|
var MIN_SNAPSHOT_LENGTH = 60;
|
|
152847
153194
|
var MAX_SNAPSHOT_LENGTH = 32768;
|
|
152848
153195
|
function summaryFilePath(tmpdir3) {
|
|
152849
|
-
return
|
|
153196
|
+
return join15(tmpdir3, SUMMARY_FILE_NAME);
|
|
152850
153197
|
}
|
|
152851
153198
|
async function seedSummaryFile(params) {
|
|
152852
153199
|
const path3 = summaryFilePath(params.tmpdir);
|
|
152853
|
-
await
|
|
153200
|
+
await mkdir2(dirname5(path3), { recursive: true });
|
|
152854
153201
|
const seed = params.previousSnapshot && params.previousSnapshot.trim().length >= MIN_SNAPSHOT_LENGTH ? params.previousSnapshot : SUMMARY_SCAFFOLD;
|
|
152855
|
-
await
|
|
153202
|
+
await writeFile3(path3, seed, "utf8");
|
|
152856
153203
|
return path3;
|
|
152857
153204
|
}
|
|
152858
153205
|
async function readSummaryFile(path3) {
|
|
152859
153206
|
let raw2;
|
|
152860
153207
|
try {
|
|
152861
|
-
raw2 = await
|
|
153208
|
+
raw2 = await readFile3(path3, "utf8");
|
|
152862
153209
|
} catch {
|
|
152863
153210
|
return null;
|
|
152864
153211
|
}
|
|
@@ -153076,9 +153423,9 @@ async function resolveRunContextData(params) {
|
|
|
153076
153423
|
import { execFileSync as execFileSync5, execSync as execSync3 } from "node:child_process";
|
|
153077
153424
|
import { mkdtempSync } from "node:fs";
|
|
153078
153425
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
153079
|
-
import { join as
|
|
153426
|
+
import { join as join16 } from "node:path";
|
|
153080
153427
|
function createTempDirectory() {
|
|
153081
|
-
const sharedTempDir = mkdtempSync(
|
|
153428
|
+
const sharedTempDir = mkdtempSync(join16(tmpdir2(), "pullfrog-"));
|
|
153082
153429
|
process.env.PULLFROG_TEMP_DIR = sharedTempDir;
|
|
153083
153430
|
log.info(`\xBB created temp dir at ${sharedTempDir}`);
|
|
153084
153431
|
return sharedTempDir;
|
|
@@ -153480,15 +153827,12 @@ function formatTransientErrorSummary(error49, owner) {
|
|
|
153480
153827
|
}
|
|
153481
153828
|
async function mintProxyKey(ctx) {
|
|
153482
153829
|
try {
|
|
153483
|
-
|
|
153484
|
-
|
|
153485
|
-
const oidcToken = await core6.getIDToken("pullfrog-api");
|
|
153486
|
-
delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
153487
|
-
delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
153830
|
+
const headers = await buildProxyTokenHeaders(ctx);
|
|
153831
|
+
if (!headers) return null;
|
|
153488
153832
|
const response = await apiFetch({
|
|
153489
153833
|
path: "/api/proxy-token",
|
|
153490
153834
|
method: "POST",
|
|
153491
|
-
headers
|
|
153835
|
+
headers
|
|
153492
153836
|
});
|
|
153493
153837
|
if (response.status === 402) {
|
|
153494
153838
|
const body = await response.json().catch(() => null);
|
|
@@ -153520,15 +153864,30 @@ async function mintProxyKey(ctx) {
|
|
|
153520
153864
|
delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
153521
153865
|
}
|
|
153522
153866
|
}
|
|
153867
|
+
async function buildProxyTokenHeaders(ctx) {
|
|
153868
|
+
if (ctx.oidcCredentials) {
|
|
153869
|
+
process.env.ACTIONS_ID_TOKEN_REQUEST_URL = ctx.oidcCredentials.requestUrl;
|
|
153870
|
+
process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = ctx.oidcCredentials.requestToken;
|
|
153871
|
+
const oidcToken = await core6.getIDToken("pullfrog-api");
|
|
153872
|
+
delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
153873
|
+
delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
153874
|
+
return { Authorization: `Bearer ${oidcToken}` };
|
|
153875
|
+
}
|
|
153876
|
+
if (isLocalApiUrl()) {
|
|
153877
|
+
log.info(`\xBB proxy: dev bypass (x-dev-repo) for ${ctx.repo.owner}/${ctx.repo.name}`);
|
|
153878
|
+
return { "x-dev-repo": `${ctx.repo.owner}/${ctx.repo.name}` };
|
|
153879
|
+
}
|
|
153880
|
+
return null;
|
|
153881
|
+
}
|
|
153523
153882
|
async function resolveProxyModel(ctx) {
|
|
153524
153883
|
if (process.env.PULLFROG_MODEL?.trim()) return;
|
|
153525
153884
|
const needsProxy = isInfraCovered({ isOss: ctx.oss, plan: ctx.plan }) && ctx.proxyModel;
|
|
153526
153885
|
if (!needsProxy) return;
|
|
153527
|
-
if (!ctx.oidcCredentials) {
|
|
153886
|
+
if (!ctx.oidcCredentials && !isLocalApiUrl()) {
|
|
153528
153887
|
log.warning("\xBB proxy requested but no OIDC credentials available \u2014 skipping");
|
|
153529
153888
|
return;
|
|
153530
153889
|
}
|
|
153531
|
-
const key = await mintProxyKey({ oidcCredentials: ctx.oidcCredentials });
|
|
153890
|
+
const key = await mintProxyKey({ oidcCredentials: ctx.oidcCredentials, repo: ctx.repo });
|
|
153532
153891
|
if (!key) return;
|
|
153533
153892
|
process.env.OPENROUTER_API_KEY = key;
|
|
153534
153893
|
core6.setSecret(key);
|
|
@@ -153552,6 +153911,45 @@ async function fetchPreviousSnapshot(ctx, prNumber) {
|
|
|
153552
153911
|
return null;
|
|
153553
153912
|
}
|
|
153554
153913
|
}
|
|
153914
|
+
async function persistLearnings(ctx) {
|
|
153915
|
+
const filePath = ctx.toolState.learningsFilePath;
|
|
153916
|
+
if (!filePath) return;
|
|
153917
|
+
if (ctx.toolState.learningsPersistAttempted) return;
|
|
153918
|
+
ctx.toolState.learningsPersistAttempted = true;
|
|
153919
|
+
const current = await readLearningsFile(filePath);
|
|
153920
|
+
if (current === null) {
|
|
153921
|
+
log.debug(`learnings tmpfile missing or unreadable at ${filePath} \u2014 skipping persist`);
|
|
153922
|
+
return;
|
|
153923
|
+
}
|
|
153924
|
+
const seed = ctx.toolState.learningsSeed?.trim() ?? "";
|
|
153925
|
+
if (current === seed) {
|
|
153926
|
+
log.debug("learnings tmpfile unchanged from seed \u2014 skipping persist");
|
|
153927
|
+
return;
|
|
153928
|
+
}
|
|
153929
|
+
try {
|
|
153930
|
+
const response = await apiFetch({
|
|
153931
|
+
path: `/api/repo/${ctx.repo.owner}/${ctx.repo.name}/learnings`,
|
|
153932
|
+
method: "PATCH",
|
|
153933
|
+
headers: {
|
|
153934
|
+
authorization: `Bearer ${ctx.apiToken}`,
|
|
153935
|
+
"content-type": "application/json"
|
|
153936
|
+
},
|
|
153937
|
+
body: JSON.stringify({
|
|
153938
|
+
learnings: current,
|
|
153939
|
+
model: ctx.toolState.model
|
|
153940
|
+
}),
|
|
153941
|
+
signal: AbortSignal.timeout(1e4)
|
|
153942
|
+
});
|
|
153943
|
+
if (!response.ok) {
|
|
153944
|
+
const error49 = await response.text().catch(() => "(no body)");
|
|
153945
|
+
log.debug(`learnings persist failed (${response.status}): ${error49}`);
|
|
153946
|
+
return;
|
|
153947
|
+
}
|
|
153948
|
+
log.info("\xBB learnings updated");
|
|
153949
|
+
} catch (err) {
|
|
153950
|
+
log.debug(`learnings persist failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
153951
|
+
}
|
|
153952
|
+
}
|
|
153555
153953
|
async function persistSummary(ctx) {
|
|
153556
153954
|
const filePath = ctx.toolState.summaryFilePath;
|
|
153557
153955
|
if (!filePath) return;
|
|
@@ -153573,9 +153971,10 @@ async function persistSummary(ctx) {
|
|
|
153573
153971
|
log.debug(`pr summary persist failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
153574
153972
|
});
|
|
153575
153973
|
}
|
|
153576
|
-
async function writeJobSummary(toolState) {
|
|
153974
|
+
async function writeJobSummary(toolState, finalOutput) {
|
|
153577
153975
|
const usageSummary = formatUsageSummary(toolState.usageEntries);
|
|
153578
|
-
const
|
|
153976
|
+
const body = toolState.lastProgressBody || finalOutput;
|
|
153977
|
+
const summaryParts = [body, usageSummary].filter(Boolean);
|
|
153579
153978
|
if (summaryParts.length > 0) {
|
|
153580
153979
|
await writeSummary(summaryParts.join("\n\n"));
|
|
153581
153980
|
}
|
|
@@ -153633,7 +154032,8 @@ async function main() {
|
|
|
153633
154032
|
oss: runContext.oss,
|
|
153634
154033
|
plan: runContext.plan,
|
|
153635
154034
|
proxyModel: runContext.proxyModel,
|
|
153636
|
-
oidcCredentials
|
|
154035
|
+
oidcCredentials,
|
|
154036
|
+
repo: runContext.repo
|
|
153637
154037
|
});
|
|
153638
154038
|
} catch (error49) {
|
|
153639
154039
|
if (error49 instanceof BillingError) {
|
|
@@ -153736,12 +154136,32 @@ async function main() {
|
|
|
153736
154136
|
toolContext.mcpServerUrl = mcpHttpServer.url;
|
|
153737
154137
|
log.info(`\xBB MCP server started at ${mcpHttpServer.url}`);
|
|
153738
154138
|
timer.checkpoint("mcpServer");
|
|
154139
|
+
try {
|
|
154140
|
+
const learningsPath = await seedLearningsFile({
|
|
154141
|
+
tmpdir: tmpdir3,
|
|
154142
|
+
current: runContext.repoSettings.learnings
|
|
154143
|
+
});
|
|
154144
|
+
toolState.learningsFilePath = learningsPath;
|
|
154145
|
+
try {
|
|
154146
|
+
toolState.learningsSeed = await readFile4(learningsPath, "utf8");
|
|
154147
|
+
} catch {
|
|
154148
|
+
}
|
|
154149
|
+
log.info(
|
|
154150
|
+
`\xBB learnings seeded at ${learningsPath} (existing=${runContext.repoSettings.learnings ? "yes" : "no"})`
|
|
154151
|
+
);
|
|
154152
|
+
const ctxForExit = toolContext;
|
|
154153
|
+
onExitSignal(() => persistLearnings(ctxForExit));
|
|
154154
|
+
} catch (err) {
|
|
154155
|
+
log.warning(
|
|
154156
|
+
`\xBB learnings seed failed: ${err instanceof Error ? err.message : String(err)} \u2014 continuing without learnings file`
|
|
154157
|
+
);
|
|
154158
|
+
}
|
|
153739
154159
|
if (payload.generateSummary && payload.event.is_pr && payload.event.issue_number) {
|
|
153740
154160
|
const previousSnapshot = await fetchPreviousSnapshot(toolContext, payload.event.issue_number);
|
|
153741
154161
|
const filePath = await seedSummaryFile({ tmpdir: tmpdir3, previousSnapshot });
|
|
153742
154162
|
toolState.summaryFilePath = filePath;
|
|
153743
154163
|
try {
|
|
153744
|
-
toolState.summarySeed = await
|
|
154164
|
+
toolState.summarySeed = await readFile4(filePath, "utf8");
|
|
153745
154165
|
} catch {
|
|
153746
154166
|
}
|
|
153747
154167
|
log.info(
|
|
@@ -153765,7 +154185,7 @@ async function main() {
|
|
|
153765
154185
|
modes: modes2,
|
|
153766
154186
|
agentId,
|
|
153767
154187
|
outputSchema,
|
|
153768
|
-
|
|
154188
|
+
learningsFilePath: toolState.learningsFilePath ?? null
|
|
153769
154189
|
});
|
|
153770
154190
|
const logParts = [
|
|
153771
154191
|
instructions.eventInstructions ? `EVENT-LEVEL INSTRUCTIONS:
|
|
@@ -153781,7 +154201,7 @@ ${instructions.user}` : null,
|
|
|
153781
154201
|
log.info(instructions.full);
|
|
153782
154202
|
});
|
|
153783
154203
|
if (agentId === "opencode") {
|
|
153784
|
-
const pluginDir =
|
|
154204
|
+
const pluginDir = join17(process.cwd(), ".opencode", "plugin");
|
|
153785
154205
|
const hasPlugins = existsSync7(pluginDir) && readdirSync(pluginDir).some((f) => /\.[jt]sx?$/.test(f));
|
|
153786
154206
|
if (hasPlugins && toolState.dependencyInstallation?.promise) {
|
|
153787
154207
|
log.info(
|
|
@@ -153840,8 +154260,7 @@ ${instructions.user}` : null,
|
|
|
153840
154260
|
instructions,
|
|
153841
154261
|
todoTracker,
|
|
153842
154262
|
stopScript: runContext.repoSettings.stopScript,
|
|
153843
|
-
|
|
153844
|
-
summarySeed: toolState.summarySeed,
|
|
154263
|
+
toolState,
|
|
153845
154264
|
onActivityTimeout: onInnerActivityTimeout,
|
|
153846
154265
|
onToolUse: (event) => {
|
|
153847
154266
|
const wasTracked = recordDiffReadFromToolUse({
|
|
@@ -153899,12 +154318,27 @@ ${instructions.user}` : null,
|
|
|
153899
154318
|
if (toolContext) {
|
|
153900
154319
|
await persistSummary(toolContext);
|
|
153901
154320
|
}
|
|
153902
|
-
if (toolContext
|
|
154321
|
+
if (toolContext) {
|
|
154322
|
+
await persistLearnings(toolContext);
|
|
154323
|
+
}
|
|
154324
|
+
if (!result.success && toolContext && toolState.progressComment) {
|
|
154325
|
+
await reportErrorToComment({
|
|
154326
|
+
toolState,
|
|
154327
|
+
error: result.error || "agent run failed"
|
|
154328
|
+
}).catch((error49) => {
|
|
154329
|
+
log.debug(`failure error report failed: ${error49}`);
|
|
154330
|
+
});
|
|
154331
|
+
}
|
|
154332
|
+
if (toolContext && result.success && toolState.progressComment && !toolState.finalSummaryWritten) {
|
|
153903
154333
|
await deleteProgressComment(toolContext).catch((error49) => {
|
|
153904
154334
|
log.debug(`stranded progress comment cleanup failed: ${error49}`);
|
|
153905
154335
|
});
|
|
153906
154336
|
}
|
|
153907
|
-
|
|
154337
|
+
try {
|
|
154338
|
+
await writeJobSummary(toolState, result.output);
|
|
154339
|
+
} catch (error49) {
|
|
154340
|
+
log.debug(`job summary write failed: ${error49}`);
|
|
154341
|
+
}
|
|
153908
154342
|
if (toolState.output) {
|
|
153909
154343
|
log.info(`::pullfrog-output::${Buffer.from(toolState.output).toString("base64")}`);
|
|
153910
154344
|
core6.setOutput("result", toolState.output);
|
|
@@ -153951,6 +154385,9 @@ ${errorMessage}
|
|
|
153951
154385
|
if (toolContext) {
|
|
153952
154386
|
await persistSummary(toolContext);
|
|
153953
154387
|
}
|
|
154388
|
+
if (toolContext) {
|
|
154389
|
+
await persistLearnings(toolContext);
|
|
154390
|
+
}
|
|
153954
154391
|
return {
|
|
153955
154392
|
success: false,
|
|
153956
154393
|
error: errorMessage
|