pullfrog 0.1.31 → 0.1.33

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/cli.mjs CHANGED
@@ -101618,15 +101618,20 @@ var providers = {
101618
101618
  displayName: "Anthropic",
101619
101619
  envVars: ["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"],
101620
101620
  models: {
101621
- // claude-fable-5 is disabled globally access has been revoked, so it
101622
- // can't run as-is anywhere (BYOK, Claude Code, or Router). the fallback
101623
- // redirects all resolution to opus and hides it from pickers; opus is the
101624
- // universally-available flagship, the AUTO_INTELLIGENT tier target, and
101625
- // the recommended pick. clear the fallback if fable access returns (#959).
101621
+ // claude-fable-5 is selectable but not recommended/auto-selected it's
101622
+ // moving to usage-credits-only billing and isn't broadly available yet, so
101623
+ // opus stays the universally-available flagship, the AUTO_INTELLIGENT tier
101624
+ // target, and the recommended pick. an explicit fable pick hits the real
101625
+ // API and is access-gated: it errors for accounts without access today and
101626
+ // just works once usage credits are live — no silent opus fallback, the
101627
+ // API is the source of truth (#959).
101626
101628
  "claude-fable": {
101627
101629
  displayName: "Claude Fable",
101628
101630
  resolve: "anthropic/claude-fable-5",
101629
- fallback: "anthropic/claude-opus",
101631
+ // rolling alias: models.dev's OpenRouter mirror lags brand-new pinned
101632
+ // versions (claude-fable-5 isn't indexed yet), so track ~…-latest to
101633
+ // stay catalog-valid and auto-follow version bumps.
101634
+ openRouterResolve: "openrouter/~anthropic/claude-fable-latest",
101630
101635
  subagentModel: "claude-sonnet"
101631
101636
  },
101632
101637
  "claude-opus": {
@@ -101638,8 +101643,8 @@ var providers = {
101638
101643
  },
101639
101644
  "claude-sonnet": {
101640
101645
  displayName: "Claude Sonnet",
101641
- resolve: "anthropic/claude-sonnet-4-6",
101642
- openRouterResolve: "openrouter/anthropic/claude-sonnet-4.6"
101646
+ resolve: "anthropic/claude-sonnet-5",
101647
+ openRouterResolve: "openrouter/anthropic/claude-sonnet-5"
101643
101648
  },
101644
101649
  "claude-haiku": {
101645
101650
  displayName: "Claude Haiku",
@@ -101817,8 +101822,8 @@ var providers = {
101817
101822
  },
101818
101823
  "claude-sonnet": {
101819
101824
  displayName: "Claude Sonnet",
101820
- resolve: "opencode/claude-sonnet-4-6",
101821
- openRouterResolve: "openrouter/anthropic/claude-sonnet-4.6"
101825
+ resolve: "opencode/claude-sonnet-5",
101826
+ openRouterResolve: "openrouter/anthropic/claude-sonnet-5"
101822
101827
  },
101823
101828
  "claude-haiku": {
101824
101829
  displayName: "Claude Haiku",
@@ -102836,7 +102841,7 @@ var import_semver = __toESM(require_semver2(), 1);
102836
102841
  // package.json
102837
102842
  var package_default = {
102838
102843
  name: "pullfrog",
102839
- version: "0.1.31",
102844
+ version: "0.1.33",
102840
102845
  type: "module",
102841
102846
  bin: {
102842
102847
  pullfrog: "dist/cli.mjs",
@@ -104575,7 +104580,7 @@ function buildAgentsJson() {
104575
104580
  [REVIEWER_AGENT_NAME]: {
104576
104581
  description: "Read-only review subagent for lens-based code review (correctness, security, billing-subsystem, etc.). Reads only \u2014 no writes, no state-changing shell or MCP calls, no nested subagent dispatch.",
104577
104582
  prompt: REVIEWER_SYSTEM_PROMPT,
104578
- model: "claude-sonnet-4-6"
104583
+ model: "claude-sonnet-5"
104579
104584
  }
104580
104585
  };
104581
104586
  return JSON.stringify(agents2);
@@ -152710,6 +152715,34 @@ function getCacheKeyString(value2) {
152710
152715
  );
152711
152716
  }
152712
152717
  }
152718
+ var DEFAULT_RETRY_AFTER_CAP_MS = 2e4;
152719
+ function getErrorResponseHeaders(error49) {
152720
+ if (error49 === null || typeof error49 !== "object") return void 0;
152721
+ const response = error49.response;
152722
+ if (response === null || typeof response !== "object") return void 0;
152723
+ const headers = response.headers;
152724
+ if (headers === null || typeof headers !== "object") return void 0;
152725
+ return headers;
152726
+ }
152727
+ function retryAfterMs(error49) {
152728
+ const headers = getErrorResponseHeaders(error49);
152729
+ if (!headers) return void 0;
152730
+ const retryAfter = headers["retry-after"];
152731
+ if (typeof retryAfter === "string" && retryAfter.trim() !== "") {
152732
+ const seconds = Number(retryAfter);
152733
+ if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
152734
+ const dateMs = Date.parse(retryAfter);
152735
+ if (!Number.isNaN(dateMs)) return Math.max(0, dateMs - Date.now());
152736
+ }
152737
+ if (headers["x-ratelimit-remaining"] === "0") {
152738
+ const reset = headers["x-ratelimit-reset"];
152739
+ if (typeof reset === "string" && reset.trim() !== "") {
152740
+ const resetEpoch = Number(reset);
152741
+ if (Number.isFinite(resetEpoch)) return Math.max(0, resetEpoch * 1e3 - Date.now());
152742
+ }
152743
+ }
152744
+ return void 0;
152745
+ }
152713
152746
  function validateSchema(schema2, value2) {
152714
152747
  const result = schema2["~standard"].validate(value2);
152715
152748
  if (result instanceof Promise) {
@@ -152847,7 +152880,15 @@ function _op(fn2, options) {
152847
152880
  log2.info({ key, error: error49, attempt });
152848
152881
  }
152849
152882
  } else {
152850
- const delay2 = retries[attempt];
152883
+ let delay2 = retries[attempt];
152884
+ if (options.retryAfter) {
152885
+ const extract = options.retryAfter === true ? retryAfterMs : options.retryAfter;
152886
+ const hint = extract(error49);
152887
+ if (hint !== void 0) {
152888
+ const cap = options.retryAfterCap ?? DEFAULT_RETRY_AFTER_CAP_MS;
152889
+ delay2 = Math.max(delay2, Math.min(cap, hint));
152890
+ }
152891
+ }
152851
152892
  if (namePrefix) {
152852
152893
  log2.info(
152853
152894
  `${namePrefix}attempt ${attempt + 1}/${retries.length + 1} failed, retrying in ${delay2}ms`
@@ -158576,6 +158617,12 @@ function isTransientNetworkError(error49, extraPatterns = []) {
158576
158617
  const patterns = ["fetch failed", "ECONNRESET", "ETIMEDOUT", ...extraPatterns];
158577
158618
  return patterns.some((p2) => error49.message.includes(p2));
158578
158619
  }
158620
+ function isTransientOctokitError(error49) {
158621
+ if (error49 !== null && typeof error49 === "object" && "status" in error49 && typeof error49.status === "number") {
158622
+ return error49.status >= 500;
158623
+ }
158624
+ return isTransientNetworkError(error49);
158625
+ }
158579
158626
 
158580
158627
  // utils/patchWorkflowRunFields.ts
158581
158628
  var STRING_KEYS = [
@@ -158669,6 +158716,195 @@ function aggregateUsage(entries) {
158669
158716
  return out;
158670
158717
  }
158671
158718
 
158719
+ // utils/payload.ts
158720
+ var core4 = __toESM(require_core(), 1);
158721
+ import { readFileSync as readFileSync4 } from "node:fs";
158722
+ import { isAbsolute as isAbsolute2, resolve as resolve2 } from "node:path";
158723
+
158724
+ // utils/versioning.ts
158725
+ var import_semver2 = __toESM(require_semver2(), 1);
158726
+ var COMPATIBILITY_POLICY = "non-breaking";
158727
+ function validateCompatibility(payloadVersion, actionVersion) {
158728
+ const payloadSemVer = import_semver2.default.parse(payloadVersion);
158729
+ if (!payloadSemVer)
158730
+ throw new Error(`Payload version ${payloadVersion} is not a valid semantic version.`);
158731
+ const major = payloadSemVer.major;
158732
+ const minor = payloadSemVer.minor;
158733
+ const patch = payloadSemVer.patch;
158734
+ const compatibilityRange = COMPATIBILITY_POLICY === "same-features" ? `^${major}.${minor}.${major === 0 ? patch : 0}` : `^${major}.${major === 0 ? minor : 0}.${major === 0 ? "x" : 0}`;
158735
+ if (!import_semver2.default.satisfies(actionVersion, compatibilityRange)) {
158736
+ throw new Error(
158737
+ `Payload version ${payloadVersion} is incompatible with action version ${actionVersion}. Please update your workflow to use at least ${import_semver2.default.minVersion(compatibilityRange)} version of the action.`
158738
+ );
158739
+ }
158740
+ }
158741
+
158742
+ // utils/payload.ts
158743
+ var ShellPermissionInput = type.enumerated("disabled", "restricted", "enabled");
158744
+ var PushPermissionInput = type.enumerated("disabled", "restricted", "enabled");
158745
+ var StatusChecksInput = type.enumerated("disabled", "enabled");
158746
+ var JsonPayload = type({
158747
+ "~pullfrog": "true",
158748
+ version: "string",
158749
+ "model?": "string | undefined",
158750
+ "modelExplicit?": "boolean | undefined",
158751
+ prompt: "string",
158752
+ "triggerer?": "string | undefined",
158753
+ "baseInstructions?": "string | undefined",
158754
+ "eventInstructions?": "string",
158755
+ "previousRunsNote?": "string",
158756
+ "event?": "object",
158757
+ "xrepo?": type({
158758
+ mode: "'all' | 'explicit'",
158759
+ read: "string[]",
158760
+ write: "string[]",
158761
+ // optional so a payload from an older server build (pre-`unavailable`)
158762
+ // still parses against a newer action across a rolling deploy.
158763
+ "unavailable?": "string[]"
158764
+ }).or("undefined"),
158765
+ "timeout?": "string | undefined",
158766
+ "progressComment?": type({
158767
+ id: "string",
158768
+ type: "'issue' | 'review'"
158769
+ }).or("undefined"),
158770
+ "generateSummary?": "boolean | undefined"
158771
+ });
158772
+ var COLLABORATOR_PERMISSIONS = ["admin", "maintain", "write"];
158773
+ function isCollaborator(event) {
158774
+ const perm = event.authorPermission;
158775
+ return perm !== void 0 && COLLABORATOR_PERMISSIONS.includes(perm);
158776
+ }
158777
+ var Inputs = type({
158778
+ "prompt?": type.string.or("undefined"),
158779
+ "prompt_file?": type.string.or("undefined"),
158780
+ "model?": type.string.or("undefined"),
158781
+ "timeout?": type.string.or("undefined"),
158782
+ "push?": PushPermissionInput.or("undefined"),
158783
+ "shell?": ShellPermissionInput.or("undefined"),
158784
+ "status_checks?": StatusChecksInput.or("undefined"),
158785
+ "cwd?": type.string.or("undefined"),
158786
+ "output_schema?": type.string.or("undefined")
158787
+ });
158788
+ function isPayloadEvent(value2) {
158789
+ return typeof value2 === "object" && value2 !== null && "trigger" in value2;
158790
+ }
158791
+ function resolveCwd(cwd) {
158792
+ const workspace = process.env.GITHUB_WORKSPACE;
158793
+ if (!cwd) return workspace;
158794
+ if (isAbsolute2(cwd)) return cwd;
158795
+ return workspace ? resolve2(workspace, cwd) : cwd;
158796
+ }
158797
+ function resolvePromptInput() {
158798
+ const promptInput = core4.getInput("prompt");
158799
+ const promptFile = core4.getInput("prompt_file");
158800
+ if (promptInput && promptFile) {
158801
+ throw new Error("set exactly one of 'prompt' or 'prompt_file' inputs, not both.");
158802
+ }
158803
+ if (promptFile) {
158804
+ return resolvePromptFile(promptFile);
158805
+ }
158806
+ if (!promptInput) {
158807
+ throw new Error("one of 'prompt' or 'prompt_file' inputs is required.");
158808
+ }
158809
+ let parsed2;
158810
+ try {
158811
+ parsed2 = JSON.parse(promptInput);
158812
+ } catch {
158813
+ return promptInput;
158814
+ }
158815
+ if (!parsed2 || typeof parsed2 !== "object" || !("~pullfrog" in parsed2)) {
158816
+ return promptInput;
158817
+ }
158818
+ const jsonPayload = JsonPayload.assert(parsed2);
158819
+ validateCompatibility(jsonPayload.version, package_default.version);
158820
+ return jsonPayload;
158821
+ }
158822
+ function resolvePromptFile(input) {
158823
+ const workspace = process.env.GITHUB_WORKSPACE;
158824
+ const path4 = isAbsolute2(input) ? input : workspace ? resolve2(workspace, input) : resolve2(input);
158825
+ const content = readFileSync4(path4, "utf-8");
158826
+ if (!content.trim()) {
158827
+ throw new Error(`prompt_file ${JSON.stringify(input)} is empty.`);
158828
+ }
158829
+ return content;
158830
+ }
158831
+ function resolveNonPromptInputs() {
158832
+ return Inputs.omit("prompt", "prompt_file").assert({
158833
+ model: core4.getInput("model") || void 0,
158834
+ timeout: core4.getInput("timeout") || void 0,
158835
+ cwd: core4.getInput("cwd") || void 0,
158836
+ push: core4.getInput("push") || void 0,
158837
+ shell: core4.getInput("shell") || void 0,
158838
+ status_checks: core4.getInput("status_checks") || void 0
158839
+ });
158840
+ }
158841
+ var isPullfrog = (actor) => {
158842
+ actor = actor?.replace("[bot]", "");
158843
+ return !!actor && (actor === "pullfrog" || actor === "pullfrogdev");
158844
+ };
158845
+ function resolvePayload(resolvedPromptInput, repoSettings) {
158846
+ const [prompt, jsonPayload] = typeof resolvedPromptInput !== "string" ? [resolvedPromptInput.prompt, resolvedPromptInput] : [resolvedPromptInput, void 0];
158847
+ const inputs = resolveNonPromptInputs();
158848
+ const rawEvent = jsonPayload?.event;
158849
+ const event = isPayloadEvent(rawEvent) ? rawEvent : { trigger: "unknown" };
158850
+ const model = jsonPayload?.model ?? inputs.model ?? repoSettings.model ?? void 0;
158851
+ const isNonCollaborator = !isCollaborator(event);
158852
+ const repoShell = repoSettings.shell ?? "restricted";
158853
+ const inputShell = inputs.shell;
158854
+ let resolvedShell = repoShell;
158855
+ if (inputShell === "disabled") {
158856
+ resolvedShell = "disabled";
158857
+ } else if (inputShell === "restricted" && resolvedShell === "enabled") {
158858
+ resolvedShell = "restricted";
158859
+ }
158860
+ if (isNonCollaborator && resolvedShell === "enabled") {
158861
+ resolvedShell = "restricted";
158862
+ }
158863
+ return {
158864
+ "~pullfrog": true,
158865
+ version: jsonPayload?.version ?? package_default.version,
158866
+ model,
158867
+ // explicit only when the model came from a per-run override flag (carried on
158868
+ // the JSON payload). a GHA `model` input or the repo default is not explicit.
158869
+ modelExplicit: jsonPayload?.modelExplicit ?? false,
158870
+ prompt,
158871
+ triggerer: jsonPayload?.triggerer ?? // it's not a common use case but GITHUB_ACTOR can be a user when the workflow is manually triggered by a user through GitHub Actions UI
158872
+ (!isPullfrog(process.env.GITHUB_ACTOR) ? process.env.GITHUB_ACTOR : void 0),
158873
+ baseInstructions: jsonPayload?.baseInstructions,
158874
+ eventInstructions: jsonPayload?.eventInstructions,
158875
+ previousRunsNote: jsonPayload?.previousRunsNote,
158876
+ event,
158877
+ xrepo: jsonPayload?.xrepo,
158878
+ timeout: inputs.timeout ?? jsonPayload?.timeout,
158879
+ cwd: resolveCwd(inputs.cwd),
158880
+ progressComment: jsonPayload?.progressComment,
158881
+ generateSummary: jsonPayload?.generateSummary,
158882
+ // permissions: inputs > repoSettings > fallbacks
158883
+ push: inputs.push ?? repoSettings.push ?? "restricted",
158884
+ shell: resolvedShell,
158885
+ // opt-in commit-status check-runs (branch protection). workflow-level
158886
+ // static input, off unless the repo's pullfrog.yml sets status_checks: enabled.
158887
+ statusChecks: inputs.status_checks === "enabled",
158888
+ // set by proxy logic in main.ts when routing through OpenRouter
158889
+ proxyModel: void 0
158890
+ };
158891
+ }
158892
+ function resolveOutputSchema() {
158893
+ const raw2 = core4.getInput("output_schema");
158894
+ if (!raw2) return void 0;
158895
+ let parsed2;
158896
+ try {
158897
+ parsed2 = JSON.parse(raw2);
158898
+ } catch {
158899
+ throw new Error(`invalid output_schema: not valid JSON`);
158900
+ }
158901
+ if (!parsed2 || typeof parsed2 !== "object" || Array.isArray(parsed2)) {
158902
+ throw new Error(`invalid output_schema: must be a JSON object`);
158903
+ }
158904
+ log.info("\xBB structured output schema provided \u2014 output will be required");
158905
+ return parsed2;
158906
+ }
158907
+
158672
158908
  // mcp/comment.ts
158673
158909
  function isNotFoundError(error49) {
158674
158910
  return error49 instanceof Error && error49.message.includes("Not Found");
@@ -159090,6 +159326,81 @@ async function buildCommentableMap(ctx, pullNumber) {
159090
159326
  }
159091
159327
  return map2;
159092
159328
  }
159329
+ var OUTSTANDING_THREADS_QUERY = `
159330
+ query ($owner: String!, $name: String!, $prNumber: Int!, $cursor: String) {
159331
+ repository(owner: $owner, name: $name) {
159332
+ pullRequest(number: $prNumber) {
159333
+ reviewThreads(first: 100, after: $cursor) {
159334
+ pageInfo { hasNextPage endCursor }
159335
+ nodes {
159336
+ isResolved
159337
+ comments(first: 1) { nodes { author { login } } }
159338
+ }
159339
+ }
159340
+ }
159341
+ }
159342
+ }
159343
+ `;
159344
+ async function countOutstandingPullfrogThreads(ctx, pullNumber) {
159345
+ let count = 0;
159346
+ let cursor = null;
159347
+ for (let page = 0; page < 50; page += 1) {
159348
+ const response = await ctx.octokit.graphql(
159349
+ OUTSTANDING_THREADS_QUERY,
159350
+ { owner: ctx.repo.owner, name: ctx.repo.name, prNumber: pullNumber, cursor }
159351
+ );
159352
+ const conn = response.repository?.pullRequest?.reviewThreads;
159353
+ for (const thread of conn?.nodes ?? []) {
159354
+ if (!thread || thread.isResolved) continue;
159355
+ const root = thread.comments?.nodes?.find((c2) => c2 != null);
159356
+ if (root && isPullfrog(root.author?.login)) count += 1;
159357
+ }
159358
+ if (!conn?.pageInfo.hasNextPage) break;
159359
+ cursor = conn.pageInfo.endCursor;
159360
+ }
159361
+ return count;
159362
+ }
159363
+ async function approveAfterFix(ctx) {
159364
+ if (ctx.payload.event.trigger !== "fix_review") return;
159365
+ if (!ctx.prApproveEnabled) return;
159366
+ if (ctx.toolState.approval) return;
159367
+ const pullNumber = ctx.payload.event.issue_number;
159368
+ if (typeof pullNumber !== "number") return;
159369
+ const outstanding = await countOutstandingPullfrogThreads(ctx, pullNumber);
159370
+ if (outstanding > 0) {
159371
+ log.info(
159372
+ `skipping fix auto-approval: ${outstanding} unresolved Pullfrog review thread(s) still open on #${pullNumber}`
159373
+ );
159374
+ return;
159375
+ }
159376
+ const pr = await ctx.octokit.rest.pulls.get({
159377
+ owner: ctx.repo.owner,
159378
+ repo: ctx.repo.name,
159379
+ pull_number: pullNumber
159380
+ });
159381
+ if (pr.data.state !== "open") return;
159382
+ const headSha = pr.data.head.sha;
159383
+ const selfAuthored = isPullfrog(pr.data.user?.login);
159384
+ const summary2 = ctx.toolState.lastProgressBody?.trim();
159385
+ const body = "> \u2705 Pullfrog addressed all of its review feedback on this PR.\n\n" + (summary2 || "all Pullfrog-raised review threads are resolved \u2014 no outstanding findings remain.");
159386
+ const params = {
159387
+ owner: ctx.repo.owner,
159388
+ repo: ctx.repo.name,
159389
+ pull_number: pullNumber,
159390
+ event: selfAuthored ? "COMMENT" : "APPROVE",
159391
+ commit_id: headSha
159392
+ };
159393
+ const result = await createAndSubmitWithFooter(ctx, params, {
159394
+ body,
159395
+ approved: true,
159396
+ hasComments: false
159397
+ });
159398
+ ctx.toolState.approval = { wouldApprove: true, sha: headSha };
159399
+ log.info(`\xBB auto-approved #${pullNumber} after fix run (review ${result.data.id})`);
159400
+ await deleteProgressComment(ctx).catch((err) => {
159401
+ log.debug(`progress comment cleanup after fix auto-approval failed: ${err}`);
159402
+ });
159403
+ }
159093
159404
  function validateInlineComments(comments, map2) {
159094
159405
  const valid = [];
159095
159406
  const dropped = [];
@@ -159180,7 +159491,7 @@ var CreatePullRequestReview = type({
159180
159491
  "1-2 sentence high-level summary with urgency level, critical callouts, and feedback about code outside the diff. Specific feedback on diff lines goes in 'comments' array."
159181
159492
  ).optional(),
159182
159493
  approved: type.boolean.describe(
159183
- "Set to true to submit as an approval. Use for `> \u2705 No new issues found.` reviews where the PR is mergeable as-is and nothing in the body warrants code changes \u2014 approving also suppresses the Fix-button footer affordance so users don't dispatch a fix run on non-actionable feedback. Reserve approved: false for `> \u2139\uFE0F ...` (minor suggestions inline), `> [!IMPORTANT]` (recommended changes), and `> [!CAUTION]` (critical) reviews. Defaults to false (comment-only review). Mutually exclusive with request_changes."
159494
+ "Set to true to submit as an approval. Use for `> \u2705 No new issues found.` reviews where the PR is mergeable as-is and nothing in the body warrants code changes \u2014 approving also suppresses the Fix-button footer affordance so users don't dispatch a fix run on non-actionable feedback. Reserve approved: false for `> \u2139\uFE0F ...` (minor suggestions inline), `> [!IMPORTANT]` (recommended changes), and `> [!CAUTION]` (critical) reviews. Defaults to false (comment-only review). Mutually exclusive with request_changes. Approval is REJECTED while any unresolved Pullfrog review thread remains open on the PR (not just the latest commit's diff): resolve the threads the current code addresses (reply + resolve_review_thread) first, or submit a non-approving review if a real issue remains."
159184
159495
  ).optional(),
159185
159496
  request_changes: type.boolean.describe(
159186
159497
  "Set to true to submit a blocking REQUEST_CHANGES review \u2014 the PR cannot merge until the requested changes are made and the review is dismissed or re-reviewed. Reserve for changes you consider required, not optional suggestions. Mutually exclusive with approved; a contentless request (no body and no comments) is skipped."
@@ -159238,21 +159549,49 @@ function CreatePullRequestReviewTool(ctx) {
159238
159549
  reviewId: dup.reviewId
159239
159550
  };
159240
159551
  }
159552
+ if (approved) {
159553
+ const outstanding = await countOutstandingPullfrogThreads(ctx, pull_number);
159554
+ if (outstanding > 0) {
159555
+ const listRef = formatMcpToolRef(ctx.agentId, "get_review_comments");
159556
+ const resolveRef2 = formatMcpToolRef(ctx.agentId, "resolve_review_thread");
159557
+ throw new Error(
159558
+ `cannot approve: ${outstanding} unresolved Pullfrog review thread(s) still open on this PR. approval requires every prior Pullfrog finding to be resolved, not just the latest commits to be clean. inspect them with \`${listRef}\`; for each thread the current code genuinely addresses, reply then call \`${resolveRef2}\`, and retry this approval. if any thread is a real outstanding issue, do NOT approve \u2014 submit a non-approving review (omit \`approved\`) that covers it instead.`
159559
+ );
159560
+ }
159561
+ }
159562
+ const prSnapshot = (await ctx.octokit.rest.pulls.get({
159563
+ owner: ctx.repo.owner,
159564
+ repo: ctx.repo.name,
159565
+ pull_number
159566
+ })).data;
159567
+ const selfAuthored = isPullfrog(prSnapshot.user?.login);
159568
+ ctx.toolState.approval = { wouldApprove: approved === true, sha: primary.checkoutSha };
159241
159569
  const skip = reviewSkipDecision({
159242
159570
  approved: approved ?? false,
159243
159571
  requestChanges: request_changes ?? false,
159244
159572
  body,
159245
159573
  hasComments: comments.length > 0,
159246
- prApproveEnabled: ctx.prApproveEnabled
159574
+ // a self-authored approve downgrades to COMMENT (GitHub blocks a
159575
+ // self-APPROVE), so treat it as non-binding here — an empty self-
159576
+ // approve is then skipped (the verdict is still recorded above)
159577
+ // instead of POSTing an empty COMMENT that GitHub 422s.
159578
+ prApproveEnabled: ctx.prApproveEnabled && !selfAuthored
159247
159579
  });
159248
159580
  if (skip) {
159249
159581
  log.info(`skipping review submission: ${skip.reason}`);
159250
159582
  return { success: true, skipped: true, reason: skip.reason };
159251
159583
  }
159252
159584
  let event = approved ? "APPROVE" : request_changes ? "REQUEST_CHANGES" : "COMMENT";
159253
- if ((event === "APPROVE" || event === "REQUEST_CHANGES") && !ctx.prApproveEnabled) {
159254
- log.info(`prApproveEnabled is disabled \u2014 downgrading ${event} to COMMENT`);
159255
- event = "COMMENT";
159585
+ if (event === "APPROVE" || event === "REQUEST_CHANGES") {
159586
+ if (!ctx.prApproveEnabled) {
159587
+ log.info(`prApproveEnabled is disabled \u2014 downgrading ${event} to COMMENT`);
159588
+ event = "COMMENT";
159589
+ } else if (selfAuthored) {
159590
+ log.info(
159591
+ `self-authored PR \u2014 downgrading binding ${event} to COMMENT (verdict recorded internally)`
159592
+ );
159593
+ event = "COMMENT";
159594
+ }
159256
159595
  }
159257
159596
  const params = {
159258
159597
  owner: ctx.repo.owner,
@@ -159264,12 +159603,7 @@ function CreatePullRequestReviewTool(ctx) {
159264
159603
  if (commit_id) {
159265
159604
  params.commit_id = commit_id;
159266
159605
  } else {
159267
- const pr = await ctx.octokit.rest.pulls.get({
159268
- owner: ctx.repo.owner,
159269
- repo: ctx.repo.name,
159270
- pull_number
159271
- });
159272
- latestHeadSha = pr.data.head.sha;
159606
+ latestHeadSha = prSnapshot.head.sha;
159273
159607
  params.commit_id = primary.checkoutSha ?? latestHeadSha;
159274
159608
  if (primary.checkoutSha && latestHeadSha !== primary.checkoutSha) {
159275
159609
  log.info(
@@ -159337,6 +159671,7 @@ function CreatePullRequestReviewTool(ctx) {
159337
159671
  }
159338
159672
  )();
159339
159673
  } catch (err) {
159674
+ if (ctx.toolState.approval) ctx.toolState.approval.wouldApprove = false;
159340
159675
  if (isTransientReviewError(err)) {
159341
159676
  const rawMsg2 = err instanceof Error ? err.message : String(err);
159342
159677
  throw new Error(
@@ -159371,6 +159706,7 @@ function CreatePullRequestReviewTool(ctx) {
159371
159706
  nodeId: reviewNodeId,
159372
159707
  reviewedSha: actuallyReviewedSha
159373
159708
  };
159709
+ if (ctx.toolState.approval) ctx.toolState.approval.sha = actuallyReviewedSha;
159374
159710
  ctx.toolState.wasUpdated = true;
159375
159711
  await deleteProgressComment(ctx).catch((err) => {
159376
159712
  log.debug(`progress comment cleanup after review failed: ${err}`);
@@ -160792,7 +161128,7 @@ function isMetadataYarnClassic(metadataPath) {
160792
161128
  }
160793
161129
 
160794
161130
  // utils/packageManager.ts
160795
- var import_semver2 = __toESM(require_semver2(), 1);
161131
+ var import_semver3 = __toESM(require_semver2(), 1);
160796
161132
  import { existsSync as existsSync5 } from "node:fs";
160797
161133
  import { mkdir, readFile as readFile2 } from "node:fs/promises";
160798
161134
  import { delimiter, join as join16 } from "node:path";
@@ -160814,7 +161150,7 @@ function parsePackageManagerField(value2) {
160814
161150
  return {
160815
161151
  name,
160816
161152
  version: version3,
160817
- concrete: import_semver2.default.valid(version3) !== null,
161153
+ concrete: import_semver3.default.valid(version3) !== null,
160818
161154
  source: "packageManager"
160819
161155
  };
160820
161156
  }
@@ -160828,7 +161164,7 @@ function parseDevEnginesField(field) {
160828
161164
  return {
160829
161165
  name: field.name,
160830
161166
  version: version3,
160831
- concrete: import_semver2.default.valid(version3) !== null,
161167
+ concrete: import_semver3.default.valid(version3) !== null,
160832
161168
  source: "devEngines"
160833
161169
  };
160834
161170
  }
@@ -160862,7 +161198,7 @@ async function resolvePackageManagerSpec(cwd) {
160862
161198
  }
160863
161199
  return devSpec;
160864
161200
  }
160865
- if (pmSpec.concrete && import_semver2.default.satisfies(pmSpec.version, devSpec.version)) {
161201
+ if (pmSpec.concrete && import_semver3.default.satisfies(pmSpec.version, devSpec.version)) {
160866
161202
  return pmSpec;
160867
161203
  }
160868
161204
  if (pmSpec.concrete) {
@@ -161074,8 +161410,15 @@ ${errorMessage}`]
161074
161410
  };
161075
161411
 
161076
161412
  // prep/installPythonDependencies.ts
161077
- import { existsSync as existsSync7 } from "node:fs";
161413
+ import { existsSync as existsSync7, readFileSync as readFileSync5 } from "node:fs";
161078
161414
  import { join as join18 } from "node:path";
161415
+ function declaresBuildSystem(path4) {
161416
+ return /^\s*\[\s*build-system\s*\]/m.test(readFileSync5(path4, "utf8"));
161417
+ }
161418
+ function configApplies(config3, cwd) {
161419
+ const path4 = join18(cwd, config3.file);
161420
+ return existsSync7(path4) && (config3.applies?.(path4) ?? true);
161421
+ }
161079
161422
  var PYTHON_CONFIGS = [
161080
161423
  {
161081
161424
  file: "requirements.txt",
@@ -161085,7 +161428,8 @@ var PYTHON_CONFIGS = [
161085
161428
  {
161086
161429
  file: "pyproject.toml",
161087
161430
  tool: "pip",
161088
- installCmd: ["pip", "install", "."]
161431
+ installCmd: ["pip", "install", "."],
161432
+ applies: declaresBuildSystem
161089
161433
  },
161090
161434
  {
161091
161435
  file: "Pipfile",
@@ -161147,11 +161491,11 @@ var installPythonDependencies = {
161147
161491
  return false;
161148
161492
  }
161149
161493
  const cwd = process.cwd();
161150
- return PYTHON_CONFIGS.some((config3) => existsSync7(join18(cwd, config3.file)));
161494
+ return PYTHON_CONFIGS.some((config3) => configApplies(config3, cwd));
161151
161495
  },
161152
161496
  run: async (options) => {
161153
161497
  const cwd = process.cwd();
161154
- const config3 = PYTHON_CONFIGS.find((c2) => existsSync7(join18(cwd, c2.file)));
161498
+ const config3 = PYTHON_CONFIGS.find((c2) => configApplies(c2, cwd));
161155
161499
  if (!config3) {
161156
161500
  return {
161157
161501
  language: "python",
@@ -163602,12 +163946,17 @@ function resolveModel(ctx) {
163602
163946
  if (envModel) {
163603
163947
  return resolveSlug(envModel) ?? envModel;
163604
163948
  }
163605
- if (ctx.slug) {
163606
- const resolved = resolveSlug(ctx.slug);
163949
+ const slug2 = ctx.slug?.trim();
163950
+ if (slug2) {
163951
+ const resolved = resolveSlug(slug2);
163607
163952
  if (resolved) {
163608
163953
  return resolved;
163609
163954
  }
163610
- log.warning(`\xBB unknown model slug "${ctx.slug}" \u2014 agent will auto-select`);
163955
+ if (slug2.includes("/")) {
163956
+ log.info(`\xBB "${slug2}" is not a curated alias \u2014 passing through as a raw model specifier`);
163957
+ return slug2;
163958
+ }
163959
+ log.warning(`\xBB unknown model slug "${slug2}" \u2014 agent will auto-select`);
163611
163960
  }
163612
163961
  return void 0;
163613
163962
  }
@@ -164170,6 +164519,8 @@ Never use \`sleep\` to wait for commands to complete. Commands run synchronously
164170
164519
 
164171
164520
  When posting comments via ${pullfrogMcpName}, write as a professional team member would. Your final comments should be polished and actionable \u2014 do not include intermediate reasoning like "I'll now look at the code" or "Let me respond to the question."
164172
164521
 
164522
+ Never \`@\`-mention a GitHub username unless that exact handle appears in the user's request or the event context. GitHub already notifies the author and thread participants, so write "the author" or omit it.
164523
+
164173
164524
  When embedding images (e.g. uploaded screenshots) in comments or PR bodies, always use markdown image syntax: \`![description](url)\`. Never paste a naked URL \u2014 it will not render as an image.
164174
164525
 
164175
164526
  ### Progress reporting
@@ -164479,7 +164830,7 @@ function buildModelAccessError(input) {
164479
164830
  }
164480
164831
 
164481
164832
  // utils/normalizeEnv.ts
164482
- var core4 = __toESM(require_core(), 1);
164833
+ var core5 = __toESM(require_core(), 1);
164483
164834
  function sanitizeSecret(key, value2) {
164484
164835
  const trimmed = value2.trim();
164485
164836
  if (trimmed.length === 0) {
@@ -164493,7 +164844,7 @@ function sanitizeSecret(key, value2) {
164493
164844
  `\xBB stripped whitespace from ${key} (whitespace in secret values breaks GitHub Actions log masking)`
164494
164845
  );
164495
164846
  }
164496
- core4.setSecret(trimmed);
164847
+ core5.setSecret(trimmed);
164497
164848
  return trimmed;
164498
164849
  }
164499
164850
  function normalizeEnv() {
@@ -164537,7 +164888,7 @@ function normalizeEnv() {
164537
164888
  }
164538
164889
 
164539
164890
  // utils/overrides.ts
164540
- var core5 = __toESM(require_core(), 1);
164891
+ var core6 = __toESM(require_core(), 1);
164541
164892
  var DENIED_OVERRIDE_NAMES = /* @__PURE__ */ new Set([
164542
164893
  "GITHUB_TOKEN",
164543
164894
  "GH_TOKEN",
@@ -164583,7 +164934,7 @@ function applyOverrides(params) {
164583
164934
  denied.push(key);
164584
164935
  continue;
164585
164936
  }
164586
- if (value2.length > 0) core5.setSecret(value2);
164937
+ if (value2.length > 0) core6.setSecret(value2);
164587
164938
  params.env[key] = value2;
164588
164939
  applied.push(key);
164589
164940
  }
@@ -164591,168 +164942,6 @@ function applyOverrides(params) {
164591
164942
  return { applied, denied };
164592
164943
  }
164593
164944
 
164594
- // utils/payload.ts
164595
- var core6 = __toESM(require_core(), 1);
164596
- import { isAbsolute as isAbsolute2, resolve as resolve2 } from "node:path";
164597
-
164598
- // utils/versioning.ts
164599
- var import_semver3 = __toESM(require_semver2(), 1);
164600
- var COMPATIBILITY_POLICY = "non-breaking";
164601
- function validateCompatibility(payloadVersion, actionVersion) {
164602
- const payloadSemVer = import_semver3.default.parse(payloadVersion);
164603
- if (!payloadSemVer)
164604
- throw new Error(`Payload version ${payloadVersion} is not a valid semantic version.`);
164605
- const major = payloadSemVer.major;
164606
- const minor = payloadSemVer.minor;
164607
- const patch = payloadSemVer.patch;
164608
- const compatibilityRange = COMPATIBILITY_POLICY === "same-features" ? `^${major}.${minor}.${major === 0 ? patch : 0}` : `^${major}.${major === 0 ? minor : 0}.${major === 0 ? "x" : 0}`;
164609
- if (!import_semver3.default.satisfies(actionVersion, compatibilityRange)) {
164610
- throw new Error(
164611
- `Payload version ${payloadVersion} is incompatible with action version ${actionVersion}. Please update your workflow to use at least ${import_semver3.default.minVersion(compatibilityRange)} version of the action.`
164612
- );
164613
- }
164614
- }
164615
-
164616
- // utils/payload.ts
164617
- var ShellPermissionInput = type.enumerated("disabled", "restricted", "enabled");
164618
- var PushPermissionInput = type.enumerated("disabled", "restricted", "enabled");
164619
- var JsonPayload = type({
164620
- "~pullfrog": "true",
164621
- version: "string",
164622
- "model?": "string | undefined",
164623
- "modelExplicit?": "boolean | undefined",
164624
- prompt: "string",
164625
- "triggerer?": "string | undefined",
164626
- "baseInstructions?": "string | undefined",
164627
- "eventInstructions?": "string",
164628
- "previousRunsNote?": "string",
164629
- "event?": "object",
164630
- "xrepo?": type({
164631
- mode: "'all' | 'explicit'",
164632
- read: "string[]",
164633
- write: "string[]",
164634
- // optional so a payload from an older server build (pre-`unavailable`)
164635
- // still parses against a newer action across a rolling deploy.
164636
- "unavailable?": "string[]"
164637
- }).or("undefined"),
164638
- "timeout?": "string | undefined",
164639
- "progressComment?": type({
164640
- id: "string",
164641
- type: "'issue' | 'review'"
164642
- }).or("undefined"),
164643
- "generateSummary?": "boolean | undefined"
164644
- });
164645
- var COLLABORATOR_PERMISSIONS = ["admin", "maintain", "write"];
164646
- function isCollaborator(event) {
164647
- const perm = event.authorPermission;
164648
- return perm !== void 0 && COLLABORATOR_PERMISSIONS.includes(perm);
164649
- }
164650
- var Inputs = type({
164651
- prompt: "string",
164652
- "model?": type.string.or("undefined"),
164653
- "timeout?": type.string.or("undefined"),
164654
- "push?": PushPermissionInput.or("undefined"),
164655
- "shell?": ShellPermissionInput.or("undefined"),
164656
- "cwd?": type.string.or("undefined"),
164657
- "output_schema?": type.string.or("undefined")
164658
- });
164659
- function isPayloadEvent(value2) {
164660
- return typeof value2 === "object" && value2 !== null && "trigger" in value2;
164661
- }
164662
- function resolveCwd(cwd) {
164663
- const workspace = process.env.GITHUB_WORKSPACE;
164664
- if (!cwd) return workspace;
164665
- if (isAbsolute2(cwd)) return cwd;
164666
- return workspace ? resolve2(workspace, cwd) : cwd;
164667
- }
164668
- function resolvePromptInput() {
164669
- const prompt = core6.getInput("prompt", { required: true });
164670
- let parsed2;
164671
- try {
164672
- parsed2 = JSON.parse(prompt);
164673
- } catch {
164674
- return prompt;
164675
- }
164676
- if (!parsed2 || typeof parsed2 !== "object" || !("~pullfrog" in parsed2)) {
164677
- return prompt;
164678
- }
164679
- const jsonPayload = JsonPayload.assert(parsed2);
164680
- validateCompatibility(jsonPayload.version, package_default.version);
164681
- return jsonPayload;
164682
- }
164683
- function resolveNonPromptInputs() {
164684
- return Inputs.omit("prompt").assert({
164685
- model: core6.getInput("model") || void 0,
164686
- timeout: core6.getInput("timeout") || void 0,
164687
- cwd: core6.getInput("cwd") || void 0,
164688
- push: core6.getInput("push") || void 0,
164689
- shell: core6.getInput("shell") || void 0
164690
- });
164691
- }
164692
- var isPullfrog = (actor) => {
164693
- actor = actor?.replace("[bot]", "");
164694
- return !!actor && (actor === "pullfrog" || actor === "pullfrogdev");
164695
- };
164696
- function resolvePayload(resolvedPromptInput, repoSettings) {
164697
- const [prompt, jsonPayload] = typeof resolvedPromptInput !== "string" ? [resolvedPromptInput.prompt, resolvedPromptInput] : [resolvedPromptInput, void 0];
164698
- const inputs = resolveNonPromptInputs();
164699
- const rawEvent = jsonPayload?.event;
164700
- const event = isPayloadEvent(rawEvent) ? rawEvent : { trigger: "unknown" };
164701
- const model = jsonPayload?.model ?? inputs.model ?? repoSettings.model ?? void 0;
164702
- const isNonCollaborator = !isCollaborator(event);
164703
- const repoShell = repoSettings.shell ?? "restricted";
164704
- const inputShell = inputs.shell;
164705
- let resolvedShell = repoShell;
164706
- if (inputShell === "disabled") {
164707
- resolvedShell = "disabled";
164708
- } else if (inputShell === "restricted" && resolvedShell === "enabled") {
164709
- resolvedShell = "restricted";
164710
- }
164711
- if (isNonCollaborator && resolvedShell === "enabled") {
164712
- resolvedShell = "restricted";
164713
- }
164714
- return {
164715
- "~pullfrog": true,
164716
- version: jsonPayload?.version ?? package_default.version,
164717
- model,
164718
- // explicit only when the model came from a per-run override flag (carried on
164719
- // the JSON payload). a GHA `model` input or the repo default is not explicit.
164720
- modelExplicit: jsonPayload?.modelExplicit ?? false,
164721
- prompt,
164722
- triggerer: jsonPayload?.triggerer ?? // it's not a common use case but GITHUB_ACTOR can be a user when the workflow is manually triggered by a user through GitHub Actions UI
164723
- (!isPullfrog(process.env.GITHUB_ACTOR) ? process.env.GITHUB_ACTOR : void 0),
164724
- baseInstructions: jsonPayload?.baseInstructions,
164725
- eventInstructions: jsonPayload?.eventInstructions,
164726
- previousRunsNote: jsonPayload?.previousRunsNote,
164727
- event,
164728
- xrepo: jsonPayload?.xrepo,
164729
- timeout: inputs.timeout ?? jsonPayload?.timeout,
164730
- cwd: resolveCwd(inputs.cwd),
164731
- progressComment: jsonPayload?.progressComment,
164732
- generateSummary: jsonPayload?.generateSummary,
164733
- // permissions: inputs > repoSettings > fallbacks
164734
- push: inputs.push ?? repoSettings.push ?? "restricted",
164735
- shell: resolvedShell,
164736
- // set by proxy logic in main.ts when routing through OpenRouter
164737
- proxyModel: void 0
164738
- };
164739
- }
164740
- function resolveOutputSchema() {
164741
- const raw2 = core6.getInput("output_schema");
164742
- if (!raw2) return void 0;
164743
- let parsed2;
164744
- try {
164745
- parsed2 = JSON.parse(raw2);
164746
- } catch {
164747
- throw new Error(`invalid output_schema: not valid JSON`);
164748
- }
164749
- if (!parsed2 || typeof parsed2 !== "object" || Array.isArray(parsed2)) {
164750
- throw new Error(`invalid output_schema: must be a JSON object`);
164751
- }
164752
- log.info("\xBB structured output schema provided \u2014 output will be required");
164753
- return parsed2;
164754
- }
164755
-
164756
164945
  // utils/proxy.ts
164757
164946
  var core8 = __toESM(require_core(), 1);
164758
164947
 
@@ -164903,7 +165092,10 @@ async function resolveTokens(params) {
164903
165092
  contents: "write",
164904
165093
  pull_requests: "write",
164905
165094
  issues: "write",
164906
- checks: "read",
165095
+ // write (not read) so the run can post `pullfrog` / `pullfrog-approval`
165096
+ // commit-status check-runs for branch protection. the app already grants
165097
+ // checks:write; this scopes the MCP token up to use it.
165098
+ checks: "write",
164907
165099
  actions: "read"
164908
165100
  };
164909
165101
  const mcpToken = await acquireNewToken({ repos: writeRepos, permissions: mcpPermissions });
@@ -165012,7 +165204,9 @@ async function resolveTokens(params) {
165012
165204
  };
165013
165205
  const removeSignalHandler = onExitSignal(dispose);
165014
165206
  return {
165015
- gitToken,
165207
+ get gitToken() {
165208
+ return currentGitToken;
165209
+ },
165016
165210
  mcpToken,
165017
165211
  readToken,
165018
165212
  refreshGitToken,
@@ -165365,6 +165559,7 @@ var defaultSettings = {
165365
165559
  push: "restricted",
165366
165560
  shell: "restricted",
165367
165561
  prApproveEnabled: false,
165562
+ autoMergeEnabled: false,
165368
165563
  signedCommits: false,
165369
165564
  modeInstructions: {},
165370
165565
  learnings: null,
@@ -165452,7 +165647,11 @@ async function resolveRunContextData(params) {
165452
165647
  } catch {
165453
165648
  }
165454
165649
  const [repoResponse, runContext] = await Promise.all([
165455
- params.octokit.repos.get({ owner: repoContext.owner, repo: repoContext.name }),
165650
+ op(() => params.octokit.repos.get({ owner: repoContext.owner, repo: repoContext.name }), {
165651
+ name: "repos.get",
165652
+ retries: [100, 500],
165653
+ bail: (error49) => !isTransientOctokitError(error49)
165654
+ })(),
165456
165655
  fetchRunContext({ token: params.token, repoContext, oidcToken })
165457
165656
  ]);
165458
165657
  return {
@@ -165681,6 +165880,108 @@ ${genericBody}`,
165681
165880
  // utils/runLifecycle.ts
165682
165881
  var core10 = __toESM(require_core(), 1);
165683
165882
 
165883
+ // utils/checksGate.ts
165884
+ var OK_CONCLUSIONS = /* @__PURE__ */ new Set(["success", "neutral", "skipped"]);
165885
+ var PULLFROG_CHECKS = /* @__PURE__ */ new Set(["pullfrog", "pullfrog-approval"]);
165886
+ function checkRunsGate(params) {
165887
+ for (const run4 of params.checkRuns) {
165888
+ if (PULLFROG_CHECKS.has(run4.name.toLowerCase())) continue;
165889
+ if (run4.status !== "completed")
165890
+ return { ok: false, reason: `check "${run4.name}" ${run4.status}` };
165891
+ if (!OK_CONCLUSIONS.has(run4.conclusion ?? "")) {
165892
+ return { ok: false, reason: `check "${run4.name}" concluded ${run4.conclusion}` };
165893
+ }
165894
+ }
165895
+ return { ok: true, reason: "all checks passed" };
165896
+ }
165897
+ function hasVerifiedCheck(params) {
165898
+ return params.checkRuns.some(
165899
+ (r) => !PULLFROG_CHECKS.has(r.name.toLowerCase()) && r.status === "completed"
165900
+ );
165901
+ }
165902
+
165903
+ // utils/autoMerge.ts
165904
+ var NON_MERGEABLE_STATES = /* @__PURE__ */ new Set(["dirty", "behind", "blocked"]);
165905
+ function hasBlockingHumanReview(reviews) {
165906
+ const latestByReviewer = /* @__PURE__ */ new Map();
165907
+ for (const review of reviews) {
165908
+ const login = review.user?.login;
165909
+ if (!login || isPullfrog(login)) continue;
165910
+ if (review.state === "APPROVED" || review.state === "CHANGES_REQUESTED") {
165911
+ latestByReviewer.set(login, review.state);
165912
+ } else if (review.state === "DISMISSED") {
165913
+ latestByReviewer.delete(login);
165914
+ }
165915
+ }
165916
+ for (const state of latestByReviewer.values()) {
165917
+ if (state === "CHANGES_REQUESTED") return true;
165918
+ }
165919
+ return false;
165920
+ }
165921
+ async function autoMergeAfterApprove(ctx) {
165922
+ if (!ctx.autoMergeEnabled) return;
165923
+ if (!ctx.prApproveEnabled) return;
165924
+ const approval = ctx.toolState.approval;
165925
+ if (!approval?.wouldApprove) return;
165926
+ const pullNumber = ctx.payload.event.issue_number;
165927
+ if (typeof pullNumber !== "number") return;
165928
+ const skip = (reason) => log.info(
165929
+ `autoMerge: pr=#${pullNumber} approvalSha=${approval.sha?.slice(0, 7)} \u2192 skipped: ${reason}`
165930
+ );
165931
+ const pr = await ctx.octokit.rest.pulls.get({
165932
+ owner: ctx.repo.owner,
165933
+ repo: ctx.repo.name,
165934
+ pull_number: pullNumber
165935
+ });
165936
+ if (pr.data.state !== "open") return skip("pr not open");
165937
+ if (pr.data.draft) return skip("pr is draft");
165938
+ const headSha = pr.data.head.sha;
165939
+ if (approval.sha !== headSha) return skip(`approval sha != head ${headSha.slice(0, 7)}`);
165940
+ const outstanding = await countOutstandingPullfrogThreads(ctx, pullNumber);
165941
+ if (outstanding > 0) return skip(`${outstanding} unresolved Pullfrog thread(s)`);
165942
+ const reviews = await ctx.octokit.paginate(ctx.octokit.rest.pulls.listReviews, {
165943
+ owner: ctx.repo.owner,
165944
+ repo: ctx.repo.name,
165945
+ pull_number: pullNumber,
165946
+ per_page: 100
165947
+ });
165948
+ if (hasBlockingHumanReview(reviews)) return skip("human changes requested");
165949
+ if (pr.data.mergeable !== true) return skip(`not mergeable (mergeable=${pr.data.mergeable})`);
165950
+ if (pr.data.mergeable_state && NON_MERGEABLE_STATES.has(pr.data.mergeable_state)) {
165951
+ return skip(`mergeable_state=${pr.data.mergeable_state}`);
165952
+ }
165953
+ const checkRuns = await ctx.octokit.paginate(ctx.octokit.rest.checks.listForRef, {
165954
+ owner: ctx.repo.owner,
165955
+ repo: ctx.repo.name,
165956
+ ref: headSha,
165957
+ per_page: 100
165958
+ });
165959
+ const gate = checkRunsGate({ checkRuns });
165960
+ if (!gate.ok) return skip(`ci: ${gate.reason}`);
165961
+ if (!hasVerifiedCheck({ checkRuns })) return skip("no completed external check-run to verify");
165962
+ const merge4 = await ctx.octokit.rest.pulls.merge({
165963
+ owner: ctx.repo.owner,
165964
+ repo: ctx.repo.name,
165965
+ pull_number: pullNumber,
165966
+ sha: headSha,
165967
+ merge_method: "squash",
165968
+ commit_title: `${pr.data.title} (#${pullNumber})`,
165969
+ commit_message: "merged autonomously by Pullfrog after a clean approval."
165970
+ });
165971
+ log.info(
165972
+ `autoMerge: pr=#${pullNumber} approvalSha=${headSha.slice(0, 7)} outstanding=0 \u2192 merged ${merge4.data.sha?.slice(0, 7)}`
165973
+ );
165974
+ await ctx.octokit.rest.issues.createComment({
165975
+ owner: ctx.repo.owner,
165976
+ repo: ctx.repo.name,
165977
+ issue_number: pullNumber,
165978
+ body: addFooter(
165979
+ ctx,
165980
+ "> \u2705 Merged autonomously after Pullfrog approved this PR and resolved all of its own review threads."
165981
+ )
165982
+ }).catch((err) => log.debug(`autoMerge comment failed: ${err}`));
165983
+ }
165984
+
165684
165985
  // utils/reviewCleanup.ts
165685
165986
  var RE_REVIEW_PREAMBLE = "Incrementally re-review the new commits on this pull request. Use the IncrementalReview mode.";
165686
165987
  async function postReviewCleanup(ctx) {
@@ -165737,13 +166038,20 @@ async function dispatchFollowUpReReview(ctx, reviewedSha) {
165737
166038
  eventInstructions: RE_REVIEW_PREAMBLE,
165738
166039
  event
165739
166040
  };
165740
- await ctx.octokit.rest.actions.createWorkflowDispatch({
165741
- owner: ctx.repo.owner,
165742
- repo: ctx.repo.name,
165743
- workflow_id: getCurrentWorkflowFilename(),
165744
- ref: pr.data.base.repo.default_branch,
165745
- inputs: { prompt: JSON.stringify(payload) }
165746
- });
166041
+ await op(
166042
+ () => ctx.octokit.rest.actions.createWorkflowDispatch({
166043
+ owner: ctx.repo.owner,
166044
+ repo: ctx.repo.name,
166045
+ workflow_id: getCurrentWorkflowFilename(),
166046
+ ref: pr.data.base.repo.default_branch,
166047
+ inputs: { prompt: JSON.stringify(payload) }
166048
+ }),
166049
+ {
166050
+ name: "reReviewDispatch",
166051
+ retries: [500, 2e3],
166052
+ bail: (error49) => !isTransientOctokitError(error49)
166053
+ }
166054
+ )();
165747
166055
  }
165748
166056
  function getCurrentWorkflowFilename() {
165749
166057
  const ref = process.env.GITHUB_WORKFLOW_REF ?? "";
@@ -165751,6 +166059,62 @@ function getCurrentWorkflowFilename() {
165751
166059
  return match3?.[1] ?? "pullfrog.yml";
165752
166060
  }
165753
166061
 
166062
+ // utils/statusChecks.ts
166063
+ var COMPLETION_CHECK = "pullfrog";
166064
+ var APPROVAL_CHECK = "pullfrog-approval";
166065
+ async function createCheckRun(ctx, params) {
166066
+ const createParams = {
166067
+ owner: ctx.repo.owner,
166068
+ repo: ctx.repo.name,
166069
+ name: params.name,
166070
+ head_sha: params.headSha,
166071
+ status: "completed",
166072
+ conclusion: params.conclusion,
166073
+ output: { title: params.title, summary: params.summary }
166074
+ };
166075
+ if (ctx.runId) {
166076
+ createParams.details_url = `https://github.com/${ctx.repo.owner}/${ctx.repo.name}/actions/runs/${ctx.runId}`;
166077
+ }
166078
+ await ctx.octokit.rest.checks.create(createParams);
166079
+ log.info(`\xBB posted ${params.name} check (${params.conclusion}) on ${params.headSha.slice(0, 7)}`);
166080
+ }
166081
+ async function reportStatusChecks(ctx, params) {
166082
+ if (!ctx.payload.statusChecks) return;
166083
+ const event = ctx.payload.event;
166084
+ const pullNumber = event.issue_number;
166085
+ if (event.is_pr !== true || typeof pullNumber !== "number") return;
166086
+ let headSha;
166087
+ try {
166088
+ const pr = await ctx.octokit.rest.pulls.get({
166089
+ owner: ctx.repo.owner,
166090
+ repo: ctx.repo.name,
166091
+ pull_number: pullNumber
166092
+ });
166093
+ headSha = pr.data.head.sha;
166094
+ } catch (err) {
166095
+ log.debug(`status checks: failed to resolve PR #${pullNumber} head sha: ${err}`);
166096
+ return;
166097
+ }
166098
+ const completionSha = primaryRepoState(ctx.toolState).checkoutSha ?? headSha;
166099
+ await createCheckRun(ctx, {
166100
+ name: COMPLETION_CHECK,
166101
+ headSha: completionSha,
166102
+ conclusion: params.runSucceeded ? "success" : "failure",
166103
+ title: params.runSucceeded ? "Pullfrog run completed" : "Pullfrog run failed",
166104
+ summary: params.runSucceeded ? "The Pullfrog run finished successfully." : "The Pullfrog run failed or timed out. See the run logs for details."
166105
+ }).catch((err) => log.debug(`status checks: ${COMPLETION_CHECK} post failed: ${err}`));
166106
+ const approval = ctx.toolState.approval;
166107
+ if (params.runSucceeded && approval) {
166108
+ await createCheckRun(ctx, {
166109
+ name: APPROVAL_CHECK,
166110
+ headSha: approval.sha ?? headSha,
166111
+ conclusion: approval.wouldApprove ? "success" : "failure",
166112
+ title: approval.wouldApprove ? "Pullfrog would approve" : "Pullfrog would not approve",
166113
+ summary: approval.wouldApprove ? "Pullfrog has no outstanding review feedback on this PR." : "Pullfrog has outstanding review feedback or requested changes on this PR."
166114
+ }).catch((err) => log.debug(`status checks: ${APPROVAL_CHECK} post failed: ${err}`));
166115
+ }
166116
+ }
166117
+
165754
166118
  // utils/runLifecycle.ts
165755
166119
  async function persistRunArtifacts(toolContext) {
165756
166120
  await postReviewCleanup(toolContext).catch((error49) => {
@@ -165794,6 +166158,17 @@ async function finalizeSuccessRun(input) {
165794
166158
  log.info(`::pullfrog-output::${Buffer.from(input.toolState.output).toString("base64")}`);
165795
166159
  core10.setOutput("result", input.toolState.output);
165796
166160
  }
166161
+ if (input.result.success) {
166162
+ await approveAfterFix(input.toolContext).catch((error49) => {
166163
+ log.debug(`fix auto-approval failed: ${error49}`);
166164
+ });
166165
+ await autoMergeAfterApprove(input.toolContext).catch((error49) => {
166166
+ const raced = typeof error49 === "object" && error49 !== null && "status" in error49 && error49.status === 409;
166167
+ if (raced) log.debug(`auto-merge skipped (head moved): ${error49}`);
166168
+ else log.warning(`auto-merge failed: ${error49}`);
166169
+ });
166170
+ }
166171
+ await reportStatusChecks(input.toolContext, { runSucceeded: input.result.success });
165797
166172
  }
165798
166173
  async function writeRunErrorOutputs(input) {
165799
166174
  try {
@@ -166220,7 +166595,11 @@ async function main() {
166220
166595
  get githubInstallationToken() {
166221
166596
  return getGitHubInstallationToken();
166222
166597
  },
166223
- gitToken: tokenRef.gitToken,
166598
+ // live getter, same reason as #891 above — reads the current git token
166599
+ // (canonical rationale on TokenRef.gitToken). see #964.
166600
+ get gitToken() {
166601
+ return tokenRef.gitToken;
166602
+ },
166224
166603
  refreshGitToken: tokenRef.refreshGitToken,
166225
166604
  readToken: tokenRef.readToken,
166226
166605
  xrepo: payload.xrepo,
@@ -166229,6 +166608,7 @@ async function main() {
166229
166608
  postCheckoutScript: runContext.repoSettings.postCheckoutScript,
166230
166609
  prepushScript: runContext.repoSettings.prepushScript,
166231
166610
  prApproveEnabled: runContext.repoSettings.prApproveEnabled,
166611
+ autoMergeEnabled: runContext.repoSettings.autoMergeEnabled,
166232
166612
  signedCommits: runContext.repoSettings.signedCommits,
166233
166613
  modeInstructions: runContext.repoSettings.modeInstructions,
166234
166614
  toolState,
@@ -166470,6 +166850,7 @@ ${instructions.user}` : null,
166470
166850
  await writeRunErrorOutputs({ rendered, toolState });
166471
166851
  if (toolContext) {
166472
166852
  await persistRunArtifacts(toolContext);
166853
+ await reportStatusChecks(toolContext, { runSucceeded: false });
166473
166854
  }
166474
166855
  return {
166475
166856
  success: false,
@@ -167384,7 +167765,7 @@ async function run2() {
167384
167765
  }
167385
167766
 
167386
167767
  // cli.ts
167387
- var VERSION10 = "0.1.31";
167768
+ var VERSION10 = "0.1.33";
167388
167769
  var bin = basename2(process.argv[1] || "");
167389
167770
  var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
167390
167771
  var rawArgs = process.argv.slice(2);