pullfrog 0.1.33 → 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.33",
101044
+ version: "0.1.34",
101045
101045
  type: "module",
101046
101046
  bin: {
101047
101047
  pullfrog: "dist/cli.mjs",
@@ -163558,6 +163558,11 @@ async function mintProxyKey(ctx) {
163558
163558
  body?.error ?? "billing service temporarily unavailable \u2014 retry shortly"
163559
163559
  );
163560
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
+ }
163561
163566
  if (!response.ok) {
163562
163567
  log.warning(`proxy key mint failed (${response.status})`);
163563
163568
  return null;
@@ -164122,28 +164127,7 @@ ${genericBody}`,
164122
164127
  // utils/runLifecycle.ts
164123
164128
  var core10 = __toESM(require_core(), 1);
164124
164129
 
164125
- // utils/checksGate.ts
164126
- var OK_CONCLUSIONS = /* @__PURE__ */ new Set(["success", "neutral", "skipped"]);
164127
- var PULLFROG_CHECKS = /* @__PURE__ */ new Set(["pullfrog", "pullfrog-approval"]);
164128
- function checkRunsGate(params) {
164129
- for (const run of params.checkRuns) {
164130
- if (PULLFROG_CHECKS.has(run.name.toLowerCase())) continue;
164131
- if (run.status !== "completed")
164132
- return { ok: false, reason: `check "${run.name}" ${run.status}` };
164133
- if (!OK_CONCLUSIONS.has(run.conclusion ?? "")) {
164134
- return { ok: false, reason: `check "${run.name}" concluded ${run.conclusion}` };
164135
- }
164136
- }
164137
- return { ok: true, reason: "all checks passed" };
164138
- }
164139
- function hasVerifiedCheck(params) {
164140
- return params.checkRuns.some(
164141
- (r) => !PULLFROG_CHECKS.has(r.name.toLowerCase()) && r.status === "completed"
164142
- );
164143
- }
164144
-
164145
164130
  // utils/autoMerge.ts
164146
- var NON_MERGEABLE_STATES = /* @__PURE__ */ new Set(["dirty", "behind", "blocked"]);
164147
164131
  function hasBlockingHumanReview(reviews) {
164148
164132
  const latestByReviewer = /* @__PURE__ */ new Map();
164149
164133
  for (const review of reviews) {
@@ -164160,6 +164144,20 @@ function hasBlockingHumanReview(reviews) {
164160
164144
  }
164161
164145
  return false;
164162
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);
164163
164161
  async function autoMergeAfterApprove(ctx) {
164164
164162
  if (!ctx.autoMergeEnabled) return;
164165
164163
  if (!ctx.prApproveEnabled) return;
@@ -164170,6 +164168,12 @@ async function autoMergeAfterApprove(ctx) {
164170
164168
  const skip = (reason) => log.info(
164171
164169
  `autoMerge: pr=#${pullNumber} approvalSha=${approval.sha?.slice(0, 7)} \u2192 skipped: ${reason}`
164172
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}`));
164173
164177
  const pr = await ctx.octokit.rest.pulls.get({
164174
164178
  owner: ctx.repo.owner,
164175
164179
  repo: ctx.repo.name,
@@ -164177,6 +164181,7 @@ async function autoMergeAfterApprove(ctx) {
164177
164181
  });
164178
164182
  if (pr.data.state !== "open") return skip("pr not open");
164179
164183
  if (pr.data.draft) return skip("pr is draft");
164184
+ if (pr.data.auto_merge) return skip("auto-merge already enabled");
164180
164185
  const headSha = pr.data.head.sha;
164181
164186
  if (approval.sha !== headSha) return skip(`approval sha != head ${headSha.slice(0, 7)}`);
164182
164187
  const outstanding = await countOutstandingPullfrogThreads(ctx, pullNumber);
@@ -164188,40 +164193,49 @@ async function autoMergeAfterApprove(ctx) {
164188
164193
  per_page: 100
164189
164194
  });
164190
164195
  if (hasBlockingHumanReview(reviews)) return skip("human changes requested");
164191
- if (pr.data.mergeable !== true) return skip(`not mergeable (mergeable=${pr.data.mergeable})`);
164192
- if (pr.data.mergeable_state && NON_MERGEABLE_STATES.has(pr.data.mergeable_state)) {
164193
- return skip(`mergeable_state=${pr.data.mergeable_state}`);
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;
164194
164222
  }
164195
- const checkRuns = await ctx.octokit.paginate(ctx.octokit.rest.checks.listForRef, {
164196
- owner: ctx.repo.owner,
164197
- repo: ctx.repo.name,
164198
- ref: headSha,
164199
- per_page: 100
164200
- });
164201
- const gate = checkRunsGate({ checkRuns });
164202
- if (!gate.ok) return skip(`ci: ${gate.reason}`);
164203
- if (!hasVerifiedCheck({ checkRuns })) return skip("no completed external check-run to verify");
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");
164204
164225
  const merge4 = await ctx.octokit.rest.pulls.merge({
164205
164226
  owner: ctx.repo.owner,
164206
164227
  repo: ctx.repo.name,
164207
164228
  pull_number: pullNumber,
164208
164229
  sha: headSha,
164209
164230
  merge_method: "squash",
164210
- commit_title: `${pr.data.title} (#${pullNumber})`,
164211
- commit_message: "merged autonomously by Pullfrog after a clean approval."
164231
+ commit_title: `${pr.data.title} (#${pullNumber})`
164212
164232
  });
164213
164233
  log.info(
164214
- `autoMerge: pr=#${pullNumber} approvalSha=${headSha.slice(0, 7)} outstanding=0 \u2192 merged ${merge4.data.sha?.slice(0, 7)}`
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."
164215
164238
  );
164216
- await ctx.octokit.rest.issues.createComment({
164217
- owner: ctx.repo.owner,
164218
- repo: ctx.repo.name,
164219
- issue_number: pullNumber,
164220
- body: addFooter(
164221
- ctx,
164222
- "> \u2705 Merged autonomously after Pullfrog approved this PR and resolved all of its own review threads."
164223
- )
164224
- }).catch((err) => log.debug(`autoMerge comment failed: ${err}`));
164225
164239
  }
164226
164240
 
164227
164241
  // utils/reviewCleanup.ts
@@ -8,7 +8,6 @@ export type { Mode } from "../modes.ts";
8
8
  export { modes } from "../modes.ts";
9
9
  export type { BuildPullfrogFooterParams, WorkflowRunFooterInfo, } from "../utils/buildPullfrogFooter.ts";
10
10
  export { buildPullfrogFooter, PULLFROG_DIVIDER, stripExistingFooter, } from "../utils/buildPullfrogFooter.ts";
11
- export { checkRunsGate, hasVerifiedCheck } from "../utils/checksGate.ts";
12
11
  export type { CodexAuthBody } from "../utils/codexOAuth.ts";
13
12
  export { decodeJwtExpMs, OAuthInvalidGrantError, parseCodexAuthBody, refreshCodexAuthBody, stringifyCodexAuthBody, } from "../utils/codexOAuth.ts";
14
13
  export type { ResourceUsage, UsageSummary } from "../utils/github.ts";
package/dist/internal.js CHANGED
@@ -1245,26 +1245,6 @@ function stripExistingFooter(body) {
1245
1245
  return body.substring(0, dividerIndex).trimEnd();
1246
1246
  }
1247
1247
 
1248
- // utils/checksGate.ts
1249
- var OK_CONCLUSIONS = /* @__PURE__ */ new Set(["success", "neutral", "skipped"]);
1250
- var PULLFROG_CHECKS = /* @__PURE__ */ new Set(["pullfrog", "pullfrog-approval"]);
1251
- function checkRunsGate(params) {
1252
- for (const run of params.checkRuns) {
1253
- if (PULLFROG_CHECKS.has(run.name.toLowerCase())) continue;
1254
- if (run.status !== "completed")
1255
- return { ok: false, reason: `check "${run.name}" ${run.status}` };
1256
- if (!OK_CONCLUSIONS.has(run.conclusion ?? "")) {
1257
- return { ok: false, reason: `check "${run.name}" concluded ${run.conclusion}` };
1258
- }
1259
- }
1260
- return { ok: true, reason: "all checks passed" };
1261
- }
1262
- function hasVerifiedCheck(params) {
1263
- return params.checkRuns.some(
1264
- (r) => !PULLFROG_CHECKS.has(r.name.toLowerCase()) && r.status === "completed"
1265
- );
1266
- }
1267
-
1268
1248
  // utils/codexOAuth.ts
1269
1249
  var CODEX_OAUTH_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
1270
1250
  var CODEX_OAUTH_TOKEN_URL = "https://auth.openai.com/oauth/token";
@@ -1492,7 +1472,6 @@ export {
1492
1472
  PULLFROG_DIVIDER,
1493
1473
  TIMEOUT_DISABLED,
1494
1474
  buildPullfrogFooter,
1495
- checkRunsGate,
1496
1475
  createLeapingProgressComment,
1497
1476
  decodeJwtExpMs,
1498
1477
  defaultAutoTier,
@@ -1503,7 +1482,6 @@ export {
1503
1482
  getModelProvider,
1504
1483
  getProgressComment,
1505
1484
  getProviderDisplayName,
1506
- hasVerifiedCheck,
1507
1485
  isAutoTier,
1508
1486
  isCardGatedModel,
1509
1487
  isLeapingIntoActionCommentBody,
@@ -1,37 +1,28 @@
1
1
  import type { ToolContext } from "../mcp/server.ts";
2
2
  /**
3
- * Run-end lifecycle action: merge ANY open PR that this run mechanically
4
- * approved on its current head — Pullfrog acting as a full repo maintainer,
5
- * contributor PRs included, not just merging its own work. There is deliberately
6
- * NO agent-callable merge tool — the merge is a pure, deterministic consequence
7
- * of the runtime observing a clean approved state, so a prompt-injected agent has
8
- * no merge surface to reach for. Every clause is fail-closed and re-verified
9
- * against GitHub's own state at merge time; any failure logs why and returns
10
- * without merging. Best-effort — the caller wraps this in a `.catch`, and a
11
- * merge/comment failure can never flip the run's outcome.
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.
12
8
  *
13
- * There is NO self-authorship restriction: the approval verdict IS the trust
14
- * decision (Pullfrog reviewed it and would approve), exactly like a human
15
- * maintainer merging a contributor PR. The population of mergeable PRs is bounded
16
- * upstream by the review triggers (`prCreated` / `prCreatedAllowNonCollaborator`)
17
- * Pullfrog can only merge what it was configured to review + approve — and the
18
- * whole capability is opt-in per repo (clause 1). The remaining clauses are the
19
- * maintainer's controls.
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.
20
15
  *
21
16
  * The invariant (all must hold):
22
17
  * 1. `ctx.autoMergeEnabled` — the per-repo toggle ANDed with the global
23
18
  * `isAutonomousMaintenanceEnabled()` kill switch, server-side (run-context).
24
19
  * 2. `ctx.prApproveEnabled` — cannot auto-merge what it may not approve.
25
- * 3. this run recorded an APPROVE verdict (`toolState.approval.wouldApprove`)
26
- * Pullfrog's review is the merge decision.
20
+ * 3. this run recorded an APPROVE verdict (`toolState.approval.wouldApprove`).
27
21
  * 4. the PR is open and not a draft.
28
- * 5. `toolState.approval.sha === <current head sha>` — approved THIS head, so a
29
- * commit pushed after the review can't ride the approval in (409-safe below).
22
+ * 5. `toolState.approval.sha === <current head sha>` — approved THIS head.
30
23
  * 6. `countOutstandingPullfrogThreads === 0` — re-queried at merge time.
31
- * 7. no un-dismissed human CHANGES_REQUESTED the human veto always wins.
32
- * 8. GitHub reports the PR mergeable and not blocked/dirty/behind.
33
- * 9. every external check-run/commit-status on the head is complete and
34
- * non-failing (`checkRunsGate`) — red or in-flight CI never auto-merges,
35
- * even on a repo without branch protection.
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.
36
27
  */
37
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";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pullfrog",
3
- "version": "0.1.33",
3
+ "version": "0.1.34",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "pullfrog": "dist/cli.mjs",
@@ -1,45 +0,0 @@
1
- /**
2
- * Shared, dependency-free CI green/red predicate — the single source of truth for
3
- * "given these check-runs, is this PR head safe to merge?". Consumed by the
4
- * auto-merge path: `autoMerge.ts` gates the PR head at merge time, and the
5
- * `pr-merge-completion` webhook re-checks it before dispatching a slow-CI re-wake.
6
- * A red or in-flight sha never merges, and a sha with no confirmable green
7
- * check-run is treated as unverified (refused, not merged blind).
8
- *
9
- * Gates on the Checks API only. The Pullfrog App deliberately holds no `statuses`
10
- * permission (requesting it would force re-approval on every install — see
11
- * wiki/app-permissions.md), so the legacy Commit Statuses API is unreadable
12
- * (`listCommitStatusesForRef` 403s). Modern CI (GitHub Actions, etc.) reports via
13
- * check-runs; a repo relying on the legacy Statuses API is covered by branch
14
- * protection instead — a failing required status makes `mergeable_state` `blocked`,
15
- * which the merge invariant already refuses (`NON_MERGEABLE_STATES`).
16
- */
17
- export type CheckRun = {
18
- name: string;
19
- status: string;
20
- conclusion: string | null;
21
- };
22
- /**
23
- * Refuse if any external check-run is incomplete (queued/in_progress) or concluded
24
- * anything other than success/neutral/skipped. Fail-closed on unknown/absent
25
- * conclusions. Pure so the pass/fail logic is inspectable directly.
26
- */
27
- export declare function checkRunsGate(params: {
28
- checkRuns: CheckRun[];
29
- }): {
30
- ok: boolean;
31
- reason: string;
32
- };
33
- /**
34
- * true when the head has ≥1 CONCLUDED non-failing EXTERNAL check-run — CI
35
- * positively ran, not merely "no red was seen". paired with `checkRunsGate.ok`
36
- * (any completed check is non-failing), this == "≥1 concluded non-failing check".
37
- * the merge requires it: a head with ZERO check-runs passes
38
- * `checkRunsGate` (nothing red) but has no verification, and since the app can't
39
- * read legacy commit statuses, "zero check-runs" is indistinguishable from "green
40
- * Actions but a red CircleCI status" — so we only act on a head we can positively
41
- * confirm is green. Pullfrog's own verdict checks don't count as external CI.
42
- */
43
- export declare function hasVerifiedCheck(params: {
44
- checkRuns: CheckRun[];
45
- }): boolean;