pullfrog 0.1.16 → 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.16",
101883
+ version: "0.1.18",
101884
101884
  type: "module",
101885
101885
  bin: {
101886
101886
  pullfrog: "dist/cli.mjs",
@@ -103582,6 +103582,11 @@ async function installClaudeCli() {
103582
103582
  installDependencies: true
103583
103583
  });
103584
103584
  }
103585
+ var CLAUDE_EXEC_TOOLS = ["Bash", "Monitor", "REPL", "Workflow"];
103586
+ var CLAUDE_DISALLOWED_TOOLS = [
103587
+ ...CLAUDE_EXEC_TOOLS,
103588
+ ...CLAUDE_EXEC_TOOLS.map((t2) => `Agent(${t2})`)
103589
+ ].join(",");
103585
103590
  function writeMcpConfig(ctx) {
103586
103591
  const configDir = join5(ctx.tmpdir, ".claude");
103587
103592
  mkdirSync4(configDir, { recursive: true });
@@ -104104,7 +104109,7 @@ var claude = agent({
104104
104109
  "--effort",
104105
104110
  effort,
104106
104111
  "--disallowedTools",
104107
- "Bash,Agent(Bash)",
104112
+ CLAUDE_DISALLOWED_TOOLS,
104108
104113
  "--agents",
104109
104114
  buildAgentsJson()
104110
104115
  ];
@@ -108079,7 +108084,7 @@ function newTurn() {
108079
108084
  };
108080
108085
  }
108081
108086
  async function consumeEvents(ctx, signal) {
108082
- const result = await ctx.client.event.subscribe();
108087
+ const result = await ctx.client.event.subscribe({}, { signal });
108083
108088
  for await (const event of result.stream) {
108084
108089
  if (signal.aborted) break;
108085
108090
  ctx.eventCount += 1;
@@ -151958,6 +151963,30 @@ function classifyPushError(msg) {
151958
151963
  return "unknown";
151959
151964
  }
151960
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
+ }
151961
151990
  function PushBranchTool(ctx) {
151962
151991
  const defaultBranch = ctx.repo.data.default_branch || "main";
151963
151992
  const pushPermission = ctx.payload.push;
@@ -152025,48 +152054,23 @@ ${postHookStatus}`
152025
152054
  if (force) {
152026
152055
  log.warning(`force pushing - this will overwrite remote history`);
152027
152056
  }
152028
- let lastErr;
152029
- let pushed = false;
152030
- for (let attempt = 0; attempt <= TRANSIENT_RETRY_DELAYS_MS.length; attempt++) {
152031
- try {
152032
- await $git("push", pushArgs, {
152033
- token: ctx.gitToken
152034
- });
152035
- if (attempt > 0) {
152036
- log.info(`push succeeded on attempt ${attempt + 1}`);
152037
- }
152038
- pushed = true;
152039
- break;
152040
- } catch (err) {
152041
- lastErr = err;
152042
- const msg = err instanceof Error ? err.message : String(err);
152043
- const kind = classifyPushError(msg);
152044
- if (kind === "concurrent-push") {
152045
- 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')`;
152046
- throw new Error(
152047
- `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).
152048
152065
 
152049
152066
  to resolve this:
152050
152067
  1. use git_fetch to fetch the remote branch: git_fetch({ ref: "${pushDest.remoteBranch}" })
152051
152068
  ${integrateStep}
152052
152069
  3. resolve any merge conflicts if needed
152053
152070
  4. retry push_branch`
152054
- );
152055
- }
152056
- if (kind === "transient" && attempt < TRANSIENT_RETRY_DELAYS_MS.length) {
152057
- const baseDelay = TRANSIENT_RETRY_DELAYS_MS[attempt] ?? 5e3;
152058
- const delay2 = Math.round(baseDelay * (0.75 + Math.random() * 0.5));
152059
- log.info(
152060
- `push attempt ${attempt + 1} failed (transient), retrying in ${delay2}ms: ${msg.slice(0, 300)}`
152061
- );
152062
- await new Promise((r) => setTimeout(r, delay2));
152063
- continue;
152064
- }
152065
- throw err;
152071
+ );
152066
152072
  }
152067
- }
152068
- if (!pushed) {
152069
- throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
152073
+ throw err;
152070
152074
  }
152071
152075
  const pushedSha = $2("git", ["rev-parse", "HEAD"], { log: false }).trim();
152072
152076
  log.info(
@@ -152309,9 +152313,7 @@ function DeleteBranchTool(ctx) {
152309
152313
  `Blocked: cannot delete the default branch '${defaultBranch}'. If you really need to delete or rename it, do it manually via the repository settings.`
152310
152314
  );
152311
152315
  }
152312
- await $git("push", ["origin", "--delete", `refs/heads/${params.branchName}`], {
152313
- token: ctx.gitToken
152314
- });
152316
+ await pushWithRetry(["origin", "--delete", `refs/heads/${params.branchName}`], ctx.gitToken);
152315
152317
  log.info(`\xBB deleted branch ${params.branchName}`);
152316
152318
  return { success: true, deleted: params.branchName };
152317
152319
  })
@@ -152335,9 +152337,7 @@ function PushTagsTool(ctx) {
152335
152337
  }
152336
152338
  validateTagName(params.tag);
152337
152339
  const pushArgs = [...params.force ? ["-f"] : [], "origin", `refs/tags/${params.tag}`];
152338
- await $git("push", pushArgs, {
152339
- token: ctx.gitToken
152340
- });
152340
+ await pushWithRetry(pushArgs, ctx.gitToken);
152341
152341
  log.info(`\xBB pushed tag ${params.tag}`);
152342
152342
  return { success: true, tag: params.tag };
152343
152343
  })
@@ -161262,6 +161262,13 @@ function formatGenericFailure(errorMessage) {
161262
161262
  "```"
161263
161263
  ].join("\n");
161264
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
+ }
161265
161272
  var PROVIDER_BILLING_URLS = {
161266
161273
  deepseek: "https://platform.deepseek.com/top_up",
161267
161274
  anthropic: "https://console.anthropic.com/settings/billing",
@@ -161335,11 +161342,12 @@ ${body}`, comment: body };
161335
161342
  return { summary: body, comment: body };
161336
161343
  }
161337
161344
  if (hangBody) {
161345
+ const isBillingExhausted = input.agentDiagnostic?.lastProviderError === "provider billing exhausted";
161338
161346
  return {
161339
161347
  summary: `### \u274C Pullfrog failed
161340
161348
 
161341
161349
  ${hangBody}`,
161342
- comment: hangBody
161350
+ comment: isBillingExhausted ? hangBody : formatMinimalFailureComment(input.repo)
161343
161351
  };
161344
161352
  }
161345
161353
  const genericBody = formatGenericFailure(input.errorMessage);
@@ -161347,7 +161355,7 @@ ${hangBody}`,
161347
161355
  summary: `### \u274C Pullfrog failed
161348
161356
 
161349
161357
  ${genericBody}`,
161350
- comment: genericBody
161358
+ comment: formatMinimalFailureComment(input.repo)
161351
161359
  };
161352
161360
  }
161353
161361
 
@@ -162997,7 +163005,7 @@ async function run2() {
162997
163005
  }
162998
163006
 
162999
163007
  // cli.ts
163000
- var VERSION10 = "0.1.16";
163008
+ var VERSION10 = "0.1.18";
163001
163009
  var bin = basename2(process.argv[1] || "");
163002
163010
  var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
163003
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.16",
100083
+ version: "0.1.18",
100084
100084
  type: "module",
100085
100085
  bin: {
100086
100086
  pullfrog: "dist/cli.mjs",
@@ -101782,6 +101782,11 @@ async function installClaudeCli() {
101782
101782
  installDependencies: true
101783
101783
  });
101784
101784
  }
101785
+ var CLAUDE_EXEC_TOOLS = ["Bash", "Monitor", "REPL", "Workflow"];
101786
+ var CLAUDE_DISALLOWED_TOOLS = [
101787
+ ...CLAUDE_EXEC_TOOLS,
101788
+ ...CLAUDE_EXEC_TOOLS.map((t) => `Agent(${t})`)
101789
+ ].join(",");
101785
101790
  function writeMcpConfig(ctx) {
101786
101791
  const configDir = join4(ctx.tmpdir, ".claude");
101787
101792
  mkdirSync4(configDir, { recursive: true });
@@ -102304,7 +102309,7 @@ var claude = agent({
102304
102309
  "--effort",
102305
102310
  effort,
102306
102311
  "--disallowedTools",
102307
- "Bash,Agent(Bash)",
102312
+ CLAUDE_DISALLOWED_TOOLS,
102308
102313
  "--agents",
102309
102314
  buildAgentsJson()
102310
102315
  ];
@@ -106321,7 +106326,7 @@ function newTurn() {
106321
106326
  };
106322
106327
  }
106323
106328
  async function consumeEvents(ctx, signal) {
106324
- const result = await ctx.client.event.subscribe();
106329
+ const result = await ctx.client.event.subscribe({}, { signal });
106325
106330
  for await (const event of result.stream) {
106326
106331
  if (signal.aborted) break;
106327
106332
  ctx.eventCount += 1;
@@ -150200,6 +150205,30 @@ function classifyPushError(msg) {
150200
150205
  return "unknown";
150201
150206
  }
150202
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
+ }
150203
150232
  function PushBranchTool(ctx) {
150204
150233
  const defaultBranch = ctx.repo.data.default_branch || "main";
150205
150234
  const pushPermission = ctx.payload.push;
@@ -150267,48 +150296,23 @@ ${postHookStatus}`
150267
150296
  if (force) {
150268
150297
  log.warning(`force pushing - this will overwrite remote history`);
150269
150298
  }
150270
- let lastErr;
150271
- let pushed = false;
150272
- for (let attempt = 0; attempt <= TRANSIENT_RETRY_DELAYS_MS.length; attempt++) {
150273
- try {
150274
- await $git("push", pushArgs, {
150275
- token: ctx.gitToken
150276
- });
150277
- if (attempt > 0) {
150278
- log.info(`push succeeded on attempt ${attempt + 1}`);
150279
- }
150280
- pushed = true;
150281
- break;
150282
- } catch (err) {
150283
- lastErr = err;
150284
- const msg = err instanceof Error ? err.message : String(err);
150285
- const kind = classifyPushError(msg);
150286
- if (kind === "concurrent-push") {
150287
- 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')`;
150288
- throw new Error(
150289
- `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).
150290
150307
 
150291
150308
  to resolve this:
150292
150309
  1. use git_fetch to fetch the remote branch: git_fetch({ ref: "${pushDest.remoteBranch}" })
150293
150310
  ${integrateStep}
150294
150311
  3. resolve any merge conflicts if needed
150295
150312
  4. retry push_branch`
150296
- );
150297
- }
150298
- if (kind === "transient" && attempt < TRANSIENT_RETRY_DELAYS_MS.length) {
150299
- const baseDelay = TRANSIENT_RETRY_DELAYS_MS[attempt] ?? 5e3;
150300
- const delay2 = Math.round(baseDelay * (0.75 + Math.random() * 0.5));
150301
- log.info(
150302
- `push attempt ${attempt + 1} failed (transient), retrying in ${delay2}ms: ${msg.slice(0, 300)}`
150303
- );
150304
- await new Promise((r) => setTimeout(r, delay2));
150305
- continue;
150306
- }
150307
- throw err;
150313
+ );
150308
150314
  }
150309
- }
150310
- if (!pushed) {
150311
- throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
150315
+ throw err;
150312
150316
  }
150313
150317
  const pushedSha = $("git", ["rev-parse", "HEAD"], { log: false }).trim();
150314
150318
  log.info(
@@ -150551,9 +150555,7 @@ function DeleteBranchTool(ctx) {
150551
150555
  `Blocked: cannot delete the default branch '${defaultBranch}'. If you really need to delete or rename it, do it manually via the repository settings.`
150552
150556
  );
150553
150557
  }
150554
- await $git("push", ["origin", "--delete", `refs/heads/${params.branchName}`], {
150555
- token: ctx.gitToken
150556
- });
150558
+ await pushWithRetry(["origin", "--delete", `refs/heads/${params.branchName}`], ctx.gitToken);
150557
150559
  log.info(`\xBB deleted branch ${params.branchName}`);
150558
150560
  return { success: true, deleted: params.branchName };
150559
150561
  })
@@ -150577,9 +150579,7 @@ function PushTagsTool(ctx) {
150577
150579
  }
150578
150580
  validateTagName(params.tag);
150579
150581
  const pushArgs = [...params.force ? ["-f"] : [], "origin", `refs/tags/${params.tag}`];
150580
- await $git("push", pushArgs, {
150581
- token: ctx.gitToken
150582
- });
150582
+ await pushWithRetry(pushArgs, ctx.gitToken);
150583
150583
  log.info(`\xBB pushed tag ${params.tag}`);
150584
150584
  return { success: true, tag: params.tag };
150585
150585
  })
@@ -159504,6 +159504,13 @@ function formatGenericFailure(errorMessage) {
159504
159504
  "```"
159505
159505
  ].join("\n");
159506
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
+ }
159507
159514
  var PROVIDER_BILLING_URLS = {
159508
159515
  deepseek: "https://platform.deepseek.com/top_up",
159509
159516
  anthropic: "https://console.anthropic.com/settings/billing",
@@ -159577,11 +159584,12 @@ ${body}`, comment: body };
159577
159584
  return { summary: body, comment: body };
159578
159585
  }
159579
159586
  if (hangBody) {
159587
+ const isBillingExhausted = input.agentDiagnostic?.lastProviderError === "provider billing exhausted";
159580
159588
  return {
159581
159589
  summary: `### \u274C Pullfrog failed
159582
159590
 
159583
159591
  ${hangBody}`,
159584
- comment: hangBody
159592
+ comment: isBillingExhausted ? hangBody : formatMinimalFailureComment(input.repo)
159585
159593
  };
159586
159594
  }
159587
159595
  const genericBody = formatGenericFailure(input.errorMessage);
@@ -159589,7 +159597,7 @@ ${hangBody}`,
159589
159597
  summary: `### \u274C Pullfrog failed
159590
159598
 
159591
159599
  ${genericBody}`,
159592
- comment: genericBody
159600
+ comment: formatMinimalFailureComment(input.repo)
159593
159601
  };
159594
159602
  }
159595
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.16",
3
+ "version": "0.1.18",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "pullfrog": "dist/cli.mjs",