Compare commits

...

5 Commits

Author SHA1 Message Date
Colin McDonnell 727e407ed3 176
Made-with: Cursor
2026-03-04 08:03:13 -08:00
Mateusz Burzyński 421eecebe3 Correctly import modes through @pullfrog/pullfrog/internal (#428)
* Corectly import `modes` through `@pullfrog/pullfrog/internal`

* add a biome rule

* fix rule
2026-03-03 13:59:37 +00:00
pullfrog[bot] cc46af0d47 Share GitHub rate limit tracking between the action and the worker (#326)
* share GitHub rate limit tracking between the action and the worker

The action now counts all GitHub API requests and captures the latest
`x-ratelimit-remaining`/`x-ratelimit-reset` headers via a global
request hook on every Octokit instance.

On exit, the usage summary is written atomically to a path specified
by `PULLFROG_USAGE_SUMMARY_PATH`. The worker sets this env var before
sandbox execution, reads the file afterward, and feeds the data into
the Durable Object's rate limit state.

This closes the visibility gap where the worker had no insight into
API calls made by the sandboxed action process.

* address review: refactor rate limit state, randomize usage summary path

* track actual rate limit cost using x-ratelimit-remaining delta

* refactor usage summary writing to use onExitSignal API

Replace the monolithic registerUsageSummaryHandler with direct use of
onExitSignal in main.ts and a writeGitHubUsageSummaryToFile utility
in github.ts. This keeps exitHandler.ts as a pure signal handler
registry (from #299) and also writes the summary on normal exit.

* tweak

* unify

* deduplicate stuff

* improve error handling

---------

Co-authored-by: pullfrog[bot] <226033991+pullfrog[bot]@users.noreply.github.com>
Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>
2026-03-03 12:45:36 +00:00
David Blass 53970308ee Add incremental re-review on new PR commits (#388)
* add incremental re-review on new PR commits

When new commits are pushed to a PR that Pullfrog has previously reviewed,
automatically perform a focused re-review on only the new changes. Includes
a supersede mechanism to abort stale in-flight reviews on rapid pushes, a
new IncrementalReview mode with incremental diff + prior-feedback awareness,
and a PRReview tracking model so re-review fires for both auto-reviewed and
manually-triggered PRs. Reviews now always submit (APPROVE when clean).

Co-authored-by: Cursor <cursoragent@cursor.com>

* simplify re-review eligibility and add summary to incremental reviews

Remove the path1/path2 distinction for re-review eligibility — now simply
requires prReReview=enabled and a prior Pullfrog review on the PR. Show
the re-review toggle regardless of prCreated setting. Add a top-level
summary body to incremental reviews for consistency with full reviews.

Co-authored-by: Cursor <cursoragent@cursor.com>

* replace superseded polling with server-side in-flight dedup and add prApproveEnabled setting

Made-with: Cursor

* add armstrong cursor command

Made-with: Cursor

* update armstrong

* feat: Pullfrogger game v1 (#378)

* Frogger game basis code (CC0 1.0 Unversal license).

* Initial React port.

* fix props for Sprite.

* fixed context issue in FroggerGame.

* Fixed format.

* Fixed lint issue in FroggerGame.

* Restoring LeapingLoader, using FroggerGame as a new fallback in Suspense.

* feat: Display toast when URL ready.

* Add props constraints on Sprite.

* feat: frog sprite.

* fix: zoom and alignment.

* fix: extract const.

* fix: mv types.

* fix: mv game into index.tsx file.

* fix: replacing deprecated event prop which with key.

* feat: Log sprite.

* feat: turtle sprite.

* Adjusting game colors.

* feat: sprites for the cars.

* rm primitive sprites.

* fix: bulldozer sprite position.

* Adjusting colors.

* Shape constraints.

* rm original.

* minor: naming, cleanup.

* fix: renderers dict.

* fix: adjusting and renaming racer.

* fix renderer binding for scored frogs.

* feat: responsive layout with gap below and maintained aspect ratio.

* grammar fix.

* feat: road lane dividers.

* feat: using AbortController to cleanup events.

* fix: cleanup and shortening.

* feat: extracting drawGameBackground.

* feat: initObstacleRows and updateAndDrawObstacles helpers.

* feat: initFroggers helper.

* feat: drawFroggers helper.

* feat: checkForCollision helper.

* feat: makeKeydownHandler helper.

* feat: listenToKeyboardEvents helper.

* mv cleanup into drawGameBackground.

* fix: cleanup.

* feat: better adjustBrightness helper.

* Polling on the page.tsx side, restoring timeout and fallbacks, dynamic link msg.

* FEAT: wait for workflow to complete and notify additionally with a big link (incl.db migration).

* Fix: larger title, shorter link.

* fix(hook): Writing completedAt from hook.workflow_run.updated_at according to suggestion.

* fix(loader): rm unused props from WorkflowRunClientProps as suggested.

* fix(loader): mv id=dev case into the page.

* fix(DNRY): mv PollStartedResult and PollCompletedResult types.

* fix(DNRY): reusing drawEllipse() helper in Sprite.

* fix(style): reordering methods by priority in Sprite.

* fix(frogger): rm empty rows from canvas, square game.

* feat: game canvas rounded corners.

* fix(DNRY): extracting SpriteShape type for faster reference.

* fix: rm target from links, use same window.

* add incremental re-review on new PR commits

When new commits are pushed to a PR that Pullfrog has previously reviewed,
automatically perform a focused re-review on only the new changes. Includes
a supersede mechanism to abort stale in-flight reviews on rapid pushes, a
new IncrementalReview mode with incremental diff + prior-feedback awareness,
and a PRReview tracking model so re-review fires for both auto-reviewed and
manually-triggered PRs. Reviews now always submit (APPROVE when clean).

Co-authored-by: Cursor <cursoragent@cursor.com>

* replace superseded polling with server-side in-flight dedup and add prApproveEnabled setting

Made-with: Cursor

* consolidate workflow_run completed handling and track completedAt

Removes the duplicate exported handleWorkflowRunCompleted in favor of
the private one, merges status + orphan resolution logic into a single
path, and sets completedAt on both normal completion and orphan cancel.

Made-with: Cursor

* fix rebase conflict resolution: restore eligibility logic, incremental review summaries, and exhaustiveness check

- replace deleted hasPullfrogReviewedPR call with WorkflowRun.findFirst
  (the utility file was removed by the dedup improvements commit)
- restore IncrementalReview summary body in modes.ts and selectMode.ts
  (lost during ca0168b conflict resolution; origin had re-added them via f98f902)
- use switch + satisfies never for workflow_run event dispatch
- lowercase comments per project conventions

Made-with: Cursor

* fix workflow-run polling architecture and improve incremental review prompts

move polling loops from server actions to client to avoid serverless timeouts
(pollForCompleted ran up to 600s in a single invocation). each server action
is now a single DB check; client drives retries. also fix misleading prompt
text about incremental diff scope and remove dead code in handleWebhook.

Made-with: Cursor

* await reportReviewNodeId to eliminate race condition and webhook sleep

- refactor reportReviewNodeId from fire-and-forget to async/awaited,
  guaranteeing the dedup signal lands before the tool returns
- remove the 5-second grace period sleep in the synchronize webhook
  handler (no longer needed with the awaited PATCH)
- update IncrementalReview guidance to use get_review_comments for
  detailed prior line-level feedback instead of just review summaries
- remove dead fallbackUrl field from CheckStartedResult type

Made-with: Cursor

* fix rebase artifacts: broken triggeringIssue reference, review formatting, and prompt wording

Made-with: Cursor

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Anna Bocharova <robin_tail@me.com>
2026-02-28 18:30:49 +00:00
Mateusz Burzyński 7c8dd7f43c 175 2026-02-27 13:23:34 +00:00
15 changed files with 575 additions and 110 deletions
+260 -89
View File
@@ -18198,7 +18198,7 @@ var require_summary = __commonJS({
exports.summary = exports.markdownSummary = exports.SUMMARY_DOCS_URL = exports.SUMMARY_ENV_VAR = void 0;
var os_1 = __require("os");
var fs_1 = __require("fs");
var { access, appendFile, writeFile } = fs_1.promises;
var { access, appendFile, writeFile: writeFile2 } = fs_1.promises;
exports.SUMMARY_ENV_VAR = "GITHUB_STEP_SUMMARY";
exports.SUMMARY_DOCS_URL = "https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary";
var Summary = class {
@@ -18256,7 +18256,7 @@ var require_summary = __commonJS({
return __awaiter(this, void 0, void 0, function* () {
const overwrite = !!(options === null || options === void 0 ? void 0 : options.overwrite);
const filePath = yield this.filePath();
const writeFunc = overwrite ? writeFile : appendFile;
const writeFunc = overwrite ? writeFile2 : appendFile;
yield writeFunc(filePath, this._buffer, { encoding: "utf8" });
return this.emptyBuffer();
});
@@ -26579,13 +26579,13 @@ var init_schemas = __esm({
}
return propValues;
});
const isObject4 = isObject;
const isObject5 = isObject;
const catchall = def.catchall;
let value2;
inst._zod.parse = (payload, ctx) => {
value2 ?? (value2 = _normalized.value);
const input = payload.value;
if (!isObject4(input)) {
if (!isObject5(input)) {
payload.issues.push({
expected: "object",
code: "invalid_type",
@@ -26683,7 +26683,7 @@ var init_schemas = __esm({
return (payload, ctx) => fn2(shape, payload, ctx);
};
let fastpass;
const isObject4 = isObject;
const isObject5 = isObject;
const jit = !globalConfig.jitless;
const allowsEval3 = allowsEval;
const fastEnabled = jit && allowsEval3.value;
@@ -26692,7 +26692,7 @@ var init_schemas = __esm({
inst._zod.parse = (payload, ctx) => {
value2 ?? (value2 = _normalized.value);
const input = payload.value;
if (!isObject4(input)) {
if (!isObject5(input)) {
payload.issues.push({
expected: "object",
code: "invalid_type",
@@ -56273,8 +56273,8 @@ var require_snapshot_utils = __commonJS({
var require_snapshot_recorder = __commonJS({
"node_modules/.pnpm/undici@7.16.0/node_modules/undici/lib/mock/snapshot-recorder.js"(exports, module) {
"use strict";
var { writeFile, readFile, mkdir } = __require("node:fs/promises");
var { dirname: dirname3, resolve: resolve3 } = __require("node:path");
var { writeFile: writeFile2, readFile, mkdir } = __require("node:fs/promises");
var { dirname: dirname4, resolve: resolve3 } = __require("node:path");
var { setTimeout: setTimeout2, clearTimeout: clearTimeout2 } = __require("node:timers");
var { InvalidArgumentError, UndiciError } = require_errors4();
var { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = require_snapshot_utils();
@@ -56498,12 +56498,12 @@ var require_snapshot_recorder = __commonJS({
throw new InvalidArgumentError("Snapshot path is required");
}
const resolvedPath = resolve3(path3);
await mkdir(dirname3(resolvedPath), { recursive: true });
await mkdir(dirname4(resolvedPath), { recursive: true });
const data = Array.from(this.#snapshots.entries()).map(([hash2, snapshot2]) => ({
hash: hash2,
snapshot: snapshot2
}));
await writeFile(resolvedPath, JSON.stringify(data, null, 2), { flush: true });
await writeFile2(resolvedPath, JSON.stringify(data, null, 2), { flush: true });
}
/**
* Clears all recorded snapshots
@@ -72140,7 +72140,7 @@ var require_lodash = __commonJS({
var symbolProto = Symbol2 ? Symbol2.prototype : void 0;
var symbolToString = symbolProto ? symbolProto.toString : void 0;
function baseIsRegExp(value2) {
return isObject4(value2) && objectToString.call(value2) == regexpTag;
return isObject5(value2) && objectToString.call(value2) == regexpTag;
}
function baseSlice(array3, start, end) {
var index = -1, length = array3.length;
@@ -72174,7 +72174,7 @@ var require_lodash = __commonJS({
end = end === void 0 ? length : end;
return !start && end >= length ? array3 : baseSlice(array3, start, end);
}
function isObject4(value2) {
function isObject5(value2) {
var type2 = typeof value2;
return !!value2 && (type2 == "object" || type2 == "function");
}
@@ -72207,9 +72207,9 @@ var require_lodash = __commonJS({
if (isSymbol(value2)) {
return NAN;
}
if (isObject4(value2)) {
if (isObject5(value2)) {
var other = typeof value2.valueOf == "function" ? value2.valueOf() : value2;
value2 = isObject4(other) ? other + "" : other;
value2 = isObject5(other) ? other + "" : other;
}
if (typeof value2 != "string") {
return value2 === 0 ? value2 : +value2;
@@ -72223,7 +72223,7 @@ var require_lodash = __commonJS({
}
function truncate(string6, options) {
var length = DEFAULT_TRUNC_LENGTH, omission = DEFAULT_TRUNC_OMISSION;
if (isObject4(options)) {
if (isObject5(options)) {
var separator2 = "separator" in options ? options.separator : separator2;
length = "length" in options ? toInteger(options.length) : length;
omission = "omission" in options ? baseToString2(options.omission) : omission;
@@ -95647,14 +95647,14 @@ var require_turndown_cjs = __commonJS({
} else if (node2.nodeType === 1) {
replacement = replacementForNode.call(self2, node2);
}
return join17(output, replacement);
return join18(output, replacement);
}, "");
}
function postProcess(output) {
var self2 = this;
this.rules.forEach(function(rule) {
if (typeof rule.append === "function") {
output = join17(output, rule.append(self2.options));
output = join18(output, rule.append(self2.options));
}
});
return output.replace(/^[\t\r\n]+/, "").replace(/[\t\r\n\s]+$/, "");
@@ -95666,7 +95666,7 @@ var require_turndown_cjs = __commonJS({
if (whitespace.leading || whitespace.trailing) content = content.trim();
return whitespace.leading + rule.replacement(content, node2, this.options) + whitespace.trailing;
}
function join17(output, replacement) {
function join18(output, replacement) {
var s1 = trimTrailingNewlines(output);
var s2 = trimLeadingNewlines(replacement);
var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
@@ -97612,7 +97612,7 @@ var require_semver2 = __commonJS({
// entry.ts
var core5 = __toESM(require_core(), 1);
import { dirname as dirname2 } from "node:path";
import { dirname as dirname3 } from "node:path";
// node_modules/.pnpm/@ark+util@0.56.0/node_modules/@ark/util/out/arrays.js
var liftArray = (data) => Array.isArray(data) ? data : [data];
@@ -136544,7 +136544,7 @@ function AskQuestionTool(ctx) {
// mcp/checkout.ts
import { writeFileSync as writeFileSync2 } from "node:fs";
import { join as join2 } from "node:path";
import { join as join3 } from "node:path";
// utils/gitAuth.ts
import { execSync as execSync2, spawnSync } from "node:child_process";
@@ -136583,6 +136583,8 @@ function exitWithSignal(signal) {
// utils/github.ts
var core2 = __toESM(require_core(), 1);
import { createSign } from "node:crypto";
import { rename, writeFile } from "node:fs/promises";
import { dirname, join as join2 } from "node:path";
// node_modules/.pnpm/@octokit+plugin-throttling@11.0.3_@octokit+core@7.0.5/node_modules/@octokit/plugin-throttling/dist-bundle/index.js
var import_light = __toESM(require_light(), 1);
@@ -140291,6 +140293,9 @@ async function retry(fn2, options = {}) {
}
// utils/github.ts
function isObject4(value2) {
return typeof value2 === "object" && value2 !== null;
}
function isOIDCAvailable() {
return Boolean(
process.env.ACTIONS_ID_TOKEN_REQUEST_URL && process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN
@@ -140451,9 +140456,35 @@ function parseRepoContext() {
}
return { owner, name };
}
function emptyResourceUsage() {
return {
requestCount: 0,
rateLimitRemaining: null,
rateLimitResetMs: null
};
}
var usageByResource = {
core: emptyResourceUsage(),
graphql: emptyResourceUsage()
};
function getGitHubUsageSummary() {
return {
version: 1,
github: {
core: usageByResource.core,
graphql: usageByResource.graphql
}
};
}
async function writeGitHubUsageSummaryToFile(path3) {
const summary2 = getGitHubUsageSummary();
const tmpPath = join2(dirname(path3), `.usage-summary-${process.pid}.tmp`);
await writeFile(tmpPath, JSON.stringify(summary2));
await rename(tmpPath, path3);
}
function createOctokit(token) {
const OctokitWithPlugins = Octokit2.plugin(throttling);
return new OctokitWithPlugins({
const octokit = new OctokitWithPlugins({
auth: token,
throttle: {
onRateLimit: (_retryAfter, _options, _octokit, retryCount) => {
@@ -140464,6 +140495,37 @@ function createOctokit(token) {
}
}
});
const onResponse = (response) => {
const resource = response.headers["x-ratelimit-resource"];
if (!resource) {
return response;
}
usageByResource[resource] ??= emptyResourceUsage();
const usage = usageByResource[resource];
usage.requestCount++;
const remaining = response.headers["x-ratelimit-remaining"];
const reset = response.headers["x-ratelimit-reset"];
if (remaining !== void 0) {
usage.rateLimitRemaining = Number(remaining);
}
if (reset !== void 0) {
usage.rateLimitResetMs = Number(reset) * 1e3;
}
return response;
};
octokit.hook.wrap("request", async (request2, options) => {
try {
const response = await request2(options);
onResponse(response);
return response;
} catch (error49) {
if (isObject4(error49) && "response" in error49 && isObject4(error49.response) && "headers" in error49.response && isObject4(error49.response.headers)) {
onResponse(error49.response);
}
throw error49;
}
});
return octokit;
}
// utils/token.ts
@@ -141081,7 +141143,7 @@ ${diffPreview}`);
"PULLFROG_TEMP_DIR not set - checkout_pr must run in pullfrog action context"
);
}
const diffPath = join2(tempDir, `pr-${pull_number}.diff`);
const diffPath = join3(tempDir, `pr-${pull_number}.diff`);
writeFileSync2(diffPath, formatResult.content);
log.debug(`wrote diff to ${diffPath} (${formatResult.content.length} bytes)`);
return {
@@ -141105,7 +141167,7 @@ ${diffPreview}`);
// mcp/checkSuite.ts
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "node:fs";
import { join as join3 } from "node:path";
import { join as join4 } from "node:path";
var GetCheckSuiteLogs = type({
check_suite_id: type.number.describe("the id from check_suite.id")
});
@@ -141200,7 +141262,7 @@ function GetCheckSuiteLogsTool(ctx) {
if (!tempDir) {
throw new Error("PULLFROG_TEMP_DIR not set");
}
const logsDir = join3(tempDir, "ci-logs");
const logsDir = join4(tempDir, "ci-logs");
mkdirSync2(logsDir, { recursive: true });
const jobResults = [];
for (const run2 of failedRuns) {
@@ -141219,7 +141281,7 @@ function GetCheckSuiteLogsTool(ctx) {
});
const logsUrl = logsResponse.url;
const logsText = await fetch(logsUrl).then((r) => r.text());
const logPath = join3(logsDir, `job-${job.id}.log`);
const logPath = join4(logsDir, `job-${job.id}.log`);
writeFileSync3(logPath, logsText);
const analysis = analyzeLog(logsText, 80);
const failedSteps = job.steps?.filter((s) => s.conclusion === "failure").map((s) => `Step ${s.number}: ${s.name}`) ?? [];
@@ -141556,7 +141618,7 @@ function ReplyToReviewCommentTool(ctx) {
// mcp/commitInfo.ts
import { writeFileSync as writeFileSync4 } from "node:fs";
import { join as join4 } from "node:path";
import { join as join5 } from "node:path";
var CommitInfo = type({
sha: type.string.describe("the commit SHA (full or abbreviated) to fetch")
});
@@ -141580,7 +141642,7 @@ function CommitInfoTool(ctx) {
"PULLFROG_TEMP_DIR not set - get_commit_info must run in pullfrog action context"
);
}
const diffFile = join4(tempDir, `commit-${sha.slice(0, 7)}.diff`);
const diffFile = join5(tempDir, `commit-${sha.slice(0, 7)}.diff`);
writeFileSync4(diffFile, formatResult.content);
log.debug(`wrote commit diff to ${diffFile} (${formatResult.content.length} bytes)`);
return {
@@ -141688,7 +141750,7 @@ import { performance as performance4 } from "node:perf_hooks";
// prep/installNodeDependencies.ts
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
import { join as join5 } from "node:path";
import { join as join6 } from "node:path";
// node_modules/.pnpm/package-manager-detector@1.6.0/node_modules/package-manager-detector/dist/commands.mjs
function dashDashArg(agent2, agentCommand) {
@@ -142000,7 +142062,7 @@ async function isCommandAvailable(command) {
return result.exitCode === 0;
}
function getPackageManagerFromPackageJson() {
const packageJsonPath = join5(process.cwd(), "package.json");
const packageJsonPath = join6(process.cwd(), "package.json");
try {
const content = readFileSync2(packageJsonPath, "utf-8");
const pkg = JSON.parse(content);
@@ -142031,7 +142093,7 @@ async function installPackageManager(name, installSpec) {
return result.stderr || `failed to install ${name}`;
}
if (name === "deno") {
const denoPath = join5(process.env.HOME || "", ".deno", "bin");
const denoPath = join6(process.env.HOME || "", ".deno", "bin");
process.env.PATH = `${denoPath}:${process.env.PATH}`;
}
log.info(`\xBB installed ${name}`);
@@ -142040,7 +142102,7 @@ async function installPackageManager(name, installSpec) {
var installNodeDependencies = {
name: "installNodeDependencies",
shouldRun: () => {
const packageJsonPath = join5(process.cwd(), "package.json");
const packageJsonPath = join6(process.cwd(), "package.json");
return existsSync2(packageJsonPath);
},
run: async (options) => {
@@ -142125,7 +142187,7 @@ ${errorMessage}`]
// prep/installPythonDependencies.ts
import { existsSync as existsSync3 } from "node:fs";
import { join as join6 } from "node:path";
import { join as join7 } from "node:path";
var PYTHON_CONFIGS = [
{
file: "requirements.txt",
@@ -142197,11 +142259,11 @@ var installPythonDependencies = {
return false;
}
const cwd = process.cwd();
return PYTHON_CONFIGS.some((config3) => existsSync3(join6(cwd, config3.file)));
return PYTHON_CONFIGS.some((config3) => existsSync3(join7(cwd, config3.file)));
},
run: async (options) => {
const cwd = process.cwd();
const config3 = PYTHON_CONFIGS.find((c) => existsSync3(join6(cwd, c.file)));
const config3 = PYTHON_CONFIGS.find((c) => existsSync3(join7(cwd, c.file)));
if (!config3) {
return {
language: "python",
@@ -142446,7 +142508,7 @@ import {
unlinkSync,
writeFileSync as writeFileSync5
} from "node:fs";
import { dirname, join as join7, resolve } from "node:path";
import { dirname as dirname2, join as join8, resolve } from "node:path";
var FileReadParams = type({
path: "string",
"offset?": "number",
@@ -142478,7 +142540,7 @@ function resolveReadPath(filePath) {
}
const home = process.env.HOME;
if (home) {
const cursorProjectsDir = join7(home, ".cursor", "projects");
const cursorProjectsDir = join8(home, ".cursor", "projects");
if (resolved.startsWith(cursorProjectsDir + "/")) {
return resolved;
}
@@ -142505,9 +142567,9 @@ function resolveWritePath(filePath, shellPermission) {
throw new Error(`path must be within the repository (symlink escape blocked): ${filePath}`);
}
} else {
let ancestor = dirname(resolved);
let ancestor = dirname2(resolved);
while (!existsSync4(ancestor)) {
const parent = dirname(ancestor);
const parent = dirname2(ancestor);
if (parent === ancestor) break;
ancestor = parent;
}
@@ -142566,7 +142628,7 @@ function FileWriteTool(ctx) {
parameters: FileWriteParams,
execute: execute(async (params) => {
const resolved = resolveWritePath(params.path, ctx.payload.shell);
const dir = dirname(resolved);
const dir = dirname2(resolved);
mkdirSync3(dir, { recursive: true });
writeFileSync5(resolved, params.content, "utf-8");
return { path: params.path, written: true };
@@ -143289,11 +143351,16 @@ function CreatePullRequestReviewTool(ctx) {
parameters: CreatePullRequestReview,
execute: execute(async ({ pull_number, body, approved, commit_id, comments = [] }) => {
ctx.toolState.issueNumber = pull_number;
let event = approved ? "APPROVE" : "COMMENT";
if (event === "APPROVE" && !ctx.prApproveEnabled) {
log.info("prApproveEnabled is disabled \u2014 downgrading APPROVE to COMMENT");
event = "COMMENT";
}
const params = {
owner: ctx.repo.owner,
repo: ctx.repo.name,
pull_number,
event: approved ? "APPROVE" : "COMMENT"
event
};
if (body) params.body = body;
if (commit_id) {
@@ -143331,6 +143398,8 @@ function CreatePullRequestReviewTool(ctx) {
throw new Error(`createReview returned invalid data: ${JSON.stringify(result.data)}`);
}
const reviewId = result.data.id;
const reviewNodeId = result.data.node_id;
await reportReviewNodeId(ctx, reviewNodeId);
const customParts = [];
if (!approved) {
if (comments.length > 0) {
@@ -143373,10 +143442,38 @@ function CreatePullRequestReviewTool(ctx) {
})
});
}
async function reportReviewNodeId(ctx, reviewNodeId) {
for (let remaining = 2; remaining >= 0; remaining--) {
try {
const response = await apiFetch({
path: `/api/workflow-run/${ctx.runId}`,
method: "PATCH",
headers: {
authorization: `Bearer ${ctx.apiToken}`,
"content-type": "application/json"
},
body: JSON.stringify({ reviewNodeId }),
signal: AbortSignal.timeout(1e4)
});
if (response.ok) return;
if (remaining > 0) {
log.debug(`reportReviewNodeId got ${response.status}, retrying (${remaining} left)`);
await new Promise((r) => setTimeout(r, 2e3));
}
} catch (error49) {
if (remaining > 0) {
log.debug(`reportReviewNodeId failed, retrying (${remaining} left): ${error49}`);
await new Promise((r) => setTimeout(r, 2e3));
} else {
log.debug(`reportReviewNodeId exhausted retries: ${error49}`);
}
}
}
}
// mcp/reviewComments.ts
import { writeFileSync as writeFileSync6 } from "node:fs";
import { join as join8 } from "node:path";
import { join as join9 } from "node:path";
var REVIEW_THREADS_QUERY = `
query ($owner: String!, $name: String!, $prNumber: Int!) {
repository(owner: $owner, name: $name) {
@@ -143746,7 +143843,7 @@ function GetReviewCommentsTool(ctx) {
throw new Error("PULLFROG_TEMP_DIR not set");
}
const filename = `review-${params.review_id}-threads.md`;
const commentsPath = join8(tempDir, filename);
const commentsPath = join9(tempDir, filename);
writeFileSync6(commentsPath, formatted.content);
log.debug(`wrote ${threadBlocks.length} threads to ${commentsPath}`);
return {
@@ -143840,7 +143937,7 @@ function ResolveReviewThreadTool(ctx) {
// mcp/selectMode.ts
var SelectModeParams = type({
mode: type.string.describe(
"the name of the mode to select (e.g., 'Build', 'Plan', 'Review', 'Fix', 'AddressReviews', 'Task')"
"the name of the mode to select (e.g., 'Build', 'Plan', 'Review', 'IncrementalReview', 'Fix', 'AddressReviews', 'Task')"
)
});
function resolveMode(modes2, modeName) {
@@ -143919,9 +144016,37 @@ Each task in the \`tasks\` array should include:
After all tasks complete, consolidate into a **single** review:
- merge the \`comments\` arrays from all subagent outputs
- submit one \`${ghPullfrogMcpName}/create_pull_request_review\` with the merged comments and a unified summary body
- if subagents found actionable issues: submit one \`${ghPullfrogMcpName}/create_pull_request_review\` with \`approved: false\`, the merged comments, and a unified summary body
- if no subagent found actionable issues: submit with \`approved: true\` and a brief positive summary (no inline comments)
- call \`${ghPullfrogMcpName}/report_progress\` with the summary
Use max effort for thorough reviews.`,
IncrementalReview: `### Checklist
1. Checkout the PR via \`${ghPullfrogMcpName}/checkout_pr\` \u2014 this returns PR metadata and a \`diffPath\`. Read the diff to identify the major areas of change.
2. Generate the incremental diff using the \`before_sha\` from EVENT DATA: \`git diff <before_sha>...HEAD\`. This isolates only the new commits. If the command fails (e.g., force-push rewrote history), fall back to reviewing the full PR diff.
3. Fetch previous reviews via \`${ghPullfrogMcpName}/list_pull_request_reviews\`. For the most recent Pullfrog review, call \`${ghPullfrogMcpName}/get_review_comments\` with the review ID to retrieve specific prior line-level feedback. Include the prior review summary and comment details when crafting subagent tasks.
4. Delegate multiple subagents in a single \`${ghPullfrogMcpName}/delegate\` call, each focused on a specific area of the new changes. Provide both the full diff path and the incremental diff.
5. After all subagents return, consolidate their findings into a single review.
### Crafting each task
Each task in the \`tasks\` array should include:
- the full diff file path AND the incremental diff (so the subagent can see both new changes and full context)
- what specific area/aspect to focus on
- instruct it to prioritize reviewing code in the incremental diff while using the full diff for context and to catch any changes not covered by the incremental diff
- include the prior review comments (from step 3) so the subagent knows what feedback was already given \u2014 instruct it to avoid repeating prior issues and to note whether prior feedback was addressed by the new commits
- instruct it to actively hunt for problems: trace data flow, check boundaries, explore failure modes, verify assumptions, consider lifecycle, spot performance issues
- draft inline comments with NEW line numbers from the full PR diff \u2014 every comment must be actionable (2-3 sentences max)
- call \`${ghPullfrogMcpName}/set_output\` with a JSON object: \`{ "summary": "...", "comments": [{ "path": "file.ts", "line": 42, "body": "..." }, ...] }\`
### Post-delegation
After all tasks complete, consolidate into a **single** review:
- merge the \`comments\` arrays from all subagent outputs
- if subagents found actionable issues: submit one \`${ghPullfrogMcpName}/create_pull_request_review\` with \`approved: false\`, the merged comments, and a unified summary body
- if no subagent found actionable issues: submit with \`approved: true\` and a brief positive summary (no inline comments)
- call \`${ghPullfrogMcpName}/report_progress\` with the summary
- if no subagent found actionable issues, skip the review \u2014 just call \`report_progress\` noting the PR was reviewed
Use max effort for thorough reviews.`,
Plan: `### Checklist
@@ -144010,7 +144135,7 @@ import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
import { randomUUID as randomUUID3 } from "node:crypto";
import { closeSync, openSync, writeFileSync as writeFileSync7 } from "node:fs";
import { userInfo } from "node:os";
import { join as join9 } from "node:path";
import { join as join10 } from "node:path";
var ShellParams = type({
command: "string",
description: "string",
@@ -144144,8 +144269,8 @@ Do NOT use this tool for git commands \u2014 use the dedicated git tools instead
if (params.background) {
const tempDir = getTempDir();
const handle = `bg-${randomUUID3().slice(0, 8)}`;
const outputPath = join9(tempDir, `${handle}.log`);
const pidPath = join9(tempDir, `${handle}.pid`);
const outputPath = join10(tempDir, `${handle}.log`);
const pidPath = join10(tempDir, `${handle}.pid`);
const logFd = openSync(outputPath, "a");
let proc2;
try {
@@ -144571,7 +144696,7 @@ function computeModes() {
2. **DEPENDENCIES** - ${dependencyInstallationStep}
3. **FETCH COMMENTS** - Fetch review comments using ${ghPullfrogMcpName}/get_review_comments with \`pull_number\` and \`review_id\` from EVENT DATA. This returns \`commentsPath\` - read that file for full comment details with diff context. If EVENT DATA contains a \`triggerer\` field (indicating who requested fixes), you can pass \`approved_by\` to filter to only comments they approved with \u{1F44D}.
3. **FETCH COMMENTS** - Fetch review comments using ${ghPullfrogMcpName}/get_review_comments with \`pull_number\` and \`review_id\` from EVENT DATA. This returns \`commentsPath\` - read that file for full comment details with diff context. When \`approved_only\` is set in EVENT DATA, only approved comments are returned automatically.
4. **UNDERSTAND** - Review the feedback provided. Understand each review comment and what changes are being requested.
@@ -144592,7 +144717,7 @@ Keep the progress comment extremely brief. The summary should be 1-2 sentences m
{
name: "Review",
description: "Review code, PRs, or implementations; provide feedback or suggestions; identify issues; or check code quality, style, and correctness",
prompt: `Follow these steps to review the PR. Your job is to find problems\u2014assume they exist until you've proven otherwise. Do not submit a clean review without thorough investigation. **If you have nothing interesting to say, do NOT submit a review at all\u2014use \`report_progress\` instead.**
prompt: `Follow these steps to review the PR. Your job is to find problems\u2014assume they exist until you've proven otherwise. Do not submit a clean review without thorough investigation.
1. **CHECKOUT** - Call ${ghPullfrogMcpName}/checkout_pr with the PR number. This should give you all PR metadata you need, including a \`diffPath\`: a path to a temp file containing the PR diff.
@@ -144610,15 +144735,50 @@ Keep the progress comment extremely brief. The summary should be 1-2 sentences m
- **Check PR consistency**: Does the PR title/description match the actual code changes? Flag significant discrepancies.
- Do NOT stop at "this looks reasonable." Dig until you either find a problem or have concrete evidence there isn't one.
4. **DRAFT LINE-BY-LINE COMMENTS** - Every comment must be actionable: the author should need to change something in response. 2-3 sentences max. Use the NEW line number from the diff (second column: \`| OLD | NEW | TYPE | CODE\`). If no issues found, skip to step 5. NO COMPLIMENTS. NO NITPICKING ABOUT CHANGES UNRELATED TO THE MAIN CHANGE. Non-actionable comments (praise, style preferences, minor optimizatfixons, documentation nits) must not be drafted. If no comments survive and you have no significant concerns, **do not submit a review**. Use \`${ghPullfrogMcpName}/report_progress\` to note the PR was reviewed and no issues were found.
4. **DRAFT LINE-BY-LINE COMMENTS** - Every comment must be actionable: the author should need to change something in response. 2-3 sentences max. Use the NEW line number from the diff (second column: \`| OLD | NEW | TYPE | CODE\`). If no issues found, skip to step 5. NO COMPLIMENTS. NO NITPICKING ABOUT CHANGES UNRELATED TO THE MAIN CHANGE. Non-actionable comments (praise, style preferences, minor optimizatfixons, documentation nits) must not be drafted.
5. **WRITE SUMMARY** - Draft a 1-3 sentence summary for the review body. Include urgency level and any concerns about code outside the diff.
5. **WRITE SUMMARY** - Draft a 1-3 sentence summary for the review body. If issues were found, include urgency level and any concerns about code outside the diff. If no issues were found, write a brief approval summary (e.g., "Changes look good. No issues found.").
6. **SUBMIT** \u2014 Use ${ghPullfrogMcpName}/create_pull_request_review:
6. **SUBMIT** \u2014 Always submit a review via ${ghPullfrogMcpName}/create_pull_request_review:
- \`body\`: The summary from step 5
- \`comments\`: The inline comments from step 4
- \`approved\`: Set to \`true\` ONLY if the review contains no actionable feedback \u2014 neither inline comments nor actionable content in the body. An approval signals "no changes needed."
${permalinkTip}
`
},
{
name: "IncrementalReview",
description: "Re-review a PR after new commits are pushed; focus on new changes since the last review",
prompt: `Follow these steps to incrementally re-review the PR after new commits were pushed. Focus on what changed since the last review.
1. **CHECKOUT** - Call ${ghPullfrogMcpName}/checkout_pr with the PR number. This gives you the full PR diff via \`diffPath\`.
2. **INCREMENTAL DIFF** - EVENT DATA contains \`before_sha\` (the HEAD before this push). Generate the incremental diff:
\`git diff <before_sha>...HEAD\`
This shows the changes introduced by this push. Cross-reference with previous reviews (step 3) to confirm coverage of all unreviewed changes \u2014 the full PR diff fills any gaps.
**If the diff command fails** (e.g., force-push rewrote history), fall back to reviewing the full PR diff from step 1.
3. **FETCH PREVIOUS REVIEWS** - Use ${ghPullfrogMcpName}/list_pull_request_reviews to find previous Pullfrog reviews. For the most recent one, call ${ghPullfrogMcpName}/get_review_comments with the review ID to see specific line-level feedback. This lets you avoid repeating issues and assess whether prior feedback was addressed by the new commits.
4. **ANALYZE** - Read the incremental diff to understand the new changes. Use the full PR diff for surrounding context and to catch any changes not covered by the incremental diff.
- **Understand the change**: What is new or modified since the last review?
- **Evaluate the approach**: Are the new changes sound? Do they address prior feedback?
5. **INVESTIGATE** - Hunt for problems in the new code using the same techniques as a full review:
- Trace data flow, check boundaries, explore failure modes, verify assumptions, consider lifecycle, spot performance issues.
- Focus investigation on code that changed in the incremental diff, but trace its effects through the broader codebase.
- Do NOT repeat feedback already given in previous reviews unless it was not addressed.
6. **DRAFT LINE-BY-LINE COMMENTS** - Every comment must be actionable. 2-3 sentences max. Use the NEW line number from the full PR diff. NO COMPLIMENTS. NO NITPICKING.
7. **WRITE SUMMARY** - Draft a 1-3 sentence summary for the review body. Focus on what changed since the last review and whether the new changes are sound. If issues were found, include urgency level. If no issues were found, write a brief approval summary.
8. **SUBMIT** \u2014 Use ${ghPullfrogMcpName}/create_pull_request_review:
- \`body\`: The summary from step 7
- \`comments\`: The inline comments from step 6
- \`approved\`: Set to \`true\` ONLY if the review contains no actionable feedback \u2014 neither inline comments nor actionable content in the body. An approval signals "no changes needed."
${permalinkTip}
`
},
@@ -144736,12 +144896,12 @@ var modes = computeModes();
// agents/claude.ts
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "node:fs";
import { join as join11 } from "node:path";
import { join as join12 } from "node:path";
// package.json
var package_default = {
name: "@pullfrog/pullfrog",
version: "0.0.174",
version: "0.0.175",
type: "module",
files: [
"index.js",
@@ -144834,13 +144994,13 @@ import { spawnSync as spawnSync4 } from "node:child_process";
import { chmodSync, createWriteStream, existsSync as existsSync5, mkdirSync as mkdirSync4 } from "node:fs";
import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join as join10 } from "node:path";
import { join as join11 } from "node:path";
import { pipeline } from "node:stream/promises";
async function installFromNpmTarball(params) {
const tempDir = process.env.PULLFROG_TEMP_DIR;
if (!tempDir) throw new Error("PULLFROG_TEMP_DIR is not set");
const extractedDir = join10(tempDir, "package");
const cliPath = join10(extractedDir, params.executablePath);
const extractedDir = join11(tempDir, "package");
const cliPath = join11(extractedDir, params.executablePath);
if (existsSync5(cliPath)) {
log.debug(`\xBB using cached binary at ${cliPath}`);
return cliPath;
@@ -144865,7 +145025,7 @@ async function installFromNpmTarball(params) {
}
}
log.debug(`\xBB installing ${params.packageName}@${resolvedVersion}...`);
const tarballPath = join10(tempDir, "package.tgz");
const tarballPath = join11(tempDir, "package.tgz");
const npmRegistry = process.env.NPM_REGISTRY || "https://registry.npmjs.org";
let tarballUrl;
if (params.packageName.startsWith("@")) {
@@ -144939,8 +145099,8 @@ async function fetchWithRetry(url4, headers, errorMessage) {
}
async function installFromGithub(params) {
const pullfrogTemp = process.env.PULLFROG_TEMP_DIR;
const installDir = pullfrogTemp ? join10(pullfrogTemp, `github-${params.owner}-${params.repo}`) : await mkdtemp(join10(tmpdir(), `${params.owner}-${params.repo}-github-`));
const expectedCliPath = join10(installDir, params.executablePath ?? params.assetName ?? "asset");
const installDir = pullfrogTemp ? join11(pullfrogTemp, `github-${params.owner}-${params.repo}`) : await mkdtemp(join11(tmpdir(), `${params.owner}-${params.repo}-github-`));
const expectedCliPath = join11(installDir, params.executablePath ?? params.assetName ?? "asset");
if (existsSync5(expectedCliPath)) {
log.debug(`\xBB using cached binary at ${expectedCliPath}`);
return expectedCliPath;
@@ -144964,13 +145124,13 @@ async function installFromGithub(params) {
mkdirSync4(installDir, { recursive: true });
const urlPath = new URL(assetUrl).pathname;
const fileName2 = urlPath.split("/").pop() || "asset";
const downloadPath = join10(installDir, fileName2);
const downloadPath = join11(installDir, fileName2);
const assetResponse = await fetchWithRetry(assetUrl, headers, "Failed to download asset");
if (!assetResponse.body) throw new Error("Response body is null");
const fileStream = createWriteStream(downloadPath);
await pipeline(assetResponse.body, fileStream);
log.debug(`\xBB downloaded asset to ${downloadPath}`);
const cliPath = params.executablePath ? join10(installDir, params.executablePath) : downloadPath;
const cliPath = params.executablePath ? join11(installDir, params.executablePath) : downloadPath;
if (!existsSync5(cliPath)) {
throw new Error(`Executable not found at ${cliPath}`);
}
@@ -144981,14 +145141,14 @@ async function installFromGithub(params) {
async function installFromDirectTarball(params) {
const tempDir = process.env.PULLFROG_TEMP_DIR;
if (!tempDir) throw new Error("PULLFROG_TEMP_DIR is not set");
const extractDir = join10(tempDir, "direct-package");
const cliPath = join10(extractDir, params.executablePath);
const extractDir = join11(tempDir, "direct-package");
const cliPath = join11(extractDir, params.executablePath);
if (existsSync5(cliPath)) {
log.debug(`\xBB using cached binary at ${cliPath}`);
return cliPath;
}
log.info(`\xBB downloading tarball from ${params.url}...`);
const tarballPath = join10(tempDir, "direct-package.tgz");
const tarballPath = join11(tempDir, "direct-package.tgz");
const response = await fetchWithRetry(params.url, {}, "failed to download tarball");
if (!response.body) throw new Error("response body is null");
const fileStream = createWriteStream(tarballPath);
@@ -145100,9 +145260,9 @@ function buildDisallowedTools(ctx) {
return disallowed;
}
function writeMcpConfig(ctx) {
const configDir = join11(ctx.tmpdir, ".claude");
const configDir = join12(ctx.tmpdir, ".claude");
mkdirSync5(configDir, { recursive: true });
const configPath = join11(configDir, "mcp.json");
const configPath = join12(configDir, "mcp.json");
const mcpConfig = {
mcpServers: {
[ghPullfrogMcpName]: { type: "http", url: ctx.mcpServerUrl }
@@ -145319,7 +145479,7 @@ var messageHandlers = {
// agents/codex.ts
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync9 } from "node:fs";
import { join as join12 } from "node:path";
import { join as join13 } from "node:path";
var CODEX_CLI_VERSION = "0.101.0";
var PREFERRED_MODEL = "gpt-5.3-codex";
var FALLBACK_MODEL = "gpt-5.2-codex";
@@ -145359,9 +145519,9 @@ async function resolveModel(apiKey) {
return FALLBACK_MODEL;
}
function writeCodexConfig(ctx) {
const codexDir = join12(ctx.tmpdir, ".codex");
const codexDir = join13(ctx.tmpdir, ".codex");
mkdirSync6(codexDir, { recursive: true });
const configPath = join12(codexDir, "config.toml");
const configPath = join13(codexDir, "config.toml");
log.info(`\xBB adding MCP server '${ghPullfrogMcpName}' at ${ctx.mcpServerUrl}`);
const mcpServerSections = [`[mcp_servers.${ghPullfrogMcpName}]
url = "${ctx.mcpServerUrl}"`];
@@ -145613,7 +145773,7 @@ function createMessageHandlers() {
import { spawn as spawn3 } from "node:child_process";
import { existsSync as existsSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync10 } from "node:fs";
import { homedir } from "node:os";
import { join as join13 } from "node:path";
import { join as join14 } from "node:path";
import { performance as performance6 } from "node:perf_hooks";
var CURSOR_CLI_VERSION = "2026.01.28-fd13201";
var cursorEffortModels = {
@@ -145643,7 +145803,7 @@ var cursor = agent({
const cliPath = await installCursor();
configureCursorMcpServers(ctx);
configureCursorTools(ctx);
const projectCliConfigPath = join13(process.cwd(), ".cursor", "cli.json");
const projectCliConfigPath = join14(process.cwd(), ".cursor", "cli.json");
let modelOverride = null;
if (existsSync6(projectCliConfigPath)) {
try {
@@ -145828,11 +145988,11 @@ var cursor = agent({
}
});
function getCursorConfigDir() {
return join13(homedir(), ".cursor");
return join14(homedir(), ".cursor");
}
function configureCursorMcpServers(ctx) {
const cursorConfigDir = getCursorConfigDir();
const mcpConfigPath = join13(cursorConfigDir, "mcp.json");
const mcpConfigPath = join14(cursorConfigDir, "mcp.json");
mkdirSync7(cursorConfigDir, { recursive: true });
const mcpServers = {
[ghPullfrogMcpName]: { type: "http", url: ctx.mcpServerUrl }
@@ -145842,7 +146002,7 @@ function configureCursorMcpServers(ctx) {
}
function configureCursorTools(ctx) {
const cursorConfigDir = getCursorConfigDir();
const cliConfigPath = join13(cursorConfigDir, "cli-config.json");
const cliConfigPath = join14(cursorConfigDir, "cli-config.json");
mkdirSync7(cursorConfigDir, { recursive: true });
const shell = ctx.payload.shell;
const deny = [];
@@ -145871,7 +146031,7 @@ function configureCursorTools(ctx) {
// agents/gemini.ts
import { mkdirSync as mkdirSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync11 } from "node:fs";
import { homedir as homedir2 } from "node:os";
import { join as join14 } from "node:path";
import { join as join15 } from "node:path";
var geminiEffortConfig = {
// https://ai.google.dev/gemini-api/docs/models
// the docs mention needing to enable preview features for these models but if you
@@ -146095,8 +146255,8 @@ function configureGeminiSettings(ctx) {
const thinkingLevel = effortConfig.thinkingLevel;
log.info(`\xBB model: ${model} (thinkingLevel: ${thinkingLevel})`);
const realHome = homedir2();
const geminiConfigDir = join14(realHome, ".gemini");
const settingsPath = join14(geminiConfigDir, "settings.json");
const geminiConfigDir = join15(realHome, ".gemini");
const settingsPath = join15(geminiConfigDir, "settings.json");
mkdirSync8(geminiConfigDir, { recursive: true });
let existingSettings = {};
try {
@@ -146143,7 +146303,7 @@ function configureGeminiSettings(ctx) {
// agents/opencode.ts
import { existsSync as existsSync7, mkdirSync as mkdirSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync12 } from "node:fs";
import { join as join15 } from "node:path";
import { join as join16 } from "node:path";
import { performance as performance7 } from "node:perf_hooks";
var OPENCODE_CLI_VERSION = "1.1.56";
var PROVIDER_ERROR_PATTERNS = [
@@ -146272,7 +146432,7 @@ var opencode = agent({
run: async (ctx) => {
const cliPath = await installOpencode();
const tempHome = ctx.tmpdir;
const configDir = join15(tempHome, ".config", "opencode");
const configDir = join16(tempHome, ".config", "opencode");
mkdirSync9(configDir, { recursive: true });
configureOpenCode(ctx);
const args2 = ["run", ctx.instructions.full, "--format", "json", "--print-logs"];
@@ -146290,7 +146450,7 @@ var opencode = agent({
const env2 = {
...process.env,
HOME: tempHome,
XDG_CONFIG_HOME: join15(tempHome, ".config"),
XDG_CONFIG_HOME: join16(tempHome, ".config"),
// set GOOGLE_GENERATIVE_AI_API_KEY alias for Google provider compatibility (if not already set)
GOOGLE_GENERATIVE_AI_API_KEY: process.env.GOOGLE_GENERATIVE_AI_API_KEY || process.env.GEMINI_API_KEY
};
@@ -146473,10 +146633,10 @@ ${stderrContext}`
}
});
function configureOpenCode(ctx) {
const configDir = join15(ctx.tmpdir, ".config", "opencode");
const configDir = join16(ctx.tmpdir, ".config", "opencode");
mkdirSync9(configDir, { recursive: true });
const configPath = join15(configDir, "opencode.json");
const repoConfigPath = join15(process.cwd(), "opencode.json");
const configPath = join16(configDir, "opencode.json");
const repoConfigPath = join16(process.cwd(), "opencode.json");
const repoConfig = loadRepoOpenCodeConfig({ repoConfigPath });
if (repoConfig?.model) {
log.info(`\xBB repo opencode model configured: ${repoConfig.model}`);
@@ -146828,6 +146988,7 @@ async function fetchBodyHtml(ctx) {
})).data.body_html;
case "pull_request_opened":
case "pull_request_ready_for_review":
case "pull_request_synchronize":
case "pull_request_review_requested":
if (!event.issue_number) return;
return (await ctx.octokit.rest.issues.get({
@@ -147422,6 +147583,7 @@ function resolvePayload(resolvedPromptInput, repoSettings) {
effort: inputs.effort ?? jsonPayload?.effort ?? "auto",
timeout: inputs.timeout ?? jsonPayload?.timeout,
cwd: resolveCwd(inputs.cwd),
progressCommentId: jsonPayload?.progressCommentId,
debug: jsonPayload?.debug,
// permissions: inputs > repoSettings > fallbacks
web: inputs.web ?? repoSettings.web ?? "enabled",
@@ -147473,7 +147635,8 @@ var defaultSettings = {
web: "enabled",
search: "enabled",
push: "restricted",
shell: "restricted"
shell: "restricted",
prApproveEnabled: false
};
var defaultRunContext = {
settings: defaultSettings,
@@ -147540,9 +147703,9 @@ async function resolveRunContextData(params) {
import { execSync as execSync4 } from "node:child_process";
import { mkdtempSync } from "node:fs";
import { tmpdir as tmpdir2 } from "node:os";
import { join as join16 } from "node:path";
import { join as join17 } from "node:path";
function createTempDirectory() {
const sharedTempDir = mkdtempSync(join16(tmpdir2(), "pullfrog-"));
const sharedTempDir = mkdtempSync(join17(tmpdir2(), "pullfrog-"));
process.env.PULLFROG_TEMP_DIR = sharedTempDir;
log.info(`\xBB created temp dir at ${sharedTempDir}`);
return sharedTempDir;
@@ -147667,6 +147830,10 @@ async function main() {
var _stack2 = [];
try {
normalizeEnv();
const usageSummaryPath = process.env.PULLFROG_USAGE_SUMMARY_PATH;
if (usageSummaryPath) {
onExitSignal(() => writeGitHubUsageSummaryToFile(usageSummaryPath));
}
const timer = new Timer();
let activityTimeout = null;
const resolvedPromptInput = resolvePromptInput();
@@ -147741,6 +147908,7 @@ async function main() {
agent: agent2,
modes: modes2,
postCheckoutScript: runContext.repoSettings.postCheckoutScript,
prApproveEnabled: runContext.repoSettings.prApproveEnabled,
toolState,
runId: runInfo.runId,
jobId: runInfo.jobId,
@@ -147842,6 +148010,9 @@ ${instructions.user}` : null,
};
} finally {
activityTimeout?.stop();
if (usageSummaryPath) {
await writeGitHubUsageSummaryToFile(usageSummaryPath);
}
}
} catch (_2) {
var _error2 = _2, _hasError2 = true;
@@ -147852,7 +148023,7 @@ ${instructions.user}` : null,
}
// entry.ts
process.env.PATH = `${dirname2(process.execPath)}:${process.env.PATH}`;
process.env.PATH = `${dirname3(process.execPath)}:${process.env.PATH}`;
async function run() {
try {
const result = await main();
+12
View File
@@ -241,6 +241,17 @@ interface ImplementPlanEvent extends BasePayloadEvent {
body: string | null;
}
interface PullRequestSynchronizeEvent extends BasePayloadEvent {
trigger: "pull_request_synchronize";
issue_number: number;
is_pr: true;
title: string;
body: string | null;
branch: string;
/** SHA before the push -- used to compute incremental diff via `git diff before_sha...HEAD` */
before_sha: string;
}
interface UnknownEvent extends BasePayloadEvent {
trigger: "unknown";
}
@@ -250,6 +261,7 @@ interface UnknownEvent extends BasePayloadEvent {
export type PayloadEvent =
| PullRequestOpenedEvent
| PullRequestReadyForReviewEvent
| PullRequestSynchronizeEvent
| PullRequestReviewRequestedEvent
| PullRequestReviewSubmittedEvent
| PullRequestReviewCommentCreatedEvent
+12 -1
View File
@@ -25699,8 +25699,8 @@ function formatJsonValue(value) {
}
// utils/github.ts
var core2 = __toESM(require_core(), 1);
import { createSign } from "node:crypto";
var core2 = __toESM(require_core(), 1);
// utils/apiUrl.ts
function isLocalUrl(url) {
@@ -25930,6 +25930,17 @@ function parseRepoContext() {
}
return { owner, name };
}
function emptyResourceUsage() {
return {
requestCount: 0,
rateLimitRemaining: null,
rateLimitResetMs: null
};
}
var usageByResource = {
core: emptyResourceUsage(),
graphql: emptyResourceUsage()
};
// utils/token.ts
async function revokeGitHubInstallationToken(token) {
+3 -1
View File
@@ -20,6 +20,8 @@ export {
Effort,
ghPullfrogMcpName,
} from "../external.ts";
export type { Mode } from "../modes.ts";
export { modes } from "../modes.ts";
export type {
AgentInfo,
BuildPullfrogFooterParams,
@@ -30,7 +32,7 @@ export {
PULLFROG_DIVIDER,
stripExistingFooter,
} from "../utils/buildPullfrogFooter.ts";
export type { ResourceUsage, UsageSummary } from "../utils/github.ts";
export {
isValidTimeString,
parseTimeString,
+12 -1
View File
@@ -12,8 +12,9 @@ import { validateAgentApiKey } from "./utils/apiKeys.ts";
import { resolveBody } from "./utils/body.ts";
import { formatUsageSummary, log, writeSummary } from "./utils/cli.ts";
import { reportErrorToComment } from "./utils/errorReport.ts";
import { onExitSignal } from "./utils/exitHandler.ts";
import { resolveGit } from "./utils/gitAuth.ts";
import { createOctokit } from "./utils/github.ts";
import { createOctokit, writeGitHubUsageSummaryToFile } from "./utils/github.ts";
import { resolveInstructions } from "./utils/instructions.ts";
import { executeLifecycleHook } from "./utils/lifecycle.ts";
import { normalizeEnv } from "./utils/normalizeEnv.ts";
@@ -48,6 +49,12 @@ export async function main(): Promise<MainResult> {
// normalize env var names to uppercase (handles case-insensitive workflow files)
normalizeEnv();
// write usage summary on SIGINT/SIGTERM so the worker can read it after sandbox.exec
const usageSummaryPath = process.env.PULLFROG_USAGE_SUMMARY_PATH;
if (usageSummaryPath) {
onExitSignal(() => writeGitHubUsageSummaryToFile(usageSummaryPath));
}
const timer = new Timer();
let activityTimeout: ActivityTimeout | null = null;
@@ -155,6 +162,7 @@ export async function main(): Promise<MainResult> {
agent,
modes,
postCheckoutScript: runContext.repoSettings.postCheckoutScript,
prApproveEnabled: runContext.repoSettings.prApproveEnabled,
toolState,
runId: runInfo.runId,
jobId: runInfo.jobId,
@@ -266,5 +274,8 @@ export async function main(): Promise<MainResult> {
};
} finally {
activityTimeout?.stop();
if (usageSummaryPath) {
await writeGitHubUsageSummaryToFile(usageSummaryPath);
}
}
}
+46 -1
View File
@@ -1,5 +1,6 @@
import type { RestEndpointMethodTypes } from "@octokit/rest";
import { type } from "arktype";
import { apiFetch } from "../utils/apiFetch.ts";
import { getApiUrl } from "../utils/apiUrl.ts";
import { buildPullfrogFooter } from "../utils/buildPullfrogFooter.ts";
import { log } from "../utils/cli.ts";
@@ -73,12 +74,19 @@ export function CreatePullRequestReviewTool(ctx: ToolContext) {
// set issue context (PRs are issues)
ctx.toolState.issueNumber = pull_number;
// enforce prApproveEnabled: downgrade APPROVE to COMMENT if disabled
let event: "APPROVE" | "COMMENT" = approved ? "APPROVE" : "COMMENT";
if (event === "APPROVE" && !ctx.prApproveEnabled) {
log.info("prApproveEnabled is disabled — downgrading APPROVE to COMMENT");
event = "COMMENT";
}
// compose the request
const params: RestEndpointMethodTypes["pulls"]["createReview"]["parameters"] = {
owner: ctx.repo.owner,
repo: ctx.repo.name,
pull_number,
event: approved ? "APPROVE" : "COMMENT",
event,
};
if (body) params.body = body;
if (commit_id) {
@@ -115,12 +123,20 @@ export function CreatePullRequestReviewTool(ctx: ToolContext) {
return reviewComment;
});
}
const result = await ctx.octokit.rest.pulls.createReview(params);
log.debug(`createReview response: ${JSON.stringify(result.data)}`);
if (!result.data.id) {
throw new Error(`createReview returned invalid data: ${JSON.stringify(result.data)}`);
}
const reviewId = result.data.id;
const reviewNodeId = result.data.node_id;
// report review node ID to server so the in-flight dedup check
// in the synchronize webhook handler sees this run as "review submitted."
// awaited (not fire-and-forget) to guarantee the signal lands before
// any subsequent push's webhook checks for in-flight runs.
await reportReviewNodeId(ctx, reviewNodeId);
// build quick links footer and update the review body
// only include "Fix all" and "Fix 👍s" links if there are actual review comments
@@ -175,6 +191,35 @@ export function CreatePullRequestReviewTool(ctx: ToolContext) {
});
}
async function reportReviewNodeId(ctx: ToolContext, reviewNodeId: string): Promise<void> {
for (let remaining = 2; remaining >= 0; remaining--) {
try {
const response = await apiFetch({
path: `/api/workflow-run/${ctx.runId}`,
method: "PATCH",
headers: {
authorization: `Bearer ${ctx.apiToken}`,
"content-type": "application/json",
},
body: JSON.stringify({ reviewNodeId }),
signal: AbortSignal.timeout(10_000),
});
if (response.ok) return;
if (remaining > 0) {
log.debug(`reportReviewNodeId got ${response.status}, retrying (${remaining} left)`);
await new Promise((r) => setTimeout(r, 2000));
}
} catch (error) {
if (remaining > 0) {
log.debug(`reportReviewNodeId failed, retrying (${remaining} left): ${error}`);
await new Promise((r) => setTimeout(r, 2000));
} else {
log.debug(`reportReviewNodeId exhausted retries: ${error}`);
}
}
}
}
// =============================================================================
// COMMENTED OUT: Three-step review flow (start_review, add_review_comment, submit_review)
// This approach used GraphQL to add comments to a pending review one-by-one,
+32 -3
View File
@@ -6,7 +6,7 @@ import { execute, tool } from "./shared.ts";
export const SelectModeParams = type({
mode: type.string.describe(
"the name of the mode to select (e.g., 'Build', 'Plan', 'Review', 'Fix', 'AddressReviews', 'Task')"
"the name of the mode to select (e.g., 'Build', 'Plan', 'Review', 'IncrementalReview', 'Fix', 'AddressReviews', 'Task')"
),
});
@@ -89,9 +89,38 @@ Each task in the \`tasks\` array should include:
After all tasks complete, consolidate into a **single** review:
- merge the \`comments\` arrays from all subagent outputs
- submit one \`${ghPullfrogMcpName}/create_pull_request_review\` with the merged comments and a unified summary body
- if subagents found actionable issues: submit one \`${ghPullfrogMcpName}/create_pull_request_review\` with \`approved: false\`, the merged comments, and a unified summary body
- if no subagent found actionable issues: submit with \`approved: true\` and a brief positive summary (no inline comments)
- call \`${ghPullfrogMcpName}/report_progress\` with the summary
Use max effort for thorough reviews.`,
IncrementalReview: `### Checklist
1. Checkout the PR via \`${ghPullfrogMcpName}/checkout_pr\` — this returns PR metadata and a \`diffPath\`. Read the diff to identify the major areas of change.
2. Generate the incremental diff using the \`before_sha\` from EVENT DATA: \`git diff <before_sha>...HEAD\`. This isolates only the new commits. If the command fails (e.g., force-push rewrote history), fall back to reviewing the full PR diff.
3. Fetch previous reviews via \`${ghPullfrogMcpName}/list_pull_request_reviews\`. For the most recent Pullfrog review, call \`${ghPullfrogMcpName}/get_review_comments\` with the review ID to retrieve specific prior line-level feedback. Include the prior review summary and comment details when crafting subagent tasks.
4. Delegate multiple subagents in a single \`${ghPullfrogMcpName}/delegate\` call, each focused on a specific area of the new changes. Provide both the full diff path and the incremental diff.
5. After all subagents return, consolidate their findings into a single review.
### Crafting each task
Each task in the \`tasks\` array should include:
- the full diff file path AND the incremental diff (so the subagent can see both new changes and full context)
- what specific area/aspect to focus on
- instruct it to prioritize reviewing code in the incremental diff while using the full diff for context and to catch any changes not covered by the incremental diff
- include the prior review comments (from step 3) so the subagent knows what feedback was already given — instruct it to avoid repeating prior issues and to note whether prior feedback was addressed by the new commits
- instruct it to actively hunt for problems: trace data flow, check boundaries, explore failure modes, verify assumptions, consider lifecycle, spot performance issues
- draft inline comments with NEW line numbers from the full PR diff — every comment must be actionable (2-3 sentences max)
- call \`${ghPullfrogMcpName}/set_output\` with a JSON object: \`{ "summary": "...", "comments": [{ "path": "file.ts", "line": 42, "body": "..." }, ...] }\`
### Post-delegation
After all tasks complete, consolidate into a **single** review:
- merge the \`comments\` arrays from all subagent outputs
- if subagents found actionable issues: submit one \`${ghPullfrogMcpName}/create_pull_request_review\` with \`approved: false\`, the merged comments, and a unified summary body
- if no subagent found actionable issues: submit with \`approved: true\` and a brief positive summary (no inline comments)
- call \`${ghPullfrogMcpName}/report_progress\` with the summary
- if no subagent found actionable issues, skip the review — just call \`report_progress\` noting the PR was reviewed
Use max effort for thorough reviews.`,
+1
View File
@@ -99,6 +99,7 @@ export interface ToolContext {
agent: Agent;
modes: Mode[];
postCheckoutScript: string | null;
prApproveEnabled: boolean;
toolState: ToolState;
runId: number | undefined;
jobId: string | undefined;
+41 -5
View File
@@ -79,7 +79,7 @@ export function computeModes(): Mode[] {
2. **DEPENDENCIES** - ${dependencyInstallationStep}
3. **FETCH COMMENTS** - Fetch review comments using ${ghPullfrogMcpName}/get_review_comments with \`pull_number\` and \`review_id\` from EVENT DATA. This returns \`commentsPath\` - read that file for full comment details with diff context. If EVENT DATA contains a \`triggerer\` field (indicating who requested fixes), you can pass \`approved_by\` to filter to only comments they approved with 👍.
3. **FETCH COMMENTS** - Fetch review comments using ${ghPullfrogMcpName}/get_review_comments with \`pull_number\` and \`review_id\` from EVENT DATA. This returns \`commentsPath\` - read that file for full comment details with diff context. When \`approved_only\` is set in EVENT DATA, only approved comments are returned automatically.
4. **UNDERSTAND** - Review the feedback provided. Understand each review comment and what changes are being requested.
@@ -101,7 +101,7 @@ Keep the progress comment extremely brief. The summary should be 1-2 sentences m
name: "Review",
description:
"Review code, PRs, or implementations; provide feedback or suggestions; identify issues; or check code quality, style, and correctness",
prompt: `Follow these steps to review the PR. Your job is to find problems—assume they exist until you've proven otherwise. Do not submit a clean review without thorough investigation. **If you have nothing interesting to say, do NOT submit a review at all—use \`report_progress\` instead.**
prompt: `Follow these steps to review the PR. Your job is to find problems—assume they exist until you've proven otherwise. Do not submit a clean review without thorough investigation.
1. **CHECKOUT** - Call ${ghPullfrogMcpName}/checkout_pr with the PR number. This should give you all PR metadata you need, including a \`diffPath\`: a path to a temp file containing the PR diff.
@@ -119,15 +119,51 @@ Keep the progress comment extremely brief. The summary should be 1-2 sentences m
- **Check PR consistency**: Does the PR title/description match the actual code changes? Flag significant discrepancies.
- Do NOT stop at "this looks reasonable." Dig until you either find a problem or have concrete evidence there isn't one.
4. **DRAFT LINE-BY-LINE COMMENTS** - Every comment must be actionable: the author should need to change something in response. 2-3 sentences max. Use the NEW line number from the diff (second column: \`| OLD | NEW | TYPE | CODE\`). If no issues found, skip to step 5. NO COMPLIMENTS. NO NITPICKING ABOUT CHANGES UNRELATED TO THE MAIN CHANGE. Non-actionable comments (praise, style preferences, minor optimizatfixons, documentation nits) must not be drafted. If no comments survive and you have no significant concerns, **do not submit a review**. Use \`${ghPullfrogMcpName}/report_progress\` to note the PR was reviewed and no issues were found.
4. **DRAFT LINE-BY-LINE COMMENTS** - Every comment must be actionable: the author should need to change something in response. 2-3 sentences max. Use the NEW line number from the diff (second column: \`| OLD | NEW | TYPE | CODE\`). If no issues found, skip to step 5. NO COMPLIMENTS. NO NITPICKING ABOUT CHANGES UNRELATED TO THE MAIN CHANGE. Non-actionable comments (praise, style preferences, minor optimizatfixons, documentation nits) must not be drafted.
5. **WRITE SUMMARY** - Draft a 1-3 sentence summary for the review body. Include urgency level and any concerns about code outside the diff.
5. **WRITE SUMMARY** - Draft a 1-3 sentence summary for the review body. If issues were found, include urgency level and any concerns about code outside the diff. If no issues were found, write a brief approval summary (e.g., "Changes look good. No issues found.").
6. **SUBMIT** — Use ${ghPullfrogMcpName}/create_pull_request_review:
6. **SUBMIT** — Always submit a review via ${ghPullfrogMcpName}/create_pull_request_review:
- \`body\`: The summary from step 5
- \`comments\`: The inline comments from step 4
- \`approved\`: Set to \`true\` ONLY if the review contains no actionable feedback — neither inline comments nor actionable content in the body. An approval signals "no changes needed."
${permalinkTip}
`,
},
{
name: "IncrementalReview",
description:
"Re-review a PR after new commits are pushed; focus on new changes since the last review",
prompt: `Follow these steps to incrementally re-review the PR after new commits were pushed. Focus on what changed since the last review.
1. **CHECKOUT** - Call ${ghPullfrogMcpName}/checkout_pr with the PR number. This gives you the full PR diff via \`diffPath\`.
2. **INCREMENTAL DIFF** - EVENT DATA contains \`before_sha\` (the HEAD before this push). Generate the incremental diff:
\`git diff <before_sha>...HEAD\`
This shows the changes introduced by this push. Cross-reference with previous reviews (step 3) to confirm coverage of all unreviewed changes — the full PR diff fills any gaps.
**If the diff command fails** (e.g., force-push rewrote history), fall back to reviewing the full PR diff from step 1.
3. **FETCH PREVIOUS REVIEWS** - Use ${ghPullfrogMcpName}/list_pull_request_reviews to find previous Pullfrog reviews. For the most recent one, call ${ghPullfrogMcpName}/get_review_comments with the review ID to see specific line-level feedback. This lets you avoid repeating issues and assess whether prior feedback was addressed by the new commits.
4. **ANALYZE** - Read the incremental diff to understand the new changes. Use the full PR diff for surrounding context and to catch any changes not covered by the incremental diff.
- **Understand the change**: What is new or modified since the last review?
- **Evaluate the approach**: Are the new changes sound? Do they address prior feedback?
5. **INVESTIGATE** - Hunt for problems in the new code using the same techniques as a full review:
- Trace data flow, check boundaries, explore failure modes, verify assumptions, consider lifecycle, spot performance issues.
- Focus investigation on code that changed in the incremental diff, but trace its effects through the broader codebase.
- Do NOT repeat feedback already given in previous reviews unless it was not addressed.
6. **DRAFT LINE-BY-LINE COMMENTS** - Every comment must be actionable. 2-3 sentences max. Use the NEW line number from the full PR diff. NO COMPLIMENTS. NO NITPICKING.
7. **WRITE SUMMARY** - Draft a 1-3 sentence summary for the review body. Focus on what changed since the last review and whether the new changes are sound. If issues were found, include urgency level. If no issues were found, write a brief approval summary.
8. **SUBMIT** — Use ${ghPullfrogMcpName}/create_pull_request_review:
- \`body\`: The summary from step 7
- \`comments\`: The inline comments from step 6
- \`approved\`: Set to \`true\` ONLY if the review contains no actionable feedback — neither inline comments nor actionable content in the body. An approval signals "no changes needed."
${permalinkTip}
`,
},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@pullfrog/pullfrog",
"version": "0.0.174",
"version": "0.0.176",
"type": "module",
"files": [
"index.js",
+52 -7
View File
@@ -24695,7 +24695,7 @@ var require_lodash = __commonJS({
var symbolProto = Symbol2 ? Symbol2.prototype : void 0;
var symbolToString = symbolProto ? symbolProto.toString : void 0;
function baseIsRegExp(value2) {
return isObject(value2) && objectToString.call(value2) == regexpTag;
return isObject2(value2) && objectToString.call(value2) == regexpTag;
}
function baseSlice(array, start, end) {
var index = -1, length = array.length;
@@ -24729,7 +24729,7 @@ var require_lodash = __commonJS({
end = end === void 0 ? length : end;
return !start && end >= length ? array : baseSlice(array, start, end);
}
function isObject(value2) {
function isObject2(value2) {
var type2 = typeof value2;
return !!value2 && (type2 == "object" || type2 == "function");
}
@@ -24762,9 +24762,9 @@ var require_lodash = __commonJS({
if (isSymbol(value2)) {
return NAN;
}
if (isObject(value2)) {
if (isObject2(value2)) {
var other = typeof value2.valueOf == "function" ? value2.valueOf() : value2;
value2 = isObject(other) ? other + "" : other;
value2 = isObject2(other) ? other + "" : other;
}
if (typeof value2 != "string") {
return value2 === 0 ? value2 : +value2;
@@ -24778,7 +24778,7 @@ var require_lodash = __commonJS({
}
function truncate(string2, options) {
var length = DEFAULT_TRUNC_LENGTH, omission = DEFAULT_TRUNC_OMISSION;
if (isObject(options)) {
if (isObject2(options)) {
var separator2 = "separator" in options ? options.separator : separator2;
length = "length" in options ? toInteger(options.length) : length;
omission = "omission" in options ? baseToString(options.omission) : omission;
@@ -41183,6 +41183,9 @@ var Octokit2 = Octokit.plugin(requestLog, legacyRestEndpointMethods, paginateRes
);
// utils/github.ts
function isObject(value2) {
return typeof value2 === "object" && value2 !== null;
}
function parseRepoContext() {
const githubRepo = process.env.GITHUB_REPOSITORY;
if (!githubRepo) {
@@ -41194,9 +41197,20 @@ function parseRepoContext() {
}
return { owner, name };
}
function emptyResourceUsage() {
return {
requestCount: 0,
rateLimitRemaining: null,
rateLimitResetMs: null
};
}
var usageByResource = {
core: emptyResourceUsage(),
graphql: emptyResourceUsage()
};
function createOctokit(token) {
const OctokitWithPlugins = Octokit2.plugin(throttling);
return new OctokitWithPlugins({
const octokit = new OctokitWithPlugins({
auth: token,
throttle: {
onRateLimit: (_retryAfter, _options, _octokit, retryCount) => {
@@ -41207,6 +41221,37 @@ function createOctokit(token) {
}
}
});
const onResponse = (response) => {
const resource = response.headers["x-ratelimit-resource"];
if (!resource) {
return response;
}
usageByResource[resource] ??= emptyResourceUsage();
const usage = usageByResource[resource];
usage.requestCount++;
const remaining = response.headers["x-ratelimit-remaining"];
const reset = response.headers["x-ratelimit-reset"];
if (remaining !== void 0) {
usage.rateLimitRemaining = Number(remaining);
}
if (reset !== void 0) {
usage.rateLimitResetMs = Number(reset) * 1e3;
}
return response;
};
octokit.hook.wrap("request", async (request2, options) => {
try {
const response = await request2(options);
onResponse(response);
return response;
} catch (error2) {
if (isObject(error2) && "response" in error2 && isObject(error2.response) && "headers" in error2.response && isObject(error2.response.headers)) {
onResponse(error2.response);
}
throw error2;
}
});
return octokit;
}
// mcp/comment.ts
@@ -41267,7 +41312,7 @@ var Effort = type.enumerated("mini", "auto", "max");
// package.json
var package_default = {
name: "@pullfrog/pullfrog",
version: "0.0.174",
version: "0.0.175",
type: "module",
files: [
"index.js",
+1
View File
@@ -72,6 +72,7 @@ async function fetchBodyHtml(ctx: ResolveBodyContext): Promise<string | undefine
case "pull_request_opened":
case "pull_request_ready_for_review":
case "pull_request_synchronize":
case "pull_request_review_requested":
// PRs are also issues - use issues.get which returns body_html
if (!event.issue_number) return;
+99 -1
View File
@@ -1,10 +1,24 @@
import { createSign } from "node:crypto";
import { rename, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import * as core from "@actions/core";
import { throttling } from "@octokit/plugin-throttling";
import { Octokit } from "@octokit/rest";
import { apiFetch } from "./apiFetch.ts";
import { retry } from "./retry.ts";
function isObject(value: unknown) {
return typeof value === "object" && value !== null;
}
// we don't get access to the actual class from @octokit/rest
// it's reachable from @octokit/request-error but we'd have to add a dependency on it
// and it would pose a risk of accidentally pulling a different version of that class (node_modules dep graphs ❤️)
// so it's safer to ducktype this
interface OctokitResponseShim {
headers: Record<string, string | number | undefined>;
}
export interface InstallationToken {
token: string;
expires_at: string;
@@ -339,11 +353,55 @@ export type OctokitWithPlugins = InstanceType<
ReturnType<typeof Octokit.plugin<typeof Octokit, [typeof throttling]>>
>;
export interface ResourceUsage {
requestCount: number;
rateLimitRemaining: number | null;
rateLimitResetMs: number | null;
}
function emptyResourceUsage(): ResourceUsage {
return {
requestCount: 0,
rateLimitRemaining: null,
rateLimitResetMs: null,
};
}
const usageByResource: Record<string, ResourceUsage> = {
core: emptyResourceUsage(),
graphql: emptyResourceUsage(),
};
export interface UsageSummary {
version: 1;
github: {
core: ResourceUsage;
graphql: ResourceUsage;
};
}
function getGitHubUsageSummary(): UsageSummary {
return {
version: 1,
github: {
core: usageByResource.core,
graphql: usageByResource.graphql,
},
};
}
export async function writeGitHubUsageSummaryToFile(path: string): Promise<void> {
const summary = getGitHubUsageSummary();
const tmpPath = join(dirname(path), `.usage-summary-${process.pid}.tmp`);
await writeFile(tmpPath, JSON.stringify(summary));
await rename(tmpPath, path);
}
export function createOctokit(token: string): OctokitWithPlugins {
// `OctokitWithPlugins` initialization based on https://github.com/actions/toolkit/blob/2506e78e82fbd2f9e94d63e75f5309118c8de1b1/packages/github/src/github.ts#L15-L22
// we can't use it directly because it's stuck on `@octokit/core@v5` and we use the hottest `@octokit/core@v7`
const OctokitWithPlugins = Octokit.plugin(throttling);
return new OctokitWithPlugins({
const octokit = new OctokitWithPlugins({
auth: token,
throttle: {
onRateLimit: (_retryAfter, _options, _octokit, retryCount) => {
@@ -354,4 +412,44 @@ export function createOctokit(token: string): OctokitWithPlugins {
},
},
});
const onResponse = (response: OctokitResponseShim) => {
const resource = response.headers["x-ratelimit-resource"];
if (!resource) {
return response;
}
usageByResource[resource] ??= emptyResourceUsage();
const usage = usageByResource[resource];
usage.requestCount++;
const remaining = response.headers["x-ratelimit-remaining"];
const reset = response.headers["x-ratelimit-reset"];
if (remaining !== undefined) {
usage.rateLimitRemaining = Number(remaining);
}
if (reset !== undefined) {
usage.rateLimitResetMs = Number(reset) * 1000;
}
return response;
};
octokit.hook.wrap("request", async (request, options) => {
try {
const response = await request(options);
onResponse(response);
return response;
} catch (error) {
if (
isObject(error) &&
"response" in error &&
isObject(error.response) &&
"headers" in error.response &&
isObject(error.response.headers)
) {
onResponse(error.response as OctokitResponseShim);
}
throw error;
}
});
return octokit;
}
+1
View File
@@ -178,6 +178,7 @@ export function resolvePayload(
effort: inputs.effort ?? jsonPayload?.effort ?? "auto",
timeout: inputs.timeout ?? jsonPayload?.timeout,
cwd: resolveCwd(inputs.cwd),
progressCommentId: jsonPayload?.progressCommentId,
debug: jsonPayload?.debug,
// permissions: inputs > repoSettings > fallbacks
+2
View File
@@ -19,6 +19,7 @@ export interface RepoSettings {
search: ToolPermission;
push: PushPermission;
shell: ShellPermission;
prApproveEnabled: boolean;
}
export interface RunContext {
@@ -36,6 +37,7 @@ const defaultSettings: RepoSettings = {
search: "enabled",
push: "restricted",
shell: "restricted",
prApproveEnabled: false,
};
const defaultRunContext: RunContext = {