pullfrog 0.1.1 → 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,
@@ -109741,11 +109755,6 @@ async function spawn(options) {
109741
109755
  `spawn activity timer: pid=${child.pid} cmd=${options.cmd} timeout=${activityTimeoutMs}ms`
109742
109756
  );
109743
109757
  activityCheckIntervalId = setInterval(() => {
109744
- if (options.isPausedExternally?.()) {
109745
- lastActivityTime = performance3.now();
109746
- log.debug(`spawn activity check: pid=${child.pid} paused externally`);
109747
- return;
109748
- }
109749
109758
  const idleMs = performance3.now() - lastActivityTime;
109750
109759
  log.debug(
109751
109760
  `spawn activity check: pid=${child.pid} idle=${Math.round(idleMs)}ms / ${activityTimeoutMs}ms`
@@ -142266,7 +142275,7 @@ var import_semver = __toESM(require_semver2(), 1);
142266
142275
  // package.json
142267
142276
  var package_default = {
142268
142277
  name: "pullfrog",
142269
- version: "0.1.1",
142278
+ version: "0.1.2",
142270
142279
  type: "module",
142271
142280
  bin: {
142272
142281
  pullfrog: "dist/cli.mjs",
@@ -143210,6 +143219,10 @@ ${integrateStep}
143210
143219
  if (!pushed) {
143211
143220
  throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
143212
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
+ );
143213
143226
  return {
143214
143227
  success: true,
143215
143228
  branch,
@@ -143358,6 +143371,7 @@ function DeleteBranchTool(ctx) {
143358
143371
  await $git("push", ["origin", "--delete", `refs/heads/${params.branchName}`], {
143359
143372
  token: ctx.gitToken
143360
143373
  });
143374
+ log.info(`\xBB deleted branch ${params.branchName}`);
143361
143375
  return { success: true, deleted: params.branchName };
143362
143376
  })
143363
143377
  });
@@ -143383,6 +143397,7 @@ function PushTagsTool(ctx) {
143383
143397
  await $git("push", pushArgs, {
143384
143398
  token: ctx.gitToken
143385
143399
  });
143400
+ log.info(`\xBB pushed tag ${params.tag}`);
143386
143401
  return { success: true, tag: params.tag };
143387
143402
  })
143388
143403
  });
@@ -143707,6 +143722,7 @@ function CreatePullRequestReviewTool(ctx) {
143707
143722
  }
143708
143723
  const reviewId = result.data.id;
143709
143724
  const reviewNodeId = result.data.node_id;
143725
+ log.info(`\xBB created review ${reviewId} on pull request #${pull_number}`);
143710
143726
  const actuallyReviewedSha = ctx.toolState.checkoutSha ?? params.commit_id;
143711
143727
  ctx.toolState.review = {
143712
143728
  id: reviewId,
@@ -144571,6 +144587,7 @@ function IssueTool(ctx) {
144571
144587
  labels: params.labels ?? [],
144572
144588
  assignees: params.assignees ?? []
144573
144589
  });
144590
+ log.info(`\xBB created issue #${result.data.number} (id ${result.data.id})`);
144574
144591
  const nodeId = result.data.node_id;
144575
144592
  if (typeof nodeId === "string" && nodeId.length > 0) {
144576
144593
  await patchWorkflowRunFields(ctx, {
@@ -144762,6 +144779,7 @@ function AddLabelsTool(ctx) {
144762
144779
  issue_number,
144763
144780
  labels
144764
144781
  });
144782
+ log.info(`\xBB added labels [${labels.join(", ")}] to issue #${issue_number}`);
144765
144783
  return {
144766
144784
  success: true,
144767
144785
  labels: result.data.map((label) => label.name)
@@ -144770,40 +144788,6 @@ function AddLabelsTool(ctx) {
144770
144788
  });
144771
144789
  }
144772
144790
 
144773
- // mcp/learnings.ts
144774
- var UpdateLearningsParams = type({
144775
- learnings: type.string.describe(
144776
- "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."
144777
- )
144778
- });
144779
- function UpdateLearningsTool(ctx) {
144780
- return tool({
144781
- name: "update_learnings",
144782
- 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.",
144783
- parameters: UpdateLearningsParams,
144784
- execute: execute(async (params) => {
144785
- const response = await apiFetch({
144786
- path: `/api/repo/${ctx.repo.owner}/${ctx.repo.name}/learnings`,
144787
- method: "PATCH",
144788
- headers: {
144789
- authorization: `Bearer ${ctx.apiToken}`,
144790
- "content-type": "application/json"
144791
- },
144792
- body: JSON.stringify({
144793
- learnings: params.learnings,
144794
- model: ctx.toolState.model
144795
- }),
144796
- signal: AbortSignal.timeout(1e4)
144797
- });
144798
- if (!response.ok) {
144799
- const error49 = await response.text();
144800
- throw new Error(`failed to update learnings: ${error49}`);
144801
- }
144802
- return { success: true };
144803
- })
144804
- });
144805
- }
144806
-
144807
144791
  // mcp/output.ts
144808
144792
  var import_ajv3 = __toESM(require_ajv(), 1);
144809
144793
  var SetOutputParams = type({
@@ -144897,6 +144881,7 @@ function UpdatePullRequestBodyTool(ctx) {
144897
144881
  pull_number: params.pull_number,
144898
144882
  body: bodyWithFooter
144899
144883
  });
144884
+ log.info(`\xBB updated pull request #${result.data.number}`);
144900
144885
  ctx.toolState.wasUpdated = true;
144901
144886
  return {
144902
144887
  success: true,
@@ -144924,6 +144909,7 @@ function CreatePullRequestTool(ctx) {
144924
144909
  base: params.base,
144925
144910
  draft: params.draft ?? false
144926
144911
  });
144912
+ log.info(`\xBB created pull request #${result.data.number} (id ${result.data.id})`);
144927
144913
  const reviewer = ctx.payload.triggerer;
144928
144914
  if (reviewer) {
144929
144915
  try {
@@ -145475,7 +145461,7 @@ function ResolveReviewThreadTool(ctx) {
145475
145461
  threadId: params.thread_id
145476
145462
  });
145477
145463
  const thread = response.resolveReviewThread.thread;
145478
- log.debug(`resolved thread ${thread.id}, isResolved=${thread.isResolved}`);
145464
+ log.info(`\xBB resolved review thread ${thread.id}`);
145479
145465
  return {
145480
145466
  thread_id: thread.id,
145481
145467
  is_resolved: thread.isResolved,
@@ -145947,6 +145933,7 @@ function UploadFileTool(ctx) {
145947
145933
  if (!uploadResponse.ok) {
145948
145934
  throw new Error(`failed to upload file: ${uploadResponse.statusText}`);
145949
145935
  }
145936
+ log.info(`\xBB uploaded file ${publicUrl}`);
145950
145937
  return { success: true, publicUrl, filename, contentLength, contentType };
145951
145938
  })
145952
145939
  });
@@ -146040,8 +146027,7 @@ function buildOrchestratorTools(ctx, outputSchema) {
146040
146027
  PushTagsTool(ctx),
146041
146028
  DeleteBranchTool(ctx),
146042
146029
  CreatePullRequestTool(ctx),
146043
- UpdatePullRequestBodyTool(ctx),
146044
- UpdateLearningsTool(ctx)
146030
+ UpdatePullRequestBodyTool(ctx)
146045
146031
  ];
146046
146032
  }
146047
146033
  async function tryStartMcpServer(ctx, tools, port) {
@@ -146198,9 +146184,6 @@ Rules:
146198
146184
  - Do NOT include a changelog section \u2014 the key changes list serves this purpose
146199
146185
  - Focus on *intent*, not *what* \u2014 the diff already shows what changed
146200
146186
  - Get the file count and commit count from the checkout_pr metadata, not by counting manually`;
146201
- function learningsStep(t, n) {
146202
- 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.`;
146203
- }
146204
146187
  function computeModes(agentId) {
146205
146188
  const t = (toolName) => formatMcpToolRef(agentId, toolName);
146206
146189
  return [
@@ -146256,8 +146239,6 @@ function computeModes(agentId) {
146256
146239
  - create a PR via \`${t("create_pull_request")}\`
146257
146240
  - call \`${t("report_progress")}\` with the PR link or the exact error if push/PR failed
146258
146241
 
146259
- ${learningsStep(t, 6)}
146260
-
146261
146242
  ### Notes
146262
146243
 
146263
146244
  For simple, well-defined tasks, skip the plan phase and go straight to build.`
@@ -146285,9 +146266,7 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
146285
146266
  - confirm a clean working tree, then push via \`${t("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*)
146286
146267
  - reply to each comment using \`${t("reply_to_review_comment")}\`
146287
146268
  - resolve addressed threads via \`${t("resolve_review_thread")}\`
146288
- - call \`${t("report_progress")}\` with a brief summary (or the exact push error if push failed)
146289
-
146290
- ${learningsStep(t, 6)}`
146269
+ - call \`${t("report_progress")}\` with a brief summary (or the exact push error if push failed)`
146291
146270
  },
146292
146271
  // Review and IncrementalReview use the multi-lens orchestrator pattern
146293
146272
  // (canonical source: .claude/commands/anneal.md). The orchestrator does
@@ -146458,9 +146437,7 @@ ${PR_SUMMARY_FORMAT}`
146458
146437
 
146459
146438
  2. Produce a structured, actionable plan with clear milestones.
146460
146439
 
146461
- 3. Call \`${t("report_progress")}\` with the plan.
146462
-
146463
- ${learningsStep(t, 4)}`
146440
+ 3. Call \`${t("report_progress")}\` with the plan.`
146464
146441
  },
146465
146442
  {
146466
146443
  name: "Fix",
@@ -146482,9 +146459,7 @@ ${learningsStep(t, 4)}`
146482
146459
 
146483
146460
  5. Finalize:
146484
146461
  - confirm a clean working tree, then push via \`${t("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*)
146485
- - call \`${t("report_progress")}\` with the diagnosis and fix summary (or the exact push error if push failed)
146486
-
146487
- ${learningsStep(t, 6)}`
146462
+ - call \`${t("report_progress")}\` with the diagnosis and fix summary (or the exact push error if push failed)`
146488
146463
  },
146489
146464
  {
146490
146465
  name: "ResolveConflicts",
@@ -146528,9 +146503,7 @@ ${learningsStep(t, 6)}`
146528
146503
  3. Finalize:
146529
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).
146530
146505
  - call \`${t("report_progress")}\` once with results \u2014 include exact tool errors if push or PR creation failed
146531
- - if the task involved labeling, commenting, or other GitHub operations, perform those directly
146532
-
146533
- ${learningsStep(t, 4)}`
146506
+ - if the task involved labeling, commenting, or other GitHub operations, perform those directly`
146534
146507
  }
146535
146508
  ];
146536
146509
  }
@@ -146630,6 +146603,17 @@ async function installFromNpmTarball(params) {
146630
146603
  // utils/providerErrors.ts
146631
146604
  var statusKey = `\\b(?:status[_ ]?code|http[_ ]?status|status)["']?\\s*[:=]\\s*["']?`;
146632
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)" },
146633
146617
  { regex: new RegExp(`${statusKey}429\\b`, "i"), label: "rate limited (429)" },
146634
146618
  { regex: new RegExp(`${statusKey}500\\b`, "i"), label: "provider 500 error" },
146635
146619
  { regex: new RegExp(`${statusKey}503\\b`, "i"), label: "provider unavailable (503)" },
@@ -146693,7 +146677,7 @@ function installBundledSkills(params) {
146693
146677
  writeFileSync6(join9(skillDir, "SKILL.md"), content);
146694
146678
  }
146695
146679
  }
146696
- log.info(`installed bundled skills: ${BUNDLED_SKILL_NAMES.join(", ")}`);
146680
+ log.success(`installed bundled skills: ${BUNDLED_SKILL_NAMES.join(", ")}`);
146697
146681
  }
146698
146682
  function addSkill(params) {
146699
146683
  const result = spawnSync5(
@@ -146718,7 +146702,7 @@ function addSkill(params) {
146718
146702
  }
146719
146703
  );
146720
146704
  if (result.status === 0) {
146721
- log.info(`installed ${params.skill} skill (${params.agent})`);
146705
+ log.success(`installed ${params.skill} skill (${params.agent})`);
146722
146706
  } else {
146723
146707
  const stderr = (result.stderr?.toString() || "").trim();
146724
146708
  const errorMsg = result.error ? result.error.message : stderr;
@@ -146852,18 +146836,17 @@ function buildPostRunPrompt(issues) {
146852
146836
  if (issues.summaryStale) parts.push(buildSummaryStalePrompt(issues.summaryStale.filePath));
146853
146837
  return parts.join("\n\n---\n\n");
146854
146838
  }
146855
- function buildLearningsReflectionPrompt(agentId) {
146856
- const t = (name) => formatMcpToolRef(agentId, name);
146839
+ function buildLearningsReflectionPrompt(filePath) {
146857
146840
  return [
146858
- `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?`,
146859
146842
  "",
146860
- `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.`,
146861
146844
  "",
146862
- `rules:`,
146863
- `- only call \`${t("update_learnings")}\` when the finding is high-confidence and broadly useful. skip if unsure, speculative, or one-off.`,
146864
- `- pass the FULL merged list: existing learnings from the original prompt + your new discoveries. one fact per bullet, lines starting with \`- \`.`,
146865
- `- deduplicate, and drop bullets that are clearly wrong or no longer relevant to the current codebase.`,
146866
- `- 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.`
146867
146850
  ].join("\n");
146868
146851
  }
146869
146852
  async function runPostRunRetryLoop(params) {
@@ -147446,7 +147429,7 @@ var claude = agent({
147446
147429
  stopScript: ctx.stopScript,
147447
147430
  summaryFilePath: ctx.summaryFilePath,
147448
147431
  summarySeed: ctx.summarySeed,
147449
- reflectionPrompt: buildLearningsReflectionPrompt("claude"),
147432
+ reflectionPrompt: ctx.learningsFilePath ? buildLearningsReflectionPrompt(ctx.learningsFilePath) : void 0,
147450
147433
  canResume: (r) => Boolean(r.sessionId),
147451
147434
  resume: async (c) => {
147452
147435
  const sessionId = c.previousResult.sessionId;
@@ -147462,9 +147445,92 @@ var claude = agent({
147462
147445
 
147463
147446
  // agents/opencode.ts
147464
147447
  import { execFileSync as execFileSync4 } from "node:child_process";
147465
- import { mkdirSync as mkdirSync5 } from "node:fs";
147448
+ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "node:fs";
147466
147449
  import { join as join11 } from "node:path";
147467
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
147468
147534
  async function installOpencodeCli() {
147469
147535
  return await installFromNpmTarball({
147470
147536
  packageName: "opencode-ai",
@@ -147566,9 +147632,6 @@ async function runOpenCode(params) {
147566
147632
  const taskDispatchByCallID = /* @__PURE__ */ new Map();
147567
147633
  const pendingTaskDispatches = [];
147568
147634
  const knownNonTaskCallIDs = /* @__PURE__ */ new Set();
147569
- function isSubagentInFlight() {
147570
- return taskDispatchByCallID.size > 0 || pendingTaskDispatches.length > 0;
147571
- }
147572
147635
  function emitSubagentFinished(dispatch, status, output2, matchKind) {
147573
147636
  const subagentDuration = performance7.now() - dispatch.startedAt;
147574
147637
  const outputStr = typeof output2 === "string" ? output2 : "";
@@ -147687,18 +147750,20 @@ async function runOpenCode(params) {
147687
147750
  return;
147688
147751
  }
147689
147752
  if (toolName === "task") {
147690
- const taskInput = event.part?.state?.input ?? {};
147691
- const dispatchedLabel = labeler.recordTaskDispatch(taskInput);
147692
- const dispatch = {
147693
- label: dispatchedLabel,
147694
- startedAt: performance7.now(),
147695
- toolUseCallID: toolId
147696
- };
147697
- taskDispatchByCallID.set(toolId, dispatch);
147698
- pendingTaskDispatches.push(dispatch);
147699
- log.info(
147700
- `\xBB dispatching subagent: ${dispatchedLabel}` + (taskInput.subagent_type ? ` (subagent_type=${taskInput.subagent_type})` : "")
147701
- );
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
+ }
147702
147767
  } else {
147703
147768
  knownNonTaskCallIDs.add(toolId);
147704
147769
  }
@@ -147719,6 +147784,10 @@ async function runOpenCode(params) {
147719
147784
  if (event.part?.state?.status === "completed" && event.part.state.output) {
147720
147785
  log.debug(withLabel(label, ` output: ${event.part.state.output}`));
147721
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
+ }
147722
147791
  if (toolName.includes("report_progress") && params.todoTracker) {
147723
147792
  log.debug("\xBB report_progress detected, disabling todo tracking");
147724
147793
  params.todoTracker.cancel();
@@ -147805,6 +147874,53 @@ async function runOpenCode(params) {
147805
147874
  tokensLogged = true;
147806
147875
  }
147807
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
+ }
147808
147924
  }
147809
147925
  };
147810
147926
  const recentStderr = [];
@@ -147828,13 +147944,13 @@ async function runOpenCode(params) {
147828
147944
  // never fires — producing zombie runs. detached + killGroup nukes the
147829
147945
  // whole tree.
147830
147946
  killGroup: true,
147831
- // suspend the inner activity timer while a `task` subagent is in flight.
147832
- // opencode's task tool encapsulates subagent execution in-process the
147833
- // subagent's internal events don't surface on the parent NDJSON stream,
147834
- // so without this the 5min timeout would falsely fire mid-subagent.
147835
- // suspend/resume is preferable to a heartbeat because there's no race
147836
- // between a periodic tick and a subagent finishing between ticks.
147837
- isPausedExternally: isSubagentInFlight,
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).
147838
147954
  onStdout: async (chunk) => {
147839
147955
  const text = chunk.toString();
147840
147956
  output += text;
@@ -147989,6 +148105,12 @@ var opencode = agent({
147989
148105
  XDG_CONFIG_HOME: join11(ctx.tmpdir, ".config")
147990
148106
  };
147991
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
+ );
147992
148114
  const agentBrowserVersion = getDevDependencyVersion("agent-browser");
147993
148115
  addSkill({
147994
148116
  ref: `vercel-labs/agent-browser@v${agentBrowserVersion}`,
@@ -148031,7 +148153,7 @@ var opencode = agent({
148031
148153
  stopScript: ctx.stopScript,
148032
148154
  summaryFilePath: ctx.summaryFilePath,
148033
148155
  summarySeed: ctx.summarySeed,
148034
- reflectionPrompt: buildLearningsReflectionPrompt("opencode"),
148156
+ reflectionPrompt: ctx.learningsFilePath ? buildLearningsReflectionPrompt(ctx.learningsFilePath) : void 0,
148035
148157
  resume: async (c) => runOpenCode({
148036
148158
  ...runParams,
148037
148159
  args: [...baseArgs, "--continue", c.prompt]
@@ -152244,7 +152366,7 @@ ${ctx.error}` : ctx.error;
152244
152366
 
152245
152367
  // utils/gitAuthServer.ts
152246
152368
  import { randomUUID as randomUUID3 } from "node:crypto";
152247
- import { writeFileSync as writeFileSync8 } from "node:fs";
152369
+ import { writeFileSync as writeFileSync9 } from "node:fs";
152248
152370
  import { createServer as createServer2 } from "node:http";
152249
152371
  import { join as join13 } from "node:path";
152250
152372
  var CODE_TTL_MS = 5 * 60 * 1e3;
@@ -152333,7 +152455,7 @@ async function startGitAuthServer(tmpdir3) {
152333
152455
  `try{require("fs").unlinkSync("${scriptPath.replace(/\\/g, "\\\\")}")}catch(e){}`,
152334
152456
  `})}).on("error",function(){process.exit(1)})}`
152335
152457
  ].join("\n");
152336
- writeFileSync8(scriptPath, content, { mode: 448 });
152458
+ writeFileSync9(scriptPath, content, { mode: 448 });
152337
152459
  return scriptPath;
152338
152460
  }
152339
152461
  async function close() {
@@ -152607,9 +152729,9 @@ function buildPromptContext(ctx) {
152607
152729
  };
152608
152730
  }
152609
152731
  function assembleFullPrompt(ctx) {
152610
- const learningsSection = ctx.learnings ? `************* LEARNINGS *************
152732
+ const learningsSection = ctx.learningsFilePath ? `************* LEARNINGS *************
152611
152733
 
152612
- ${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.` : "";
152613
152735
  const runtimeSection = `************* RUNTIME *************
152614
152736
 
152615
152737
  ${ctx.runtime}`;
@@ -152636,8 +152758,8 @@ function resolveInstructions(ctx) {
152636
152758
  if (eventContext)
152637
152759
  tocEntries.push({ label: "EVENT CONTEXT", description: "related PR/issue data" });
152638
152760
  tocEntries.push({ label: "SYSTEM", description: "persona, security, tools, workflow rules" });
152639
- if (pctx.learnings)
152640
- tocEntries.push({ label: "LEARNINGS", description: "repo-specific knowledge" });
152761
+ if (pctx.learningsFilePath)
152762
+ tocEntries.push({ label: "LEARNINGS", description: "repo-specific knowledge file path" });
152641
152763
  tocEntries.push({ label: "RUNTIME", description: "environment metadata" });
152642
152764
  const toc = buildToc(tocEntries);
152643
152765
  const full = assembleFullPrompt({
@@ -152646,7 +152768,7 @@ function resolveInstructions(ctx) {
152646
152768
  procedure,
152647
152769
  eventContext,
152648
152770
  system,
152649
- learnings: pctx.learnings,
152771
+ learningsFilePath: pctx.learningsFilePath,
152650
152772
  runtime: pctx.runtime
152651
152773
  });
152652
152774
  const event = [pctx.eventTitle, pctx.eventMetadata].filter(Boolean).join("\n\n---\n\n");
@@ -152660,6 +152782,32 @@ function resolveInstructions(ctx) {
152660
152782
  };
152661
152783
  }
152662
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
+
152663
152811
  // utils/normalizeEnv.ts
152664
152812
  function maskValue(value2) {
152665
152813
  if (value2 && typeof value2 === "string" && value2.trim().length > 0) {
@@ -152835,8 +152983,8 @@ function resolvePayload(resolvedPromptInput, repoSettings) {
152835
152983
  }
152836
152984
 
152837
152985
  // utils/prSummary.ts
152838
- import { mkdir, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
152839
- 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";
152840
152988
  var SUMMARY_FILE_NAME = "pullfrog-summary.md";
152841
152989
  var SUMMARY_SCAFFOLD = `# PR summary
152842
152990
 
@@ -152846,19 +152994,19 @@ var SUMMARY_SCAFFOLD = `# PR summary
152846
152994
  var MIN_SNAPSHOT_LENGTH = 60;
152847
152995
  var MAX_SNAPSHOT_LENGTH = 32768;
152848
152996
  function summaryFilePath(tmpdir3) {
152849
- return join14(tmpdir3, SUMMARY_FILE_NAME);
152997
+ return join15(tmpdir3, SUMMARY_FILE_NAME);
152850
152998
  }
152851
152999
  async function seedSummaryFile(params) {
152852
153000
  const path3 = summaryFilePath(params.tmpdir);
152853
- await mkdir(dirname4(path3), { recursive: true });
153001
+ await mkdir2(dirname5(path3), { recursive: true });
152854
153002
  const seed = params.previousSnapshot && params.previousSnapshot.trim().length >= MIN_SNAPSHOT_LENGTH ? params.previousSnapshot : SUMMARY_SCAFFOLD;
152855
- await writeFile2(path3, seed, "utf8");
153003
+ await writeFile3(path3, seed, "utf8");
152856
153004
  return path3;
152857
153005
  }
152858
153006
  async function readSummaryFile(path3) {
152859
153007
  let raw2;
152860
153008
  try {
152861
- raw2 = await readFile2(path3, "utf8");
153009
+ raw2 = await readFile3(path3, "utf8");
152862
153010
  } catch {
152863
153011
  return null;
152864
153012
  }
@@ -153076,9 +153224,9 @@ async function resolveRunContextData(params) {
153076
153224
  import { execFileSync as execFileSync5, execSync as execSync3 } from "node:child_process";
153077
153225
  import { mkdtempSync } from "node:fs";
153078
153226
  import { tmpdir as tmpdir2 } from "node:os";
153079
- import { join as join15 } from "node:path";
153227
+ import { join as join16 } from "node:path";
153080
153228
  function createTempDirectory() {
153081
- const sharedTempDir = mkdtempSync(join15(tmpdir2(), "pullfrog-"));
153229
+ const sharedTempDir = mkdtempSync(join16(tmpdir2(), "pullfrog-"));
153082
153230
  process.env.PULLFROG_TEMP_DIR = sharedTempDir;
153083
153231
  log.info(`\xBB created temp dir at ${sharedTempDir}`);
153084
153232
  return sharedTempDir;
@@ -153480,15 +153628,12 @@ function formatTransientErrorSummary(error49, owner) {
153480
153628
  }
153481
153629
  async function mintProxyKey(ctx) {
153482
153630
  try {
153483
- process.env.ACTIONS_ID_TOKEN_REQUEST_URL = ctx.oidcCredentials.requestUrl;
153484
- process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = ctx.oidcCredentials.requestToken;
153485
- const oidcToken = await core6.getIDToken("pullfrog-api");
153486
- delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
153487
- delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
153631
+ const headers = await buildProxyTokenHeaders(ctx);
153632
+ if (!headers) return null;
153488
153633
  const response = await apiFetch({
153489
153634
  path: "/api/proxy-token",
153490
153635
  method: "POST",
153491
- headers: { Authorization: `Bearer ${oidcToken}` }
153636
+ headers
153492
153637
  });
153493
153638
  if (response.status === 402) {
153494
153639
  const body = await response.json().catch(() => null);
@@ -153520,15 +153665,30 @@ async function mintProxyKey(ctx) {
153520
153665
  delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
153521
153666
  }
153522
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
+ }
153523
153683
  async function resolveProxyModel(ctx) {
153524
153684
  if (process.env.PULLFROG_MODEL?.trim()) return;
153525
153685
  const needsProxy = isInfraCovered({ isOss: ctx.oss, plan: ctx.plan }) && ctx.proxyModel;
153526
153686
  if (!needsProxy) return;
153527
- if (!ctx.oidcCredentials) {
153687
+ if (!ctx.oidcCredentials && !isLocalApiUrl()) {
153528
153688
  log.warning("\xBB proxy requested but no OIDC credentials available \u2014 skipping");
153529
153689
  return;
153530
153690
  }
153531
- const key = await mintProxyKey({ oidcCredentials: ctx.oidcCredentials });
153691
+ const key = await mintProxyKey({ oidcCredentials: ctx.oidcCredentials, repo: ctx.repo });
153532
153692
  if (!key) return;
153533
153693
  process.env.OPENROUTER_API_KEY = key;
153534
153694
  core6.setSecret(key);
@@ -153552,6 +153712,45 @@ async function fetchPreviousSnapshot(ctx, prNumber) {
153552
153712
  return null;
153553
153713
  }
153554
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
+ }
153555
153754
  async function persistSummary(ctx) {
153556
153755
  const filePath = ctx.toolState.summaryFilePath;
153557
153756
  if (!filePath) return;
@@ -153633,7 +153832,8 @@ async function main() {
153633
153832
  oss: runContext.oss,
153634
153833
  plan: runContext.plan,
153635
153834
  proxyModel: runContext.proxyModel,
153636
- oidcCredentials
153835
+ oidcCredentials,
153836
+ repo: runContext.repo
153637
153837
  });
153638
153838
  } catch (error49) {
153639
153839
  if (error49 instanceof BillingError) {
@@ -153736,12 +153936,32 @@ async function main() {
153736
153936
  toolContext.mcpServerUrl = mcpHttpServer.url;
153737
153937
  log.info(`\xBB MCP server started at ${mcpHttpServer.url}`);
153738
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
+ }
153739
153959
  if (payload.generateSummary && payload.event.is_pr && payload.event.issue_number) {
153740
153960
  const previousSnapshot = await fetchPreviousSnapshot(toolContext, payload.event.issue_number);
153741
153961
  const filePath = await seedSummaryFile({ tmpdir: tmpdir3, previousSnapshot });
153742
153962
  toolState.summaryFilePath = filePath;
153743
153963
  try {
153744
- toolState.summarySeed = await readFile3(filePath, "utf8");
153964
+ toolState.summarySeed = await readFile4(filePath, "utf8");
153745
153965
  } catch {
153746
153966
  }
153747
153967
  log.info(
@@ -153765,7 +153985,7 @@ async function main() {
153765
153985
  modes: modes2,
153766
153986
  agentId,
153767
153987
  outputSchema,
153768
- learnings: runContext.repoSettings.learnings
153988
+ learningsFilePath: toolState.learningsFilePath ?? null
153769
153989
  });
153770
153990
  const logParts = [
153771
153991
  instructions.eventInstructions ? `EVENT-LEVEL INSTRUCTIONS:
@@ -153781,7 +154001,7 @@ ${instructions.user}` : null,
153781
154001
  log.info(instructions.full);
153782
154002
  });
153783
154003
  if (agentId === "opencode") {
153784
- const pluginDir = join16(process.cwd(), ".opencode", "plugin");
154004
+ const pluginDir = join17(process.cwd(), ".opencode", "plugin");
153785
154005
  const hasPlugins = existsSync7(pluginDir) && readdirSync(pluginDir).some((f) => /\.[jt]sx?$/.test(f));
153786
154006
  if (hasPlugins && toolState.dependencyInstallation?.promise) {
153787
154007
  log.info(
@@ -153842,6 +154062,7 @@ ${instructions.user}` : null,
153842
154062
  stopScript: runContext.repoSettings.stopScript,
153843
154063
  summaryFilePath: toolState.summaryFilePath,
153844
154064
  summarySeed: toolState.summarySeed,
154065
+ learningsFilePath: toolState.learningsFilePath,
153845
154066
  onActivityTimeout: onInnerActivityTimeout,
153846
154067
  onToolUse: (event) => {
153847
154068
  const wasTracked = recordDiffReadFromToolUse({
@@ -153899,6 +154120,9 @@ ${instructions.user}` : null,
153899
154120
  if (toolContext) {
153900
154121
  await persistSummary(toolContext);
153901
154122
  }
154123
+ if (toolContext) {
154124
+ await persistLearnings(toolContext);
154125
+ }
153902
154126
  if (toolContext && toolState.progressComment && !toolState.finalSummaryWritten) {
153903
154127
  await deleteProgressComment(toolContext).catch((error49) => {
153904
154128
  log.debug(`stranded progress comment cleanup failed: ${error49}`);
@@ -153951,6 +154175,9 @@ ${errorMessage}
153951
154175
  if (toolContext) {
153952
154176
  await persistSummary(toolContext);
153953
154177
  }
154178
+ if (toolContext) {
154179
+ await persistLearnings(toolContext);
154180
+ }
153954
154181
  return {
153955
154182
  success: false,
153956
154183
  error: errorMessage