fix(push_branch): retry transient push errors and surface full stderr/stdout (#573)

* fix(push_branch): retry transient push errors and surface full stderr/stdout

issue #571 motivated three small improvements to `mcp__pullfrog__push_branch`:

1. classify push errors into `concurrent-push` / `transient` / `unknown`.
   - `concurrent-push` extends the existing `fetch first` / `non-fast-forward`
     matcher to also catch the server-side `cannot lock ref` form (the case
     #571 reports). all three route to the same fetch + integrate + retry
     recovery message; copy now mentions concurrent push as a likely cause.
   - `transient` covers RPC failed, early EOF, connection reset, dns flake,
     HTTP 5xx, HTTP/2 stream not closed, and unexpected sideband disconnect.
     these are retried in-tool with 2s + 5s backoff before surfacing the
     error. push is idempotent so verbatim retry is safe.
   - `unknown` (auth/permission/protected-branch/4xx) is rethrown unchanged —
     retrying these wastes time and noise.

2. surface stdout alongside stderr in `$git` failure messages and include the
   exit code. previously only `stderr.trim()` was forwarded, which could be
   empty in rare HTTPS failure modes (the agent on issue #571's run saw a
   one-line `failed to push some refs` and had nothing to diagnose with).

3. unit tests for the classifier covering all three branches plus the
   concurrent-push-wins-over-transient ordering.

does not introduce auto fetch+rebase+retry inside the tool — that path is
blocked under shell=disabled, can leave the working tree mid-conflict, and
would create unwanted merge commits. the recovery message keeps the agent
in the loop.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(push_branch): retry 429, jitter backoff, downgrade retry log to info

- treat HTTP 429 (rate-limit / abuse detection) as transient — GitHub
  occasionally surfaces it on git push, where it is retry-safe unlike
  401/403/404
- add ±25% jitter to backoff so concurrent agents hit by the same
  upstream blip don't retry in lockstep
- log retries with log.info instead of log.warning to match retry.ts
  convention; a successful retry shouldn't leave a yellow GHA annotation
  behind in the job summary

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: pullfrog[bot] <226033991+pullfrog[bot]@users.noreply.github.com>
This commit is contained in:
David Blass
2026-05-05 21:59:40 +00:00
committed by pullfrog[bot]
parent e58299740d
commit b6e2c61d30
3 changed files with 248 additions and 25 deletions
+13 -2
View File
@@ -154,8 +154,19 @@ export async function $git(
if (result.exitCode !== 0) {
const stderr = result.stderr.trim();
log.info(`git ${subcommand} failed: ${stderr}`);
throw new Error(`git ${subcommand} failed: ${stderr}`);
const stdout = result.stdout.trim();
// stderr is the primary channel for git diagnostics, but in rare cases
// (e.g. some HTTPS smart-protocol failures) the only useful detail is
// on stdout — without it the agent / operator sees an empty error.
// include exit code so we can distinguish e.g. signal-killed (1 with
// empty output) from a genuine git-level rejection.
const detail =
stderr && stdout
? `${stderr}\n--- stdout ---\n${stdout}`
: stderr || stdout || "(no output)";
const message = `git ${subcommand} failed (exit ${result.exitCode}): ${detail}`;
log.info(message);
throw new Error(message);
}
return {