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/cli.mjs CHANGED
@@ -142697,7 +142697,7 @@ var import_semver = __toESM(require_semver2(), 1);
142697
142697
  // package.json
142698
142698
  var package_default = {
142699
142699
  name: "pullfrog",
142700
- version: "0.1.7",
142700
+ version: "0.1.8",
142701
142701
  type: "module",
142702
142702
  bin: {
142703
142703
  pullfrog: "dist/cli.mjs",
@@ -143165,6 +143165,51 @@ function readNumber(params) {
143165
143165
  import { execSync } from "node:child_process";
143166
143166
  import { createHash } from "node:crypto";
143167
143167
  import { readFileSync as readFileSync2, realpathSync, unlinkSync } from "node:fs";
143168
+
143169
+ // utils/shell.ts
143170
+ import { spawnSync as spawnSync2 } from "node:child_process";
143171
+ function $(cmd, args2, options) {
143172
+ const encoding = options?.encoding ?? "utf-8";
143173
+ const env2 = resolveEnv(options?.env);
143174
+ const result = spawnSync2(cmd, args2, {
143175
+ stdio: ["ignore", "pipe", "pipe"],
143176
+ encoding,
143177
+ cwd: options?.cwd,
143178
+ env: env2
143179
+ });
143180
+ const stdout = result.stdout ?? "";
143181
+ const stderr = result.stderr ?? "";
143182
+ if (options?.log !== false) {
143183
+ const canWriteToStdout = process.stdout.isTTY === true;
143184
+ if (stdout) {
143185
+ if (canWriteToStdout) {
143186
+ process.stdout.write(stdout);
143187
+ } else {
143188
+ process.stderr.write(stdout);
143189
+ }
143190
+ }
143191
+ if (stderr) {
143192
+ process.stderr.write(stderr);
143193
+ }
143194
+ }
143195
+ if (result.status !== 0) {
143196
+ const errorResult = {
143197
+ status: result.status ?? -1,
143198
+ stdout,
143199
+ stderr
143200
+ };
143201
+ if (options?.onError) {
143202
+ options.onError(errorResult);
143203
+ return stdout.trim();
143204
+ }
143205
+ throw new Error(
143206
+ `Command failed with exit code ${errorResult.status}: ${stderr || "Unknown error"}`
143207
+ );
143208
+ }
143209
+ return stdout.trim();
143210
+ }
143211
+
143212
+ // utils/gitAuth.ts
143168
143213
  var gitBinary;
143169
143214
  function hashFile(path3) {
143170
143215
  return createHash("sha256").update(readFileSync2(path3)).digest("hex");
@@ -143256,6 +143301,27 @@ ${stdout}` : stderr || stdout || "(no output)";
143256
143301
  }
143257
143302
  }
143258
143303
  }
143304
+ var SHALLOW_UNREACHABLE_PATTERNS = [
143305
+ /Could not read [a-f0-9]{40,64}/,
143306
+ /remote did not send all necessary objects/
143307
+ ];
143308
+ var DEEPEN_RETRY_DEPTH = 1e3;
143309
+ async function $gitFetchWithDeepen(args2, options, label) {
143310
+ try {
143311
+ return await $git("fetch", args2, options);
143312
+ } catch (err) {
143313
+ const msg = err instanceof Error ? err.message : String(err);
143314
+ const isShallowUnreachable = SHALLOW_UNREACHABLE_PATTERNS.some((p2) => p2.test(msg));
143315
+ if (!isShallowUnreachable) throw err;
143316
+ const isShallow = $("git", ["rev-parse", "--is-shallow-repository"], { log: false }).trim() === "true";
143317
+ if (!isShallow) throw err;
143318
+ log.info(
143319
+ `\xBB ${label ?? "git fetch"} hit shallow-unreachable error, retrying with --deepen=${DEEPEN_RETRY_DEPTH}`
143320
+ );
143321
+ const retryArgs = args2.filter((a) => !a.startsWith("--depth="));
143322
+ return await $git("fetch", [`--deepen=${DEEPEN_RETRY_DEPTH}`, ...retryArgs], options);
143323
+ }
143324
+ }
143259
143325
 
143260
143326
  // lifecycle.ts
143261
143327
  var LIFECYCLE_HOOK_TIMEOUT_MS = 6e5;
@@ -143297,49 +143363,6 @@ async function executeLifecycleHook(params) {
143297
143363
  }
143298
143364
  }
143299
143365
 
143300
- // utils/shell.ts
143301
- import { spawnSync as spawnSync2 } from "node:child_process";
143302
- function $(cmd, args2, options) {
143303
- const encoding = options?.encoding ?? "utf-8";
143304
- const env2 = resolveEnv(options?.env);
143305
- const result = spawnSync2(cmd, args2, {
143306
- stdio: ["ignore", "pipe", "pipe"],
143307
- encoding,
143308
- cwd: options?.cwd,
143309
- env: env2
143310
- });
143311
- const stdout = result.stdout ?? "";
143312
- const stderr = result.stderr ?? "";
143313
- if (options?.log !== false) {
143314
- const canWriteToStdout = process.stdout.isTTY === true;
143315
- if (stdout) {
143316
- if (canWriteToStdout) {
143317
- process.stdout.write(stdout);
143318
- } else {
143319
- process.stderr.write(stdout);
143320
- }
143321
- }
143322
- if (stderr) {
143323
- process.stderr.write(stderr);
143324
- }
143325
- }
143326
- if (result.status !== 0) {
143327
- const errorResult = {
143328
- status: result.status ?? -1,
143329
- stdout,
143330
- stderr
143331
- };
143332
- if (options?.onError) {
143333
- options.onError(errorResult);
143334
- return stdout.trim();
143335
- }
143336
- throw new Error(
143337
- `Command failed with exit code ${errorResult.status}: ${stderr || "Unknown error"}`
143338
- );
143339
- }
143340
- return stdout.trim();
143341
- }
143342
-
143343
143366
  // utils/rangeDiff.ts
143344
143367
  function computeIncrementalDiff(params) {
143345
143368
  try {
@@ -143734,11 +143757,6 @@ var GitFetch = type({
143734
143757
  ref: type.string.describe("Ref to fetch: branch name, tag, or 'pull/N/head' for PRs"),
143735
143758
  depth: type.number.describe("Fetch depth (for shallow clones)").optional()
143736
143759
  });
143737
- var SHALLOW_UNREACHABLE_PATTERNS = [
143738
- /Could not read [a-f0-9]{40,64}/,
143739
- /remote did not send all necessary objects/
143740
- ];
143741
- var DEEPEN_RETRY_DEPTH = 1e3;
143742
143760
  function GitFetchTool(ctx) {
143743
143761
  return tool({
143744
143762
  name: "git_fetch",
@@ -143750,20 +143768,7 @@ function GitFetchTool(ctx) {
143750
143768
  if (params.depth !== void 0) {
143751
143769
  fetchArgs.push(`--depth=${params.depth}`);
143752
143770
  }
143753
- try {
143754
- await $git("fetch", fetchArgs, { token: ctx.gitToken });
143755
- } catch (err) {
143756
- const msg = err instanceof Error ? err.message : String(err);
143757
- const isShallowUnreachable = SHALLOW_UNREACHABLE_PATTERNS.some((p2) => p2.test(msg));
143758
- const isShallow = isShallowUnreachable && $("git", ["rev-parse", "--is-shallow-repository"], { log: false }).trim() === "true";
143759
- if (!isShallow) throw err;
143760
- log.info(
143761
- `\xBB git_fetch hit shallow-unreachable error, retrying with --deepen=${DEEPEN_RETRY_DEPTH}`
143762
- );
143763
- await $git("fetch", [`--deepen=${DEEPEN_RETRY_DEPTH}`, "--no-tags", "origin", params.ref], {
143764
- token: ctx.gitToken
143765
- });
143766
- }
143771
+ await $gitFetchWithDeepen(fetchArgs, { token: ctx.gitToken }, "git_fetch");
143767
143772
  return { success: true, ref: params.ref };
143768
143773
  })
143769
143774
  });
@@ -144487,10 +144492,10 @@ async function ensureBeforeShaReachable(params) {
144487
144492
  sha: params.sha,
144488
144493
  ref: tempBranch
144489
144494
  }), true);
144490
- await $git(
144491
- "fetch",
144495
+ await $gitFetchWithDeepen(
144492
144496
  ["--no-tags", ...params.isShallow ? ["--depth=1"] : [], "origin", tempBranch],
144493
- { token: params.gitToken }
144497
+ { token: params.gitToken },
144498
+ `before_sha temp branch ${tempBranch}`
144494
144499
  );
144495
144500
  log.debug(`\xBB fetched before_sha via temp branch ${tempBranch}`);
144496
144501
  return true;
@@ -144566,16 +144571,22 @@ async function checkoutPrBranch(pr, params) {
144566
144571
  toolState.checkoutSha = $("git", ["rev-parse", "HEAD"], { log: false }).trim();
144567
144572
  const alreadyOnBranch = toolState.checkoutSha === pr.headSha;
144568
144573
  log.debug(`\xBB fetching base branch (${pr.baseRef})...`);
144569
- await $git("fetch", ["--no-tags", "origin", pr.baseRef], { token: gitToken });
144574
+ await $gitFetchWithDeepen(
144575
+ ["--no-tags", "origin", pr.baseRef],
144576
+ { token: gitToken },
144577
+ `base branch ${pr.baseRef}`
144578
+ );
144570
144579
  if (!alreadyOnBranch) {
144571
144580
  $("git", ["checkout", "-B", pr.baseRef, `origin/${pr.baseRef}`], { log: false });
144572
144581
  log.debug(`\xBB fetching PR #${pr.number} (${localBranch})...`);
144573
144582
  await retry(
144574
144583
  async () => {
144575
144584
  try {
144576
- await $git("fetch", ["--no-tags", "origin", `+pull/${pr.number}/head:${localBranch}`], {
144577
- token: gitToken
144578
- });
144585
+ await $gitFetchWithDeepen(
144586
+ ["--no-tags", "origin", `+pull/${pr.number}/head:${localBranch}`],
144587
+ { token: gitToken },
144588
+ `PR #${pr.number}`
144589
+ );
144579
144590
  } catch (e) {
144580
144591
  const msg = e instanceof Error ? e.message : String(e);
144581
144592
  if (PULL_REF_MISSING_PATTERN.test(msg)) {
@@ -144676,134 +144687,159 @@ async function checkoutPrBranch(pr, params) {
144676
144687
  });
144677
144688
  return { hookWarning: postCheckoutHook.warning };
144678
144689
  }
144690
+ var inFlightCheckouts = /* @__PURE__ */ new Map();
144679
144691
  function CheckoutPrTool(ctx) {
144692
+ const runCheckout = async (pull_number) => {
144693
+ const prResponse = await ctx.octokit.rest.pulls.get({
144694
+ owner: ctx.repo.owner,
144695
+ repo: ctx.repo.name,
144696
+ pull_number
144697
+ });
144698
+ const headRepo = prResponse.data.head.repo;
144699
+ if (!headRepo) {
144700
+ throw new Error(`PR #${pull_number} source repository was deleted`);
144701
+ }
144702
+ const pr = {
144703
+ number: pull_number,
144704
+ headSha: prResponse.data.head.sha,
144705
+ headRef: prResponse.data.head.ref,
144706
+ headRepoFullName: headRepo.full_name,
144707
+ baseRef: prResponse.data.base.ref,
144708
+ baseRepoFullName: prResponse.data.base.repo.full_name,
144709
+ maintainerCanModify: prResponse.data.maintainer_can_modify
144710
+ };
144711
+ const checkoutResult = await checkoutPrBranch(pr, {
144712
+ octokit: ctx.octokit,
144713
+ owner: ctx.repo.owner,
144714
+ name: ctx.repo.name,
144715
+ gitToken: ctx.gitToken,
144716
+ toolState: ctx.toolState,
144717
+ shell: ctx.payload.shell,
144718
+ postCheckoutScript: ctx.postCheckoutScript,
144719
+ beforeSha: ctx.toolState.beforeSha
144720
+ });
144721
+ const tempDir = process.env.PULLFROG_TEMP_DIR;
144722
+ if (!tempDir) {
144723
+ throw new Error(
144724
+ "PULLFROG_TEMP_DIR not set - checkout_pr must run in pullfrog action context"
144725
+ );
144726
+ }
144727
+ const headShort = ctx.toolState.checkoutSha.slice(0, 7);
144728
+ let incrementalDiffPath;
144729
+ if (ctx.toolState.beforeSha && ctx.toolState.checkoutSha) {
144730
+ const beforeShort = ctx.toolState.beforeSha.slice(0, 7);
144731
+ const incremental = computeIncrementalDiff({
144732
+ baseBranch: pr.baseRef,
144733
+ beforeSha: ctx.toolState.beforeSha,
144734
+ headSha: ctx.toolState.checkoutSha
144735
+ });
144736
+ if (incremental) {
144737
+ incrementalDiffPath = join3(
144738
+ tempDir,
144739
+ `pr-${pull_number}-${beforeShort}-${headShort}-incremental.diff`
144740
+ );
144741
+ writeFileSync(incrementalDiffPath, incremental);
144742
+ log.info(
144743
+ `\xBB incremental diff computed (${incremental.length} bytes) \u2192 ${incrementalDiffPath}`
144744
+ );
144745
+ }
144746
+ }
144747
+ const formatResult = await fetchAndFormatPrDiff(ctx, pull_number);
144748
+ const diffPreview = formatResult.content.split("\n").slice(0, 100).join("\n");
144749
+ log.debug(`formatted diff preview (first 100 lines):
144750
+ ${diffPreview}`);
144751
+ const diffPath = join3(tempDir, `pr-${pull_number}-${headShort}.diff`);
144752
+ writeFileSync(diffPath, formatResult.content);
144753
+ log.debug(`wrote diff to ${diffPath} (${formatResult.content.length} bytes)`);
144754
+ ctx.toolState.diffCoverage = createDiffCoverageState({
144755
+ diffPath,
144756
+ totalLines: countLines({ content: formatResult.content }),
144757
+ toc: formatResult.toc,
144758
+ previous: ctx.toolState.diffCoverage
144759
+ });
144760
+ log.debug(
144761
+ `\xBB diff coverage initialized: diffPath=${diffPath}, totalLines=${ctx.toolState.diffCoverage.totalLines}, tocEntries=${ctx.toolState.diffCoverage.tocEntries.length}`
144762
+ );
144763
+ const cached4 = /* @__PURE__ */ new Map();
144764
+ for (const file2 of formatResult.files) {
144765
+ cached4.set(file2.filename, commentableLinesForFile(file2.patch));
144766
+ }
144767
+ ctx.toolState.commentableLinesByFile = cached4;
144768
+ ctx.toolState.commentableLinesPullNumber = pull_number;
144769
+ ctx.toolState.commentableLinesCheckoutSha = ctx.toolState.checkoutSha;
144770
+ 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.` : "";
144771
+ const COMMIT_LOG_MAX = 200;
144772
+ const baseRange = `origin/${pr.baseRef}..HEAD`;
144773
+ let commitCount = 0;
144774
+ let commitLog = "";
144775
+ let commitLogUnavailable = false;
144776
+ try {
144777
+ commitCount = parseInt(
144778
+ $("git", ["rev-list", "--count", baseRange], { log: false }).trim() || "0",
144779
+ 10
144780
+ );
144781
+ commitLog = $("git", ["log", "--oneline", `--max-count=${COMMIT_LOG_MAX}`, baseRange], {
144782
+ log: false
144783
+ });
144784
+ } catch (err) {
144785
+ commitLogUnavailable = true;
144786
+ log.debug(
144787
+ `\xBB unable to compute commit metadata for ${baseRange}: ${err instanceof Error ? err.message : String(err)}`
144788
+ );
144789
+ }
144790
+ const commitLogTruncated = commitCount > COMMIT_LOG_MAX;
144791
+ 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.` : "";
144792
+ 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.` : "";
144793
+ return {
144794
+ success: true,
144795
+ number: prResponse.data.number,
144796
+ title: prResponse.data.title,
144797
+ body: prResponse.data.body,
144798
+ base: pr.baseRef,
144799
+ localBranch: `pr-${pull_number}`,
144800
+ remoteBranch: `refs/heads/${pr.headRef}`,
144801
+ isFork: pr.headRepoFullName !== pr.baseRepoFullName,
144802
+ maintainerCanModify: pr.maintainerCanModify,
144803
+ url: prResponse.data.html_url,
144804
+ headRepo: pr.headRepoFullName,
144805
+ diffPath,
144806
+ incrementalDiffPath,
144807
+ toc: formatResult.toc,
144808
+ commitCount,
144809
+ commitLog,
144810
+ commitLogTruncated,
144811
+ commitLogUnavailable,
144812
+ hookWarning: checkoutResult.hookWarning,
144813
+ 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
144814
+ };
144815
+ };
144680
144816
  return tool({
144681
144817
  name: "checkout_pr",
144682
144818
  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.",
144683
144819
  parameters: CheckoutPr,
144684
144820
  execute: execute(async ({ pull_number }) => {
144685
- const prResponse = await ctx.octokit.rest.pulls.get({
144686
- owner: ctx.repo.owner,
144687
- repo: ctx.repo.name,
144688
- pull_number
144689
- });
144690
- const headRepo = prResponse.data.head.repo;
144691
- if (!headRepo) {
144692
- throw new Error(`PR #${pull_number} source repository was deleted`);
144693
- }
144694
- const pr = {
144695
- number: pull_number,
144696
- headSha: prResponse.data.head.sha,
144697
- headRef: prResponse.data.head.ref,
144698
- headRepoFullName: headRepo.full_name,
144699
- baseRef: prResponse.data.base.ref,
144700
- baseRepoFullName: prResponse.data.base.repo.full_name,
144701
- maintainerCanModify: prResponse.data.maintainer_can_modify
144702
- };
144703
- const checkoutResult = await checkoutPrBranch(pr, {
144704
- octokit: ctx.octokit,
144705
- owner: ctx.repo.owner,
144706
- name: ctx.repo.name,
144707
- gitToken: ctx.gitToken,
144708
- toolState: ctx.toolState,
144709
- shell: ctx.payload.shell,
144710
- postCheckoutScript: ctx.postCheckoutScript,
144711
- beforeSha: ctx.toolState.beforeSha
144712
- });
144713
- const tempDir = process.env.PULLFROG_TEMP_DIR;
144714
- if (!tempDir) {
144715
- throw new Error(
144716
- "PULLFROG_TEMP_DIR not set - checkout_pr must run in pullfrog action context"
144717
- );
144718
- }
144719
- const headShort = ctx.toolState.checkoutSha.slice(0, 7);
144720
- let incrementalDiffPath;
144721
- if (ctx.toolState.beforeSha && ctx.toolState.checkoutSha) {
144722
- const beforeShort = ctx.toolState.beforeSha.slice(0, 7);
144723
- const incremental = computeIncrementalDiff({
144724
- baseBranch: pr.baseRef,
144725
- beforeSha: ctx.toolState.beforeSha,
144726
- headSha: ctx.toolState.checkoutSha
144727
- });
144728
- if (incremental) {
144729
- incrementalDiffPath = join3(
144730
- tempDir,
144731
- `pr-${pull_number}-${beforeShort}-${headShort}-incremental.diff`
144732
- );
144733
- writeFileSync(incrementalDiffPath, incremental);
144734
- log.info(
144735
- `\xBB incremental diff computed (${incremental.length} bytes) \u2192 ${incrementalDiffPath}`
144821
+ const inFlight = inFlightCheckouts.get(pull_number);
144822
+ if (inFlight) {
144823
+ log.info(`\xBB checkout_pr({pull_number:${pull_number}}) already in flight \u2014 sharing result`);
144824
+ return inFlight;
144825
+ }
144826
+ const current = ctx.toolState.issueNumber;
144827
+ if (current !== void 0 && current !== pull_number) {
144828
+ const dirty = $("git", ["status", "--porcelain"], { log: false }).trim();
144829
+ if (dirty) {
144830
+ throw new Error(
144831
+ `cannot checkout PR #${pull_number} while the working tree has uncommitted changes. commit, push, or discard them before switching. dirty paths:
144832
+ ${dirty}`
144736
144833
  );
144737
144834
  }
144738
144835
  }
144739
- const formatResult = await fetchAndFormatPrDiff(ctx, pull_number);
144740
- const diffPreview = formatResult.content.split("\n").slice(0, 100).join("\n");
144741
- log.debug(`formatted diff preview (first 100 lines):
144742
- ${diffPreview}`);
144743
- const diffPath = join3(tempDir, `pr-${pull_number}-${headShort}.diff`);
144744
- writeFileSync(diffPath, formatResult.content);
144745
- log.debug(`wrote diff to ${diffPath} (${formatResult.content.length} bytes)`);
144746
- ctx.toolState.diffCoverage = createDiffCoverageState({
144747
- diffPath,
144748
- totalLines: countLines({ content: formatResult.content }),
144749
- toc: formatResult.toc,
144750
- previous: ctx.toolState.diffCoverage
144751
- });
144752
- log.debug(
144753
- `\xBB diff coverage initialized: diffPath=${diffPath}, totalLines=${ctx.toolState.diffCoverage.totalLines}, tocEntries=${ctx.toolState.diffCoverage.tocEntries.length}`
144754
- );
144755
- const cached4 = /* @__PURE__ */ new Map();
144756
- for (const file2 of formatResult.files) {
144757
- cached4.set(file2.filename, commentableLinesForFile(file2.patch));
144758
- }
144759
- ctx.toolState.commentableLinesByFile = cached4;
144760
- ctx.toolState.commentableLinesPullNumber = pull_number;
144761
- ctx.toolState.commentableLinesCheckoutSha = ctx.toolState.checkoutSha;
144762
- 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.` : "";
144763
- const COMMIT_LOG_MAX = 200;
144764
- const baseRange = `origin/${pr.baseRef}..HEAD`;
144765
- let commitCount = 0;
144766
- let commitLog = "";
144767
- let commitLogUnavailable = false;
144836
+ const promise2 = runCheckout(pull_number);
144837
+ inFlightCheckouts.set(pull_number, promise2);
144768
144838
  try {
144769
- commitCount = parseInt(
144770
- $("git", ["rev-list", "--count", baseRange], { log: false }).trim() || "0",
144771
- 10
144772
- );
144773
- commitLog = $("git", ["log", "--oneline", `--max-count=${COMMIT_LOG_MAX}`, baseRange], {
144774
- log: false
144775
- });
144776
- } catch (err) {
144777
- commitLogUnavailable = true;
144778
- log.debug(
144779
- `\xBB unable to compute commit metadata for ${baseRange}: ${err instanceof Error ? err.message : String(err)}`
144780
- );
144839
+ return await promise2;
144840
+ } finally {
144841
+ inFlightCheckouts.delete(pull_number);
144781
144842
  }
144782
- const commitLogTruncated = commitCount > COMMIT_LOG_MAX;
144783
- 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.` : "";
144784
- 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.` : "";
144785
- return {
144786
- success: true,
144787
- number: prResponse.data.number,
144788
- title: prResponse.data.title,
144789
- body: prResponse.data.body,
144790
- base: pr.baseRef,
144791
- localBranch: `pr-${pull_number}`,
144792
- remoteBranch: `refs/heads/${pr.headRef}`,
144793
- isFork: pr.headRepoFullName !== pr.baseRepoFullName,
144794
- maintainerCanModify: pr.maintainerCanModify,
144795
- url: prResponse.data.html_url,
144796
- headRepo: pr.headRepoFullName,
144797
- diffPath,
144798
- incrementalDiffPath,
144799
- toc: formatResult.toc,
144800
- commitCount,
144801
- commitLog,
144802
- commitLogTruncated,
144803
- commitLogUnavailable,
144804
- hookWarning: checkoutResult.hookWarning,
144805
- 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
144806
- };
144807
144843
  })
144808
144844
  });
144809
144845
  }
@@ -146199,6 +146235,15 @@ function getTempDir() {
146199
146235
  }
146200
146236
  return tempDir;
146201
146237
  }
146238
+ var MAX_OUTPUT_CHARS = 5e3;
146239
+ function capOutput(output) {
146240
+ if (output.length <= MAX_OUTPUT_CHARS) return output;
146241
+ const fullPath = join7(getTempDir(), `shell-${randomUUID2().slice(0, 8)}.log`);
146242
+ writeFileSync5(fullPath, output);
146243
+ const elided = output.length - MAX_OUTPUT_CHARS;
146244
+ return `... [${elided} chars truncated; full output saved to ${fullPath}] ...
146245
+ ${output.slice(-MAX_OUTPUT_CHARS)}`;
146246
+ }
146202
146247
  function isGitCommand(command) {
146203
146248
  const trimmed = command.trim();
146204
146249
  if (trimmed === "git" || trimmed.startsWith("git ")) return true;
@@ -146217,6 +146262,8 @@ Use this tool to:
146217
146262
  - Execute build tools (npm, pnpm, cargo, make, etc.)
146218
146263
  - Run tests and linters
146219
146264
 
146265
+ 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.
146266
+
146220
146267
  Do NOT use this tool for git commands \u2014 use the dedicated git tools instead.`,
146221
146268
  parameters: ShellParams,
146222
146269
  execute: execute(async (params) => {
@@ -146307,12 +146354,13 @@ ${stderr}` : stderr : stdout;
146307
146354
  output = output ? `${output}
146308
146355
  [timed out after ${timeout}ms]` : `[timed out after ${timeout}ms]`;
146309
146356
  const finalExitCode = exitCode ?? (timedOut ? 124 : -1);
146357
+ const trimmed = output.trim();
146310
146358
  if (finalExitCode !== 0) {
146311
146359
  log.info(`shell command failed with exit code ${finalExitCode}: ${params.command}`);
146312
- if (output) log.info(`output: ${output.trim()}`);
146360
+ if (trimmed) log.info(`output: ${trimmed}`);
146313
146361
  }
146314
146362
  return {
146315
- output: output.trim(),
146363
+ output: capOutput(trimmed),
146316
146364
  exit_code: finalExitCode,
146317
146365
  timed_out: timedOut
146318
146366
  };
@@ -147185,12 +147233,38 @@ var PROVIDER_ERROR_PATTERNS = [
147185
147233
  // around `limit` rejects keys like `time_limit` or `field_limit`.
147186
147234
  { regex: /["']?\blimit\b["']?\s*:\s*0\b/, label: "zero quota" }
147187
147235
  ];
147188
- function detectProviderError(text) {
147236
+ var EXCERPT_MAX_BYTES = 600;
147237
+ var LINES_BEFORE = 1;
147238
+ var LINES_AFTER = 2;
147239
+ function findProviderErrorMatch(text) {
147189
147240
  for (const entry of PROVIDER_ERROR_PATTERNS) {
147190
- if (entry.regex.test(text)) return entry.label;
147241
+ const m = entry.regex.exec(text);
147242
+ if (!m) continue;
147243
+ return { label: entry.label, excerpt: extractExcerpt(text, m.index) };
147191
147244
  }
147192
147245
  return null;
147193
147246
  }
147247
+ function extractExcerpt(text, matchIndex) {
147248
+ const lineStart = text.lastIndexOf("\n", matchIndex - 1) + 1;
147249
+ const lineEndRaw = text.indexOf("\n", matchIndex);
147250
+ const lineEnd = lineEndRaw === -1 ? text.length : lineEndRaw;
147251
+ let start = lineStart;
147252
+ for (let i = 0; i < LINES_BEFORE && start > 0; i++) {
147253
+ const prev = text.lastIndexOf("\n", start - 2);
147254
+ start = prev < 0 ? 0 : prev + 1;
147255
+ }
147256
+ let end = lineEnd;
147257
+ for (let i = 0; i < LINES_AFTER && end < text.length; i++) {
147258
+ const next2 = text.indexOf("\n", end + 1);
147259
+ end = next2 < 0 ? text.length : next2;
147260
+ }
147261
+ let excerpt = text.slice(start, end);
147262
+ if (excerpt.length > EXCERPT_MAX_BYTES) {
147263
+ excerpt = text.slice(lineStart, lineEnd);
147264
+ if (excerpt.length > EXCERPT_MAX_BYTES) excerpt = excerpt.slice(0, EXCERPT_MAX_BYTES);
147265
+ }
147266
+ return excerpt.trim();
147267
+ }
147194
147268
  var ROUTER_KEYLIMIT_EXHAUSTED_PATTERN = /requires more credits.*?fewer max_tokens|requested up to \d+ tokens.*?can only afford/is;
147195
147269
  function isRouterKeylimitExhaustedError(text) {
147196
147270
  return ROUTER_KEYLIMIT_EXHAUSTED_PATTERN.test(text);
@@ -147895,10 +147969,10 @@ async function runClaude(params) {
147895
147969
  if (!trimmed) return;
147896
147970
  recentStderr.push(trimmed);
147897
147971
  if (recentStderr.length > MAX_STDERR_LINES) recentStderr.shift();
147898
- const providerError = detectProviderError(trimmed);
147899
- if (providerError) {
147900
- lastProviderError = providerError;
147901
- log.info(`\xBB provider error detected (${providerError}): ${trimmed.substring(0, 500)}`);
147972
+ const match3 = findProviderErrorMatch(trimmed);
147973
+ if (match3) {
147974
+ lastProviderError = match3.label;
147975
+ log.info(`\xBB provider error detected (${match3.label}): ${match3.excerpt}`);
147902
147976
  } else {
147903
147977
  log.debug(trimmed);
147904
147978
  }
@@ -148116,6 +148190,68 @@ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "node:f
148116
148190
  import { join as join11 } from "node:path";
148117
148191
  import { performance as performance7 } from "node:perf_hooks";
148118
148192
 
148193
+ // utils/agentHangReport.ts
148194
+ var MAX_STDERR_BYTES = 3e3;
148195
+ function formatAgentHangBody(input) {
148196
+ if (!input.diagnostic) return null;
148197
+ const verb = input.isHang ? "stalled" : "failed";
148198
+ const cause = input.diagnostic.lastProviderError ? ` \u2014 likely cause: \`${input.diagnostic.lastProviderError}\`` : "";
148199
+ const headline = `**${input.diagnostic.label} ${verb}**${cause}`;
148200
+ const explanation = formatExplanation({
148201
+ isHang: input.isHang,
148202
+ errorMessage: input.errorMessage
148203
+ });
148204
+ const parts = [headline, "", `${explanation} ${formatEventsPart(input.diagnostic)}`];
148205
+ const tail = renderStderrTail(input.diagnostic.recentStderr);
148206
+ if (tail) {
148207
+ const fence = pickFence(tail);
148208
+ parts.push(
148209
+ "",
148210
+ "<details><summary>Recent agent stderr</summary>",
148211
+ "",
148212
+ fence,
148213
+ tail,
148214
+ fence,
148215
+ "",
148216
+ "</details>"
148217
+ );
148218
+ }
148219
+ return parts.join("\n");
148220
+ }
148221
+ function formatExplanation(input) {
148222
+ if (!input.isHang) return `The agent exited unexpectedly: ${input.errorMessage}`;
148223
+ const idleSec = parseIdleSec(input.errorMessage);
148224
+ if (idleSec === void 0) {
148225
+ return "The agent stopped emitting events and was killed by the activity-timeout watchdog.";
148226
+ }
148227
+ return `The agent stopped emitting events for ${idleSec}s and was killed by the activity-timeout watchdog.`;
148228
+ }
148229
+ function parseIdleSec(message) {
148230
+ const match3 = /no output for (\d+)s/.exec(message);
148231
+ return match3 ? Number(match3[1]) : void 0;
148232
+ }
148233
+ function formatEventsPart(diagnostic) {
148234
+ if (diagnostic.eventCount > 0) {
148235
+ return `${diagnostic.eventCount} events were processed before the failure.`;
148236
+ }
148237
+ if (diagnostic.lastProviderError) return "No events were emitted before the failure.";
148238
+ return "No events were emitted \u2014 check whether the model provider is reachable.";
148239
+ }
148240
+ function renderStderrTail(lines) {
148241
+ if (lines.length === 0) return "";
148242
+ const joined = lines.join("\n");
148243
+ if (joined.length <= MAX_STDERR_BYTES) return joined;
148244
+ return `... (older lines truncated)
148245
+ ${joined.slice(-MAX_STDERR_BYTES)}`;
148246
+ }
148247
+ function pickFence(content) {
148248
+ let max = 0;
148249
+ for (const match3 of content.matchAll(/`+/g)) {
148250
+ if (match3[0].length > max) max = match3[0].length;
148251
+ }
148252
+ return "`".repeat(Math.max(3, max + 1));
148253
+ }
148254
+
148119
148255
  // agents/opencodePlugin.ts
148120
148256
  var PULLFROG_BUS_EVENT_TYPE = "pullfrog_bus_event";
148121
148257
  var PULLFROG_OPENCODE_PLUGIN_FILENAME = "pullfrog-events.ts";
@@ -148508,8 +148644,7 @@ async function runOpenCode(params) {
148508
148644
  log.debug(withLabel(label, ` output: ${event.part.state.output}`));
148509
148645
  }
148510
148646
  if (event.part?.state?.status === "error") {
148511
- const errorMsg = event.part.state.output ?? "(no error message)";
148512
- log.info(withLabel(label, `\xBB tool call failed: ${errorMsg}`));
148647
+ log.info(withLabel(label, `\xBB tool call failed: ${event.part.state.error}`));
148513
148648
  }
148514
148649
  if (toolName.includes("report_progress") && params.todoTracker) {
148515
148650
  log.debug("\xBB report_progress detected, disabling todo tracking");
@@ -148521,19 +148656,20 @@ async function runOpenCode(params) {
148521
148656
  },
148522
148657
  tool_result: (event) => {
148523
148658
  const toolId = event.part?.callID || event.tool_id;
148524
- const status = event.part?.state?.status || event.status || "unknown";
148525
- const output2 = event.part?.state?.output || event.output;
148659
+ const state = event.part?.state;
148660
+ const status = state?.status ?? event.status ?? "unknown";
148661
+ const payload = state?.status === "completed" ? state.output : state?.status === "error" ? state.error : event.output;
148526
148662
  const label = eventLabel(event);
148527
148663
  timerFor(label).markToolResult();
148528
148664
  if (taskDispatchByCallID.size > 0 || pendingTaskDispatches.length > 0) {
148529
148665
  if (toolId && taskDispatchByCallID.has(toolId)) {
148530
148666
  const dispatch = taskDispatchByCallID.get(toolId);
148531
- if (dispatch) emitSubagentFinished(dispatch, status, output2, "exact");
148667
+ if (dispatch) emitSubagentFinished(dispatch, status, payload, "exact");
148532
148668
  } else {
148533
148669
  const callIDIsKnownNonTask = toolId ? knownNonTaskCallIDs.has(toolId) : false;
148534
148670
  if (!callIDIsKnownNonTask && pendingTaskDispatches.length > 0) {
148535
148671
  const dispatch = pendingTaskDispatches[0];
148536
- emitSubagentFinished(dispatch, status, output2, "fifo");
148672
+ emitSubagentFinished(dispatch, status, payload, "fifo");
148537
148673
  }
148538
148674
  }
148539
148675
  }
@@ -148549,13 +148685,8 @@ async function runOpenCode(params) {
148549
148685
  `\xBB ${params.label} tool_result${stepContext}: id=${toolId}, status=${status}, duration=${Math.round(toolDuration)}ms`
148550
148686
  )
148551
148687
  );
148552
- if (output2) {
148553
- log.debug(
148554
- withLabel(
148555
- label,
148556
- ` output: ${typeof output2 === "string" ? output2 : JSON.stringify(output2)}`
148557
- )
148558
- );
148688
+ if (payload) {
148689
+ log.debug(withLabel(label, ` output: ${payload}`));
148559
148690
  }
148560
148691
  if (toolDuration > 5e3) {
148561
148692
  log.info(
@@ -148568,11 +148699,9 @@ async function runOpenCode(params) {
148568
148699
  }
148569
148700
  }
148570
148701
  if (status === "error") {
148571
- const errorMsg = typeof output2 === "string" ? output2 : JSON.stringify(output2);
148572
- log.info(withLabel(label, `\xBB tool call failed: ${errorMsg}`));
148573
- } else if (output2) {
148574
- const outputStr = typeof output2 === "string" ? output2 : JSON.stringify(output2);
148575
- log.debug(withLabel(label, `tool output: ${outputStr}`));
148702
+ log.info(withLabel(label, `\xBB tool call failed: ${payload ?? "(no error message)"}`));
148703
+ } else if (payload) {
148704
+ log.debug(withLabel(label, `tool output: ${payload}`));
148576
148705
  }
148577
148706
  },
148578
148707
  error: (event) => {
@@ -148649,6 +148778,13 @@ async function runOpenCode(params) {
148649
148778
  const recentStderr = [];
148650
148779
  let lastProviderError = null;
148651
148780
  let agentErrorEvent = null;
148781
+ const diagnostic = {
148782
+ label: params.label,
148783
+ recentStderr,
148784
+ lastProviderError: void 0,
148785
+ eventCount: 0
148786
+ };
148787
+ params.toolState.agentDiagnostic = diagnostic;
148652
148788
  const output = new TailBuffer(DEFAULT_MAX_RETAINED_BYTES);
148653
148789
  let stdoutBuffer = "";
148654
148790
  try {
@@ -148697,6 +148833,7 @@ async function runOpenCode(params) {
148697
148833
  continue;
148698
148834
  }
148699
148835
  eventCount++;
148836
+ diagnostic.eventCount = eventCount;
148700
148837
  log.debug(JSON.stringify(event, null, 2));
148701
148838
  const timeSinceLastActivity = getIdleMs();
148702
148839
  if (timeSinceLastActivity > 1e4) {
@@ -148728,10 +148865,11 @@ async function runOpenCode(params) {
148728
148865
  if (!trimmed) return;
148729
148866
  recentStderr.push(trimmed);
148730
148867
  if (recentStderr.length > MAX_STDERR_LINES) recentStderr.shift();
148731
- const providerError = detectProviderError(trimmed);
148732
- if (providerError) {
148733
- lastProviderError = providerError;
148734
- log.info(`\xBB provider error detected (${providerError}): ${trimmed.substring(0, 500)}`);
148868
+ const match3 = findProviderErrorMatch(trimmed);
148869
+ if (match3) {
148870
+ lastProviderError = match3.label;
148871
+ diagnostic.lastProviderError = match3.label;
148872
+ log.info(`\xBB provider error detected (${match3.label}): ${match3.excerpt}`);
148735
148873
  } else {
148736
148874
  log.debug(trimmed);
148737
148875
  }
@@ -148821,10 +148959,11 @@ ${stderrContext}`);
148821
148959
  `\xBB recent stderr (last ${Math.min(recentStderr.length, 10)} lines):
148822
148960
  ${stderrContext}`
148823
148961
  );
148962
+ const body = formatAgentHangBody({ diagnostic, isHang: isActivityTimeout, errorMessage });
148824
148963
  return {
148825
148964
  success: false,
148826
148965
  output: finalOutput || output.toString(),
148827
- error: `${errorMessage} [${diagnosis}]`,
148966
+ error: body ?? `${errorMessage} [${diagnosis}]`,
148828
148967
  usage: buildUsage()
148829
148968
  };
148830
148969
  }
@@ -148876,6 +149015,7 @@ var opencode = agent({
148876
149015
  cliPath,
148877
149016
  cwd: repoDir,
148878
149017
  env: env2,
149018
+ toolState: ctx.toolState,
148879
149019
  todoTracker: ctx.todoTracker,
148880
149020
  onActivityTimeout: ctx.onActivityTimeout,
148881
149021
  onToolUse: ctx.onToolUse
@@ -155047,24 +155187,29 @@ ${instructions.user}` : null,
155047
155187
  killTrackedChildren();
155048
155188
  log.error(errorMessage);
155049
155189
  const billingError = isRouterKeylimitExhaustedError(errorMessage) ? new BillingError(errorMessage, { code: "router_keylimit_exhausted" }) : null;
155050
- const apiKeyErrorSummary = !billingError && isApiKeyAuthError(errorMessage) ? formatApiKeyErrorSummary({
155190
+ const isHang = errorMessage.startsWith("activity timeout") || errorMessage.startsWith("agent still pending");
155191
+ const hangBody = isHang ? formatAgentHangBody({ diagnostic: toolState.agentDiagnostic, isHang: true, errorMessage }) : null;
155192
+ const apiKeySource = hangBody ?? errorMessage;
155193
+ const apiKeyErrorSummary = !billingError && isApiKeyAuthError(apiKeySource) ? formatApiKeyErrorSummary({
155051
155194
  owner: runContext.repo.owner,
155052
155195
  name: runContext.repo.name,
155053
- raw: errorMessage
155196
+ raw: apiKeySource
155054
155197
  }) : null;
155055
155198
  try {
155056
- const errorSummary = billingError ? formatBillingErrorSummary(billingError, runContext.repo.owner) : apiKeyErrorSummary ?? `### \u274C Pullfrog failed
155199
+ const errorSummary = billingError ? formatBillingErrorSummary(billingError, runContext.repo.owner) : apiKeyErrorSummary ?? (hangBody ? `### \u274C Pullfrog failed
155200
+
155201
+ ${hangBody}` : `### \u274C Pullfrog failed
155057
155202
 
155058
155203
  \`\`\`
155059
155204
  ${errorMessage}
155060
- \`\`\``;
155205
+ \`\`\``);
155061
155206
  const usageSummary = formatUsageSummary(toolState.usageEntries);
155062
155207
  const parts = [errorSummary, toolState.lastProgressBody, usageSummary].filter(Boolean);
155063
155208
  await writeSummary(parts.join("\n\n"));
155064
155209
  } catch {
155065
155210
  }
155066
155211
  try {
155067
- const commentBody = billingError ? formatBillingErrorSummary(billingError, runContext.repo.owner) : apiKeyErrorSummary ?? errorMessage;
155212
+ const commentBody = billingError ? formatBillingErrorSummary(billingError, runContext.repo.owner) : apiKeyErrorSummary ?? hangBody ?? errorMessage;
155068
155213
  await reportErrorToComment({ toolState, error: commentBody });
155069
155214
  } catch {
155070
155215
  }
@@ -156923,7 +157068,7 @@ async function run2() {
156923
157068
  }
156924
157069
 
156925
157070
  // cli.ts
156926
- var VERSION10 = "0.1.7";
157071
+ var VERSION10 = "0.1.8";
156927
157072
  var bin = basename2(process.argv[1] || "");
156928
157073
  var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
156929
157074
  var rawArgs = process.argv.slice(2);