pullfrog 0.1.17 → 0.1.18

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
@@ -101880,7 +101880,7 @@ var import_semver = __toESM(require_semver2(), 1);
101880
101880
  // package.json
101881
101881
  var package_default = {
101882
101882
  name: "pullfrog",
101883
- version: "0.1.17",
101883
+ version: "0.1.18",
101884
101884
  type: "module",
101885
101885
  bin: {
101886
101886
  pullfrog: "dist/cli.mjs",
@@ -108084,7 +108084,7 @@ function newTurn() {
108084
108084
  };
108085
108085
  }
108086
108086
  async function consumeEvents(ctx, signal) {
108087
- const result = await ctx.client.event.subscribe();
108087
+ const result = await ctx.client.event.subscribe({}, { signal });
108088
108088
  for await (const event of result.stream) {
108089
108089
  if (signal.aborted) break;
108090
108090
  ctx.eventCount += 1;
@@ -151963,6 +151963,30 @@ function classifyPushError(msg) {
151963
151963
  return "unknown";
151964
151964
  }
151965
151965
  var TRANSIENT_RETRY_DELAYS_MS = [2e3, 5e3];
151966
+ async function pushWithRetry(args2, token) {
151967
+ let lastErr;
151968
+ for (let attempt = 0; attempt <= TRANSIENT_RETRY_DELAYS_MS.length; attempt++) {
151969
+ try {
151970
+ await $git("push", args2, { token });
151971
+ if (attempt > 0) log.info(`push succeeded on attempt ${attempt + 1}`);
151972
+ return;
151973
+ } catch (err) {
151974
+ lastErr = err;
151975
+ const msg = err instanceof Error ? err.message : String(err);
151976
+ if (classifyPushError(msg) === "transient" && attempt < TRANSIENT_RETRY_DELAYS_MS.length) {
151977
+ const baseDelay = TRANSIENT_RETRY_DELAYS_MS[attempt] ?? 5e3;
151978
+ const delay2 = Math.round(baseDelay * (0.75 + Math.random() * 0.5));
151979
+ log.info(
151980
+ `push attempt ${attempt + 1} failed (transient), retrying in ${delay2}ms: ${msg.slice(0, 300)}`
151981
+ );
151982
+ await new Promise((r) => setTimeout(r, delay2));
151983
+ continue;
151984
+ }
151985
+ throw err;
151986
+ }
151987
+ }
151988
+ throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
151989
+ }
151966
151990
  function PushBranchTool(ctx) {
151967
151991
  const defaultBranch = ctx.repo.data.default_branch || "main";
151968
151992
  const pushPermission = ctx.payload.push;
@@ -152030,48 +152054,23 @@ ${postHookStatus}`
152030
152054
  if (force) {
152031
152055
  log.warning(`force pushing - this will overwrite remote history`);
152032
152056
  }
152033
- let lastErr;
152034
- let pushed = false;
152035
- for (let attempt = 0; attempt <= TRANSIENT_RETRY_DELAYS_MS.length; attempt++) {
152036
- try {
152037
- await $git("push", pushArgs, {
152038
- token: ctx.gitToken
152039
- });
152040
- if (attempt > 0) {
152041
- log.info(`push succeeded on attempt ${attempt + 1}`);
152042
- }
152043
- pushed = true;
152044
- break;
152045
- } catch (err) {
152046
- lastErr = err;
152047
- const msg = err instanceof Error ? err.message : String(err);
152048
- const kind = classifyPushError(msg);
152049
- if (kind === "concurrent-push") {
152050
- const integrateStep = ctx.payload.shell === "disabled" ? `2. use the git tool to merge the remote branch into yours: git({ command: "merge", args: ["origin/${pushDest.remoteBranch}"] })` : `2. use the git tool to rebase or merge your changes on top: git({ command: "merge", args: ["origin/${pushDest.remoteBranch}"] }) (or 'rebase')`;
152051
- throw new Error(
152052
- `push rejected: the remote branch '${pushDest.remoteBranch}' has new commits you don't have locally (often a concurrent push to the same branch).
152057
+ try {
152058
+ await pushWithRetry(pushArgs, ctx.gitToken);
152059
+ } catch (err) {
152060
+ const msg = err instanceof Error ? err.message : String(err);
152061
+ if (classifyPushError(msg) === "concurrent-push") {
152062
+ const integrateStep = ctx.payload.shell === "disabled" ? `2. use the git tool to merge the remote branch into yours: git({ command: "merge", args: ["origin/${pushDest.remoteBranch}"] })` : `2. use the git tool to rebase or merge your changes on top: git({ command: "merge", args: ["origin/${pushDest.remoteBranch}"] }) (or 'rebase')`;
152063
+ throw new Error(
152064
+ `push rejected: the remote branch '${pushDest.remoteBranch}' has new commits you don't have locally (often a concurrent push to the same branch).
152053
152065
 
152054
152066
  to resolve this:
152055
152067
  1. use git_fetch to fetch the remote branch: git_fetch({ ref: "${pushDest.remoteBranch}" })
152056
152068
  ${integrateStep}
152057
152069
  3. resolve any merge conflicts if needed
152058
152070
  4. retry push_branch`
152059
- );
152060
- }
152061
- if (kind === "transient" && attempt < TRANSIENT_RETRY_DELAYS_MS.length) {
152062
- const baseDelay = TRANSIENT_RETRY_DELAYS_MS[attempt] ?? 5e3;
152063
- const delay2 = Math.round(baseDelay * (0.75 + Math.random() * 0.5));
152064
- log.info(
152065
- `push attempt ${attempt + 1} failed (transient), retrying in ${delay2}ms: ${msg.slice(0, 300)}`
152066
- );
152067
- await new Promise((r) => setTimeout(r, delay2));
152068
- continue;
152069
- }
152070
- throw err;
152071
+ );
152071
152072
  }
152072
- }
152073
- if (!pushed) {
152074
- throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
152073
+ throw err;
152075
152074
  }
152076
152075
  const pushedSha = $2("git", ["rev-parse", "HEAD"], { log: false }).trim();
152077
152076
  log.info(
@@ -152314,9 +152313,7 @@ function DeleteBranchTool(ctx) {
152314
152313
  `Blocked: cannot delete the default branch '${defaultBranch}'. If you really need to delete or rename it, do it manually via the repository settings.`
152315
152314
  );
152316
152315
  }
152317
- await $git("push", ["origin", "--delete", `refs/heads/${params.branchName}`], {
152318
- token: ctx.gitToken
152319
- });
152316
+ await pushWithRetry(["origin", "--delete", `refs/heads/${params.branchName}`], ctx.gitToken);
152320
152317
  log.info(`\xBB deleted branch ${params.branchName}`);
152321
152318
  return { success: true, deleted: params.branchName };
152322
152319
  })
@@ -152340,9 +152337,7 @@ function PushTagsTool(ctx) {
152340
152337
  }
152341
152338
  validateTagName(params.tag);
152342
152339
  const pushArgs = [...params.force ? ["-f"] : [], "origin", `refs/tags/${params.tag}`];
152343
- await $git("push", pushArgs, {
152344
- token: ctx.gitToken
152345
- });
152340
+ await pushWithRetry(pushArgs, ctx.gitToken);
152346
152341
  log.info(`\xBB pushed tag ${params.tag}`);
152347
152342
  return { success: true, tag: params.tag };
152348
152343
  })
@@ -161267,6 +161262,13 @@ function formatGenericFailure(errorMessage) {
161267
161262
  "```"
161268
161263
  ].join("\n");
161269
161264
  }
161265
+ function formatMinimalFailureComment(repo) {
161266
+ const runId = process.env.GITHUB_RUN_ID;
161267
+ if (!runId) return "**Run failed.**";
161268
+ const server = process.env.GITHUB_SERVER_URL ?? "https://github.com";
161269
+ const url4 = `${server}/${repo.owner}/${repo.name}/actions/runs/${runId}`;
161270
+ return `**Run failed.** [View the logs \u2192](${url4})`;
161271
+ }
161270
161272
  var PROVIDER_BILLING_URLS = {
161271
161273
  deepseek: "https://platform.deepseek.com/top_up",
161272
161274
  anthropic: "https://console.anthropic.com/settings/billing",
@@ -161340,11 +161342,12 @@ ${body}`, comment: body };
161340
161342
  return { summary: body, comment: body };
161341
161343
  }
161342
161344
  if (hangBody) {
161345
+ const isBillingExhausted = input.agentDiagnostic?.lastProviderError === "provider billing exhausted";
161343
161346
  return {
161344
161347
  summary: `### \u274C Pullfrog failed
161345
161348
 
161346
161349
  ${hangBody}`,
161347
- comment: hangBody
161350
+ comment: isBillingExhausted ? hangBody : formatMinimalFailureComment(input.repo)
161348
161351
  };
161349
161352
  }
161350
161353
  const genericBody = formatGenericFailure(input.errorMessage);
@@ -161352,7 +161355,7 @@ ${hangBody}`,
161352
161355
  summary: `### \u274C Pullfrog failed
161353
161356
 
161354
161357
  ${genericBody}`,
161355
- comment: genericBody
161358
+ comment: formatMinimalFailureComment(input.repo)
161356
161359
  };
161357
161360
  }
161358
161361
 
@@ -163002,7 +163005,7 @@ async function run2() {
163002
163005
  }
163003
163006
 
163004
163007
  // cli.ts
163005
- var VERSION10 = "0.1.17";
163008
+ var VERSION10 = "0.1.18";
163006
163009
  var bin = basename2(process.argv[1] || "");
163007
163010
  var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
163008
163011
  var rawArgs = process.argv.slice(2);
package/dist/index.js CHANGED
@@ -100080,7 +100080,7 @@ var import_semver = __toESM(require_semver2(), 1);
100080
100080
  // package.json
100081
100081
  var package_default = {
100082
100082
  name: "pullfrog",
100083
- version: "0.1.17",
100083
+ version: "0.1.18",
100084
100084
  type: "module",
100085
100085
  bin: {
100086
100086
  pullfrog: "dist/cli.mjs",
@@ -106326,7 +106326,7 @@ function newTurn() {
106326
106326
  };
106327
106327
  }
106328
106328
  async function consumeEvents(ctx, signal) {
106329
- const result = await ctx.client.event.subscribe();
106329
+ const result = await ctx.client.event.subscribe({}, { signal });
106330
106330
  for await (const event of result.stream) {
106331
106331
  if (signal.aborted) break;
106332
106332
  ctx.eventCount += 1;
@@ -150205,6 +150205,30 @@ function classifyPushError(msg) {
150205
150205
  return "unknown";
150206
150206
  }
150207
150207
  var TRANSIENT_RETRY_DELAYS_MS = [2e3, 5e3];
150208
+ async function pushWithRetry(args2, token) {
150209
+ let lastErr;
150210
+ for (let attempt = 0; attempt <= TRANSIENT_RETRY_DELAYS_MS.length; attempt++) {
150211
+ try {
150212
+ await $git("push", args2, { token });
150213
+ if (attempt > 0) log.info(`push succeeded on attempt ${attempt + 1}`);
150214
+ return;
150215
+ } catch (err) {
150216
+ lastErr = err;
150217
+ const msg = err instanceof Error ? err.message : String(err);
150218
+ if (classifyPushError(msg) === "transient" && attempt < TRANSIENT_RETRY_DELAYS_MS.length) {
150219
+ const baseDelay = TRANSIENT_RETRY_DELAYS_MS[attempt] ?? 5e3;
150220
+ const delay2 = Math.round(baseDelay * (0.75 + Math.random() * 0.5));
150221
+ log.info(
150222
+ `push attempt ${attempt + 1} failed (transient), retrying in ${delay2}ms: ${msg.slice(0, 300)}`
150223
+ );
150224
+ await new Promise((r) => setTimeout(r, delay2));
150225
+ continue;
150226
+ }
150227
+ throw err;
150228
+ }
150229
+ }
150230
+ throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
150231
+ }
150208
150232
  function PushBranchTool(ctx) {
150209
150233
  const defaultBranch = ctx.repo.data.default_branch || "main";
150210
150234
  const pushPermission = ctx.payload.push;
@@ -150272,48 +150296,23 @@ ${postHookStatus}`
150272
150296
  if (force) {
150273
150297
  log.warning(`force pushing - this will overwrite remote history`);
150274
150298
  }
150275
- let lastErr;
150276
- let pushed = false;
150277
- for (let attempt = 0; attempt <= TRANSIENT_RETRY_DELAYS_MS.length; attempt++) {
150278
- try {
150279
- await $git("push", pushArgs, {
150280
- token: ctx.gitToken
150281
- });
150282
- if (attempt > 0) {
150283
- log.info(`push succeeded on attempt ${attempt + 1}`);
150284
- }
150285
- pushed = true;
150286
- break;
150287
- } catch (err) {
150288
- lastErr = err;
150289
- const msg = err instanceof Error ? err.message : String(err);
150290
- const kind = classifyPushError(msg);
150291
- if (kind === "concurrent-push") {
150292
- const integrateStep = ctx.payload.shell === "disabled" ? `2. use the git tool to merge the remote branch into yours: git({ command: "merge", args: ["origin/${pushDest.remoteBranch}"] })` : `2. use the git tool to rebase or merge your changes on top: git({ command: "merge", args: ["origin/${pushDest.remoteBranch}"] }) (or 'rebase')`;
150293
- throw new Error(
150294
- `push rejected: the remote branch '${pushDest.remoteBranch}' has new commits you don't have locally (often a concurrent push to the same branch).
150299
+ try {
150300
+ await pushWithRetry(pushArgs, ctx.gitToken);
150301
+ } catch (err) {
150302
+ const msg = err instanceof Error ? err.message : String(err);
150303
+ if (classifyPushError(msg) === "concurrent-push") {
150304
+ const integrateStep = ctx.payload.shell === "disabled" ? `2. use the git tool to merge the remote branch into yours: git({ command: "merge", args: ["origin/${pushDest.remoteBranch}"] })` : `2. use the git tool to rebase or merge your changes on top: git({ command: "merge", args: ["origin/${pushDest.remoteBranch}"] }) (or 'rebase')`;
150305
+ throw new Error(
150306
+ `push rejected: the remote branch '${pushDest.remoteBranch}' has new commits you don't have locally (often a concurrent push to the same branch).
150295
150307
 
150296
150308
  to resolve this:
150297
150309
  1. use git_fetch to fetch the remote branch: git_fetch({ ref: "${pushDest.remoteBranch}" })
150298
150310
  ${integrateStep}
150299
150311
  3. resolve any merge conflicts if needed
150300
150312
  4. retry push_branch`
150301
- );
150302
- }
150303
- if (kind === "transient" && attempt < TRANSIENT_RETRY_DELAYS_MS.length) {
150304
- const baseDelay = TRANSIENT_RETRY_DELAYS_MS[attempt] ?? 5e3;
150305
- const delay2 = Math.round(baseDelay * (0.75 + Math.random() * 0.5));
150306
- log.info(
150307
- `push attempt ${attempt + 1} failed (transient), retrying in ${delay2}ms: ${msg.slice(0, 300)}`
150308
- );
150309
- await new Promise((r) => setTimeout(r, delay2));
150310
- continue;
150311
- }
150312
- throw err;
150313
+ );
150313
150314
  }
150314
- }
150315
- if (!pushed) {
150316
- throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
150315
+ throw err;
150317
150316
  }
150318
150317
  const pushedSha = $("git", ["rev-parse", "HEAD"], { log: false }).trim();
150319
150318
  log.info(
@@ -150556,9 +150555,7 @@ function DeleteBranchTool(ctx) {
150556
150555
  `Blocked: cannot delete the default branch '${defaultBranch}'. If you really need to delete or rename it, do it manually via the repository settings.`
150557
150556
  );
150558
150557
  }
150559
- await $git("push", ["origin", "--delete", `refs/heads/${params.branchName}`], {
150560
- token: ctx.gitToken
150561
- });
150558
+ await pushWithRetry(["origin", "--delete", `refs/heads/${params.branchName}`], ctx.gitToken);
150562
150559
  log.info(`\xBB deleted branch ${params.branchName}`);
150563
150560
  return { success: true, deleted: params.branchName };
150564
150561
  })
@@ -150582,9 +150579,7 @@ function PushTagsTool(ctx) {
150582
150579
  }
150583
150580
  validateTagName(params.tag);
150584
150581
  const pushArgs = [...params.force ? ["-f"] : [], "origin", `refs/tags/${params.tag}`];
150585
- await $git("push", pushArgs, {
150586
- token: ctx.gitToken
150587
- });
150582
+ await pushWithRetry(pushArgs, ctx.gitToken);
150588
150583
  log.info(`\xBB pushed tag ${params.tag}`);
150589
150584
  return { success: true, tag: params.tag };
150590
150585
  })
@@ -159509,6 +159504,13 @@ function formatGenericFailure(errorMessage) {
159509
159504
  "```"
159510
159505
  ].join("\n");
159511
159506
  }
159507
+ function formatMinimalFailureComment(repo) {
159508
+ const runId = process.env.GITHUB_RUN_ID;
159509
+ if (!runId) return "**Run failed.**";
159510
+ const server = process.env.GITHUB_SERVER_URL ?? "https://github.com";
159511
+ const url4 = `${server}/${repo.owner}/${repo.name}/actions/runs/${runId}`;
159512
+ return `**Run failed.** [View the logs \u2192](${url4})`;
159513
+ }
159512
159514
  var PROVIDER_BILLING_URLS = {
159513
159515
  deepseek: "https://platform.deepseek.com/top_up",
159514
159516
  anthropic: "https://console.anthropic.com/settings/billing",
@@ -159582,11 +159584,12 @@ ${body}`, comment: body };
159582
159584
  return { summary: body, comment: body };
159583
159585
  }
159584
159586
  if (hangBody) {
159587
+ const isBillingExhausted = input.agentDiagnostic?.lastProviderError === "provider billing exhausted";
159585
159588
  return {
159586
159589
  summary: `### \u274C Pullfrog failed
159587
159590
 
159588
159591
  ${hangBody}`,
159589
- comment: hangBody
159592
+ comment: isBillingExhausted ? hangBody : formatMinimalFailureComment(input.repo)
159590
159593
  };
159591
159594
  }
159592
159595
  const genericBody = formatGenericFailure(input.errorMessage);
@@ -159594,7 +159597,7 @@ ${hangBody}`,
159594
159597
  summary: `### \u274C Pullfrog failed
159595
159598
 
159596
159599
  ${genericBody}`,
159597
- comment: genericBody
159600
+ comment: formatMinimalFailureComment(input.repo)
159598
159601
  };
159599
159602
  }
159600
159603
 
@@ -30,18 +30,24 @@
30
30
  * 5. Activity-timeout hang — `errorMessage` starts with
31
31
  * `"activity timeout"` or `"agent still pending"` AND none of the
32
32
  * above matched. The harness keeps structured diagnostic state on
33
- * `toolState.agentDiagnostic`; `formatAgentHangBody` renders that as
34
- * a markdown block.
35
- *
36
- * 6. Default a plain-English lead sentence explaining the run failed,
37
- * followed by the raw error message in a fenced code block (so the user
38
- * never sees a bare internal string). The job summary adds the
39
- * `### Pullfrog failed` banner on top of the same body.
40
- *
41
- * The hang body and the API-key body diverge between the two surfaces only
42
- * in that the job summary wraps them in the `### ❌ Pullfrog failed` H3
43
- * banner; the PR comment uses the bare body since it already has Pullfrog
44
- * branding in its footer.
33
+ * `toolState.agentDiagnostic`; `formatAgentHangBody` renders that into
34
+ * the job summary. The PR comment instead collapses to a one-line
35
+ * `**Run failed.** [View the logs →]` — the watchdog jargon, event
36
+ * counts, and benign stderr tail are operator-grade detail that only
37
+ * alarm the average user. The one exception is a hang masking billing
38
+ * exhaustion (#778), where `formatAgentHangBody` emits an actionable
39
+ * top-up CTA that the comment keeps verbatim.
40
+ *
41
+ * 6. Default the job summary gets a plain-English lead sentence plus the
42
+ * raw error in a fenced code block under the `### ❌ Pullfrog failed`
43
+ * banner; the PR comment collapses to the same one-line logs link as
44
+ * the hang case, since the raw internal string helps nobody on the PR.
45
+ *
46
+ * Net: the actionable classifications (billing, API-key, model-not-found)
47
+ * render identical bodies on both surfaces; the non-actionable ones (hang,
48
+ * generic) keep the forensics in the Actions job summary and show a calm
49
+ * one-liner in the PR comment, whose footer already carries Pullfrog
50
+ * branding + rerun links.
45
51
  */
46
52
  import type { AgentDiagnostic } from "./agentHangReport.ts";
47
53
  export type RenderedRunError = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pullfrog",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "pullfrog": "dist/cli.mjs",