pullfrog 0.1.103 → 0.1.105

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.
@@ -375,8 +375,9 @@ export interface WriteablePayload {
375
375
  export type Payload = Readonly<WriteablePayload>;
376
376
  /**
377
377
  * Whether `find_similar_issues` is registered for this run: an entitled account,
378
- * on an issue rather than a PR. Shared by the MCP registration and the prompt so
379
- * the duplicate-detection instructions can never describe an absent tool.
378
+ * on an issue or a run with no target (`pullfrog run "enrich issue #42"`), never a PR.
379
+ * Shared by the MCP registration and the prompt so the duplicate-detection
380
+ * instructions can never describe an absent tool.
380
381
  */
381
382
  export declare function hasSimilarIssues(params: {
382
383
  repoIntelligence: boolean;
package/dist/index.js CHANGED
@@ -109939,9 +109939,34 @@ var AZURE_USE_CHAT_COMPLETIONS_ENV = "AZURE_USE_CHAT_COMPLETIONS";
109939
109939
  var OPENAI_COMPATIBLE_PROVIDER = "openai-compatible";
109940
109940
  var OPENAI_COMPATIBLE_BASE_URL_ENV = "OPENAI_COMPATIBLE_BASE_URL";
109941
109941
  var OPENAI_COMPATIBLE_API_KEY_ENV = "OPENAI_COMPATIBLE_API_KEY";
109942
- var OPENAI_COMPATIBLE_MODEL_ENV = "OPENAI_COMPATIBLE_MODEL";
109943
109942
  var OPENAI_COMPATIBLE_CONTEXT_ENV = "OPENAI_COMPATIBLE_CONTEXT";
109944
109943
  var OPENAI_COMPATIBLE_MAX_OUTPUT_ENV = "OPENAI_COMPATIBLE_MAX_OUTPUT";
109944
+ function isOpenAICompatibleSetName(name) {
109945
+ return name !== "byok" && name.length <= 32 && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name);
109946
+ }
109947
+ function openAICompatibleSetName(slug2) {
109948
+ const prefix = `${OPENAI_COMPATIBLE_PROVIDER}/`;
109949
+ if (!slug2.startsWith(prefix)) return void 0;
109950
+ const name = slug2.slice(prefix.length);
109951
+ return isOpenAICompatibleSetName(name) ? name : void 0;
109952
+ }
109953
+ var OPENAI_COMPATIBLE_FIELDS = [
109954
+ "baseUrl",
109955
+ "apiKey",
109956
+ "model",
109957
+ "context",
109958
+ "maxOutput"
109959
+ ];
109960
+ function openAICompatibleEnv(name) {
109961
+ const prefix = `OPENAI_COMPATIBLE_${name ? `${name.toUpperCase().replaceAll("-", "_")}_` : ""}`;
109962
+ return {
109963
+ baseUrl: `${prefix}BASE_URL`,
109964
+ apiKey: `${prefix}API_KEY`,
109965
+ model: `${prefix}MODEL`,
109966
+ context: `${prefix}CONTEXT`,
109967
+ maxOutput: `${prefix}MAX_OUTPUT`
109968
+ };
109969
+ }
109945
109970
  var PROVIDER_GATEWAY_URL_ENV = {
109946
109971
  anthropic: "ANTHROPIC_BASE_URL",
109947
109972
  openai: "OPENAI_BASE_URL"
@@ -109986,7 +110011,7 @@ function findDanglingPromptToolRefs(params) {
109986
110011
  return [...new Set(extractMcpToolRefs(params.agentId, params.prompt))].filter((name) => !registered.has(name)).sort();
109987
110012
  }
109988
110013
  function hasSimilarIssues(params) {
109989
- return params.repoIntelligence && !!params.event.issue_number && !params.event.is_pr;
110014
+ return params.repoIntelligence && !params.event.is_pr;
109990
110015
  }
109991
110016
 
109992
110017
  // utils/activity.ts
@@ -112115,7 +112140,11 @@ function resolveRunEffort(ctx) {
112115
112140
  const configured = ctx.payload.effort !== void 0;
112116
112141
  const position = ctx.payload.effort ?? DEFAULT_EFFORT_POSITION;
112117
112142
  const alias = resolveRunAlias(ctx);
112118
- if (!alias) return { position, configured, rung: void 0, alias: void 0 };
112143
+ if (!alias) {
112144
+ const declarable = configured && !ctx.resolvedModel?.startsWith(`${AZURE_PROVIDER}/`);
112145
+ const rung = declarable && ctx.declaredVariants ? resolveRung({ position, published: ctx.declaredVariants }) : void 0;
112146
+ return { position, configured, rung, alias: void 0, declarable };
112147
+ }
112119
112148
  return {
112120
112149
  position,
112121
112150
  configured,
@@ -112124,7 +112153,8 @@ function resolveRunEffort(ctx) {
112124
112153
  position,
112125
112154
  useOpenRouter: !!ctx.payload.proxyModel
112126
112155
  }),
112127
- alias
112156
+ alias,
112157
+ declarable: false
112128
112158
  };
112129
112159
  }
112130
112160
 
@@ -112141,7 +112171,7 @@ var import_semver = __toESM(require_semver2(), 1);
112141
112171
  // package.json
112142
112172
  var package_default = {
112143
112173
  name: "pullfrog",
112144
- version: "0.1.103",
112174
+ version: "0.1.105",
112145
112175
  cliContractVersion: "1",
112146
112176
  type: "module",
112147
112177
  bin: {
@@ -124270,6 +124300,17 @@ function parseModel2(value2) {
124270
124300
  if (slash <= 0) return void 0;
124271
124301
  return { providerID: value2.slice(0, slash), modelID: value2.slice(slash + 1) };
124272
124302
  }
124303
+ async function resolveDeclaredRung(ctx, params) {
124304
+ const resp = await params.client.config.providers();
124305
+ const provider2 = resp.data?.providers.find((p) => p.id === params.model.providerID);
124306
+ const variants = provider2?.models[params.model.modelID]?.variants ?? {};
124307
+ const name = `${params.model.providerID}/${params.model.modelID}`;
124308
+ const rung = resolveRunEffort({ ...ctx, declaredVariants: Object.keys(variants) }).rung;
124309
+ log.info(
124310
+ `\xBB effort: ${rung ? `${rung} (declared by opencode for ${name})` : `not applied \u2014 opencode declares no variants for ${name}`}`
124311
+ );
124312
+ return rung;
124313
+ }
124273
124314
  function bootOpencodeServer(params) {
124274
124315
  const logLevel = isDebugEnabled() ? "INFO" : "ERROR";
124275
124316
  if (logLevel !== "ERROR") log.info(`\xBB opencode log level: ${logLevel} (debug run)`);
@@ -124924,6 +124965,11 @@ var opencode = agent({
124924
124965
  log.info(`\xBB opencode session: ${sessionID}`);
124925
124966
  const labeler = new SessionLabeler();
124926
124967
  labeler.labelFor(sessionID);
124968
+ const sdkModel = parseModel2(model);
124969
+ const variant = effort.declarable && sdkModel ? await resolveDeclaredRung(
124970
+ { ...ctx, resolvedModel: rawModel },
124971
+ { client: client2, model: sdkModel }
124972
+ ) : effort.rung;
124927
124973
  const runnerCtx = {
124928
124974
  client: client2,
124929
124975
  sessionID,
@@ -124932,7 +124978,7 @@ var opencode = agent({
124932
124978
  labeler,
124933
124979
  toolState: ctx.toolState,
124934
124980
  // `rawModel` folds in the auto-select pick — see the opencode.ts twin.
124935
- variant: effort.rung,
124981
+ variant,
124936
124982
  todoTracker: ctx.todoTracker,
124937
124983
  onActivityTimeout: ctx.onActivityTimeout,
124938
124984
  onTurnRecovered: ctx.onTurnRecovered,
@@ -124994,7 +125040,6 @@ var opencode = agent({
124994
125040
  timeoutMs: AGENT_ACTIVITY_TIMEOUT_MS,
124995
125041
  abortTurn: () => turnController.abort()
124996
125042
  });
124997
- const sdkModel = parseModel2(model);
124998
125043
  let salvagesLeft = 1;
124999
125044
  const runTurn = async (text) => {
125000
125045
  const attempt = (prompt) => {
@@ -190129,14 +190174,18 @@ async function runGitLines(args2) {
190129
190174
  // utils/rangeDiff.ts
190130
190175
  function computeIncrementalDiff(params) {
190131
190176
  try {
190177
+ const tips = [`origin/${params.baseBranch}`, ...params.formerBaseTips];
190178
+ const oldBase = versionBase({ sha: params.beforeSha, tips });
190179
+ const newBase = versionBase({ sha: params.headSha, tips });
190132
190180
  const raw2 = $(
190133
190181
  "sh",
190134
190182
  [
190135
190183
  "-c",
190136
- 'old_base=$(git merge-base "$1" "origin/$2") && new_base=$(git merge-base "$3" "origin/$2") && git range-diff --no-color --creation-factor=1000 "$old_base..$(git commit-tree "$1^{tree}" -p "$old_base" -m x)" "$new_base..$(git commit-tree "$3^{tree}" -p "$new_base" -m x)"',
190184
+ 'git range-diff --no-color --creation-factor=1000 "$1..$(git commit-tree "$2^{tree}" -p "$1" -m x)" "$3..$(git commit-tree "$4^{tree}" -p "$3" -m x)"',
190137
190185
  "--",
190186
+ oldBase,
190138
190187
  params.beforeSha,
190139
- params.baseBranch,
190188
+ newBase,
190140
190189
  params.headSha
190141
190190
  ],
190142
190191
  { log: false }
@@ -190149,6 +190198,27 @@ function computeIncrementalDiff(params) {
190149
190198
  return { status: "unavailable", reason };
190150
190199
  }
190151
190200
  }
190201
+ function git(args2) {
190202
+ return $("git", args2, { log: false }).trim();
190203
+ }
190204
+ function versionBase(params) {
190205
+ let best;
190206
+ for (const tip of params.tips) {
190207
+ let sha;
190208
+ try {
190209
+ sha = git(["merge-base", params.sha, tip]);
190210
+ } catch {
190211
+ continue;
190212
+ }
190213
+ const commits = Number(git(["rev-list", "--count", `${sha}..${params.sha}`]));
190214
+ if (!best || commits < best.commits) best = { tip, sha, commits };
190215
+ }
190216
+ if (!best) throw new Error(`no merge base between ${params.sha} and the base branch`);
190217
+ if (best.tip !== params.tips[0]) {
190218
+ log.info(`\xBB incremental diff: ${params.sha.slice(0, 7)} sits on former base tip ${best.tip}`);
190219
+ }
190220
+ return best.sha;
190221
+ }
190152
190222
  function isDiffPrefix(ch) {
190153
190223
  return ch === " " || ch === "+" || ch === "-";
190154
190224
  }
@@ -190176,14 +190246,28 @@ function postProcessRangeDiff(raw2, contextLines = 3) {
190176
190246
  if (lastHunkHdr?.seq === line.seq) hunkHdrEmitted = true;
190177
190247
  }
190178
190248
  function flushBefore() {
190179
- if (lastOuterHdr && !outerHdrEmitted) emit(lastOuterHdr);
190180
- if (lastFileHdr && !fileHdrEmitted) emit(lastFileHdr);
190181
- if (lastHunkHdr && !hunkHdrEmitted) emit(lastHunkHdr);
190182
- for (const line of beforeBuf) {
190183
- if (line.seq > lastEmittedSeq) emit(line);
190249
+ const headers = [
190250
+ outerHdrEmitted ? null : lastOuterHdr,
190251
+ fileHdrEmitted ? null : lastFileHdr,
190252
+ hunkHdrEmitted ? null : lastHunkHdr
190253
+ ].filter((line) => line !== null);
190254
+ const pending = [...headers, ...beforeBuf.filter((line) => line.seq > lastEmittedSeq)].sort(
190255
+ (a, b) => a.seq - b.seq
190256
+ );
190257
+ for (const [i, line] of pending.entries()) {
190258
+ if (pending[i - 1]?.seq !== line.seq) emit(line);
190184
190259
  }
190185
190260
  beforeBuf.length = 0;
190186
190261
  }
190262
+ function isFileHeader(pos) {
190263
+ return raw2.startsWith("## ", pos) && !raw2.startsWith("## Commit message", pos);
190264
+ }
190265
+ function markFileHeader(line, emitted) {
190266
+ lastFileHdr = line;
190267
+ fileHdrEmitted = emitted;
190268
+ lastHunkHdr = null;
190269
+ hunkHdrEmitted = true;
190270
+ }
190187
190271
  let cursor = 0;
190188
190272
  while (cursor < raw2.length) {
190189
190273
  const eol = raw2.indexOf("\n", cursor);
@@ -190193,7 +190277,8 @@ function postProcessRangeDiff(raw2, contextLines = 3) {
190193
190277
  continue;
190194
190278
  }
190195
190279
  if (raw2.startsWith(" @@", cursor)) {
190196
- lastOuterHdr = { prefix: "", from: cursor + 4, to: lineEnd, seq };
190280
+ const label = raw2.slice(cursor + 4, lineEnd);
190281
+ lastOuterHdr = label === "@@ Metadata" || label === "@@ Commit message" ? null : { prefix: "", from: cursor + 4, to: lineEnd, seq };
190197
190282
  outerHdrEmitted = false;
190198
190283
  lastFileHdr = null;
190199
190284
  fileHdrEmitted = true;
@@ -190214,13 +190299,13 @@ function postProcessRangeDiff(raw2, contextLines = 3) {
190214
190299
  } else if (isDiffPrefix(raw2[contentPos])) {
190215
190300
  isChange = isOuterChange;
190216
190301
  line = { prefix, from: contentPos + 1, to: lineEnd, seq };
190302
+ if (raw2[contentPos] === " " && isFileHeader(contentPos + 1)) {
190303
+ markFileHeader(line, isChange);
190304
+ }
190217
190305
  } else {
190218
190306
  line = { prefix, from: contentPos, to: lineEnd, seq };
190219
- if (raw2.startsWith("## ", contentPos) && !raw2.startsWith("## Commit message", contentPos)) {
190220
- lastFileHdr = line;
190221
- fileHdrEmitted = false;
190222
- lastHunkHdr = null;
190223
- hunkHdrEmitted = true;
190307
+ if (isFileHeader(contentPos)) {
190308
+ markFileHeader(line, false);
190224
190309
  } else if (raw2.startsWith("@@", contentPos) && !raw2.startsWith("@@ Metadata", contentPos)) {
190225
190310
  lastHunkHdr = line;
190226
190311
  hunkHdrEmitted = false;
@@ -196213,6 +196298,7 @@ var STRING_KEYS = [
196213
196298
  "prNodeId",
196214
196299
  "issueNodeId",
196215
196300
  "reviewNodeId",
196301
+ "reviewedCommitSha",
196216
196302
  "planCommentNodeId",
196217
196303
  "summarySnapshot",
196218
196304
  "model",
@@ -196343,13 +196429,16 @@ function buildImplementPlanLink(ctx, issueNumber, commentId) {
196343
196429
  const apiUrl = getApiUrl();
196344
196430
  return `[Implement plan \u2794](${apiUrl}/trigger/${ctx.repo.owner}/${ctx.repo.name}/${issueNumber}?action=implement&comment_id=${commentId})`;
196345
196431
  }
196346
- function buildEnrichmentLinks(ctx) {
196432
+ function runIssueNumber(ctx) {
196433
+ return ctx.payload.event.issue_number ?? primaryRepoState(ctx.toolState).issueNumber;
196434
+ }
196435
+ function buildEnrichmentLinks(ctx, issueNumber) {
196347
196436
  const event = ctx.payload.event;
196348
- if (event.trigger !== "issues_opened" || event.is_pr || ctx.payload.prompt || ctx.toolState.selectedMode === "Build") {
196349
- return [];
196350
- }
196437
+ const mode = ctx.toolState.selectedMode;
196438
+ const enriching = mode === "Enrich" || event.trigger === "issues_opened" && !ctx.payload.prompt && mode !== "Build";
196439
+ if (issueNumber === void 0 || event.is_pr || !enriching) return [];
196351
196440
  return [
196352
- `[Implement a fix \u2794](${getApiUrl()}/trigger/${ctx.repo.owner}/${ctx.repo.name}/${event.issue_number}?action=build)`
196441
+ `[Implement a fix \u2794](${getApiUrl()}/trigger/${ctx.repo.owner}/${ctx.repo.name}/${issueNumber}?action=build)`
196353
196442
  ];
196354
196443
  }
196355
196444
  function addFooter(ctx, body) {
@@ -196385,10 +196474,11 @@ function CreateCommentTool(ctx) {
196385
196474
  });
196386
196475
  ctx.toolState.wasUpdated = true;
196387
196476
  log.info(`\xBB created comment ${result.data.id}`);
196388
- if (issueNumber === ctx.payload.event.issue_number) {
196477
+ const ownTarget = issueNumber === runIssueNumber(ctx);
196478
+ if (ownTarget) {
196389
196479
  ctx.toolState.standaloneCommentId = result.data.id;
196390
196480
  }
196391
- const enrichmentLinks = issueNumber === ctx.payload.event.issue_number ? buildEnrichmentLinks(ctx) : [];
196481
+ const enrichmentLinks = ownTarget ? buildEnrichmentLinks(ctx, issueNumber) : [];
196392
196482
  if (commentType === "Plan" || enrichmentLinks.length > 0) {
196393
196483
  if (commentType === "Plan" && result.data.node_id) {
196394
196484
  await patchWorkflowRunFields(ctx, { planCommentNodeId: result.data.node_id });
@@ -196468,9 +196558,9 @@ async function reportProgress(ctx, params) {
196468
196558
  if (params.liveProgress && !ctx.payload.progressComments) {
196469
196559
  return { body, action: "skipped" };
196470
196560
  }
196471
- const issueNumber = ctx.payload.event.issue_number ?? primaryRepoState(ctx.toolState).issueNumber;
196561
+ const issueNumber = runIssueNumber(ctx);
196472
196562
  const isPlanMode = ctx.toolState.selectedMode === "Plan";
196473
- const enrichmentLinks = params.liveProgress ? [] : buildEnrichmentLinks(ctx);
196563
+ const enrichmentLinks = params.liveProgress ? [] : buildEnrichmentLinks(ctx, issueNumber);
196474
196564
  const apiCtx = { octokit: ctx.octokit, owner: ctx.repo.owner, repo: ctx.repo.name };
196475
196565
  if (target_plan_comment === true && ctx.toolState.existingPlanCommentId === void 0) {
196476
196566
  log.warning("target_plan_comment requested but no existingPlanCommentId in tool state");
@@ -197377,7 +197467,10 @@ async function createAndSubmitWithFooter(ctx, params, opts) {
197377
197467
  }
197378
197468
  }
197379
197469
  async function reportReviewNodeId(ctx, params) {
197380
- await patchWorkflowRunFields(ctx, { reviewNodeId: params.nodeId });
197470
+ await patchWorkflowRunFields(ctx, {
197471
+ reviewNodeId: params.nodeId,
197472
+ reviewedCommitSha: params.reviewedSha
197473
+ });
197381
197474
  }
197382
197475
 
197383
197476
  // utils/setup.ts
@@ -197971,6 +198064,34 @@ function describeHead(h) {
197971
198064
  if (h.kind === "branch") return `branch \`${h.name}\``;
197972
198065
  return `detached HEAD \`${h.sha}\``;
197973
198066
  }
198067
+ var FORMER_BASE_TIPS_QUERY = `
198068
+ query ($owner: String!, $repo: String!, $number: Int!) {
198069
+ repository(owner: $owner, name: $repo) {
198070
+ pullRequest(number: $number) {
198071
+ timelineItems(itemTypes: [BASE_REF_FORCE_PUSHED_EVENT], last: 100) {
198072
+ nodes { ... on BaseRefForcePushedEvent { beforeCommit { oid } } }
198073
+ }
198074
+ }
198075
+ }
198076
+ }
198077
+ `;
198078
+ async function getFormerBaseTips(ctx, pullNumber) {
198079
+ try {
198080
+ const response = await ctx.octokit.graphql(FORMER_BASE_TIPS_QUERY, {
198081
+ owner: ctx.repo.owner,
198082
+ repo: ctx.repo.name,
198083
+ number: pullNumber
198084
+ });
198085
+ return response.repository.pullRequest.timelineItems.nodes.flatMap(
198086
+ (node3) => node3.beforeCommit ? [node3.beforeCommit.oid] : []
198087
+ );
198088
+ } catch (error63) {
198089
+ log.warning(
198090
+ `\xBB former base tips unavailable: ${error63 instanceof Error ? error63.message : String(error63)}`
198091
+ );
198092
+ return [];
198093
+ }
198094
+ }
197974
198095
  function CheckoutPrTool(ctx) {
197975
198096
  const runCheckout = async (pull_number, signal) => {
197976
198097
  const prResponse = await ctx.octokit.rest.pulls.get({
@@ -198023,7 +198144,8 @@ function CheckoutPrTool(ctx) {
198023
198144
  const incremental = computeIncrementalDiff({
198024
198145
  baseBranch: pr.baseRef,
198025
198146
  beforeSha: primary.beforeSha,
198026
- headSha: checkoutSha
198147
+ headSha: checkoutSha,
198148
+ formerBaseTips: await getFormerBaseTips(ctx, pull_number)
198027
198149
  });
198028
198150
  if (incremental.status === "ok") {
198029
198151
  incrementalDiffPath = join16(
@@ -199449,7 +199571,7 @@ function GetIssueCommentsTool(ctx) {
199449
199571
  description: "Get all comments for a GitHub issue. Returns all comments including the issue body and all subsequent discussion comments. Example: `get_issue_comments({ issue_number: 1234 })`.",
199450
199572
  parameters: GetIssueComments,
199451
199573
  execute: execute(async ({ issue_number }) => {
199452
- primaryRepoState(ctx.toolState).issueNumber = issue_number;
199574
+ primaryRepoState(ctx.toolState).issueNumber ??= issue_number;
199453
199575
  const comments = await ctx.octokit.paginate(ctx.octokit.rest.issues.listComments, {
199454
199576
  owner: ctx.repo.owner,
199455
199577
  repo: ctx.repo.name,
@@ -199487,7 +199609,7 @@ function GetIssueEventsTool(ctx) {
199487
199609
  description: "Get timeline events for a GitHub issue that aren't reflected in the current state. Returns cross-references to other issues/PRs and commit references. Note: current labels, assignees, state, and milestone are already available via get_issue.",
199488
199610
  parameters: GetIssueEvents,
199489
199611
  execute: execute(async ({ issue_number }) => {
199490
- primaryRepoState(ctx.toolState).issueNumber = issue_number;
199612
+ primaryRepoState(ctx.toolState).issueNumber ??= issue_number;
199491
199613
  const events = await ctx.octokit.paginate(ctx.octokit.rest.issues.listEventsForTimeline, {
199492
199614
  owner: ctx.repo.owner,
199493
199615
  repo: ctx.repo.name,
@@ -199571,7 +199693,7 @@ function IssueInfoTool(ctx) {
199571
199693
  tmpdir: ctx.tmpdir,
199572
199694
  githubToken: ctx.githubInstallationToken
199573
199695
  });
199574
- primaryRepoState(ctx.toolState).issueNumber = issue_number;
199696
+ primaryRepoState(ctx.toolState).issueNumber ??= issue_number;
199575
199697
  const hints = [];
199576
199698
  if (data.comments > 0) {
199577
199699
  hints.push("use get_issue_comments to retrieve all comments for this issue");
@@ -201039,8 +201161,11 @@ function SimilarIssuesTool(ctx) {
201039
201161
  mutates: true,
201040
201162
  parameters: SimilarIssues,
201041
201163
  execute: execute(async (input2) => {
201042
- if (ctx.payload.event.is_pr || ctx.payload.event.issue_number !== input2.issue_number) {
201043
- throw new Error("find_similar_issues is limited to the issue that triggered this run");
201164
+ const repoState = primaryRepoState(ctx.toolState);
201165
+ repoState.issueNumber ??= input2.issue_number;
201166
+ const subject = ctx.payload.event.issue_number ?? repoState.issueNumber;
201167
+ if (ctx.payload.event.is_pr || subject !== input2.issue_number) {
201168
+ throw new Error(`find_similar_issues is limited to this run's issue, #${subject}`);
201044
201169
  }
201045
201170
  const response = await apiFetch({
201046
201171
  path: `/api/repo/${ctx.repo.owner}/${ctx.repo.name}/issues/${input2.issue_number}/similar`,
@@ -201322,7 +201447,7 @@ function buildCommonTools(ctx, outputSchema) {
201322
201447
  if (hasSimilarIssues({ repoIntelligence: ctx.repoIntelligence, event: ctx.payload.event })) {
201323
201448
  tools.push(SimilarIssuesTool(ctx));
201324
201449
  }
201325
- if (ctx.payload.event.issue_number !== void 0 && !ctx.payload.event.is_pr) {
201450
+ if (!ctx.payload.event.is_pr) {
201326
201451
  tools.push(SearchIssuesTool(ctx));
201327
201452
  }
201328
201453
  const isStandalone = ctx.payload.event.trigger === "unknown";
@@ -201720,11 +201845,11 @@ function buildOpenAICompatibleSetupError(params) {
201720
201845
  missing: params.missing,
201721
201846
  where: `Set them on the OpenAI-compatible provider in [Pullfrog's provider settings](${getApiUrl()}/console/${params.owner}#billing), or in the workflow's \`env:\` block with the key as an [Actions secret](${secretsUrl(params)}):`,
201722
201847
  env: [
201723
- `${OPENAI_COMPATIBLE_BASE_URL_ENV}: https://your-endpoint.example.com/v1`,
201724
- `${OPENAI_COMPATIBLE_API_KEY_ENV}: \${{ secrets.${OPENAI_COMPATIBLE_API_KEY_ENV} }}`,
201725
- `${OPENAI_COMPATIBLE_MODEL_ENV}: <model-id>`,
201726
- `${OPENAI_COMPATIBLE_CONTEXT_ENV}: "128000"`,
201727
- `${OPENAI_COMPATIBLE_MAX_OUTPUT_ENV}: "16384"`
201848
+ `${params.env.baseUrl}: https://your-endpoint.example.com/v1`,
201849
+ `${params.env.apiKey}: \${{ secrets.${params.env.apiKey} }}`,
201850
+ `${params.env.model}: <model-id>`,
201851
+ `${params.env.context}: "128000"`,
201852
+ `${params.env.maxOutput}: "16384"`
201728
201853
  ],
201729
201854
  note: "Use the real context and output limits of the model your endpoint serves: Pullfrog can't discover them, and without them long runs grow until the endpoint refuses them.",
201730
201855
  guide: "https://docs.pullfrog.com/openai-compatible"
@@ -201758,17 +201883,16 @@ function hasPositiveNumberEnvVar(name) {
201758
201883
  return Number(process.env[name]) > 0;
201759
201884
  }
201760
201885
  function validateOpenAICompatibleSetup(params) {
201886
+ const env2 = openAICompatibleEnv(params.set);
201761
201887
  const missing = [];
201762
- if (!hasEnvVar2(OPENAI_COMPATIBLE_BASE_URL_ENV)) missing.push(OPENAI_COMPATIBLE_BASE_URL_ENV);
201763
- if (!hasEnvVar2(OPENAI_COMPATIBLE_API_KEY_ENV)) missing.push(OPENAI_COMPATIBLE_API_KEY_ENV);
201764
- if (!hasEnvVar2(OPENAI_COMPATIBLE_MODEL_ENV)) missing.push(OPENAI_COMPATIBLE_MODEL_ENV);
201765
- if (!hasPositiveNumberEnvVar(OPENAI_COMPATIBLE_CONTEXT_ENV))
201766
- missing.push(OPENAI_COMPATIBLE_CONTEXT_ENV);
201767
- if (!hasPositiveNumberEnvVar(OPENAI_COMPATIBLE_MAX_OUTPUT_ENV))
201768
- missing.push(OPENAI_COMPATIBLE_MAX_OUTPUT_ENV);
201888
+ if (!hasEnvVar2(env2.baseUrl)) missing.push(env2.baseUrl);
201889
+ if (!hasEnvVar2(env2.apiKey)) missing.push(env2.apiKey);
201890
+ if (!hasEnvVar2(env2.model)) missing.push(env2.model);
201891
+ if (!hasPositiveNumberEnvVar(env2.context)) missing.push(env2.context);
201892
+ if (!hasPositiveNumberEnvVar(env2.maxOutput)) missing.push(env2.maxOutput);
201769
201893
  if (missing.length > 0) {
201770
201894
  throw new Error(
201771
- buildOpenAICompatibleSetupError({ owner: params.owner, name: params.name, missing })
201895
+ buildOpenAICompatibleSetupError({ owner: params.owner, name: params.name, missing, env: env2 })
201772
201896
  );
201773
201897
  }
201774
201898
  }
@@ -201824,7 +201948,11 @@ function validateAgentApiKey(params) {
201824
201948
  return;
201825
201949
  }
201826
201950
  if (params.model.startsWith(`${OPENAI_COMPATIBLE_PROVIDER}/`)) {
201827
- validateOpenAICompatibleSetup({ owner: params.owner, name: params.name });
201951
+ validateOpenAICompatibleSetup({
201952
+ owner: params.owner,
201953
+ name: params.name,
201954
+ set: params.openAICompatibleSet
201955
+ });
201828
201956
  return;
201829
201957
  }
201830
201958
  if (params.model.startsWith(`${AZURE_PROVIDER}/`)) {
@@ -202138,17 +202266,37 @@ function resolveSlug(slug2) {
202138
202266
  }
202139
202267
  return `${AZURE_PROVIDER}/${deployment}`;
202140
202268
  }
202141
- if (alias?.routing === "openai-compatible") {
202142
- const modelId = process.env[OPENAI_COMPATIBLE_MODEL_ENV]?.trim();
202269
+ const set2 = heldOpenAICompatibleSet(slug2);
202270
+ if (alias?.routing === "openai-compatible" || set2) {
202271
+ const modelEnv = openAICompatibleEnv(set2).model;
202272
+ const modelId = process.env[modelEnv]?.trim();
202143
202273
  if (!modelId) {
202144
202274
  throw new Error(
202145
- `\`${OPENAI_COMPATIBLE_MODEL_ENV}\` ${BYOK_SLUG_MARKER} \`${slug2}\`. Set it to the model ID your endpoint serves, on the OpenAI-compatible provider in Pullfrog or in the workflow's \`env:\` block. [OpenAI-compatible setup guide](https://docs.pullfrog.com/openai-compatible)`
202275
+ `\`${modelEnv}\` ${BYOK_SLUG_MARKER} \`${slug2}\`. Set it to the model ID your endpoint serves, on the OpenAI-compatible provider in Pullfrog or in the workflow's \`env:\` block. [OpenAI-compatible setup guide](https://docs.pullfrog.com/openai-compatible)`
202146
202276
  );
202147
202277
  }
202148
202278
  return `${OPENAI_COMPATIBLE_PROVIDER}/${modelId}`;
202149
202279
  }
202150
202280
  return resolveCliModel(slug2);
202151
202281
  }
202282
+ function heldOpenAICompatibleSet(slug2) {
202283
+ const set2 = openAICompatibleSetName(slug2);
202284
+ return set2 && Object.values(openAICompatibleEnv(set2)).some(hasEnvVar3) ? set2 : void 0;
202285
+ }
202286
+ function activateOpenAICompatibleSet(ctx) {
202287
+ const slug2 = process.env.PULLFROG_MODEL?.trim() || ctx.slug?.trim();
202288
+ const set2 = slug2 ? heldOpenAICompatibleSet(slug2) : void 0;
202289
+ if (!set2) return void 0;
202290
+ const from = openAICompatibleEnv(set2);
202291
+ const to = openAICompatibleEnv();
202292
+ for (const field of OPENAI_COMPATIBLE_FIELDS) {
202293
+ const value2 = process.env[from[field]];
202294
+ if (value2 === void 0) delete process.env[to[field]];
202295
+ else process.env[to[field]] = value2;
202296
+ }
202297
+ log.info(`\xBB using the OpenAI-compatible endpoint "${set2}"`);
202298
+ return set2;
202299
+ }
202152
202300
  function resolveModel(ctx) {
202153
202301
  const envModel = process.env.PULLFROG_MODEL?.trim();
202154
202302
  if (envModel) {
@@ -203191,7 +203339,7 @@ Rules:
203191
203339
  ### GitHub
203192
203340
 
203193
203341
  Use MCP tools from ${pullfrogMcpName} for all GitHub operations. Never use the \`gh\` CLI \u2014 it is not authenticated and will fail. The MCP tools handle authentication and enforce permissions.
203194
- ${hasSimilarIssues({ repoIntelligence: ctx.repoIntelligence, event: ctx.payload.event }) ? `
203342
+ ${ctx.payload.event.issue_number !== void 0 && hasSimilarIssues({ repoIntelligence: ctx.repoIntelligence, event: ctx.payload.event }) ? `
203195
203343
  #### Duplicate detection (enabled for this repository)
203196
203344
 
203197
203345
  Call \`${t("find_similar_issues")}\` for #${ctx.payload.event.issue_number} before planning. If it duplicates an existing issue, link that instead of producing a plan; never close or label on similarity alone.
@@ -204661,7 +204809,7 @@ async function postReviewCleanup(ctx) {
204661
204809
  if (!review) {
204662
204810
  const checkoutSha = primaryRepoState(ctx.toolState).checkoutSha;
204663
204811
  if (ctx.toolState.selectedMode === "IncrementalReview" && checkoutSha) {
204664
- await patchWorkflowRunFields(ctx, { reviewSkipped: true });
204812
+ await patchWorkflowRunFields(ctx, { reviewSkipped: true, reviewedCommitSha: checkoutSha });
204665
204813
  await bestEffort(
204666
204814
  () => dispatchFollowUpReReview(ctx, checkoutSha),
204667
204815
  "follow-up re-review dispatch"
@@ -204670,7 +204818,7 @@ async function postReviewCleanup(ctx) {
204670
204818
  return;
204671
204819
  }
204672
204820
  delete ctx.toolState.review;
204673
- await bestEffort(() => reportReviewNodeId(ctx, { nodeId: review.nodeId }), "reportReviewNodeId");
204821
+ await bestEffort(() => reportReviewNodeId(ctx, review), "reportReviewNodeId");
204674
204822
  if (review.reviewedSha) {
204675
204823
  await bestEffort(
204676
204824
  () => dispatchFollowUpReReview(ctx, review.reviewedSha),
@@ -205070,6 +205218,9 @@ function resolveModelForLog(ctx) {
205070
205218
  function resolveEffortForLog(ctx) {
205071
205219
  const effort = resolveRunEffort(ctx);
205072
205220
  if (!ctx.resolvedModel && !ctx.payload.proxyModel) return "pending \u2014 model not chosen yet";
205221
+ if (effort.declarable && ctx.agentName === "opencode") {
205222
+ return "pending \u2014 reading the variants opencode declares";
205223
+ }
205073
205224
  if (!effort.alias) return "not applied \u2014 model not recognized";
205074
205225
  if (!effort.rung) return "n/a (model has no effort control)";
205075
205226
  return effort.configured ? effort.rung : `${effort.rung} (default)`;
@@ -205088,9 +205239,7 @@ function logRunStartup(ctx) {
205088
205239
  log.info(
205089
205240
  `\xBB model: ${resolveModelForLog({ payload: ctx.payload, resolvedModel: ctx.resolvedModel })}`
205090
205241
  );
205091
- log.info(
205092
- `\xBB effort: ${resolveEffortForLog({ payload: ctx.payload, resolvedModel: ctx.resolvedModel })}`
205093
- );
205242
+ log.info(`\xBB effort: ${resolveEffortForLog(ctx)}`);
205094
205243
  const routing = ctx.payload.routing;
205095
205244
  if (routing) {
205096
205245
  log.info(
@@ -205361,6 +205510,7 @@ async function main() {
205361
205510
  const count = Object.keys(runContext.dbSecrets).length;
205362
205511
  if (count > 0) log.info(`\xBB ${count} db secret(s) loaded`);
205363
205512
  }
205513
+ const openAICompatibleSet = activateOpenAICompatibleSet({ slug: payload.model });
205364
205514
  await initializeCredentialPool({ ctx: runContext, slug: payload.model, injected });
205365
205515
  installCodexAuth();
205366
205516
  installXaiAuth();
@@ -205509,7 +205659,8 @@ async function main() {
205509
205659
  owner: runContext.repo.owner,
205510
205660
  name: runContext.repo.name,
205511
205661
  secretsUnavailable: runContext.secretsUnavailable,
205512
- routerUnfunded: runContext.routerUnfunded
205662
+ routerUnfunded: runContext.routerUnfunded,
205663
+ openAICompatibleSet
205513
205664
  });
205514
205665
  } catch (missingKey) {
205515
205666
  const funded = runContext.trialFallback && missingKey instanceof NoUsableCredentialError ? await resolveTrialFallback({
@@ -7,6 +7,7 @@ export type { ConfigField, ConfigScope, ConfigValue } from "../configuration.ts"
7
7
  export { configEntrySchema, configFields, configResponseSchema, configValueSchema, } from "../configuration.ts";
8
8
  export type { AuthorPermission, AutoTier, EffortPosition, ModelAlias, ModelProvider, Payload, PayloadEvent, PayloadRouting, ProviderConfig, PushPermission, RouterSource, RouterTier, ShellPermission, ToolPermission, WriteablePayload, XrepoConfig, } from "../external.ts";
9
9
  export { AUTO_EFFICIENT, AUTO_INTELLIGENT, AUTO_ROUTER, autoLadder, autoRoutedSlug, DEFAULT_EFFORT_POSITION, DEFAULT_PROXY_MODEL, defaultAutoTier, EFFORT_ALIASES, getAutoSelectHintModel, getModelEffortLevels, getModelEnvVars, getModelManagedCredentials, getModelProvider, getProviderDisplayName, isAutoRouted, isAutoSlug, isAutoTier, isCardGatedModel, isEffortPosition, isOssAllowedModel, isRouterTier, ladderRungs, modelAliases, modelHasStoredAuth, modelIdLabel, OSS_MODEL_ALLOWLIST, OSS_RECOMMENDED_MODEL, offeredRungs, ossFundedModelNames, PROVIDER_GATEWAY_URL_ENV, parseEffortPosition, parseModel, providers, pullfrogMcpName, ROUTED_PROVIDERS, ROUTER_LADDER, ROUTER_TIERS, resolveAutoTier, resolveCliModel, resolveDisplayAlias, resolveModelRung, resolveModelSlug, resolveOpenRouterModel, resolvePickModel, resolveRoutedModel, resolveRung, routerProxyOptOut, rungLabel, rungPosition, SUBSIDY_RUNG, TRIAL_FUNDED_USD_LIMIT, } from "../external.ts";
10
+ export { isOpenAICompatibleSetName, openAICompatibleEnv, openAICompatibleSetAlias, openAICompatibleSetName, openAICompatibleSetOf, openAICompatibleSets, } from "../models.ts";
10
11
  export type { Mode } from "../modes.ts";
11
12
  export { modes } from "../modes.ts";
12
13
  export { type CommercialRefusal, commercialPaywallBody } from "../utils/billingErrors.ts";