pullfrog 0.1.57 → 0.1.60

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
@@ -3955,7 +3955,7 @@ var require_util2 = __commonJS({
3955
3955
  "use strict";
3956
3956
  var { redirectStatusSet, referrerPolicySet: referrerPolicyTokens, badPortsSet } = require_constants2();
3957
3957
  var { getGlobalOrigin } = require_global();
3958
- var { performance: performance9 } = __require("perf_hooks");
3958
+ var { performance: performance10 } = __require("perf_hooks");
3959
3959
  var { isBlobLike, toUSVString, ReadableStreamFrom } = require_util();
3960
3960
  var assert3 = __require("assert");
3961
3961
  var { isUint8Array } = __require("util/types");
@@ -4118,7 +4118,7 @@ var require_util2 = __commonJS({
4118
4118
  }
4119
4119
  }
4120
4120
  function coarsenedSharedCurrentTime(crossOriginIsolatedCapability) {
4121
- return performance9.now();
4121
+ return performance10.now();
4122
4122
  }
4123
4123
  function createOpaqueTimingInfo(timingInfo) {
4124
4124
  return {
@@ -20099,14 +20099,14 @@ Support boolean input list: \`true | True | TRUE | false | False | FALSE\``);
20099
20099
  });
20100
20100
  }
20101
20101
  exports.group = group2;
20102
- function saveState3(name, value2) {
20102
+ function saveState4(name, value2) {
20103
20103
  const filePath = process.env["GITHUB_STATE"] || "";
20104
20104
  if (filePath) {
20105
20105
  return (0, file_command_1.issueFileCommand)("STATE", (0, file_command_1.prepareKeyValueMessage)(name, value2));
20106
20106
  }
20107
20107
  (0, command_1.issueCommand)("save-state", { name }, (0, utils_1.toCommandValue)(value2));
20108
20108
  }
20109
- exports.saveState = saveState3;
20109
+ exports.saveState = saveState4;
20110
20110
  function getState2(name) {
20111
20111
  return process.env[`STATE_${name}`] || "";
20112
20112
  }
@@ -27894,11 +27894,11 @@ var require_mode = __commonJS({
27894
27894
  var require_isexe = __commonJS({
27895
27895
  "node_modules/.pnpm/isexe@2.0.0/node_modules/isexe/index.js"(exports, module) {
27896
27896
  var fs5 = __require("fs");
27897
- var core11;
27897
+ var core12;
27898
27898
  if (process.platform === "win32" || global.TESTING_WINDOWS) {
27899
- core11 = require_windows();
27899
+ core12 = require_windows();
27900
27900
  } else {
27901
- core11 = require_mode();
27901
+ core12 = require_mode();
27902
27902
  }
27903
27903
  module.exports = isexe;
27904
27904
  isexe.sync = sync;
@@ -27921,7 +27921,7 @@ var require_isexe = __commonJS({
27921
27921
  });
27922
27922
  });
27923
27923
  }
27924
- core11(path4, options || {}, function(er, is) {
27924
+ core12(path4, options || {}, function(er, is) {
27925
27925
  if (er) {
27926
27926
  if (er.code === "EACCES" || options && options.ignoreErrors) {
27927
27927
  er = null;
@@ -27933,7 +27933,7 @@ var require_isexe = __commonJS({
27933
27933
  }
27934
27934
  function sync(path4, options) {
27935
27935
  try {
27936
- return core11.sync(path4, options || {});
27936
+ return core12.sync(path4, options || {});
27937
27937
  } catch (er) {
27938
27938
  if (options && options.ignoreErrors || er.code === "EACCES") {
27939
27939
  return false;
@@ -33299,7 +33299,7 @@ var require_util9 = __commonJS({
33299
33299
  var { redirectStatusSet, referrerPolicyTokens, badPortsSet } = require_constants9();
33300
33300
  var { getGlobalOrigin } = require_global3();
33301
33301
  var { collectAnHTTPQuotedString, parseMIMEType } = require_data_url();
33302
- var { performance: performance9 } = __require("node:perf_hooks");
33302
+ var { performance: performance10 } = __require("node:perf_hooks");
33303
33303
  var { ReadableStreamFrom, isValidHTTPToken, normalizedMethodRecordsBase } = require_util8();
33304
33304
  var assert3 = __require("node:assert");
33305
33305
  var { isUint8Array } = __require("node:util/types");
@@ -33454,7 +33454,7 @@ var require_util9 = __commonJS({
33454
33454
  };
33455
33455
  }
33456
33456
  function coarsenedSharedCurrentTime(crossOriginIsolatedCapability) {
33457
- return coarsenTime(performance9.now(), crossOriginIsolatedCapability);
33457
+ return coarsenTime(performance10.now(), crossOriginIsolatedCapability);
33458
33458
  }
33459
33459
  function createOpaqueTimingInfo(timingInfo) {
33460
33460
  return {
@@ -76726,7 +76726,7 @@ var require_core3 = __commonJS({
76726
76726
  Object.defineProperty(exports, "__esModule", { value: true });
76727
76727
  var id_1 = require_id();
76728
76728
  var ref_1 = require_ref();
76729
- var core11 = [
76729
+ var core12 = [
76730
76730
  "$schema",
76731
76731
  "$id",
76732
76732
  "$defs",
@@ -76736,7 +76736,7 @@ var require_core3 = __commonJS({
76736
76736
  id_1.default,
76737
76737
  ref_1.default
76738
76738
  ];
76739
- exports.default = core11;
76739
+ exports.default = core12;
76740
76740
  }
76741
76741
  });
76742
76742
 
@@ -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 join28(output, replacement);
98610
+ return join29(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 = join28(output, rule.append(self2.options));
98617
+ output = join29(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 join28(output, replacement) {
98629
+ function join29(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);
@@ -105306,14 +105306,14 @@ async function runClaudeAuth() {
105306
105306
  }
105307
105307
 
105308
105308
  // commands/gha.ts
105309
- var core10 = __toESM(require_core(), 1);
105309
+ var core11 = __toESM(require_core(), 1);
105310
105310
  var import_arg2 = __toESM(require_arg(), 1);
105311
105311
  import { dirname as dirname6 } from "node:path";
105312
105312
 
105313
105313
  // main.ts
105314
- import { existsSync as existsSync8, readdirSync as readdirSync2 } from "node:fs";
105314
+ import { existsSync as existsSync9, readdirSync as readdirSync2 } from "node:fs";
105315
105315
  import { readFile as readFile5 } from "node:fs/promises";
105316
- import { join as join27 } from "node:path";
105316
+ import { join as join28 } 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 = []) {
@@ -106186,6 +106186,22 @@ var providers = {
106186
106186
  }
106187
106187
  }
106188
106188
  }),
106189
+ azure: provider({
106190
+ displayName: "Azure OpenAI",
106191
+ // the resource name is half the endpoint URL and the deployment is the model
106192
+ // id, so both are as load-bearing as the key. only the key is sensitive.
106193
+ envVars: ["AZURE_API_KEY", "AZURE_RESOURCE_NAME", "AZURE_DEPLOYMENT"],
106194
+ models: {
106195
+ // single routing entry — the real model is the customer's deployment name,
106196
+ // read from AZURE_DEPLOYMENT at run time. see ModelRouting docs for why
106197
+ // Azure can't be cataloged even though models.dev lists it.
106198
+ byok: {
106199
+ displayName: "Azure OpenAI",
106200
+ resolve: "azure",
106201
+ routing: "azure"
106202
+ }
106203
+ }
106204
+ }),
106189
106205
  "openai-compatible": provider({
106190
106206
  // "Custom" is the picker group, "OpenAI-compatible" the entry under it, so the
106191
106207
  // menu reads `Custom › OpenAI-compatible` and a second custom backend (a
@@ -106411,6 +106427,11 @@ function getModelEnvVars(slug2) {
106411
106427
  }
106412
106428
  return providerConfig.envVars.slice();
106413
106429
  }
106430
+ function getModelManagedCredentials(slug2) {
106431
+ const parsed2 = parseModel(slug2);
106432
+ const providerConfig = providers[parsed2.provider];
106433
+ return providerConfig?.managedCredentials?.slice() ?? [];
106434
+ }
106414
106435
  var modelAliases = Object.entries(providers).flatMap(
106415
106436
  ([providerKey, config3]) => Object.entries(config3.models).map(([modelId, def]) => ({
106416
106437
  slug: `${providerKey}/${modelId}`,
@@ -106508,6 +106529,13 @@ function isOssAllowedModel(slug2) {
106508
106529
  }
106509
106530
  var BEDROCK_MODEL_ID_ENV = "BEDROCK_MODEL_ID";
106510
106531
  var VERTEX_MODEL_ID_ENV = "VERTEX_MODEL_ID";
106532
+ var AZURE_PROVIDER = "azure";
106533
+ var AZURE_RESOURCE_NAME_ENV = "AZURE_RESOURCE_NAME";
106534
+ var AZURE_API_KEY_ENV = "AZURE_API_KEY";
106535
+ var AZURE_DEPLOYMENT_ENV = "AZURE_DEPLOYMENT";
106536
+ var AZURE_CONTEXT_ENV = "AZURE_CONTEXT";
106537
+ var AZURE_MAX_OUTPUT_ENV = "AZURE_MAX_OUTPUT";
106538
+ var AZURE_USE_CHAT_COMPLETIONS_ENV = "AZURE_USE_CHAT_COMPLETIONS";
106511
106539
  var OPENAI_COMPATIBLE_PROVIDER = "openai-compatible";
106512
106540
  var OPENAI_COMPATIBLE_BASE_URL_ENV = "OPENAI_COMPATIBLE_BASE_URL";
106513
106541
  var OPENAI_COMPATIBLE_API_KEY_ENV = "OPENAI_COMPATIBLE_API_KEY";
@@ -106527,6 +106555,8 @@ function formatMcpToolRef(agentId, toolName) {
106527
106555
  switch (agentId) {
106528
106556
  case "claude":
106529
106557
  return `mcp__${pullfrogMcpName}__${toolName}`;
106558
+ case "codex":
106559
+ return `${pullfrogMcpName}__${toolName}`;
106530
106560
  case "opencode":
106531
106561
  return `${pullfrogMcpName}_${toolName}`;
106532
106562
  default:
@@ -106557,6 +106587,11 @@ var DEFAULT_ACTIVITY_TIMEOUT_MS = 3e5;
106557
106587
  var AGENT_ACTIVITY_TIMEOUT_MS = 9e5;
106558
106588
  var AGENT_FIRST_EVENT_TIMEOUT_MS = 12e4;
106559
106589
  var DEFAULT_ACTIVITY_CHECK_INTERVAL_MS = 5e3;
106590
+ function watchdogBudgetMs(compiled, envVar) {
106591
+ const raw2 = Number(process.env[envVar]);
106592
+ if (!Number.isFinite(raw2) || raw2 <= 0) return compiled;
106593
+ return Math.min(compiled, raw2);
106594
+ }
106560
106595
  var DEBUG_TS_PREFIX = /^(?:\[\d{4}-\d{2}-\d{2}T[^\]]+\]\s+)?/.source;
106561
106596
  var ACTIVITY_NOISE_PATTERNS = [
106562
106597
  new RegExp(`${DEBUG_TS_PREFIX}\\[mcp-proxy\\]`),
@@ -107028,7 +107063,8 @@ import { pipeline } from "node:stream/promises";
107028
107063
  async function installFromNpmTarball(params) {
107029
107064
  const tempDir = process.env.PULLFROG_TEMP_DIR;
107030
107065
  if (!tempDir) throw new Error("PULLFROG_TEMP_DIR is not set");
107031
- const extractedDir = join2(tempDir, "package");
107066
+ const packageRoot = join2(tempDir, `npm-${params.packageName.replace(/[^a-z0-9]+/gi, "-")}`);
107067
+ const extractedDir = join2(packageRoot, "package");
107032
107068
  const cliPath = join2(extractedDir, params.executablePath);
107033
107069
  if (existsSync2(cliPath)) {
107034
107070
  log2.debug(`\xBB using cached binary at ${cliPath}`);
@@ -107054,7 +107090,8 @@ async function installFromNpmTarball(params) {
107054
107090
  }
107055
107091
  }
107056
107092
  log2.debug(`\xBB installing ${params.packageName}@${resolvedVersion}...`);
107057
- const tarballPath = join2(tempDir, "package.tgz");
107093
+ mkdirSync(packageRoot, { recursive: true });
107094
+ const tarballPath = join2(packageRoot, "package.tgz");
107058
107095
  const npmRegistry = process.env.NPM_REGISTRY || "https://registry.npmjs.org";
107059
107096
  let tarballUrl;
107060
107097
  if (params.packageName.startsWith("@")) {
@@ -107074,7 +107111,7 @@ async function installFromNpmTarball(params) {
107074
107111
  await pipeline(response.body, fileStream);
107075
107112
  log2.debug(`\xBB downloaded tarball to ${tarballPath}`);
107076
107113
  log2.debug(`\xBB extracting tarball...`);
107077
- const extractResult = spawnSync("tar", ["-xzf", tarballPath, "-C", tempDir], {
107114
+ const extractResult = spawnSync("tar", ["-xzf", tarballPath, "-C", packageRoot], {
107078
107115
  stdio: "pipe",
107079
107116
  encoding: "utf-8"
107080
107117
  });
@@ -107083,9 +107120,6 @@ async function installFromNpmTarball(params) {
107083
107120
  `Failed to extract tarball: ${extractResult.stderr || extractResult.stdout || "Unknown error"}`
107084
107121
  );
107085
107122
  }
107086
- if (!existsSync2(cliPath)) {
107087
- throw new Error(`Executable not found in extracted package at ${cliPath}`);
107088
- }
107089
107123
  if (params.installDependencies) {
107090
107124
  log2.debug(`\xBB installing dependencies for ${params.packageName}...`);
107091
107125
  const installResult = spawnSync("npm", ["install", "--production"], {
@@ -107106,6 +107140,9 @@ async function installFromNpmTarball(params) {
107106
107140
  }
107107
107141
  log2.debug(`\xBB dependencies installed`);
107108
107142
  }
107143
+ if (!existsSync2(cliPath)) {
107144
+ throw new Error(`Executable not found in extracted package at ${cliPath}`);
107145
+ }
107109
107146
  chmodSync(cliPath, 493);
107110
107147
  log2.debug(`\xBB ${params.packageName} installed at ${cliPath}`);
107111
107148
  return cliPath;
@@ -107279,6 +107316,7 @@ function resolveRunAlias(ctx) {
107279
107316
  }
107280
107317
  const model = ctx.resolvedModel;
107281
107318
  if (!model) return void 0;
107319
+ if (model.startsWith(`${AZURE_PROVIDER}/`)) return void 0;
107282
107320
  return modelAliases.find((a2) => !a2.fallback && a2.resolve === model) ?? matchHostedAnthropicAlias(model);
107283
107321
  }
107284
107322
  function ossEffortFloor(ctx) {
@@ -107317,7 +107355,7 @@ var import_semver = __toESM(require_semver2(), 1);
107317
107355
  // package.json
107318
107356
  var package_default = {
107319
107357
  name: "pullfrog",
107320
- version: "0.1.57",
107358
+ version: "0.1.60",
107321
107359
  type: "module",
107322
107360
  bin: {
107323
107361
  pullfrog: "dist/cli.mjs",
@@ -107349,6 +107387,8 @@ var package_default = {
107349
107387
  "@clack/prompts": "^1.2.0",
107350
107388
  "@modelcontextprotocol/sdk": "^1.26.0",
107351
107389
  "@opencode-ai/sdk": "1.18.5",
107390
+ "@openai/codex": "0.147.0",
107391
+ "@openai/codex-sdk": "0.147.0",
107352
107392
  "@octokit/plugin-throttling": "^11.0.3",
107353
107393
  "@octokit/rest": "^22.0.0",
107354
107394
  "@octokit/webhooks-types": "^7.6.1",
@@ -109424,7 +109464,7 @@ var claude = agent({
109424
109464
  if ((isBedrockRoute || isVertexRoute2) && specifier && effort.alias?.effort) {
109425
109465
  applyHostedEffortCapabilities({ env: env2, modelId: specifier, levels: effort.alias.effort });
109426
109466
  }
109427
- if (env2.CLAUDE_CODE_OAUTH_TOKEN && !isBedrockRoute && env2.ANTHROPIC_API_KEY) {
109467
+ if (env2.CLAUDE_CODE_OAUTH_TOKEN && !isBedrockRoute && !isVertexRoute2 && !env2.ANTHROPIC_BASE_URL && !env2.ANTHROPIC_AUTH_TOKEN && env2.ANTHROPIC_API_KEY) {
109428
109468
  const preflight = await preflightClaudeSubscription({
109429
109469
  token: env2.CLAUDE_CODE_OAUTH_TOKEN,
109430
109470
  model
@@ -109470,13 +109510,735 @@ var claude = agent({
109470
109510
  }
109471
109511
  });
109472
109512
 
109473
- // agents/opencode.ts
109513
+ // agents/codex.ts
109474
109514
  var core2 = __toESM(require_core(), 1);
109475
- import { spawn as nodeSpawn2 } from "node:child_process";
109476
- import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "node:fs";
109515
+ import { existsSync as existsSync4, mkdirSync as mkdirSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync6 } from "node:fs";
109477
109516
  import { join as join7 } from "node:path";
109478
109517
  import { performance as performance6 } from "node:perf_hooks";
109479
109518
 
109519
+ // utils/codexHome.ts
109520
+ import { execFileSync as execFileSync4 } from "node:child_process";
109521
+ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "node:fs";
109522
+ import { homedir as homedir2, userInfo } from "node:os";
109523
+ import { join as join6 } from "node:path";
109524
+ var CODEX_AUTH_ENV = "CODEX_AUTH_JSON";
109525
+ var PULLFROG_DATA_DIR = "/var/lib/pullfrog";
109526
+ function isRejectedChain(body) {
109527
+ if (!body.refresh_rejected_at) return false;
109528
+ log2.warning(
109529
+ `\xBB ${CODEX_AUTH_ENV} was rejected by OpenAI at ${body.refresh_rejected_at} and cannot be refreshed \u2014 re-run \`npx pullfrog auth codex\`. skipping it for this run.`
109530
+ );
109531
+ return true;
109532
+ }
109533
+ function installCodexAuth() {
109534
+ const raw2 = process.env[CODEX_AUTH_ENV];
109535
+ if (!raw2) return null;
109536
+ const body = parseCodexAuthBody(raw2);
109537
+ if (!body) {
109538
+ log2.warning(`\xBB ${CODEX_AUTH_ENV} present but malformed; ignoring`);
109539
+ return null;
109540
+ }
109541
+ if (isRejectedChain(body)) return null;
109542
+ const expiresMs = decodeJwtExpMs(body.tokens.access_token) ?? 0;
109543
+ const xdgDataHome = resolveDataHome();
109544
+ const opencodeDir = join6(xdgDataHome, "opencode");
109545
+ const authPath = join6(opencodeDir, "auth.json");
109546
+ const opencodeAuth = {
109547
+ openai: {
109548
+ type: "oauth",
109549
+ refresh: body.tokens.refresh_token,
109550
+ access: body.tokens.access_token,
109551
+ expires: expiresMs,
109552
+ ...body.tokens.account_id ? { accountId: body.tokens.account_id } : {}
109553
+ }
109554
+ };
109555
+ mkdirSync5(opencodeDir, { recursive: true });
109556
+ writeFileSync5(authPath, `${JSON.stringify(opencodeAuth, null, 2)}
109557
+ `, { mode: 384 });
109558
+ process.env.XDG_DATA_HOME = xdgDataHome;
109559
+ log2.info(`\xBB installed Codex auth at ${authPath}`);
109560
+ return {
109561
+ authPath,
109562
+ xdgDataHome,
109563
+ originalRefresh: body.tokens.refresh_token,
109564
+ originalIdToken: body.tokens.id_token
109565
+ };
109566
+ }
109567
+ function installCodexHome() {
109568
+ const raw2 = process.env[CODEX_AUTH_ENV];
109569
+ if (!raw2) return null;
109570
+ const body = parseCodexAuthBody(raw2);
109571
+ if (!body) {
109572
+ log2.warning(`\xBB ${CODEX_AUTH_ENV} present but malformed; ignoring`);
109573
+ return null;
109574
+ }
109575
+ if (isRejectedChain(body)) return null;
109576
+ if (!body.tokens.id_token) {
109577
+ log2.warning(
109578
+ `\xBB ${CODEX_AUTH_ENV} carries no id_token \u2014 the codex CLI cannot load it. re-run \`npx pullfrog auth codex\` to mint a complete credential.`
109579
+ );
109580
+ return null;
109581
+ }
109582
+ const codexHome = join6(resolveDataHome(), "codex");
109583
+ const authPath = join6(codexHome, "auth.json");
109584
+ mkdirSync5(codexHome, { recursive: true });
109585
+ writeFileSync5(authPath, stringifyCodexAuthBody(body), { mode: 384 });
109586
+ log2.info(`\xBB installed Codex auth at ${authPath}`);
109587
+ return { codexHome, authPath, originalRefresh: body.tokens.refresh_token };
109588
+ }
109589
+ function resolveDataHome() {
109590
+ if (process.env.CI !== "true") return join6(homedir2(), ".local", "share");
109591
+ bootstrapPullfrogDataDir();
109592
+ return PULLFROG_DATA_DIR;
109593
+ }
109594
+ function bootstrapPullfrogDataDir() {
109595
+ const user = userInfo().username;
109596
+ let primaryGroup;
109597
+ try {
109598
+ primaryGroup = execFileSync4("id", ["-gn", user], { stdio: "pipe", encoding: "utf-8" }).trim();
109599
+ } catch {
109600
+ primaryGroup = user;
109601
+ }
109602
+ try {
109603
+ execFileSync4("sudo", ["-n", "mkdir", "-p", PULLFROG_DATA_DIR], { stdio: "pipe" });
109604
+ execFileSync4("sudo", ["-n", "chown", `${user}:${primaryGroup}`, PULLFROG_DATA_DIR], {
109605
+ stdio: "pipe"
109606
+ });
109607
+ execFileSync4("sudo", ["-n", "chmod", "700", PULLFROG_DATA_DIR], { stdio: "pipe" });
109608
+ } catch (err) {
109609
+ throw new Error(
109610
+ `failed to bootstrap ${PULLFROG_DATA_DIR} (required for codex auth in CI): ${err instanceof Error ? err.message : String(err)}. the MCP shell's mount-namespace sandbox cannot protect the auth file when it lives under $HOME, and silently falling back would contradict the "three independent layers" claim in wiki/codex-auth.md. passwordless sudo is required for codex auth on this runner \u2014 either configure it, or remove CODEX_AUTH_JSON from the run.`
109611
+ );
109612
+ }
109613
+ }
109614
+
109615
+ // utils/secrets.ts
109616
+ var SENSITIVE_PATTERNS = [
109617
+ /_KEY$/i,
109618
+ /_SECRET$/i,
109619
+ /_TOKEN$/i,
109620
+ /_PASSWORD$/i,
109621
+ /_CREDENTIAL$/i
109622
+ ];
109623
+ function isSensitiveEnvName(key) {
109624
+ return SENSITIVE_PATTERNS.some((p) => p.test(key));
109625
+ }
109626
+ var SAFE_ENV_PREFIXES = ["GITHUB_", "RUNNER_", "JAVA_HOME_", "GOROOT_"];
109627
+ var SAFE_ENV_NAMES = /* @__PURE__ */ new Set([
109628
+ // system
109629
+ "CI",
109630
+ "HOME",
109631
+ "LANG",
109632
+ "LOGNAME",
109633
+ "PATH",
109634
+ "SHELL",
109635
+ "SHLVL",
109636
+ "TERM",
109637
+ "TMPDIR",
109638
+ "TZ",
109639
+ "USER",
109640
+ "XDG_CONFIG_HOME",
109641
+ "XDG_RUNTIME_DIR",
109642
+ "DEBIAN_FRONTEND",
109643
+ // runner image toolchain
109644
+ "ACCEPT_EULA",
109645
+ "AGENT_TOOLSDIRECTORY",
109646
+ "ANDROID_HOME",
109647
+ "ANDROID_NDK",
109648
+ "ANDROID_NDK_HOME",
109649
+ "ANDROID_NDK_LATEST_HOME",
109650
+ "ANDROID_NDK_ROOT",
109651
+ "ANDROID_SDK_ROOT",
109652
+ "ANT_HOME",
109653
+ "AZURE_EXTENSION_DIR",
109654
+ "BOOTSTRAP_HASKELL_NONINTERACTIVE",
109655
+ "CHROME_BIN",
109656
+ "CHROMEWEBDRIVER",
109657
+ "CONDA",
109658
+ "DOTNET_MULTILEVEL_LOOKUP",
109659
+ "DOTNET_NOLOGO",
109660
+ "DOTNET_SKIP_FIRST_TIME_EXPERIENCE",
109661
+ "EDGEWEBDRIVER",
109662
+ "GECKOWEBDRIVER",
109663
+ "GHCUP_INSTALL_BASE_PREFIX",
109664
+ "GRADLE_HOME",
109665
+ "JAVA_HOME",
109666
+ "HOMEBREW_CLEANUP_PERIODIC_FULL_DAYS",
109667
+ "HOMEBREW_NO_AUTO_UPDATE",
109668
+ "ImageOS",
109669
+ "ImageVersion",
109670
+ "NVM_DIR",
109671
+ "PIPX_BIN_DIR",
109672
+ "PIPX_HOME",
109673
+ "PSModulePath",
109674
+ "SELENIUM_JAR_PATH",
109675
+ "SGX_AESM_ADDR",
109676
+ "SWIFT_PATH",
109677
+ "VCPKG_INSTALLATION_ROOT"
109678
+ ]);
109679
+ var _userAllowlist = null;
109680
+ function setEnvAllowlist(raw2) {
109681
+ const names = raw2.split("\n").map((line) => line.trim()).filter(Boolean);
109682
+ _userAllowlist = new Set(names);
109683
+ }
109684
+ function isSafeEnvVar(key) {
109685
+ if (SAFE_ENV_NAMES.has(key)) return true;
109686
+ return SAFE_ENV_PREFIXES.some((p) => key.startsWith(p));
109687
+ }
109688
+ var WORKFLOW_COMMAND_ENV_NAMES = [
109689
+ "GITHUB_ENV",
109690
+ "GITHUB_PATH",
109691
+ "GITHUB_OUTPUT",
109692
+ "GITHUB_STATE",
109693
+ "GITHUB_STEP_SUMMARY",
109694
+ // dropping the five paths above but keeping RUNNER_TEMP would be theatre:
109695
+ // the file-command files live in `$RUNNER_TEMP/_runner_file_commands/`, so the
109696
+ // directory is one `ls` away. nothing in the install path reads it (only our
109697
+ // own leak-surface wipe in `setup.ts`, which runs in the parent), and package
109698
+ // managers use TMPDIR.
109699
+ "RUNNER_TEMP"
109700
+ ];
109701
+ function filterEnvForUntrustedCode() {
109702
+ const env2 = filterEnv();
109703
+ for (const name of WORKFLOW_COMMAND_ENV_NAMES) delete env2[name];
109704
+ return env2;
109705
+ }
109706
+ function filterEnv() {
109707
+ const filtered = {};
109708
+ for (const [key, value2] of Object.entries(process.env)) {
109709
+ if (value2 === void 0) continue;
109710
+ const userAllowed = _userAllowlist?.has(key) ?? false;
109711
+ if (isSensitiveEnvName(key) && !userAllowed) continue;
109712
+ if (isSafeEnvVar(key) || userAllowed) {
109713
+ filtered[key] = value2;
109714
+ }
109715
+ }
109716
+ return filtered;
109717
+ }
109718
+ function resolveEnv(mode) {
109719
+ if (mode === "inherit") {
109720
+ return process.env;
109721
+ }
109722
+ if (mode === "restricted" || mode === void 0) {
109723
+ return filterEnv();
109724
+ }
109725
+ return { ...filterEnv(), ...mode };
109726
+ }
109727
+
109728
+ // agents/codex.ts
109729
+ var CODEX_TARGETS = {
109730
+ "linux-x64": "x86_64-unknown-linux-musl",
109731
+ "linux-arm64": "aarch64-unknown-linux-musl",
109732
+ "darwin-x64": "x86_64-apple-darwin",
109733
+ "darwin-arm64": "aarch64-apple-darwin",
109734
+ "win32-x64": "x86_64-pc-windows-msvc",
109735
+ "win32-arm64": "aarch64-pc-windows-msvc"
109736
+ };
109737
+ async function installCodexCli() {
109738
+ const target = CODEX_TARGETS[`${process.platform}-${process.arch}`];
109739
+ if (!target) {
109740
+ throw new Error(`the codex CLI ships no binary for ${process.platform}-${process.arch}`);
109741
+ }
109742
+ const binary = process.platform === "win32" ? "codex.exe" : "codex";
109743
+ return await installFromNpmTarball({
109744
+ packageName: "@openai/codex",
109745
+ version: getDevDependencyVersion("@openai/codex"),
109746
+ executablePath: `node_modules/@openai/codex-${process.platform}-${process.arch}/vendor/${target}/bin/${binary}`,
109747
+ installDependencies: true
109748
+ });
109749
+ }
109750
+ var CODEX_DISABLED_FEATURES = [
109751
+ // browser / desktop control — network reach and arbitrary JS
109752
+ "browser_use",
109753
+ "browser_use_external",
109754
+ "browser_use_full_cdp_access",
109755
+ "computer_use",
109756
+ "in_app_browser",
109757
+ // third-party code loaded into the agent process
109758
+ "plugins",
109759
+ "remote_plugin",
109760
+ "plugin_sharing",
109761
+ "skill_mcp_dependency_install",
109762
+ // lifecycle hooks run commands sourced from repo config, which a PR can edit
109763
+ "hooks",
109764
+ // connectors
109765
+ "apps",
109766
+ // ungated subagent fan-out (see the file header)
109767
+ "multi_agent",
109768
+ "multi_agent_v2"
109769
+ ];
109770
+ var CODEX_SHELL_FEATURES = ["shell_tool", "unified_exec"];
109771
+ function tomlBool(entries, value2) {
109772
+ return entries.map((name) => `${name} = ${value2}`).join("\n");
109773
+ }
109774
+ function securityOverrideFlags(params) {
109775
+ const shellEnabled = params.ctx.payload.shell === "enabled";
109776
+ const features = [
109777
+ ...CODEX_DISABLED_FEATURES.map((name) => `${name}=false`),
109778
+ ...CODEX_SHELL_FEATURES.map((name) => `${name}=${shellEnabled}`)
109779
+ ];
109780
+ return [
109781
+ "-c",
109782
+ `approval_policy=${JSON.stringify("never")}`,
109783
+ "-c",
109784
+ `sandbox_mode=${JSON.stringify(params.sandboxMode)}`,
109785
+ // the sandbox's EXTENT has to be locked too, not just its mode: a repo
109786
+ // setting `writable_roots = ["/"]` walked straight out of the checkout with
109787
+ // `sandbox_mode` already flag-locked (measured — apply_patch wrote a file
109788
+ // under $HOME).
109789
+ "-c",
109790
+ `sandbox_workspace_write.writable_roots=[${JSON.stringify(params.repoDir)}]`,
109791
+ "-c",
109792
+ "sandbox_workspace_write.network_access=true",
109793
+ // our own MCP server, for a third time in the same class: a repo pointing
109794
+ // `mcp_servers.pullfrog.url` at its own endpoint (measured — `codex mcp get`
109795
+ // reported the repo's URL) turns every pullfrog tool call into an
109796
+ // exfiltration channel, carrying diffs, file contents and cross-repo
109797
+ // learnings to whoever it names.
109798
+ "-c",
109799
+ `mcp_servers.${pullfrogMcpName}.url=${JSON.stringify(params.mcpServerUrl)}`,
109800
+ "-c",
109801
+ `mcp_servers.${pullfrogMcpName}.${APPROVAL_MODE_KEY}=${JSON.stringify("approve")}`,
109802
+ "-c",
109803
+ `mcp_servers.${pullfrogMcpName}.required=true`,
109804
+ "-c",
109805
+ `mcp_servers.${pullfrogMcpName}.tool_timeout_sec=660`,
109806
+ // `enabled` closes the silent-DoS variant: a repo setting it false would
109807
+ // otherwise strip every GitHub tool and leave the agent narrating failure.
109808
+ "-c",
109809
+ `mcp_servers.${pullfrogMcpName}.enabled=true`,
109810
+ // not a boundary, but the same override: a repo silently re-pointing the
109811
+ // model spends the account's money on a pick it never made, and makes the
109812
+ // "Using `…`" run footer a lie about what actually ran.
109813
+ ...params.model ? ["-c", `model=${JSON.stringify(params.model)}`] : [],
109814
+ ...params.effortRung ? ["-c", `model_reasoning_effort=${JSON.stringify(params.effortRung)}`] : [],
109815
+ ...features.flatMap((entry) => ["-c", `features.${entry}`])
109816
+ ];
109817
+ }
109818
+ var CODEX_EFFORTS = ["minimal", "low", "medium", "high", "xhigh"];
109819
+ function resolveCodexModel(ctx) {
109820
+ const specifier = ctx.payload.proxyModel ?? ctx.resolvedModel;
109821
+ if (!specifier) return void 0;
109822
+ try {
109823
+ if (getModelProvider(specifier) === "openai") return stripProviderPrefix(specifier);
109824
+ } catch {
109825
+ }
109826
+ log2.warning(`\xBB ${specifier} isn't an OpenAI model \u2014 running codex on its own default instead`);
109827
+ return void 0;
109828
+ }
109829
+ var APPROVAL_MODE_KEY = "default_tools_approval_mode";
109830
+ var APPROVE_TOOLS = `${APPROVAL_MODE_KEY} = "approve"`;
109831
+ function repoMcpApprovals(repoDir) {
109832
+ const path4 = join7(repoDir, ".codex", "config.toml");
109833
+ if (!existsSync4(path4)) return [];
109834
+ const out = [];
109835
+ let section = [];
109836
+ let isServerTable = false;
109837
+ function flush() {
109838
+ if (section.length === 0) return;
109839
+ const declared = section.some((line) => line.trimStart().startsWith(APPROVAL_MODE_KEY));
109840
+ if (isServerTable && !declared) section.splice(1, 0, APPROVE_TOOLS);
109841
+ out.push(...section, "");
109842
+ section = [];
109843
+ }
109844
+ for (const line of readFileSync3(path4, "utf8").split("\n")) {
109845
+ const header = line.trim().match(/^\[(?<key>[^\]]+)\]$/);
109846
+ if (!header?.groups) {
109847
+ if (section.length > 0) section.push(line);
109848
+ continue;
109849
+ }
109850
+ flush();
109851
+ const key = header.groups.key.trim();
109852
+ if (!key.startsWith("mcp_servers.")) continue;
109853
+ const name = key.slice("mcp_servers.".length).replace(/^"([^"]*)"/, "$1");
109854
+ if (name === pullfrogMcpName || name.startsWith(`${pullfrogMcpName}.`)) continue;
109855
+ isServerTable = !name.includes(".");
109856
+ section = [line.trim()];
109857
+ }
109858
+ flush();
109859
+ return out;
109860
+ }
109861
+ function warnIfNativeEditsUnavailable(ctx) {
109862
+ if (process.platform !== "linux" || ctx.payload.shell !== "disabled") return;
109863
+ log2.warning(
109864
+ "\xBB this run cannot change files: codex's native write tool depends on bubblewrap, which Ubuntu 24.04 blocks (https://github.com/openai/codex/issues/29908), and the MCP shell is disabled for this repo. read-only work is unaffected \u2014 set `shell: restricted` if the agent needs to edit anything."
109865
+ );
109866
+ }
109867
+ function writeCodexConfig(params) {
109868
+ const shellEnabled = params.ctx.payload.shell === "enabled";
109869
+ const sandboxMode = params.ctx.payload.push === "disabled" ? "read-only" : "workspace-write";
109870
+ const repoDir = process.cwd();
109871
+ const config3 = [
109872
+ "# written by pullfrog",
109873
+ // "never" keeps the run non-interactive WITHOUT
109874
+ // --dangerously-bypass-approvals-and-sandbox, which would also switch the
109875
+ // sandbox off and let the native apply_patch tool write anywhere.
109876
+ 'approval_policy = "never"',
109877
+ `sandbox_mode = ${JSON.stringify(sandboxMode)}`,
109878
+ // matches opencode's `webfetch: "allow"` — research is part of the job and
109879
+ // neither harness gates it on a permission.
109880
+ 'web_search = "live"',
109881
+ ...params.model ? [`model = ${JSON.stringify(params.model)}`] : [],
109882
+ ...params.effortRung ? [`model_reasoning_effort = ${JSON.stringify(params.effortRung)}`] : [],
109883
+ "",
109884
+ "[features]",
109885
+ tomlBool(CODEX_DISABLED_FEATURES, false),
109886
+ tomlBool(CODEX_SHELL_FEATURES, shellEnabled),
109887
+ "",
109888
+ "[sandbox_workspace_write]",
109889
+ "network_access = true",
109890
+ // the checkout, named explicitly rather than left to `workspace-write` to
109891
+ // imply. does NOT fix the Linux native-write failure — nothing here does;
109892
+ // see wiki/codex-agent.md "The Linux native-write blocker".
109893
+ `writable_roots = [${JSON.stringify(repoDir)}]`,
109894
+ "",
109895
+ `[mcp_servers.${pullfrogMcpName}]`,
109896
+ `url = ${JSON.stringify(params.ctx.mcpServerUrl)}`,
109897
+ // codex's default MCP approval mode PROMPTS for anything it can't read as
109898
+ // read-only, and a `codex exec` run has nobody to answer — the call comes
109899
+ // back "user cancelled MCP tool call" and the agent silently accomplishes
109900
+ // nothing. our own server is the trusted one; the boundary is what we
109901
+ // register, not a second confirmation.
109902
+ APPROVE_TOOLS,
109903
+ // a run whose GitHub tools never registered can't do its job, so fail loudly
109904
+ // rather than let the agent improvise without them.
109905
+ "required = true",
109906
+ // mirrors the opencode harness's 660s: `checkout_pr` runs a multi-minute
109907
+ // fetch on large repos and enforces its own 600s deadline, so a shorter
109908
+ // client abort turns a working checkout into a spurious tool error.
109909
+ "tool_timeout_sec = 660",
109910
+ "",
109911
+ ...repoMcpApprovals(repoDir),
109912
+ // UNTRUSTED, deliberately, and this one line is the whole config-precedence
109913
+ // boundary. Codex loads `<repo>/.codex/config.toml` as a layer that
109914
+ // OUTRANKS ours (project 25 vs user 20), and its denylist strips none of
109915
+ // `sandbox_mode`, `approval_policy`, `features` or `mcp_servers` — so a
109916
+ // fork PR could ship a config re-enabling the native shell. Marking the
109917
+ // checkout untrusted makes that layer load `disabled`, closing the class
109918
+ // outright rather than key by key: `RawMcpServerConfig` alone has ~26
109919
+ // fields and enumeration can never finish.
109920
+ //
109921
+ // It costs nothing. `codex exec`'s only startup gate is a GIT-REPO check
109922
+ // (`exec/src/lib.rs:796` — `get_git_repo_root(...).is_none()`, whose error
109923
+ // text unhelpfully says "Not inside a trusted directory"), so an untrusted
109924
+ // checkout still runs. And the repo's own MCP servers survive, because
109925
+ // `repoMcpApprovals` above copies them into OUR layer — which is why that
109926
+ // copying exists at all.
109927
+ //
109928
+ // Measured both ways at 0.147.0: `trusted` → the repo's `model` won;
109929
+ // `untrusted` → it did not, and the run proceeded normally.
109930
+ `[projects.${JSON.stringify(repoDir)}]`,
109931
+ 'trust_level = "untrusted"',
109932
+ ""
109933
+ ].join("\n");
109934
+ writeFileSync6(join7(params.codexHome, "config.toml"), config3, { mode: 384 });
109935
+ log2.info(`\xBB codex config: sandbox=${sandboxMode}, nativeShell=${shellEnabled}`);
109936
+ }
109937
+ function toTodoWriteInput(item) {
109938
+ return {
109939
+ todos: item.items.map((todo, index) => ({
109940
+ id: String(index),
109941
+ content: todo.text,
109942
+ status: todo.completed ? "completed" : "pending"
109943
+ }))
109944
+ };
109945
+ }
109946
+ async function runCodex2(params) {
109947
+ const startTime = performance6.now();
109948
+ const thinkingTimer = new ThinkingTimer();
109949
+ const output = new TailBuffer(DEFAULT_MAX_RETAINED_BYTES);
109950
+ const recentStderr = [];
109951
+ let eventCount = 0;
109952
+ let finalOutput = "";
109953
+ let threadId;
109954
+ let turnError = null;
109955
+ let lastProviderError = null;
109956
+ let stdoutBuffer = "";
109957
+ const tokens = { input: 0, cacheRead: 0, output: 0 };
109958
+ function buildUsage2() {
109959
+ if (tokens.input === 0 && tokens.output === 0) return void 0;
109960
+ return {
109961
+ agent: "codex",
109962
+ inputTokens: tokens.input,
109963
+ outputTokens: tokens.output,
109964
+ cacheReadTokens: tokens.cacheRead || void 0
109965
+ };
109966
+ }
109967
+ function onItem(item, phase) {
109968
+ switch (item.type) {
109969
+ case "agent_message":
109970
+ if (phase === "completed" && item.text.trim()) {
109971
+ finalOutput = item.text.trim();
109972
+ log2.box(finalOutput, { title: "Pullfrog" });
109973
+ }
109974
+ return;
109975
+ case "reasoning":
109976
+ if (phase === "completed" && item.text.trim()) {
109977
+ log2.box(item.text.trim(), { title: "Pullfrog" });
109978
+ }
109979
+ return;
109980
+ case "command_execution":
109981
+ if (phase === "started") {
109982
+ thinkingTimer.markToolCall();
109983
+ log2.toolCall({ toolName: "shell", input: { command: item.command } });
109984
+ return;
109985
+ }
109986
+ thinkingTimer.markToolResult();
109987
+ log2.startGroup("shell output");
109988
+ log2.info(item.aggregated_output || "");
109989
+ log2.endGroup();
109990
+ return;
109991
+ case "mcp_tool_call":
109992
+ if (phase === "started") {
109993
+ thinkingTimer.markToolCall();
109994
+ params.onToolUse?.({ toolName: item.tool, input: item.arguments });
109995
+ log2.toolCall({ toolName: `${item.server}__${item.tool}`, input: item.arguments });
109996
+ if (item.tool.includes("report_progress")) params.todoTracker?.cancel();
109997
+ return;
109998
+ }
109999
+ thinkingTimer.markToolResult();
110000
+ if (item.error) log2.info(`\xBB tool error: ${item.error.message}`);
110001
+ return;
110002
+ case "file_change":
110003
+ if (phase === "completed") {
110004
+ const paths = item.changes.map((c2) => `${c2.kind} ${c2.path}`).join(", ");
110005
+ log2.info(`\xBB apply_patch (${item.status}): ${paths}`);
110006
+ }
110007
+ return;
110008
+ case "web_search":
110009
+ if (phase === "started") log2.toolCall({ toolName: "web_search", input: item.query });
110010
+ return;
110011
+ case "todo_list":
110012
+ if (params.todoTracker?.enabled) params.todoTracker.update(toTodoWriteInput(item));
110013
+ return;
110014
+ case "error":
110015
+ log2.info(`\xBB ${item.message}`);
110016
+ return;
110017
+ default:
110018
+ item;
110019
+ }
110020
+ }
110021
+ function onEvent(event) {
110022
+ switch (event.type) {
110023
+ case "thread.started":
110024
+ threadId = event.thread_id;
110025
+ return;
110026
+ case "turn.completed":
110027
+ tokens.input += event.usage.input_tokens ?? 0;
110028
+ tokens.cacheRead += event.usage.cached_input_tokens ?? 0;
110029
+ tokens.output += event.usage.output_tokens ?? 0;
110030
+ return;
110031
+ case "turn.failed":
110032
+ turnError = event.error.message;
110033
+ log2.info(`\xBB turn failed: ${turnError}`);
110034
+ return;
110035
+ case "error":
110036
+ turnError = event.message;
110037
+ log2.info(`\xBB codex error: ${event.message}`);
110038
+ return;
110039
+ case "item.started":
110040
+ onItem(event.item, "started");
110041
+ return;
110042
+ case "item.completed":
110043
+ onItem(event.item, "completed");
110044
+ return;
110045
+ case "item.updated":
110046
+ if (event.item.type === "todo_list") onItem(event.item, "started");
110047
+ return;
110048
+ case "turn.started":
110049
+ return;
110050
+ default:
110051
+ event;
110052
+ }
110053
+ }
110054
+ try {
110055
+ const result = await spawn3({
110056
+ cmd: params.cliPath,
110057
+ args: params.args,
110058
+ cwd: params.cwd,
110059
+ env: params.env,
110060
+ activityTimeout: AGENT_ACTIVITY_TIMEOUT_MS,
110061
+ onActivityTimeout: params.onActivityTimeout,
110062
+ // stdin MUST be `ignore`: codex reads a piped stdin as an extra `<stdin>`
110063
+ // prompt block and blocks on EOF, so an inherited pipe hangs the run
110064
+ // before the first model call.
110065
+ stdio: ["ignore", "pipe", "pipe"],
110066
+ killGroup: true,
110067
+ retain: "none",
110068
+ onStdout: (chunk) => {
110069
+ output.append(chunk);
110070
+ markActivity();
110071
+ stdoutBuffer += chunk;
110072
+ const lines = stdoutBuffer.split("\n");
110073
+ stdoutBuffer = lines.pop() || "";
110074
+ for (const line of lines) {
110075
+ const trimmed = line.trim();
110076
+ if (!trimmed) continue;
110077
+ let event;
110078
+ try {
110079
+ event = JSON.parse(trimmed);
110080
+ } catch {
110081
+ log2.debug(`\xBB non-JSON stdout line: ${trimmed.substring(0, 200)}`);
110082
+ continue;
110083
+ }
110084
+ eventCount++;
110085
+ log2.debug(JSON.stringify(event, null, 2));
110086
+ const idleMs = getIdleMs();
110087
+ if (idleMs > 1e4) {
110088
+ log2.info(
110089
+ `\xBB no activity for ${(idleMs / 1e3).toFixed(1)}s (${eventCount} events processed so far)`
110090
+ );
110091
+ }
110092
+ markActivity();
110093
+ onEvent(event);
110094
+ }
110095
+ },
110096
+ onStderr: (chunk) => {
110097
+ const trimmed = chunk.trim();
110098
+ if (!trimmed) return;
110099
+ recentStderr.push(trimmed);
110100
+ if (recentStderr.length > MAX_STDERR_LINES) recentStderr.shift();
110101
+ const match3 = findProviderErrorMatch(trimmed);
110102
+ if (match3) {
110103
+ lastProviderError = match3.label;
110104
+ log2.info(`\xBB provider error detected (${match3.label}): ${match3.excerpt}`);
110105
+ } else {
110106
+ log2.debug(trimmed);
110107
+ }
110108
+ }
110109
+ });
110110
+ const duration4 = performance6.now() - startTime;
110111
+ log2.info(`\xBB codex completed in ${Math.round(duration4)}ms with exit code ${result.exitCode}`);
110112
+ const usage = buildUsage2();
110113
+ if (usage) {
110114
+ logTokenTable({
110115
+ input: tokens.input - tokens.cacheRead,
110116
+ cacheRead: tokens.cacheRead,
110117
+ cacheWrite: 0,
110118
+ output: tokens.output
110119
+ });
110120
+ }
110121
+ if (result.exitCode !== 0 || turnError) {
110122
+ const stderrSnapshot = recentStderr.join("\n");
110123
+ const error52 = turnError || stderrSnapshot || lastProviderError && `provider error: ${lastProviderError}` || `unknown error - no output from Codex CLI (exit ${result.exitCode})`;
110124
+ log2.error(`codex exited with code ${result.exitCode}: ${error52}`);
110125
+ return { success: false, output: finalOutput || output.toString(), error: error52, usage, threadId };
110126
+ }
110127
+ return { success: true, output: finalOutput || output.toString(), usage, threadId };
110128
+ } catch (error52) {
110129
+ const duration4 = performance6.now() - startTime;
110130
+ const message = error52 instanceof Error ? error52.message : String(error52);
110131
+ const isActivityTimeout = error52 instanceof SpawnTimeoutError && error52.code === SPAWN_ACTIVITY_TIMEOUT_CODE;
110132
+ const diagnosis = lastProviderError ? `likely cause: ${lastProviderError}` : eventCount === 0 ? "codex produced 0 stdout events - check if the API is reachable" : `${eventCount} events were processed before the hang`;
110133
+ log2.info(
110134
+ `\xBB codex ${isActivityTimeout ? "hung" : "failed"} after ${(duration4 / 1e3).toFixed(1)}s: ${message}`
110135
+ );
110136
+ log2.info(`\xBB diagnosis: ${diagnosis}`);
110137
+ if (recentStderr.length > 0) log2.info(`\xBB recent stderr:
110138
+ ${recentStderr.join("\n")}`);
110139
+ return {
110140
+ success: false,
110141
+ output: finalOutput || output.toString(),
110142
+ error: `${message} [${diagnosis}]`,
110143
+ usage: buildUsage2(),
110144
+ threadId
110145
+ };
110146
+ }
110147
+ }
110148
+ var codex = agent({
110149
+ name: "codex",
110150
+ install: installCodexCli,
110151
+ run: async (ctx) => {
110152
+ const cliPath = await installCodexCli();
110153
+ warnIfNativeEditsUnavailable(ctx);
110154
+ const codexHomeAuth = installCodexHome();
110155
+ const codexHome = codexHomeAuth?.codexHome ?? join7(ctx.tmpdir, ".codex");
110156
+ mkdirSync6(codexHome, { recursive: true });
110157
+ const effort = resolveRunEffort(ctx);
110158
+ if (effort.rung && !CODEX_EFFORTS.includes(effort.rung)) {
110159
+ log2.warning(`\xBB effort ${effort.rung} not sent \u2014 codex doesn't accept that level`);
110160
+ }
110161
+ writeCodexConfig({
110162
+ ctx,
110163
+ codexHome,
110164
+ model: resolveCodexModel(ctx),
110165
+ effortRung: effort.rung && CODEX_EFFORTS.includes(effort.rung) ? effort.rung : void 0
110166
+ });
110167
+ if (codexHomeAuth) {
110168
+ core2.saveState(
110169
+ "codex_writeback",
110170
+ JSON.stringify({
110171
+ apiToken: ctx.apiToken,
110172
+ authPath: codexHomeAuth.authPath,
110173
+ originalRefresh: codexHomeAuth.originalRefresh
110174
+ })
110175
+ );
110176
+ }
110177
+ const baseEnv = ctx.payload.shell === "enabled" ? process.env : filterEnv();
110178
+ const env2 = {
110179
+ ...baseEnv,
110180
+ CODEX_HOME: codexHome,
110181
+ PWD: process.cwd()
110182
+ };
110183
+ delete env2.CODEX_API_KEY;
110184
+ if (codexHomeAuth) {
110185
+ delete env2.OPENAI_API_KEY;
110186
+ } else if (process.env.OPENAI_API_KEY) {
110187
+ env2.CODEX_API_KEY = process.env.OPENAI_API_KEY;
110188
+ }
110189
+ const securityFlags = securityOverrideFlags({
110190
+ ctx,
110191
+ sandboxMode: ctx.payload.push === "disabled" ? "read-only" : "workspace-write",
110192
+ repoDir: process.cwd(),
110193
+ mcpServerUrl: ctx.mcpServerUrl,
110194
+ model: resolveCodexModel(ctx),
110195
+ effortRung: effort.rung && CODEX_EFFORTS.includes(effort.rung) ? effort.rung : void 0
110196
+ });
110197
+ const runnerArgs = { cliPath, cwd: process.cwd(), env: env2, todoTracker: ctx.todoTracker };
110198
+ const initial = await runCodex2({
110199
+ ...runnerArgs,
110200
+ args: [...securityFlags, "exec", "--json", ctx.instructions.full],
110201
+ onActivityTimeout: ctx.onActivityTimeout,
110202
+ onToolUse: ctx.onToolUse
110203
+ });
110204
+ const result = await runPostRunRetryLoop({
110205
+ ctx,
110206
+ initialResult: initial,
110207
+ initialUsage: initial.usage,
110208
+ reflectionPrompt: buildReflectionPrompt(ctx.toolState),
110209
+ // a resume needs the thread codex recorded. without one there is nothing
110210
+ // to continue, and `resume --last` would be worse than not resuming: it
110211
+ // picks the newest recorded session in this cwd, which on a self-hosted
110212
+ // runner with a shared CODEX_HOME is somebody else's run.
110213
+ canResume: (r2) => r2.threadId !== void 0,
110214
+ resume: async (c2) => runCodex2({
110215
+ ...runnerArgs,
110216
+ args: [
110217
+ ...securityFlags,
110218
+ "exec",
110219
+ "resume",
110220
+ "--json",
110221
+ c2.previousResult.threadId ?? "",
110222
+ c2.prompt
110223
+ ]
110224
+ })
110225
+ });
110226
+ if (result.success) {
110227
+ await ctx.todoTracker?.flush();
110228
+ } else {
110229
+ ctx.todoTracker?.cancel();
110230
+ }
110231
+ return result;
110232
+ }
110233
+ });
110234
+
110235
+ // agents/opencode.ts
110236
+ var core3 = __toESM(require_core(), 1);
110237
+ import { spawn as nodeSpawn2 } from "node:child_process";
110238
+ import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "node:fs";
110239
+ import { join as join8 } from "node:path";
110240
+ import { performance as performance7 } from "node:perf_hooks";
110241
+
109480
110242
  // node_modules/.pnpm/@opencode-ai+sdk@1.18.5/node_modules/@opencode-ai/sdk/dist/v2/gen/core/serverSentEvents.gen.js
109481
110243
  var createSseClient = ({ onRequest, onSseError, onSseEvent, responseTransformer, responseValidator, sseDefaultRetryDelay, sseMaxRetryAttempts, sseMaxRetryDelay, sseSleepFn, url: url4, ...options }) => {
109482
110244
  let lastEventId;
@@ -114883,71 +115645,6 @@ var import_cross_spawn = __toESM(require_cross_spawn(), 1);
114883
115645
  // agents/opencode.ts
114884
115646
  var import_undici = __toESM(require_undici2(), 1);
114885
115647
 
114886
- // utils/codexHome.ts
114887
- import { execFileSync as execFileSync4 } from "node:child_process";
114888
- import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "node:fs";
114889
- import { homedir as homedir2, userInfo } from "node:os";
114890
- import { join as join6 } from "node:path";
114891
- var CODEX_AUTH_ENV = "CODEX_AUTH_JSON";
114892
- var PULLFROG_DATA_DIR = "/var/lib/pullfrog";
114893
- function installCodexAuth() {
114894
- const raw2 = process.env[CODEX_AUTH_ENV];
114895
- if (!raw2) return null;
114896
- const body = parseCodexAuthBody(raw2);
114897
- if (!body) {
114898
- log2.warning(`\xBB ${CODEX_AUTH_ENV} present but malformed; ignoring`);
114899
- return null;
114900
- }
114901
- const expiresMs = decodeJwtExpMs(body.tokens.access_token) ?? 0;
114902
- const xdgDataHome = resolveDataHome();
114903
- const opencodeDir = join6(xdgDataHome, "opencode");
114904
- const authPath = join6(opencodeDir, "auth.json");
114905
- const opencodeAuth = {
114906
- openai: {
114907
- type: "oauth",
114908
- refresh: body.tokens.refresh_token,
114909
- access: body.tokens.access_token,
114910
- expires: expiresMs,
114911
- ...body.tokens.account_id ? { accountId: body.tokens.account_id } : {}
114912
- }
114913
- };
114914
- mkdirSync5(opencodeDir, { recursive: true });
114915
- writeFileSync5(authPath, `${JSON.stringify(opencodeAuth, null, 2)}
114916
- `, { mode: 384 });
114917
- process.env.XDG_DATA_HOME = xdgDataHome;
114918
- log2.info(`\xBB installed Codex auth at ${authPath}`);
114919
- return {
114920
- authPath,
114921
- xdgDataHome,
114922
- originalRefresh: body.tokens.refresh_token
114923
- };
114924
- }
114925
- function resolveDataHome() {
114926
- if (process.env.CI !== "true") return join6(homedir2(), ".local", "share");
114927
- bootstrapPullfrogDataDir();
114928
- return PULLFROG_DATA_DIR;
114929
- }
114930
- function bootstrapPullfrogDataDir() {
114931
- const user = userInfo().username;
114932
- let primaryGroup;
114933
- try {
114934
- primaryGroup = execFileSync4("id", ["-gn", user], { stdio: "pipe", encoding: "utf-8" }).trim();
114935
- } catch {
114936
- primaryGroup = user;
114937
- }
114938
- try {
114939
- execFileSync4("sudo", ["-n", "mkdir", "-p", PULLFROG_DATA_DIR], { stdio: "pipe" });
114940
- execFileSync4("sudo", ["-n", "chown", `${user}:${primaryGroup}`, PULLFROG_DATA_DIR], {
114941
- stdio: "pipe"
114942
- });
114943
- execFileSync4("sudo", ["-n", "chmod", "700", PULLFROG_DATA_DIR], { stdio: "pipe" });
114944
- } catch (err) {
114945
- throw new Error(
114946
- `failed to bootstrap ${PULLFROG_DATA_DIR} (required for codex auth in CI): ${err instanceof Error ? err.message : String(err)}. the MCP shell's mount-namespace sandbox cannot protect the auth file when it lives under $HOME, and silently falling back would contradict the "three independent layers" claim in wiki/codex-auth.md. passwordless sudo is required for codex auth on this runner \u2014 either configure it, or remove CODEX_AUTH_JSON from the run.`
114947
- );
114948
- }
114949
- }
114950
-
114951
115648
  // utils/worktree.ts
114952
115649
  async function dirtyTrackedPaths(params = {}) {
114953
115650
  const result = await spawn3({
@@ -115215,6 +115912,30 @@ function openAICompatibleProvider(model) {
115215
115912
  }
115216
115913
  };
115217
115914
  }
115915
+ function azureProvider(model) {
115916
+ const prefix = `${AZURE_PROVIDER}/`;
115917
+ if (!model?.startsWith(prefix)) return {};
115918
+ const deployment = model.slice(prefix.length);
115919
+ return {
115920
+ [AZURE_PROVIDER]: {
115921
+ // always sent: opencode reads it as `Boolean(options?.useCompletionUrls)`,
115922
+ // so `false` and absent are the same, and the deep merge keeps the native
115923
+ // loader's `resourceName` either way.
115924
+ options: {
115925
+ useCompletionUrls: process.env[AZURE_USE_CHAT_COMPLETIONS_ENV]?.trim() === "true"
115926
+ },
115927
+ models: {
115928
+ [deployment]: {
115929
+ name: deployment,
115930
+ limit: {
115931
+ context: Number(process.env[AZURE_CONTEXT_ENV]),
115932
+ output: Number(process.env[AZURE_MAX_OUTPUT_ENV])
115933
+ }
115934
+ }
115935
+ }
115936
+ }
115937
+ };
115938
+ }
115218
115939
  var KIMI_ENFORCERLESS_PROVIDERS = ["siliconflow", "together"];
115219
115940
  function kimiOpenRouterProviderOverrides() {
115220
115941
  return Object.fromEntries(
@@ -115306,7 +116027,8 @@ function buildSecurityConfig(ctx, model) {
115306
116027
  // see opencodeShared.ts.
115307
116028
  provider: {
115308
116029
  openrouter: { models: kimiOpenRouterProviderOverrides() },
115309
- ...openAICompatibleProvider(model)
116030
+ ...openAICompatibleProvider(model),
116031
+ ...azureProvider(model)
115310
116032
  }
115311
116033
  };
115312
116034
  if (model) {
@@ -115474,7 +116196,7 @@ function isModelOutput(ctx, part) {
115474
116196
  }
115475
116197
  async function dispatchEvent(ctx, event) {
115476
116198
  if (event.type === "message.part.updated") {
115477
- ctx.lastEventAt = performance6.now();
116199
+ ctx.lastEventAt = performance7.now();
115478
116200
  if (isModelOutput(ctx, event.properties.part)) ctx.sawModelOutput = true;
115479
116201
  await onPartUpdated(ctx, event.properties.part);
115480
116202
  return;
@@ -115541,7 +116263,7 @@ async function onToolPart(ctx, part, label, isOrchestrator) {
115541
116263
  if (toolName === "task" && status === "running" && isOrchestrator && !ctx.taskDispatchByCallID.has(toolId)) {
115542
116264
  const input = part.state.input ?? {};
115543
116265
  const dispatched = ctx.labeler.recordTaskDispatch(input);
115544
- ctx.taskDispatchByCallID.set(toolId, { label: dispatched, startedAt: performance6.now() });
116266
+ ctx.taskDispatchByCallID.set(toolId, { label: dispatched, startedAt: performance7.now() });
115545
116267
  log2.info(
115546
116268
  `\xBB dispatching subagent: ${dispatched}` + (input.subagent_type ? ` (subagent_type=${input.subagent_type})` : "")
115547
116269
  );
@@ -115560,6 +116282,7 @@ function processTerminalToolPart(ctx, part, label, isOrchestrator) {
115560
116282
  const callLine = inputFormatted !== "{}" ? `\xBB ${toolName}(${inputFormatted})` : `\xBB ${toolName}()`;
115561
116283
  log2.info(withLabel(label, callLine));
115562
116284
  if (isOrchestrator) ctx.loggedToolCallIDs.add(toolId);
116285
+ if (isOrchestrator && toolName.startsWith("pullfrog_")) ctx.mcpToolCalls++;
115563
116286
  if (state.status === "completed") {
115564
116287
  log2.debug(withLabel(label, ` output: ${state.output}`));
115565
116288
  } else {
@@ -115571,7 +116294,7 @@ function processTerminalToolPart(ctx, part, label, isOrchestrator) {
115571
116294
  if (toolName === "task") {
115572
116295
  const dispatch = ctx.taskDispatchByCallID.get(toolId);
115573
116296
  if (dispatch) {
115574
- const dur = ((performance6.now() - dispatch.startedAt) / 1e3).toFixed(1);
116297
+ const dur = ((performance7.now() - dispatch.startedAt) / 1e3).toFixed(1);
115575
116298
  const outputStr = state.status === "completed" ? state.output : "";
115576
116299
  const preview = typeof outputStr === "string" && outputStr.length > 120 ? `${outputStr.slice(0, 120)}\u2026` : outputStr;
115577
116300
  log2.info(
@@ -115614,7 +116337,7 @@ function withLabel(label, message) {
115614
116337
  return label === ORCHESTRATOR_LABEL ? message : formatWithLabel(label, message);
115615
116338
  }
115616
116339
  async function runPromptTurn(ctx, params) {
115617
- const start = performance6.now();
116340
+ const start = performance7.now();
115618
116341
  const turnStartMs = Date.now();
115619
116342
  ctx.currentTurn = newTurn();
115620
116343
  const turn = ctx.currentTurn;
@@ -115648,7 +116371,7 @@ async function runPromptTurn(ctx, params) {
115648
116371
  } catch (err) {
115649
116372
  networkError = err instanceof Error ? err.message : String(err);
115650
116373
  }
115651
- const durationMs = performance6.now() - start;
116374
+ const durationMs = performance7.now() - start;
115652
116375
  const aggregatedUsage = await aggregateTurnUsage(ctx, turnStartMs);
115653
116376
  const usage = aggregatedUsage ?? buildUsage(turn, assistant);
115654
116377
  const finalText = turn.finalText || extractTextFromParts(returnedParts) || "";
@@ -115798,21 +116521,28 @@ function formatConfigError(name, data) {
115798
116521
  ).join("");
115799
116522
  return `${name}${detail}${bullets}`;
115800
116523
  }
116524
+ var WATCHDOG_SALVAGE_PROMPT = "Your previous turn was cut off mid-response by a provider stall. Nothing you had not already submitted through a tool was saved. Continue from where you stopped and submit your work now \u2014 do not restart your analysis.";
115801
116525
  function startInnerActivityWatchdog(params) {
115802
116526
  let fired = false;
116527
+ let everFired = false;
115803
116528
  const id = setInterval(() => {
115804
116529
  if (fired) return;
115805
- const idleMs = performance6.now() - params.ctx.lastEventAt;
115806
- const budgetMs = params.ctx.sawModelOutput ? params.timeoutMs : AGENT_FIRST_EVENT_TIMEOUT_MS;
116530
+ const idleMs = performance7.now() - params.ctx.lastEventAt;
116531
+ const compiledMs = params.ctx.sawModelOutput ? params.timeoutMs : AGENT_FIRST_EVENT_TIMEOUT_MS;
116532
+ const budgetMs = everFired ? compiledMs : watchdogBudgetMs(
116533
+ compiledMs,
116534
+ params.ctx.sawModelOutput ? "PULLFROG_E2E_ACTIVITY_TIMEOUT_MS" : "PULLFROG_E2E_FIRST_EVENT_TIMEOUT_MS"
116535
+ );
115807
116536
  if (idleMs <= budgetMs) return;
115808
116537
  fired = true;
116538
+ everFired = true;
115809
116539
  const idleSec = Math.round(idleMs / 1e3);
115810
116540
  params.ctx.diagnostic.idleSec = idleSec;
115811
116541
  params.ctx.diagnostic.sawModelOutput = params.ctx.sawModelOutput;
115812
116542
  log2.info(
115813
116543
  params.ctx.sawModelOutput ? `\xBB no opencode events for ${idleSec}s \u2014 aborting in-flight prompt and notifying harness` : `\xBB no opencode events for ${idleSec}s \u2014 the provider never returned a first token; aborting in-flight prompt and notifying harness`
115814
116544
  );
115815
- params.abortController.abort();
116545
+ params.abortTurn();
115816
116546
  try {
115817
116547
  params.ctx.onActivityTimeout?.();
115818
116548
  } catch (err) {
@@ -115822,7 +116552,22 @@ function startInnerActivityWatchdog(params) {
115822
116552
  }
115823
116553
  }, 5e3);
115824
116554
  id.unref?.();
115825
- return { stop: () => clearInterval(id) };
116555
+ return {
116556
+ stop: () => clearInterval(id),
116557
+ /**
116558
+ * Start a turn's idle budget from now. The clock must be reset with the
116559
+ * latch: `lastEventAt` only advances on model output, so a turn following a
116560
+ * stall would inherit the full stalled interval and be aborted on the very
116561
+ * next 5s tick — killing the salvage before the provider could answer. It
116562
+ * is the right semantics for an ordinary resume too, whose budget should
116563
+ * not be pre-spent by the post-run gate checks that ran between turns.
116564
+ */
116565
+ armForTurn: () => {
116566
+ fired = false;
116567
+ params.ctx.lastEventAt = performance7.now();
116568
+ },
116569
+ firedThisTurn: () => fired
116570
+ };
115826
116571
  }
115827
116572
  var opencode = agent({
115828
116573
  name: "opencode",
@@ -115841,12 +116586,12 @@ var opencode = agent({
115841
116586
  const model = vertexModel ?? (isBedrockRoute ? `amazon-bedrock/${rawModel}` : rawModel);
115842
116587
  const homeEnv = {
115843
116588
  HOME: ctx.tmpdir,
115844
- XDG_CONFIG_HOME: join7(ctx.tmpdir, ".config")
116589
+ XDG_CONFIG_HOME: join8(ctx.tmpdir, ".config")
115845
116590
  };
115846
- const opencodePluginDir = join7(homeEnv.XDG_CONFIG_HOME, "opencode", "plugin");
115847
- mkdirSync6(opencodePluginDir, { recursive: true });
115848
- writeFileSync6(
115849
- join7(opencodePluginDir, PULLFROG_OPENCODE_GATE_PLUGIN_FILENAME),
116591
+ const opencodePluginDir = join8(homeEnv.XDG_CONFIG_HOME, "opencode", "plugin");
116592
+ mkdirSync7(opencodePluginDir, { recursive: true });
116593
+ writeFileSync7(
116594
+ join8(opencodePluginDir, PULLFROG_OPENCODE_GATE_PLUGIN_FILENAME),
115850
116595
  buildOpencodeSubagentGateSource(ctx.subagentDeniedTools)
115851
116596
  );
115852
116597
  const agentBrowserVersion = getDevDependencyVersion("agent-browser");
@@ -115877,12 +116622,13 @@ var opencode = agent({
115877
116622
  if (codexAuth) {
115878
116623
  env2.XDG_DATA_HOME = codexAuth.xdgDataHome;
115879
116624
  delete env2.OPENAI_API_KEY;
115880
- core2.saveState(
116625
+ core3.saveState(
115881
116626
  "codex_writeback",
115882
116627
  JSON.stringify({
115883
116628
  apiToken: ctx.apiToken,
115884
116629
  authPath: codexAuth.authPath,
115885
- originalRefresh: codexAuth.originalRefresh
116630
+ originalRefresh: codexAuth.originalRefresh,
116631
+ originalIdToken: codexAuth.originalIdToken
115886
116632
  })
115887
116633
  );
115888
116634
  }
@@ -115933,14 +116679,16 @@ var opencode = agent({
115933
116679
  variant: effort.rung,
115934
116680
  todoTracker: ctx.todoTracker,
115935
116681
  onActivityTimeout: ctx.onActivityTimeout,
116682
+ onTurnRecovered: ctx.onTurnRecovered,
115936
116683
  onToolUse: ctx.onToolUse,
115937
116684
  currentTurn: null,
115938
116685
  eventCount: 0,
115939
- lastEventAt: performance6.now(),
116686
+ lastEventAt: performance7.now(),
115940
116687
  sawModelOutput: false,
115941
116688
  promptMessageID: void 0,
115942
116689
  taskDispatchByCallID: /* @__PURE__ */ new Map(),
115943
116690
  loggedToolCallIDs: /* @__PURE__ */ new Set(),
116691
+ mcpToolCalls: 0,
115944
116692
  recentStderr: server.recentStderr,
115945
116693
  diagnostic: {
115946
116694
  label: "Pullfrog",
@@ -115964,9 +116712,14 @@ var opencode = agent({
115964
116712
  }
115965
116713
  }
115966
116714
  });
115967
- const abortController = new AbortController();
115968
- const eventLoopPromise = consumeEvents(runnerCtx, abortController.signal).catch((err) => {
115969
- if (!abortController.signal.aborted) {
116715
+ const runController = new AbortController();
116716
+ let turnController = new AbortController();
116717
+ const nextTurnSignal = () => {
116718
+ turnController = new AbortController();
116719
+ return AbortSignal.any([runController.signal, turnController.signal]);
116720
+ };
116721
+ const eventLoopPromise = consumeEvents(runnerCtx, runController.signal).catch((err) => {
116722
+ if (!runController.signal.aborted) {
115970
116723
  log2.warning(
115971
116724
  `\xBB opencode event subscription ended: ${err instanceof Error ? err.message : String(err)}`
115972
116725
  );
@@ -115982,31 +116735,48 @@ var opencode = agent({
115982
116735
  // a long synchronous tool call (no part.updated while it runs) can't
115983
116736
  // false-positive it.
115984
116737
  timeoutMs: AGENT_ACTIVITY_TIMEOUT_MS,
115985
- abortController
116738
+ abortTurn: () => turnController.abort()
115986
116739
  });
115987
116740
  const sdkModel = parseModel2(model);
116741
+ let salvagesLeft = 1;
116742
+ const runTurn = async (text) => {
116743
+ const attempt = (prompt) => {
116744
+ watchdog.armForTurn();
116745
+ return runTurnGuarded(
116746
+ runnerCtx,
116747
+ () => runPromptTurn(runnerCtx, { text: prompt, model: sdkModel, signal: nextTurnSignal() })
116748
+ );
116749
+ };
116750
+ const standDownIfRecovered = (turn) => {
116751
+ if (watchdog.firedThisTurn() && turn.success) ctx.onTurnRecovered?.();
116752
+ return turn;
116753
+ };
116754
+ const result = await attempt(text);
116755
+ if (!watchdog.firedThisTurn()) return result;
116756
+ if (result.success) return standDownIfRecovered(result);
116757
+ if (salvagesLeft <= 0) return result;
116758
+ salvagesLeft--;
116759
+ ctx.onTurnRecovered?.();
116760
+ log2.info("\xBB activity watchdog cut the turn off \u2014 re-prompting once on the same session");
116761
+ const mcpCallsBefore = runnerCtx.mcpToolCalls;
116762
+ const salvaged = await attempt(WATCHDOG_SALVAGE_PROMPT);
116763
+ const usage = mergeAgentUsage(result.usage, salvaged.usage);
116764
+ if (runnerCtx.mcpToolCalls === mcpCallsBefore) {
116765
+ log2.info(
116766
+ "\xBB salvage turn reached no Pullfrog tool \u2014 keeping the activity-timeout failure"
116767
+ );
116768
+ return { ...result, usage };
116769
+ }
116770
+ return standDownIfRecovered({ ...salvaged, usage });
116771
+ };
115988
116772
  try {
115989
- const initial = await runTurnGuarded(
115990
- runnerCtx,
115991
- () => runPromptTurn(runnerCtx, {
115992
- text: ctx.instructions.full,
115993
- model: sdkModel,
115994
- signal: abortController.signal
115995
- })
115996
- );
116773
+ const initial = await runTurn(ctx.instructions.full);
115997
116774
  const result = await runPostRunRetryLoop({
115998
116775
  ctx,
115999
116776
  initialResult: initial,
116000
116777
  initialUsage: initial.usage,
116001
116778
  reflectionPrompt: buildReflectionPrompt(ctx.toolState),
116002
- resume: async (c2) => runTurnGuarded(
116003
- runnerCtx,
116004
- () => runPromptTurn(runnerCtx, {
116005
- text: c2.prompt,
116006
- model: sdkModel,
116007
- signal: abortController.signal
116008
- })
116009
- )
116779
+ resume: async (c2) => runTurn(c2.prompt)
116010
116780
  });
116011
116781
  if (result.success) {
116012
116782
  await ctx.todoTracker?.flush();
@@ -116016,7 +116786,7 @@ var opencode = agent({
116016
116786
  return result;
116017
116787
  } finally {
116018
116788
  watchdog.stop();
116019
- abortController.abort();
116789
+ runController.abort();
116020
116790
  await eventLoopPromise.catch(() => {
116021
116791
  });
116022
116792
  }
@@ -116046,7 +116816,7 @@ async function runTurnGuarded(ctx, fn2) {
116046
116816
  }
116047
116817
 
116048
116818
  // agents/index.ts
116049
- var agents = { claude, opencode };
116819
+ var agents = { claude, codex, opencode };
116050
116820
 
116051
116821
  // node_modules/.pnpm/@ark+util@0.56.0/node_modules/@ark/util/out/arrays.js
116052
116822
  var liftArray = (data) => Array.isArray(data) ? data : [data];
@@ -136378,7 +137148,7 @@ var require_core$2 = /* @__PURE__ */ __commonJSMin(((exports) => {
136378
137148
  Object.defineProperty(exports, "__esModule", { value: true });
136379
137149
  const id_1 = require_id2();
136380
137150
  const ref_1 = require_ref2();
136381
- const core11 = [
137151
+ const core12 = [
136382
137152
  "$schema",
136383
137153
  "$id",
136384
137154
  "$defs",
@@ -136388,7 +137158,7 @@ var require_core$2 = /* @__PURE__ */ __commonJSMin(((exports) => {
136388
137158
  id_1.default,
136389
137159
  ref_1.default
136390
137160
  ];
136391
- exports.default = core11;
137161
+ exports.default = core12;
136392
137162
  }));
136393
137163
  var require_limitNumber2 = /* @__PURE__ */ __commonJSMin(((exports) => {
136394
137164
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -138526,7 +139296,7 @@ var require_json_schema_2019_09 = /* @__PURE__ */ __commonJSMin(((exports) => {
138526
139296
  const metaSchema = require_schema$1();
138527
139297
  const applicator = require_applicator$1();
138528
139298
  const content = require_content$1();
138529
- const core11 = require_core$1();
139299
+ const core12 = require_core$1();
138530
139300
  const format2 = require_format3();
138531
139301
  const metadata = require_meta_data$1();
138532
139302
  const validation = require_validation$1();
@@ -138536,7 +139306,7 @@ var require_json_schema_2019_09 = /* @__PURE__ */ __commonJSMin(((exports) => {
138536
139306
  metaSchema,
138537
139307
  applicator,
138538
139308
  content,
138539
- core11,
139309
+ core12,
138540
139310
  with$data(this, format2),
138541
139311
  metadata,
138542
139312
  with$data(this, validation)
@@ -138976,7 +139746,7 @@ var require_json_schema_2020_12 = /* @__PURE__ */ __commonJSMin(((exports) => {
138976
139746
  const applicator = require_applicator2();
138977
139747
  const unevaluated = require_unevaluated();
138978
139748
  const content = require_content();
138979
- const core11 = require_core4();
139749
+ const core12 = require_core4();
138980
139750
  const format2 = require_format_annotation();
138981
139751
  const metadata = require_meta_data();
138982
139752
  const validation = require_validation2();
@@ -138987,7 +139757,7 @@ var require_json_schema_2020_12 = /* @__PURE__ */ __commonJSMin(((exports) => {
138987
139757
  applicator,
138988
139758
  unevaluated,
138989
139759
  content,
138990
- core11,
139760
+ core12,
138991
139761
  with$data(this, format2),
138992
139762
  metadata,
138993
139763
  with$data(this, validation)
@@ -150429,7 +151199,7 @@ var require_core$22 = /* @__PURE__ */ __commonJSMin2(((exports) => {
150429
151199
  Object.defineProperty(exports, "__esModule", { value: true });
150430
151200
  const id_1 = require_id3();
150431
151201
  const ref_1 = require_ref3();
150432
- const core11 = [
151202
+ const core12 = [
150433
151203
  "$schema",
150434
151204
  "$id",
150435
151205
  "$defs",
@@ -150439,7 +151209,7 @@ var require_core$22 = /* @__PURE__ */ __commonJSMin2(((exports) => {
150439
151209
  id_1.default,
150440
151210
  ref_1.default
150441
151211
  ];
150442
- exports.default = core11;
151212
+ exports.default = core12;
150443
151213
  }));
150444
151214
  var require_limitNumber3 = /* @__PURE__ */ __commonJSMin2(((exports) => {
150445
151215
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -152577,7 +153347,7 @@ var require_json_schema_2019_092 = /* @__PURE__ */ __commonJSMin2(((exports) =>
152577
153347
  const metaSchema = require_schema$12();
152578
153348
  const applicator = require_applicator$12();
152579
153349
  const content = require_content$12();
152580
- const core11 = require_core$12();
153350
+ const core12 = require_core$12();
152581
153351
  const format2 = require_format4();
152582
153352
  const metadata = require_meta_data$12();
152583
153353
  const validation = require_validation$12();
@@ -152587,7 +153357,7 @@ var require_json_schema_2019_092 = /* @__PURE__ */ __commonJSMin2(((exports) =>
152587
153357
  metaSchema,
152588
153358
  applicator,
152589
153359
  content,
152590
- core11,
153360
+ core12,
152591
153361
  with$data(this, format2),
152592
153362
  metadata,
152593
153363
  with$data(this, validation)
@@ -153027,7 +153797,7 @@ var require_json_schema_2020_122 = /* @__PURE__ */ __commonJSMin2(((exports) =>
153027
153797
  const applicator = require_applicator3();
153028
153798
  const unevaluated = require_unevaluated2();
153029
153799
  const content = require_content2();
153030
- const core11 = require_core5();
153800
+ const core12 = require_core5();
153031
153801
  const format2 = require_format_annotation2();
153032
153802
  const metadata = require_meta_data2();
153033
153803
  const validation = require_validation3();
@@ -153038,7 +153808,7 @@ var require_json_schema_2020_122 = /* @__PURE__ */ __commonJSMin2(((exports) =>
153038
153808
  applicator,
153039
153809
  unevaluated,
153040
153810
  content,
153041
- core11,
153811
+ core12,
153042
153812
  with$data(this, format2),
153043
153813
  metadata,
153044
153814
  with$data(this, validation)
@@ -172999,123 +173769,8 @@ var pbkdf2Async = promisify(pbkdf2);
172999
173769
 
173000
173770
  // utils/browser.ts
173001
173771
  import { execFileSync as execFileSync5, spawnSync as spawnSync4 } from "node:child_process";
173002
- import { existsSync as existsSync4 } from "node:fs";
173772
+ import { existsSync as existsSync5 } from "node:fs";
173003
173773
  import { dirname as dirname2 } from "node:path";
173004
-
173005
- // utils/secrets.ts
173006
- var SENSITIVE_PATTERNS = [
173007
- /_KEY$/i,
173008
- /_SECRET$/i,
173009
- /_TOKEN$/i,
173010
- /_PASSWORD$/i,
173011
- /_CREDENTIAL$/i
173012
- ];
173013
- function isSensitiveEnvName(key) {
173014
- return SENSITIVE_PATTERNS.some((p) => p.test(key));
173015
- }
173016
- var SAFE_ENV_PREFIXES = ["GITHUB_", "RUNNER_", "JAVA_HOME_", "GOROOT_"];
173017
- var SAFE_ENV_NAMES = /* @__PURE__ */ new Set([
173018
- // system
173019
- "CI",
173020
- "HOME",
173021
- "LANG",
173022
- "LOGNAME",
173023
- "PATH",
173024
- "SHELL",
173025
- "SHLVL",
173026
- "TERM",
173027
- "TMPDIR",
173028
- "TZ",
173029
- "USER",
173030
- "XDG_CONFIG_HOME",
173031
- "XDG_RUNTIME_DIR",
173032
- "DEBIAN_FRONTEND",
173033
- // runner image toolchain
173034
- "ACCEPT_EULA",
173035
- "AGENT_TOOLSDIRECTORY",
173036
- "ANDROID_HOME",
173037
- "ANDROID_NDK",
173038
- "ANDROID_NDK_HOME",
173039
- "ANDROID_NDK_LATEST_HOME",
173040
- "ANDROID_NDK_ROOT",
173041
- "ANDROID_SDK_ROOT",
173042
- "ANT_HOME",
173043
- "AZURE_EXTENSION_DIR",
173044
- "BOOTSTRAP_HASKELL_NONINTERACTIVE",
173045
- "CHROME_BIN",
173046
- "CHROMEWEBDRIVER",
173047
- "CONDA",
173048
- "DOTNET_MULTILEVEL_LOOKUP",
173049
- "DOTNET_NOLOGO",
173050
- "DOTNET_SKIP_FIRST_TIME_EXPERIENCE",
173051
- "EDGEWEBDRIVER",
173052
- "GECKOWEBDRIVER",
173053
- "GHCUP_INSTALL_BASE_PREFIX",
173054
- "GRADLE_HOME",
173055
- "JAVA_HOME",
173056
- "HOMEBREW_CLEANUP_PERIODIC_FULL_DAYS",
173057
- "HOMEBREW_NO_AUTO_UPDATE",
173058
- "ImageOS",
173059
- "ImageVersion",
173060
- "NVM_DIR",
173061
- "PIPX_BIN_DIR",
173062
- "PIPX_HOME",
173063
- "PSModulePath",
173064
- "SELENIUM_JAR_PATH",
173065
- "SGX_AESM_ADDR",
173066
- "SWIFT_PATH",
173067
- "VCPKG_INSTALLATION_ROOT"
173068
- ]);
173069
- var _userAllowlist = null;
173070
- function setEnvAllowlist(raw2) {
173071
- const names = raw2.split("\n").map((line) => line.trim()).filter(Boolean);
173072
- _userAllowlist = new Set(names);
173073
- }
173074
- function isSafeEnvVar(key) {
173075
- if (SAFE_ENV_NAMES.has(key)) return true;
173076
- return SAFE_ENV_PREFIXES.some((p) => key.startsWith(p));
173077
- }
173078
- var WORKFLOW_COMMAND_ENV_NAMES = [
173079
- "GITHUB_ENV",
173080
- "GITHUB_PATH",
173081
- "GITHUB_OUTPUT",
173082
- "GITHUB_STATE",
173083
- "GITHUB_STEP_SUMMARY",
173084
- // dropping the five paths above but keeping RUNNER_TEMP would be theatre:
173085
- // the file-command files live in `$RUNNER_TEMP/_runner_file_commands/`, so the
173086
- // directory is one `ls` away. nothing in the install path reads it (only our
173087
- // own leak-surface wipe in `setup.ts`, which runs in the parent), and package
173088
- // managers use TMPDIR.
173089
- "RUNNER_TEMP"
173090
- ];
173091
- function filterEnvForUntrustedCode() {
173092
- const env2 = filterEnv();
173093
- for (const name of WORKFLOW_COMMAND_ENV_NAMES) delete env2[name];
173094
- return env2;
173095
- }
173096
- function filterEnv() {
173097
- const filtered = {};
173098
- for (const [key, value2] of Object.entries(process.env)) {
173099
- if (value2 === void 0) continue;
173100
- const userAllowed = _userAllowlist?.has(key) ?? false;
173101
- if (isSensitiveEnvName(key) && !userAllowed) continue;
173102
- if (isSafeEnvVar(key) || userAllowed) {
173103
- filtered[key] = value2;
173104
- }
173105
- }
173106
- return filtered;
173107
- }
173108
- function resolveEnv(mode) {
173109
- if (mode === "inherit") {
173110
- return process.env;
173111
- }
173112
- if (mode === "restricted" || mode === void 0) {
173113
- return filterEnv();
173114
- }
173115
- return { ...filterEnv(), ...mode };
173116
- }
173117
-
173118
- // utils/browser.ts
173119
173774
  var CHROME_PATHS = ["/usr/bin/google-chrome-stable"];
173120
173775
  var systemChromePath;
173121
173776
  function findSystemChromePath() {
@@ -173123,7 +173778,7 @@ function findSystemChromePath() {
173123
173778
  return systemChromePath || void 0;
173124
173779
  }
173125
173780
  for (const p of CHROME_PATHS) {
173126
- if (existsSync4(p)) {
173781
+ if (existsSync5(p)) {
173127
173782
  systemChromePath = p;
173128
173783
  log2.info(`found system chrome: ${p}`);
173129
173784
  return p;
@@ -173207,8 +173862,8 @@ function closeBrowserDaemon(toolState) {
173207
173862
 
173208
173863
  // mcp/checkout.ts
173209
173864
  import { createHash as createHash2 } from "node:crypto";
173210
- import { statSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync9 } from "node:fs";
173211
- import { join as join14 } from "node:path";
173865
+ import { statSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync10 } from "node:fs";
173866
+ import { join as join15 } from "node:path";
173212
173867
 
173213
173868
  // node_modules/.pnpm/@ark+schema@0.56.0/node_modules/@ark/schema/out/shared/errors.js
173214
173869
  var ArkError = class _ArkError extends CastableBase {
@@ -180680,8 +181335,8 @@ function ensureRepoState(toolState, init) {
180680
181335
 
180681
181336
  // utils/changeImpact.ts
180682
181337
  import { spawn as spawn4 } from "node:child_process";
180683
- import { writeFileSync as writeFileSync7 } from "node:fs";
180684
- import { join as join8 } from "node:path";
181338
+ import { writeFileSync as writeFileSync8 } from "node:fs";
181339
+ import { join as join9 } from "node:path";
180685
181340
  var MAX_CANDIDATES = 24;
180686
181341
  var MAX_ATOM_LENGTH = 128;
180687
181342
  var MAX_ATOMS = 12;
@@ -181028,8 +181683,8 @@ async function createChangeImpactArtifact(ctx, params) {
181028
181683
  headSha: params.headSha,
181029
181684
  lookupError: lookup2.error
181030
181685
  });
181031
- const path4 = join8(ctx.tmpdir, `pr-${params.pullNumber}-${params.headSha.slice(0, 7)}-impact.md`);
181032
- writeFileSync7(path4, artifact.content);
181686
+ const path4 = join9(ctx.tmpdir, `pr-${params.pullNumber}-${params.headSha.slice(0, 7)}-impact.md`);
181687
+ writeFileSync8(path4, artifact.content);
181033
181688
  return {
181034
181689
  path: path4,
181035
181690
  candidateCount: candidates.selected.length,
@@ -181281,8 +181936,8 @@ function readNumber(params) {
181281
181936
  // utils/gitAuth.ts
181282
181937
  import { execSync } from "node:child_process";
181283
181938
  import { createHash } from "node:crypto";
181284
- import { readFileSync as readFileSync3, realpathSync, unlinkSync } from "node:fs";
181285
- import { join as join9 } from "node:path";
181939
+ import { readFileSync as readFileSync4, realpathSync, unlinkSync } from "node:fs";
181940
+ import { join as join10 } from "node:path";
181286
181941
 
181287
181942
  // utils/shell.ts
181288
181943
  import { spawnSync as spawnSync5 } from "node:child_process";
@@ -181343,7 +181998,7 @@ var TRANSIENT_AUTH_PATTERNS = [
181343
181998
  ];
181344
181999
  var gitBinary;
181345
182000
  function hashFile(path4) {
181346
- return createHash("sha256").update(readFileSync3(path4)).digest("hex");
182001
+ return createHash("sha256").update(readFileSync4(path4)).digest("hex");
181347
182002
  }
181348
182003
  function resolveGit() {
181349
182004
  const whichPath = execSync("which git", { encoding: "utf-8" }).trim();
@@ -181372,7 +182027,7 @@ function resolveHooksDir(cwd, gitPath) {
181372
182027
  cwd,
181373
182028
  log: false
181374
182029
  }).trim();
181375
- const hooksDir = join9(commonDir, "hooks");
182030
+ const hooksDir = join10(commonDir, "hooks");
181376
182031
  hooksDirCache.set(cwd, hooksDir);
181377
182032
  return hooksDir;
181378
182033
  }
@@ -181679,7 +182334,7 @@ function postProcessRangeDiff(raw2, contextLines = 3) {
181679
182334
  }
181680
182335
 
181681
182336
  // yes/index.ts
181682
- import { performance as performance7 } from "node:perf_hooks";
182337
+ import { performance as performance8 } from "node:perf_hooks";
181683
182338
  import { setTimeout as sleep2 } from "node:timers/promises";
181684
182339
 
181685
182340
  // node_modules/.pnpm/lru-cache@11.5.2/node_modules/lru-cache/dist/esm/node/index.min.js
@@ -182394,14 +183049,14 @@ function _op(fn2, options) {
182394
183049
  `failed to create cache key${namePrefix ? ` for ${options.name}` : ""}: ${errorMessage}`
182395
183050
  );
182396
183051
  }
182397
- const startTime = performance7.now();
183052
+ const startTime = performance8.now();
182398
183053
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
182399
183054
  log3.debug(`${namePrefix}[${timestamp}] cache lookup started`, { key });
182400
183055
  if (shouldCache && lruCache) {
182401
183056
  const cached4 = lruCache.get(key);
182402
183057
  if (cached4 !== void 0) {
182403
183058
  if (keyMap?.get(key) === void 0) keyMap?.set(key, input);
182404
- const runtime = performance7.now() - startTime;
183059
+ const runtime = performance8.now() - startTime;
182405
183060
  log3.debug(`${namePrefix}[${timestamp}] cache hit (from lru)`, {
182406
183061
  key,
182407
183062
  runtime: `${Math.round(runtime)}ms`
@@ -182420,12 +183075,12 @@ function _op(fn2, options) {
182420
183075
  defaultCacheMiss(key);
182421
183076
  const retries = options.retries ?? [];
182422
183077
  let lastError;
182423
- const execStartTime = performance7.now();
183078
+ const execStartTime = performance8.now();
182424
183079
  const promise2 = (async () => {
182425
183080
  for (let attempt = 0; attempt <= retries.length; attempt++) {
182426
183081
  try {
182427
183082
  const result = acceptsCtx ? await fn2(input, ctx) : await fn2(input);
182428
- const execRuntime2 = performance7.now() - execStartTime;
183083
+ const execRuntime2 = performance8.now() - execStartTime;
182429
183084
  const skip = options.skipCache ? options.skipCache(result) : false;
182430
183085
  if (shouldCache && lruCache && result !== null && result !== void 0 && !skip) {
182431
183086
  lruCache.set(key, result);
@@ -182478,7 +183133,7 @@ function _op(fn2, options) {
182478
183133
  }
182479
183134
  }
182480
183135
  }
182481
- const execRuntime = performance7.now() - execStartTime;
183136
+ const execRuntime = performance8.now() - execStartTime;
182482
183137
  log3.debug(`${namePrefix}[${timestamp}] function failed after all retries`, {
182483
183138
  key,
182484
183139
  runtime: `${Math.round(execRuntime)}ms`,
@@ -182543,13 +183198,13 @@ function _op(fn2, options) {
182543
183198
 
182544
183199
  // mcp/git.ts
182545
183200
  import { randomUUID as randomUUID3 } from "node:crypto";
182546
- import { writeFileSync as writeFileSync8 } from "node:fs";
182547
- import { join as join12 } from "node:path";
183201
+ import { writeFileSync as writeFileSync9 } from "node:fs";
183202
+ import { join as join13 } from "node:path";
182548
183203
 
182549
183204
  // utils/apiCommit.ts
182550
183205
  import { execFileSync as execFileSync6 } from "node:child_process";
182551
183206
  import { lstat, readlink } from "node:fs/promises";
182552
- import { join as join10 } from "node:path";
183207
+ import { join as join11 } from "node:path";
182553
183208
  var GITHUB_API = "https://api.github.com";
182554
183209
  var MAX_BLOB_BYTES = 30 * 1024 * 1024;
182555
183210
  var BLOB_UPLOAD_CONCURRENCY = 8;
@@ -182614,7 +183269,7 @@ async function assertApiCommittable(files) {
182614
183269
  const root = getRepoRoot();
182615
183270
  const attrs = $(
182616
183271
  "git",
182617
- ["check-attr", "filter", "-z", "--", ...present.map((p) => join10(root, p))],
183272
+ ["check-attr", "filter", "-z", "--", ...present.map((p) => join11(root, p))],
182618
183273
  { log: false }
182619
183274
  );
182620
183275
  const parts = attrs.split("\0");
@@ -182626,7 +183281,7 @@ async function assertApiCommittable(files) {
182626
183281
  }
182627
183282
  }
182628
183283
  for (const path4 of present) {
182629
- const stat = await lstat(join10(root, path4));
183284
+ const stat = await lstat(join11(root, path4));
182630
183285
  if (stat.isDirectory()) {
182631
183286
  throw new Error(
182632
183287
  `'${path4}' is a directory (nested repository or submodule?) \u2014 signed commits only support files and symlinks.`
@@ -182635,7 +183290,7 @@ async function assertApiCommittable(files) {
182635
183290
  }
182636
183291
  }
182637
183292
  async function createBlobEntry(params) {
182638
- const absPath = join10(params.repoRoot, params.path);
183293
+ const absPath = join11(params.repoRoot, params.path);
182639
183294
  const stat = await lstat(absPath);
182640
183295
  if (stat.size > MAX_BLOB_BYTES) {
182641
183296
  throw new Error(
@@ -182812,10 +183467,10 @@ this usually means your local branch has commits that were never pushed \u2014 s
182812
183467
  }
182813
183468
 
182814
183469
  // utils/github.ts
182815
- var core3 = __toESM(require_core(), 1);
183470
+ var core4 = __toESM(require_core(), 1);
182816
183471
  import { createSign } from "node:crypto";
182817
183472
  import { rename, writeFile } from "node:fs/promises";
182818
- import { dirname as dirname3, join as join11 } from "node:path";
183473
+ import { dirname as dirname3, join as join12 } from "node:path";
182819
183474
 
182820
183475
  // node_modules/.pnpm/@octokit+plugin-throttling@11.0.5_@octokit+core@7.0.7/node_modules/@octokit/plugin-throttling/dist-bundle/index.js
182821
183476
  var import_light = __toESM(require_light(), 1);
@@ -187013,7 +187668,7 @@ async function fetchIdTokenFromStash(creds) {
187013
187668
  throw new Error("ID token response has no value field");
187014
187669
  }
187015
187670
  if (isGitHubActions) {
187016
- core3.setSecret(body.value);
187671
+ core4.setSecret(body.value);
187017
187672
  }
187018
187673
  return body.value;
187019
187674
  }
@@ -187026,7 +187681,7 @@ function translateIdTokenError(error52) {
187026
187681
  var mintIdToken = op(
187027
187682
  async () => {
187028
187683
  try {
187029
- return await core3.getIDToken(OIDC_AUDIENCE);
187684
+ return await core4.getIDToken(OIDC_AUDIENCE);
187030
187685
  } catch (error52) {
187031
187686
  throw translateIdTokenError(error52);
187032
187687
  }
@@ -187235,7 +187890,7 @@ function getGitHubUsageSummary() {
187235
187890
  }
187236
187891
  async function writeGitHubUsageSummaryToFile(path4) {
187237
187892
  const summary2 = getGitHubUsageSummary();
187238
- const tmpPath = join11(dirname3(path4), `.usage-summary-${process.pid}.tmp`);
187893
+ const tmpPath = join12(dirname3(path4), `.usage-summary-${process.pid}.tmp`);
187239
187894
  await writeFile(tmpPath, JSON.stringify(summary2));
187240
187895
  await rename(tmpPath, path4);
187241
187896
  }
@@ -188387,8 +189042,8 @@ function countAhead(head, base, cwd) {
188387
189042
  function spillGitOutput(params) {
188388
189043
  const tempDir = process.env.PULLFROG_TEMP_DIR;
188389
189044
  if (!tempDir) throw new Error("PULLFROG_TEMP_DIR not set");
188390
- const outputPath = join12(tempDir, `git-${params.command}-${randomUUID3().slice(0, 8)}.txt`);
188391
- writeFileSync8(outputPath, params.output);
189045
+ const outputPath = join13(tempDir, `git-${params.command}-${randomUUID3().slice(0, 8)}.txt`);
189046
+ writeFileSync9(outputPath, params.output);
188392
189047
  const previewByLines = params.output.split("\n").slice(0, OVERFLOW_PREVIEW_LINES).join("\n");
188393
189048
  const preview = previewByLines.length <= OVERFLOW_PREVIEW_MAX_CHARS ? previewByLines : `${previewByLines.slice(0, OVERFLOW_PREVIEW_MAX_CHARS)}\u2026`;
188394
189049
  log2.info(
@@ -189902,9 +190557,9 @@ async function reportReviewNodeId(ctx, params) {
189902
190557
  import { execFileSync as execFileSync7, execSync as execSync2 } from "node:child_process";
189903
190558
  import { mkdtempSync as mkdtempSync2, readdirSync, realpathSync as realpathSync2, unlinkSync as unlinkSync2 } from "node:fs";
189904
190559
  import { tmpdir as tmpdir3 } from "node:os";
189905
- import { join as join13 } from "node:path";
190560
+ import { join as join14 } from "node:path";
189906
190561
  function createTempDirectory() {
189907
- const sharedTempDir = mkdtempSync2(join13(tmpdir3(), "pullfrog-"));
190562
+ const sharedTempDir = mkdtempSync2(join14(tmpdir3(), "pullfrog-"));
189908
190563
  process.env.PULLFROG_TEMP_DIR = sharedTempDir;
189909
190564
  log2.info(`\xBB created temp dir at ${sharedTempDir}`);
189910
190565
  return sharedTempDir;
@@ -189949,13 +190604,13 @@ function wipeRunnerLeakSurface() {
189949
190604
  return [];
189950
190605
  }
189951
190606
  };
189952
- const fileCommandsDir = join13(runnerTemp, "_runner_file_commands");
190607
+ const fileCommandsDir = join14(runnerTemp, "_runner_file_commands");
189953
190608
  for (const entry of listDir(fileCommandsDir)) {
189954
- tryUnlink(join13(fileCommandsDir, entry));
190609
+ tryUnlink(join14(fileCommandsDir, entry));
189955
190610
  }
189956
190611
  for (const entry of listDir(runnerTemp)) {
189957
190612
  if (entry.endsWith(".sh") || /^git-credentials-.*\.config$/.test(entry)) {
189958
- tryUnlink(join13(runnerTemp, entry));
190613
+ tryUnlink(join14(runnerTemp, entry));
189959
190614
  }
189960
190615
  }
189961
190616
  if (wiped.length > 0) {
@@ -190505,11 +191160,11 @@ function CheckoutPrTool(ctx) {
190505
191160
  headSha: checkoutSha
190506
191161
  });
190507
191162
  if (incremental.status === "ok") {
190508
- incrementalDiffPath = join14(
191163
+ incrementalDiffPath = join15(
190509
191164
  tempDir,
190510
191165
  `pr-${pull_number}-${beforeShort}-${headShort}-incremental.diff`
190511
191166
  );
190512
- writeFileSync9(incrementalDiffPath, incremental.diff);
191167
+ writeFileSync10(incrementalDiffPath, incremental.diff);
190513
191168
  log2.info(
190514
191169
  `\xBB incremental diff computed (${incremental.diff.length} bytes) \u2192 ${incrementalDiffPath}`
190515
191170
  );
@@ -190522,8 +191177,8 @@ function CheckoutPrTool(ctx) {
190522
191177
  const diffPreview = formatResult.content.split("\n").slice(0, 100).join("\n");
190523
191178
  log2.debug(`formatted diff preview (first 100 lines):
190524
191179
  ${diffPreview}`);
190525
- const diffPath = join14(tempDir, `pr-${pull_number}-${headShort}.diff`);
190526
- writeFileSync9(diffPath, formatResult.content);
191180
+ const diffPath = join15(tempDir, `pr-${pull_number}-${headShort}.diff`);
191181
+ writeFileSync10(diffPath, formatResult.content);
190527
191182
  log2.debug(`wrote diff to ${diffPath} (${formatResult.content.length} bytes)`);
190528
191183
  primary.diffCoverage = createDiffCoverageState({
190529
191184
  diffPath,
@@ -190680,8 +191335,8 @@ ${dirty}`
190680
191335
  }
190681
191336
 
190682
191337
  // mcp/checkSuite.ts
190683
- import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync10 } from "node:fs";
190684
- import { join as join15 } from "node:path";
191338
+ import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync11 } from "node:fs";
191339
+ import { join as join16 } from "node:path";
190685
191340
  var GetCheckSuiteLogs = type({
190686
191341
  check_suite_id: type.number.describe("the id from check_suite.id")
190687
191342
  });
@@ -190777,8 +191432,8 @@ function GetCheckSuiteLogsTool(ctx) {
190777
191432
  if (!tempDir) {
190778
191433
  throw new Error("PULLFROG_TEMP_DIR not set");
190779
191434
  }
190780
- const logsDir = join15(tempDir, "ci-logs");
190781
- mkdirSync7(logsDir, { recursive: true });
191435
+ const logsDir = join16(tempDir, "ci-logs");
191436
+ mkdirSync8(logsDir, { recursive: true });
190782
191437
  const jobResults = [];
190783
191438
  for (const run4 of failedRuns) {
190784
191439
  const jobs = await ctx.octokit.paginate(ctx.octokit.rest.actions.listJobsForWorkflowRun, {
@@ -190804,8 +191459,8 @@ function GetCheckSuiteLogsTool(ctx) {
190804
191459
  );
190805
191460
  }
190806
191461
  const logsText = await logsResult.text();
190807
- const logPath = join15(logsDir, `job-${job.id}.log`);
190808
- writeFileSync10(logPath, logsText);
191462
+ const logPath = join16(logsDir, `job-${job.id}.log`);
191463
+ writeFileSync11(logPath, logsText);
190809
191464
  const analysis = analyzeLog(logsText, 80);
190810
191465
  const failedSteps = job.steps?.filter((s) => s.conclusion === "failure").map((s) => `Step ${s.number}: ${s.name}`) ?? [];
190811
191466
  jobResults.push({
@@ -190853,8 +191508,8 @@ function GetCheckSuiteLogsTool(ctx) {
190853
191508
  }
190854
191509
 
190855
191510
  // mcp/commitInfo.ts
190856
- import { writeFileSync as writeFileSync11 } from "node:fs";
190857
- import { join as join16 } from "node:path";
191511
+ import { writeFileSync as writeFileSync12 } from "node:fs";
191512
+ import { join as join17 } from "node:path";
190858
191513
  var CommitInfo = type({
190859
191514
  sha: type.string.describe("the commit SHA (full or abbreviated) to fetch")
190860
191515
  });
@@ -190878,8 +191533,8 @@ function CommitInfoTool(ctx) {
190878
191533
  "PULLFROG_TEMP_DIR not set - get_commit_info must run in pullfrog action context"
190879
191534
  );
190880
191535
  }
190881
- const diffFile = join16(tempDir, `commit-${sha.slice(0, 7)}.diff`);
190882
- writeFileSync11(diffFile, formatResult.content);
191536
+ const diffFile = join17(tempDir, `commit-${sha.slice(0, 7)}.diff`);
191537
+ writeFileSync12(diffFile, formatResult.content);
190883
191538
  log2.debug(`wrote commit diff to ${diffFile} (${formatResult.content.length} bytes)`);
190884
191539
  return {
190885
191540
  sha: data.sha,
@@ -191012,17 +191667,17 @@ function ReopenCurrentTool(ctx) {
191012
191667
  }
191013
191668
 
191014
191669
  // prep/index.ts
191015
- import { performance as performance8 } from "node:perf_hooks";
191670
+ import { performance as performance9 } from "node:perf_hooks";
191016
191671
 
191017
191672
  // prep/installNodeDependencies.ts
191018
- import { existsSync as existsSync6 } from "node:fs";
191019
- import { join as join18 } from "node:path";
191673
+ import { existsSync as existsSync7 } from "node:fs";
191674
+ import { join as join19 } from "node:path";
191020
191675
 
191021
191676
  // utils/packageManager.ts
191022
191677
  var import_semver2 = __toESM(require_semver2(), 1);
191023
- import { existsSync as existsSync5 } from "node:fs";
191678
+ import { existsSync as existsSync6 } from "node:fs";
191024
191679
  import { mkdir, readFile as readFile2 } from "node:fs/promises";
191025
- import { delimiter, join as join17 } from "node:path";
191680
+ import { delimiter, join as join18 } from "node:path";
191026
191681
  var SUPPORTED_NAMES = ["npm", "pnpm", "yarn", "bun"];
191027
191682
  var COREPACK_MANAGED = ["pnpm", "yarn"];
191028
191683
  function isSupported(name) {
@@ -191060,8 +191715,8 @@ function parseDevEnginesField(field) {
191060
191715
  };
191061
191716
  }
191062
191717
  async function resolvePackageManagerSpec(cwd) {
191063
- const pkgPath = join17(cwd, "package.json");
191064
- if (!existsSync5(pkgPath)) return null;
191718
+ const pkgPath = join18(cwd, "package.json");
191719
+ if (!existsSync6(pkgPath)) return null;
191065
191720
  let pkg;
191066
191721
  try {
191067
191722
  pkg = JSON.parse(await readFile2(pkgPath, "utf-8"));
@@ -191119,7 +191774,7 @@ async function currentVersion(name) {
191119
191774
  return result.stdout.trim();
191120
191775
  }
191121
191776
  function packageManagerBinDir(tmpdir4) {
191122
- return join17(tmpdir4, "pm-bin");
191777
+ return join18(tmpdir4, "pm-bin");
191123
191778
  }
191124
191779
  async function ensurePackageManager(params) {
191125
191780
  const spec = params.spec;
@@ -191182,7 +191837,7 @@ async function installFallback(name, installSpec) {
191182
191837
  });
191183
191838
  if (result.exitCode !== 0) return result.stderr || `failed to install ${name}`;
191184
191839
  if (name === "deno") {
191185
- const denoPath = join17(process.env.HOME || "", ".deno", "bin");
191840
+ const denoPath = join18(process.env.HOME || "", ".deno", "bin");
191186
191841
  process.env.PATH = `${denoPath}:${process.env.PATH}`;
191187
191842
  }
191188
191843
  log2.info(`\xBB installed ${name}`);
@@ -191215,8 +191870,8 @@ async function isCommandAvailable2(command) {
191215
191870
  var installNodeDependencies = {
191216
191871
  name: "installNodeDependencies",
191217
191872
  shouldRun: () => {
191218
- const packageJsonPath = join18(process.cwd(), "package.json");
191219
- return existsSync6(packageJsonPath);
191873
+ const packageJsonPath = join19(process.cwd(), "package.json");
191874
+ return existsSync7(packageJsonPath);
191220
191875
  },
191221
191876
  run: async (options) => {
191222
191877
  const declared = await resolvePackageManagerSpec(process.cwd());
@@ -191310,14 +191965,14 @@ ${errorMessage}`]
191310
191965
  };
191311
191966
 
191312
191967
  // prep/installPythonDependencies.ts
191313
- import { existsSync as existsSync7, readFileSync as readFileSync4 } from "node:fs";
191314
- import { join as join19 } from "node:path";
191968
+ import { existsSync as existsSync8, readFileSync as readFileSync5 } from "node:fs";
191969
+ import { join as join20 } from "node:path";
191315
191970
  function declaresBuildSystem(path4) {
191316
- return /^\s*\[\s*build-system\s*\]/m.test(readFileSync4(path4, "utf8"));
191971
+ return /^\s*\[\s*build-system\s*\]/m.test(readFileSync5(path4, "utf8"));
191317
191972
  }
191318
191973
  function configApplies(config3, cwd) {
191319
- const path4 = join19(cwd, config3.file);
191320
- return existsSync7(path4) && (config3.applies?.(path4) ?? true);
191974
+ const path4 = join20(cwd, config3.file);
191975
+ return existsSync8(path4) && (config3.applies?.(path4) ?? true);
191321
191976
  }
191322
191977
  var PYTHON_CONFIGS = [
191323
191978
  {
@@ -191471,7 +192126,7 @@ var installPythonDependencies = {
191471
192126
  var prepSteps = [installNodeDependencies, installPythonDependencies];
191472
192127
  async function runPrepPhase(options) {
191473
192128
  log2.debug("\xBB starting prep phase...");
191474
- const startTime = performance8.now();
192129
+ const startTime = performance9.now();
191475
192130
  const results = [];
191476
192131
  const preDirty = await dirtyTrackedPaths();
191477
192132
  try {
@@ -191493,7 +192148,7 @@ async function runPrepPhase(options) {
191493
192148
  } finally {
191494
192149
  await restoreDirtiedSince({ before: preDirty, actor: "prep" });
191495
192150
  }
191496
- const totalDurationMs = performance8.now() - startTime;
192151
+ const totalDurationMs = performance9.now() - startTime;
191497
192152
  log2.debug(`\xBB prep phase completed (${Math.round(totalDurationMs)}ms)`);
191498
192153
  return results;
191499
192154
  }
@@ -191638,15 +192293,15 @@ function AwaitDependencyInstallationTool(ctx) {
191638
192293
  }
191639
192294
 
191640
192295
  // mcp/gh.ts
191641
- import { mkdirSync as mkdirSync8 } from "node:fs";
191642
- import { join as join21 } from "node:path";
192296
+ import { mkdirSync as mkdirSync9 } from "node:fs";
192297
+ import { join as join22 } from "node:path";
191643
192298
 
191644
192299
  // mcp/shell.ts
191645
192300
  import { spawn as spawn5, spawnSync as spawnSync6 } from "node:child_process";
191646
192301
  import { randomUUID as randomUUID4 } from "node:crypto";
191647
- import { closeSync, openSync, writeFileSync as writeFileSync12 } from "node:fs";
192302
+ import { closeSync, openSync, writeFileSync as writeFileSync13 } from "node:fs";
191648
192303
  import { userInfo as userInfo2 } from "node:os";
191649
- import { join as join20 } from "node:path";
192304
+ import { join as join21 } from "node:path";
191650
192305
  import { setTimeout as sleep3 } from "node:timers/promises";
191651
192306
  var ShellParams = type({
191652
192307
  command: "string",
@@ -191909,8 +192564,8 @@ ${spawnError}` : spawnError;
191909
192564
  }
191910
192565
  function capOutput(output) {
191911
192566
  if (output.length <= MAX_OUTPUT_CHARS) return output;
191912
- const fullPath = join20(getTempDir(), `shell-${randomUUID4().slice(0, 8)}.log`);
191913
- writeFileSync12(fullPath, output);
192567
+ const fullPath = join21(getTempDir(), `shell-${randomUUID4().slice(0, 8)}.log`);
192568
+ writeFileSync13(fullPath, output);
191914
192569
  const elided = output.length - MAX_OUTPUT_CHARS;
191915
192570
  return `... [${elided} chars truncated; full output saved to ${fullPath}] ...
191916
192571
  ${output.slice(-MAX_OUTPUT_CHARS)}`;
@@ -191964,8 +192619,8 @@ Do NOT use this tool for git commands \u2014 use the dedicated git tools instead
191964
192619
  if (params.background) {
191965
192620
  const tempDir = getTempDir();
191966
192621
  const handle = `bg-${randomUUID4().slice(0, 8)}`;
191967
- const outputPath = join20(tempDir, `${handle}.log`);
191968
- const pidPath = join20(tempDir, `${handle}.pid`);
192622
+ const outputPath = join21(tempDir, `${handle}.log`);
192623
+ const pidPath = join21(tempDir, `${handle}.pid`);
191969
192624
  const logFd = openSync(outputPath, "a");
191970
192625
  let proc;
191971
192626
  try {
@@ -191982,7 +192637,7 @@ Do NOT use this tool for git commands \u2014 use the dedicated git tools instead
191982
192637
  throw new Error("failed to start background process");
191983
192638
  }
191984
192639
  proc.unref();
191985
- writeFileSync12(pidPath, `${proc.pid}
192640
+ writeFileSync13(pidPath, `${proc.pid}
191986
192641
  `);
191987
192642
  ctx.toolState.backgroundProcesses.set(handle, { pid: proc.pid, outputPath, pidPath });
191988
192643
  return {
@@ -192062,8 +192717,8 @@ function GhTool(ctx) {
192062
192717
  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.`,
192063
192718
  parameters: GhParams,
192064
192719
  execute: execute(async (params) => {
192065
- const configDir = join21(ctx.tmpdir, "gh-config");
192066
- mkdirSync8(configDir, { recursive: true });
192720
+ const configDir = join22(ctx.tmpdir, "gh-config");
192721
+ mkdirSync9(configDir, { recursive: true });
192067
192722
  const command = ["gh", ...params.args.map(shellQuote)].join(" ");
192068
192723
  const result = await runSandboxed({
192069
192724
  command,
@@ -192786,8 +193441,8 @@ function PullRequestInfoTool(ctx) {
192786
193441
  }
192787
193442
 
192788
193443
  // mcp/reviewComments.ts
192789
- import { writeFileSync as writeFileSync13 } from "node:fs";
192790
- import { join as join22 } from "node:path";
193444
+ import { writeFileSync as writeFileSync14 } from "node:fs";
193445
+ import { join as join23 } from "node:path";
192791
193446
  var COMMENTS_PER_THREAD = 50;
192792
193447
  var REVIEW_THREADS_QUERY = `
192793
193448
  query ($owner: String!, $name: String!, $prNumber: Int!, $cursor: String) {
@@ -193260,8 +193915,8 @@ function GetReviewCommentsTool(ctx) {
193260
193915
  throw new Error("PULLFROG_TEMP_DIR not set");
193261
193916
  }
193262
193917
  const filename = `review-${params.review_id}-threads.md`;
193263
- const commentsPath = join22(tempDir, filename);
193264
- writeFileSync13(commentsPath, formatted.content);
193918
+ const commentsPath = join23(tempDir, filename);
193919
+ writeFileSync14(commentsPath, formatted.content);
193265
193920
  log2.debug(`wrote ${threadBlocks.length} threads to ${commentsPath}`);
193266
193921
  return {
193267
193922
  review_id: params.review_id,
@@ -193594,8 +194249,8 @@ function UploadFileTool(ctx) {
193594
194249
  }
193595
194250
 
193596
194251
  // mcp/xrepo.ts
193597
- import { mkdirSync as mkdirSync9, rmSync as rmSync3 } from "node:fs";
193598
- import { join as join23 } from "node:path";
194252
+ import { mkdirSync as mkdirSync10, rmSync as rmSync3 } from "node:fs";
194253
+ import { join as join24 } from "node:path";
193599
194254
  function assertValidRepoName(name) {
193600
194255
  if (!/^[A-Za-z0-9._-]+$/.test(name) || name.startsWith("-") || name.includes("..")) {
193601
194256
  throw new Error(
@@ -193675,8 +194330,8 @@ function CheckoutRepoTool(ctx) {
193675
194330
  return { path: existing.dir, access: existing.access, note: "already checked out." };
193676
194331
  }
193677
194332
  const access = accessFor(ctx, repo);
193678
- const dir = join23(ctx.tmpdir, "xrepo", repo);
193679
- mkdirSync9(dir, { recursive: true });
194333
+ const dir = join24(ctx.tmpdir, "xrepo", repo);
194334
+ mkdirSync10(dir, { recursive: true });
193680
194335
  const state = ensureRepoState(ctx.toolState, { owner, name: repo, dir, access });
193681
194336
  const rc = resolveRepoCtx(ctx, repo);
193682
194337
  try {
@@ -193922,7 +194577,10 @@ function hasEnvVar2(name) {
193922
194577
  return typeof val === "string" && val.length > 0;
193923
194578
  }
193924
194579
  function hasClaudeCodeAuth() {
193925
- return hasEnvVar2("CLAUDE_CODE_OAUTH_TOKEN") || hasEnvVar2("ANTHROPIC_API_KEY");
194580
+ return hasEnvVar2("CLAUDE_CODE_OAUTH_TOKEN") || hasEnvVar2("ANTHROPIC_API_KEY") || hasEnvVar2("ANTHROPIC_AUTH_TOKEN");
194581
+ }
194582
+ function hasCodexAuth() {
194583
+ return hasEnvVar2("CODEX_AUTH_JSON") || hasEnvVar2("OPENAI_API_KEY");
193926
194584
  }
193927
194585
  function hasBedrockAuth() {
193928
194586
  return hasEnvVar2("AWS_BEARER_TOKEN_BEDROCK") || hasEnvVar2("AWS_ACCESS_KEY_ID") && hasEnvVar2("AWS_SECRET_ACCESS_KEY");
@@ -193950,6 +194608,15 @@ function resolveSlug(slug2) {
193950
194608
  }
193951
194609
  return vertexId;
193952
194610
  }
194611
+ if (alias?.routing === "azure") {
194612
+ const deployment = process.env[AZURE_DEPLOYMENT_ENV]?.trim();
194613
+ if (!deployment) {
194614
+ throw new Error(
194615
+ `${AZURE_DEPLOYMENT_ENV} env var is required when the model is set to "${slug2}". set it to the name of your Azure OpenAI deployment (not the model it serves \u2014 Azure routes on the deployment name). see https://docs.pullfrog.com/azure for setup.`
194616
+ );
194617
+ }
194618
+ return `${AZURE_PROVIDER}/${deployment}`;
194619
+ }
193953
194620
  if (alias?.routing === "openai-compatible") {
193954
194621
  const modelId = process.env[OPENAI_COMPATIBLE_MODEL_ENV]?.trim();
193955
194622
  if (!modelId) {
@@ -193988,6 +194655,7 @@ function resolveAgent(ctx) {
193988
194655
  }
193989
194656
  log2.warning(`\xBB unknown PULLFROG_AGENT="${envAgent}" \u2014 falling through to auto-select`);
193990
194657
  }
194658
+ if (ctx.proxyModel) return agents.opencode;
193991
194659
  if (ctx.model && hasBedrockAuth() && process.env[BEDROCK_MODEL_ID_ENV]?.trim() === ctx.model) {
193992
194660
  return isBedrockAnthropicId(ctx.model) ? agents.claude : agents.opencode;
193993
194661
  }
@@ -193997,12 +194665,17 @@ function resolveAgent(ctx) {
193997
194665
  if (ctx.model) {
193998
194666
  try {
193999
194667
  const provider2 = getModelProvider(ctx.model);
194000
- if (provider2 === "anthropic" && hasClaudeCodeAuth()) {
194001
- return agents.claude;
194002
- }
194668
+ if (provider2 === "anthropic" && hasClaudeCodeAuth()) return agents.claude;
194669
+ if (provider2 === "openai" && ctx.codexAgent && hasCodexAuth()) return agents.codex;
194003
194670
  } catch {
194004
194671
  }
194005
194672
  }
194673
+ if (!ctx.model) {
194674
+ if (hasEnvVar2("ANTHROPIC_AUTH_TOKEN") && !hasEnvVar2("ANTHROPIC_API_KEY") && !hasEnvVar2("CLAUDE_CODE_OAUTH_TOKEN")) {
194675
+ return agents.claude;
194676
+ }
194677
+ if (ctx.codexAgent && hasCodexAuth() && !hasClaudeCodeAuth()) return agents.codex;
194678
+ }
194006
194679
  return agents.opencode;
194007
194680
  }
194008
194681
 
@@ -194100,10 +194773,37 @@ auto-compaction is disabled, so long runs grow until your endpoint refuses them.
194100
194773
 
194101
194774
  for full setup instructions, see https://docs.pullfrog.com/openai-compatible`;
194102
194775
  }
194776
+ function buildAzureSetupError(params) {
194777
+ const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
194778
+ return `Azure OpenAI selected but required configuration is missing: ${params.missing.join(", ")}.
194779
+
194780
+ only the API key is sensitive \u2014 add it as a secret at ${githubSecretsUrl}. the rest is plain workflow \`env:\`:
194781
+
194782
+ ${AZURE_RESOURCE_NAME_ENV}: <name>
194783
+ ${AZURE_API_KEY_ENV}: \${{ secrets.${AZURE_API_KEY_ENV} }}
194784
+ ${AZURE_DEPLOYMENT_ENV}: <deployment-name>
194785
+ ${AZURE_CONTEXT_ENV}: "400000"
194786
+ ${AZURE_MAX_OUTPUT_ENV}: "128000"
194787
+
194788
+ ${AZURE_RESOURCE_NAME_ENV} is the \`<name>\` in your endpoint https://<name>.openai.azure.com.
194789
+ ${AZURE_DEPLOYMENT_ENV} is the name of the deployment, which is not necessarily the name of
194790
+ the model it serves \u2014 Azure routes on the deployment name.
194791
+
194792
+ set the last two to the real limits of whichever model your deployment serves. Pullfrog can't
194793
+ discover them \u2014 a deployment name carries no catalog metadata \u2014 and without them completions
194794
+ are capped at 32000 tokens (rejected outright by models with a smaller cap) and auto-compaction
194795
+ is disabled, so long runs grow until Azure refuses them.
194796
+
194797
+ for full setup instructions, see https://docs.pullfrog.com/azure`;
194798
+ }
194103
194799
  function hasEnvVar3(name) {
194104
194800
  const value2 = process.env[name];
194105
194801
  return typeof value2 === "string" && value2.length > 0;
194106
194802
  }
194803
+ function modelHasRuntimeAuth(model) {
194804
+ const authVars = [...getModelEnvVars(model), ...getModelManagedCredentials(model)];
194805
+ return authVars.length === 0 || authVars.some(hasEnvVar3);
194806
+ }
194107
194807
  function hasPositiveNumberEnvVar(name) {
194108
194808
  return Number(process.env[name]) > 0;
194109
194809
  }
@@ -194122,6 +194822,17 @@ function validateOpenAICompatibleSetup(params) {
194122
194822
  );
194123
194823
  }
194124
194824
  }
194825
+ function validateAzureSetup(params) {
194826
+ const missing = [];
194827
+ if (!hasEnvVar3(AZURE_API_KEY_ENV)) missing.push(AZURE_API_KEY_ENV);
194828
+ if (!hasEnvVar3(AZURE_RESOURCE_NAME_ENV)) missing.push(AZURE_RESOURCE_NAME_ENV);
194829
+ if (!hasEnvVar3(AZURE_DEPLOYMENT_ENV)) missing.push(AZURE_DEPLOYMENT_ENV);
194830
+ if (!hasPositiveNumberEnvVar(AZURE_CONTEXT_ENV)) missing.push(AZURE_CONTEXT_ENV);
194831
+ if (!hasPositiveNumberEnvVar(AZURE_MAX_OUTPUT_ENV)) missing.push(AZURE_MAX_OUTPUT_ENV);
194832
+ if (missing.length > 0) {
194833
+ throw new Error(buildAzureSetupError({ owner: params.owner, name: params.name, missing }));
194834
+ }
194835
+ }
194125
194836
  function validateBedrockSetup(params) {
194126
194837
  const hasAuth = hasEnvVar3("AWS_BEARER_TOKEN_BEDROCK") || hasEnvVar3("AWS_ACCESS_KEY_ID") && hasEnvVar3("AWS_SECRET_ACCESS_KEY");
194127
194838
  const missing = [];
@@ -194145,6 +194856,12 @@ function validateVertexSetup(params) {
194145
194856
  throw new Error(buildVertexSetupError({ owner: params.owner, name: params.name, missing }));
194146
194857
  }
194147
194858
  }
194859
+ function hasSingleProviderAuth(agentName) {
194860
+ if (agentName === "codex") {
194861
+ return hasEnvVar3("OPENAI_API_KEY") || hasEnvVar3("CODEX_AUTH_JSON");
194862
+ }
194863
+ return hasEnvVar3("ANTHROPIC_API_KEY") || hasEnvVar3("ANTHROPIC_AUTH_TOKEN") || hasEnvVar3("CLAUDE_CODE_OAUTH_TOKEN");
194864
+ }
194148
194865
  function validateAgentApiKey(params) {
194149
194866
  if (params.model) {
194150
194867
  const alias = resolveDisplayAlias(params.model);
@@ -194160,6 +194877,10 @@ function validateAgentApiKey(params) {
194160
194877
  validateOpenAICompatibleSetup({ owner: params.owner, name: params.name });
194161
194878
  return;
194162
194879
  }
194880
+ if (params.model.startsWith(`${AZURE_PROVIDER}/`)) {
194881
+ validateAzureSetup({ owner: params.owner, name: params.name });
194882
+ return;
194883
+ }
194163
194884
  if (!params.model.includes("/")) {
194164
194885
  if (process.env[VERTEX_MODEL_ID_ENV]?.trim() === params.model) {
194165
194886
  validateVertexSetup({ owner: params.owner, name: params.name });
@@ -194183,7 +194904,7 @@ function validateAgentApiKey(params) {
194183
194904
  })
194184
194905
  );
194185
194906
  }
194186
- if (hasEnvVar3("ANTHROPIC_API_KEY") || hasEnvVar3("CLAUDE_CODE_OAUTH_TOKEN")) return;
194907
+ if (hasSingleProviderAuth(params.agent.name)) return;
194187
194908
  throw new Error(
194188
194909
  buildKeyError({
194189
194910
  owner: params.owner,
@@ -194195,7 +194916,7 @@ function validateAgentApiKey(params) {
194195
194916
  );
194196
194917
  }
194197
194918
  if (params.agent.name === "opencode") {
194198
- if (params.authorized.size > 0) return;
194919
+ if ([...params.authorized].some(modelHasRuntimeAuth)) return;
194199
194920
  const reason = getModelsFailure();
194200
194921
  if (reason) throw new Error(reason);
194201
194922
  throw new Error(
@@ -194207,7 +194928,7 @@ function validateAgentApiKey(params) {
194207
194928
  })
194208
194929
  );
194209
194930
  }
194210
- if (hasEnvVar3("ANTHROPIC_API_KEY") || hasEnvVar3("CLAUDE_CODE_OAUTH_TOKEN")) return;
194931
+ if (hasSingleProviderAuth(params.agent.name)) return;
194211
194932
  throw new Error(
194212
194933
  buildKeyError({
194213
194934
  owner: params.owner,
@@ -194471,7 +195192,8 @@ var PROBES = {
194471
195192
  request: (value2) => ({
194472
195193
  url: "https://api.anthropic.com/v1/models",
194473
195194
  headers: { "x-api-key": value2, "anthropic-version": "2023-06-01" }
194474
- })
195195
+ }),
195196
+ hostConfigurable: true
194475
195197
  },
194476
195198
  OPENROUTER_API_KEY: {
194477
195199
  request: (value2) => ({
@@ -194520,12 +195242,18 @@ function hasEnvVar4(name) {
194520
195242
  const value2 = process.env[name];
194521
195243
  return typeof value2 === "string" && value2.length > 0;
194522
195244
  }
195245
+ var HOST_OVERRIDES = {
195246
+ ANTHROPIC_API_KEY: "ANTHROPIC_BASE_URL",
195247
+ CLAUDE_CODE_OAUTH_TOKEN: "ANTHROPIC_BASE_URL"
195248
+ };
194523
195249
  function envVarsFor(model) {
194524
195250
  return model.includes("/") ? getModelEnvVars(model) : [];
194525
195251
  }
194526
195252
  async function checkOne(params) {
194527
195253
  const value2 = process.env[params.envVar];
194528
195254
  if (!value2) return null;
195255
+ const hostOverride = HOST_OVERRIDES[params.envVar];
195256
+ if (hostOverride && hasEnvVar4(hostOverride)) return null;
194529
195257
  if (params.envVar === "CLAUDE_CODE_OAUTH_TOKEN") {
194530
195258
  const preflight = await preflightClaudeSubscription({
194531
195259
  token: value2,
@@ -194580,9 +195308,9 @@ async function checkConfiguredCredentials(params) {
194580
195308
 
194581
195309
  // utils/gitAuthServer.ts
194582
195310
  import { randomUUID as randomUUID5 } from "node:crypto";
194583
- import { writeFileSync as writeFileSync14 } from "node:fs";
195311
+ import { writeFileSync as writeFileSync15 } from "node:fs";
194584
195312
  import { createServer as createServer3 } from "node:http";
194585
- import { join as join24 } from "node:path";
195313
+ import { join as join25 } from "node:path";
194586
195314
  var REVOKED_TRAP_MS = 6e4;
194587
195315
  function revokeGitHubToken(token) {
194588
195316
  fetch("https://api.github.com/installation/token", {
@@ -194651,7 +195379,7 @@ async function startGitAuthServer(tmpdir4) {
194651
195379
  function writeAskpassScript(code) {
194652
195380
  const scriptId = randomUUID5();
194653
195381
  const scriptName = `askpass-${scriptId}.js`;
194654
- const scriptPath = join24(tmpdir4, scriptName);
195382
+ const scriptPath = join25(tmpdir4, scriptName);
194655
195383
  const content = [
194656
195384
  `#!/usr/bin/env node`,
194657
195385
  `var a=process.argv[2]||"";`,
@@ -194664,7 +195392,7 @@ async function startGitAuthServer(tmpdir4) {
194664
195392
  `r.on("end",function(){process.stdout.write(d+"\\n")})`,
194665
195393
  `}).on("error",function(){process.exit(1)})}`
194666
195394
  ].join("\n");
194667
- writeFileSync14(scriptPath, content, { mode: 448 });
195395
+ writeFileSync15(scriptPath, content, { mode: 448 });
194668
195396
  return scriptPath;
194669
195397
  }
194670
195398
  async function close() {
@@ -195103,7 +195831,7 @@ function resolveInstructions(ctx) {
195103
195831
 
195104
195832
  // utils/learnings.ts
195105
195833
  import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
195106
- import { dirname as dirname4, join as join25 } from "node:path";
195834
+ import { dirname as dirname4, join as join26 } from "node:path";
195107
195835
 
195108
195836
  // utils/learningsTruncate.ts
195109
195837
  var MAX_LEARNINGS_LENGTH = 1e5;
@@ -195121,10 +195849,10 @@ function truncateAtLineBoundary(body, cap) {
195121
195849
  var LEARNINGS_FILE_NAME = "pullfrog-learnings.md";
195122
195850
  var XREPO_LEARNINGS_FILE_NAME = "pullfrog-xrepo-learnings.md";
195123
195851
  function learningsFilePath(tmpdir4) {
195124
- return join25(tmpdir4, LEARNINGS_FILE_NAME);
195852
+ return join26(tmpdir4, LEARNINGS_FILE_NAME);
195125
195853
  }
195126
195854
  function xrepoLearningsFilePath(tmpdir4) {
195127
- return join25(tmpdir4, XREPO_LEARNINGS_FILE_NAME);
195855
+ return join26(tmpdir4, XREPO_LEARNINGS_FILE_NAME);
195128
195856
  }
195129
195857
  async function seedLearningsFile(params) {
195130
195858
  const path4 = learningsFilePath(params.tmpdir);
@@ -195240,7 +195968,7 @@ async function persistXrepoLearnings(ctx) {
195240
195968
  // utils/modelAccess.ts
195241
195969
  function decideModelAccess(input) {
195242
195970
  if (!input.modelExplicit || !input.model) return { kind: "ok" };
195243
- const byokAuthorized = !!input.resolvedModel && (input.authorized.has(input.resolvedModel) || !input.resolvedModel.includes("/") || input.resolvedModel.startsWith(`${OPENAI_COMPATIBLE_PROVIDER}/`));
195971
+ const byokAuthorized = !!input.resolvedModel && (input.authorized.has(input.resolvedModel) || !input.resolvedModel.includes("/") || input.resolvedModel.startsWith(`${OPENAI_COMPATIBLE_PROVIDER}/`) || input.resolvedModel.startsWith(`${AZURE_PROVIDER}/`));
195244
195972
  if (input.proxyActive) {
195245
195973
  const target = resolveOpenRouterModel(input.model);
195246
195974
  if (input.oss) {
@@ -195285,7 +196013,7 @@ function buildModelAccessError(input) {
195285
196013
  }
195286
196014
 
195287
196015
  // utils/normalizeEnv.ts
195288
- var core4 = __toESM(require_core(), 1);
196016
+ var core5 = __toESM(require_core(), 1);
195289
196017
  function hasControlCharacter(value2) {
195290
196018
  return [...value2].some((ch) => {
195291
196019
  const code = ch.charCodeAt(0);
@@ -195311,7 +196039,7 @@ function sanitizeSecret(key, value2) {
195311
196039
  `\xBB stripped whitespace from ${key} (whitespace in secret values breaks GitHub Actions log masking)`
195312
196040
  );
195313
196041
  }
195314
- core4.setSecret(trimmed);
196042
+ core5.setSecret(trimmed);
195315
196043
  return trimmed;
195316
196044
  }
195317
196045
  function normalizeEnv() {
@@ -195355,7 +196083,7 @@ function normalizeEnv() {
195355
196083
  }
195356
196084
 
195357
196085
  // utils/overrides.ts
195358
- var core5 = __toESM(require_core(), 1);
196086
+ var core6 = __toESM(require_core(), 1);
195359
196087
  var DENIED_OVERRIDE_NAMES = /* @__PURE__ */ new Set([
195360
196088
  "GITHUB_TOKEN",
195361
196089
  "GH_TOKEN",
@@ -195401,7 +196129,7 @@ function applyOverrides(params) {
195401
196129
  denied.push(key);
195402
196130
  continue;
195403
196131
  }
195404
- if (value2.length > 0) core5.setSecret(value2);
196132
+ if (value2.length > 0) core6.setSecret(value2);
195405
196133
  params.env[key] = value2;
195406
196134
  applied.push(key);
195407
196135
  }
@@ -195410,8 +196138,8 @@ function applyOverrides(params) {
195410
196138
  }
195411
196139
 
195412
196140
  // utils/payload.ts
195413
- var core6 = __toESM(require_core(), 1);
195414
- import { readFileSync as readFileSync6 } from "node:fs";
196141
+ var core7 = __toESM(require_core(), 1);
196142
+ import { readFileSync as readFileSync7 } from "node:fs";
195415
196143
  import { isAbsolute as isAbsolute2, resolve as resolve2 } from "node:path";
195416
196144
 
195417
196145
  // utils/versioning.ts
@@ -195498,8 +196226,8 @@ function resolveCwd(cwd) {
195498
196226
  return workspace ? resolve2(workspace, cwd) : cwd;
195499
196227
  }
195500
196228
  function resolvePromptInput() {
195501
- const promptInput = core6.getInput("prompt");
195502
- const promptFile = core6.getInput("prompt_file");
196229
+ const promptInput = core7.getInput("prompt");
196230
+ const promptFile = core7.getInput("prompt_file");
195503
196231
  if (promptInput && promptFile) {
195504
196232
  throw new Error("set exactly one of 'prompt' or 'prompt_file' inputs, not both.");
195505
196233
  }
@@ -195525,7 +196253,7 @@ function resolvePromptInput() {
195525
196253
  function resolvePromptFile(input) {
195526
196254
  const workspace = process.env.GITHUB_WORKSPACE;
195527
196255
  const path4 = isAbsolute2(input) ? input : workspace ? resolve2(workspace, input) : resolve2(input);
195528
- const content = readFileSync6(path4, "utf-8");
196256
+ const content = readFileSync7(path4, "utf-8");
195529
196257
  if (!content.trim()) {
195530
196258
  throw new Error(`prompt_file ${JSON.stringify(input)} is empty.`);
195531
196259
  }
@@ -195533,7 +196261,7 @@ function resolvePromptFile(input) {
195533
196261
  }
195534
196262
  function warnIfDeprecatedStatusChecks(value2) {
195535
196263
  if (value2 !== void 0) {
195536
- core6.warning(
196264
+ core7.warning(
195537
196265
  "`status_checks` is deprecated. Both checks are now repository settings \u2014 open the Pullfrog console for this repo (Automations \u2192 Review PRs) and set them there, then remove `status_checks` from your workflow. It keeps working until you do."
195538
196266
  );
195539
196267
  }
@@ -195541,15 +196269,15 @@ function warnIfDeprecatedStatusChecks(value2) {
195541
196269
  }
195542
196270
  function resolveNonPromptInputs() {
195543
196271
  return Inputs.omit("prompt", "prompt_file").assert({
195544
- model: core6.getInput("model") || void 0,
195545
- effort: core6.getInput("effort") || void 0,
195546
- debug: core6.getInput("debug") || void 0,
195547
- timeout: core6.getInput("timeout") || void 0,
195548
- cwd: core6.getInput("cwd") || void 0,
195549
- push: core6.getInput("push") || void 0,
195550
- shell: core6.getInput("shell") || void 0,
195551
- status_checks: warnIfDeprecatedStatusChecks(core6.getInput("status_checks") || void 0),
195552
- progress_comments: core6.getInput("progress_comments") || void 0
196272
+ model: core7.getInput("model") || void 0,
196273
+ effort: core7.getInput("effort") || void 0,
196274
+ debug: core7.getInput("debug") || void 0,
196275
+ timeout: core7.getInput("timeout") || void 0,
196276
+ cwd: core7.getInput("cwd") || void 0,
196277
+ push: core7.getInput("push") || void 0,
196278
+ shell: core7.getInput("shell") || void 0,
196279
+ status_checks: warnIfDeprecatedStatusChecks(core7.getInput("status_checks") || void 0),
196280
+ progress_comments: core7.getInput("progress_comments") || void 0
195553
196281
  });
195554
196282
  }
195555
196283
  function resolvePayload(resolvedPromptInput, repoSettings) {
@@ -195617,7 +196345,7 @@ function resolvePayload(resolvedPromptInput, repoSettings) {
195617
196345
  };
195618
196346
  }
195619
196347
  function resolveOutputSchema() {
195620
- const raw2 = core6.getInput("output_schema");
196348
+ const raw2 = core7.getInput("output_schema");
195621
196349
  if (!raw2) return void 0;
195622
196350
  let parsed2;
195623
196351
  try {
@@ -195633,10 +196361,10 @@ function resolveOutputSchema() {
195633
196361
  }
195634
196362
 
195635
196363
  // utils/proxy.ts
195636
- var core8 = __toESM(require_core(), 1);
196364
+ var core9 = __toESM(require_core(), 1);
195637
196365
 
195638
196366
  // utils/token.ts
195639
- var core7 = __toESM(require_core(), 1);
196367
+ var core8 = __toESM(require_core(), 1);
195640
196368
  import assert2 from "node:assert/strict";
195641
196369
  var mcpTokenValue;
195642
196370
  var refreshMcpTokenFn;
@@ -195644,7 +196372,7 @@ function getMcpTokenRefresh() {
195644
196372
  return refreshMcpTokenFn;
195645
196373
  }
195646
196374
  function getJobToken() {
195647
- const inputToken = core7.getInput("token");
196375
+ const inputToken = core8.getInput("token");
195648
196376
  if (inputToken) {
195649
196377
  return inputToken;
195650
196378
  }
@@ -195660,7 +196388,7 @@ async function resolveTokens(params) {
195660
196388
  if (externalToken) {
195661
196389
  mcpTokenValue = externalToken;
195662
196390
  if (isGitHubActions) {
195663
- core7.setSecret(externalToken);
196391
+ core8.setSecret(externalToken);
195664
196392
  }
195665
196393
  log2.info("\xBB using external GH_TOKEN for both git and MCP");
195666
196394
  return {
@@ -195680,7 +196408,7 @@ async function resolveTokens(params) {
195680
196408
  const gitPermissions = params.push === "disabled" ? { contents: "read" } : { contents: "write", workflows: "write" };
195681
196409
  const gitToken = await acquireNewToken({ repos: writeRepos, permissions: gitPermissions });
195682
196410
  if (isGitHubActions) {
195683
- core7.setSecret(gitToken);
196411
+ core8.setSecret(gitToken);
195684
196412
  }
195685
196413
  log2.info(
195686
196414
  `\xBB acquired git token (${Object.entries(gitPermissions).map((e) => e.join(":")).join(", ")})`
@@ -195697,7 +196425,7 @@ async function resolveTokens(params) {
195697
196425
  };
195698
196426
  const mcpToken = await acquireNewToken({ repos: writeRepos, permissions: mcpPermissions });
195699
196427
  if (isGitHubActions) {
195700
- core7.setSecret(mcpToken);
196428
+ core8.setSecret(mcpToken);
195701
196429
  }
195702
196430
  log2.info(
195703
196431
  `\xBB acquired scoped MCP token (${Object.entries(mcpPermissions).map((e) => e.join(":")).join(", ")})`
@@ -195706,7 +196434,7 @@ async function resolveTokens(params) {
195706
196434
  let ghToken;
195707
196435
  if (ghPermissions) {
195708
196436
  ghToken = await acquireNewToken({ permissions: ghPermissions });
195709
- if (isGitHubActions) core7.setSecret(ghToken);
196437
+ if (isGitHubActions) core8.setSecret(ghToken);
195710
196438
  log2.info(
195711
196439
  `\xBB acquired gh token mirroring ${params.authorPermission} (${formatPermissions(ghPermissions)})`
195712
196440
  );
@@ -195717,7 +196445,7 @@ async function resolveTokens(params) {
195717
196445
  repos: params.xrepo.read,
195718
196446
  permissions: { contents: "read" }
195719
196447
  });
195720
- if (isGitHubActions) core7.setSecret(readToken);
196448
+ if (isGitHubActions) core8.setSecret(readToken);
195721
196449
  log2.info(`\xBB acquired cross-repo read token (contents:read, ${params.xrepo.read.length} repos)`);
195722
196450
  }
195723
196451
  mcpTokenValue = mcpToken;
@@ -195736,7 +196464,7 @@ async function resolveTokens(params) {
195736
196464
  oidc: params.oidc ?? void 0
195737
196465
  }).then((fresh) => {
195738
196466
  if (isGitHubActions) {
195739
- core7.setSecret(fresh);
196467
+ core8.setSecret(fresh);
195740
196468
  }
195741
196469
  mcpTokenValue = fresh;
195742
196470
  currentMcpToken = fresh;
@@ -195757,7 +196485,7 @@ async function resolveTokens(params) {
195757
196485
  permissions: gitPermissions,
195758
196486
  oidc: params.oidc ?? void 0
195759
196487
  }).then((fresh) => {
195760
- if (isGitHubActions) core7.setSecret(fresh);
196488
+ if (isGitHubActions) core8.setSecret(fresh);
195761
196489
  void revokeGitHubInstallationToken(currentGitToken);
195762
196490
  currentGitToken = fresh;
195763
196491
  log2.warning("\xBB GitHub rejected the git token; re-acquired a fresh git token");
@@ -195774,7 +196502,7 @@ async function resolveTokens(params) {
195774
196502
  permissions: { contents: "read" },
195775
196503
  oidc: params.oidc ?? void 0
195776
196504
  }).then((fresh) => {
195777
- if (isGitHubActions) core7.setSecret(fresh);
196505
+ if (isGitHubActions) core8.setSecret(fresh);
195778
196506
  void revokeGitHubInstallationToken(read2);
195779
196507
  currentReadToken = fresh;
195780
196508
  log2.warning("\xBB GitHub rejected the read token; re-acquired a fresh read token");
@@ -195988,7 +196716,7 @@ async function resolveProxyModel(ctx) {
195988
196716
  });
195989
196717
  if (!key) return;
195990
196718
  process.env.OPENROUTER_API_KEY = key;
195991
- core8.setSecret(key);
196719
+ core9.setSecret(key);
195992
196720
  ctx.payload.proxyModel = ctx.proxyModel;
195993
196721
  ctx.toolState.model = ctx.proxyModel;
195994
196722
  const label = ctx.oss ? "oss" : "router";
@@ -196059,7 +196787,7 @@ async function runProxyResolution(ctx) {
196059
196787
 
196060
196788
  // utils/prSummary.ts
196061
196789
  import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
196062
- import { dirname as dirname5, join as join26 } from "node:path";
196790
+ import { dirname as dirname5, join as join27 } from "node:path";
196063
196791
  var SUMMARY_FILE_NAME = "pullfrog-summary.md";
196064
196792
  var SUMMARY_SCAFFOLD = `# PR summary
196065
196793
 
@@ -196069,7 +196797,7 @@ var SUMMARY_SCAFFOLD = `# PR summary
196069
196797
  var MIN_SNAPSHOT_LENGTH = 60;
196070
196798
  var MAX_SNAPSHOT_LENGTH = 32768;
196071
196799
  function summaryFilePath(tmpdir4) {
196072
- return join26(tmpdir4, SUMMARY_FILE_NAME);
196800
+ return join27(tmpdir4, SUMMARY_FILE_NAME);
196073
196801
  }
196074
196802
  async function seedSummaryFile(params) {
196075
196803
  const path4 = summaryFilePath(params.tmpdir);
@@ -196198,6 +196926,7 @@ var defaultSettings = {
196198
196926
  shell: "restricted",
196199
196927
  prApproveEnabled: false,
196200
196928
  autoMergeEnabled: false,
196929
+ codexAgent: false,
196201
196930
  signedCommits: false,
196202
196931
  repoIntelligence: false,
196203
196932
  progressComments: true,
@@ -196686,7 +197415,7 @@ ${genericBody}`,
196686
197415
  }
196687
197416
 
196688
197417
  // utils/runLifecycle.ts
196689
- var core9 = __toESM(require_core(), 1);
197418
+ var core10 = __toESM(require_core(), 1);
196690
197419
 
196691
197420
  // utils/autoMerge.ts
196692
197421
  function hasBlockingHumanReview(reviews) {
@@ -197072,7 +197801,7 @@ async function finalizeSuccessRun(input) {
197072
197801
  }
197073
197802
  if (input.toolState.output) {
197074
197803
  log2.info(`::pullfrog-output::${Buffer.from(input.toolState.output).toString("base64")}`);
197075
- core9.setOutput("result", input.toolState.output);
197804
+ core10.setOutput("result", input.toolState.output);
197076
197805
  }
197077
197806
  if (input.result.success) {
197078
197807
  await approveAfterFix(input.toolContext).catch((error52) => {
@@ -197536,7 +198265,11 @@ async function main() {
197536
198265
  }
197537
198266
  const resolvedModel = credentials.kind === "fellBack" ? credentials.replacement : configuredModel;
197538
198267
  vertexCredentials = materializeVertexCredentials({ model: resolvedModel });
197539
- const agent2 = resolveAgent({ model: resolvedModel });
198268
+ const agent2 = resolveAgent({
198269
+ model: resolvedModel,
198270
+ proxyModel: payload.proxyModel,
198271
+ codexAgent: runContext.repoSettings.codexAgent
198272
+ });
197540
198273
  const effectiveModel = payload.proxyModel ?? resolvedModel ?? payload.model;
197541
198274
  toolState.model = effectiveModel;
197542
198275
  if (!payload.proxyModel) {
@@ -197720,8 +198453,8 @@ ${instructions.user}` : null,
197720
198453
  log2.info(instructions.full);
197721
198454
  });
197722
198455
  if (agentId === "opencode") {
197723
- const pluginDir = join27(process.cwd(), ".opencode", "plugin");
197724
- const hasPlugins = existsSync8(pluginDir) && readdirSync2(pluginDir).some((f) => /\.[jt]sx?$/.test(f));
198456
+ const pluginDir = join28(process.cwd(), ".opencode", "plugin");
198457
+ const hasPlugins = existsSync9(pluginDir) && readdirSync2(pluginDir).some((f) => /\.[jt]sx?$/.test(f));
197725
198458
  if (hasPlugins && toolState.dependencyInstallation?.promise) {
197726
198459
  log2.info(
197727
198460
  "\xBB .opencode/plugin/ detected \u2014 awaiting dependency installation before agent start"
@@ -197753,16 +198486,14 @@ ${instructions.user}` : null,
197753
198486
  const onInnerActivityTimeout = () => {
197754
198487
  if (innerTimeoutFired) return;
197755
198488
  innerTimeoutFired = true;
197756
- log2.info(
197757
- "\xBB inner activity timeout fired \u2014 stopping MCP server and starting 5min safety-net timer"
197758
- );
197759
- mcpHttpServer[Symbol.asyncDispose]().catch((err) => {
197760
- log2.debug(
197761
- `mcp server stop after inner kill failed: ${err instanceof Error ? err.message : String(err)}`
197762
- );
197763
- });
198489
+ log2.info("\xBB inner activity timeout fired \u2014 starting 5min safety-net timer");
197764
198490
  safetyNetTimer = setTimeout(
197765
198491
  () => {
198492
+ mcpHttpServer[Symbol.asyncDispose]().catch((err) => {
198493
+ log2.debug(
198494
+ `mcp server stop after inner kill failed: ${err instanceof Error ? err.message : String(err)}`
198495
+ );
198496
+ });
197766
198497
  activityTimeout?.forceReject(
197767
198498
  "agent still pending 5min after inner activity kill \u2014 forcing exit"
197768
198499
  );
@@ -197771,6 +198502,13 @@ ${instructions.user}` : null,
197771
198502
  );
197772
198503
  safetyNetTimer.unref?.();
197773
198504
  };
198505
+ const onTurnRecovered = () => {
198506
+ if (!innerTimeoutFired) return;
198507
+ innerTimeoutFired = false;
198508
+ if (safetyNetTimer) clearTimeout(safetyNetTimer);
198509
+ safetyNetTimer = void 0;
198510
+ log2.info("\xBB inner activity safety net stood down \u2014 turn recovered");
198511
+ };
197774
198512
  const agentPromise = agent2.run({
197775
198513
  payload,
197776
198514
  resolvedModel,
@@ -197791,6 +198529,7 @@ ${instructions.user}` : null,
197791
198529
  toolState,
197792
198530
  apiToken: runContext.apiToken,
197793
198531
  onActivityTimeout: onInnerActivityTimeout,
198532
+ onTurnRecovered,
197794
198533
  onToolUse: (event) => {
197795
198534
  const wasTracked = recordDiffReadFromToolUse({
197796
198535
  state: primaryRepoState(toolState).diffCoverage,
@@ -197913,27 +198652,27 @@ async function runMain() {
197913
198652
  }
197914
198653
  } catch (error52) {
197915
198654
  const errorMessage = error52 instanceof Error ? error52.message : "unknown error occurred";
197916
- core10.setFailed(`action failed: ${errorMessage}`);
198655
+ core11.setFailed(`action failed: ${errorMessage}`);
197917
198656
  }
197918
198657
  }
197919
198658
  async function tokenMain() {
197920
- const reposInput = core10.getInput("repos");
198659
+ const reposInput = core11.getInput("repos");
197921
198660
  const additionalRepos = reposInput ? reposInput.split(",").map((r2) => r2.trim()).filter(Boolean) : [];
197922
198661
  const token = await acquireNewToken({ repos: additionalRepos });
197923
- core10.setSecret(token);
197924
- core10.saveState(STATE_TOKEN, token);
197925
- core10.setOutput("token", token);
198662
+ core11.setSecret(token);
198663
+ core11.saveState(STATE_TOKEN, token);
198664
+ core11.setOutput("token", token);
197926
198665
  const scope2 = additionalRepos.length ? `current repo + ${additionalRepos.join(", ")}` : "current repo only";
197927
- core10.info(`\xBB installation token acquired (${scope2})`);
198666
+ core11.info(`\xBB installation token acquired (${scope2})`);
197928
198667
  }
197929
198668
  async function tokenPost() {
197930
- const token = core10.getState(STATE_TOKEN);
198669
+ const token = core11.getState(STATE_TOKEN);
197931
198670
  if (!token) {
197932
- core10.debug("no token found in state, skipping revocation");
198671
+ core11.debug("no token found in state, skipping revocation");
197933
198672
  return;
197934
198673
  }
197935
198674
  await revokeGitHubInstallationToken(token);
197936
- core10.info("\xBB installation token revoked");
198675
+ core11.info("\xBB installation token revoked");
197937
198676
  }
197938
198677
  function printGhaUsage(params) {
197939
198678
  params.stream(`usage: ${params.prog} gha [subcommand]
@@ -198049,7 +198788,7 @@ async function run(args2) {
198049
198788
  }
198050
198789
  } catch (error52) {
198051
198790
  const message = error52 instanceof Error ? error52.message : String(error52);
198052
- core10.setFailed(message);
198791
+ core11.setFailed(message);
198053
198792
  }
198054
198793
  }
198055
198794
 
@@ -198064,34 +198803,6 @@ var PULLFROG_API_URL2 = (process.env.PULLFROG_API_URL || "https://pullfrog.com")
198064
198803
  function link(text, url4) {
198065
198804
  return `\x1B]8;;${url4}\x07${text}\x1B]8;;\x07`;
198066
198805
  }
198067
- function buildProviders() {
198068
- return Object.entries(providers).filter(([key]) => key !== "opencode" && key !== "openrouter").map(([key, config3]) => {
198069
- const aliases = modelAliases.filter(
198070
- (a2) => a2.provider === key && !a2.fallback && !a2.routing && !a2.hidden
198071
- );
198072
- const recommended = aliases.find((a2) => a2.preferred);
198073
- const sorted = [...aliases].sort((a2, b) => {
198074
- if (a2.preferred && !b.preferred) return -1;
198075
- if (!a2.preferred && b.preferred) return 1;
198076
- return 0;
198077
- });
198078
- return {
198079
- id: key,
198080
- name: config3.displayName,
198081
- envVars: config3.envVars,
198082
- models: sorted.map((a2) => ({
198083
- value: a2.slug,
198084
- label: a2.displayName,
198085
- hint: a2 === recommended ? "recommended" : void 0
198086
- }))
198087
- };
198088
- }).filter((p) => p.models.length > 0);
198089
- }
198090
- var CLI_PROVIDERS = buildProviders();
198091
- function resolveModelProvider(slug2) {
198092
- const providerId = slug2.split("/")[0];
198093
- return CLI_PROVIDERS.find((p) => p.id === providerId) ?? null;
198094
- }
198095
198806
  var activeSpin2 = null;
198096
198807
  function bail2(msg) {
198097
198808
  if (activeSpin2) {
@@ -198176,15 +198887,11 @@ function openBrowser(url4) {
198176
198887
  }
198177
198888
  }
198178
198889
  async function pullfrogApi2(ctx) {
198179
- const headers = { authorization: `Bearer ${ctx.token}` };
198180
- if (ctx.body) headers["content-type"] = "application/json";
198181
198890
  const controller = new AbortController();
198182
198891
  const timeout = setTimeout(() => controller.abort(), 3e4);
198183
198892
  try {
198184
198893
  const response = await fetch(`${PULLFROG_API_URL2}${ctx.path}`, {
198185
- method: ctx.method || "GET",
198186
- headers,
198187
- body: ctx.body ? JSON.stringify(ctx.body) : null,
198894
+ headers: { authorization: `Bearer ${ctx.token}` },
198188
198895
  signal: controller.signal
198189
198896
  });
198190
198897
  const data = await response.json().catch(() => ({}));
@@ -198212,406 +198919,27 @@ async function fetchStatus2(ctx) {
198212
198919
  isOrg: result.data.isOrg === true
198213
198920
  };
198214
198921
  }
198215
- bail2(errorMsg || `secrets check failed (${result.status})`);
198922
+ bail2(errorMsg || `installation check failed (${result.status})`);
198216
198923
  }
198217
198924
  return {
198218
198925
  installed: true,
198219
198926
  isOrg: result.data.isOrg === true,
198220
- installationId: typeof result.data.installationId === "number" ? result.data.installationId : null,
198221
- secretsAccessible: result.data.accessible !== false,
198222
- repoSecrets: result.data.repoSecrets || [],
198223
- orgSecrets: result.data.orgSecrets || [],
198224
- pullfrogSecrets: result.data.pullfrogSecrets || [],
198225
- model: result.data.repoModel ?? null,
198226
- hasRuns: result.data.hasRuns === true
198227
- };
198228
- }
198229
- async function createSession(ctx) {
198230
- try {
198231
- const result = await pullfrogApi2({
198232
- path: "/api/cli/session",
198233
- token: ctx.token,
198234
- method: "POST",
198235
- body: { owner: ctx.owner.toLowerCase(), repo: ctx.repo.toLowerCase() }
198236
- });
198237
- if (!result.ok || !result.data.id) return null;
198238
- return result.data.id;
198239
- } catch {
198240
- return null;
198241
- }
198242
- }
198243
- async function pollSession(ctx) {
198244
- const result = await pullfrogApi2({
198245
- path: `/api/cli/session/${ctx.sessionId}`,
198246
- token: ctx.token
198247
- });
198248
- if (result.status === 410) return "expired";
198249
- if (!result.ok) return "pending";
198250
- return result.data.installed === true ? "installed" : "pending";
198251
- }
198252
- function cleanupSession(ctx) {
198253
- void pullfrogApi2({
198254
- path: `/api/cli/session/${ctx.sessionId}`,
198255
- token: ctx.token,
198256
- method: "DELETE"
198257
- }).catch(() => {
198258
- });
198259
- }
198260
- var SESSION_POLL_MS = 750;
198261
- var FALLBACK_POLL_MS = 5e3;
198262
- var HINT_AFTER_MS = 1e4;
198263
- var TIMEOUT_MS = 3 * 60 * 1e3;
198264
- function listenForKey(key) {
198265
- let triggered = false;
198266
- const onData = (data) => {
198267
- if (data.toString().toLowerCase() === key) triggered = true;
198268
- };
198269
- process.stdin.setRawMode?.(true);
198270
- process.stdin.resume();
198271
- process.stdin.on("data", onData);
198272
- return {
198273
- consume() {
198274
- if (!triggered) return false;
198275
- triggered = false;
198276
- return true;
198277
- },
198278
- stop() {
198279
- process.stdin.removeListener("data", onData);
198280
- process.stdin.setRawMode?.(false);
198281
- process.stdin.pause();
198282
- }
198927
+ installationId: typeof result.data.installationId === "number" ? result.data.installationId : null
198283
198928
  };
198284
198929
  }
198285
198930
  function installationConfigUrl(ctx) {
198286
198931
  return ctx.isOrg ? `https://github.com/organizations/${ctx.owner}/settings/installations/${ctx.installationId}` : `https://github.com/settings/installations/${ctx.installationId}`;
198287
198932
  }
198288
- async function ensureInstallation(ctx) {
198289
- activeSpin2.start("checking pullfrog app installation");
198290
- const initial = await fetchStatus2(ctx);
198291
- if (initial.installed) {
198292
- activeSpin2.stop(`pullfrog app is installed on ${import_picocolors3.default.cyan(`@${ctx.owner}`)}`);
198293
- if (initial.installationId) {
198294
- const configUrl = installationConfigUrl({
198295
- owner: ctx.owner,
198296
- installationId: initial.installationId,
198297
- isOrg: initial.isOrg
198298
- });
198299
- process.stdout.write(`${import_picocolors3.default.gray(S_BAR)} ${link(import_picocolors3.default.dim(configUrl), configUrl)}
198300
- `);
198301
- }
198302
- return initial;
198303
- }
198304
- const sessionId = await createSession(ctx);
198305
- if (initial.installationId) {
198306
- const repoRef = import_picocolors3.default.bold(`${ctx.owner}/${ctx.repo}`);
198307
- const configUrl = installationConfigUrl({
198308
- owner: ctx.owner,
198309
- installationId: initial.installationId,
198310
- isOrg: initial.isOrg
198311
- });
198312
- activeSpin2.stop(`pullfrog is installed on selected repos, but ${repoRef} is not included.`);
198313
- log.info(
198314
- `add it under "Repository access" on the installation config page.
198315
- ${import_picocolors3.default.dim(configUrl)}`
198316
- );
198317
- const openIt = await confirm({ message: "open browser?", active: "yes", inactive: "no" });
198318
- handleCancel2(openIt);
198319
- if (openIt) openBrowser(configUrl);
198320
- } else {
198321
- activeSpin2.stop("pullfrog app not installed");
198322
- const installUrl = `https://github.com/apps/${initial.appSlug}/installations/select_target?state=cli`;
198323
- log.info(`opening browser to install...
198324
- ${import_picocolors3.default.dim(installUrl)}`);
198325
- openBrowser(installUrl);
198326
- }
198327
- const isRepoAccessUpdate = !!initial.installationId;
198328
- const baseMsg = isRepoAccessUpdate ? "once you've added the repo, onboarding will proceed automatically" : "once you've installed the app, onboarding will proceed automatically";
198329
- activeSpin2.start(baseMsg);
198330
- let activeSessionId = sessionId;
198331
- let pollMs = activeSessionId ? SESSION_POLL_MS : FALLBACK_POLL_MS;
198332
- const listener = listenForKey("r");
198333
- const startedAt = Date.now();
198334
- let hintShown = false;
198335
- try {
198336
- while (Date.now() - startedAt < TIMEOUT_MS) {
198337
- await new Promise((r2) => setTimeout(r2, pollMs));
198338
- if (!hintShown && Date.now() - startedAt > HINT_AFTER_MS) {
198339
- activeSpin2.message(`${baseMsg} ${import_picocolors3.default.dim("(press r to recheck manually)")}`);
198340
- hintShown = true;
198341
- }
198342
- const doneMsg = isRepoAccessUpdate ? "repo access confirmed" : "pullfrog app installed";
198343
- if (listener.consume()) {
198344
- activeSpin2.message("rechecking via GitHub API");
198345
- try {
198346
- const status = await fetchStatus2(ctx);
198347
- if (status.installed) {
198348
- if (activeSessionId) cleanupSession({ token: ctx.token, sessionId: activeSessionId });
198349
- activeSpin2.stop(doneMsg);
198350
- return status;
198351
- }
198352
- } catch {
198353
- }
198354
- activeSpin2.message(`${baseMsg} ${import_picocolors3.default.dim("(press r to recheck manually)")}`);
198355
- continue;
198356
- }
198357
- if (activeSessionId) {
198358
- try {
198359
- const result = await pollSession({ token: ctx.token, sessionId: activeSessionId });
198360
- if (result === "expired") {
198361
- activeSessionId = null;
198362
- pollMs = FALLBACK_POLL_MS;
198363
- continue;
198364
- }
198365
- if (result === "installed") {
198366
- const status = await fetchStatus2(ctx);
198367
- if (status.installed) {
198368
- cleanupSession({ token: ctx.token, sessionId: activeSessionId });
198369
- activeSpin2.stop(doneMsg);
198370
- return status;
198371
- }
198372
- }
198373
- } catch {
198374
- }
198375
- } else {
198376
- try {
198377
- const status = await fetchStatus2(ctx);
198378
- if (status.installed) {
198379
- activeSpin2.stop(doneMsg);
198380
- return status;
198381
- }
198382
- } catch {
198383
- }
198384
- }
198385
- }
198386
- } finally {
198387
- listener.stop();
198388
- }
198389
- if (activeSessionId) cleanupSession({ token: ctx.token, sessionId: activeSessionId });
198390
- bail2(
198391
- isRepoAccessUpdate ? `timed out waiting for repo access.
198392
- ${import_picocolors3.default.dim("add the repo, then re-run:")} npx pullfrog init` : `timed out waiting for app installation.
198393
- ${import_picocolors3.default.dim("if your org requires admin approval, ask an admin to approve,")}
198394
- ${import_picocolors3.default.dim("then re-run:")} npx pullfrog init`
198395
- );
198933
+ function consoleUrl(ctx) {
198934
+ return `${PULLFROG_API_URL2}/console/${ctx.owner}?repo=${encodeURIComponent(ctx.repo)}`;
198396
198935
  }
198397
- function setGhSecret(ctx) {
198398
- let orgFailed = false;
198399
- if (ctx.org) {
198400
- try {
198401
- execFileSync8("gh", ["secret", "set", ctx.name, "--org", ctx.org, "--visibility", "all"], {
198402
- input: ctx.value,
198403
- stdio: ["pipe", "ignore", "pipe"],
198404
- encoding: "utf-8"
198405
- });
198406
- return { saved: true, orgFailed: false };
198407
- } catch {
198408
- orgFailed = true;
198409
- }
198410
- }
198411
- try {
198412
- execFileSync8("gh", ["secret", "set", ctx.name, "--repo", ctx.repoSlug], {
198413
- input: ctx.value,
198414
- stdio: ["pipe", "ignore", "pipe"],
198415
- encoding: "utf-8"
198416
- });
198417
- return { saved: true, orgFailed };
198418
- } catch {
198419
- return { saved: false, orgFailed };
198420
- }
198936
+ function installUrl(ctx) {
198937
+ const state = encodeURIComponent(`cli:${ctx.owner}/${ctx.repo}`);
198938
+ return `https://github.com/apps/${ctx.appSlug}/installations/select_target?state=${state}`;
198421
198939
  }
198422
- async function setPullfrogSecret2(ctx) {
198423
- const result = await pullfrogApi2({
198424
- path: "/api/cli/secrets",
198425
- token: ctx.token,
198426
- method: "POST",
198427
- body: {
198428
- owner: ctx.owner,
198429
- repo: ctx.repo,
198430
- name: ctx.name,
198431
- value: ctx.value,
198432
- scope: ctx.scope
198433
- }
198434
- });
198435
- if (result.ok && result.data.success === true) {
198436
- return { saved: true, error: "" };
198437
- }
198438
- return { saved: false, error: result.data.error || `api returned ${result.status}` };
198439
- }
198440
- async function promptScope2(ctx) {
198441
- const scope2 = await select({
198442
- message: "secret scope",
198443
- options: [
198444
- { value: "account", label: `${ctx.owner} organization`, hint: "shared across repos" },
198445
- { value: "repo", label: `${ctx.owner}/${ctx.repo} only` }
198446
- ]
198447
- });
198448
- handleCancel2(scope2);
198449
- return scope2;
198450
- }
198451
- async function handleSecret(ctx) {
198452
- const repoSecretsUrl = `https://github.com/${ctx.owner}/${ctx.repo}/settings/secrets/actions`;
198453
- const matches = [];
198454
- for (const v of ctx.provider.envVars) {
198455
- if (ctx.secrets.pullfrogSecrets.includes(v)) matches.push({ name: v, source: "pullfrog" });
198456
- else if (ctx.secrets.secretsAccessible && ctx.secrets.orgSecrets.includes(v))
198457
- matches.push({ name: v, source: "org secret" });
198458
- else if (ctx.secrets.secretsAccessible && ctx.secrets.repoSecrets.includes(v))
198459
- matches.push({ name: v, source: "repo secret" });
198460
- }
198461
- if (matches.length > 0) {
198462
- activeSpin2.start("");
198463
- activeSpin2.stop("secrets already configured");
198464
- for (const m of matches) {
198465
- process.stdout.write(
198466
- `${import_picocolors3.default.gray(S_BAR)} ${import_picocolors3.default.cyan(m.name)} ${import_picocolors3.default.dim(`(${m.source})`)}
198467
- `
198468
- );
198469
- }
198470
- return;
198471
- }
198472
- if (!ctx.secrets.secretsAccessible) {
198473
- log.info(`could not verify GitHub secrets (app lacks permission)`);
198474
- }
198475
- const hasOAuthOption = ctx.provider.envVars.includes("CLAUDE_CODE_OAUTH_TOKEN");
198476
- let envVar = ctx.provider.envVars[0];
198477
- if (hasOAuthOption) {
198478
- const authMethod = await select({
198479
- message: "which credential do you want to use?",
198480
- options: [
198481
- {
198482
- value: "oauth",
198483
- label: "Claude Code OAuth token",
198484
- hint: `run ${import_picocolors3.default.cyan("claude setup-token")} \u2014 works with Pro/Max subscriptions`
198485
- },
198486
- {
198487
- value: "api",
198488
- label: "Anthropic API key",
198489
- hint: "from console.anthropic.com"
198490
- }
198491
- ]
198492
- });
198493
- handleCancel2(authMethod);
198494
- if (authMethod === "oauth") envVar = "CLAUDE_CODE_OAUTH_TOKEN";
198495
- }
198496
- const method = await select({
198497
- message: `where should ${import_picocolors3.default.cyan(envVar)} be stored?`,
198498
- options: [
198499
- {
198500
- value: "pullfrog",
198501
- label: "Pullfrog",
198502
- hint: "recommended \u2014 auto-injected, no workflow changes"
198503
- },
198504
- {
198505
- value: "github",
198506
- label: "GitHub Actions secret",
198507
- hint: "requires env block in pullfrog.yml"
198508
- }
198509
- ]
198510
- });
198511
- handleCancel2(method);
198512
- const pasteLabel = envVar === "CLAUDE_CODE_OAUTH_TOKEN" ? "OAuth token" : `${ctx.provider.name} API key`;
198513
- const apiKey = await password({
198514
- message: `paste your ${pasteLabel} ${import_picocolors3.default.dim("(Enter to skip)")}`,
198515
- mask: "*",
198516
- validate: () => void 0
198517
- });
198518
- handleCancel2(apiKey);
198519
- if (!apiKey) {
198520
- log.info(
198521
- `skipped \u2014 set it manually at:
198522
- ${import_picocolors3.default.dim(method === "pullfrog" ? `${PULLFROG_API_URL2}/console/${ctx.owner}` : repoSecretsUrl)}`
198523
- );
198524
- return;
198525
- }
198526
- if (method === "pullfrog") {
198527
- const scope2 = ctx.secrets.isOrg ? await promptScope2(ctx) : "account";
198528
- const target = describeSecretTarget({ owner: ctx.owner, repo: ctx.repo, scope: scope2 });
198529
- activeSpin2.start(`saving ${import_picocolors3.default.cyan(envVar)} to ${target}`);
198530
- let saveResult;
198531
- try {
198532
- saveResult = await setPullfrogSecret2({
198533
- token: ctx.token,
198534
- owner: ctx.owner,
198535
- repo: ctx.repo,
198536
- name: envVar,
198537
- value: apiKey,
198538
- scope: scope2
198539
- });
198540
- } catch (error52) {
198541
- activeSpin2.stop(import_picocolors3.default.red("could not save secret"));
198542
- log.warn(
198543
- `${error52 instanceof Error ? error52.message : "network error"}
198544
- set it manually at: ${import_picocolors3.default.dim(`${PULLFROG_API_URL2}/console/${ctx.owner}`)}`
198545
- );
198546
- return;
198547
- }
198548
- if (saveResult.saved) {
198549
- activeSpin2.stop(`saved ${import_picocolors3.default.cyan(envVar)} to ${target}`);
198550
- } else {
198551
- activeSpin2.stop(import_picocolors3.default.red("could not save secret"));
198552
- log.warn(
198553
- `${saveResult.error}
198554
- set it manually at: ${import_picocolors3.default.dim(`${PULLFROG_API_URL2}/console/${ctx.owner}`)}`
198555
- );
198556
- }
198557
- return;
198558
- }
198559
- let org = null;
198560
- if (ctx.secrets.isOrg) {
198561
- const scope2 = await promptScope2(ctx);
198562
- org = scope2 === "account" ? ctx.owner : null;
198563
- }
198564
- const secretsUrl = org ? `https://github.com/organizations/${org}/settings/secrets/actions` : repoSecretsUrl;
198565
- activeSpin2.start(`saving ${envVar}`);
198566
- const secretResult = setGhSecret({
198567
- name: envVar,
198568
- value: apiKey,
198569
- org,
198570
- repoSlug: `${ctx.owner}/${ctx.repo}`
198571
- });
198572
- if (secretResult.saved) {
198573
- activeSpin2.stop(
198574
- `saved ${import_picocolors3.default.cyan(envVar)} to ${org && !secretResult.orgFailed ? `${import_picocolors3.default.dim(ctx.owner)} org secret` : "GitHub Actions secret"}`
198575
- );
198576
- if (secretResult.orgFailed) {
198577
- log.warn("org secret failed (admin access required) \u2014 saved as repo secret instead");
198578
- }
198579
- } else {
198580
- activeSpin2.stop(import_picocolors3.default.red("could not set secret"));
198581
- log.warn(`set it manually at:
198582
- ${import_picocolors3.default.dim(secretsUrl)}`);
198583
- }
198584
- }
198585
- async function promptTestRun(ctx) {
198586
- const proceed = await select({
198587
- message: "test your installation?",
198588
- options: [
198589
- { value: true, label: "yes", hint: "dispatches a test run in your GitHub Actions" },
198590
- { value: false, label: "skip" }
198591
- ]
198592
- });
198593
- handleCancel2(proceed);
198594
- if (!proceed) return;
198595
- activeSpin2.start("dispatching test run");
198596
- const result = await pullfrogApi2({
198597
- path: "/api/cli/dispatch",
198598
- token: ctx.token,
198599
- method: "POST",
198600
- body: { owner: ctx.owner, repo: ctx.repo, prompt: "Tell me a joke" }
198601
- });
198602
- if (!result.ok) {
198603
- activeSpin2.stop(import_picocolors3.default.red("could not dispatch"));
198604
- log.warn(result.data.error || `dispatch failed (${result.status})`);
198605
- return;
198606
- }
198607
- activeSpin2.stop("dispatched test run");
198608
- if (result.data.url) {
198609
- process.stdout.write(
198610
- `${import_picocolors3.default.gray(S_BAR)} ${link(import_picocolors3.default.dim(result.data.url), result.data.url)}
198611
- `
198612
- );
198613
- openBrowser(result.data.url);
198614
- }
198940
+ function printLink(url4) {
198941
+ process.stdout.write(`${import_picocolors3.default.gray(S_BAR)} ${link(import_picocolors3.default.dim(url4), url4)}
198942
+ `);
198615
198943
  }
198616
198944
  async function main2() {
198617
198945
  intro(import_picocolors3.default.bgGreen(import_picocolors3.default.black(" pullfrog ")));
@@ -198633,98 +198961,58 @@ async function main2() {
198633
198961
  spin.start("detecting repository");
198634
198962
  const remote = parseGitRemote2();
198635
198963
  spin.stop(`detected repo ${import_picocolors3.default.cyan(`${remote.owner}/${remote.repo}`)}`);
198636
- const secrets = await ensureInstallation({ token, owner: remote.owner, repo: remote.repo });
198637
- let model;
198638
- let provider2;
198639
- if (secrets.model) {
198640
- model = secrets.model;
198641
- const resolved = resolveModelProvider(secrets.model);
198642
- if (!resolved) bail2(`unknown model provider: ${secrets.model}`);
198643
- provider2 = resolved;
198644
- const displayAlias = resolveDisplayAlias(secrets.model);
198645
- const label = displayAlias ? displayAlias.displayName : secrets.model;
198964
+ spin.start("checking pullfrog app installation");
198965
+ const status = await fetchStatus2({ token, owner: remote.owner, repo: remote.repo });
198966
+ const handoff = consoleUrl({ owner: remote.owner, repo: remote.repo });
198967
+ if (status.installed) {
198968
+ spin.stop(`pullfrog app is installed on ${import_picocolors3.default.cyan(`@${remote.owner}`)}`);
198646
198969
  spin.start("");
198647
- spin.stop(`using model ${import_picocolors3.default.cyan(label)}`);
198648
- } else {
198649
- const providerId = await select({
198650
- message: "select your preferred model provider",
198651
- options: CLI_PROVIDERS.map((cp) => ({
198652
- value: cp.id,
198653
- label: cp.name
198654
- }))
198655
- });
198656
- handleCancel2(providerId);
198657
- const found = CLI_PROVIDERS.find((cp) => cp.id === providerId);
198658
- if (!found) bail2(`unknown provider: ${providerId}`);
198659
- provider2 = found;
198660
- if (provider2.models.length === 1) {
198661
- model = provider2.models[0].value;
198662
- spin.start("");
198663
- spin.stop(`using ${import_picocolors3.default.bold(provider2.models[0].label)}`);
198664
- } else {
198665
- const recommendedModel = provider2.models.find((m) => m.hint === "recommended");
198666
- const options = provider2.models.map((m) => {
198667
- if (m.hint) return { value: m.value, label: m.label, hint: m.hint };
198668
- return { value: m.value, label: m.label };
198669
- });
198670
- const selected = await select(
198671
- recommendedModel ? { message: "select model", initialValue: recommendedModel.value, options } : { message: "select model", options }
198672
- );
198673
- handleCancel2(selected);
198674
- model = selected;
198675
- }
198676
- }
198677
- await handleSecret({ token, owner: remote.owner, repo: remote.repo, provider: provider2, secrets });
198678
- spin.start("creating pullfrog.yml workflow");
198679
- const result = await pullfrogApi2({
198680
- path: "/api/cli/setup",
198681
- token,
198682
- method: "POST",
198683
- body: { owner: remote.owner, repo: remote.repo, model }
198684
- });
198685
- if (!result.ok) {
198686
- bail2(result.data.error || `api returned ${result.status}`);
198687
- }
198688
- let skipTestRun = false;
198689
- if (result.data.already_existed) {
198690
- spin.stop("pullfrog.yml already exists");
198691
- } else if (result.data.pull_request_url) {
198692
- spin.stop("opened pull request with pullfrog.yml");
198693
- process.stdout.write(
198694
- `${import_picocolors3.default.gray(S_BAR)} ${link(import_picocolors3.default.dim(result.data.pull_request_url), result.data.pull_request_url)}
198695
- `
198970
+ spin.stop("opening your dashboard");
198971
+ printLink(handoff);
198972
+ openBrowser(handoff);
198973
+ activeSpin2 = null;
198974
+ outro(
198975
+ `finish setup in your browser \u2014 ${import_picocolors3.default.cyan(`${remote.owner}/${remote.repo}`)} is ready.`
198696
198976
  );
198697
- openBrowser(result.data.pull_request_url);
198698
- const merged = await select({
198699
- message: "merge the PR to activate pullfrog, then continue",
198700
- options: [
198701
- { value: true, label: "continue", hint: "PR has been merged" },
198702
- { value: false, label: "skip" }
198703
- ]
198977
+ return;
198978
+ }
198979
+ if (status.installationId) {
198980
+ const repoRef = import_picocolors3.default.bold(`${remote.owner}/${remote.repo}`);
198981
+ const configUrl = installationConfigUrl({
198982
+ owner: remote.owner,
198983
+ installationId: status.installationId,
198984
+ isOrg: status.isOrg
198704
198985
  });
198705
- handleCancel2(merged);
198706
- if (!merged) skipTestRun = true;
198707
- } else {
198708
- const short = result.data.hash?.slice(0, 7);
198709
- spin.stop(
198710
- short ? `committed pullfrog.yml to repo ${import_picocolors3.default.dim(short)}` : "committed pullfrog.yml to repo"
198986
+ spin.stop(`pullfrog is installed on selected repos, but ${repoRef} is not included.`);
198987
+ log.info(
198988
+ `add it under "Repository access" on the installation config page.
198989
+ ${import_picocolors3.default.dim(configUrl)}`
198711
198990
  );
198991
+ const openIt = await confirm({ message: "open browser?", active: "yes", inactive: "no" });
198992
+ handleCancel2(openIt);
198993
+ if (openIt) openBrowser(configUrl);
198994
+ log.info("once the repo is added, finish setup at:");
198995
+ printLink(handoff);
198996
+ activeSpin2 = null;
198997
+ outro("done.");
198998
+ return;
198712
198999
  }
198713
- if (!skipTestRun && !secrets.hasRuns) {
198714
- await promptTestRun({ token, owner: remote.owner, repo: remote.repo });
198715
- }
198716
- const consoleUrl = `${PULLFROG_API_URL2}/console/${remote.owner}/${remote.repo}`;
198717
- spin.start("");
198718
- spin.stop("repo is configurable via the Pullfrog dashboard");
198719
- process.stdout.write(`${import_picocolors3.default.gray(S_BAR)} ${link(import_picocolors3.default.dim(consoleUrl), consoleUrl)}
198720
- `);
199000
+ spin.stop("pullfrog app not installed");
199001
+ const install = installUrl({
199002
+ appSlug: status.appSlug,
199003
+ owner: remote.owner,
199004
+ repo: remote.repo
199005
+ });
199006
+ log.info("opening browser to install...");
199007
+ printLink(install);
199008
+ openBrowser(install);
198721
199009
  activeSpin2 = null;
198722
- outro("done.");
199010
+ outro("GitHub will drop you at your dashboard to finish setup.");
198723
199011
  }
198724
199012
  function printInitUsage(params) {
198725
199013
  params.stream(`usage: ${params.prog} init
198726
199014
  `);
198727
- params.stream("set up pullfrog on the current repository.");
199015
+ params.stream("install pullfrog on the current repository and open its dashboard.");
198728
199016
  params.stream("");
198729
199017
  params.stream("options:");
198730
199018
  params.stream(" -h, --help show help");
@@ -198896,7 +199184,7 @@ async function runCli4(input) {
198896
199184
  }
198897
199185
 
198898
199186
  // cli.ts
198899
- var VERSION10 = "0.1.57";
199187
+ var VERSION10 = "0.1.60";
198900
199188
  var bin = basename2(process.argv[1] || "");
198901
199189
  var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
198902
199190
  var rawArgs = process.argv.slice(2);
@@ -198904,7 +199192,7 @@ function printMainUsage(stream) {
198904
199192
  stream(`usage: ${PROG} <command>
198905
199193
  `);
198906
199194
  stream("commands:");
198907
- stream(" init set up pullfrog on the current repository");
199195
+ stream(" init install pullfrog on the current repository and open its dashboard");
198908
199196
  stream(" auth manage provider credentials for the current repository");
198909
199197
  stream(" watch stream a PR's activity as one JSON line per event");
198910
199198
  stream("");