pullfrog 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -18198,7 +18198,7 @@ var require_summary = __commonJS({
18198
18198
  exports.summary = exports.markdownSummary = exports.SUMMARY_DOCS_URL = exports.SUMMARY_ENV_VAR = void 0;
18199
18199
  var os_1 = __require("os");
18200
18200
  var fs_1 = __require("fs");
18201
- var { access, appendFile, writeFile: writeFile3 } = fs_1.promises;
18201
+ var { access, appendFile, writeFile: writeFile4 } = fs_1.promises;
18202
18202
  exports.SUMMARY_ENV_VAR = "GITHUB_STEP_SUMMARY";
18203
18203
  exports.SUMMARY_DOCS_URL = "https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary";
18204
18204
  var Summary = class {
@@ -18256,7 +18256,7 @@ var require_summary = __commonJS({
18256
18256
  return __awaiter(this, void 0, void 0, function* () {
18257
18257
  const overwrite = !!(options === null || options === void 0 ? void 0 : options.overwrite);
18258
18258
  const filePath = yield this.filePath();
18259
- const writeFunc = overwrite ? writeFile3 : appendFile;
18259
+ const writeFunc = overwrite ? writeFile4 : appendFile;
18260
18260
  yield writeFunc(filePath, this._buffer, { encoding: "utf8" });
18261
18261
  return this.emptyBuffer();
18262
18262
  });
@@ -62662,8 +62662,8 @@ var require_snapshot_utils = __commonJS({
62662
62662
  var require_snapshot_recorder = __commonJS({
62663
62663
  "node_modules/.pnpm/undici@7.22.0/node_modules/undici/lib/mock/snapshot-recorder.js"(exports, module) {
62664
62664
  "use strict";
62665
- var { writeFile: writeFile3, readFile: readFile4, mkdir: mkdir2 } = __require("node:fs/promises");
62666
- var { dirname: dirname5, resolve: resolve3 } = __require("node:path");
62665
+ var { writeFile: writeFile4, readFile: readFile5, mkdir: mkdir3 } = __require("node:fs/promises");
62666
+ var { dirname: dirname6, resolve: resolve3 } = __require("node:path");
62667
62667
  var { setTimeout: setTimeout2, clearTimeout: clearTimeout2 } = __require("node:timers");
62668
62668
  var { InvalidArgumentError, UndiciError } = require_errors4();
62669
62669
  var { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = require_snapshot_utils();
@@ -62864,7 +62864,7 @@ var require_snapshot_recorder = __commonJS({
62864
62864
  throw new InvalidArgumentError("Snapshot path is required");
62865
62865
  }
62866
62866
  try {
62867
- const data = await readFile4(resolve3(path3), "utf8");
62867
+ const data = await readFile5(resolve3(path3), "utf8");
62868
62868
  const parsed2 = JSON.parse(data);
62869
62869
  if (Array.isArray(parsed2)) {
62870
62870
  this.#snapshots.clear();
@@ -62894,12 +62894,12 @@ var require_snapshot_recorder = __commonJS({
62894
62894
  throw new InvalidArgumentError("Snapshot path is required");
62895
62895
  }
62896
62896
  const resolvedPath = resolve3(path3);
62897
- await mkdir2(dirname5(resolvedPath), { recursive: true });
62897
+ await mkdir3(dirname6(resolvedPath), { recursive: true });
62898
62898
  const data = Array.from(this.#snapshots.entries()).map(([hash2, snapshot2]) => ({
62899
62899
  hash: hash2,
62900
62900
  snapshot: snapshot2
62901
62901
  }));
62902
- await writeFile3(resolvedPath, JSON.stringify(data, null, 2), { flush: true });
62902
+ await writeFile4(resolvedPath, JSON.stringify(data, null, 2), { flush: true });
62903
62903
  }
62904
62904
  /**
62905
62905
  * Clears all recorded snapshots
@@ -97475,14 +97475,14 @@ var require_turndown_cjs = __commonJS({
97475
97475
  } else if (node2.nodeType === 1) {
97476
97476
  replacement = replacementForNode.call(self2, node2);
97477
97477
  }
97478
- return join17(output, replacement);
97478
+ return join18(output, replacement);
97479
97479
  }, "");
97480
97480
  }
97481
97481
  function postProcess(output) {
97482
97482
  var self2 = this;
97483
97483
  this.rules.forEach(function(rule) {
97484
97484
  if (typeof rule.append === "function") {
97485
- output = join17(output, rule.append(self2.options));
97485
+ output = join18(output, rule.append(self2.options));
97486
97486
  }
97487
97487
  });
97488
97488
  return output.replace(/^[\t\r\n]+/, "").replace(/[\t\r\n\s]+$/, "");
@@ -97494,7 +97494,7 @@ var require_turndown_cjs = __commonJS({
97494
97494
  if (whitespace.leading || whitespace.trailing) content = content.trim();
97495
97495
  return whitespace.leading + rule.replacement(content, node2, this.options) + whitespace.trailing;
97496
97496
  }
97497
- function join17(output, replacement) {
97497
+ function join18(output, replacement) {
97498
97498
  var s1 = trimTrailingNewlines(output);
97499
97499
  var s2 = trimLeadingNewlines(replacement);
97500
97500
  var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
@@ -98926,8 +98926,8 @@ var require_fast_content_type_parse = __commonJS({
98926
98926
  // main.ts
98927
98927
  var core6 = __toESM(require_core(), 1);
98928
98928
  import { existsSync as existsSync7, readdirSync } from "node:fs";
98929
- import { readFile as readFile3 } from "node:fs/promises";
98930
- import { join as join16 } from "node:path";
98929
+ import { readFile as readFile4 } from "node:fs/promises";
98930
+ import { join as join17 } from "node:path";
98931
98931
 
98932
98932
  // node_modules/.pnpm/@ark+util@0.56.0/node_modules/@ark/util/out/arrays.js
98933
98933
  var liftArray = (data) => Array.isArray(data) ? data : [data];
@@ -107723,6 +107723,13 @@ function getApiUrl() {
107723
107723
  log.debug(`resolved API_URL: ${raw2}`);
107724
107724
  return raw2;
107725
107725
  }
107726
+ function isLocalApiUrl() {
107727
+ try {
107728
+ return isLocalUrl(new URL(getApiUrl()));
107729
+ } catch {
107730
+ return false;
107731
+ }
107732
+ }
107726
107733
 
107727
107734
  // models.ts
107728
107735
  function provider(config3) {
@@ -108961,6 +108968,7 @@ function CreateCommentTool(ctx) {
108961
108968
  body: bodyWithFooter
108962
108969
  });
108963
108970
  ctx.toolState.wasUpdated = true;
108971
+ log.info(`\xBB created comment ${result.data.id}`);
108964
108972
  if (commentType === "Plan") {
108965
108973
  if (result.data.node_id) {
108966
108974
  await patchWorkflowRunFields(ctx, { planCommentNodeId: result.data.node_id });
@@ -108974,6 +108982,7 @@ function CreateCommentTool(ctx) {
108974
108982
  comment_id: result.data.id,
108975
108983
  body: bodyWithPlanLink
108976
108984
  });
108985
+ log.info(`\xBB updated comment ${updateResult.data.id}`);
108977
108986
  return {
108978
108987
  success: true,
108979
108988
  commentId: updateResult.data.id,
@@ -109007,6 +109016,7 @@ function EditCommentTool(ctx) {
109007
109016
  comment_id: commentId,
109008
109017
  body: bodyWithFooter
109009
109018
  });
109019
+ log.info(`\xBB updated comment ${result.data.id}`);
109010
109020
  return {
109011
109021
  success: true,
109012
109022
  commentId: result.data.id,
@@ -109142,6 +109152,9 @@ ${collapsible}`;
109142
109152
  message: "progress recorded (no GitHub comment created - this may occur for workflow_dispatch events or when there is no associated issue/PR)"
109143
109153
  };
109144
109154
  }
109155
+ if (result.commentId !== void 0) {
109156
+ log.info(`\xBB ${result.action} comment ${result.commentId}`);
109157
+ }
109145
109158
  if (!params.target_plan_comment) {
109146
109159
  ctx.toolState.finalSummaryWritten = true;
109147
109160
  }
@@ -109192,6 +109205,7 @@ function ReplyToReviewCommentTool(ctx) {
109192
109205
  comment_id,
109193
109206
  body: bodyWithFooter
109194
109207
  });
109208
+ log.info(`\xBB created review comment ${result.data.id} (in reply to ${comment_id})`);
109195
109209
  ctx.toolState.wasUpdated = true;
109196
109210
  return {
109197
109211
  success: true,
@@ -109696,6 +109710,7 @@ async function spawn(options) {
109696
109710
  const startTime = performance3.now();
109697
109711
  let stdoutBuffer = "";
109698
109712
  let stderrBuffer = "";
109713
+ const killGroup = options.killGroup ?? false;
109699
109714
  return new Promise((resolve3, reject) => {
109700
109715
  const child = nodeSpawn(options.cmd, options.args, {
109701
109716
  env: options.env || {
@@ -109703,9 +109718,20 @@ async function spawn(options) {
109703
109718
  HOME: process.env.HOME || ""
109704
109719
  },
109705
109720
  stdio: options.stdio || ["pipe", "pipe", "pipe"],
109706
- cwd: options.cwd || process.cwd()
109721
+ cwd: options.cwd || process.cwd(),
109722
+ detached: killGroup
109707
109723
  });
109708
- trackChild({ child });
109724
+ const killSelf = (signal) => {
109725
+ if (killGroup && child.pid) {
109726
+ try {
109727
+ process.kill(-child.pid, signal);
109728
+ return;
109729
+ } catch {
109730
+ }
109731
+ }
109732
+ child.kill(signal);
109733
+ };
109734
+ trackChild({ child, killGroup });
109709
109735
  let timeoutId;
109710
109736
  let sigkillEscalatorId;
109711
109737
  let activityCheckIntervalId;
@@ -109716,10 +109742,10 @@ async function spawn(options) {
109716
109742
  if (options.timeout) {
109717
109743
  timeoutId = setTimeout(() => {
109718
109744
  isTimedOut = true;
109719
- child.kill("SIGTERM");
109745
+ killSelf("SIGTERM");
109720
109746
  sigkillEscalatorId = setTimeout(() => {
109721
109747
  if (!child.killed) {
109722
- child.kill("SIGKILL");
109748
+ killSelf("SIGKILL");
109723
109749
  }
109724
109750
  }, 5e3);
109725
109751
  }, options.timeout);
@@ -109738,9 +109764,9 @@ async function spawn(options) {
109738
109764
  killedAtIdleMs = idleMs;
109739
109765
  const idleSec = Math.round(idleMs / 1e3);
109740
109766
  log.info(
109741
- `no output for ${idleSec}s from pid=${child.pid} (${options.cmd}), killing process`
109767
+ `no output for ${idleSec}s from pid=${child.pid} (${options.cmd}), killing process${killGroup ? " group" : ""}`
109742
109768
  );
109743
- child.kill("SIGKILL");
109769
+ killSelf("SIGKILL");
109744
109770
  clearInterval(activityCheckIntervalId);
109745
109771
  try {
109746
109772
  options.onActivityTimeout?.();
@@ -142249,7 +142275,7 @@ var import_semver = __toESM(require_semver2(), 1);
142249
142275
  // package.json
142250
142276
  var package_default = {
142251
142277
  name: "pullfrog",
142252
- version: "0.1.0",
142278
+ version: "0.1.2",
142253
142279
  type: "module",
142254
142280
  bin: {
142255
142281
  pullfrog: "dist/cli.mjs",
@@ -143193,6 +143219,10 @@ ${integrateStep}
143193
143219
  if (!pushed) {
143194
143220
  throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
143195
143221
  }
143222
+ const pushedSha = $("git", ["rev-parse", "HEAD"], { log: false }).trim();
143223
+ log.info(
143224
+ `\xBB pushed branch ${branch} to ${pushDest.remoteName}/${pushDest.remoteBranch} (sha ${pushedSha})`
143225
+ );
143196
143226
  return {
143197
143227
  success: true,
143198
143228
  branch,
@@ -143341,6 +143371,7 @@ function DeleteBranchTool(ctx) {
143341
143371
  await $git("push", ["origin", "--delete", `refs/heads/${params.branchName}`], {
143342
143372
  token: ctx.gitToken
143343
143373
  });
143374
+ log.info(`\xBB deleted branch ${params.branchName}`);
143344
143375
  return { success: true, deleted: params.branchName };
143345
143376
  })
143346
143377
  });
@@ -143366,6 +143397,7 @@ function PushTagsTool(ctx) {
143366
143397
  await $git("push", pushArgs, {
143367
143398
  token: ctx.gitToken
143368
143399
  });
143400
+ log.info(`\xBB pushed tag ${params.tag}`);
143369
143401
  return { success: true, tag: params.tag };
143370
143402
  })
143371
143403
  });
@@ -143690,6 +143722,7 @@ function CreatePullRequestReviewTool(ctx) {
143690
143722
  }
143691
143723
  const reviewId = result.data.id;
143692
143724
  const reviewNodeId = result.data.node_id;
143725
+ log.info(`\xBB created review ${reviewId} on pull request #${pull_number}`);
143693
143726
  const actuallyReviewedSha = ctx.toolState.checkoutSha ?? params.commit_id;
143694
143727
  ctx.toolState.review = {
143695
143728
  id: reviewId,
@@ -144554,6 +144587,7 @@ function IssueTool(ctx) {
144554
144587
  labels: params.labels ?? [],
144555
144588
  assignees: params.assignees ?? []
144556
144589
  });
144590
+ log.info(`\xBB created issue #${result.data.number} (id ${result.data.id})`);
144557
144591
  const nodeId = result.data.node_id;
144558
144592
  if (typeof nodeId === "string" && nodeId.length > 0) {
144559
144593
  await patchWorkflowRunFields(ctx, {
@@ -144745,6 +144779,7 @@ function AddLabelsTool(ctx) {
144745
144779
  issue_number,
144746
144780
  labels
144747
144781
  });
144782
+ log.info(`\xBB added labels [${labels.join(", ")}] to issue #${issue_number}`);
144748
144783
  return {
144749
144784
  success: true,
144750
144785
  labels: result.data.map((label) => label.name)
@@ -144753,40 +144788,6 @@ function AddLabelsTool(ctx) {
144753
144788
  });
144754
144789
  }
144755
144790
 
144756
- // mcp/learnings.ts
144757
- var UpdateLearningsParams = type({
144758
- learnings: type.string.describe(
144759
- "the FULL merged learnings as a flat bullet list. each line starts with `- `. one discrete, actionable fact per bullet. combine existing bullets from the prompt with your new discoveries. deduplicate \u2014 if an existing bullet covers the same fact, update it in place rather than adding a new one. drop bullets that are clearly wrong or no longer relevant to the current codebase. keep the list focused and concise."
144760
- )
144761
- });
144762
- function UpdateLearningsTool(ctx) {
144763
- return tool({
144764
- name: "update_learnings",
144765
- description: "persist operational learnings about this repository (setup steps, test commands, key conventions, patterns). ONLY call this when you have high confidence the information is correct and broadly useful for future runs \u2014 not for one-off findings or uncertain observations. format: flat bullet list (`- ` per line, one fact per bullet). pass the FULL merged list \u2014 combine existing learnings from the prompt with new discoveries. deduplicate, and drop bullets that are clearly wrong or no longer relevant to the current codebase.",
144766
- parameters: UpdateLearningsParams,
144767
- execute: execute(async (params) => {
144768
- const response = await apiFetch({
144769
- path: `/api/repo/${ctx.repo.owner}/${ctx.repo.name}/learnings`,
144770
- method: "PATCH",
144771
- headers: {
144772
- authorization: `Bearer ${ctx.apiToken}`,
144773
- "content-type": "application/json"
144774
- },
144775
- body: JSON.stringify({
144776
- learnings: params.learnings,
144777
- model: ctx.toolState.model
144778
- }),
144779
- signal: AbortSignal.timeout(1e4)
144780
- });
144781
- if (!response.ok) {
144782
- const error49 = await response.text();
144783
- throw new Error(`failed to update learnings: ${error49}`);
144784
- }
144785
- return { success: true };
144786
- })
144787
- });
144788
- }
144789
-
144790
144791
  // mcp/output.ts
144791
144792
  var import_ajv3 = __toESM(require_ajv(), 1);
144792
144793
  var SetOutputParams = type({
@@ -144880,6 +144881,7 @@ function UpdatePullRequestBodyTool(ctx) {
144880
144881
  pull_number: params.pull_number,
144881
144882
  body: bodyWithFooter
144882
144883
  });
144884
+ log.info(`\xBB updated pull request #${result.data.number}`);
144883
144885
  ctx.toolState.wasUpdated = true;
144884
144886
  return {
144885
144887
  success: true,
@@ -144907,6 +144909,7 @@ function CreatePullRequestTool(ctx) {
144907
144909
  base: params.base,
144908
144910
  draft: params.draft ?? false
144909
144911
  });
144912
+ log.info(`\xBB created pull request #${result.data.number} (id ${result.data.id})`);
144910
144913
  const reviewer = ctx.payload.triggerer;
144911
144914
  if (reviewer) {
144912
144915
  try {
@@ -145458,7 +145461,7 @@ function ResolveReviewThreadTool(ctx) {
145458
145461
  threadId: params.thread_id
145459
145462
  });
145460
145463
  const thread = response.resolveReviewThread.thread;
145461
- log.debug(`resolved thread ${thread.id}, isResolved=${thread.isResolved}`);
145464
+ log.info(`\xBB resolved review thread ${thread.id}`);
145462
145465
  return {
145463
145466
  thread_id: thread.id,
145464
145467
  is_resolved: thread.isResolved,
@@ -145930,6 +145933,7 @@ function UploadFileTool(ctx) {
145930
145933
  if (!uploadResponse.ok) {
145931
145934
  throw new Error(`failed to upload file: ${uploadResponse.statusText}`);
145932
145935
  }
145936
+ log.info(`\xBB uploaded file ${publicUrl}`);
145933
145937
  return { success: true, publicUrl, filename, contentLength, contentType };
145934
145938
  })
145935
145939
  });
@@ -146023,8 +146027,7 @@ function buildOrchestratorTools(ctx, outputSchema) {
146023
146027
  PushTagsTool(ctx),
146024
146028
  DeleteBranchTool(ctx),
146025
146029
  CreatePullRequestTool(ctx),
146026
- UpdatePullRequestBodyTool(ctx),
146027
- UpdateLearningsTool(ctx)
146030
+ UpdatePullRequestBodyTool(ctx)
146028
146031
  ];
146029
146032
  }
146030
146033
  async function tryStartMcpServer(ctx, tools, port) {
@@ -146181,9 +146184,6 @@ Rules:
146181
146184
  - Do NOT include a changelog section \u2014 the key changes list serves this purpose
146182
146185
  - Focus on *intent*, not *what* \u2014 the diff already shows what changed
146183
146186
  - Get the file count and commit count from the checkout_pr metadata, not by counting manually`;
146184
- function learningsStep(t, n) {
146185
- return `${n}. **learnings** (only if high confidence): if you discovered something about repo setup, test commands, conventions, or patterns that you are confident is correct and would reliably help future runs, call \`${t("update_learnings")}\` to persist it. skip this step if you are unsure or the finding is speculative/one-off. format as a flat bullet list (\`- \` per line, one fact per bullet). merge with existing learnings from the prompt \u2014 pass the FULL merged list. deduplicate, and drop bullets that are clearly wrong or no longer relevant to the current codebase.`;
146186
- }
146187
146187
  function computeModes(agentId) {
146188
146188
  const t = (toolName) => formatMcpToolRef(agentId, toolName);
146189
146189
  return [
@@ -146239,8 +146239,6 @@ function computeModes(agentId) {
146239
146239
  - create a PR via \`${t("create_pull_request")}\`
146240
146240
  - call \`${t("report_progress")}\` with the PR link or the exact error if push/PR failed
146241
146241
 
146242
- ${learningsStep(t, 6)}
146243
-
146244
146242
  ### Notes
146245
146243
 
146246
146244
  For simple, well-defined tasks, skip the plan phase and go straight to build.`
@@ -146268,9 +146266,7 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
146268
146266
  - confirm a clean working tree, then push via \`${t("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*)
146269
146267
  - reply to each comment using \`${t("reply_to_review_comment")}\`
146270
146268
  - resolve addressed threads via \`${t("resolve_review_thread")}\`
146271
- - call \`${t("report_progress")}\` with a brief summary (or the exact push error if push failed)
146272
-
146273
- ${learningsStep(t, 6)}`
146269
+ - call \`${t("report_progress")}\` with a brief summary (or the exact push error if push failed)`
146274
146270
  },
146275
146271
  // Review and IncrementalReview use the multi-lens orchestrator pattern
146276
146272
  // (canonical source: .claude/commands/anneal.md). The orchestrator does
@@ -146441,9 +146437,7 @@ ${PR_SUMMARY_FORMAT}`
146441
146437
 
146442
146438
  2. Produce a structured, actionable plan with clear milestones.
146443
146439
 
146444
- 3. Call \`${t("report_progress")}\` with the plan.
146445
-
146446
- ${learningsStep(t, 4)}`
146440
+ 3. Call \`${t("report_progress")}\` with the plan.`
146447
146441
  },
146448
146442
  {
146449
146443
  name: "Fix",
@@ -146465,9 +146459,7 @@ ${learningsStep(t, 4)}`
146465
146459
 
146466
146460
  5. Finalize:
146467
146461
  - confirm a clean working tree, then push via \`${t("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*)
146468
- - call \`${t("report_progress")}\` with the diagnosis and fix summary (or the exact push error if push failed)
146469
-
146470
- ${learningsStep(t, 6)}`
146462
+ - call \`${t("report_progress")}\` with the diagnosis and fix summary (or the exact push error if push failed)`
146471
146463
  },
146472
146464
  {
146473
146465
  name: "ResolveConflicts",
@@ -146511,9 +146503,7 @@ ${learningsStep(t, 6)}`
146511
146503
  3. Finalize:
146512
146504
  - if code changes were made, push to a pull request (new or existing) using \`${t("push_branch")}\` and \`${t("create_pull_request")}\` as needed. \`git status\` must be clean before you finish (see *SYSTEM* Git rules if push fails).
146513
146505
  - call \`${t("report_progress")}\` once with results \u2014 include exact tool errors if push or PR creation failed
146514
- - if the task involved labeling, commenting, or other GitHub operations, perform those directly
146515
-
146516
- ${learningsStep(t, 4)}`
146506
+ - if the task involved labeling, commenting, or other GitHub operations, perform those directly`
146517
146507
  }
146518
146508
  ];
146519
146509
  }
@@ -146613,6 +146603,17 @@ async function installFromNpmTarball(params) {
146613
146603
  // utils/providerErrors.ts
146614
146604
  var statusKey = `\\b(?:status[_ ]?code|http[_ ]?status|status)["']?\\s*[:=]\\s*["']?`;
146615
146605
  var PROVIDER_ERROR_PATTERNS = [
146606
+ // auth patterns must come BEFORE rate-limit patterns. OpenRouter 401 error
146607
+ // payloads carry `x-ratelimit-*` response headers in the dump, and the
146608
+ // free-form rate-limit regex below would otherwise win on word-boundary
146609
+ // matches inside header names. canonical 401 messages: OpenRouter returns
146610
+ // `{"error":{"message":"User not found","code":401}}` for disabled or
146611
+ // invalid keys (https://openai.luzhipeng.com/docs/api/reference/errors-and-debugging).
146612
+ { regex: new RegExp(`${statusKey}401\\b`, "i"), label: "auth error (401)" },
146613
+ { regex: new RegExp(`${statusKey}403\\b`, "i"), label: "auth error (403)" },
146614
+ { regex: /\bUser not found\b/i, label: "auth error (invalid/disabled key)" },
146615
+ { regex: /\bInvalid authentication\b/i, label: "auth error (invalid credentials)" },
146616
+ { regex: /\bNo auth credentials found\b/i, label: "auth error (missing credentials)" },
146616
146617
  { regex: new RegExp(`${statusKey}429\\b`, "i"), label: "rate limited (429)" },
146617
146618
  { regex: new RegExp(`${statusKey}500\\b`, "i"), label: "provider 500 error" },
146618
146619
  { regex: new RegExp(`${statusKey}503\\b`, "i"), label: "provider unavailable (503)" },
@@ -146676,7 +146677,7 @@ function installBundledSkills(params) {
146676
146677
  writeFileSync6(join9(skillDir, "SKILL.md"), content);
146677
146678
  }
146678
146679
  }
146679
- log.info(`installed bundled skills: ${BUNDLED_SKILL_NAMES.join(", ")}`);
146680
+ log.success(`installed bundled skills: ${BUNDLED_SKILL_NAMES.join(", ")}`);
146680
146681
  }
146681
146682
  function addSkill(params) {
146682
146683
  const result = spawnSync5(
@@ -146701,7 +146702,7 @@ function addSkill(params) {
146701
146702
  }
146702
146703
  );
146703
146704
  if (result.status === 0) {
146704
- log.info(`installed ${params.skill} skill (${params.agent})`);
146705
+ log.success(`installed ${params.skill} skill (${params.agent})`);
146705
146706
  } else {
146706
146707
  const stderr = (result.stderr?.toString() || "").trim();
146707
146708
  const errorMsg = result.error ? result.error.message : stderr;
@@ -146835,18 +146836,17 @@ function buildPostRunPrompt(issues) {
146835
146836
  if (issues.summaryStale) parts.push(buildSummaryStalePrompt(issues.summaryStale.filePath));
146836
146837
  return parts.join("\n\n---\n\n");
146837
146838
  }
146838
- function buildLearningsReflectionPrompt(agentId) {
146839
- const t = (name) => formatMcpToolRef(agentId, name);
146839
+ function buildLearningsReflectionPrompt(filePath) {
146840
146840
  return [
146841
- `REFLECTION \u2014 before you finish, think back over this task: did you discover anything about this repo's setup, test commands, conventions, or patterns that you are confident is correct and would reliably help future runs?`,
146841
+ `REFLECTION \u2014 before you finish, think back over this task: did you discover anything about this repo's setup, test commands, conventions, or patterns that is high-confidence and would reliably help future runs?`,
146842
146842
  "",
146843
- `if so, call \`${t("update_learnings")}\` to persist it.`,
146843
+ `the rolling learnings file is at \`${filePath}\`. read it first if you haven't already, then edit it in place using your native file tools. the server reads this file at end-of-run and persists any changes \u2014 there is no tool to call.`,
146844
146844
  "",
146845
- `rules:`,
146846
- `- only call \`${t("update_learnings")}\` when the finding is high-confidence and broadly useful. skip if unsure, speculative, or one-off.`,
146847
- `- pass the FULL merged list: existing learnings from the original prompt + your new discoveries. one fact per bullet, lines starting with \`- \`.`,
146848
- `- deduplicate, and drop bullets that are clearly wrong or no longer relevant to the current codebase.`,
146849
- `- if you already called \`${t("update_learnings")}\` earlier in this run, or nothing new is worth capturing, just reply "done" and stop \u2014 do not edit the repo for this reflection.`
146845
+ `keep the file healthy:`,
146846
+ `- only add bullets when the finding is high-confidence AND broadly useful. skip speculative, one-off, or "maybe" findings.`,
146847
+ `- prune bullets that are clearly wrong, no longer relevant, or low-signal (rarely useful). a focused, accurate file beats a long stale one.`,
146848
+ `- format: flat bullet list, one fact per line starting with \`- \`. deduplicate against existing entries \u2014 if a bullet covers the same fact, update it in place instead of adding a duplicate.`,
146849
+ `- leave the file alone if you have nothing substantively new to add and the existing entries still look healthy. silence is a valid outcome \u2014 just reply "done" and stop.`
146850
146850
  ].join("\n");
146851
146851
  }
146852
146852
  async function runPostRunRetryLoop(params) {
@@ -147184,6 +147184,12 @@ async function runClaude(params) {
147184
147184
  activityTimeout: 3e5,
147185
147185
  onActivityTimeout: params.onActivityTimeout,
147186
147186
  stdio: ["ignore", "pipe", "pipe"],
147187
+ // run claude in its own process group so SIGKILL on activity timeout /
147188
+ // outer cancellation reaches any subprocesses it spawns (rg, file
147189
+ // watchers, mcp transports, etc). claude itself is a node bundle so
147190
+ // there's no shim-orphan issue like opencode-ai/bin/opencode, but
147191
+ // detached + killGroup is the right default for any agent runtime.
147192
+ killGroup: true,
147187
147193
  onStdout: async (chunk) => {
147188
147194
  const text = chunk.toString();
147189
147195
  output += text;
@@ -147423,7 +147429,7 @@ var claude = agent({
147423
147429
  stopScript: ctx.stopScript,
147424
147430
  summaryFilePath: ctx.summaryFilePath,
147425
147431
  summarySeed: ctx.summarySeed,
147426
- reflectionPrompt: buildLearningsReflectionPrompt("claude"),
147432
+ reflectionPrompt: ctx.learningsFilePath ? buildLearningsReflectionPrompt(ctx.learningsFilePath) : void 0,
147427
147433
  canResume: (r) => Boolean(r.sessionId),
147428
147434
  resume: async (c) => {
147429
147435
  const sessionId = c.previousResult.sessionId;
@@ -147439,9 +147445,92 @@ var claude = agent({
147439
147445
 
147440
147446
  // agents/opencode.ts
147441
147447
  import { execFileSync as execFileSync4 } from "node:child_process";
147442
- import { mkdirSync as mkdirSync5 } from "node:fs";
147448
+ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "node:fs";
147443
147449
  import { join as join11 } from "node:path";
147444
147450
  import { performance as performance7 } from "node:perf_hooks";
147451
+
147452
+ // agents/opencodePlugin.ts
147453
+ var PULLFROG_BUS_EVENT_TYPE = "pullfrog_bus_event";
147454
+ var PULLFROG_OPENCODE_PLUGIN_FILENAME = "pullfrog-events.ts";
147455
+ var PULLFROG_OPENCODE_PLUGIN_SOURCE = `// AUTOGENERATED by Pullfrog. do not edit; it'll be overwritten on the next run.
147456
+ // surfaces opencode subagent activity that the CLI's run-loop discards. see
147457
+ // action/agents/opencodePlugin.ts in pullfrog/app for why this exists. lives
147458
+ // inside the per-run tmpdir (XDG_CONFIG_HOME/opencode/plugin/), never inside
147459
+ // the user's working tree.
147460
+
147461
+ const PULLFROG_BUS_EVENT_TYPE = ${JSON.stringify(PULLFROG_BUS_EVENT_TYPE)};
147462
+
147463
+ // the first sessionID we see on a message.part.updated event is the
147464
+ // orchestrator \u2014 opencode's run command creates exactly one top-level session
147465
+ // before any subagent is dispatched, and the user-prompt text part fires
147466
+ // before the first task tool_use. we lock that sessionID in here and use it
147467
+ // to filter: the orchestrator's events are already streamed by the CLI's
147468
+ // run-loop, so we only forward (a) all subagent events, and (b) the
147469
+ // orchestrator's task tool dispatches at status="running". the CLI only
147470
+ // emits task tool_use at status=completed (after the subagent finishes), so
147471
+ // without the early announce the parent's labeler binds subagent sessions
147472
+ // before recordTaskDispatch fires and the lens label is lost.
147473
+ let orchestratorSessionID: string | undefined;
147474
+
147475
+ function isOrchestratorTaskDispatch(part: {
147476
+ type?: string;
147477
+ tool?: string;
147478
+ state?: { status?: string };
147479
+ }): boolean {
147480
+ if (part.type !== "tool") return false;
147481
+ if (part.tool !== "task") return false;
147482
+ // only forward at status="running" (not "pending"). at pending the
147483
+ // state.input is still {} \u2014 the orchestrator has emitted the part shell
147484
+ // but the LLM hasn't filled in description/subagent_type/prompt yet. by
147485
+ // running, input is populated and recordTaskDispatch can derive the lens
147486
+ // label correctly.
147487
+ return part.state?.status === "running";
147488
+ }
147489
+
147490
+ export default async function pullfrogEventsPlugin() {
147491
+ return {
147492
+ event: async (input: {
147493
+ event: {
147494
+ type: string;
147495
+ properties?: {
147496
+ part?: {
147497
+ sessionID?: string;
147498
+ type?: string;
147499
+ tool?: string;
147500
+ state?: { status?: string };
147501
+ };
147502
+ };
147503
+ };
147504
+ }) => {
147505
+ const event = input.event;
147506
+ if (!event || typeof event !== "object") return;
147507
+ if (event.type !== "message.part.updated") return;
147508
+ const part = event.properties?.part;
147509
+ const sessionID = part?.sessionID;
147510
+ if (typeof sessionID !== "string" || sessionID.length === 0) return;
147511
+ if (orchestratorSessionID === undefined) orchestratorSessionID = sessionID;
147512
+
147513
+ if (sessionID === orchestratorSessionID) {
147514
+ // skip orchestrator events EXCEPT early task dispatches.
147515
+ if (!part || !isOrchestratorTaskDispatch(part)) return;
147516
+ }
147517
+
147518
+ try {
147519
+ const line = JSON.stringify({
147520
+ type: PULLFROG_BUS_EVENT_TYPE,
147521
+ bus_event: event,
147522
+ });
147523
+ process.stdout.write(line + "\\n");
147524
+ } catch {
147525
+ // a circular reference or BigInt etc. would throw; swallow rather
147526
+ // than letting a single bad event take down the plugin.
147527
+ }
147528
+ },
147529
+ };
147530
+ }
147531
+ `;
147532
+
147533
+ // agents/opencode.ts
147445
147534
  async function installOpencodeCli() {
147446
147535
  return await installFromNpmTarball({
147447
147536
  packageName: "opencode-ai",
@@ -147661,18 +147750,20 @@ async function runOpenCode(params) {
147661
147750
  return;
147662
147751
  }
147663
147752
  if (toolName === "task") {
147664
- const taskInput = event.part?.state?.input ?? {};
147665
- const dispatchedLabel = labeler.recordTaskDispatch(taskInput);
147666
- const dispatch = {
147667
- label: dispatchedLabel,
147668
- startedAt: performance7.now(),
147669
- toolUseCallID: toolId
147670
- };
147671
- taskDispatchByCallID.set(toolId, dispatch);
147672
- pendingTaskDispatches.push(dispatch);
147673
- log.info(
147674
- `\xBB dispatching subagent: ${dispatchedLabel}` + (taskInput.subagent_type ? ` (subagent_type=${taskInput.subagent_type})` : "")
147675
- );
147753
+ if (!taskDispatchByCallID.has(toolId)) {
147754
+ const taskInput = event.part?.state?.input ?? {};
147755
+ const dispatchedLabel = labeler.recordTaskDispatch(taskInput);
147756
+ const dispatch = {
147757
+ label: dispatchedLabel,
147758
+ startedAt: performance7.now(),
147759
+ toolUseCallID: toolId
147760
+ };
147761
+ taskDispatchByCallID.set(toolId, dispatch);
147762
+ pendingTaskDispatches.push(dispatch);
147763
+ log.info(
147764
+ `\xBB dispatching subagent: ${dispatchedLabel}` + (taskInput.subagent_type ? ` (subagent_type=${taskInput.subagent_type})` : "")
147765
+ );
147766
+ }
147676
147767
  } else {
147677
147768
  knownNonTaskCallIDs.add(toolId);
147678
147769
  }
@@ -147693,6 +147784,10 @@ async function runOpenCode(params) {
147693
147784
  if (event.part?.state?.status === "completed" && event.part.state.output) {
147694
147785
  log.debug(withLabel(label, ` output: ${event.part.state.output}`));
147695
147786
  }
147787
+ if (event.part?.state?.status === "error") {
147788
+ const errorMsg = event.part.state.output ?? "(no error message)";
147789
+ log.info(withLabel(label, `\xBB tool call failed: ${errorMsg}`));
147790
+ }
147696
147791
  if (toolName.includes("report_progress") && params.todoTracker) {
147697
147792
  log.debug("\xBB report_progress detected, disabling todo tracking");
147698
147793
  params.todoTracker.cancel();
@@ -147779,6 +147874,53 @@ async function runOpenCode(params) {
147779
147874
  tokensLogged = true;
147780
147875
  }
147781
147876
  }
147877
+ },
147878
+ [PULLFROG_BUS_EVENT_TYPE]: async (event) => {
147879
+ const busEvent = event.bus_event;
147880
+ if (!busEvent || busEvent.type !== "message.part.updated") return;
147881
+ const part = busEvent.properties?.part;
147882
+ if (!part || typeof part.sessionID !== "string") return;
147883
+ const sessionID = part.sessionID;
147884
+ const partType = part.type;
147885
+ if (partType === "tool") {
147886
+ const status = part.state?.status;
147887
+ const partWithToolFields = part;
147888
+ const isOrchestratorTaskDispatch = partWithToolFields.tool === "task" && status === "running";
147889
+ if (isOrchestratorTaskDispatch) {
147890
+ const callID = partWithToolFields.callID;
147891
+ if (typeof callID === "string" && !taskDispatchByCallID.has(callID)) {
147892
+ const taskInput = partWithToolFields.state?.input ?? {};
147893
+ const dispatchedLabel = labeler.recordTaskDispatch(taskInput);
147894
+ const dispatch = {
147895
+ label: dispatchedLabel,
147896
+ startedAt: performance7.now(),
147897
+ toolUseCallID: callID
147898
+ };
147899
+ taskDispatchByCallID.set(callID, dispatch);
147900
+ pendingTaskDispatches.push(dispatch);
147901
+ log.info(
147902
+ `\xBB dispatching subagent: ${dispatchedLabel}` + (taskInput.subagent_type ? ` (subagent_type=${taskInput.subagent_type})` : "")
147903
+ );
147904
+ }
147905
+ return;
147906
+ }
147907
+ if (status !== "completed" && status !== "error") return;
147908
+ await handlers2.tool_use({
147909
+ type: "tool_use",
147910
+ sessionID,
147911
+ part
147912
+ });
147913
+ return;
147914
+ }
147915
+ if (partType === "step-start" || partType === "step-finish") return;
147916
+ if (partType === "text" && part.time?.end !== void 0) {
147917
+ await handlers2.text({
147918
+ type: "text",
147919
+ sessionID,
147920
+ part
147921
+ });
147922
+ return;
147923
+ }
147782
147924
  }
147783
147925
  };
147784
147926
  const recentStderr = [];
@@ -147795,6 +147937,20 @@ async function runOpenCode(params) {
147795
147937
  activityTimeout: 3e5,
147796
147938
  onActivityTimeout: params.onActivityTimeout,
147797
147939
  stdio: ["ignore", "pipe", "pipe"],
147940
+ // node_modules/opencode-ai/bin/opencode is a Node shim that spawnSyncs
147941
+ // the native opencode-<plat>-<arch> binary with stdio:"inherit". without
147942
+ // a process-group kill, SIGKILL hits only the shim, the native binary
147943
+ // is reparented to PID 1, holds our stdout pipe open, and `child.close`
147944
+ // never fires — producing zombie runs. detached + killGroup nukes the
147945
+ // whole tree.
147946
+ killGroup: true,
147947
+ // NB: we used to pass `isPausedExternally: isSubagentInFlight` to suspend
147948
+ // the activity timer during subagent dispatches. unnecessary now that
147949
+ // our injected plugin (action/agents/opencodePlugin.ts) re-emits
147950
+ // subagent `message.part.updated` events on opencode's stdout — those
147951
+ // arrive at child.stdout here, fire updateActivity(), and reset
147952
+ // lastActivityTime naturally. verified empirically in PR #634
147953
+ // (~3.3 plugin events/sec during a typical subagent run).
147798
147954
  onStdout: async (chunk) => {
147799
147955
  const text = chunk.toString();
147800
147956
  output += text;
@@ -147949,6 +148105,12 @@ var opencode = agent({
147949
148105
  XDG_CONFIG_HOME: join11(ctx.tmpdir, ".config")
147950
148106
  };
147951
148107
  mkdirSync5(join11(homeEnv.XDG_CONFIG_HOME, "opencode"), { recursive: true });
148108
+ const opencodePluginDir = join11(homeEnv.XDG_CONFIG_HOME, "opencode", "plugin");
148109
+ mkdirSync5(opencodePluginDir, { recursive: true });
148110
+ writeFileSync8(
148111
+ join11(opencodePluginDir, PULLFROG_OPENCODE_PLUGIN_FILENAME),
148112
+ PULLFROG_OPENCODE_PLUGIN_SOURCE
148113
+ );
147952
148114
  const agentBrowserVersion = getDevDependencyVersion("agent-browser");
147953
148115
  addSkill({
147954
148116
  ref: `vercel-labs/agent-browser@v${agentBrowserVersion}`,
@@ -147991,7 +148153,7 @@ var opencode = agent({
147991
148153
  stopScript: ctx.stopScript,
147992
148154
  summaryFilePath: ctx.summaryFilePath,
147993
148155
  summarySeed: ctx.summarySeed,
147994
- reflectionPrompt: buildLearningsReflectionPrompt("opencode"),
148156
+ reflectionPrompt: ctx.learningsFilePath ? buildLearningsReflectionPrompt(ctx.learningsFilePath) : void 0,
147995
148157
  resume: async (c) => runOpenCode({
147996
148158
  ...runParams,
147997
148159
  args: [...baseArgs, "--continue", c.prompt]
@@ -152204,7 +152366,7 @@ ${ctx.error}` : ctx.error;
152204
152366
 
152205
152367
  // utils/gitAuthServer.ts
152206
152368
  import { randomUUID as randomUUID3 } from "node:crypto";
152207
- import { writeFileSync as writeFileSync8 } from "node:fs";
152369
+ import { writeFileSync as writeFileSync9 } from "node:fs";
152208
152370
  import { createServer as createServer2 } from "node:http";
152209
152371
  import { join as join13 } from "node:path";
152210
152372
  var CODE_TTL_MS = 5 * 60 * 1e3;
@@ -152293,7 +152455,7 @@ async function startGitAuthServer(tmpdir3) {
152293
152455
  `try{require("fs").unlinkSync("${scriptPath.replace(/\\/g, "\\\\")}")}catch(e){}`,
152294
152456
  `})}).on("error",function(){process.exit(1)})}`
152295
152457
  ].join("\n");
152296
- writeFileSync8(scriptPath, content, { mode: 448 });
152458
+ writeFileSync9(scriptPath, content, { mode: 448 });
152297
152459
  return scriptPath;
152298
152460
  }
152299
152461
  async function close() {
@@ -152567,9 +152729,9 @@ function buildPromptContext(ctx) {
152567
152729
  };
152568
152730
  }
152569
152731
  function assembleFullPrompt(ctx) {
152570
- const learningsSection = ctx.learnings ? `************* LEARNINGS *************
152732
+ const learningsSection = ctx.learningsFilePath ? `************* LEARNINGS *************
152571
152733
 
152572
- ${ctx.learnings}` : "";
152734
+ Repo-level learnings accumulated by previous agent runs live at \`${ctx.learningsFilePath}\`. Read this file early and let the entries inform your approach (test commands, conventions, gotchas, etc.). The file may be empty if no learnings have been collected yet.` : "";
152573
152735
  const runtimeSection = `************* RUNTIME *************
152574
152736
 
152575
152737
  ${ctx.runtime}`;
@@ -152596,8 +152758,8 @@ function resolveInstructions(ctx) {
152596
152758
  if (eventContext)
152597
152759
  tocEntries.push({ label: "EVENT CONTEXT", description: "related PR/issue data" });
152598
152760
  tocEntries.push({ label: "SYSTEM", description: "persona, security, tools, workflow rules" });
152599
- if (pctx.learnings)
152600
- tocEntries.push({ label: "LEARNINGS", description: "repo-specific knowledge" });
152761
+ if (pctx.learningsFilePath)
152762
+ tocEntries.push({ label: "LEARNINGS", description: "repo-specific knowledge file path" });
152601
152763
  tocEntries.push({ label: "RUNTIME", description: "environment metadata" });
152602
152764
  const toc = buildToc(tocEntries);
152603
152765
  const full = assembleFullPrompt({
@@ -152606,7 +152768,7 @@ function resolveInstructions(ctx) {
152606
152768
  procedure,
152607
152769
  eventContext,
152608
152770
  system,
152609
- learnings: pctx.learnings,
152771
+ learningsFilePath: pctx.learningsFilePath,
152610
152772
  runtime: pctx.runtime
152611
152773
  });
152612
152774
  const event = [pctx.eventTitle, pctx.eventMetadata].filter(Boolean).join("\n\n---\n\n");
@@ -152620,6 +152782,32 @@ function resolveInstructions(ctx) {
152620
152782
  };
152621
152783
  }
152622
152784
 
152785
+ // utils/learnings.ts
152786
+ import { mkdir, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
152787
+ import { dirname as dirname4, join as join14 } from "node:path";
152788
+ var LEARNINGS_FILE_NAME = "pullfrog-learnings.md";
152789
+ var MAX_LEARNINGS_LENGTH = 1e4;
152790
+ function learningsFilePath(tmpdir3) {
152791
+ return join14(tmpdir3, LEARNINGS_FILE_NAME);
152792
+ }
152793
+ async function seedLearningsFile(params) {
152794
+ const path3 = learningsFilePath(params.tmpdir);
152795
+ await mkdir(dirname4(path3), { recursive: true });
152796
+ await writeFile2(path3, params.current ?? "", "utf8");
152797
+ return path3;
152798
+ }
152799
+ async function readLearningsFile(path3) {
152800
+ let raw2;
152801
+ try {
152802
+ raw2 = await readFile2(path3, "utf8");
152803
+ } catch {
152804
+ return null;
152805
+ }
152806
+ const trimmed = raw2.trim();
152807
+ if (trimmed.length > MAX_LEARNINGS_LENGTH) return trimmed.slice(0, MAX_LEARNINGS_LENGTH);
152808
+ return trimmed;
152809
+ }
152810
+
152623
152811
  // utils/normalizeEnv.ts
152624
152812
  function maskValue(value2) {
152625
152813
  if (value2 && typeof value2 === "string" && value2.trim().length > 0) {
@@ -152795,8 +152983,8 @@ function resolvePayload(resolvedPromptInput, repoSettings) {
152795
152983
  }
152796
152984
 
152797
152985
  // utils/prSummary.ts
152798
- import { mkdir, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
152799
- import { dirname as dirname4, join as join14 } from "node:path";
152986
+ import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
152987
+ import { dirname as dirname5, join as join15 } from "node:path";
152800
152988
  var SUMMARY_FILE_NAME = "pullfrog-summary.md";
152801
152989
  var SUMMARY_SCAFFOLD = `# PR summary
152802
152990
 
@@ -152806,19 +152994,19 @@ var SUMMARY_SCAFFOLD = `# PR summary
152806
152994
  var MIN_SNAPSHOT_LENGTH = 60;
152807
152995
  var MAX_SNAPSHOT_LENGTH = 32768;
152808
152996
  function summaryFilePath(tmpdir3) {
152809
- return join14(tmpdir3, SUMMARY_FILE_NAME);
152997
+ return join15(tmpdir3, SUMMARY_FILE_NAME);
152810
152998
  }
152811
152999
  async function seedSummaryFile(params) {
152812
153000
  const path3 = summaryFilePath(params.tmpdir);
152813
- await mkdir(dirname4(path3), { recursive: true });
153001
+ await mkdir2(dirname5(path3), { recursive: true });
152814
153002
  const seed = params.previousSnapshot && params.previousSnapshot.trim().length >= MIN_SNAPSHOT_LENGTH ? params.previousSnapshot : SUMMARY_SCAFFOLD;
152815
- await writeFile2(path3, seed, "utf8");
153003
+ await writeFile3(path3, seed, "utf8");
152816
153004
  return path3;
152817
153005
  }
152818
153006
  async function readSummaryFile(path3) {
152819
153007
  let raw2;
152820
153008
  try {
152821
- raw2 = await readFile2(path3, "utf8");
153009
+ raw2 = await readFile3(path3, "utf8");
152822
153010
  } catch {
152823
153011
  return null;
152824
153012
  }
@@ -153036,9 +153224,9 @@ async function resolveRunContextData(params) {
153036
153224
  import { execFileSync as execFileSync5, execSync as execSync3 } from "node:child_process";
153037
153225
  import { mkdtempSync } from "node:fs";
153038
153226
  import { tmpdir as tmpdir2 } from "node:os";
153039
- import { join as join15 } from "node:path";
153227
+ import { join as join16 } from "node:path";
153040
153228
  function createTempDirectory() {
153041
- const sharedTempDir = mkdtempSync(join15(tmpdir2(), "pullfrog-"));
153229
+ const sharedTempDir = mkdtempSync(join16(tmpdir2(), "pullfrog-"));
153042
153230
  process.env.PULLFROG_TEMP_DIR = sharedTempDir;
153043
153231
  log.info(`\xBB created temp dir at ${sharedTempDir}`);
153044
153232
  return sharedTempDir;
@@ -153440,15 +153628,12 @@ function formatTransientErrorSummary(error49, owner) {
153440
153628
  }
153441
153629
  async function mintProxyKey(ctx) {
153442
153630
  try {
153443
- process.env.ACTIONS_ID_TOKEN_REQUEST_URL = ctx.oidcCredentials.requestUrl;
153444
- process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = ctx.oidcCredentials.requestToken;
153445
- const oidcToken = await core6.getIDToken("pullfrog-api");
153446
- delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
153447
- delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
153631
+ const headers = await buildProxyTokenHeaders(ctx);
153632
+ if (!headers) return null;
153448
153633
  const response = await apiFetch({
153449
153634
  path: "/api/proxy-token",
153450
153635
  method: "POST",
153451
- headers: { Authorization: `Bearer ${oidcToken}` }
153636
+ headers
153452
153637
  });
153453
153638
  if (response.status === 402) {
153454
153639
  const body = await response.json().catch(() => null);
@@ -153480,15 +153665,30 @@ async function mintProxyKey(ctx) {
153480
153665
  delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
153481
153666
  }
153482
153667
  }
153668
+ async function buildProxyTokenHeaders(ctx) {
153669
+ if (ctx.oidcCredentials) {
153670
+ process.env.ACTIONS_ID_TOKEN_REQUEST_URL = ctx.oidcCredentials.requestUrl;
153671
+ process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = ctx.oidcCredentials.requestToken;
153672
+ const oidcToken = await core6.getIDToken("pullfrog-api");
153673
+ delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
153674
+ delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
153675
+ return { Authorization: `Bearer ${oidcToken}` };
153676
+ }
153677
+ if (isLocalApiUrl()) {
153678
+ log.info(`\xBB proxy: dev bypass (x-dev-repo) for ${ctx.repo.owner}/${ctx.repo.name}`);
153679
+ return { "x-dev-repo": `${ctx.repo.owner}/${ctx.repo.name}` };
153680
+ }
153681
+ return null;
153682
+ }
153483
153683
  async function resolveProxyModel(ctx) {
153484
153684
  if (process.env.PULLFROG_MODEL?.trim()) return;
153485
153685
  const needsProxy = isInfraCovered({ isOss: ctx.oss, plan: ctx.plan }) && ctx.proxyModel;
153486
153686
  if (!needsProxy) return;
153487
- if (!ctx.oidcCredentials) {
153687
+ if (!ctx.oidcCredentials && !isLocalApiUrl()) {
153488
153688
  log.warning("\xBB proxy requested but no OIDC credentials available \u2014 skipping");
153489
153689
  return;
153490
153690
  }
153491
- const key = await mintProxyKey({ oidcCredentials: ctx.oidcCredentials });
153691
+ const key = await mintProxyKey({ oidcCredentials: ctx.oidcCredentials, repo: ctx.repo });
153492
153692
  if (!key) return;
153493
153693
  process.env.OPENROUTER_API_KEY = key;
153494
153694
  core6.setSecret(key);
@@ -153512,6 +153712,45 @@ async function fetchPreviousSnapshot(ctx, prNumber) {
153512
153712
  return null;
153513
153713
  }
153514
153714
  }
153715
+ async function persistLearnings(ctx) {
153716
+ const filePath = ctx.toolState.learningsFilePath;
153717
+ if (!filePath) return;
153718
+ if (ctx.toolState.learningsPersistAttempted) return;
153719
+ ctx.toolState.learningsPersistAttempted = true;
153720
+ const current = await readLearningsFile(filePath);
153721
+ if (current === null) {
153722
+ log.debug(`learnings tmpfile missing or unreadable at ${filePath} \u2014 skipping persist`);
153723
+ return;
153724
+ }
153725
+ const seed = ctx.toolState.learningsSeed?.trim() ?? "";
153726
+ if (current === seed) {
153727
+ log.debug("learnings tmpfile unchanged from seed \u2014 skipping persist");
153728
+ return;
153729
+ }
153730
+ try {
153731
+ const response = await apiFetch({
153732
+ path: `/api/repo/${ctx.repo.owner}/${ctx.repo.name}/learnings`,
153733
+ method: "PATCH",
153734
+ headers: {
153735
+ authorization: `Bearer ${ctx.apiToken}`,
153736
+ "content-type": "application/json"
153737
+ },
153738
+ body: JSON.stringify({
153739
+ learnings: current,
153740
+ model: ctx.toolState.model
153741
+ }),
153742
+ signal: AbortSignal.timeout(1e4)
153743
+ });
153744
+ if (!response.ok) {
153745
+ const error49 = await response.text().catch(() => "(no body)");
153746
+ log.debug(`learnings persist failed (${response.status}): ${error49}`);
153747
+ return;
153748
+ }
153749
+ log.info("\xBB learnings updated");
153750
+ } catch (err) {
153751
+ log.debug(`learnings persist failed: ${err instanceof Error ? err.message : String(err)}`);
153752
+ }
153753
+ }
153515
153754
  async function persistSummary(ctx) {
153516
153755
  const filePath = ctx.toolState.summaryFilePath;
153517
153756
  if (!filePath) return;
@@ -153593,7 +153832,8 @@ async function main() {
153593
153832
  oss: runContext.oss,
153594
153833
  plan: runContext.plan,
153595
153834
  proxyModel: runContext.proxyModel,
153596
- oidcCredentials
153835
+ oidcCredentials,
153836
+ repo: runContext.repo
153597
153837
  });
153598
153838
  } catch (error49) {
153599
153839
  if (error49 instanceof BillingError) {
@@ -153696,12 +153936,32 @@ async function main() {
153696
153936
  toolContext.mcpServerUrl = mcpHttpServer.url;
153697
153937
  log.info(`\xBB MCP server started at ${mcpHttpServer.url}`);
153698
153938
  timer.checkpoint("mcpServer");
153939
+ try {
153940
+ const learningsPath = await seedLearningsFile({
153941
+ tmpdir: tmpdir3,
153942
+ current: runContext.repoSettings.learnings
153943
+ });
153944
+ toolState.learningsFilePath = learningsPath;
153945
+ try {
153946
+ toolState.learningsSeed = await readFile4(learningsPath, "utf8");
153947
+ } catch {
153948
+ }
153949
+ log.info(
153950
+ `\xBB learnings seeded at ${learningsPath} (existing=${runContext.repoSettings.learnings ? "yes" : "no"})`
153951
+ );
153952
+ const ctxForExit = toolContext;
153953
+ onExitSignal(() => persistLearnings(ctxForExit));
153954
+ } catch (err) {
153955
+ log.warning(
153956
+ `\xBB learnings seed failed: ${err instanceof Error ? err.message : String(err)} \u2014 continuing without learnings file`
153957
+ );
153958
+ }
153699
153959
  if (payload.generateSummary && payload.event.is_pr && payload.event.issue_number) {
153700
153960
  const previousSnapshot = await fetchPreviousSnapshot(toolContext, payload.event.issue_number);
153701
153961
  const filePath = await seedSummaryFile({ tmpdir: tmpdir3, previousSnapshot });
153702
153962
  toolState.summaryFilePath = filePath;
153703
153963
  try {
153704
- toolState.summarySeed = await readFile3(filePath, "utf8");
153964
+ toolState.summarySeed = await readFile4(filePath, "utf8");
153705
153965
  } catch {
153706
153966
  }
153707
153967
  log.info(
@@ -153725,7 +153985,7 @@ async function main() {
153725
153985
  modes: modes2,
153726
153986
  agentId,
153727
153987
  outputSchema,
153728
- learnings: runContext.repoSettings.learnings
153988
+ learningsFilePath: toolState.learningsFilePath ?? null
153729
153989
  });
153730
153990
  const logParts = [
153731
153991
  instructions.eventInstructions ? `EVENT-LEVEL INSTRUCTIONS:
@@ -153741,7 +154001,7 @@ ${instructions.user}` : null,
153741
154001
  log.info(instructions.full);
153742
154002
  });
153743
154003
  if (agentId === "opencode") {
153744
- const pluginDir = join16(process.cwd(), ".opencode", "plugin");
154004
+ const pluginDir = join17(process.cwd(), ".opencode", "plugin");
153745
154005
  const hasPlugins = existsSync7(pluginDir) && readdirSync(pluginDir).some((f) => /\.[jt]sx?$/.test(f));
153746
154006
  if (hasPlugins && toolState.dependencyInstallation?.promise) {
153747
154007
  log.info(
@@ -153802,6 +154062,7 @@ ${instructions.user}` : null,
153802
154062
  stopScript: runContext.repoSettings.stopScript,
153803
154063
  summaryFilePath: toolState.summaryFilePath,
153804
154064
  summarySeed: toolState.summarySeed,
154065
+ learningsFilePath: toolState.learningsFilePath,
153805
154066
  onActivityTimeout: onInnerActivityTimeout,
153806
154067
  onToolUse: (event) => {
153807
154068
  const wasTracked = recordDiffReadFromToolUse({
@@ -153859,6 +154120,9 @@ ${instructions.user}` : null,
153859
154120
  if (toolContext) {
153860
154121
  await persistSummary(toolContext);
153861
154122
  }
154123
+ if (toolContext) {
154124
+ await persistLearnings(toolContext);
154125
+ }
153862
154126
  if (toolContext && toolState.progressComment && !toolState.finalSummaryWritten) {
153863
154127
  await deleteProgressComment(toolContext).catch((error49) => {
153864
154128
  log.debug(`stranded progress comment cleanup failed: ${error49}`);
@@ -153911,6 +154175,9 @@ ${errorMessage}
153911
154175
  if (toolContext) {
153912
154176
  await persistSummary(toolContext);
153913
154177
  }
154178
+ if (toolContext) {
154179
+ await persistLearnings(toolContext);
154180
+ }
153914
154181
  return {
153915
154182
  success: false,
153916
154183
  error: errorMessage