pullfrog 0.1.32 → 0.1.34

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/index.js CHANGED
@@ -101041,7 +101041,7 @@ var import_semver = __toESM(require_semver2(), 1);
101041
101041
  // package.json
101042
101042
  var package_default = {
101043
101043
  name: "pullfrog",
101044
- version: "0.1.32",
101044
+ version: "0.1.34",
101045
101045
  type: "module",
101046
101046
  bin: {
101047
101047
  pullfrog: "dist/cli.mjs",
@@ -157622,13 +157622,14 @@ async function approveAfterFix(ctx) {
157622
157622
  });
157623
157623
  if (pr.data.state !== "open") return;
157624
157624
  const headSha = pr.data.head.sha;
157625
+ const selfAuthored = isPullfrog(pr.data.user?.login);
157625
157626
  const summary2 = ctx.toolState.lastProgressBody?.trim();
157626
157627
  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.");
157627
157628
  const params = {
157628
157629
  owner: ctx.repo.owner,
157629
157630
  repo: ctx.repo.name,
157630
157631
  pull_number: pullNumber,
157631
- event: "APPROVE",
157632
+ event: selfAuthored ? "COMMENT" : "APPROVE",
157632
157633
  commit_id: headSha
157633
157634
  };
157634
157635
  const result = await createAndSubmitWithFooter(ctx, params, {
@@ -157800,22 +157801,39 @@ function CreatePullRequestReviewTool(ctx) {
157800
157801
  );
157801
157802
  }
157802
157803
  }
157804
+ const prSnapshot = (await ctx.octokit.rest.pulls.get({
157805
+ owner: ctx.repo.owner,
157806
+ repo: ctx.repo.name,
157807
+ pull_number
157808
+ })).data;
157809
+ const selfAuthored = isPullfrog(prSnapshot.user?.login);
157803
157810
  ctx.toolState.approval = { wouldApprove: approved === true, sha: primary.checkoutSha };
157804
157811
  const skip = reviewSkipDecision({
157805
157812
  approved: approved ?? false,
157806
157813
  requestChanges: request_changes ?? false,
157807
157814
  body,
157808
157815
  hasComments: comments.length > 0,
157809
- prApproveEnabled: ctx.prApproveEnabled
157816
+ // a self-authored approve downgrades to COMMENT (GitHub blocks a
157817
+ // self-APPROVE), so treat it as non-binding here — an empty self-
157818
+ // approve is then skipped (the verdict is still recorded above)
157819
+ // instead of POSTing an empty COMMENT that GitHub 422s.
157820
+ prApproveEnabled: ctx.prApproveEnabled && !selfAuthored
157810
157821
  });
157811
157822
  if (skip) {
157812
157823
  log.info(`skipping review submission: ${skip.reason}`);
157813
157824
  return { success: true, skipped: true, reason: skip.reason };
157814
157825
  }
157815
157826
  let event = approved ? "APPROVE" : request_changes ? "REQUEST_CHANGES" : "COMMENT";
157816
- if ((event === "APPROVE" || event === "REQUEST_CHANGES") && !ctx.prApproveEnabled) {
157817
- log.info(`prApproveEnabled is disabled \u2014 downgrading ${event} to COMMENT`);
157818
- event = "COMMENT";
157827
+ if (event === "APPROVE" || event === "REQUEST_CHANGES") {
157828
+ if (!ctx.prApproveEnabled) {
157829
+ log.info(`prApproveEnabled is disabled \u2014 downgrading ${event} to COMMENT`);
157830
+ event = "COMMENT";
157831
+ } else if (selfAuthored) {
157832
+ log.info(
157833
+ `self-authored PR \u2014 downgrading binding ${event} to COMMENT (verdict recorded internally)`
157834
+ );
157835
+ event = "COMMENT";
157836
+ }
157819
157837
  }
157820
157838
  const params = {
157821
157839
  owner: ctx.repo.owner,
@@ -157827,12 +157845,7 @@ function CreatePullRequestReviewTool(ctx) {
157827
157845
  if (commit_id) {
157828
157846
  params.commit_id = commit_id;
157829
157847
  } else {
157830
- const pr = await ctx.octokit.rest.pulls.get({
157831
- owner: ctx.repo.owner,
157832
- repo: ctx.repo.name,
157833
- pull_number
157834
- });
157835
- latestHeadSha = pr.data.head.sha;
157848
+ latestHeadSha = prSnapshot.head.sha;
157836
157849
  params.commit_id = primary.checkoutSha ?? latestHeadSha;
157837
157850
  if (primary.checkoutSha && latestHeadSha !== primary.checkoutSha) {
157838
157851
  log.info(
@@ -157900,6 +157913,7 @@ function CreatePullRequestReviewTool(ctx) {
157900
157913
  }
157901
157914
  )();
157902
157915
  } catch (err) {
157916
+ if (ctx.toolState.approval) ctx.toolState.approval.wouldApprove = false;
157903
157917
  if (isTransientReviewError(err)) {
157904
157918
  const rawMsg2 = err instanceof Error ? err.message : String(err);
157905
157919
  throw new Error(
@@ -163544,6 +163558,11 @@ async function mintProxyKey(ctx) {
163544
163558
  body?.error ?? "billing service temporarily unavailable \u2014 retry shortly"
163545
163559
  );
163546
163560
  }
163561
+ if (response.status === 404) {
163562
+ throw new TransientError(
163563
+ "Pullfrog couldn't match this repository to its account \u2014 it may have just been renamed or transferred. The link refreshes automatically on the next run; if it keeps failing, reinstall the GitHub App on the repo's current owner."
163564
+ );
163565
+ }
163547
163566
  if (!response.ok) {
163548
163567
  log.warning(`proxy key mint failed (${response.status})`);
163549
163568
  return null;
@@ -163787,6 +163806,7 @@ var defaultSettings = {
163787
163806
  push: "restricted",
163788
163807
  shell: "restricted",
163789
163808
  prApproveEnabled: false,
163809
+ autoMergeEnabled: false,
163790
163810
  signedCommits: false,
163791
163811
  modeInstructions: {},
163792
163812
  learnings: null,
@@ -164107,6 +164127,117 @@ ${genericBody}`,
164107
164127
  // utils/runLifecycle.ts
164108
164128
  var core10 = __toESM(require_core(), 1);
164109
164129
 
164130
+ // utils/autoMerge.ts
164131
+ function hasBlockingHumanReview(reviews) {
164132
+ const latestByReviewer = /* @__PURE__ */ new Map();
164133
+ for (const review of reviews) {
164134
+ const login = review.user?.login;
164135
+ if (!login || isPullfrog(login)) continue;
164136
+ if (review.state === "APPROVED" || review.state === "CHANGES_REQUESTED") {
164137
+ latestByReviewer.set(login, review.state);
164138
+ } else if (review.state === "DISMISSED") {
164139
+ latestByReviewer.delete(login);
164140
+ }
164141
+ }
164142
+ for (const state of latestByReviewer.values()) {
164143
+ if (state === "CHANGES_REQUESTED") return true;
164144
+ }
164145
+ return false;
164146
+ }
164147
+ var ENABLE_AUTO_MERGE = (
164148
+ /* GraphQL */
164149
+ `
164150
+ mutation ($pullRequestId: ID!, $headSha: GitObjectID!) {
164151
+ enablePullRequestAutoMerge(input: {
164152
+ pullRequestId: $pullRequestId
164153
+ mergeMethod: SQUASH
164154
+ expectedHeadOid: $headSha
164155
+ }) { clientMutationId }
164156
+ }`
164157
+ );
164158
+ var isAlreadyMergeable = (error49) => error49 instanceof Error && /clean status/i.test(error49.message);
164159
+ var isHeadChanged = (error49) => error49 instanceof Error && /head has changed|expected head/i.test(error49.message);
164160
+ var isAutoMergeDisallowed = (error49) => error49 instanceof Error && /not allowed/i.test(error49.message);
164161
+ async function autoMergeAfterApprove(ctx) {
164162
+ if (!ctx.autoMergeEnabled) return;
164163
+ if (!ctx.prApproveEnabled) return;
164164
+ const approval = ctx.toolState.approval;
164165
+ if (!approval?.wouldApprove) return;
164166
+ const pullNumber = ctx.payload.event.issue_number;
164167
+ if (typeof pullNumber !== "number") return;
164168
+ const skip = (reason) => log.info(
164169
+ `autoMerge: pr=#${pullNumber} approvalSha=${approval.sha?.slice(0, 7)} \u2192 skipped: ${reason}`
164170
+ );
164171
+ const comment = (body) => ctx.octokit.rest.issues.createComment({
164172
+ owner: ctx.repo.owner,
164173
+ repo: ctx.repo.name,
164174
+ issue_number: pullNumber,
164175
+ body: addFooter(ctx, body)
164176
+ }).catch((err) => log.debug(`autoMerge comment failed: ${err}`));
164177
+ const pr = await ctx.octokit.rest.pulls.get({
164178
+ owner: ctx.repo.owner,
164179
+ repo: ctx.repo.name,
164180
+ pull_number: pullNumber
164181
+ });
164182
+ if (pr.data.state !== "open") return skip("pr not open");
164183
+ if (pr.data.draft) return skip("pr is draft");
164184
+ if (pr.data.auto_merge) return skip("auto-merge already enabled");
164185
+ const headSha = pr.data.head.sha;
164186
+ if (approval.sha !== headSha) return skip(`approval sha != head ${headSha.slice(0, 7)}`);
164187
+ const outstanding = await countOutstandingPullfrogThreads(ctx, pullNumber);
164188
+ if (outstanding > 0) return skip(`${outstanding} unresolved Pullfrog thread(s)`);
164189
+ const reviews = await ctx.octokit.paginate(ctx.octokit.rest.pulls.listReviews, {
164190
+ owner: ctx.repo.owner,
164191
+ repo: ctx.repo.name,
164192
+ pull_number: pullNumber,
164193
+ per_page: 100
164194
+ });
164195
+ if (hasBlockingHumanReview(reviews)) return skip("human changes requested");
164196
+ try {
164197
+ await op(
164198
+ () => ctx.octokit.graphql(ENABLE_AUTO_MERGE, {
164199
+ pullRequestId: pr.data.node_id,
164200
+ headSha
164201
+ }),
164202
+ {
164203
+ name: "enablePullRequestAutoMerge",
164204
+ retries: [500, 2e3],
164205
+ bail: (error49) => !isTransientOctokitError(error49)
164206
+ }
164207
+ )();
164208
+ log.info(`autoMerge: pr=#${pullNumber} @ ${headSha.slice(0, 7)} \u2192 native auto-merge enabled`);
164209
+ await comment(
164210
+ "> \u2705 Approved \u2014 auto-merge enabled; GitHub will merge once required checks pass."
164211
+ );
164212
+ return;
164213
+ } catch (error49) {
164214
+ if (isHeadChanged(error49)) return skip("head moved during enable");
164215
+ if (isAutoMergeDisallowed(error49)) {
164216
+ await comment(
164217
+ "> \u26A0\uFE0F Approved, but auto-merge is off for this repo. Enable `Allow auto-merge` in Settings > General > Pull Requests."
164218
+ );
164219
+ return;
164220
+ }
164221
+ if (!isAlreadyMergeable(error49)) throw error49;
164222
+ }
164223
+ const base = await ctx.octokit.rest.repos.getBranch({ owner: ctx.repo.owner, repo: ctx.repo.name, branch: pr.data.base.ref }).catch(() => null);
164224
+ if (!base?.data.protected) return skip("base branch not protected \u2014 refusing CI-ungated merge");
164225
+ const merge4 = await ctx.octokit.rest.pulls.merge({
164226
+ owner: ctx.repo.owner,
164227
+ repo: ctx.repo.name,
164228
+ pull_number: pullNumber,
164229
+ sha: headSha,
164230
+ merge_method: "squash",
164231
+ commit_title: `${pr.data.title} (#${pullNumber})`
164232
+ });
164233
+ log.info(
164234
+ `autoMerge: pr=#${pullNumber} @ ${headSha.slice(0, 7)} \u2192 merged directly ${merge4.data.sha?.slice(0, 7)}`
164235
+ );
164236
+ await comment(
164237
+ "> \u2705 Merged autonomously after Pullfrog approved this PR and resolved all of its own review threads."
164238
+ );
164239
+ }
164240
+
164110
164241
  // utils/reviewCleanup.ts
164111
164242
  var RE_REVIEW_PREAMBLE = "Incrementally re-review the new commits on this pull request. Use the IncrementalReview mode.";
164112
164243
  async function postReviewCleanup(ctx) {
@@ -164287,6 +164418,11 @@ async function finalizeSuccessRun(input) {
164287
164418
  await approveAfterFix(input.toolContext).catch((error49) => {
164288
164419
  log.debug(`fix auto-approval failed: ${error49}`);
164289
164420
  });
164421
+ await autoMergeAfterApprove(input.toolContext).catch((error49) => {
164422
+ const raced = typeof error49 === "object" && error49 !== null && "status" in error49 && error49.status === 409;
164423
+ if (raced) log.debug(`auto-merge skipped (head moved): ${error49}`);
164424
+ else log.warning(`auto-merge failed: ${error49}`);
164425
+ });
164290
164426
  }
164291
164427
  await reportStatusChecks(input.toolContext, { runSucceeded: input.result.success });
164292
164428
  }
@@ -164728,6 +164864,7 @@ async function main() {
164728
164864
  postCheckoutScript: runContext.repoSettings.postCheckoutScript,
164729
164865
  prepushScript: runContext.repoSettings.prepushScript,
164730
164866
  prApproveEnabled: runContext.repoSettings.prApproveEnabled,
164867
+ autoMergeEnabled: runContext.repoSettings.autoMergeEnabled,
164731
164868
  signedCommits: runContext.repoSettings.signedCommits,
164732
164869
  modeInstructions: runContext.repoSettings.modeInstructions,
164733
164870
  toolState,
@@ -22,6 +22,7 @@ export interface ToolContext {
22
22
  postCheckoutScript: string | null;
23
23
  prepushScript: string | null;
24
24
  prApproveEnabled: boolean;
25
+ autoMergeEnabled: boolean;
25
26
  signedCommits: boolean;
26
27
  modeInstructions: Record<string, string>;
27
28
  toolState: ToolState;
@@ -0,0 +1,28 @@
1
+ import type { ToolContext } from "../mcp/server.ts";
2
+ /**
3
+ * Run-end lifecycle action: hand any PR this run mechanically approved on its
4
+ * current head to GitHub's NATIVE auto-merge — Pullfrog acting as a full repo
5
+ * maintainer, contributor PRs included. There is deliberately NO agent-callable
6
+ * merge tool — the merge is a deterministic consequence of the recorded approval
7
+ * verdict, so a prompt-injected agent has no merge surface to reach for.
8
+ *
9
+ * Native auto-merge waits for all REQUIRED checks + reviews (un-fakeable by a
10
+ * fork), respects the human veto via branch protection, and requires branch
11
+ * protection to exist. We keep only the Pullfrog-specific pre-gates GitHub can't
12
+ * express: armed toggles, approved-THIS-head, zero outstanding Pullfrog threads,
13
+ * no human CHANGES_REQUESTED. Best-effort — the caller wraps this in `.catch`,
14
+ * and a merge/comment failure can never flip the run's outcome.
15
+ *
16
+ * The invariant (all must hold):
17
+ * 1. `ctx.autoMergeEnabled` — the per-repo toggle ANDed with the global
18
+ * `isAutonomousMaintenanceEnabled()` kill switch, server-side (run-context).
19
+ * 2. `ctx.prApproveEnabled` — cannot auto-merge what it may not approve.
20
+ * 3. this run recorded an APPROVE verdict (`toolState.approval.wouldApprove`).
21
+ * 4. the PR is open and not a draft.
22
+ * 5. `toolState.approval.sha === <current head sha>` — approved THIS head.
23
+ * 6. `countOutstandingPullfrogThreads === 0` — re-queried at merge time.
24
+ * 7. no un-dismissed human CHANGES_REQUESTED at handoff.
25
+ * The remaining wait-for-required-checks + wait-for-required-reviews semantics
26
+ * are GitHub's, via native auto-merge and branch protection.
27
+ */
28
+ export declare function autoMergeAfterApprove(ctx: ToolContext): Promise<void>;
@@ -16,6 +16,7 @@
16
16
  *
17
17
  * - 402 → `BillingError` (card declined, balance empty, 3DS, etc.)
18
18
  * - 503 → `TransientError` (transient sync issue — retry next dispatch)
19
+ * - 404 → `TransientError` (stale repo↔account link — re-homes on next webhook)
19
20
  */
20
21
  import type { ToolState } from "../toolState.ts";
21
22
  import { type OidcCredentials } from "./github.ts";
@@ -31,6 +31,7 @@ export interface RepoSettings {
31
31
  push: PushPermission;
32
32
  shell: ShellPermission;
33
33
  prApproveEnabled: boolean;
34
+ autoMergeEnabled: boolean;
34
35
  signedCommits: boolean;
35
36
  modeInstructions: Record<string, string>;
36
37
  learnings: string | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pullfrog",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "pullfrog": "dist/cli.mjs",