pullfrog 0.1.0 → 0.1.2
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/opencodePlugin.d.ts +60 -0
- package/dist/agents/postRun.d.ts +12 -8
- package/dist/agents/shared.d.ts +7 -0
- package/dist/cli.mjs +396 -129
- package/dist/index.js +393 -126
- package/dist/internal.js +4 -17
- package/dist/mcp/server.d.ts +3 -0
- package/dist/utils/apiUrl.d.ts +8 -0
- package/dist/utils/instructions.d.ts +4 -1
- package/dist/utils/learnings.d.ts +31 -0
- package/dist/utils/subprocess.d.ts +1 -0
- package/package.json +1 -1
- package/dist/mcp/learnings.d.ts +0 -6
package/dist/cli.mjs
CHANGED
|
@@ -18415,7 +18415,7 @@ var require_summary = __commonJS({
|
|
|
18415
18415
|
exports.summary = exports.markdownSummary = exports.SUMMARY_DOCS_URL = exports.SUMMARY_ENV_VAR = void 0;
|
|
18416
18416
|
var os_1 = __require("os");
|
|
18417
18417
|
var fs_1 = __require("fs");
|
|
18418
|
-
var { access, appendFile, writeFile:
|
|
18418
|
+
var { access, appendFile, writeFile: writeFile4 } = fs_1.promises;
|
|
18419
18419
|
exports.SUMMARY_ENV_VAR = "GITHUB_STEP_SUMMARY";
|
|
18420
18420
|
exports.SUMMARY_DOCS_URL = "https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary";
|
|
18421
18421
|
var Summary = class {
|
|
@@ -18473,7 +18473,7 @@ var require_summary = __commonJS({
|
|
|
18473
18473
|
return __awaiter(this, void 0, void 0, function* () {
|
|
18474
18474
|
const overwrite = !!(options === null || options === void 0 ? void 0 : options.overwrite);
|
|
18475
18475
|
const filePath = yield this.filePath();
|
|
18476
|
-
const writeFunc = overwrite ?
|
|
18476
|
+
const writeFunc = overwrite ? writeFile4 : appendFile;
|
|
18477
18477
|
yield writeFunc(filePath, this._buffer, { encoding: "utf8" });
|
|
18478
18478
|
return this.emptyBuffer();
|
|
18479
18479
|
});
|
|
@@ -62879,8 +62879,8 @@ var require_snapshot_utils = __commonJS({
|
|
|
62879
62879
|
var require_snapshot_recorder = __commonJS({
|
|
62880
62880
|
"node_modules/.pnpm/undici@7.22.0/node_modules/undici/lib/mock/snapshot-recorder.js"(exports, module) {
|
|
62881
62881
|
"use strict";
|
|
62882
|
-
var { writeFile:
|
|
62883
|
-
var { dirname:
|
|
62882
|
+
var { writeFile: writeFile4, readFile: readFile5, mkdir: mkdir3 } = __require("node:fs/promises");
|
|
62883
|
+
var { dirname: dirname7, resolve: resolve3 } = __require("node:path");
|
|
62884
62884
|
var { setTimeout: setTimeout2, clearTimeout: clearTimeout2 } = __require("node:timers");
|
|
62885
62885
|
var { InvalidArgumentError, UndiciError } = require_errors4();
|
|
62886
62886
|
var { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = require_snapshot_utils();
|
|
@@ -63081,7 +63081,7 @@ var require_snapshot_recorder = __commonJS({
|
|
|
63081
63081
|
throw new InvalidArgumentError("Snapshot path is required");
|
|
63082
63082
|
}
|
|
63083
63083
|
try {
|
|
63084
|
-
const data = await
|
|
63084
|
+
const data = await readFile5(resolve3(path3), "utf8");
|
|
63085
63085
|
const parsed2 = JSON.parse(data);
|
|
63086
63086
|
if (Array.isArray(parsed2)) {
|
|
63087
63087
|
this.#snapshots.clear();
|
|
@@ -63111,12 +63111,12 @@ var require_snapshot_recorder = __commonJS({
|
|
|
63111
63111
|
throw new InvalidArgumentError("Snapshot path is required");
|
|
63112
63112
|
}
|
|
63113
63113
|
const resolvedPath = resolve3(path3);
|
|
63114
|
-
await
|
|
63114
|
+
await mkdir3(dirname7(resolvedPath), { recursive: true });
|
|
63115
63115
|
const data = Array.from(this.#snapshots.entries()).map(([hash2, snapshot2]) => ({
|
|
63116
63116
|
hash: hash2,
|
|
63117
63117
|
snapshot: snapshot2
|
|
63118
63118
|
}));
|
|
63119
|
-
await
|
|
63119
|
+
await writeFile4(resolvedPath, JSON.stringify(data, null, 2), { flush: true });
|
|
63120
63120
|
}
|
|
63121
63121
|
/**
|
|
63122
63122
|
* Clears all recorded snapshots
|
|
@@ -97692,14 +97692,14 @@ var require_turndown_cjs = __commonJS({
|
|
|
97692
97692
|
} else if (node2.nodeType === 1) {
|
|
97693
97693
|
replacement = replacementForNode.call(self2, node2);
|
|
97694
97694
|
}
|
|
97695
|
-
return
|
|
97695
|
+
return join18(output, replacement);
|
|
97696
97696
|
}, "");
|
|
97697
97697
|
}
|
|
97698
97698
|
function postProcess(output) {
|
|
97699
97699
|
var self2 = this;
|
|
97700
97700
|
this.rules.forEach(function(rule) {
|
|
97701
97701
|
if (typeof rule.append === "function") {
|
|
97702
|
-
output =
|
|
97702
|
+
output = join18(output, rule.append(self2.options));
|
|
97703
97703
|
}
|
|
97704
97704
|
});
|
|
97705
97705
|
return output.replace(/^[\t\r\n]+/, "").replace(/[\t\r\n\s]+$/, "");
|
|
@@ -97711,7 +97711,7 @@ var require_turndown_cjs = __commonJS({
|
|
|
97711
97711
|
if (whitespace.leading || whitespace.trailing) content = content.trim();
|
|
97712
97712
|
return whitespace.leading + rule.replacement(content, node2, this.options) + whitespace.trailing;
|
|
97713
97713
|
}
|
|
97714
|
-
function
|
|
97714
|
+
function join18(output, replacement) {
|
|
97715
97715
|
var s1 = trimTrailingNewlines(output);
|
|
97716
97716
|
var s2 = trimLeadingNewlines(replacement);
|
|
97717
97717
|
var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
|
|
@@ -99204,13 +99204,13 @@ import { basename as basename2 } from "node:path";
|
|
|
99204
99204
|
// commands/gha.ts
|
|
99205
99205
|
var core7 = __toESM(require_core(), 1);
|
|
99206
99206
|
var import_arg = __toESM(require_arg(), 1);
|
|
99207
|
-
import { dirname as
|
|
99207
|
+
import { dirname as dirname6 } from "node:path";
|
|
99208
99208
|
|
|
99209
99209
|
// main.ts
|
|
99210
99210
|
var core6 = __toESM(require_core(), 1);
|
|
99211
99211
|
import { existsSync as existsSync7, readdirSync } from "node:fs";
|
|
99212
|
-
import { readFile as
|
|
99213
|
-
import { join as
|
|
99212
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
99213
|
+
import { join as join17 } from "node:path";
|
|
99214
99214
|
|
|
99215
99215
|
// node_modules/.pnpm/@ark+util@0.56.0/node_modules/@ark/util/out/arrays.js
|
|
99216
99216
|
var liftArray = (data) => Array.isArray(data) ? data : [data];
|
|
@@ -108006,6 +108006,13 @@ function getApiUrl() {
|
|
|
108006
108006
|
log.debug(`resolved API_URL: ${raw2}`);
|
|
108007
108007
|
return raw2;
|
|
108008
108008
|
}
|
|
108009
|
+
function isLocalApiUrl() {
|
|
108010
|
+
try {
|
|
108011
|
+
return isLocalUrl(new URL(getApiUrl()));
|
|
108012
|
+
} catch {
|
|
108013
|
+
return false;
|
|
108014
|
+
}
|
|
108015
|
+
}
|
|
108009
108016
|
|
|
108010
108017
|
// models.ts
|
|
108011
108018
|
function provider(config3) {
|
|
@@ -109244,6 +109251,7 @@ function CreateCommentTool(ctx) {
|
|
|
109244
109251
|
body: bodyWithFooter
|
|
109245
109252
|
});
|
|
109246
109253
|
ctx.toolState.wasUpdated = true;
|
|
109254
|
+
log.info(`\xBB created comment ${result.data.id}`);
|
|
109247
109255
|
if (commentType === "Plan") {
|
|
109248
109256
|
if (result.data.node_id) {
|
|
109249
109257
|
await patchWorkflowRunFields(ctx, { planCommentNodeId: result.data.node_id });
|
|
@@ -109257,6 +109265,7 @@ function CreateCommentTool(ctx) {
|
|
|
109257
109265
|
comment_id: result.data.id,
|
|
109258
109266
|
body: bodyWithPlanLink
|
|
109259
109267
|
});
|
|
109268
|
+
log.info(`\xBB updated comment ${updateResult.data.id}`);
|
|
109260
109269
|
return {
|
|
109261
109270
|
success: true,
|
|
109262
109271
|
commentId: updateResult.data.id,
|
|
@@ -109290,6 +109299,7 @@ function EditCommentTool(ctx) {
|
|
|
109290
109299
|
comment_id: commentId,
|
|
109291
109300
|
body: bodyWithFooter
|
|
109292
109301
|
});
|
|
109302
|
+
log.info(`\xBB updated comment ${result.data.id}`);
|
|
109293
109303
|
return {
|
|
109294
109304
|
success: true,
|
|
109295
109305
|
commentId: result.data.id,
|
|
@@ -109425,6 +109435,9 @@ ${collapsible}`;
|
|
|
109425
109435
|
message: "progress recorded (no GitHub comment created - this may occur for workflow_dispatch events or when there is no associated issue/PR)"
|
|
109426
109436
|
};
|
|
109427
109437
|
}
|
|
109438
|
+
if (result.commentId !== void 0) {
|
|
109439
|
+
log.info(`\xBB ${result.action} comment ${result.commentId}`);
|
|
109440
|
+
}
|
|
109428
109441
|
if (!params.target_plan_comment) {
|
|
109429
109442
|
ctx.toolState.finalSummaryWritten = true;
|
|
109430
109443
|
}
|
|
@@ -109475,6 +109488,7 @@ function ReplyToReviewCommentTool(ctx) {
|
|
|
109475
109488
|
comment_id,
|
|
109476
109489
|
body: bodyWithFooter
|
|
109477
109490
|
});
|
|
109491
|
+
log.info(`\xBB created review comment ${result.data.id} (in reply to ${comment_id})`);
|
|
109478
109492
|
ctx.toolState.wasUpdated = true;
|
|
109479
109493
|
return {
|
|
109480
109494
|
success: true,
|
|
@@ -109979,6 +109993,7 @@ async function spawn(options) {
|
|
|
109979
109993
|
const startTime = performance3.now();
|
|
109980
109994
|
let stdoutBuffer = "";
|
|
109981
109995
|
let stderrBuffer = "";
|
|
109996
|
+
const killGroup = options.killGroup ?? false;
|
|
109982
109997
|
return new Promise((resolve3, reject) => {
|
|
109983
109998
|
const child = nodeSpawn(options.cmd, options.args, {
|
|
109984
109999
|
env: options.env || {
|
|
@@ -109986,9 +110001,20 @@ async function spawn(options) {
|
|
|
109986
110001
|
HOME: process.env.HOME || ""
|
|
109987
110002
|
},
|
|
109988
110003
|
stdio: options.stdio || ["pipe", "pipe", "pipe"],
|
|
109989
|
-
cwd: options.cwd || process.cwd()
|
|
110004
|
+
cwd: options.cwd || process.cwd(),
|
|
110005
|
+
detached: killGroup
|
|
109990
110006
|
});
|
|
109991
|
-
|
|
110007
|
+
const killSelf = (signal) => {
|
|
110008
|
+
if (killGroup && child.pid) {
|
|
110009
|
+
try {
|
|
110010
|
+
process.kill(-child.pid, signal);
|
|
110011
|
+
return;
|
|
110012
|
+
} catch {
|
|
110013
|
+
}
|
|
110014
|
+
}
|
|
110015
|
+
child.kill(signal);
|
|
110016
|
+
};
|
|
110017
|
+
trackChild({ child, killGroup });
|
|
109992
110018
|
let timeoutId;
|
|
109993
110019
|
let sigkillEscalatorId;
|
|
109994
110020
|
let activityCheckIntervalId;
|
|
@@ -109999,10 +110025,10 @@ async function spawn(options) {
|
|
|
109999
110025
|
if (options.timeout) {
|
|
110000
110026
|
timeoutId = setTimeout(() => {
|
|
110001
110027
|
isTimedOut = true;
|
|
110002
|
-
|
|
110028
|
+
killSelf("SIGTERM");
|
|
110003
110029
|
sigkillEscalatorId = setTimeout(() => {
|
|
110004
110030
|
if (!child.killed) {
|
|
110005
|
-
|
|
110031
|
+
killSelf("SIGKILL");
|
|
110006
110032
|
}
|
|
110007
110033
|
}, 5e3);
|
|
110008
110034
|
}, options.timeout);
|
|
@@ -110021,9 +110047,9 @@ async function spawn(options) {
|
|
|
110021
110047
|
killedAtIdleMs = idleMs;
|
|
110022
110048
|
const idleSec = Math.round(idleMs / 1e3);
|
|
110023
110049
|
log.info(
|
|
110024
|
-
`no output for ${idleSec}s from pid=${child.pid} (${options.cmd}), killing process`
|
|
110050
|
+
`no output for ${idleSec}s from pid=${child.pid} (${options.cmd}), killing process${killGroup ? " group" : ""}`
|
|
110025
110051
|
);
|
|
110026
|
-
|
|
110052
|
+
killSelf("SIGKILL");
|
|
110027
110053
|
clearInterval(activityCheckIntervalId);
|
|
110028
110054
|
try {
|
|
110029
110055
|
options.onActivityTimeout?.();
|
|
@@ -142532,7 +142558,7 @@ var import_semver = __toESM(require_semver2(), 1);
|
|
|
142532
142558
|
// package.json
|
|
142533
142559
|
var package_default = {
|
|
142534
142560
|
name: "pullfrog",
|
|
142535
|
-
version: "0.1.
|
|
142561
|
+
version: "0.1.2",
|
|
142536
142562
|
type: "module",
|
|
142537
142563
|
bin: {
|
|
142538
142564
|
pullfrog: "dist/cli.mjs",
|
|
@@ -143476,6 +143502,10 @@ ${integrateStep}
|
|
|
143476
143502
|
if (!pushed) {
|
|
143477
143503
|
throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
|
|
143478
143504
|
}
|
|
143505
|
+
const pushedSha = $("git", ["rev-parse", "HEAD"], { log: false }).trim();
|
|
143506
|
+
log.info(
|
|
143507
|
+
`\xBB pushed branch ${branch} to ${pushDest.remoteName}/${pushDest.remoteBranch} (sha ${pushedSha})`
|
|
143508
|
+
);
|
|
143479
143509
|
return {
|
|
143480
143510
|
success: true,
|
|
143481
143511
|
branch,
|
|
@@ -143624,6 +143654,7 @@ function DeleteBranchTool(ctx) {
|
|
|
143624
143654
|
await $git("push", ["origin", "--delete", `refs/heads/${params.branchName}`], {
|
|
143625
143655
|
token: ctx.gitToken
|
|
143626
143656
|
});
|
|
143657
|
+
log.info(`\xBB deleted branch ${params.branchName}`);
|
|
143627
143658
|
return { success: true, deleted: params.branchName };
|
|
143628
143659
|
})
|
|
143629
143660
|
});
|
|
@@ -143649,6 +143680,7 @@ function PushTagsTool(ctx) {
|
|
|
143649
143680
|
await $git("push", pushArgs, {
|
|
143650
143681
|
token: ctx.gitToken
|
|
143651
143682
|
});
|
|
143683
|
+
log.info(`\xBB pushed tag ${params.tag}`);
|
|
143652
143684
|
return { success: true, tag: params.tag };
|
|
143653
143685
|
})
|
|
143654
143686
|
});
|
|
@@ -143973,6 +144005,7 @@ function CreatePullRequestReviewTool(ctx) {
|
|
|
143973
144005
|
}
|
|
143974
144006
|
const reviewId = result.data.id;
|
|
143975
144007
|
const reviewNodeId = result.data.node_id;
|
|
144008
|
+
log.info(`\xBB created review ${reviewId} on pull request #${pull_number}`);
|
|
143976
144009
|
const actuallyReviewedSha = ctx.toolState.checkoutSha ?? params.commit_id;
|
|
143977
144010
|
ctx.toolState.review = {
|
|
143978
144011
|
id: reviewId,
|
|
@@ -144837,6 +144870,7 @@ function IssueTool(ctx) {
|
|
|
144837
144870
|
labels: params.labels ?? [],
|
|
144838
144871
|
assignees: params.assignees ?? []
|
|
144839
144872
|
});
|
|
144873
|
+
log.info(`\xBB created issue #${result.data.number} (id ${result.data.id})`);
|
|
144840
144874
|
const nodeId = result.data.node_id;
|
|
144841
144875
|
if (typeof nodeId === "string" && nodeId.length > 0) {
|
|
144842
144876
|
await patchWorkflowRunFields(ctx, {
|
|
@@ -145028,6 +145062,7 @@ function AddLabelsTool(ctx) {
|
|
|
145028
145062
|
issue_number,
|
|
145029
145063
|
labels
|
|
145030
145064
|
});
|
|
145065
|
+
log.info(`\xBB added labels [${labels.join(", ")}] to issue #${issue_number}`);
|
|
145031
145066
|
return {
|
|
145032
145067
|
success: true,
|
|
145033
145068
|
labels: result.data.map((label) => label.name)
|
|
@@ -145036,40 +145071,6 @@ function AddLabelsTool(ctx) {
|
|
|
145036
145071
|
});
|
|
145037
145072
|
}
|
|
145038
145073
|
|
|
145039
|
-
// mcp/learnings.ts
|
|
145040
|
-
var UpdateLearningsParams = type({
|
|
145041
|
-
learnings: type.string.describe(
|
|
145042
|
-
"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."
|
|
145043
|
-
)
|
|
145044
|
-
});
|
|
145045
|
-
function UpdateLearningsTool(ctx) {
|
|
145046
|
-
return tool({
|
|
145047
|
-
name: "update_learnings",
|
|
145048
|
-
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.",
|
|
145049
|
-
parameters: UpdateLearningsParams,
|
|
145050
|
-
execute: execute(async (params) => {
|
|
145051
|
-
const response = await apiFetch({
|
|
145052
|
-
path: `/api/repo/${ctx.repo.owner}/${ctx.repo.name}/learnings`,
|
|
145053
|
-
method: "PATCH",
|
|
145054
|
-
headers: {
|
|
145055
|
-
authorization: `Bearer ${ctx.apiToken}`,
|
|
145056
|
-
"content-type": "application/json"
|
|
145057
|
-
},
|
|
145058
|
-
body: JSON.stringify({
|
|
145059
|
-
learnings: params.learnings,
|
|
145060
|
-
model: ctx.toolState.model
|
|
145061
|
-
}),
|
|
145062
|
-
signal: AbortSignal.timeout(1e4)
|
|
145063
|
-
});
|
|
145064
|
-
if (!response.ok) {
|
|
145065
|
-
const error49 = await response.text();
|
|
145066
|
-
throw new Error(`failed to update learnings: ${error49}`);
|
|
145067
|
-
}
|
|
145068
|
-
return { success: true };
|
|
145069
|
-
})
|
|
145070
|
-
});
|
|
145071
|
-
}
|
|
145072
|
-
|
|
145073
145074
|
// mcp/output.ts
|
|
145074
145075
|
var import_ajv3 = __toESM(require_ajv(), 1);
|
|
145075
145076
|
var SetOutputParams = type({
|
|
@@ -145163,6 +145164,7 @@ function UpdatePullRequestBodyTool(ctx) {
|
|
|
145163
145164
|
pull_number: params.pull_number,
|
|
145164
145165
|
body: bodyWithFooter
|
|
145165
145166
|
});
|
|
145167
|
+
log.info(`\xBB updated pull request #${result.data.number}`);
|
|
145166
145168
|
ctx.toolState.wasUpdated = true;
|
|
145167
145169
|
return {
|
|
145168
145170
|
success: true,
|
|
@@ -145190,6 +145192,7 @@ function CreatePullRequestTool(ctx) {
|
|
|
145190
145192
|
base: params.base,
|
|
145191
145193
|
draft: params.draft ?? false
|
|
145192
145194
|
});
|
|
145195
|
+
log.info(`\xBB created pull request #${result.data.number} (id ${result.data.id})`);
|
|
145193
145196
|
const reviewer = ctx.payload.triggerer;
|
|
145194
145197
|
if (reviewer) {
|
|
145195
145198
|
try {
|
|
@@ -145741,7 +145744,7 @@ function ResolveReviewThreadTool(ctx) {
|
|
|
145741
145744
|
threadId: params.thread_id
|
|
145742
145745
|
});
|
|
145743
145746
|
const thread = response.resolveReviewThread.thread;
|
|
145744
|
-
log.
|
|
145747
|
+
log.info(`\xBB resolved review thread ${thread.id}`);
|
|
145745
145748
|
return {
|
|
145746
145749
|
thread_id: thread.id,
|
|
145747
145750
|
is_resolved: thread.isResolved,
|
|
@@ -146213,6 +146216,7 @@ function UploadFileTool(ctx) {
|
|
|
146213
146216
|
if (!uploadResponse.ok) {
|
|
146214
146217
|
throw new Error(`failed to upload file: ${uploadResponse.statusText}`);
|
|
146215
146218
|
}
|
|
146219
|
+
log.info(`\xBB uploaded file ${publicUrl}`);
|
|
146216
146220
|
return { success: true, publicUrl, filename, contentLength, contentType };
|
|
146217
146221
|
})
|
|
146218
146222
|
});
|
|
@@ -146306,8 +146310,7 @@ function buildOrchestratorTools(ctx, outputSchema) {
|
|
|
146306
146310
|
PushTagsTool(ctx),
|
|
146307
146311
|
DeleteBranchTool(ctx),
|
|
146308
146312
|
CreatePullRequestTool(ctx),
|
|
146309
|
-
UpdatePullRequestBodyTool(ctx)
|
|
146310
|
-
UpdateLearningsTool(ctx)
|
|
146313
|
+
UpdatePullRequestBodyTool(ctx)
|
|
146311
146314
|
];
|
|
146312
146315
|
}
|
|
146313
146316
|
async function tryStartMcpServer(ctx, tools, port) {
|
|
@@ -146464,9 +146467,6 @@ Rules:
|
|
|
146464
146467
|
- Do NOT include a changelog section \u2014 the key changes list serves this purpose
|
|
146465
146468
|
- Focus on *intent*, not *what* \u2014 the diff already shows what changed
|
|
146466
146469
|
- Get the file count and commit count from the checkout_pr metadata, not by counting manually`;
|
|
146467
|
-
function learningsStep(t2, n) {
|
|
146468
|
-
return `${n}. **learnings** (only if high confidence): if you discovered something about repo setup, test commands, conventions, or patterns that you are confident is correct and would reliably help future runs, call \`${t2("update_learnings")}\` to persist it. skip this step if you are unsure or the finding is speculative/one-off. format as a flat bullet list (\`- \` per line, one fact per bullet). merge with existing learnings from the prompt \u2014 pass the FULL merged list. deduplicate, and drop bullets that are clearly wrong or no longer relevant to the current codebase.`;
|
|
146469
|
-
}
|
|
146470
146470
|
function computeModes(agentId) {
|
|
146471
146471
|
const t2 = (toolName) => formatMcpToolRef(agentId, toolName);
|
|
146472
146472
|
return [
|
|
@@ -146522,8 +146522,6 @@ function computeModes(agentId) {
|
|
|
146522
146522
|
- create a PR via \`${t2("create_pull_request")}\`
|
|
146523
146523
|
- call \`${t2("report_progress")}\` with the PR link or the exact error if push/PR failed
|
|
146524
146524
|
|
|
146525
|
-
${learningsStep(t2, 6)}
|
|
146526
|
-
|
|
146527
146525
|
### Notes
|
|
146528
146526
|
|
|
146529
146527
|
For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
@@ -146551,9 +146549,7 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
146551
146549
|
- confirm a clean working tree, then push via \`${t2("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*)
|
|
146552
146550
|
- reply to each comment using \`${t2("reply_to_review_comment")}\`
|
|
146553
146551
|
- resolve addressed threads via \`${t2("resolve_review_thread")}\`
|
|
146554
|
-
- call \`${t2("report_progress")}\` with a brief summary (or the exact push error if push failed)
|
|
146555
|
-
|
|
146556
|
-
${learningsStep(t2, 6)}`
|
|
146552
|
+
- call \`${t2("report_progress")}\` with a brief summary (or the exact push error if push failed)`
|
|
146557
146553
|
},
|
|
146558
146554
|
// Review and IncrementalReview use the multi-lens orchestrator pattern
|
|
146559
146555
|
// (canonical source: .claude/commands/anneal.md). The orchestrator does
|
|
@@ -146724,9 +146720,7 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
146724
146720
|
|
|
146725
146721
|
2. Produce a structured, actionable plan with clear milestones.
|
|
146726
146722
|
|
|
146727
|
-
3. Call \`${t2("report_progress")}\` with the plan
|
|
146728
|
-
|
|
146729
|
-
${learningsStep(t2, 4)}`
|
|
146723
|
+
3. Call \`${t2("report_progress")}\` with the plan.`
|
|
146730
146724
|
},
|
|
146731
146725
|
{
|
|
146732
146726
|
name: "Fix",
|
|
@@ -146748,9 +146742,7 @@ ${learningsStep(t2, 4)}`
|
|
|
146748
146742
|
|
|
146749
146743
|
5. Finalize:
|
|
146750
146744
|
- confirm a clean working tree, then push via \`${t2("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*)
|
|
146751
|
-
- call \`${t2("report_progress")}\` with the diagnosis and fix summary (or the exact push error if push failed)
|
|
146752
|
-
|
|
146753
|
-
${learningsStep(t2, 6)}`
|
|
146745
|
+
- call \`${t2("report_progress")}\` with the diagnosis and fix summary (or the exact push error if push failed)`
|
|
146754
146746
|
},
|
|
146755
146747
|
{
|
|
146756
146748
|
name: "ResolveConflicts",
|
|
@@ -146794,9 +146786,7 @@ ${learningsStep(t2, 6)}`
|
|
|
146794
146786
|
3. Finalize:
|
|
146795
146787
|
- if code changes were made, push to a pull request (new or existing) using \`${t2("push_branch")}\` and \`${t2("create_pull_request")}\` as needed. \`git status\` must be clean before you finish (see *SYSTEM* Git rules if push fails).
|
|
146796
146788
|
- call \`${t2("report_progress")}\` once with results \u2014 include exact tool errors if push or PR creation failed
|
|
146797
|
-
- if the task involved labeling, commenting, or other GitHub operations, perform those directly
|
|
146798
|
-
|
|
146799
|
-
${learningsStep(t2, 4)}`
|
|
146789
|
+
- if the task involved labeling, commenting, or other GitHub operations, perform those directly`
|
|
146800
146790
|
}
|
|
146801
146791
|
];
|
|
146802
146792
|
}
|
|
@@ -146896,6 +146886,17 @@ async function installFromNpmTarball(params) {
|
|
|
146896
146886
|
// utils/providerErrors.ts
|
|
146897
146887
|
var statusKey = `\\b(?:status[_ ]?code|http[_ ]?status|status)["']?\\s*[:=]\\s*["']?`;
|
|
146898
146888
|
var PROVIDER_ERROR_PATTERNS = [
|
|
146889
|
+
// auth patterns must come BEFORE rate-limit patterns. OpenRouter 401 error
|
|
146890
|
+
// payloads carry `x-ratelimit-*` response headers in the dump, and the
|
|
146891
|
+
// free-form rate-limit regex below would otherwise win on word-boundary
|
|
146892
|
+
// matches inside header names. canonical 401 messages: OpenRouter returns
|
|
146893
|
+
// `{"error":{"message":"User not found","code":401}}` for disabled or
|
|
146894
|
+
// invalid keys (https://openai.luzhipeng.com/docs/api/reference/errors-and-debugging).
|
|
146895
|
+
{ regex: new RegExp(`${statusKey}401\\b`, "i"), label: "auth error (401)" },
|
|
146896
|
+
{ regex: new RegExp(`${statusKey}403\\b`, "i"), label: "auth error (403)" },
|
|
146897
|
+
{ regex: /\bUser not found\b/i, label: "auth error (invalid/disabled key)" },
|
|
146898
|
+
{ regex: /\bInvalid authentication\b/i, label: "auth error (invalid credentials)" },
|
|
146899
|
+
{ regex: /\bNo auth credentials found\b/i, label: "auth error (missing credentials)" },
|
|
146899
146900
|
{ regex: new RegExp(`${statusKey}429\\b`, "i"), label: "rate limited (429)" },
|
|
146900
146901
|
{ regex: new RegExp(`${statusKey}500\\b`, "i"), label: "provider 500 error" },
|
|
146901
146902
|
{ regex: new RegExp(`${statusKey}503\\b`, "i"), label: "provider unavailable (503)" },
|
|
@@ -146959,7 +146960,7 @@ function installBundledSkills(params) {
|
|
|
146959
146960
|
writeFileSync6(join9(skillDir, "SKILL.md"), content);
|
|
146960
146961
|
}
|
|
146961
146962
|
}
|
|
146962
|
-
log.
|
|
146963
|
+
log.success(`installed bundled skills: ${BUNDLED_SKILL_NAMES.join(", ")}`);
|
|
146963
146964
|
}
|
|
146964
146965
|
function addSkill(params) {
|
|
146965
146966
|
const result = spawnSync5(
|
|
@@ -146984,7 +146985,7 @@ function addSkill(params) {
|
|
|
146984
146985
|
}
|
|
146985
146986
|
);
|
|
146986
146987
|
if (result.status === 0) {
|
|
146987
|
-
log.
|
|
146988
|
+
log.success(`installed ${params.skill} skill (${params.agent})`);
|
|
146988
146989
|
} else {
|
|
146989
146990
|
const stderr = (result.stderr?.toString() || "").trim();
|
|
146990
146991
|
const errorMsg = result.error ? result.error.message : stderr;
|
|
@@ -147118,18 +147119,17 @@ function buildPostRunPrompt(issues) {
|
|
|
147118
147119
|
if (issues.summaryStale) parts.push(buildSummaryStalePrompt(issues.summaryStale.filePath));
|
|
147119
147120
|
return parts.join("\n\n---\n\n");
|
|
147120
147121
|
}
|
|
147121
|
-
function buildLearningsReflectionPrompt(
|
|
147122
|
-
const t2 = (name) => formatMcpToolRef(agentId, name);
|
|
147122
|
+
function buildLearningsReflectionPrompt(filePath) {
|
|
147123
147123
|
return [
|
|
147124
|
-
`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
|
|
147124
|
+
`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?`,
|
|
147125
147125
|
"",
|
|
147126
|
-
`
|
|
147126
|
+
`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.`,
|
|
147127
147127
|
"",
|
|
147128
|
-
`
|
|
147129
|
-
`- only
|
|
147130
|
-
`-
|
|
147131
|
-
`-
|
|
147132
|
-
`- if you
|
|
147128
|
+
`keep the file healthy:`,
|
|
147129
|
+
`- only add bullets when the finding is high-confidence AND broadly useful. skip speculative, one-off, or "maybe" findings.`,
|
|
147130
|
+
`- prune bullets that are clearly wrong, no longer relevant, or low-signal (rarely useful). a focused, accurate file beats a long stale one.`,
|
|
147131
|
+
`- 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.`,
|
|
147132
|
+
`- 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.`
|
|
147133
147133
|
].join("\n");
|
|
147134
147134
|
}
|
|
147135
147135
|
async function runPostRunRetryLoop(params) {
|
|
@@ -147467,6 +147467,12 @@ async function runClaude(params) {
|
|
|
147467
147467
|
activityTimeout: 3e5,
|
|
147468
147468
|
onActivityTimeout: params.onActivityTimeout,
|
|
147469
147469
|
stdio: ["ignore", "pipe", "pipe"],
|
|
147470
|
+
// run claude in its own process group so SIGKILL on activity timeout /
|
|
147471
|
+
// outer cancellation reaches any subprocesses it spawns (rg, file
|
|
147472
|
+
// watchers, mcp transports, etc). claude itself is a node bundle so
|
|
147473
|
+
// there's no shim-orphan issue like opencode-ai/bin/opencode, but
|
|
147474
|
+
// detached + killGroup is the right default for any agent runtime.
|
|
147475
|
+
killGroup: true,
|
|
147470
147476
|
onStdout: async (chunk) => {
|
|
147471
147477
|
const text = chunk.toString();
|
|
147472
147478
|
output += text;
|
|
@@ -147706,7 +147712,7 @@ var claude = agent({
|
|
|
147706
147712
|
stopScript: ctx.stopScript,
|
|
147707
147713
|
summaryFilePath: ctx.summaryFilePath,
|
|
147708
147714
|
summarySeed: ctx.summarySeed,
|
|
147709
|
-
reflectionPrompt: buildLearningsReflectionPrompt(
|
|
147715
|
+
reflectionPrompt: ctx.learningsFilePath ? buildLearningsReflectionPrompt(ctx.learningsFilePath) : void 0,
|
|
147710
147716
|
canResume: (r) => Boolean(r.sessionId),
|
|
147711
147717
|
resume: async (c2) => {
|
|
147712
147718
|
const sessionId = c2.previousResult.sessionId;
|
|
@@ -147722,9 +147728,92 @@ var claude = agent({
|
|
|
147722
147728
|
|
|
147723
147729
|
// agents/opencode.ts
|
|
147724
147730
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
147725
|
-
import { mkdirSync as mkdirSync5 } from "node:fs";
|
|
147731
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "node:fs";
|
|
147726
147732
|
import { join as join11 } from "node:path";
|
|
147727
147733
|
import { performance as performance7 } from "node:perf_hooks";
|
|
147734
|
+
|
|
147735
|
+
// agents/opencodePlugin.ts
|
|
147736
|
+
var PULLFROG_BUS_EVENT_TYPE = "pullfrog_bus_event";
|
|
147737
|
+
var PULLFROG_OPENCODE_PLUGIN_FILENAME = "pullfrog-events.ts";
|
|
147738
|
+
var PULLFROG_OPENCODE_PLUGIN_SOURCE = `// AUTOGENERATED by Pullfrog. do not edit; it'll be overwritten on the next run.
|
|
147739
|
+
// surfaces opencode subagent activity that the CLI's run-loop discards. see
|
|
147740
|
+
// action/agents/opencodePlugin.ts in pullfrog/app for why this exists. lives
|
|
147741
|
+
// inside the per-run tmpdir (XDG_CONFIG_HOME/opencode/plugin/), never inside
|
|
147742
|
+
// the user's working tree.
|
|
147743
|
+
|
|
147744
|
+
const PULLFROG_BUS_EVENT_TYPE = ${JSON.stringify(PULLFROG_BUS_EVENT_TYPE)};
|
|
147745
|
+
|
|
147746
|
+
// the first sessionID we see on a message.part.updated event is the
|
|
147747
|
+
// orchestrator \u2014 opencode's run command creates exactly one top-level session
|
|
147748
|
+
// before any subagent is dispatched, and the user-prompt text part fires
|
|
147749
|
+
// before the first task tool_use. we lock that sessionID in here and use it
|
|
147750
|
+
// to filter: the orchestrator's events are already streamed by the CLI's
|
|
147751
|
+
// run-loop, so we only forward (a) all subagent events, and (b) the
|
|
147752
|
+
// orchestrator's task tool dispatches at status="running". the CLI only
|
|
147753
|
+
// emits task tool_use at status=completed (after the subagent finishes), so
|
|
147754
|
+
// without the early announce the parent's labeler binds subagent sessions
|
|
147755
|
+
// before recordTaskDispatch fires and the lens label is lost.
|
|
147756
|
+
let orchestratorSessionID: string | undefined;
|
|
147757
|
+
|
|
147758
|
+
function isOrchestratorTaskDispatch(part: {
|
|
147759
|
+
type?: string;
|
|
147760
|
+
tool?: string;
|
|
147761
|
+
state?: { status?: string };
|
|
147762
|
+
}): boolean {
|
|
147763
|
+
if (part.type !== "tool") return false;
|
|
147764
|
+
if (part.tool !== "task") return false;
|
|
147765
|
+
// only forward at status="running" (not "pending"). at pending the
|
|
147766
|
+
// state.input is still {} \u2014 the orchestrator has emitted the part shell
|
|
147767
|
+
// but the LLM hasn't filled in description/subagent_type/prompt yet. by
|
|
147768
|
+
// running, input is populated and recordTaskDispatch can derive the lens
|
|
147769
|
+
// label correctly.
|
|
147770
|
+
return part.state?.status === "running";
|
|
147771
|
+
}
|
|
147772
|
+
|
|
147773
|
+
export default async function pullfrogEventsPlugin() {
|
|
147774
|
+
return {
|
|
147775
|
+
event: async (input: {
|
|
147776
|
+
event: {
|
|
147777
|
+
type: string;
|
|
147778
|
+
properties?: {
|
|
147779
|
+
part?: {
|
|
147780
|
+
sessionID?: string;
|
|
147781
|
+
type?: string;
|
|
147782
|
+
tool?: string;
|
|
147783
|
+
state?: { status?: string };
|
|
147784
|
+
};
|
|
147785
|
+
};
|
|
147786
|
+
};
|
|
147787
|
+
}) => {
|
|
147788
|
+
const event = input.event;
|
|
147789
|
+
if (!event || typeof event !== "object") return;
|
|
147790
|
+
if (event.type !== "message.part.updated") return;
|
|
147791
|
+
const part = event.properties?.part;
|
|
147792
|
+
const sessionID = part?.sessionID;
|
|
147793
|
+
if (typeof sessionID !== "string" || sessionID.length === 0) return;
|
|
147794
|
+
if (orchestratorSessionID === undefined) orchestratorSessionID = sessionID;
|
|
147795
|
+
|
|
147796
|
+
if (sessionID === orchestratorSessionID) {
|
|
147797
|
+
// skip orchestrator events EXCEPT early task dispatches.
|
|
147798
|
+
if (!part || !isOrchestratorTaskDispatch(part)) return;
|
|
147799
|
+
}
|
|
147800
|
+
|
|
147801
|
+
try {
|
|
147802
|
+
const line = JSON.stringify({
|
|
147803
|
+
type: PULLFROG_BUS_EVENT_TYPE,
|
|
147804
|
+
bus_event: event,
|
|
147805
|
+
});
|
|
147806
|
+
process.stdout.write(line + "\\n");
|
|
147807
|
+
} catch {
|
|
147808
|
+
// a circular reference or BigInt etc. would throw; swallow rather
|
|
147809
|
+
// than letting a single bad event take down the plugin.
|
|
147810
|
+
}
|
|
147811
|
+
},
|
|
147812
|
+
};
|
|
147813
|
+
}
|
|
147814
|
+
`;
|
|
147815
|
+
|
|
147816
|
+
// agents/opencode.ts
|
|
147728
147817
|
async function installOpencodeCli() {
|
|
147729
147818
|
return await installFromNpmTarball({
|
|
147730
147819
|
packageName: "opencode-ai",
|
|
@@ -147944,18 +148033,20 @@ async function runOpenCode(params) {
|
|
|
147944
148033
|
return;
|
|
147945
148034
|
}
|
|
147946
148035
|
if (toolName === "task") {
|
|
147947
|
-
|
|
147948
|
-
|
|
147949
|
-
|
|
147950
|
-
|
|
147951
|
-
|
|
147952
|
-
|
|
147953
|
-
|
|
147954
|
-
|
|
147955
|
-
|
|
147956
|
-
|
|
147957
|
-
|
|
147958
|
-
|
|
148036
|
+
if (!taskDispatchByCallID.has(toolId)) {
|
|
148037
|
+
const taskInput = event.part?.state?.input ?? {};
|
|
148038
|
+
const dispatchedLabel = labeler.recordTaskDispatch(taskInput);
|
|
148039
|
+
const dispatch = {
|
|
148040
|
+
label: dispatchedLabel,
|
|
148041
|
+
startedAt: performance7.now(),
|
|
148042
|
+
toolUseCallID: toolId
|
|
148043
|
+
};
|
|
148044
|
+
taskDispatchByCallID.set(toolId, dispatch);
|
|
148045
|
+
pendingTaskDispatches.push(dispatch);
|
|
148046
|
+
log.info(
|
|
148047
|
+
`\xBB dispatching subagent: ${dispatchedLabel}` + (taskInput.subagent_type ? ` (subagent_type=${taskInput.subagent_type})` : "")
|
|
148048
|
+
);
|
|
148049
|
+
}
|
|
147959
148050
|
} else {
|
|
147960
148051
|
knownNonTaskCallIDs.add(toolId);
|
|
147961
148052
|
}
|
|
@@ -147976,6 +148067,10 @@ async function runOpenCode(params) {
|
|
|
147976
148067
|
if (event.part?.state?.status === "completed" && event.part.state.output) {
|
|
147977
148068
|
log.debug(withLabel(label, ` output: ${event.part.state.output}`));
|
|
147978
148069
|
}
|
|
148070
|
+
if (event.part?.state?.status === "error") {
|
|
148071
|
+
const errorMsg = event.part.state.output ?? "(no error message)";
|
|
148072
|
+
log.info(withLabel(label, `\xBB tool call failed: ${errorMsg}`));
|
|
148073
|
+
}
|
|
147979
148074
|
if (toolName.includes("report_progress") && params.todoTracker) {
|
|
147980
148075
|
log.debug("\xBB report_progress detected, disabling todo tracking");
|
|
147981
148076
|
params.todoTracker.cancel();
|
|
@@ -148062,6 +148157,53 @@ async function runOpenCode(params) {
|
|
|
148062
148157
|
tokensLogged = true;
|
|
148063
148158
|
}
|
|
148064
148159
|
}
|
|
148160
|
+
},
|
|
148161
|
+
[PULLFROG_BUS_EVENT_TYPE]: async (event) => {
|
|
148162
|
+
const busEvent = event.bus_event;
|
|
148163
|
+
if (!busEvent || busEvent.type !== "message.part.updated") return;
|
|
148164
|
+
const part = busEvent.properties?.part;
|
|
148165
|
+
if (!part || typeof part.sessionID !== "string") return;
|
|
148166
|
+
const sessionID = part.sessionID;
|
|
148167
|
+
const partType = part.type;
|
|
148168
|
+
if (partType === "tool") {
|
|
148169
|
+
const status = part.state?.status;
|
|
148170
|
+
const partWithToolFields = part;
|
|
148171
|
+
const isOrchestratorTaskDispatch = partWithToolFields.tool === "task" && status === "running";
|
|
148172
|
+
if (isOrchestratorTaskDispatch) {
|
|
148173
|
+
const callID = partWithToolFields.callID;
|
|
148174
|
+
if (typeof callID === "string" && !taskDispatchByCallID.has(callID)) {
|
|
148175
|
+
const taskInput = partWithToolFields.state?.input ?? {};
|
|
148176
|
+
const dispatchedLabel = labeler.recordTaskDispatch(taskInput);
|
|
148177
|
+
const dispatch = {
|
|
148178
|
+
label: dispatchedLabel,
|
|
148179
|
+
startedAt: performance7.now(),
|
|
148180
|
+
toolUseCallID: callID
|
|
148181
|
+
};
|
|
148182
|
+
taskDispatchByCallID.set(callID, dispatch);
|
|
148183
|
+
pendingTaskDispatches.push(dispatch);
|
|
148184
|
+
log.info(
|
|
148185
|
+
`\xBB dispatching subagent: ${dispatchedLabel}` + (taskInput.subagent_type ? ` (subagent_type=${taskInput.subagent_type})` : "")
|
|
148186
|
+
);
|
|
148187
|
+
}
|
|
148188
|
+
return;
|
|
148189
|
+
}
|
|
148190
|
+
if (status !== "completed" && status !== "error") return;
|
|
148191
|
+
await handlers2.tool_use({
|
|
148192
|
+
type: "tool_use",
|
|
148193
|
+
sessionID,
|
|
148194
|
+
part
|
|
148195
|
+
});
|
|
148196
|
+
return;
|
|
148197
|
+
}
|
|
148198
|
+
if (partType === "step-start" || partType === "step-finish") return;
|
|
148199
|
+
if (partType === "text" && part.time?.end !== void 0) {
|
|
148200
|
+
await handlers2.text({
|
|
148201
|
+
type: "text",
|
|
148202
|
+
sessionID,
|
|
148203
|
+
part
|
|
148204
|
+
});
|
|
148205
|
+
return;
|
|
148206
|
+
}
|
|
148065
148207
|
}
|
|
148066
148208
|
};
|
|
148067
148209
|
const recentStderr = [];
|
|
@@ -148078,6 +148220,20 @@ async function runOpenCode(params) {
|
|
|
148078
148220
|
activityTimeout: 3e5,
|
|
148079
148221
|
onActivityTimeout: params.onActivityTimeout,
|
|
148080
148222
|
stdio: ["ignore", "pipe", "pipe"],
|
|
148223
|
+
// node_modules/opencode-ai/bin/opencode is a Node shim that spawnSyncs
|
|
148224
|
+
// the native opencode-<plat>-<arch> binary with stdio:"inherit". without
|
|
148225
|
+
// a process-group kill, SIGKILL hits only the shim, the native binary
|
|
148226
|
+
// is reparented to PID 1, holds our stdout pipe open, and `child.close`
|
|
148227
|
+
// never fires — producing zombie runs. detached + killGroup nukes the
|
|
148228
|
+
// whole tree.
|
|
148229
|
+
killGroup: true,
|
|
148230
|
+
// NB: we used to pass `isPausedExternally: isSubagentInFlight` to suspend
|
|
148231
|
+
// the activity timer during subagent dispatches. unnecessary now that
|
|
148232
|
+
// our injected plugin (action/agents/opencodePlugin.ts) re-emits
|
|
148233
|
+
// subagent `message.part.updated` events on opencode's stdout — those
|
|
148234
|
+
// arrive at child.stdout here, fire updateActivity(), and reset
|
|
148235
|
+
// lastActivityTime naturally. verified empirically in PR #634
|
|
148236
|
+
// (~3.3 plugin events/sec during a typical subagent run).
|
|
148081
148237
|
onStdout: async (chunk) => {
|
|
148082
148238
|
const text = chunk.toString();
|
|
148083
148239
|
output += text;
|
|
@@ -148232,6 +148388,12 @@ var opencode = agent({
|
|
|
148232
148388
|
XDG_CONFIG_HOME: join11(ctx.tmpdir, ".config")
|
|
148233
148389
|
};
|
|
148234
148390
|
mkdirSync5(join11(homeEnv.XDG_CONFIG_HOME, "opencode"), { recursive: true });
|
|
148391
|
+
const opencodePluginDir = join11(homeEnv.XDG_CONFIG_HOME, "opencode", "plugin");
|
|
148392
|
+
mkdirSync5(opencodePluginDir, { recursive: true });
|
|
148393
|
+
writeFileSync8(
|
|
148394
|
+
join11(opencodePluginDir, PULLFROG_OPENCODE_PLUGIN_FILENAME),
|
|
148395
|
+
PULLFROG_OPENCODE_PLUGIN_SOURCE
|
|
148396
|
+
);
|
|
148235
148397
|
const agentBrowserVersion = getDevDependencyVersion("agent-browser");
|
|
148236
148398
|
addSkill({
|
|
148237
148399
|
ref: `vercel-labs/agent-browser@v${agentBrowserVersion}`,
|
|
@@ -148274,7 +148436,7 @@ var opencode = agent({
|
|
|
148274
148436
|
stopScript: ctx.stopScript,
|
|
148275
148437
|
summaryFilePath: ctx.summaryFilePath,
|
|
148276
148438
|
summarySeed: ctx.summarySeed,
|
|
148277
|
-
reflectionPrompt: buildLearningsReflectionPrompt(
|
|
148439
|
+
reflectionPrompt: ctx.learningsFilePath ? buildLearningsReflectionPrompt(ctx.learningsFilePath) : void 0,
|
|
148278
148440
|
resume: async (c2) => runOpenCode({
|
|
148279
148441
|
...runParams,
|
|
148280
148442
|
args: [...baseArgs, "--continue", c2.prompt]
|
|
@@ -152487,7 +152649,7 @@ ${ctx.error}` : ctx.error;
|
|
|
152487
152649
|
|
|
152488
152650
|
// utils/gitAuthServer.ts
|
|
152489
152651
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
152490
|
-
import { writeFileSync as
|
|
152652
|
+
import { writeFileSync as writeFileSync9 } from "node:fs";
|
|
152491
152653
|
import { createServer as createServer2 } from "node:http";
|
|
152492
152654
|
import { join as join13 } from "node:path";
|
|
152493
152655
|
var CODE_TTL_MS = 5 * 60 * 1e3;
|
|
@@ -152576,7 +152738,7 @@ async function startGitAuthServer(tmpdir3) {
|
|
|
152576
152738
|
`try{require("fs").unlinkSync("${scriptPath.replace(/\\/g, "\\\\")}")}catch(e){}`,
|
|
152577
152739
|
`})}).on("error",function(){process.exit(1)})}`
|
|
152578
152740
|
].join("\n");
|
|
152579
|
-
|
|
152741
|
+
writeFileSync9(scriptPath, content, { mode: 448 });
|
|
152580
152742
|
return scriptPath;
|
|
152581
152743
|
}
|
|
152582
152744
|
async function close() {
|
|
@@ -152850,9 +153012,9 @@ function buildPromptContext(ctx) {
|
|
|
152850
153012
|
};
|
|
152851
153013
|
}
|
|
152852
153014
|
function assembleFullPrompt(ctx) {
|
|
152853
|
-
const learningsSection = ctx.
|
|
153015
|
+
const learningsSection = ctx.learningsFilePath ? `************* LEARNINGS *************
|
|
152854
153016
|
|
|
152855
|
-
|
|
153017
|
+
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.` : "";
|
|
152856
153018
|
const runtimeSection = `************* RUNTIME *************
|
|
152857
153019
|
|
|
152858
153020
|
${ctx.runtime}`;
|
|
@@ -152879,8 +153041,8 @@ function resolveInstructions(ctx) {
|
|
|
152879
153041
|
if (eventContext)
|
|
152880
153042
|
tocEntries.push({ label: "EVENT CONTEXT", description: "related PR/issue data" });
|
|
152881
153043
|
tocEntries.push({ label: "SYSTEM", description: "persona, security, tools, workflow rules" });
|
|
152882
|
-
if (pctx.
|
|
152883
|
-
tocEntries.push({ label: "LEARNINGS", description: "repo-specific knowledge" });
|
|
153044
|
+
if (pctx.learningsFilePath)
|
|
153045
|
+
tocEntries.push({ label: "LEARNINGS", description: "repo-specific knowledge file path" });
|
|
152884
153046
|
tocEntries.push({ label: "RUNTIME", description: "environment metadata" });
|
|
152885
153047
|
const toc = buildToc(tocEntries);
|
|
152886
153048
|
const full = assembleFullPrompt({
|
|
@@ -152889,7 +153051,7 @@ function resolveInstructions(ctx) {
|
|
|
152889
153051
|
procedure,
|
|
152890
153052
|
eventContext,
|
|
152891
153053
|
system,
|
|
152892
|
-
|
|
153054
|
+
learningsFilePath: pctx.learningsFilePath,
|
|
152893
153055
|
runtime: pctx.runtime
|
|
152894
153056
|
});
|
|
152895
153057
|
const event = [pctx.eventTitle, pctx.eventMetadata].filter(Boolean).join("\n\n---\n\n");
|
|
@@ -152903,6 +153065,32 @@ function resolveInstructions(ctx) {
|
|
|
152903
153065
|
};
|
|
152904
153066
|
}
|
|
152905
153067
|
|
|
153068
|
+
// utils/learnings.ts
|
|
153069
|
+
import { mkdir, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
153070
|
+
import { dirname as dirname4, join as join14 } from "node:path";
|
|
153071
|
+
var LEARNINGS_FILE_NAME = "pullfrog-learnings.md";
|
|
153072
|
+
var MAX_LEARNINGS_LENGTH = 1e4;
|
|
153073
|
+
function learningsFilePath(tmpdir3) {
|
|
153074
|
+
return join14(tmpdir3, LEARNINGS_FILE_NAME);
|
|
153075
|
+
}
|
|
153076
|
+
async function seedLearningsFile(params) {
|
|
153077
|
+
const path3 = learningsFilePath(params.tmpdir);
|
|
153078
|
+
await mkdir(dirname4(path3), { recursive: true });
|
|
153079
|
+
await writeFile2(path3, params.current ?? "", "utf8");
|
|
153080
|
+
return path3;
|
|
153081
|
+
}
|
|
153082
|
+
async function readLearningsFile(path3) {
|
|
153083
|
+
let raw2;
|
|
153084
|
+
try {
|
|
153085
|
+
raw2 = await readFile2(path3, "utf8");
|
|
153086
|
+
} catch {
|
|
153087
|
+
return null;
|
|
153088
|
+
}
|
|
153089
|
+
const trimmed = raw2.trim();
|
|
153090
|
+
if (trimmed.length > MAX_LEARNINGS_LENGTH) return trimmed.slice(0, MAX_LEARNINGS_LENGTH);
|
|
153091
|
+
return trimmed;
|
|
153092
|
+
}
|
|
153093
|
+
|
|
152906
153094
|
// utils/normalizeEnv.ts
|
|
152907
153095
|
function maskValue(value2) {
|
|
152908
153096
|
if (value2 && typeof value2 === "string" && value2.trim().length > 0) {
|
|
@@ -153078,8 +153266,8 @@ function resolvePayload(resolvedPromptInput, repoSettings) {
|
|
|
153078
153266
|
}
|
|
153079
153267
|
|
|
153080
153268
|
// utils/prSummary.ts
|
|
153081
|
-
import { mkdir, readFile as
|
|
153082
|
-
import { dirname as
|
|
153269
|
+
import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
|
|
153270
|
+
import { dirname as dirname5, join as join15 } from "node:path";
|
|
153083
153271
|
var SUMMARY_FILE_NAME = "pullfrog-summary.md";
|
|
153084
153272
|
var SUMMARY_SCAFFOLD = `# PR summary
|
|
153085
153273
|
|
|
@@ -153089,19 +153277,19 @@ var SUMMARY_SCAFFOLD = `# PR summary
|
|
|
153089
153277
|
var MIN_SNAPSHOT_LENGTH = 60;
|
|
153090
153278
|
var MAX_SNAPSHOT_LENGTH = 32768;
|
|
153091
153279
|
function summaryFilePath(tmpdir3) {
|
|
153092
|
-
return
|
|
153280
|
+
return join15(tmpdir3, SUMMARY_FILE_NAME);
|
|
153093
153281
|
}
|
|
153094
153282
|
async function seedSummaryFile(params) {
|
|
153095
153283
|
const path3 = summaryFilePath(params.tmpdir);
|
|
153096
|
-
await
|
|
153284
|
+
await mkdir2(dirname5(path3), { recursive: true });
|
|
153097
153285
|
const seed = params.previousSnapshot && params.previousSnapshot.trim().length >= MIN_SNAPSHOT_LENGTH ? params.previousSnapshot : SUMMARY_SCAFFOLD;
|
|
153098
|
-
await
|
|
153286
|
+
await writeFile3(path3, seed, "utf8");
|
|
153099
153287
|
return path3;
|
|
153100
153288
|
}
|
|
153101
153289
|
async function readSummaryFile(path3) {
|
|
153102
153290
|
let raw2;
|
|
153103
153291
|
try {
|
|
153104
|
-
raw2 = await
|
|
153292
|
+
raw2 = await readFile3(path3, "utf8");
|
|
153105
153293
|
} catch {
|
|
153106
153294
|
return null;
|
|
153107
153295
|
}
|
|
@@ -153319,9 +153507,9 @@ async function resolveRunContextData(params) {
|
|
|
153319
153507
|
import { execFileSync as execFileSync5, execSync as execSync3 } from "node:child_process";
|
|
153320
153508
|
import { mkdtempSync } from "node:fs";
|
|
153321
153509
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
153322
|
-
import { join as
|
|
153510
|
+
import { join as join16 } from "node:path";
|
|
153323
153511
|
function createTempDirectory() {
|
|
153324
|
-
const sharedTempDir = mkdtempSync(
|
|
153512
|
+
const sharedTempDir = mkdtempSync(join16(tmpdir2(), "pullfrog-"));
|
|
153325
153513
|
process.env.PULLFROG_TEMP_DIR = sharedTempDir;
|
|
153326
153514
|
log.info(`\xBB created temp dir at ${sharedTempDir}`);
|
|
153327
153515
|
return sharedTempDir;
|
|
@@ -153723,15 +153911,12 @@ function formatTransientErrorSummary(error49, owner) {
|
|
|
153723
153911
|
}
|
|
153724
153912
|
async function mintProxyKey(ctx) {
|
|
153725
153913
|
try {
|
|
153726
|
-
|
|
153727
|
-
|
|
153728
|
-
const oidcToken = await core6.getIDToken("pullfrog-api");
|
|
153729
|
-
delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
153730
|
-
delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
153914
|
+
const headers = await buildProxyTokenHeaders(ctx);
|
|
153915
|
+
if (!headers) return null;
|
|
153731
153916
|
const response = await apiFetch({
|
|
153732
153917
|
path: "/api/proxy-token",
|
|
153733
153918
|
method: "POST",
|
|
153734
|
-
headers
|
|
153919
|
+
headers
|
|
153735
153920
|
});
|
|
153736
153921
|
if (response.status === 402) {
|
|
153737
153922
|
const body = await response.json().catch(() => null);
|
|
@@ -153763,15 +153948,30 @@ async function mintProxyKey(ctx) {
|
|
|
153763
153948
|
delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
153764
153949
|
}
|
|
153765
153950
|
}
|
|
153951
|
+
async function buildProxyTokenHeaders(ctx) {
|
|
153952
|
+
if (ctx.oidcCredentials) {
|
|
153953
|
+
process.env.ACTIONS_ID_TOKEN_REQUEST_URL = ctx.oidcCredentials.requestUrl;
|
|
153954
|
+
process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = ctx.oidcCredentials.requestToken;
|
|
153955
|
+
const oidcToken = await core6.getIDToken("pullfrog-api");
|
|
153956
|
+
delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
153957
|
+
delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
153958
|
+
return { Authorization: `Bearer ${oidcToken}` };
|
|
153959
|
+
}
|
|
153960
|
+
if (isLocalApiUrl()) {
|
|
153961
|
+
log.info(`\xBB proxy: dev bypass (x-dev-repo) for ${ctx.repo.owner}/${ctx.repo.name}`);
|
|
153962
|
+
return { "x-dev-repo": `${ctx.repo.owner}/${ctx.repo.name}` };
|
|
153963
|
+
}
|
|
153964
|
+
return null;
|
|
153965
|
+
}
|
|
153766
153966
|
async function resolveProxyModel(ctx) {
|
|
153767
153967
|
if (process.env.PULLFROG_MODEL?.trim()) return;
|
|
153768
153968
|
const needsProxy = isInfraCovered({ isOss: ctx.oss, plan: ctx.plan }) && ctx.proxyModel;
|
|
153769
153969
|
if (!needsProxy) return;
|
|
153770
|
-
if (!ctx.oidcCredentials) {
|
|
153970
|
+
if (!ctx.oidcCredentials && !isLocalApiUrl()) {
|
|
153771
153971
|
log.warning("\xBB proxy requested but no OIDC credentials available \u2014 skipping");
|
|
153772
153972
|
return;
|
|
153773
153973
|
}
|
|
153774
|
-
const key = await mintProxyKey({ oidcCredentials: ctx.oidcCredentials });
|
|
153974
|
+
const key = await mintProxyKey({ oidcCredentials: ctx.oidcCredentials, repo: ctx.repo });
|
|
153775
153975
|
if (!key) return;
|
|
153776
153976
|
process.env.OPENROUTER_API_KEY = key;
|
|
153777
153977
|
core6.setSecret(key);
|
|
@@ -153795,6 +153995,45 @@ async function fetchPreviousSnapshot(ctx, prNumber) {
|
|
|
153795
153995
|
return null;
|
|
153796
153996
|
}
|
|
153797
153997
|
}
|
|
153998
|
+
async function persistLearnings(ctx) {
|
|
153999
|
+
const filePath = ctx.toolState.learningsFilePath;
|
|
154000
|
+
if (!filePath) return;
|
|
154001
|
+
if (ctx.toolState.learningsPersistAttempted) return;
|
|
154002
|
+
ctx.toolState.learningsPersistAttempted = true;
|
|
154003
|
+
const current = await readLearningsFile(filePath);
|
|
154004
|
+
if (current === null) {
|
|
154005
|
+
log.debug(`learnings tmpfile missing or unreadable at ${filePath} \u2014 skipping persist`);
|
|
154006
|
+
return;
|
|
154007
|
+
}
|
|
154008
|
+
const seed = ctx.toolState.learningsSeed?.trim() ?? "";
|
|
154009
|
+
if (current === seed) {
|
|
154010
|
+
log.debug("learnings tmpfile unchanged from seed \u2014 skipping persist");
|
|
154011
|
+
return;
|
|
154012
|
+
}
|
|
154013
|
+
try {
|
|
154014
|
+
const response = await apiFetch({
|
|
154015
|
+
path: `/api/repo/${ctx.repo.owner}/${ctx.repo.name}/learnings`,
|
|
154016
|
+
method: "PATCH",
|
|
154017
|
+
headers: {
|
|
154018
|
+
authorization: `Bearer ${ctx.apiToken}`,
|
|
154019
|
+
"content-type": "application/json"
|
|
154020
|
+
},
|
|
154021
|
+
body: JSON.stringify({
|
|
154022
|
+
learnings: current,
|
|
154023
|
+
model: ctx.toolState.model
|
|
154024
|
+
}),
|
|
154025
|
+
signal: AbortSignal.timeout(1e4)
|
|
154026
|
+
});
|
|
154027
|
+
if (!response.ok) {
|
|
154028
|
+
const error49 = await response.text().catch(() => "(no body)");
|
|
154029
|
+
log.debug(`learnings persist failed (${response.status}): ${error49}`);
|
|
154030
|
+
return;
|
|
154031
|
+
}
|
|
154032
|
+
log.info("\xBB learnings updated");
|
|
154033
|
+
} catch (err) {
|
|
154034
|
+
log.debug(`learnings persist failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
154035
|
+
}
|
|
154036
|
+
}
|
|
153798
154037
|
async function persistSummary(ctx) {
|
|
153799
154038
|
const filePath = ctx.toolState.summaryFilePath;
|
|
153800
154039
|
if (!filePath) return;
|
|
@@ -153876,7 +154115,8 @@ async function main() {
|
|
|
153876
154115
|
oss: runContext.oss,
|
|
153877
154116
|
plan: runContext.plan,
|
|
153878
154117
|
proxyModel: runContext.proxyModel,
|
|
153879
|
-
oidcCredentials
|
|
154118
|
+
oidcCredentials,
|
|
154119
|
+
repo: runContext.repo
|
|
153880
154120
|
});
|
|
153881
154121
|
} catch (error49) {
|
|
153882
154122
|
if (error49 instanceof BillingError) {
|
|
@@ -153979,12 +154219,32 @@ async function main() {
|
|
|
153979
154219
|
toolContext.mcpServerUrl = mcpHttpServer.url;
|
|
153980
154220
|
log.info(`\xBB MCP server started at ${mcpHttpServer.url}`);
|
|
153981
154221
|
timer.checkpoint("mcpServer");
|
|
154222
|
+
try {
|
|
154223
|
+
const learningsPath = await seedLearningsFile({
|
|
154224
|
+
tmpdir: tmpdir3,
|
|
154225
|
+
current: runContext.repoSettings.learnings
|
|
154226
|
+
});
|
|
154227
|
+
toolState.learningsFilePath = learningsPath;
|
|
154228
|
+
try {
|
|
154229
|
+
toolState.learningsSeed = await readFile4(learningsPath, "utf8");
|
|
154230
|
+
} catch {
|
|
154231
|
+
}
|
|
154232
|
+
log.info(
|
|
154233
|
+
`\xBB learnings seeded at ${learningsPath} (existing=${runContext.repoSettings.learnings ? "yes" : "no"})`
|
|
154234
|
+
);
|
|
154235
|
+
const ctxForExit = toolContext;
|
|
154236
|
+
onExitSignal(() => persistLearnings(ctxForExit));
|
|
154237
|
+
} catch (err) {
|
|
154238
|
+
log.warning(
|
|
154239
|
+
`\xBB learnings seed failed: ${err instanceof Error ? err.message : String(err)} \u2014 continuing without learnings file`
|
|
154240
|
+
);
|
|
154241
|
+
}
|
|
153982
154242
|
if (payload.generateSummary && payload.event.is_pr && payload.event.issue_number) {
|
|
153983
154243
|
const previousSnapshot = await fetchPreviousSnapshot(toolContext, payload.event.issue_number);
|
|
153984
154244
|
const filePath = await seedSummaryFile({ tmpdir: tmpdir3, previousSnapshot });
|
|
153985
154245
|
toolState.summaryFilePath = filePath;
|
|
153986
154246
|
try {
|
|
153987
|
-
toolState.summarySeed = await
|
|
154247
|
+
toolState.summarySeed = await readFile4(filePath, "utf8");
|
|
153988
154248
|
} catch {
|
|
153989
154249
|
}
|
|
153990
154250
|
log.info(
|
|
@@ -154008,7 +154268,7 @@ async function main() {
|
|
|
154008
154268
|
modes: modes2,
|
|
154009
154269
|
agentId,
|
|
154010
154270
|
outputSchema,
|
|
154011
|
-
|
|
154271
|
+
learningsFilePath: toolState.learningsFilePath ?? null
|
|
154012
154272
|
});
|
|
154013
154273
|
const logParts = [
|
|
154014
154274
|
instructions.eventInstructions ? `EVENT-LEVEL INSTRUCTIONS:
|
|
@@ -154024,7 +154284,7 @@ ${instructions.user}` : null,
|
|
|
154024
154284
|
log.info(instructions.full);
|
|
154025
154285
|
});
|
|
154026
154286
|
if (agentId === "opencode") {
|
|
154027
|
-
const pluginDir =
|
|
154287
|
+
const pluginDir = join17(process.cwd(), ".opencode", "plugin");
|
|
154028
154288
|
const hasPlugins = existsSync7(pluginDir) && readdirSync(pluginDir).some((f) => /\.[jt]sx?$/.test(f));
|
|
154029
154289
|
if (hasPlugins && toolState.dependencyInstallation?.promise) {
|
|
154030
154290
|
log.info(
|
|
@@ -154085,6 +154345,7 @@ ${instructions.user}` : null,
|
|
|
154085
154345
|
stopScript: runContext.repoSettings.stopScript,
|
|
154086
154346
|
summaryFilePath: toolState.summaryFilePath,
|
|
154087
154347
|
summarySeed: toolState.summarySeed,
|
|
154348
|
+
learningsFilePath: toolState.learningsFilePath,
|
|
154088
154349
|
onActivityTimeout: onInnerActivityTimeout,
|
|
154089
154350
|
onToolUse: (event) => {
|
|
154090
154351
|
const wasTracked = recordDiffReadFromToolUse({
|
|
@@ -154142,6 +154403,9 @@ ${instructions.user}` : null,
|
|
|
154142
154403
|
if (toolContext) {
|
|
154143
154404
|
await persistSummary(toolContext);
|
|
154144
154405
|
}
|
|
154406
|
+
if (toolContext) {
|
|
154407
|
+
await persistLearnings(toolContext);
|
|
154408
|
+
}
|
|
154145
154409
|
if (toolContext && toolState.progressComment && !toolState.finalSummaryWritten) {
|
|
154146
154410
|
await deleteProgressComment(toolContext).catch((error49) => {
|
|
154147
154411
|
log.debug(`stranded progress comment cleanup failed: ${error49}`);
|
|
@@ -154194,6 +154458,9 @@ ${errorMessage}
|
|
|
154194
154458
|
if (toolContext) {
|
|
154195
154459
|
await persistSummary(toolContext);
|
|
154196
154460
|
}
|
|
154461
|
+
if (toolContext) {
|
|
154462
|
+
await persistLearnings(toolContext);
|
|
154463
|
+
}
|
|
154197
154464
|
return {
|
|
154198
154465
|
success: false,
|
|
154199
154466
|
error: errorMessage
|
|
@@ -154226,7 +154493,7 @@ ${errorMessage}
|
|
|
154226
154493
|
}
|
|
154227
154494
|
|
|
154228
154495
|
// commands/gha.ts
|
|
154229
|
-
process.env.PATH = `${
|
|
154496
|
+
process.env.PATH = `${dirname6(process.execPath)}:${process.env.PATH}`;
|
|
154230
154497
|
var STATE_TOKEN = "token";
|
|
154231
154498
|
async function runMain() {
|
|
154232
154499
|
try {
|
|
@@ -156036,7 +156303,7 @@ async function run2() {
|
|
|
156036
156303
|
}
|
|
156037
156304
|
|
|
156038
156305
|
// cli.ts
|
|
156039
|
-
var VERSION10 = "0.1.
|
|
156306
|
+
var VERSION10 = "0.1.2";
|
|
156040
156307
|
var bin = basename2(process.argv[1] || "");
|
|
156041
156308
|
var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
|
|
156042
156309
|
var rawArgs = process.argv.slice(2);
|