pullfrog 0.1.54 → 0.1.56

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
@@ -98607,14 +98607,14 @@ var require_turndown_cjs = __commonJS({
98607
98607
  } else if (node2.nodeType === 1) {
98608
98608
  replacement = replacementForNode.call(self2, node2);
98609
98609
  }
98610
- return join27(output, replacement);
98610
+ return join28(output, replacement);
98611
98611
  }, "");
98612
98612
  }
98613
98613
  function postProcess(output) {
98614
98614
  var self2 = this;
98615
98615
  this.rules.forEach(function(rule) {
98616
98616
  if (typeof rule.append === "function") {
98617
- output = join27(output, rule.append(self2.options));
98617
+ output = join28(output, rule.append(self2.options));
98618
98618
  }
98619
98619
  });
98620
98620
  return output.replace(/^[\t\r\n]+/, "").replace(/[\t\r\n\s]+$/, "");
@@ -98626,7 +98626,7 @@ var require_turndown_cjs = __commonJS({
98626
98626
  if (whitespace.leading || whitespace.trailing) content = content.trim();
98627
98627
  return whitespace.leading + rule.replacement(content, node2, this.options) + whitespace.trailing;
98628
98628
  }
98629
- function join27(output, replacement) {
98629
+ function join28(output, replacement) {
98630
98630
  var s1 = trimTrailingNewlines(output);
98631
98631
  var s2 = trimLeadingNewlines(replacement);
98632
98632
  var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
@@ -105313,7 +105313,7 @@ import { dirname as dirname6 } from "node:path";
105313
105313
  // main.ts
105314
105314
  import { existsSync as existsSync8, readdirSync as readdirSync2 } from "node:fs";
105315
105315
  import { readFile as readFile5 } from "node:fs/promises";
105316
- import { join as join26 } from "node:path";
105316
+ import { join as join27 } from "node:path";
105317
105317
 
105318
105318
  // node_modules/.pnpm/package-manager-detector@1.8.0/node_modules/package-manager-detector/dist/commands.mjs
105319
105319
  function splitRunArgs(args2, valueFlags = []) {
@@ -105902,12 +105902,22 @@ var providers = {
105902
105902
  displayName: "DeepSeek",
105903
105903
  envVars: ["DEEPSEEK_API_KEY"],
105904
105904
  models: {
105905
+ // same fork trap as `deepseek-flash` below, and Pro fell into it on
105906
+ // 2026-08-12: OpenRouter forked the 0813 release into its own id and left
105907
+ // the April preview live under the unversioned one at 2.7x the input and
105908
+ // 27x the CACHE-READ rate ($1.168/$2.336/$0.09855 vs
105909
+ // $0.435/$0.87/$0.003625). the direct route upgraded in place, so only the
105910
+ // OpenRouter side was stale — and that side is what OSS and Router runs
105911
+ // use. measured: $0.1719/run authoritative against the funded default's
105912
+ // $0.0405. there is no `~deepseek/*-pro-latest` pointer to track, so this
105913
+ // one has to be a dated pin; the `models-catalog` drift test is what
105914
+ // catches the next release.
105905
105915
  "deepseek-pro": {
105906
105916
  displayName: "DeepSeek Pro",
105907
105917
  resolve: "deepseek/deepseek-v4-pro",
105908
105918
  effort: ["high", "max"],
105909
- openRouterEffort: ["high", "xhigh"],
105910
- openRouterResolve: "openrouter/deepseek/deepseek-v4-pro",
105919
+ openRouterEffort: ["low", "high", "max"],
105920
+ openRouterResolve: "openrouter/deepseek/deepseek-v4-pro-0813",
105911
105921
  preferred: true
105912
105922
  },
105913
105923
  // DeepSeek upgrades the `deepseek-v4-flash` API model in place, so the
@@ -105915,14 +105925,19 @@ var providers = {
105915
105925
  // release into its own id and left the April preview live under the
105916
105926
  // unversioned one at a HIGHER price ($0.14/$0.28 vs $0.09/$0.18) — so the
105917
105927
  // OpenRouter route can never use the bare id. it tracks the rolling
105918
- // `~deepseek/*-latest` pointer (rule 4), which resolves to 0731 at the
105919
- // same price today and follows the next in-place upgrade on its own,
105920
- // rather than pinning a dated snapshot that goes stale every release.
105921
- // 0731 gained a `low` rung and renamed the top to `max`, unlike Pro
105922
- // (`high, max` direct / `high, xhigh` via OpenRouter). that ladder now
105923
- // reads the same on BOTH routes — the in-place direct upgrade caught up
105924
- // to the fork so `low`, not `high`, is position 0 either way, which is
105925
- // why the OSS effort floor in `main.ts` pins the `high` rung by name.
105928
+ // `~deepseek/*-latest` pointer (rule 4), which follows the next in-place
105929
+ // upgrade on its own rather than pinning a dated snapshot that goes stale
105930
+ // every release. that pointer is NOT priced like 0731 its listed rates
105931
+ // run +40% on output and +57.5% on cache read, and it costs 12% more per
105932
+ // run on authoritative spend ($0.0405 vs $0.0361). kept anyway: ~$45/yr
105933
+ // buys auto-following the upgrade, and the listed gap overstates the real
105934
+ // one because a rolling alias prices at its routing ceiling.
105935
+ // 0731 gained a `low` rung and renamed the top to `max`, and that ladder
105936
+ // now reads the same on BOTH routes — the in-place direct upgrade caught
105937
+ // up to the fork. so `low`, not `high`, is position 0 either way, which is
105938
+ // why the OSS effort floor in `main.ts` pins the `high` rung by name. Pro
105939
+ // is the same shape via OpenRouter since 0813; only its direct route still
105940
+ // starts at `high`.
105926
105941
  "deepseek-flash": {
105927
105942
  displayName: "DeepSeek Flash",
105928
105943
  resolve: "deepseek/deepseek-v4-flash",
@@ -106321,11 +106336,13 @@ var providers = {
106321
106336
  effort: ["low", "medium", "high"],
106322
106337
  openRouterResolve: "openrouter/x-ai/grok-4.5"
106323
106338
  },
106339
+ // dated pin, not the bare id — see `deepseek/deepseek-pro` for why
106340
+ // OpenRouter's unversioned `deepseek-v4-pro` is the stale April preview.
106324
106341
  "deepseek-pro": {
106325
106342
  displayName: "DeepSeek Pro",
106326
- resolve: "openrouter/deepseek/deepseek-v4-pro",
106327
- effort: ["high", "xhigh"],
106328
- openRouterResolve: "openrouter/deepseek/deepseek-v4-pro"
106343
+ resolve: "openrouter/deepseek/deepseek-v4-pro-0813",
106344
+ effort: ["low", "high", "max"],
106345
+ openRouterResolve: "openrouter/deepseek/deepseek-v4-pro-0813"
106329
106346
  },
106330
106347
  // rolling pointer, not the bare id — see `deepseek/deepseek-flash` for why
106331
106348
  // OpenRouter's unversioned `deepseek-v4-flash` is the stale April preview.
@@ -106516,6 +106533,19 @@ function formatMcpToolRef(agentId, toolName) {
106516
106533
  return agentId;
106517
106534
  }
106518
106535
  }
106536
+ function extractMcpToolRefs(agentId, text) {
106537
+ const prefix = formatMcpToolRef(agentId, "");
106538
+ const escaped = prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
106539
+ return [...text.matchAll(new RegExp(`${escaped}([a-z0-9_]+)`, "g"))].map((match3) => match3[1]);
106540
+ }
106541
+ function assertPromptToolRefs(params) {
106542
+ const registered = new Set(params.toolNames);
106543
+ const dangling = [...new Set(extractMcpToolRefs(params.agentId, params.prompt))].filter((name) => !registered.has(name)).sort();
106544
+ if (dangling.length === 0) return;
106545
+ throw new Error(
106546
+ `prompt advertises ${dangling.length} tool(s) the MCP server did not register: ${dangling.join(", ")}. registered: ${[...registered].sort().join(", ")}`
106547
+ );
106548
+ }
106519
106549
  function hasSimilarIssues(params) {
106520
106550
  return params.repoIntelligence && !!params.event.issue_number && !params.event.is_pr;
106521
106551
  }
@@ -107289,7 +107319,7 @@ var import_semver = __toESM(require_semver2(), 1);
107289
107319
  // package.json
107290
107320
  var package_default = {
107291
107321
  name: "pullfrog",
107292
- version: "0.1.54",
107322
+ version: "0.1.56",
107293
107323
  type: "module",
107294
107324
  bin: {
107295
107325
  pullfrog: "dist/cli.mjs",
@@ -114921,11 +114951,12 @@ function bootstrapPullfrogDataDir() {
114921
114951
  }
114922
114952
 
114923
114953
  // utils/worktree.ts
114924
- async function dirtyTrackedPaths() {
114954
+ async function dirtyTrackedPaths(params = {}) {
114925
114955
  const result = await spawn3({
114926
114956
  cmd: "git",
114927
114957
  args: ["diff", "--name-only", "HEAD"],
114928
114958
  env: process.env,
114959
+ cwd: params.cwd ?? process.cwd(),
114929
114960
  activityTimeout: 0
114930
114961
  });
114931
114962
  if (result.exitCode !== 0) {
@@ -114936,12 +114967,15 @@ async function dirtyTrackedPaths() {
114936
114967
  return new Set(result.stdout.split("\n").filter(Boolean));
114937
114968
  }
114938
114969
  async function restoreDirtiedSince(params) {
114939
- const dirtied = [...await dirtyTrackedPaths()].filter((path4) => !params.before.has(path4));
114970
+ const dirtied = [...await dirtyTrackedPaths({ cwd: params.cwd ?? process.cwd() })].filter(
114971
+ (path4) => !params.before.has(path4)
114972
+ );
114940
114973
  if (dirtied.length === 0) return;
114941
114974
  const result = await spawn3({
114942
114975
  cmd: "git",
114943
114976
  args: ["restore", "--staged", "--worktree", "--", ...dirtied],
114944
114977
  env: process.env,
114978
+ cwd: params.cwd ?? process.cwd(),
114945
114979
  activityTimeout: 0
114946
114980
  });
114947
114981
  if (result.exitCode !== 0) {
@@ -190869,6 +190903,116 @@ function CommitInfoTool(ctx) {
190869
190903
  });
190870
190904
  }
190871
190905
 
190906
+ // utils/roleMirror.ts
190907
+ function mirrorRolePermissions(role) {
190908
+ switch (role) {
190909
+ case "admin":
190910
+ case "maintain":
190911
+ case "write":
190912
+ case "triage":
190913
+ return { contents: "read", issues: "write", pull_requests: "write" };
190914
+ case "read":
190915
+ case "none":
190916
+ case void 0:
190917
+ return null;
190918
+ default:
190919
+ return role;
190920
+ }
190921
+ }
190922
+ function formatPermissions(permissions) {
190923
+ return Object.entries(permissions).map((entry) => entry.join(":")).join(", ");
190924
+ }
190925
+
190926
+ // mcp/currentObject.ts
190927
+ async function resolveCurrentObject(ctx) {
190928
+ const issueNumber = ctx.payload.event.issue_number;
190929
+ if (!issueNumber) {
190930
+ throw new Error("this run was not triggered on an issue or pull request");
190931
+ }
190932
+ if (mirrorRolePermissions(ctx.payload.event.authorPermission)) {
190933
+ return { issueNumber, isPr: ctx.payload.event.is_pr === true };
190934
+ }
190935
+ const issue3 = await ctx.octokit.rest.issues.get({
190936
+ owner: ctx.repo.owner,
190937
+ repo: ctx.repo.name,
190938
+ issue_number: issueNumber
190939
+ });
190940
+ const author = issue3.data.user?.login;
190941
+ const triggerer = ctx.payload.triggerer;
190942
+ if (!author || !triggerer || author.toLowerCase() !== triggerer.toLowerCase()) {
190943
+ throw new Error(
190944
+ `${triggerer ?? "the triggering user"} did not open #${issueNumber} and has no write access to ${ctx.repo.owner}/${ctx.repo.name}, so this run cannot change its state.`
190945
+ );
190946
+ }
190947
+ return { issueNumber, isPr: issue3.data.pull_request !== void 0 };
190948
+ }
190949
+ var CloseCurrent = type({
190950
+ state_reason: type.enumerated("completed", "not_planned", "duplicate").describe(
190951
+ "why it is being closed: 'completed' (resolved), 'not_planned' (won't fix, out of scope), or 'duplicate' (already tracked elsewhere). ignored for pull requests."
190952
+ )
190953
+ });
190954
+ function CloseCurrentTool(ctx) {
190955
+ return tool({
190956
+ name: "close_current",
190957
+ mutates: true,
190958
+ description: 'Close the issue or pull request this run was triggered on. Example: `close_current({ state_reason: "duplicate" })`. Comment first to explain the decision \u2014 closing alone is opaque to the author.',
190959
+ parameters: CloseCurrent,
190960
+ execute: execute(async (params) => {
190961
+ const target = await resolveCurrentObject(ctx);
190962
+ const result = target.isPr ? await ctx.octokit.rest.pulls.update({
190963
+ owner: ctx.repo.owner,
190964
+ repo: ctx.repo.name,
190965
+ pull_number: target.issueNumber,
190966
+ state: "closed"
190967
+ }) : await ctx.octokit.rest.issues.update({
190968
+ owner: ctx.repo.owner,
190969
+ repo: ctx.repo.name,
190970
+ issue_number: target.issueNumber,
190971
+ state: "closed",
190972
+ state_reason: params.state_reason
190973
+ });
190974
+ ctx.toolState.wasUpdated = true;
190975
+ log2.info(`\xBB closed #${target.issueNumber} (${params.state_reason})`);
190976
+ return {
190977
+ success: true,
190978
+ number: result.data.number,
190979
+ url: result.data.html_url,
190980
+ state: result.data.state
190981
+ };
190982
+ }, "close_current")
190983
+ });
190984
+ }
190985
+ function ReopenCurrentTool(ctx) {
190986
+ return tool({
190987
+ name: "reopen_current",
190988
+ mutates: true,
190989
+ description: "Reopen the issue or pull request this run was triggered on.",
190990
+ parameters: type({}),
190991
+ execute: execute(async () => {
190992
+ const target = await resolveCurrentObject(ctx);
190993
+ const result = target.isPr ? await ctx.octokit.rest.pulls.update({
190994
+ owner: ctx.repo.owner,
190995
+ repo: ctx.repo.name,
190996
+ pull_number: target.issueNumber,
190997
+ state: "open"
190998
+ }) : await ctx.octokit.rest.issues.update({
190999
+ owner: ctx.repo.owner,
191000
+ repo: ctx.repo.name,
191001
+ issue_number: target.issueNumber,
191002
+ state: "open"
191003
+ });
191004
+ ctx.toolState.wasUpdated = true;
191005
+ log2.info(`\xBB reopened #${target.issueNumber}`);
191006
+ return {
191007
+ success: true,
191008
+ number: result.data.number,
191009
+ url: result.data.html_url,
191010
+ state: result.data.state
191011
+ };
191012
+ }, "reopen_current")
191013
+ });
191014
+ }
191015
+
190872
191016
  // prep/index.ts
190873
191017
  import { performance as performance8 } from "node:perf_hooks";
190874
191018
 
@@ -191495,6 +191639,456 @@ function AwaitDependencyInstallationTool(ctx) {
191495
191639
  });
191496
191640
  }
191497
191641
 
191642
+ // mcp/gh.ts
191643
+ import { mkdirSync as mkdirSync8 } from "node:fs";
191644
+ import { join as join21 } from "node:path";
191645
+
191646
+ // mcp/shell.ts
191647
+ import { spawn as spawn5, spawnSync as spawnSync6 } from "node:child_process";
191648
+ import { randomUUID as randomUUID4 } from "node:crypto";
191649
+ import { closeSync, openSync, writeFileSync as writeFileSync12 } from "node:fs";
191650
+ import { userInfo as userInfo2 } from "node:os";
191651
+ import { join as join20 } from "node:path";
191652
+ import { setTimeout as sleep3 } from "node:timers/promises";
191653
+ var ShellParams = type({
191654
+ command: "string",
191655
+ // advisory and unread — no code path consumes it. kept in the schema only
191656
+ // because it nudges the model to state intent before running a command;
191657
+ // REQUIRING it cost a full rejected turn every time one was omitted. #1140.
191658
+ "description?": "string",
191659
+ "timeout?": type.number.describe(
191660
+ "Timeout in MILLISECONDS (not seconds). Default 30000 (30s), max 120000 (2m). e.g. timeout: 180000 for 3 minutes; timeout: 180 means 180ms and will kill the process almost immediately."
191661
+ ),
191662
+ "working_directory?": "string",
191663
+ "background?": "boolean"
191664
+ });
191665
+ var detectedSandboxMethod;
191666
+ var usernsProbeError;
191667
+ function getSandboxMethod() {
191668
+ return detectSandboxMethod();
191669
+ }
191670
+ function detectSandboxMethod() {
191671
+ if (detectedSandboxMethod !== void 0) {
191672
+ return detectedSandboxMethod;
191673
+ }
191674
+ if (process.env.CI !== "true") {
191675
+ detectedSandboxMethod = "none";
191676
+ log2.debug("sandbox disabled (CI !== true)");
191677
+ return "none";
191678
+ }
191679
+ try {
191680
+ const result = spawnSync6("unshare", ["--pid", "--fork", "--mount-proc", "true"], {
191681
+ timeout: 5e3,
191682
+ stdio: "ignore"
191683
+ });
191684
+ if (result.status === 0) {
191685
+ detectedSandboxMethod = "unshare";
191686
+ log2.debug("PID namespace isolation enabled (unprivileged unshare)");
191687
+ return "unshare";
191688
+ }
191689
+ } catch {
191690
+ }
191691
+ try {
191692
+ const result = spawnSync6("sudo", ["unshare", "--pid", "--fork", "--mount-proc", "true"], {
191693
+ timeout: 5e3,
191694
+ stdio: "ignore"
191695
+ });
191696
+ if (result.status === 0) {
191697
+ detectedSandboxMethod = "sudo-unshare";
191698
+ log2.debug("PID namespace isolation enabled (sudo unshare)");
191699
+ return "sudo-unshare";
191700
+ }
191701
+ } catch {
191702
+ }
191703
+ try {
191704
+ const result = spawnSync6(
191705
+ "unshare",
191706
+ [
191707
+ "--user",
191708
+ "--map-root-user",
191709
+ "--pid",
191710
+ "--fork",
191711
+ "--mount",
191712
+ "setpriv",
191713
+ "--no-new-privs",
191714
+ "--bounding-set",
191715
+ "-all",
191716
+ "--inh-caps",
191717
+ "-all",
191718
+ "--ambient-caps",
191719
+ "-all",
191720
+ "true"
191721
+ ],
191722
+ { timeout: 5e3, stdio: ["ignore", "ignore", "pipe"] }
191723
+ );
191724
+ if (result.status === 0) {
191725
+ detectedSandboxMethod = "userns-unshare";
191726
+ log2.debug("PID namespace isolation enabled (unprivileged userns unshare)");
191727
+ return "userns-unshare";
191728
+ }
191729
+ usernsProbeError = result.stderr?.toString().trim() || result.error?.message;
191730
+ } catch {
191731
+ }
191732
+ detectedSandboxMethod = "none";
191733
+ log2.info("PID namespace isolation not available");
191734
+ return "none";
191735
+ }
191736
+ var PROC_CLEANUP = "umount /proc 2>/dev/null; umount /proc 2>/dev/null; mount -t proc proc /proc 2>/dev/null;";
191737
+ var SOCKET_CLEANUP = [
191738
+ "/var/run/docker.sock",
191739
+ "/run/docker.sock",
191740
+ "/var/run/podman/podman.sock",
191741
+ "/run/podman/podman.sock",
191742
+ "/run/containerd/containerd.sock",
191743
+ "/var/run/crio/crio.sock"
191744
+ ].map((path4) => `mount --bind /dev/null ${path4} 2>/dev/null;`).join(" ");
191745
+ function buildFsMounts(repoDir) {
191746
+ const escaped = repoDir.replace(/'/g, "'\\''");
191747
+ return [
191748
+ `mkdir -p /var/lib/pullfrog 2>/dev/null;`,
191749
+ `mount -t tmpfs tmpfs /var/lib/pullfrog 2>/dev/null;`,
191750
+ `[ -n "$RUNNER_TEMP" ] && [ -d "$RUNNER_TEMP/_runner_file_commands" ] && mount -t tmpfs tmpfs "$RUNNER_TEMP/_runner_file_commands" 2>/dev/null;`,
191751
+ `[ -e '${escaped}/.git' ] && mount --bind '${escaped}/.git' '${escaped}/.git' 2>/dev/null && mount -o remount,bind,ro '${escaped}/.git' 2>/dev/null;`
191752
+ ].join(" ");
191753
+ }
191754
+ var _repoRoot;
191755
+ function resolveRepoRoot() {
191756
+ if (_repoRoot) return _repoRoot;
191757
+ const fromEnv = process.env.GITHUB_WORKSPACE;
191758
+ if (fromEnv) {
191759
+ _repoRoot = fromEnv;
191760
+ return _repoRoot;
191761
+ }
191762
+ try {
191763
+ _repoRoot = spawnSync6("git", ["rev-parse", "--show-toplevel"], {
191764
+ cwd: process.cwd(),
191765
+ stdio: ["ignore", "pipe", "ignore"],
191766
+ encoding: "utf-8"
191767
+ }).stdout?.trim();
191768
+ } catch {
191769
+ }
191770
+ if (!_repoRoot) _repoRoot = process.cwd();
191771
+ return _repoRoot;
191772
+ }
191773
+ function spawnShell(params) {
191774
+ const spawnOpts = { env: params.env, cwd: params.cwd, stdio: params.stdio, detached: true };
191775
+ const sandboxMethod = detectSandboxMethod();
191776
+ const ci = process.env.CI === "true";
191777
+ if (ci && sandboxMethod === "none") {
191778
+ throw new Error(
191779
+ "PID-namespace isolation is required in CI but unavailable: unprivileged unshare, sudo unshare, and userns-nested unshare all failed. " + (usernsProbeError ? `userns probe said: ${usernsProbeError}. ` : "") + "on a containerized / Kubernetes self-hosted runner the cause is usually one of: the pod's seccomp profile blocks user-namespace creation (Pod Security Standards baseline/restricted); the pod runs as root without CAP_SETFCAP, which the kernel requires to map uid 0; or the node kernel disallows unprivileged user namespaces (Ubuntu >= 23.10, Bottlerocket, Talos). elsewhere, check that `unshare` and `setpriv` (util-linux >= 2.31, which is what added --ambient-caps) are on PATH. see https://docs.pullfrog.com/security#self-hosted-runners"
191780
+ );
191781
+ }
191782
+ const repoRoot = resolveRepoRoot();
191783
+ const fsMounts = buildFsMounts(repoRoot);
191784
+ if (sandboxMethod === "unshare") {
191785
+ return spawn5(
191786
+ "unshare",
191787
+ [
191788
+ "--pid",
191789
+ "--fork",
191790
+ "--mount-proc",
191791
+ "bash",
191792
+ "-c",
191793
+ `${PROC_CLEANUP} ${SOCKET_CLEANUP} ${fsMounts} ${params.command}`
191794
+ ],
191795
+ spawnOpts
191796
+ );
191797
+ }
191798
+ if (sandboxMethod === "userns-unshare") {
191799
+ const escaped = params.command.replace(/'/g, "'\\''");
191800
+ return spawn5(
191801
+ "unshare",
191802
+ [
191803
+ "--user",
191804
+ "--map-root-user",
191805
+ "--pid",
191806
+ "--fork",
191807
+ "--mount",
191808
+ "bash",
191809
+ "-c",
191810
+ `${PROC_CLEANUP} ${SOCKET_CLEANUP} ${fsMounts} exec setpriv --no-new-privs --bounding-set -all --inh-caps -all --ambient-caps -all bash -c '${escaped}'`
191811
+ ],
191812
+ spawnOpts
191813
+ );
191814
+ }
191815
+ if (sandboxMethod === "sudo-unshare") {
191816
+ const envArgs = [];
191817
+ for (const [k2, v] of Object.entries(params.env)) {
191818
+ if (v !== void 0) {
191819
+ envArgs.push(`${k2}=${v}`);
191820
+ }
191821
+ }
191822
+ const username = userInfo2().username;
191823
+ const pathRestore = 'export PATH="${SANDBOX_PATH:-$PATH}"; ';
191824
+ const escaped = (pathRestore + params.command).replace(/'/g, "'\\''");
191825
+ envArgs.push(`SANDBOX_PATH=${params.env.PATH ?? ""}`);
191826
+ return spawn5(
191827
+ "sudo",
191828
+ [
191829
+ "env",
191830
+ ...envArgs,
191831
+ "unshare",
191832
+ "--pid",
191833
+ "--fork",
191834
+ "--mount-proc",
191835
+ "bash",
191836
+ "-c",
191837
+ `${PROC_CLEANUP} ${SOCKET_CLEANUP} ${fsMounts} exec su -p -s /bin/bash ${username} -c '${escaped}'`
191838
+ ],
191839
+ { ...spawnOpts, env: {} }
191840
+ );
191841
+ }
191842
+ return spawn5("bash", ["-c", params.command], spawnOpts);
191843
+ }
191844
+ async function killProcessGroup(proc) {
191845
+ if (!proc.pid) return;
191846
+ try {
191847
+ process.kill(-proc.pid, "SIGTERM");
191848
+ await new Promise((r2) => setTimeout(r2, 200));
191849
+ process.kill(-proc.pid, "SIGKILL");
191850
+ } catch {
191851
+ try {
191852
+ proc.kill("SIGKILL");
191853
+ } catch {
191854
+ }
191855
+ }
191856
+ }
191857
+ function getTempDir() {
191858
+ const tempDir = process.env.PULLFROG_TEMP_DIR;
191859
+ if (!tempDir) {
191860
+ throw new Error("PULLFROG_TEMP_DIR not set");
191861
+ }
191862
+ return tempDir;
191863
+ }
191864
+ var MAX_OUTPUT_CHARS = 5e3;
191865
+ async function runSandboxed(params) {
191866
+ const proc = spawnShell({
191867
+ command: params.command,
191868
+ env: params.env,
191869
+ cwd: params.cwd,
191870
+ stdio: ["ignore", "pipe", "pipe"]
191871
+ });
191872
+ let stdout2 = "", stderr = "", timedOut = false, exited = false, spawnError = "";
191873
+ proc.stdout?.on("data", (chunk) => {
191874
+ stdout2 += chunk.toString();
191875
+ });
191876
+ proc.stderr?.on("data", (chunk) => {
191877
+ stderr += chunk.toString();
191878
+ });
191879
+ const timeoutId = setTimeout(async () => {
191880
+ if (!exited) {
191881
+ timedOut = true;
191882
+ await killProcessGroup(proc);
191883
+ }
191884
+ }, params.timeout);
191885
+ const exitCode = await new Promise((resolve3) => {
191886
+ const done = (code) => {
191887
+ exited = true;
191888
+ clearTimeout(timeoutId);
191889
+ resolve3(code);
191890
+ };
191891
+ proc.on("exit", done);
191892
+ proc.on("error", (err) => {
191893
+ spawnError = err.message;
191894
+ done(null);
191895
+ });
191896
+ });
191897
+ proc.stdout?.destroy();
191898
+ proc.stderr?.destroy();
191899
+ let output = stderr ? stdout2 ? `${stdout2}
191900
+ ${stderr}` : stderr : stdout2;
191901
+ if (spawnError) output = output ? `${output}
191902
+ ${spawnError}` : spawnError;
191903
+ if (timedOut)
191904
+ output = output ? `${output}
191905
+ [timed out after ${params.timeout}ms]` : `[timed out after ${params.timeout}ms]`;
191906
+ return {
191907
+ output: output.trim(),
191908
+ exitCode: exitCode ?? (timedOut ? 124 : -1),
191909
+ timedOut
191910
+ };
191911
+ }
191912
+ function capOutput(output) {
191913
+ if (output.length <= MAX_OUTPUT_CHARS) return output;
191914
+ const fullPath = join20(getTempDir(), `shell-${randomUUID4().slice(0, 8)}.log`);
191915
+ writeFileSync12(fullPath, output);
191916
+ const elided = output.length - MAX_OUTPUT_CHARS;
191917
+ return `... [${elided} chars truncated; full output saved to ${fullPath}] ...
191918
+ ${output.slice(-MAX_OUTPUT_CHARS)}`;
191919
+ }
191920
+ function isGitCommand(command) {
191921
+ const trimmed = command.trim();
191922
+ if (trimmed === "git" || trimmed.startsWith("git ")) return true;
191923
+ if (trimmed.startsWith("sudo git")) return true;
191924
+ return /[;&|]\s*(?:sudo\s+)?git(?:\s|$)/.test(trimmed);
191925
+ }
191926
+ function ShellTool(ctx) {
191927
+ return tool({
191928
+ name: "shell",
191929
+ timeoutMs: 12e4,
191930
+ description: `Execute shell commands securely. Environment is filtered to remove API keys and secrets.
191931
+
191932
+ Example: \`shell({ command: "pnpm test", description: "run the test suite" })\`.
191933
+
191934
+ Use this tool to:
191935
+ - Run shell commands (ls, cat, grep, find, etc.)
191936
+ - Execute build tools (npm, pnpm, cargo, make, etc.)
191937
+ - Run tests and linters
191938
+
191939
+ 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.
191940
+
191941
+ Do NOT use this tool for git commands \u2014 use the dedicated git tools instead.`,
191942
+ parameters: ShellParams,
191943
+ execute: execute(async (params) => {
191944
+ if (isGitCommand(params.command)) {
191945
+ throw new Error(
191946
+ 'git commands are not allowed in the shell tool. use the dedicated git tools instead:\n- git: local operations, e.g. git({ command: "log", args: ["--oneline"] }) \u2014 `command` is the subcommand ONLY\n- push_branch: push to remote (handles authentication)\n- git_fetch: fetch from remote (handles authentication)\n- checkout_pr: check out PR branches'
191947
+ );
191948
+ }
191949
+ const timeout = Math.min(params.timeout ?? 3e4, 12e4);
191950
+ const cwd = params.working_directory ?? process.cwd();
191951
+ const env2 = resolveEnv(ctx.payload.shell === "enabled" ? "inherit" : "restricted");
191952
+ if (params.command.includes("agent-browser")) {
191953
+ const daemonError = ensureBrowserDaemon(ctx.toolState);
191954
+ if (daemonError) {
191955
+ return {
191956
+ output: `browser daemon unavailable: ${daemonError}`,
191957
+ exit_code: 1,
191958
+ timed_out: false
191959
+ };
191960
+ }
191961
+ const binDir = ctx.toolState.browserDaemon?.binDir;
191962
+ if (binDir) {
191963
+ env2.PATH = `${binDir}:${env2.PATH ?? ""}`;
191964
+ }
191965
+ }
191966
+ if (params.background) {
191967
+ const tempDir = getTempDir();
191968
+ const handle = `bg-${randomUUID4().slice(0, 8)}`;
191969
+ const outputPath = join20(tempDir, `${handle}.log`);
191970
+ const pidPath = join20(tempDir, `${handle}.pid`);
191971
+ const logFd = openSync(outputPath, "a");
191972
+ let proc;
191973
+ try {
191974
+ proc = spawnShell({
191975
+ command: params.command,
191976
+ env: env2,
191977
+ cwd,
191978
+ stdio: ["ignore", logFd, logFd]
191979
+ });
191980
+ } finally {
191981
+ closeSync(logFd);
191982
+ }
191983
+ if (!proc.pid) {
191984
+ throw new Error("failed to start background process");
191985
+ }
191986
+ proc.unref();
191987
+ writeFileSync12(pidPath, `${proc.pid}
191988
+ `);
191989
+ ctx.toolState.backgroundProcesses.set(handle, { pid: proc.pid, outputPath, pidPath });
191990
+ return {
191991
+ handle,
191992
+ outputPath,
191993
+ pidPath,
191994
+ message: `started background process ${handle} (pid ${proc.pid})`
191995
+ };
191996
+ }
191997
+ const result = await runSandboxed({ command: params.command, env: env2, cwd, timeout });
191998
+ if (result.exitCode !== 0) {
191999
+ log2.info(`shell command failed with exit code ${result.exitCode}: ${params.command}`);
192000
+ if (result.output) log2.info(`output: ${result.output}`);
192001
+ }
192002
+ return {
192003
+ output: capOutput(result.output),
192004
+ exit_code: result.exitCode,
192005
+ timed_out: result.timedOut
192006
+ };
192007
+ })
192008
+ });
192009
+ }
192010
+ var KillBackgroundParams = type({
192011
+ handle: type.string.describe("The handle of the background process to kill (e.g., bg-a1b2c3d4)")
192012
+ });
192013
+ function KillBackgroundTool(ctx) {
192014
+ return tool({
192015
+ name: "kill_background",
192016
+ mutates: true,
192017
+ description: `Kill a background process by its handle. Use this to stop dev servers or other long-running processes started with shell({ background: true }).`,
192018
+ parameters: KillBackgroundParams,
192019
+ execute: execute(async (params) => {
192020
+ const proc = ctx.toolState.backgroundProcesses.get(params.handle);
192021
+ if (!proc) {
192022
+ return {
192023
+ success: false,
192024
+ message: `no background process with handle ${params.handle}`
192025
+ };
192026
+ }
192027
+ try {
192028
+ process.kill(-proc.pid, "SIGTERM");
192029
+ } catch {
192030
+ }
192031
+ await sleep3(200);
192032
+ try {
192033
+ process.kill(-proc.pid, "SIGKILL");
192034
+ } catch {
192035
+ }
192036
+ ctx.toolState.backgroundProcesses.delete(params.handle);
192037
+ return {
192038
+ success: true,
192039
+ message: `killed background process ${params.handle} (pid ${proc.pid})`
192040
+ };
192041
+ })
192042
+ });
192043
+ }
192044
+
192045
+ // mcp/gh.ts
192046
+ var GH_TIMEOUT_MS = 12e4;
192047
+ var GhParams = type({
192048
+ args: type.string.array().describe(
192049
+ 'gh arguments as an argv array, e.g. ["pr", "list", "--json", "number,title"] or ["api", "graphql", "-f", "query=..."]'
192050
+ )
192051
+ });
192052
+ function shellQuote(arg6) {
192053
+ return `'${arg6.replace(/'/g, "'\\''")}'`;
192054
+ }
192055
+ function GhTool(ctx) {
192056
+ const permissions = mirrorRolePermissions(ctx.payload.event.authorPermission);
192057
+ return tool({
192058
+ name: "gh",
192059
+ // denied to subagents. `shell` is deliberately unmarked because git is
192060
+ // blocked inside it, but `gh` reaches PR and issue state changes — the
192061
+ // shape of the 2026-05-18 cross-branch clobber the gate exists for.
192062
+ mutates: true,
192063
+ timeoutMs: GH_TIMEOUT_MS,
192064
+ description: `Run the GitHub CLI against ${ctx.repo.owner}/${ctx.repo.name}. Pass argv as an array \u2014 there is no shell, so pipes, redirects and \`&&\` do not work; use gh's own \`--json\`/\`--jq\` flags instead. \`gh api graphql\` is available. Authenticated with ${permissions ? formatPermissions(permissions) : "no permissions"} on this repo only; anything else 403s. Pushing, merging and ref writes are not available \u2014 use the git tools.`,
192065
+ parameters: GhParams,
192066
+ execute: execute(async (params) => {
192067
+ const configDir = join21(ctx.tmpdir, "gh-config");
192068
+ mkdirSync8(configDir, { recursive: true });
192069
+ const command = ["gh", ...params.args.map(shellQuote)].join(" ");
192070
+ const result = await runSandboxed({
192071
+ command,
192072
+ cwd: process.cwd(),
192073
+ timeout: GH_TIMEOUT_MS,
192074
+ env: {
192075
+ ...filterEnvForUntrustedCode(),
192076
+ GH_TOKEN: ctx.ghToken,
192077
+ GH_CONFIG_DIR: configDir,
192078
+ GH_PROMPT_DISABLED: "1",
192079
+ GH_NO_UPDATE_NOTIFIER: "1"
192080
+ }
192081
+ });
192082
+ log2.info(`\xBB gh ${params.args.join(" ")} (exit ${result.exitCode})`);
192083
+ return {
192084
+ output: capOutput(result.output),
192085
+ exit_code: result.exitCode,
192086
+ timed_out: result.timedOut
192087
+ };
192088
+ }, "gh")
192089
+ });
192090
+ }
192091
+
191498
192092
  // mcp/issue.ts
191499
192093
  var Issue = type({
191500
192094
  title: type.string.describe("the title of the issue"),
@@ -191539,65 +192133,6 @@ function IssueTool(ctx) {
191539
192133
  })
191540
192134
  });
191541
192135
  }
191542
- var CloseIssue = type({
191543
- issue_number: type.number.describe("the issue number to close"),
191544
- state_reason: type.enumerated("completed", "not_planned", "duplicate").describe(
191545
- "why the issue is being closed: 'completed' (resolved/done), 'not_planned' (won't fix, out of scope), or 'duplicate' (already tracked elsewhere)."
191546
- )
191547
- });
191548
- function CloseIssueTool(ctx) {
191549
- return tool({
191550
- name: "close_issue",
191551
- mutates: true,
191552
- description: 'Close a GitHub issue with a reason. Example: `close_issue({ issue_number: 1234, state_reason: "not_planned" })`. Comment first to explain the decision \u2014 closing alone is opaque to the author.',
191553
- parameters: CloseIssue,
191554
- execute: execute(async (params) => {
191555
- const result = await ctx.octokit.rest.issues.update({
191556
- owner: ctx.repo.owner,
191557
- repo: ctx.repo.name,
191558
- issue_number: params.issue_number,
191559
- state: "closed",
191560
- state_reason: params.state_reason
191561
- });
191562
- ctx.toolState.wasUpdated = true;
191563
- log2.info(`\xBB closed issue #${params.issue_number} (${params.state_reason})`);
191564
- return {
191565
- success: true,
191566
- number: result.data.number,
191567
- url: result.data.html_url,
191568
- state: result.data.state,
191569
- stateReason: result.data.state_reason
191570
- };
191571
- })
191572
- });
191573
- }
191574
- var ReopenIssue = type({
191575
- issue_number: type.number.describe("the issue number to reopen")
191576
- });
191577
- function ReopenIssueTool(ctx) {
191578
- return tool({
191579
- name: "reopen_issue",
191580
- mutates: true,
191581
- description: "Reopen a previously closed GitHub issue. Example: `reopen_issue({ issue_number: 1234 })`.",
191582
- parameters: ReopenIssue,
191583
- execute: execute(async (params) => {
191584
- const result = await ctx.octokit.rest.issues.update({
191585
- owner: ctx.repo.owner,
191586
- repo: ctx.repo.name,
191587
- issue_number: params.issue_number,
191588
- state: "open"
191589
- });
191590
- ctx.toolState.wasUpdated = true;
191591
- log2.info(`\xBB reopened issue #${params.issue_number}`);
191592
- return {
191593
- success: true,
191594
- number: result.data.number,
191595
- url: result.data.html_url,
191596
- state: result.data.state
191597
- };
191598
- })
191599
- });
191600
- }
191601
192136
 
191602
192137
  // utils/body.ts
191603
192138
  var import_turndown = __toESM(require_turndown_cjs(), 1);
@@ -192094,33 +192629,6 @@ function buildPrBodyWithFooter(ctx, body) {
192094
192629
  const bodyWithoutFooter = stripExistingFooter(fixDoubleEscapedString(body));
192095
192630
  return `${bodyWithoutFooter}${footer}`;
192096
192631
  }
192097
- var ClosePullRequest = type({
192098
- pull_number: type.number.describe("the pull request number to close")
192099
- });
192100
- function ClosePullRequestTool(ctx) {
192101
- return tool({
192102
- name: "close_pull_request",
192103
- mutates: true,
192104
- description: "Close an open pull request WITHOUT merging it. Example: `close_pull_request({ pull_number: 1234 })`. Comment first to explain why \u2014 closing alone is opaque to the author. Merging is a human action and is never done here.",
192105
- parameters: ClosePullRequest,
192106
- execute: execute(async (params) => {
192107
- const result = await ctx.octokit.rest.pulls.update({
192108
- owner: ctx.repo.owner,
192109
- repo: ctx.repo.name,
192110
- pull_number: params.pull_number,
192111
- state: "closed"
192112
- });
192113
- ctx.toolState.wasUpdated = true;
192114
- log2.info(`\xBB closed pull request #${params.pull_number}`);
192115
- return {
192116
- success: true,
192117
- number: result.data.number,
192118
- url: result.data.html_url,
192119
- state: result.data.state
192120
- };
192121
- })
192122
- });
192123
- }
192124
192632
  var UpdatePullRequestBody = type({
192125
192633
  pull_number: type.number.describe("the pull request number to update"),
192126
192634
  body: type.string.describe("the new body content for the pull request")
@@ -192280,8 +192788,8 @@ function PullRequestInfoTool(ctx) {
192280
192788
  }
192281
192789
 
192282
192790
  // mcp/reviewComments.ts
192283
- import { writeFileSync as writeFileSync12 } from "node:fs";
192284
- import { join as join20 } from "node:path";
192791
+ import { writeFileSync as writeFileSync13 } from "node:fs";
192792
+ import { join as join22 } from "node:path";
192285
192793
  var COMMENTS_PER_THREAD = 50;
192286
192794
  var REVIEW_THREADS_QUERY = `
192287
192795
  query ($owner: String!, $name: String!, $prNumber: Int!, $cursor: String) {
@@ -192754,8 +193262,8 @@ function GetReviewCommentsTool(ctx) {
192754
193262
  throw new Error("PULLFROG_TEMP_DIR not set");
192755
193263
  }
192756
193264
  const filename = `review-${params.review_id}-threads.md`;
192757
- const commentsPath = join20(tempDir, filename);
192758
- writeFileSync12(commentsPath, formatted.content);
193265
+ const commentsPath = join22(tempDir, filename);
193266
+ writeFileSync13(commentsPath, formatted.content);
192759
193267
  log2.debug(`wrote ${threadBlocks.length} threads to ${commentsPath}`);
192760
193268
  return {
192761
193269
  review_id: params.review_id,
@@ -193001,394 +193509,6 @@ ${addendum}`,
193001
193509
  });
193002
193510
  }
193003
193511
 
193004
- // mcp/shell.ts
193005
- import { spawn as spawn5, spawnSync as spawnSync6 } from "node:child_process";
193006
- import { randomUUID as randomUUID4 } from "node:crypto";
193007
- import { closeSync, openSync, writeFileSync as writeFileSync13 } from "node:fs";
193008
- import { userInfo as userInfo2 } from "node:os";
193009
- import { join as join21 } from "node:path";
193010
- import { setTimeout as sleep3 } from "node:timers/promises";
193011
- var ShellParams = type({
193012
- command: "string",
193013
- // advisory and unread — no code path consumes it. kept in the schema only
193014
- // because it nudges the model to state intent before running a command;
193015
- // REQUIRING it cost a full rejected turn every time one was omitted. #1140.
193016
- "description?": "string",
193017
- "timeout?": type.number.describe(
193018
- "Timeout in MILLISECONDS (not seconds). Default 30000 (30s), max 120000 (2m). e.g. timeout: 180000 for 3 minutes; timeout: 180 means 180ms and will kill the process almost immediately."
193019
- ),
193020
- "working_directory?": "string",
193021
- "background?": "boolean"
193022
- });
193023
- var detectedSandboxMethod;
193024
- var usernsProbeError;
193025
- function getSandboxMethod() {
193026
- return detectSandboxMethod();
193027
- }
193028
- function detectSandboxMethod() {
193029
- if (detectedSandboxMethod !== void 0) {
193030
- return detectedSandboxMethod;
193031
- }
193032
- if (process.env.CI !== "true") {
193033
- detectedSandboxMethod = "none";
193034
- log2.debug("sandbox disabled (CI !== true)");
193035
- return "none";
193036
- }
193037
- try {
193038
- const result = spawnSync6("unshare", ["--pid", "--fork", "--mount-proc", "true"], {
193039
- timeout: 5e3,
193040
- stdio: "ignore"
193041
- });
193042
- if (result.status === 0) {
193043
- detectedSandboxMethod = "unshare";
193044
- log2.debug("PID namespace isolation enabled (unprivileged unshare)");
193045
- return "unshare";
193046
- }
193047
- } catch {
193048
- }
193049
- try {
193050
- const result = spawnSync6("sudo", ["unshare", "--pid", "--fork", "--mount-proc", "true"], {
193051
- timeout: 5e3,
193052
- stdio: "ignore"
193053
- });
193054
- if (result.status === 0) {
193055
- detectedSandboxMethod = "sudo-unshare";
193056
- log2.debug("PID namespace isolation enabled (sudo unshare)");
193057
- return "sudo-unshare";
193058
- }
193059
- } catch {
193060
- }
193061
- try {
193062
- const result = spawnSync6(
193063
- "unshare",
193064
- [
193065
- "--user",
193066
- "--map-root-user",
193067
- "--pid",
193068
- "--fork",
193069
- "--mount",
193070
- "setpriv",
193071
- "--no-new-privs",
193072
- "--bounding-set",
193073
- "-all",
193074
- "--inh-caps",
193075
- "-all",
193076
- "--ambient-caps",
193077
- "-all",
193078
- "true"
193079
- ],
193080
- { timeout: 5e3, stdio: ["ignore", "ignore", "pipe"] }
193081
- );
193082
- if (result.status === 0) {
193083
- detectedSandboxMethod = "userns-unshare";
193084
- log2.debug("PID namespace isolation enabled (unprivileged userns unshare)");
193085
- return "userns-unshare";
193086
- }
193087
- usernsProbeError = result.stderr?.toString().trim() || result.error?.message;
193088
- } catch {
193089
- }
193090
- detectedSandboxMethod = "none";
193091
- log2.info("PID namespace isolation not available");
193092
- return "none";
193093
- }
193094
- var PROC_CLEANUP = "umount /proc 2>/dev/null; umount /proc 2>/dev/null; mount -t proc proc /proc 2>/dev/null;";
193095
- var SOCKET_CLEANUP = [
193096
- "/var/run/docker.sock",
193097
- "/run/docker.sock",
193098
- "/var/run/podman/podman.sock",
193099
- "/run/podman/podman.sock",
193100
- "/run/containerd/containerd.sock",
193101
- "/var/run/crio/crio.sock"
193102
- ].map((path4) => `mount --bind /dev/null ${path4} 2>/dev/null;`).join(" ");
193103
- function buildFsMounts(repoDir) {
193104
- const escaped = repoDir.replace(/'/g, "'\\''");
193105
- return [
193106
- `mkdir -p /var/lib/pullfrog 2>/dev/null;`,
193107
- `mount -t tmpfs tmpfs /var/lib/pullfrog 2>/dev/null;`,
193108
- `[ -n "$RUNNER_TEMP" ] && [ -d "$RUNNER_TEMP/_runner_file_commands" ] && mount -t tmpfs tmpfs "$RUNNER_TEMP/_runner_file_commands" 2>/dev/null;`,
193109
- `[ -e '${escaped}/.git' ] && mount --bind '${escaped}/.git' '${escaped}/.git' 2>/dev/null && mount -o remount,bind,ro '${escaped}/.git' 2>/dev/null;`
193110
- ].join(" ");
193111
- }
193112
- var _repoRoot;
193113
- function resolveRepoRoot() {
193114
- if (_repoRoot) return _repoRoot;
193115
- const fromEnv = process.env.GITHUB_WORKSPACE;
193116
- if (fromEnv) {
193117
- _repoRoot = fromEnv;
193118
- return _repoRoot;
193119
- }
193120
- try {
193121
- _repoRoot = spawnSync6("git", ["rev-parse", "--show-toplevel"], {
193122
- cwd: process.cwd(),
193123
- stdio: ["ignore", "pipe", "ignore"],
193124
- encoding: "utf-8"
193125
- }).stdout?.trim();
193126
- } catch {
193127
- }
193128
- if (!_repoRoot) _repoRoot = process.cwd();
193129
- return _repoRoot;
193130
- }
193131
- function spawnShell(params) {
193132
- const spawnOpts = { env: params.env, cwd: params.cwd, stdio: params.stdio, detached: true };
193133
- const sandboxMethod = detectSandboxMethod();
193134
- const ci = process.env.CI === "true";
193135
- if (ci && sandboxMethod === "none") {
193136
- throw new Error(
193137
- "PID-namespace isolation is required in CI but unavailable: unprivileged unshare, sudo unshare, and userns-nested unshare all failed. " + (usernsProbeError ? `userns probe said: ${usernsProbeError}. ` : "") + "on a containerized / Kubernetes self-hosted runner the cause is usually one of: the pod's seccomp profile blocks user-namespace creation (Pod Security Standards baseline/restricted); the pod runs as root without CAP_SETFCAP, which the kernel requires to map uid 0; or the node kernel disallows unprivileged user namespaces (Ubuntu >= 23.10, Bottlerocket, Talos). elsewhere, check that `unshare` and `setpriv` (util-linux >= 2.31, which is what added --ambient-caps) are on PATH. see https://docs.pullfrog.com/security#self-hosted-runners"
193138
- );
193139
- }
193140
- const repoRoot = resolveRepoRoot();
193141
- const fsMounts = buildFsMounts(repoRoot);
193142
- if (sandboxMethod === "unshare") {
193143
- return spawn5(
193144
- "unshare",
193145
- [
193146
- "--pid",
193147
- "--fork",
193148
- "--mount-proc",
193149
- "bash",
193150
- "-c",
193151
- `${PROC_CLEANUP} ${SOCKET_CLEANUP} ${fsMounts} ${params.command}`
193152
- ],
193153
- spawnOpts
193154
- );
193155
- }
193156
- if (sandboxMethod === "userns-unshare") {
193157
- const escaped = params.command.replace(/'/g, "'\\''");
193158
- return spawn5(
193159
- "unshare",
193160
- [
193161
- "--user",
193162
- "--map-root-user",
193163
- "--pid",
193164
- "--fork",
193165
- "--mount",
193166
- "bash",
193167
- "-c",
193168
- `${PROC_CLEANUP} ${SOCKET_CLEANUP} ${fsMounts} exec setpriv --no-new-privs --bounding-set -all --inh-caps -all --ambient-caps -all bash -c '${escaped}'`
193169
- ],
193170
- spawnOpts
193171
- );
193172
- }
193173
- if (sandboxMethod === "sudo-unshare") {
193174
- const envArgs = [];
193175
- for (const [k2, v] of Object.entries(params.env)) {
193176
- if (v !== void 0) {
193177
- envArgs.push(`${k2}=${v}`);
193178
- }
193179
- }
193180
- const username = userInfo2().username;
193181
- const pathRestore = 'export PATH="${SANDBOX_PATH:-$PATH}"; ';
193182
- const escaped = (pathRestore + params.command).replace(/'/g, "'\\''");
193183
- envArgs.push(`SANDBOX_PATH=${params.env.PATH ?? ""}`);
193184
- return spawn5(
193185
- "sudo",
193186
- [
193187
- "env",
193188
- ...envArgs,
193189
- "unshare",
193190
- "--pid",
193191
- "--fork",
193192
- "--mount-proc",
193193
- "bash",
193194
- "-c",
193195
- `${PROC_CLEANUP} ${SOCKET_CLEANUP} ${fsMounts} exec su -p -s /bin/bash ${username} -c '${escaped}'`
193196
- ],
193197
- { ...spawnOpts, env: {} }
193198
- );
193199
- }
193200
- return spawn5("bash", ["-c", params.command], spawnOpts);
193201
- }
193202
- async function killProcessGroup(proc) {
193203
- if (!proc.pid) return;
193204
- try {
193205
- process.kill(-proc.pid, "SIGTERM");
193206
- await new Promise((r2) => setTimeout(r2, 200));
193207
- process.kill(-proc.pid, "SIGKILL");
193208
- } catch {
193209
- try {
193210
- proc.kill("SIGKILL");
193211
- } catch {
193212
- }
193213
- }
193214
- }
193215
- function getTempDir() {
193216
- const tempDir = process.env.PULLFROG_TEMP_DIR;
193217
- if (!tempDir) {
193218
- throw new Error("PULLFROG_TEMP_DIR not set");
193219
- }
193220
- return tempDir;
193221
- }
193222
- var MAX_OUTPUT_CHARS = 5e3;
193223
- function capOutput(output) {
193224
- if (output.length <= MAX_OUTPUT_CHARS) return output;
193225
- const fullPath = join21(getTempDir(), `shell-${randomUUID4().slice(0, 8)}.log`);
193226
- writeFileSync13(fullPath, output);
193227
- const elided = output.length - MAX_OUTPUT_CHARS;
193228
- return `... [${elided} chars truncated; full output saved to ${fullPath}] ...
193229
- ${output.slice(-MAX_OUTPUT_CHARS)}`;
193230
- }
193231
- function isGitCommand(command) {
193232
- const trimmed = command.trim();
193233
- if (trimmed === "git" || trimmed.startsWith("git ")) return true;
193234
- if (trimmed.startsWith("sudo git")) return true;
193235
- return /[;&|]\s*(?:sudo\s+)?git(?:\s|$)/.test(trimmed);
193236
- }
193237
- function ShellTool(ctx) {
193238
- return tool({
193239
- name: "shell",
193240
- timeoutMs: 12e4,
193241
- description: `Execute shell commands securely. Environment is filtered to remove API keys and secrets.
193242
-
193243
- Example: \`shell({ command: "pnpm test", description: "run the test suite" })\`.
193244
-
193245
- Use this tool to:
193246
- - Run shell commands (ls, cat, grep, find, etc.)
193247
- - Execute build tools (npm, pnpm, cargo, make, etc.)
193248
- - Run tests and linters
193249
-
193250
- 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.
193251
-
193252
- Do NOT use this tool for git commands \u2014 use the dedicated git tools instead.`,
193253
- parameters: ShellParams,
193254
- execute: execute(async (params) => {
193255
- if (isGitCommand(params.command)) {
193256
- throw new Error(
193257
- 'git commands are not allowed in the shell tool. use the dedicated git tools instead:\n- git: local operations, e.g. git({ command: "log", args: ["--oneline"] }) \u2014 `command` is the subcommand ONLY\n- push_branch: push to remote (handles authentication)\n- git_fetch: fetch from remote (handles authentication)\n- checkout_pr: check out PR branches'
193258
- );
193259
- }
193260
- const timeout = Math.min(params.timeout ?? 3e4, 12e4);
193261
- const cwd = params.working_directory ?? process.cwd();
193262
- const env2 = resolveEnv(ctx.payload.shell === "enabled" ? "inherit" : "restricted");
193263
- if (params.command.includes("agent-browser")) {
193264
- const daemonError = ensureBrowserDaemon(ctx.toolState);
193265
- if (daemonError) {
193266
- return {
193267
- output: `browser daemon unavailable: ${daemonError}`,
193268
- exit_code: 1,
193269
- timed_out: false
193270
- };
193271
- }
193272
- const binDir = ctx.toolState.browserDaemon?.binDir;
193273
- if (binDir) {
193274
- env2.PATH = `${binDir}:${env2.PATH ?? ""}`;
193275
- }
193276
- }
193277
- if (params.background) {
193278
- const tempDir = getTempDir();
193279
- const handle = `bg-${randomUUID4().slice(0, 8)}`;
193280
- const outputPath = join21(tempDir, `${handle}.log`);
193281
- const pidPath = join21(tempDir, `${handle}.pid`);
193282
- const logFd = openSync(outputPath, "a");
193283
- let proc2;
193284
- try {
193285
- proc2 = spawnShell({
193286
- command: params.command,
193287
- env: env2,
193288
- cwd,
193289
- stdio: ["ignore", logFd, logFd]
193290
- });
193291
- } finally {
193292
- closeSync(logFd);
193293
- }
193294
- if (!proc2.pid) {
193295
- throw new Error("failed to start background process");
193296
- }
193297
- proc2.unref();
193298
- writeFileSync13(pidPath, `${proc2.pid}
193299
- `);
193300
- ctx.toolState.backgroundProcesses.set(handle, { pid: proc2.pid, outputPath, pidPath });
193301
- return {
193302
- handle,
193303
- outputPath,
193304
- pidPath,
193305
- message: `started background process ${handle} (pid ${proc2.pid})`
193306
- };
193307
- }
193308
- const proc = spawnShell({
193309
- command: params.command,
193310
- env: env2,
193311
- cwd,
193312
- stdio: ["ignore", "pipe", "pipe"]
193313
- });
193314
- let stdout2 = "", stderr = "", timedOut = false, exited = false;
193315
- proc.stdout?.on("data", (chunk) => {
193316
- stdout2 += chunk.toString();
193317
- });
193318
- proc.stderr?.on("data", (chunk) => {
193319
- stderr += chunk.toString();
193320
- });
193321
- const timeoutId = setTimeout(async () => {
193322
- if (!exited) {
193323
- timedOut = true;
193324
- await killProcessGroup(proc);
193325
- }
193326
- }, timeout);
193327
- const exitCode = await new Promise((resolve3) => {
193328
- const done = (code) => {
193329
- exited = true;
193330
- clearTimeout(timeoutId);
193331
- resolve3(code);
193332
- };
193333
- proc.on("exit", done);
193334
- proc.on("error", () => done(null));
193335
- });
193336
- proc.stdout?.destroy();
193337
- proc.stderr?.destroy();
193338
- let output = stderr ? stdout2 ? `${stdout2}
193339
- ${stderr}` : stderr : stdout2;
193340
- if (timedOut)
193341
- output = output ? `${output}
193342
- [timed out after ${timeout}ms]` : `[timed out after ${timeout}ms]`;
193343
- const finalExitCode = exitCode ?? (timedOut ? 124 : -1);
193344
- const trimmed = output.trim();
193345
- if (finalExitCode !== 0) {
193346
- log2.info(`shell command failed with exit code ${finalExitCode}: ${params.command}`);
193347
- if (trimmed) log2.info(`output: ${trimmed}`);
193348
- }
193349
- return {
193350
- output: capOutput(trimmed),
193351
- exit_code: finalExitCode,
193352
- timed_out: timedOut
193353
- };
193354
- })
193355
- });
193356
- }
193357
- var KillBackgroundParams = type({
193358
- handle: type.string.describe("The handle of the background process to kill (e.g., bg-a1b2c3d4)")
193359
- });
193360
- function KillBackgroundTool(ctx) {
193361
- return tool({
193362
- name: "kill_background",
193363
- mutates: true,
193364
- description: `Kill a background process by its handle. Use this to stop dev servers or other long-running processes started with shell({ background: true }).`,
193365
- parameters: KillBackgroundParams,
193366
- execute: execute(async (params) => {
193367
- const proc = ctx.toolState.backgroundProcesses.get(params.handle);
193368
- if (!proc) {
193369
- return {
193370
- success: false,
193371
- message: `no background process with handle ${params.handle}`
193372
- };
193373
- }
193374
- try {
193375
- process.kill(-proc.pid, "SIGTERM");
193376
- } catch {
193377
- }
193378
- await sleep3(200);
193379
- try {
193380
- process.kill(-proc.pid, "SIGKILL");
193381
- } catch {
193382
- }
193383
- ctx.toolState.backgroundProcesses.delete(params.handle);
193384
- return {
193385
- success: true,
193386
- message: `killed background process ${params.handle} (pid ${proc.pid})`
193387
- };
193388
- })
193389
- });
193390
- }
193391
-
193392
193512
  // mcp/similarIssues.ts
193393
193513
  var SimilarIssues = type({
193394
193514
  issue_number: type.number.describe("The issue number to find older duplicate candidates for")
@@ -193476,8 +193596,8 @@ function UploadFileTool(ctx) {
193476
193596
  }
193477
193597
 
193478
193598
  // mcp/xrepo.ts
193479
- import { mkdirSync as mkdirSync8, rmSync as rmSync3 } from "node:fs";
193480
- import { join as join22 } from "node:path";
193599
+ import { mkdirSync as mkdirSync9, rmSync as rmSync3 } from "node:fs";
193600
+ import { join as join23 } from "node:path";
193481
193601
  function assertValidRepoName(name) {
193482
193602
  if (!/^[A-Za-z0-9._-]+$/.test(name) || name.startsWith("-") || name.includes("..")) {
193483
193603
  throw new Error(
@@ -193557,8 +193677,8 @@ function CheckoutRepoTool(ctx) {
193557
193677
  return { path: existing.dir, access: existing.access, note: "already checked out." };
193558
193678
  }
193559
193679
  const access = accessFor(ctx, repo);
193560
- const dir = join22(ctx.tmpdir, "xrepo", repo);
193561
- mkdirSync8(dir, { recursive: true });
193680
+ const dir = join23(ctx.tmpdir, "xrepo", repo);
193681
+ mkdirSync9(dir, { recursive: true });
193562
193682
  const state = ensureRepoState(ctx.toolState, { owner, name: repo, dir, access });
193563
193683
  const rc = resolveRepoCtx(ctx, repo);
193564
193684
  try {
@@ -193636,8 +193756,8 @@ function buildCommonTools(ctx, outputSchema) {
193636
193756
  EditCommentTool(ctx),
193637
193757
  ReplyToReviewCommentTool(ctx),
193638
193758
  IssueTool(ctx),
193639
- CloseIssueTool(ctx),
193640
- ReopenIssueTool(ctx),
193759
+ CloseCurrentTool(ctx),
193760
+ ReopenCurrentTool(ctx),
193641
193761
  IssueInfoTool(ctx),
193642
193762
  GetIssueCommentsTool(ctx),
193643
193763
  GetIssueEventsTool(ctx),
@@ -193669,6 +193789,9 @@ function buildCommonTools(ctx, outputSchema) {
193669
193789
  tools.push(ShellTool(ctx));
193670
193790
  tools.push(KillBackgroundTool(ctx));
193671
193791
  }
193792
+ if (ctx.ghToken) {
193793
+ tools.push(GhTool(ctx));
193794
+ }
193672
193795
  return tools;
193673
193796
  }
193674
193797
  function buildOrchestratorTools(ctx, outputSchema) {
@@ -193680,8 +193803,7 @@ function buildOrchestratorTools(ctx, outputSchema) {
193680
193803
  PushTagsTool(ctx),
193681
193804
  DeleteBranchTool(ctx),
193682
193805
  CreatePullRequestTool(ctx),
193683
- UpdatePullRequestBodyTool(ctx),
193684
- ClosePullRequestTool(ctx)
193806
+ UpdatePullRequestBodyTool(ctx)
193685
193807
  ];
193686
193808
  if (ctx.signedCommits) {
193687
193809
  tools.push(CommitChangesTool(ctx));
@@ -193771,6 +193893,10 @@ async function startMcpHttpServer(ctx, options) {
193771
193893
  let disposed = false;
193772
193894
  return {
193773
193895
  url: startResult.url,
193896
+ // the names actually registered, handed back so the prompt can be checked
193897
+ // against them rather than against a second derivation. see
193898
+ // assertPromptToolRefs.
193899
+ toolNames: tools.map((registered) => registered.name),
193774
193900
  [Symbol.asyncDispose]: async () => {
193775
193901
  if (disposed) return;
193776
193902
  disposed = true;
@@ -194458,7 +194584,7 @@ async function checkConfiguredCredentials(params) {
194458
194584
  import { randomUUID as randomUUID5 } from "node:crypto";
194459
194585
  import { writeFileSync as writeFileSync14 } from "node:fs";
194460
194586
  import { createServer as createServer3 } from "node:http";
194461
- import { join as join23 } from "node:path";
194587
+ import { join as join24 } from "node:path";
194462
194588
  var REVOKED_TRAP_MS = 6e4;
194463
194589
  function revokeGitHubToken(token) {
194464
194590
  fetch("https://api.github.com/installation/token", {
@@ -194527,7 +194653,7 @@ async function startGitAuthServer(tmpdir4) {
194527
194653
  function writeAskpassScript(code) {
194528
194654
  const scriptId = randomUUID5();
194529
194655
  const scriptName = `askpass-${scriptId}.js`;
194530
- const scriptPath = join23(tmpdir4, scriptName);
194656
+ const scriptPath = join24(tmpdir4, scriptName);
194531
194657
  const content = [
194532
194658
  `#!/usr/bin/env node`,
194533
194659
  `var a=process.argv[2]||"";`,
@@ -194979,7 +195105,7 @@ function resolveInstructions(ctx) {
194979
195105
 
194980
195106
  // utils/learnings.ts
194981
195107
  import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
194982
- import { dirname as dirname4, join as join24 } from "node:path";
195108
+ import { dirname as dirname4, join as join25 } from "node:path";
194983
195109
 
194984
195110
  // utils/learningsTruncate.ts
194985
195111
  var MAX_LEARNINGS_LENGTH = 1e5;
@@ -194997,10 +195123,10 @@ function truncateAtLineBoundary(body, cap) {
194997
195123
  var LEARNINGS_FILE_NAME = "pullfrog-learnings.md";
194998
195124
  var XREPO_LEARNINGS_FILE_NAME = "pullfrog-xrepo-learnings.md";
194999
195125
  function learningsFilePath(tmpdir4) {
195000
- return join24(tmpdir4, LEARNINGS_FILE_NAME);
195126
+ return join25(tmpdir4, LEARNINGS_FILE_NAME);
195001
195127
  }
195002
195128
  function xrepoLearningsFilePath(tmpdir4) {
195003
- return join24(tmpdir4, XREPO_LEARNINGS_FILE_NAME);
195129
+ return join25(tmpdir4, XREPO_LEARNINGS_FILE_NAME);
195004
195130
  }
195005
195131
  async function seedLearningsFile(params) {
195006
195132
  const path4 = learningsFilePath(params.tmpdir);
@@ -195544,6 +195670,9 @@ async function resolveTokens(params) {
195544
195670
  mcpToken: externalToken,
195545
195671
  // external token is whatever scope the user granted — reuse for reads too.
195546
195672
  readToken: params.xrepo ? externalToken : void 0,
195673
+ // the scope is the user's to choose here, but the role threshold still
195674
+ // decides whether the agent gets a CLI credential at all.
195675
+ ghToken: mirrorRolePermissions(params.authorPermission) ? externalToken : void 0,
195547
195676
  async [Symbol.asyncDispose]() {
195548
195677
  mcpTokenValue = void 0;
195549
195678
  }
@@ -195575,6 +195704,15 @@ async function resolveTokens(params) {
195575
195704
  log2.info(
195576
195705
  `\xBB acquired scoped MCP token (${Object.entries(mcpPermissions).map((e) => e.join(":")).join(", ")})`
195577
195706
  );
195707
+ const ghPermissions = mirrorRolePermissions(params.authorPermission);
195708
+ let ghToken;
195709
+ if (ghPermissions) {
195710
+ ghToken = await acquireNewToken({ permissions: ghPermissions });
195711
+ if (isGitHubActions) core7.setSecret(ghToken);
195712
+ log2.info(
195713
+ `\xBB acquired gh token mirroring ${params.authorPermission} (${formatPermissions(ghPermissions)})`
195714
+ );
195715
+ }
195578
195716
  let readToken;
195579
195717
  if (params.xrepo) {
195580
195718
  readToken = await acquireNewToken({
@@ -195664,7 +195802,9 @@ async function resolveTokens(params) {
195664
195802
  await Promise.all([
195665
195803
  revokeGitHubInstallationToken(currentGitToken),
195666
195804
  revokeGitHubInstallationToken(currentMcpToken),
195667
- ...currentReadToken ? [revokeGitHubInstallationToken(currentReadToken)] : []
195805
+ ...currentReadToken ? [revokeGitHubInstallationToken(currentReadToken)] : [],
195806
+ // revoking this one is what bounds a `gh` leak to the run's lifetime
195807
+ ...ghToken ? [revokeGitHubInstallationToken(ghToken)] : []
195668
195808
  ]);
195669
195809
  } finally {
195670
195810
  removeSignalHandler();
@@ -195679,6 +195819,7 @@ async function resolveTokens(params) {
195679
195819
  },
195680
195820
  mcpToken,
195681
195821
  readToken,
195822
+ ghToken,
195682
195823
  refreshGitToken,
195683
195824
  [Symbol.asyncDispose]: dispose
195684
195825
  };
@@ -195920,7 +196061,7 @@ async function runProxyResolution(ctx) {
195920
196061
 
195921
196062
  // utils/prSummary.ts
195922
196063
  import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
195923
- import { dirname as dirname5, join as join25 } from "node:path";
196064
+ import { dirname as dirname5, join as join26 } from "node:path";
195924
196065
  var SUMMARY_FILE_NAME = "pullfrog-summary.md";
195925
196066
  var SUMMARY_SCAFFOLD = `# PR summary
195926
196067
 
@@ -195930,7 +196071,7 @@ var SUMMARY_SCAFFOLD = `# PR summary
195930
196071
  var MIN_SNAPSHOT_LENGTH = 60;
195931
196072
  var MAX_SNAPSHOT_LENGTH = 32768;
195932
196073
  function summaryFilePath(tmpdir4) {
195933
- return join25(tmpdir4, SUMMARY_FILE_NAME);
196074
+ return join26(tmpdir4, SUMMARY_FILE_NAME);
195934
196075
  }
195935
196076
  async function seedSummaryFile(params) {
195936
196077
  const path4 = summaryFilePath(params.tmpdir);
@@ -196152,7 +196293,10 @@ function warnIfPinnedToSha() {
196152
196293
  }
196153
196294
  }
196154
196295
  async function resolveRunContextData(params) {
196155
- log2.info(`\xBB running Pullfrog v${package_default.version}...`);
196296
+ const actionRef = process.env.GITHUB_ACTION_REF;
196297
+ log2.info(
196298
+ `\xBB running Pullfrog v${package_default.version}${actionRef ? ` (ref: ${actionRef})` : ""}...`
196299
+ );
196156
196300
  warnIfPinnedToSha();
196157
196301
  const repoContext = parseRepoContext();
196158
196302
  let oidcToken;
@@ -197242,6 +197386,7 @@ async function main() {
197242
197386
  try {
197243
197387
  const _commentTokenRef = __using(_stack, await resolveTokens({
197244
197388
  push: "disabled",
197389
+ authorPermission: void 0,
197245
197390
  oidc: oidcCredentials
197246
197391
  }), true);
197247
197392
  const errorMessage = runContext.commercialRefused === "subscription_unpaid" ? "Pro renewal failed for this organization" : "Pro plan required for this organization";
@@ -197260,7 +197405,8 @@ async function main() {
197260
197405
  }
197261
197406
  }
197262
197407
  const tmpdir4 = createTempDirectory();
197263
- const preIntrospectionDirty = await dirtyTrackedPaths();
197408
+ const repoDir = payload.cwd;
197409
+ const preIntrospectionDirty = repoDir ? await dirtyTrackedPaths({ cwd: repoDir }) : null;
197264
197410
  const opencodeCliPath = await agents.opencode.install();
197265
197411
  captureBaselineModels(opencodeCliPath);
197266
197412
  if (runContext.dbSecrets) {
@@ -197275,7 +197421,13 @@ async function main() {
197275
197421
  }
197276
197422
  installCodexAuth();
197277
197423
  captureAuthorizedModels(opencodeCliPath);
197278
- await restoreDirtiedSince({ before: preIntrospectionDirty, actor: "model introspection" });
197424
+ if (preIntrospectionDirty && repoDir) {
197425
+ await restoreDirtiedSince({
197426
+ before: preIntrospectionDirty,
197427
+ actor: "model introspection",
197428
+ cwd: repoDir
197429
+ });
197430
+ }
197279
197431
  if (runContext.repoSettings.envAllowlist) {
197280
197432
  setEnvAllowlist(runContext.repoSettings.envAllowlist);
197281
197433
  }
@@ -197287,6 +197439,7 @@ async function main() {
197287
197439
  }
197288
197440
  const tokenRef = __using(_stack3, await resolveTokens({
197289
197441
  push: payload.push,
197442
+ authorPermission: payload.event.authorPermission,
197290
197443
  xrepo: payload.xrepo,
197291
197444
  oidc: oidcCredentials
197292
197445
  }), true);
@@ -197453,6 +197606,7 @@ async function main() {
197453
197606
  },
197454
197607
  refreshGitToken: tokenRef.refreshGitToken,
197455
197608
  readToken: tokenRef.readToken,
197609
+ ghToken: tokenRef.ghToken,
197456
197610
  xrepo: payload.xrepo,
197457
197611
  apiToken: runContext.apiToken,
197458
197612
  modes: modes2,
@@ -197544,6 +197698,11 @@ async function main() {
197544
197698
  xrepoLearningsFilePath: toolState.xrepoLearningsFilePath ?? null,
197545
197699
  xrepoLearningsHeadings: runContext.repoSettings.xrepoLearningsHeadings
197546
197700
  });
197701
+ assertPromptToolRefs({
197702
+ agentId,
197703
+ prompt: instructions.full,
197704
+ toolNames: mcpHttpServer.toolNames
197705
+ });
197547
197706
  const logParts = [
197548
197707
  instructions.eventInstructions ? `EVENT-LEVEL INSTRUCTIONS:
197549
197708
  ${instructions.eventInstructions}` : null,
@@ -197558,7 +197717,7 @@ ${instructions.user}` : null,
197558
197717
  log2.info(instructions.full);
197559
197718
  });
197560
197719
  if (agentId === "opencode") {
197561
- const pluginDir = join26(process.cwd(), ".opencode", "plugin");
197720
+ const pluginDir = join27(process.cwd(), ".opencode", "plugin");
197562
197721
  const hasPlugins = existsSync8(pluginDir) && readdirSync2(pluginDir).some((f) => /\.[jt]sx?$/.test(f));
197563
197722
  if (hasPlugins && toolState.dependencyInstallation?.promise) {
197564
197723
  log2.info(
@@ -198734,7 +198893,7 @@ async function runCli4(input) {
198734
198893
  }
198735
198894
 
198736
198895
  // cli.ts
198737
- var VERSION10 = "0.1.54";
198896
+ var VERSION10 = "0.1.56";
198738
198897
  var bin = basename2(process.argv[1] || "");
198739
198898
  var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
198740
198899
  var rawArgs = process.argv.slice(2);