pullfrog 0.1.7 → 0.1.8

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
@@ -142414,7 +142414,7 @@ var import_semver = __toESM(require_semver2(), 1);
142414
142414
  // package.json
142415
142415
  var package_default = {
142416
142416
  name: "pullfrog",
142417
- version: "0.1.7",
142417
+ version: "0.1.8",
142418
142418
  type: "module",
142419
142419
  bin: {
142420
142420
  pullfrog: "dist/cli.mjs",
@@ -142882,6 +142882,51 @@ function readNumber(params) {
142882
142882
  import { execSync } from "node:child_process";
142883
142883
  import { createHash } from "node:crypto";
142884
142884
  import { readFileSync as readFileSync2, realpathSync, unlinkSync } from "node:fs";
142885
+
142886
+ // utils/shell.ts
142887
+ import { spawnSync as spawnSync2 } from "node:child_process";
142888
+ function $(cmd, args2, options) {
142889
+ const encoding = options?.encoding ?? "utf-8";
142890
+ const env2 = resolveEnv(options?.env);
142891
+ const result = spawnSync2(cmd, args2, {
142892
+ stdio: ["ignore", "pipe", "pipe"],
142893
+ encoding,
142894
+ cwd: options?.cwd,
142895
+ env: env2
142896
+ });
142897
+ const stdout = result.stdout ?? "";
142898
+ const stderr = result.stderr ?? "";
142899
+ if (options?.log !== false) {
142900
+ const canWriteToStdout = process.stdout.isTTY === true;
142901
+ if (stdout) {
142902
+ if (canWriteToStdout) {
142903
+ process.stdout.write(stdout);
142904
+ } else {
142905
+ process.stderr.write(stdout);
142906
+ }
142907
+ }
142908
+ if (stderr) {
142909
+ process.stderr.write(stderr);
142910
+ }
142911
+ }
142912
+ if (result.status !== 0) {
142913
+ const errorResult = {
142914
+ status: result.status ?? -1,
142915
+ stdout,
142916
+ stderr
142917
+ };
142918
+ if (options?.onError) {
142919
+ options.onError(errorResult);
142920
+ return stdout.trim();
142921
+ }
142922
+ throw new Error(
142923
+ `Command failed with exit code ${errorResult.status}: ${stderr || "Unknown error"}`
142924
+ );
142925
+ }
142926
+ return stdout.trim();
142927
+ }
142928
+
142929
+ // utils/gitAuth.ts
142885
142930
  var gitBinary;
142886
142931
  function hashFile(path3) {
142887
142932
  return createHash("sha256").update(readFileSync2(path3)).digest("hex");
@@ -142973,6 +143018,27 @@ ${stdout}` : stderr || stdout || "(no output)";
142973
143018
  }
142974
143019
  }
142975
143020
  }
143021
+ var SHALLOW_UNREACHABLE_PATTERNS = [
143022
+ /Could not read [a-f0-9]{40,64}/,
143023
+ /remote did not send all necessary objects/
143024
+ ];
143025
+ var DEEPEN_RETRY_DEPTH = 1e3;
143026
+ async function $gitFetchWithDeepen(args2, options, label) {
143027
+ try {
143028
+ return await $git("fetch", args2, options);
143029
+ } catch (err) {
143030
+ const msg = err instanceof Error ? err.message : String(err);
143031
+ const isShallowUnreachable = SHALLOW_UNREACHABLE_PATTERNS.some((p) => p.test(msg));
143032
+ if (!isShallowUnreachable) throw err;
143033
+ const isShallow = $("git", ["rev-parse", "--is-shallow-repository"], { log: false }).trim() === "true";
143034
+ if (!isShallow) throw err;
143035
+ log.info(
143036
+ `\xBB ${label ?? "git fetch"} hit shallow-unreachable error, retrying with --deepen=${DEEPEN_RETRY_DEPTH}`
143037
+ );
143038
+ const retryArgs = args2.filter((a) => !a.startsWith("--depth="));
143039
+ return await $git("fetch", [`--deepen=${DEEPEN_RETRY_DEPTH}`, ...retryArgs], options);
143040
+ }
143041
+ }
142976
143042
 
142977
143043
  // lifecycle.ts
142978
143044
  var LIFECYCLE_HOOK_TIMEOUT_MS = 6e5;
@@ -143014,49 +143080,6 @@ async function executeLifecycleHook(params) {
143014
143080
  }
143015
143081
  }
143016
143082
 
143017
- // utils/shell.ts
143018
- import { spawnSync as spawnSync2 } from "node:child_process";
143019
- function $(cmd, args2, options) {
143020
- const encoding = options?.encoding ?? "utf-8";
143021
- const env2 = resolveEnv(options?.env);
143022
- const result = spawnSync2(cmd, args2, {
143023
- stdio: ["ignore", "pipe", "pipe"],
143024
- encoding,
143025
- cwd: options?.cwd,
143026
- env: env2
143027
- });
143028
- const stdout = result.stdout ?? "";
143029
- const stderr = result.stderr ?? "";
143030
- if (options?.log !== false) {
143031
- const canWriteToStdout = process.stdout.isTTY === true;
143032
- if (stdout) {
143033
- if (canWriteToStdout) {
143034
- process.stdout.write(stdout);
143035
- } else {
143036
- process.stderr.write(stdout);
143037
- }
143038
- }
143039
- if (stderr) {
143040
- process.stderr.write(stderr);
143041
- }
143042
- }
143043
- if (result.status !== 0) {
143044
- const errorResult = {
143045
- status: result.status ?? -1,
143046
- stdout,
143047
- stderr
143048
- };
143049
- if (options?.onError) {
143050
- options.onError(errorResult);
143051
- return stdout.trim();
143052
- }
143053
- throw new Error(
143054
- `Command failed with exit code ${errorResult.status}: ${stderr || "Unknown error"}`
143055
- );
143056
- }
143057
- return stdout.trim();
143058
- }
143059
-
143060
143083
  // utils/rangeDiff.ts
143061
143084
  function computeIncrementalDiff(params) {
143062
143085
  try {
@@ -143451,11 +143474,6 @@ var GitFetch = type({
143451
143474
  ref: type.string.describe("Ref to fetch: branch name, tag, or 'pull/N/head' for PRs"),
143452
143475
  depth: type.number.describe("Fetch depth (for shallow clones)").optional()
143453
143476
  });
143454
- var SHALLOW_UNREACHABLE_PATTERNS = [
143455
- /Could not read [a-f0-9]{40,64}/,
143456
- /remote did not send all necessary objects/
143457
- ];
143458
- var DEEPEN_RETRY_DEPTH = 1e3;
143459
143477
  function GitFetchTool(ctx) {
143460
143478
  return tool({
143461
143479
  name: "git_fetch",
@@ -143467,20 +143485,7 @@ function GitFetchTool(ctx) {
143467
143485
  if (params.depth !== void 0) {
143468
143486
  fetchArgs.push(`--depth=${params.depth}`);
143469
143487
  }
143470
- try {
143471
- await $git("fetch", fetchArgs, { token: ctx.gitToken });
143472
- } catch (err) {
143473
- const msg = err instanceof Error ? err.message : String(err);
143474
- const isShallowUnreachable = SHALLOW_UNREACHABLE_PATTERNS.some((p) => p.test(msg));
143475
- const isShallow = isShallowUnreachable && $("git", ["rev-parse", "--is-shallow-repository"], { log: false }).trim() === "true";
143476
- if (!isShallow) throw err;
143477
- log.info(
143478
- `\xBB git_fetch hit shallow-unreachable error, retrying with --deepen=${DEEPEN_RETRY_DEPTH}`
143479
- );
143480
- await $git("fetch", [`--deepen=${DEEPEN_RETRY_DEPTH}`, "--no-tags", "origin", params.ref], {
143481
- token: ctx.gitToken
143482
- });
143483
- }
143488
+ await $gitFetchWithDeepen(fetchArgs, { token: ctx.gitToken }, "git_fetch");
143484
143489
  return { success: true, ref: params.ref };
143485
143490
  })
143486
143491
  });
@@ -144204,10 +144209,10 @@ async function ensureBeforeShaReachable(params) {
144204
144209
  sha: params.sha,
144205
144210
  ref: tempBranch
144206
144211
  }), true);
144207
- await $git(
144208
- "fetch",
144212
+ await $gitFetchWithDeepen(
144209
144213
  ["--no-tags", ...params.isShallow ? ["--depth=1"] : [], "origin", tempBranch],
144210
- { token: params.gitToken }
144214
+ { token: params.gitToken },
144215
+ `before_sha temp branch ${tempBranch}`
144211
144216
  );
144212
144217
  log.debug(`\xBB fetched before_sha via temp branch ${tempBranch}`);
144213
144218
  return true;
@@ -144283,16 +144288,22 @@ async function checkoutPrBranch(pr, params) {
144283
144288
  toolState.checkoutSha = $("git", ["rev-parse", "HEAD"], { log: false }).trim();
144284
144289
  const alreadyOnBranch = toolState.checkoutSha === pr.headSha;
144285
144290
  log.debug(`\xBB fetching base branch (${pr.baseRef})...`);
144286
- await $git("fetch", ["--no-tags", "origin", pr.baseRef], { token: gitToken });
144291
+ await $gitFetchWithDeepen(
144292
+ ["--no-tags", "origin", pr.baseRef],
144293
+ { token: gitToken },
144294
+ `base branch ${pr.baseRef}`
144295
+ );
144287
144296
  if (!alreadyOnBranch) {
144288
144297
  $("git", ["checkout", "-B", pr.baseRef, `origin/${pr.baseRef}`], { log: false });
144289
144298
  log.debug(`\xBB fetching PR #${pr.number} (${localBranch})...`);
144290
144299
  await retry(
144291
144300
  async () => {
144292
144301
  try {
144293
- await $git("fetch", ["--no-tags", "origin", `+pull/${pr.number}/head:${localBranch}`], {
144294
- token: gitToken
144295
- });
144302
+ await $gitFetchWithDeepen(
144303
+ ["--no-tags", "origin", `+pull/${pr.number}/head:${localBranch}`],
144304
+ { token: gitToken },
144305
+ `PR #${pr.number}`
144306
+ );
144296
144307
  } catch (e) {
144297
144308
  const msg = e instanceof Error ? e.message : String(e);
144298
144309
  if (PULL_REF_MISSING_PATTERN.test(msg)) {
@@ -144393,134 +144404,159 @@ async function checkoutPrBranch(pr, params) {
144393
144404
  });
144394
144405
  return { hookWarning: postCheckoutHook.warning };
144395
144406
  }
144407
+ var inFlightCheckouts = /* @__PURE__ */ new Map();
144396
144408
  function CheckoutPrTool(ctx) {
144409
+ const runCheckout = async (pull_number) => {
144410
+ const prResponse = await ctx.octokit.rest.pulls.get({
144411
+ owner: ctx.repo.owner,
144412
+ repo: ctx.repo.name,
144413
+ pull_number
144414
+ });
144415
+ const headRepo = prResponse.data.head.repo;
144416
+ if (!headRepo) {
144417
+ throw new Error(`PR #${pull_number} source repository was deleted`);
144418
+ }
144419
+ const pr = {
144420
+ number: pull_number,
144421
+ headSha: prResponse.data.head.sha,
144422
+ headRef: prResponse.data.head.ref,
144423
+ headRepoFullName: headRepo.full_name,
144424
+ baseRef: prResponse.data.base.ref,
144425
+ baseRepoFullName: prResponse.data.base.repo.full_name,
144426
+ maintainerCanModify: prResponse.data.maintainer_can_modify
144427
+ };
144428
+ const checkoutResult = await checkoutPrBranch(pr, {
144429
+ octokit: ctx.octokit,
144430
+ owner: ctx.repo.owner,
144431
+ name: ctx.repo.name,
144432
+ gitToken: ctx.gitToken,
144433
+ toolState: ctx.toolState,
144434
+ shell: ctx.payload.shell,
144435
+ postCheckoutScript: ctx.postCheckoutScript,
144436
+ beforeSha: ctx.toolState.beforeSha
144437
+ });
144438
+ const tempDir = process.env.PULLFROG_TEMP_DIR;
144439
+ if (!tempDir) {
144440
+ throw new Error(
144441
+ "PULLFROG_TEMP_DIR not set - checkout_pr must run in pullfrog action context"
144442
+ );
144443
+ }
144444
+ const headShort = ctx.toolState.checkoutSha.slice(0, 7);
144445
+ let incrementalDiffPath;
144446
+ if (ctx.toolState.beforeSha && ctx.toolState.checkoutSha) {
144447
+ const beforeShort = ctx.toolState.beforeSha.slice(0, 7);
144448
+ const incremental = computeIncrementalDiff({
144449
+ baseBranch: pr.baseRef,
144450
+ beforeSha: ctx.toolState.beforeSha,
144451
+ headSha: ctx.toolState.checkoutSha
144452
+ });
144453
+ if (incremental) {
144454
+ incrementalDiffPath = join3(
144455
+ tempDir,
144456
+ `pr-${pull_number}-${beforeShort}-${headShort}-incremental.diff`
144457
+ );
144458
+ writeFileSync(incrementalDiffPath, incremental);
144459
+ log.info(
144460
+ `\xBB incremental diff computed (${incremental.length} bytes) \u2192 ${incrementalDiffPath}`
144461
+ );
144462
+ }
144463
+ }
144464
+ const formatResult = await fetchAndFormatPrDiff(ctx, pull_number);
144465
+ const diffPreview = formatResult.content.split("\n").slice(0, 100).join("\n");
144466
+ log.debug(`formatted diff preview (first 100 lines):
144467
+ ${diffPreview}`);
144468
+ const diffPath = join3(tempDir, `pr-${pull_number}-${headShort}.diff`);
144469
+ writeFileSync(diffPath, formatResult.content);
144470
+ log.debug(`wrote diff to ${diffPath} (${formatResult.content.length} bytes)`);
144471
+ ctx.toolState.diffCoverage = createDiffCoverageState({
144472
+ diffPath,
144473
+ totalLines: countLines({ content: formatResult.content }),
144474
+ toc: formatResult.toc,
144475
+ previous: ctx.toolState.diffCoverage
144476
+ });
144477
+ log.debug(
144478
+ `\xBB diff coverage initialized: diffPath=${diffPath}, totalLines=${ctx.toolState.diffCoverage.totalLines}, tocEntries=${ctx.toolState.diffCoverage.tocEntries.length}`
144479
+ );
144480
+ const cached4 = /* @__PURE__ */ new Map();
144481
+ for (const file2 of formatResult.files) {
144482
+ cached4.set(file2.filename, commentableLinesForFile(file2.patch));
144483
+ }
144484
+ ctx.toolState.commentableLinesByFile = cached4;
144485
+ ctx.toolState.commentableLinesPullNumber = pull_number;
144486
+ ctx.toolState.commentableLinesCheckoutSha = ctx.toolState.checkoutSha;
144487
+ const incrementalInstructions = incrementalDiffPath ? ` IMPORTANT: incrementalDiffPath contains ONLY the changes since the last reviewed version (computed via range-diff). you MUST read incrementalDiffPath FIRST to understand what changed, then use diffPath for full PR context. do NOT skip the incremental diff.` : "";
144488
+ const COMMIT_LOG_MAX = 200;
144489
+ const baseRange = `origin/${pr.baseRef}..HEAD`;
144490
+ let commitCount = 0;
144491
+ let commitLog = "";
144492
+ let commitLogUnavailable = false;
144493
+ try {
144494
+ commitCount = parseInt(
144495
+ $("git", ["rev-list", "--count", baseRange], { log: false }).trim() || "0",
144496
+ 10
144497
+ );
144498
+ commitLog = $("git", ["log", "--oneline", `--max-count=${COMMIT_LOG_MAX}`, baseRange], {
144499
+ log: false
144500
+ });
144501
+ } catch (err) {
144502
+ commitLogUnavailable = true;
144503
+ log.debug(
144504
+ `\xBB unable to compute commit metadata for ${baseRange}: ${err instanceof Error ? err.message : String(err)}`
144505
+ );
144506
+ }
144507
+ const commitLogTruncated = commitCount > COMMIT_LOG_MAX;
144508
+ const hookWarningInstructions = checkoutResult.hookWarning ? ` HOOK WARNING: the post-checkout lifecycle hook reported a non-fatal failure (see hookWarning). decide whether to retry based on the guidance in that field before proceeding.` : "";
144509
+ const commitLogInstructions = commitLogUnavailable ? ` NOTE: commit metadata is partial (base ref unreachable, likely a shallow fetch). commitCount/commitLog may be 0/empty or incomplete; treat them as "unknown" rather than "no commits", and use \`git log\` directly if you need the full history.` : commitLogTruncated ? ` NOTE: commitLog was capped at ${COMMIT_LOG_MAX} entries out of ${commitCount} commits; use \`git log\` directly if you need the full history.` : "";
144510
+ return {
144511
+ success: true,
144512
+ number: prResponse.data.number,
144513
+ title: prResponse.data.title,
144514
+ body: prResponse.data.body,
144515
+ base: pr.baseRef,
144516
+ localBranch: `pr-${pull_number}`,
144517
+ remoteBranch: `refs/heads/${pr.headRef}`,
144518
+ isFork: pr.headRepoFullName !== pr.baseRepoFullName,
144519
+ maintainerCanModify: pr.maintainerCanModify,
144520
+ url: prResponse.data.html_url,
144521
+ headRepo: pr.headRepoFullName,
144522
+ diffPath,
144523
+ incrementalDiffPath,
144524
+ toc: formatResult.toc,
144525
+ commitCount,
144526
+ commitLog,
144527
+ commitLogTruncated,
144528
+ commitLogUnavailable,
144529
+ hookWarning: checkoutResult.hookWarning,
144530
+ instructions: `the diff file at diffPath contains a table of contents (TOC) at the top listing every changed file with its line range. use the TOC line ranges as your checklist and read specific files from the diff instead of reading the entire file. for example, if the TOC says "src/foo.ts \u2192 lines 5-42", read lines 5-42 from diffPath to see that file's changes. review files selectively based on relevance rather than reading everything sequentially. to inspect the PR's changed files, use diffPath \u2014 do NOT run \`git diff <base>..<head>\` to re-derive what's already in diffPath. the formatted diff with line numbers is authoritative. \`git log\` and \`git diff --stat\` are fine for commit-range overview, and \`git diff\` / \`git diff --cached\` are fine for inspecting *your own* uncommitted changes \u2014 but PR review content MUST come from diffPath. before your review is submitted, a one-time coverage pre-flight may error listing unread TOC regions. retry the same create_pull_request_review call to proceed \u2014 optionally after reading the listed ranges. the pre-flight will not block again this session. the local branch is 'localBranch' (pr-{number}), not the remote branch name. when pushing, omit branchName to use the current branch. do not use remoteBranch as a local branch name.` + incrementalInstructions + hookWarningInstructions + commitLogInstructions
144531
+ };
144532
+ };
144397
144533
  return tool({
144398
144534
  name: "checkout_pr",
144399
144535
  description: "Checkout a pull request branch locally. This fetches the PR branch and sets up push configuration for fork PRs. Returns diffPath pointing to the formatted diff file. Example: `checkout_pr({ pull_number: 1234 })`. Transient fetch timeouts are common \u2014 retry the same call up to a few times before treating the failure as terminal. If the error mentions `.git/shallow.lock: File exists` or `.git/index.lock: File exists`, that's a stale lock from a prior timed-out fetch \u2014 remove it via the shell tool (`rm -f .git/shallow.lock .git/index.lock`) and retry.",
144400
144536
  parameters: CheckoutPr,
144401
144537
  execute: execute(async ({ pull_number }) => {
144402
- const prResponse = await ctx.octokit.rest.pulls.get({
144403
- owner: ctx.repo.owner,
144404
- repo: ctx.repo.name,
144405
- pull_number
144406
- });
144407
- const headRepo = prResponse.data.head.repo;
144408
- if (!headRepo) {
144409
- throw new Error(`PR #${pull_number} source repository was deleted`);
144410
- }
144411
- const pr = {
144412
- number: pull_number,
144413
- headSha: prResponse.data.head.sha,
144414
- headRef: prResponse.data.head.ref,
144415
- headRepoFullName: headRepo.full_name,
144416
- baseRef: prResponse.data.base.ref,
144417
- baseRepoFullName: prResponse.data.base.repo.full_name,
144418
- maintainerCanModify: prResponse.data.maintainer_can_modify
144419
- };
144420
- const checkoutResult = await checkoutPrBranch(pr, {
144421
- octokit: ctx.octokit,
144422
- owner: ctx.repo.owner,
144423
- name: ctx.repo.name,
144424
- gitToken: ctx.gitToken,
144425
- toolState: ctx.toolState,
144426
- shell: ctx.payload.shell,
144427
- postCheckoutScript: ctx.postCheckoutScript,
144428
- beforeSha: ctx.toolState.beforeSha
144429
- });
144430
- const tempDir = process.env.PULLFROG_TEMP_DIR;
144431
- if (!tempDir) {
144432
- throw new Error(
144433
- "PULLFROG_TEMP_DIR not set - checkout_pr must run in pullfrog action context"
144434
- );
144435
- }
144436
- const headShort = ctx.toolState.checkoutSha.slice(0, 7);
144437
- let incrementalDiffPath;
144438
- if (ctx.toolState.beforeSha && ctx.toolState.checkoutSha) {
144439
- const beforeShort = ctx.toolState.beforeSha.slice(0, 7);
144440
- const incremental = computeIncrementalDiff({
144441
- baseBranch: pr.baseRef,
144442
- beforeSha: ctx.toolState.beforeSha,
144443
- headSha: ctx.toolState.checkoutSha
144444
- });
144445
- if (incremental) {
144446
- incrementalDiffPath = join3(
144447
- tempDir,
144448
- `pr-${pull_number}-${beforeShort}-${headShort}-incremental.diff`
144449
- );
144450
- writeFileSync(incrementalDiffPath, incremental);
144451
- log.info(
144452
- `\xBB incremental diff computed (${incremental.length} bytes) \u2192 ${incrementalDiffPath}`
144538
+ const inFlight = inFlightCheckouts.get(pull_number);
144539
+ if (inFlight) {
144540
+ log.info(`\xBB checkout_pr({pull_number:${pull_number}}) already in flight \u2014 sharing result`);
144541
+ return inFlight;
144542
+ }
144543
+ const current = ctx.toolState.issueNumber;
144544
+ if (current !== void 0 && current !== pull_number) {
144545
+ const dirty = $("git", ["status", "--porcelain"], { log: false }).trim();
144546
+ if (dirty) {
144547
+ throw new Error(
144548
+ `cannot checkout PR #${pull_number} while the working tree has uncommitted changes. commit, push, or discard them before switching. dirty paths:
144549
+ ${dirty}`
144453
144550
  );
144454
144551
  }
144455
144552
  }
144456
- const formatResult = await fetchAndFormatPrDiff(ctx, pull_number);
144457
- const diffPreview = formatResult.content.split("\n").slice(0, 100).join("\n");
144458
- log.debug(`formatted diff preview (first 100 lines):
144459
- ${diffPreview}`);
144460
- const diffPath = join3(tempDir, `pr-${pull_number}-${headShort}.diff`);
144461
- writeFileSync(diffPath, formatResult.content);
144462
- log.debug(`wrote diff to ${diffPath} (${formatResult.content.length} bytes)`);
144463
- ctx.toolState.diffCoverage = createDiffCoverageState({
144464
- diffPath,
144465
- totalLines: countLines({ content: formatResult.content }),
144466
- toc: formatResult.toc,
144467
- previous: ctx.toolState.diffCoverage
144468
- });
144469
- log.debug(
144470
- `\xBB diff coverage initialized: diffPath=${diffPath}, totalLines=${ctx.toolState.diffCoverage.totalLines}, tocEntries=${ctx.toolState.diffCoverage.tocEntries.length}`
144471
- );
144472
- const cached4 = /* @__PURE__ */ new Map();
144473
- for (const file2 of formatResult.files) {
144474
- cached4.set(file2.filename, commentableLinesForFile(file2.patch));
144475
- }
144476
- ctx.toolState.commentableLinesByFile = cached4;
144477
- ctx.toolState.commentableLinesPullNumber = pull_number;
144478
- ctx.toolState.commentableLinesCheckoutSha = ctx.toolState.checkoutSha;
144479
- const incrementalInstructions = incrementalDiffPath ? ` IMPORTANT: incrementalDiffPath contains ONLY the changes since the last reviewed version (computed via range-diff). you MUST read incrementalDiffPath FIRST to understand what changed, then use diffPath for full PR context. do NOT skip the incremental diff.` : "";
144480
- const COMMIT_LOG_MAX = 200;
144481
- const baseRange = `origin/${pr.baseRef}..HEAD`;
144482
- let commitCount = 0;
144483
- let commitLog = "";
144484
- let commitLogUnavailable = false;
144553
+ const promise2 = runCheckout(pull_number);
144554
+ inFlightCheckouts.set(pull_number, promise2);
144485
144555
  try {
144486
- commitCount = parseInt(
144487
- $("git", ["rev-list", "--count", baseRange], { log: false }).trim() || "0",
144488
- 10
144489
- );
144490
- commitLog = $("git", ["log", "--oneline", `--max-count=${COMMIT_LOG_MAX}`, baseRange], {
144491
- log: false
144492
- });
144493
- } catch (err) {
144494
- commitLogUnavailable = true;
144495
- log.debug(
144496
- `\xBB unable to compute commit metadata for ${baseRange}: ${err instanceof Error ? err.message : String(err)}`
144497
- );
144556
+ return await promise2;
144557
+ } finally {
144558
+ inFlightCheckouts.delete(pull_number);
144498
144559
  }
144499
- const commitLogTruncated = commitCount > COMMIT_LOG_MAX;
144500
- const hookWarningInstructions = checkoutResult.hookWarning ? ` HOOK WARNING: the post-checkout lifecycle hook reported a non-fatal failure (see hookWarning). decide whether to retry based on the guidance in that field before proceeding.` : "";
144501
- const commitLogInstructions = commitLogUnavailable ? ` NOTE: commit metadata is partial (base ref unreachable, likely a shallow fetch). commitCount/commitLog may be 0/empty or incomplete; treat them as "unknown" rather than "no commits", and use \`git log\` directly if you need the full history.` : commitLogTruncated ? ` NOTE: commitLog was capped at ${COMMIT_LOG_MAX} entries out of ${commitCount} commits; use \`git log\` directly if you need the full history.` : "";
144502
- return {
144503
- success: true,
144504
- number: prResponse.data.number,
144505
- title: prResponse.data.title,
144506
- body: prResponse.data.body,
144507
- base: pr.baseRef,
144508
- localBranch: `pr-${pull_number}`,
144509
- remoteBranch: `refs/heads/${pr.headRef}`,
144510
- isFork: pr.headRepoFullName !== pr.baseRepoFullName,
144511
- maintainerCanModify: pr.maintainerCanModify,
144512
- url: prResponse.data.html_url,
144513
- headRepo: pr.headRepoFullName,
144514
- diffPath,
144515
- incrementalDiffPath,
144516
- toc: formatResult.toc,
144517
- commitCount,
144518
- commitLog,
144519
- commitLogTruncated,
144520
- commitLogUnavailable,
144521
- hookWarning: checkoutResult.hookWarning,
144522
- instructions: `the diff file at diffPath contains a table of contents (TOC) at the top listing every changed file with its line range. use the TOC line ranges as your checklist and read specific files from the diff instead of reading the entire file. for example, if the TOC says "src/foo.ts \u2192 lines 5-42", read lines 5-42 from diffPath to see that file's changes. review files selectively based on relevance rather than reading everything sequentially. to inspect the PR's changed files, use diffPath \u2014 do NOT run \`git diff <base>..<head>\` to re-derive what's already in diffPath. the formatted diff with line numbers is authoritative. \`git log\` and \`git diff --stat\` are fine for commit-range overview, and \`git diff\` / \`git diff --cached\` are fine for inspecting *your own* uncommitted changes \u2014 but PR review content MUST come from diffPath. before your review is submitted, a one-time coverage pre-flight may error listing unread TOC regions. retry the same create_pull_request_review call to proceed \u2014 optionally after reading the listed ranges. the pre-flight will not block again this session. the local branch is 'localBranch' (pr-{number}), not the remote branch name. when pushing, omit branchName to use the current branch. do not use remoteBranch as a local branch name.` + incrementalInstructions + hookWarningInstructions + commitLogInstructions
144523
- };
144524
144560
  })
144525
144561
  });
144526
144562
  }
@@ -145916,6 +145952,15 @@ function getTempDir() {
145916
145952
  }
145917
145953
  return tempDir;
145918
145954
  }
145955
+ var MAX_OUTPUT_CHARS = 5e3;
145956
+ function capOutput(output) {
145957
+ if (output.length <= MAX_OUTPUT_CHARS) return output;
145958
+ const fullPath = join7(getTempDir(), `shell-${randomUUID2().slice(0, 8)}.log`);
145959
+ writeFileSync5(fullPath, output);
145960
+ const elided = output.length - MAX_OUTPUT_CHARS;
145961
+ return `... [${elided} chars truncated; full output saved to ${fullPath}] ...
145962
+ ${output.slice(-MAX_OUTPUT_CHARS)}`;
145963
+ }
145919
145964
  function isGitCommand(command) {
145920
145965
  const trimmed = command.trim();
145921
145966
  if (trimmed === "git" || trimmed.startsWith("git ")) return true;
@@ -145934,6 +145979,8 @@ Use this tool to:
145934
145979
  - Execute build tools (npm, pnpm, cargo, make, etc.)
145935
145980
  - Run tests and linters
145936
145981
 
145982
+ Output is capped at ${MAX_OUTPUT_CHARS} chars: if exceeded, only the tail is returned and the full body is saved to a tempfile (path included in the response). Re-read the tempfile with cat/tail/grep when you need more.
145983
+
145937
145984
  Do NOT use this tool for git commands \u2014 use the dedicated git tools instead.`,
145938
145985
  parameters: ShellParams,
145939
145986
  execute: execute(async (params) => {
@@ -146024,12 +146071,13 @@ ${stderr}` : stderr : stdout;
146024
146071
  output = output ? `${output}
146025
146072
  [timed out after ${timeout}ms]` : `[timed out after ${timeout}ms]`;
146026
146073
  const finalExitCode = exitCode ?? (timedOut ? 124 : -1);
146074
+ const trimmed = output.trim();
146027
146075
  if (finalExitCode !== 0) {
146028
146076
  log.info(`shell command failed with exit code ${finalExitCode}: ${params.command}`);
146029
- if (output) log.info(`output: ${output.trim()}`);
146077
+ if (trimmed) log.info(`output: ${trimmed}`);
146030
146078
  }
146031
146079
  return {
146032
- output: output.trim(),
146080
+ output: capOutput(trimmed),
146033
146081
  exit_code: finalExitCode,
146034
146082
  timed_out: timedOut
146035
146083
  };
@@ -146902,12 +146950,38 @@ var PROVIDER_ERROR_PATTERNS = [
146902
146950
  // around `limit` rejects keys like `time_limit` or `field_limit`.
146903
146951
  { regex: /["']?\blimit\b["']?\s*:\s*0\b/, label: "zero quota" }
146904
146952
  ];
146905
- function detectProviderError(text) {
146953
+ var EXCERPT_MAX_BYTES = 600;
146954
+ var LINES_BEFORE = 1;
146955
+ var LINES_AFTER = 2;
146956
+ function findProviderErrorMatch(text) {
146906
146957
  for (const entry of PROVIDER_ERROR_PATTERNS) {
146907
- if (entry.regex.test(text)) return entry.label;
146958
+ const m = entry.regex.exec(text);
146959
+ if (!m) continue;
146960
+ return { label: entry.label, excerpt: extractExcerpt(text, m.index) };
146908
146961
  }
146909
146962
  return null;
146910
146963
  }
146964
+ function extractExcerpt(text, matchIndex) {
146965
+ const lineStart = text.lastIndexOf("\n", matchIndex - 1) + 1;
146966
+ const lineEndRaw = text.indexOf("\n", matchIndex);
146967
+ const lineEnd = lineEndRaw === -1 ? text.length : lineEndRaw;
146968
+ let start = lineStart;
146969
+ for (let i = 0; i < LINES_BEFORE && start > 0; i++) {
146970
+ const prev = text.lastIndexOf("\n", start - 2);
146971
+ start = prev < 0 ? 0 : prev + 1;
146972
+ }
146973
+ let end = lineEnd;
146974
+ for (let i = 0; i < LINES_AFTER && end < text.length; i++) {
146975
+ const next2 = text.indexOf("\n", end + 1);
146976
+ end = next2 < 0 ? text.length : next2;
146977
+ }
146978
+ let excerpt = text.slice(start, end);
146979
+ if (excerpt.length > EXCERPT_MAX_BYTES) {
146980
+ excerpt = text.slice(lineStart, lineEnd);
146981
+ if (excerpt.length > EXCERPT_MAX_BYTES) excerpt = excerpt.slice(0, EXCERPT_MAX_BYTES);
146982
+ }
146983
+ return excerpt.trim();
146984
+ }
146911
146985
  var ROUTER_KEYLIMIT_EXHAUSTED_PATTERN = /requires more credits.*?fewer max_tokens|requested up to \d+ tokens.*?can only afford/is;
146912
146986
  function isRouterKeylimitExhaustedError(text) {
146913
146987
  return ROUTER_KEYLIMIT_EXHAUSTED_PATTERN.test(text);
@@ -147612,10 +147686,10 @@ async function runClaude(params) {
147612
147686
  if (!trimmed) return;
147613
147687
  recentStderr.push(trimmed);
147614
147688
  if (recentStderr.length > MAX_STDERR_LINES) recentStderr.shift();
147615
- const providerError = detectProviderError(trimmed);
147616
- if (providerError) {
147617
- lastProviderError = providerError;
147618
- log.info(`\xBB provider error detected (${providerError}): ${trimmed.substring(0, 500)}`);
147689
+ const match3 = findProviderErrorMatch(trimmed);
147690
+ if (match3) {
147691
+ lastProviderError = match3.label;
147692
+ log.info(`\xBB provider error detected (${match3.label}): ${match3.excerpt}`);
147619
147693
  } else {
147620
147694
  log.debug(trimmed);
147621
147695
  }
@@ -147833,6 +147907,68 @@ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "node:f
147833
147907
  import { join as join11 } from "node:path";
147834
147908
  import { performance as performance7 } from "node:perf_hooks";
147835
147909
 
147910
+ // utils/agentHangReport.ts
147911
+ var MAX_STDERR_BYTES = 3e3;
147912
+ function formatAgentHangBody(input) {
147913
+ if (!input.diagnostic) return null;
147914
+ const verb = input.isHang ? "stalled" : "failed";
147915
+ const cause = input.diagnostic.lastProviderError ? ` \u2014 likely cause: \`${input.diagnostic.lastProviderError}\`` : "";
147916
+ const headline = `**${input.diagnostic.label} ${verb}**${cause}`;
147917
+ const explanation = formatExplanation({
147918
+ isHang: input.isHang,
147919
+ errorMessage: input.errorMessage
147920
+ });
147921
+ const parts = [headline, "", `${explanation} ${formatEventsPart(input.diagnostic)}`];
147922
+ const tail = renderStderrTail(input.diagnostic.recentStderr);
147923
+ if (tail) {
147924
+ const fence = pickFence(tail);
147925
+ parts.push(
147926
+ "",
147927
+ "<details><summary>Recent agent stderr</summary>",
147928
+ "",
147929
+ fence,
147930
+ tail,
147931
+ fence,
147932
+ "",
147933
+ "</details>"
147934
+ );
147935
+ }
147936
+ return parts.join("\n");
147937
+ }
147938
+ function formatExplanation(input) {
147939
+ if (!input.isHang) return `The agent exited unexpectedly: ${input.errorMessage}`;
147940
+ const idleSec = parseIdleSec(input.errorMessage);
147941
+ if (idleSec === void 0) {
147942
+ return "The agent stopped emitting events and was killed by the activity-timeout watchdog.";
147943
+ }
147944
+ return `The agent stopped emitting events for ${idleSec}s and was killed by the activity-timeout watchdog.`;
147945
+ }
147946
+ function parseIdleSec(message) {
147947
+ const match3 = /no output for (\d+)s/.exec(message);
147948
+ return match3 ? Number(match3[1]) : void 0;
147949
+ }
147950
+ function formatEventsPart(diagnostic) {
147951
+ if (diagnostic.eventCount > 0) {
147952
+ return `${diagnostic.eventCount} events were processed before the failure.`;
147953
+ }
147954
+ if (diagnostic.lastProviderError) return "No events were emitted before the failure.";
147955
+ return "No events were emitted \u2014 check whether the model provider is reachable.";
147956
+ }
147957
+ function renderStderrTail(lines) {
147958
+ if (lines.length === 0) return "";
147959
+ const joined = lines.join("\n");
147960
+ if (joined.length <= MAX_STDERR_BYTES) return joined;
147961
+ return `... (older lines truncated)
147962
+ ${joined.slice(-MAX_STDERR_BYTES)}`;
147963
+ }
147964
+ function pickFence(content) {
147965
+ let max = 0;
147966
+ for (const match3 of content.matchAll(/`+/g)) {
147967
+ if (match3[0].length > max) max = match3[0].length;
147968
+ }
147969
+ return "`".repeat(Math.max(3, max + 1));
147970
+ }
147971
+
147836
147972
  // agents/opencodePlugin.ts
147837
147973
  var PULLFROG_BUS_EVENT_TYPE = "pullfrog_bus_event";
147838
147974
  var PULLFROG_OPENCODE_PLUGIN_FILENAME = "pullfrog-events.ts";
@@ -148225,8 +148361,7 @@ async function runOpenCode(params) {
148225
148361
  log.debug(withLabel(label, ` output: ${event.part.state.output}`));
148226
148362
  }
148227
148363
  if (event.part?.state?.status === "error") {
148228
- const errorMsg = event.part.state.output ?? "(no error message)";
148229
- log.info(withLabel(label, `\xBB tool call failed: ${errorMsg}`));
148364
+ log.info(withLabel(label, `\xBB tool call failed: ${event.part.state.error}`));
148230
148365
  }
148231
148366
  if (toolName.includes("report_progress") && params.todoTracker) {
148232
148367
  log.debug("\xBB report_progress detected, disabling todo tracking");
@@ -148238,19 +148373,20 @@ async function runOpenCode(params) {
148238
148373
  },
148239
148374
  tool_result: (event) => {
148240
148375
  const toolId = event.part?.callID || event.tool_id;
148241
- const status = event.part?.state?.status || event.status || "unknown";
148242
- const output2 = event.part?.state?.output || event.output;
148376
+ const state = event.part?.state;
148377
+ const status = state?.status ?? event.status ?? "unknown";
148378
+ const payload = state?.status === "completed" ? state.output : state?.status === "error" ? state.error : event.output;
148243
148379
  const label = eventLabel(event);
148244
148380
  timerFor(label).markToolResult();
148245
148381
  if (taskDispatchByCallID.size > 0 || pendingTaskDispatches.length > 0) {
148246
148382
  if (toolId && taskDispatchByCallID.has(toolId)) {
148247
148383
  const dispatch = taskDispatchByCallID.get(toolId);
148248
- if (dispatch) emitSubagentFinished(dispatch, status, output2, "exact");
148384
+ if (dispatch) emitSubagentFinished(dispatch, status, payload, "exact");
148249
148385
  } else {
148250
148386
  const callIDIsKnownNonTask = toolId ? knownNonTaskCallIDs.has(toolId) : false;
148251
148387
  if (!callIDIsKnownNonTask && pendingTaskDispatches.length > 0) {
148252
148388
  const dispatch = pendingTaskDispatches[0];
148253
- emitSubagentFinished(dispatch, status, output2, "fifo");
148389
+ emitSubagentFinished(dispatch, status, payload, "fifo");
148254
148390
  }
148255
148391
  }
148256
148392
  }
@@ -148266,13 +148402,8 @@ async function runOpenCode(params) {
148266
148402
  `\xBB ${params.label} tool_result${stepContext}: id=${toolId}, status=${status}, duration=${Math.round(toolDuration)}ms`
148267
148403
  )
148268
148404
  );
148269
- if (output2) {
148270
- log.debug(
148271
- withLabel(
148272
- label,
148273
- ` output: ${typeof output2 === "string" ? output2 : JSON.stringify(output2)}`
148274
- )
148275
- );
148405
+ if (payload) {
148406
+ log.debug(withLabel(label, ` output: ${payload}`));
148276
148407
  }
148277
148408
  if (toolDuration > 5e3) {
148278
148409
  log.info(
@@ -148285,11 +148416,9 @@ async function runOpenCode(params) {
148285
148416
  }
148286
148417
  }
148287
148418
  if (status === "error") {
148288
- const errorMsg = typeof output2 === "string" ? output2 : JSON.stringify(output2);
148289
- log.info(withLabel(label, `\xBB tool call failed: ${errorMsg}`));
148290
- } else if (output2) {
148291
- const outputStr = typeof output2 === "string" ? output2 : JSON.stringify(output2);
148292
- log.debug(withLabel(label, `tool output: ${outputStr}`));
148419
+ log.info(withLabel(label, `\xBB tool call failed: ${payload ?? "(no error message)"}`));
148420
+ } else if (payload) {
148421
+ log.debug(withLabel(label, `tool output: ${payload}`));
148293
148422
  }
148294
148423
  },
148295
148424
  error: (event) => {
@@ -148366,6 +148495,13 @@ async function runOpenCode(params) {
148366
148495
  const recentStderr = [];
148367
148496
  let lastProviderError = null;
148368
148497
  let agentErrorEvent = null;
148498
+ const diagnostic = {
148499
+ label: params.label,
148500
+ recentStderr,
148501
+ lastProviderError: void 0,
148502
+ eventCount: 0
148503
+ };
148504
+ params.toolState.agentDiagnostic = diagnostic;
148369
148505
  const output = new TailBuffer(DEFAULT_MAX_RETAINED_BYTES);
148370
148506
  let stdoutBuffer = "";
148371
148507
  try {
@@ -148414,6 +148550,7 @@ async function runOpenCode(params) {
148414
148550
  continue;
148415
148551
  }
148416
148552
  eventCount++;
148553
+ diagnostic.eventCount = eventCount;
148417
148554
  log.debug(JSON.stringify(event, null, 2));
148418
148555
  const timeSinceLastActivity = getIdleMs();
148419
148556
  if (timeSinceLastActivity > 1e4) {
@@ -148445,10 +148582,11 @@ async function runOpenCode(params) {
148445
148582
  if (!trimmed) return;
148446
148583
  recentStderr.push(trimmed);
148447
148584
  if (recentStderr.length > MAX_STDERR_LINES) recentStderr.shift();
148448
- const providerError = detectProviderError(trimmed);
148449
- if (providerError) {
148450
- lastProviderError = providerError;
148451
- log.info(`\xBB provider error detected (${providerError}): ${trimmed.substring(0, 500)}`);
148585
+ const match3 = findProviderErrorMatch(trimmed);
148586
+ if (match3) {
148587
+ lastProviderError = match3.label;
148588
+ diagnostic.lastProviderError = match3.label;
148589
+ log.info(`\xBB provider error detected (${match3.label}): ${match3.excerpt}`);
148452
148590
  } else {
148453
148591
  log.debug(trimmed);
148454
148592
  }
@@ -148538,10 +148676,11 @@ ${stderrContext}`);
148538
148676
  `\xBB recent stderr (last ${Math.min(recentStderr.length, 10)} lines):
148539
148677
  ${stderrContext}`
148540
148678
  );
148679
+ const body = formatAgentHangBody({ diagnostic, isHang: isActivityTimeout, errorMessage });
148541
148680
  return {
148542
148681
  success: false,
148543
148682
  output: finalOutput || output.toString(),
148544
- error: `${errorMessage} [${diagnosis}]`,
148683
+ error: body ?? `${errorMessage} [${diagnosis}]`,
148545
148684
  usage: buildUsage()
148546
148685
  };
148547
148686
  }
@@ -148593,6 +148732,7 @@ var opencode = agent({
148593
148732
  cliPath,
148594
148733
  cwd: repoDir,
148595
148734
  env: env2,
148735
+ toolState: ctx.toolState,
148596
148736
  todoTracker: ctx.todoTracker,
148597
148737
  onActivityTimeout: ctx.onActivityTimeout,
148598
148738
  onToolUse: ctx.onToolUse
@@ -154764,24 +154904,29 @@ ${instructions.user}` : null,
154764
154904
  killTrackedChildren();
154765
154905
  log.error(errorMessage);
154766
154906
  const billingError = isRouterKeylimitExhaustedError(errorMessage) ? new BillingError(errorMessage, { code: "router_keylimit_exhausted" }) : null;
154767
- const apiKeyErrorSummary = !billingError && isApiKeyAuthError(errorMessage) ? formatApiKeyErrorSummary({
154907
+ const isHang = errorMessage.startsWith("activity timeout") || errorMessage.startsWith("agent still pending");
154908
+ const hangBody = isHang ? formatAgentHangBody({ diagnostic: toolState.agentDiagnostic, isHang: true, errorMessage }) : null;
154909
+ const apiKeySource = hangBody ?? errorMessage;
154910
+ const apiKeyErrorSummary = !billingError && isApiKeyAuthError(apiKeySource) ? formatApiKeyErrorSummary({
154768
154911
  owner: runContext.repo.owner,
154769
154912
  name: runContext.repo.name,
154770
- raw: errorMessage
154913
+ raw: apiKeySource
154771
154914
  }) : null;
154772
154915
  try {
154773
- const errorSummary = billingError ? formatBillingErrorSummary(billingError, runContext.repo.owner) : apiKeyErrorSummary ?? `### \u274C Pullfrog failed
154916
+ const errorSummary = billingError ? formatBillingErrorSummary(billingError, runContext.repo.owner) : apiKeyErrorSummary ?? (hangBody ? `### \u274C Pullfrog failed
154917
+
154918
+ ${hangBody}` : `### \u274C Pullfrog failed
154774
154919
 
154775
154920
  \`\`\`
154776
154921
  ${errorMessage}
154777
- \`\`\``;
154922
+ \`\`\``);
154778
154923
  const usageSummary = formatUsageSummary(toolState.usageEntries);
154779
154924
  const parts = [errorSummary, toolState.lastProgressBody, usageSummary].filter(Boolean);
154780
154925
  await writeSummary(parts.join("\n\n"));
154781
154926
  } catch {
154782
154927
  }
154783
154928
  try {
154784
- const commentBody = billingError ? formatBillingErrorSummary(billingError, runContext.repo.owner) : apiKeyErrorSummary ?? errorMessage;
154929
+ const commentBody = billingError ? formatBillingErrorSummary(billingError, runContext.repo.owner) : apiKeyErrorSummary ?? hangBody ?? errorMessage;
154785
154930
  await reportErrorToComment({ toolState, error: commentBody });
154786
154931
  } catch {
154787
154932
  }