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/cli.mjs CHANGED
@@ -18415,7 +18415,7 @@ var require_summary = __commonJS({
18415
18415
  exports.summary = exports.markdownSummary = exports.SUMMARY_DOCS_URL = exports.SUMMARY_ENV_VAR = void 0;
18416
18416
  var os_1 = __require("os");
18417
18417
  var fs_1 = __require("fs");
18418
- var { access, appendFile, writeFile: writeFile3 } = fs_1.promises;
18418
+ var { access, appendFile, writeFile: writeFile4 } = fs_1.promises;
18419
18419
  exports.SUMMARY_ENV_VAR = "GITHUB_STEP_SUMMARY";
18420
18420
  exports.SUMMARY_DOCS_URL = "https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary";
18421
18421
  var Summary = class {
@@ -18473,7 +18473,7 @@ var require_summary = __commonJS({
18473
18473
  return __awaiter(this, void 0, void 0, function* () {
18474
18474
  const overwrite = !!(options === null || options === void 0 ? void 0 : options.overwrite);
18475
18475
  const filePath = yield this.filePath();
18476
- const writeFunc = overwrite ? writeFile3 : appendFile;
18476
+ const writeFunc = overwrite ? writeFile4 : appendFile;
18477
18477
  yield writeFunc(filePath, this._buffer, { encoding: "utf8" });
18478
18478
  return this.emptyBuffer();
18479
18479
  });
@@ -62879,8 +62879,8 @@ var require_snapshot_utils = __commonJS({
62879
62879
  var require_snapshot_recorder = __commonJS({
62880
62880
  "node_modules/.pnpm/undici@7.22.0/node_modules/undici/lib/mock/snapshot-recorder.js"(exports, module) {
62881
62881
  "use strict";
62882
- var { writeFile: writeFile3, readFile: readFile4, mkdir: mkdir2 } = __require("node:fs/promises");
62883
- var { dirname: dirname6, resolve: resolve3 } = __require("node:path");
62882
+ var { writeFile: writeFile4, readFile: readFile5, mkdir: mkdir3 } = __require("node:fs/promises");
62883
+ var { dirname: dirname7, resolve: resolve3 } = __require("node:path");
62884
62884
  var { setTimeout: setTimeout2, clearTimeout: clearTimeout2 } = __require("node:timers");
62885
62885
  var { InvalidArgumentError, UndiciError } = require_errors4();
62886
62886
  var { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = require_snapshot_utils();
@@ -63081,7 +63081,7 @@ var require_snapshot_recorder = __commonJS({
63081
63081
  throw new InvalidArgumentError("Snapshot path is required");
63082
63082
  }
63083
63083
  try {
63084
- const data = await readFile4(resolve3(path3), "utf8");
63084
+ const data = await readFile5(resolve3(path3), "utf8");
63085
63085
  const parsed2 = JSON.parse(data);
63086
63086
  if (Array.isArray(parsed2)) {
63087
63087
  this.#snapshots.clear();
@@ -63111,12 +63111,12 @@ var require_snapshot_recorder = __commonJS({
63111
63111
  throw new InvalidArgumentError("Snapshot path is required");
63112
63112
  }
63113
63113
  const resolvedPath = resolve3(path3);
63114
- await mkdir2(dirname6(resolvedPath), { recursive: true });
63114
+ await mkdir3(dirname7(resolvedPath), { recursive: true });
63115
63115
  const data = Array.from(this.#snapshots.entries()).map(([hash2, snapshot2]) => ({
63116
63116
  hash: hash2,
63117
63117
  snapshot: snapshot2
63118
63118
  }));
63119
- await writeFile3(resolvedPath, JSON.stringify(data, null, 2), { flush: true });
63119
+ await writeFile4(resolvedPath, JSON.stringify(data, null, 2), { flush: true });
63120
63120
  }
63121
63121
  /**
63122
63122
  * Clears all recorded snapshots
@@ -97692,14 +97692,14 @@ var require_turndown_cjs = __commonJS({
97692
97692
  } else if (node2.nodeType === 1) {
97693
97693
  replacement = replacementForNode.call(self2, node2);
97694
97694
  }
97695
- return join17(output, replacement);
97695
+ return join18(output, replacement);
97696
97696
  }, "");
97697
97697
  }
97698
97698
  function postProcess(output) {
97699
97699
  var self2 = this;
97700
97700
  this.rules.forEach(function(rule) {
97701
97701
  if (typeof rule.append === "function") {
97702
- output = join17(output, rule.append(self2.options));
97702
+ output = join18(output, rule.append(self2.options));
97703
97703
  }
97704
97704
  });
97705
97705
  return output.replace(/^[\t\r\n]+/, "").replace(/[\t\r\n\s]+$/, "");
@@ -97711,7 +97711,7 @@ var require_turndown_cjs = __commonJS({
97711
97711
  if (whitespace.leading || whitespace.trailing) content = content.trim();
97712
97712
  return whitespace.leading + rule.replacement(content, node2, this.options) + whitespace.trailing;
97713
97713
  }
97714
- function join17(output, replacement) {
97714
+ function join18(output, replacement) {
97715
97715
  var s1 = trimTrailingNewlines(output);
97716
97716
  var s2 = trimLeadingNewlines(replacement);
97717
97717
  var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
@@ -99204,13 +99204,13 @@ import { basename as basename2 } from "node:path";
99204
99204
  // commands/gha.ts
99205
99205
  var core7 = __toESM(require_core(), 1);
99206
99206
  var import_arg = __toESM(require_arg(), 1);
99207
- import { dirname as dirname5 } from "node:path";
99207
+ import { dirname as dirname6 } from "node:path";
99208
99208
 
99209
99209
  // main.ts
99210
99210
  var core6 = __toESM(require_core(), 1);
99211
99211
  import { existsSync as existsSync7, readdirSync } from "node:fs";
99212
- import { readFile as readFile3 } from "node:fs/promises";
99213
- import { join as join16 } from "node:path";
99212
+ import { readFile as readFile4 } from "node:fs/promises";
99213
+ import { join as join17 } from "node:path";
99214
99214
 
99215
99215
  // node_modules/.pnpm/@ark+util@0.56.0/node_modules/@ark/util/out/arrays.js
99216
99216
  var liftArray = (data) => Array.isArray(data) ? data : [data];
@@ -108006,6 +108006,13 @@ function getApiUrl() {
108006
108006
  log.debug(`resolved API_URL: ${raw2}`);
108007
108007
  return raw2;
108008
108008
  }
108009
+ function isLocalApiUrl() {
108010
+ try {
108011
+ return isLocalUrl(new URL(getApiUrl()));
108012
+ } catch {
108013
+ return false;
108014
+ }
108015
+ }
108009
108016
 
108010
108017
  // models.ts
108011
108018
  function provider(config3) {
@@ -109244,6 +109251,7 @@ function CreateCommentTool(ctx) {
109244
109251
  body: bodyWithFooter
109245
109252
  });
109246
109253
  ctx.toolState.wasUpdated = true;
109254
+ log.info(`\xBB created comment ${result.data.id}`);
109247
109255
  if (commentType === "Plan") {
109248
109256
  if (result.data.node_id) {
109249
109257
  await patchWorkflowRunFields(ctx, { planCommentNodeId: result.data.node_id });
@@ -109257,6 +109265,7 @@ function CreateCommentTool(ctx) {
109257
109265
  comment_id: result.data.id,
109258
109266
  body: bodyWithPlanLink
109259
109267
  });
109268
+ log.info(`\xBB updated comment ${updateResult.data.id}`);
109260
109269
  return {
109261
109270
  success: true,
109262
109271
  commentId: updateResult.data.id,
@@ -109290,6 +109299,7 @@ function EditCommentTool(ctx) {
109290
109299
  comment_id: commentId,
109291
109300
  body: bodyWithFooter
109292
109301
  });
109302
+ log.info(`\xBB updated comment ${result.data.id}`);
109293
109303
  return {
109294
109304
  success: true,
109295
109305
  commentId: result.data.id,
@@ -109425,6 +109435,9 @@ ${collapsible}`;
109425
109435
  message: "progress recorded (no GitHub comment created - this may occur for workflow_dispatch events or when there is no associated issue/PR)"
109426
109436
  };
109427
109437
  }
109438
+ if (result.commentId !== void 0) {
109439
+ log.info(`\xBB ${result.action} comment ${result.commentId}`);
109440
+ }
109428
109441
  if (!params.target_plan_comment) {
109429
109442
  ctx.toolState.finalSummaryWritten = true;
109430
109443
  }
@@ -109475,6 +109488,7 @@ function ReplyToReviewCommentTool(ctx) {
109475
109488
  comment_id,
109476
109489
  body: bodyWithFooter
109477
109490
  });
109491
+ log.info(`\xBB created review comment ${result.data.id} (in reply to ${comment_id})`);
109478
109492
  ctx.toolState.wasUpdated = true;
109479
109493
  return {
109480
109494
  success: true,
@@ -110024,11 +110038,6 @@ async function spawn(options) {
110024
110038
  `spawn activity timer: pid=${child.pid} cmd=${options.cmd} timeout=${activityTimeoutMs}ms`
110025
110039
  );
110026
110040
  activityCheckIntervalId = setInterval(() => {
110027
- if (options.isPausedExternally?.()) {
110028
- lastActivityTime = performance3.now();
110029
- log.debug(`spawn activity check: pid=${child.pid} paused externally`);
110030
- return;
110031
- }
110032
110041
  const idleMs = performance3.now() - lastActivityTime;
110033
110042
  log.debug(
110034
110043
  `spawn activity check: pid=${child.pid} idle=${Math.round(idleMs)}ms / ${activityTimeoutMs}ms`
@@ -142549,7 +142558,7 @@ var import_semver = __toESM(require_semver2(), 1);
142549
142558
  // package.json
142550
142559
  var package_default = {
142551
142560
  name: "pullfrog",
142552
- version: "0.1.1",
142561
+ version: "0.1.2",
142553
142562
  type: "module",
142554
142563
  bin: {
142555
142564
  pullfrog: "dist/cli.mjs",
@@ -143493,6 +143502,10 @@ ${integrateStep}
143493
143502
  if (!pushed) {
143494
143503
  throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
143495
143504
  }
143505
+ const pushedSha = $("git", ["rev-parse", "HEAD"], { log: false }).trim();
143506
+ log.info(
143507
+ `\xBB pushed branch ${branch} to ${pushDest.remoteName}/${pushDest.remoteBranch} (sha ${pushedSha})`
143508
+ );
143496
143509
  return {
143497
143510
  success: true,
143498
143511
  branch,
@@ -143641,6 +143654,7 @@ function DeleteBranchTool(ctx) {
143641
143654
  await $git("push", ["origin", "--delete", `refs/heads/${params.branchName}`], {
143642
143655
  token: ctx.gitToken
143643
143656
  });
143657
+ log.info(`\xBB deleted branch ${params.branchName}`);
143644
143658
  return { success: true, deleted: params.branchName };
143645
143659
  })
143646
143660
  });
@@ -143666,6 +143680,7 @@ function PushTagsTool(ctx) {
143666
143680
  await $git("push", pushArgs, {
143667
143681
  token: ctx.gitToken
143668
143682
  });
143683
+ log.info(`\xBB pushed tag ${params.tag}`);
143669
143684
  return { success: true, tag: params.tag };
143670
143685
  })
143671
143686
  });
@@ -143990,6 +144005,7 @@ function CreatePullRequestReviewTool(ctx) {
143990
144005
  }
143991
144006
  const reviewId = result.data.id;
143992
144007
  const reviewNodeId = result.data.node_id;
144008
+ log.info(`\xBB created review ${reviewId} on pull request #${pull_number}`);
143993
144009
  const actuallyReviewedSha = ctx.toolState.checkoutSha ?? params.commit_id;
143994
144010
  ctx.toolState.review = {
143995
144011
  id: reviewId,
@@ -144854,6 +144870,7 @@ function IssueTool(ctx) {
144854
144870
  labels: params.labels ?? [],
144855
144871
  assignees: params.assignees ?? []
144856
144872
  });
144873
+ log.info(`\xBB created issue #${result.data.number} (id ${result.data.id})`);
144857
144874
  const nodeId = result.data.node_id;
144858
144875
  if (typeof nodeId === "string" && nodeId.length > 0) {
144859
144876
  await patchWorkflowRunFields(ctx, {
@@ -145045,6 +145062,7 @@ function AddLabelsTool(ctx) {
145045
145062
  issue_number,
145046
145063
  labels
145047
145064
  });
145065
+ log.info(`\xBB added labels [${labels.join(", ")}] to issue #${issue_number}`);
145048
145066
  return {
145049
145067
  success: true,
145050
145068
  labels: result.data.map((label) => label.name)
@@ -145053,40 +145071,6 @@ function AddLabelsTool(ctx) {
145053
145071
  });
145054
145072
  }
145055
145073
 
145056
- // mcp/learnings.ts
145057
- var UpdateLearningsParams = type({
145058
- learnings: type.string.describe(
145059
- "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."
145060
- )
145061
- });
145062
- function UpdateLearningsTool(ctx) {
145063
- return tool({
145064
- name: "update_learnings",
145065
- 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.",
145066
- parameters: UpdateLearningsParams,
145067
- execute: execute(async (params) => {
145068
- const response = await apiFetch({
145069
- path: `/api/repo/${ctx.repo.owner}/${ctx.repo.name}/learnings`,
145070
- method: "PATCH",
145071
- headers: {
145072
- authorization: `Bearer ${ctx.apiToken}`,
145073
- "content-type": "application/json"
145074
- },
145075
- body: JSON.stringify({
145076
- learnings: params.learnings,
145077
- model: ctx.toolState.model
145078
- }),
145079
- signal: AbortSignal.timeout(1e4)
145080
- });
145081
- if (!response.ok) {
145082
- const error49 = await response.text();
145083
- throw new Error(`failed to update learnings: ${error49}`);
145084
- }
145085
- return { success: true };
145086
- })
145087
- });
145088
- }
145089
-
145090
145074
  // mcp/output.ts
145091
145075
  var import_ajv3 = __toESM(require_ajv(), 1);
145092
145076
  var SetOutputParams = type({
@@ -145180,6 +145164,7 @@ function UpdatePullRequestBodyTool(ctx) {
145180
145164
  pull_number: params.pull_number,
145181
145165
  body: bodyWithFooter
145182
145166
  });
145167
+ log.info(`\xBB updated pull request #${result.data.number}`);
145183
145168
  ctx.toolState.wasUpdated = true;
145184
145169
  return {
145185
145170
  success: true,
@@ -145207,6 +145192,7 @@ function CreatePullRequestTool(ctx) {
145207
145192
  base: params.base,
145208
145193
  draft: params.draft ?? false
145209
145194
  });
145195
+ log.info(`\xBB created pull request #${result.data.number} (id ${result.data.id})`);
145210
145196
  const reviewer = ctx.payload.triggerer;
145211
145197
  if (reviewer) {
145212
145198
  try {
@@ -145758,7 +145744,7 @@ function ResolveReviewThreadTool(ctx) {
145758
145744
  threadId: params.thread_id
145759
145745
  });
145760
145746
  const thread = response.resolveReviewThread.thread;
145761
- log.debug(`resolved thread ${thread.id}, isResolved=${thread.isResolved}`);
145747
+ log.info(`\xBB resolved review thread ${thread.id}`);
145762
145748
  return {
145763
145749
  thread_id: thread.id,
145764
145750
  is_resolved: thread.isResolved,
@@ -146230,6 +146216,7 @@ function UploadFileTool(ctx) {
146230
146216
  if (!uploadResponse.ok) {
146231
146217
  throw new Error(`failed to upload file: ${uploadResponse.statusText}`);
146232
146218
  }
146219
+ log.info(`\xBB uploaded file ${publicUrl}`);
146233
146220
  return { success: true, publicUrl, filename, contentLength, contentType };
146234
146221
  })
146235
146222
  });
@@ -146323,8 +146310,7 @@ function buildOrchestratorTools(ctx, outputSchema) {
146323
146310
  PushTagsTool(ctx),
146324
146311
  DeleteBranchTool(ctx),
146325
146312
  CreatePullRequestTool(ctx),
146326
- UpdatePullRequestBodyTool(ctx),
146327
- UpdateLearningsTool(ctx)
146313
+ UpdatePullRequestBodyTool(ctx)
146328
146314
  ];
146329
146315
  }
146330
146316
  async function tryStartMcpServer(ctx, tools, port) {
@@ -146481,9 +146467,6 @@ Rules:
146481
146467
  - Do NOT include a changelog section \u2014 the key changes list serves this purpose
146482
146468
  - Focus on *intent*, not *what* \u2014 the diff already shows what changed
146483
146469
  - Get the file count and commit count from the checkout_pr metadata, not by counting manually`;
146484
- function learningsStep(t2, n) {
146485
- 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 \`${t2("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.`;
146486
- }
146487
146470
  function computeModes(agentId) {
146488
146471
  const t2 = (toolName) => formatMcpToolRef(agentId, toolName);
146489
146472
  return [
@@ -146539,8 +146522,6 @@ function computeModes(agentId) {
146539
146522
  - create a PR via \`${t2("create_pull_request")}\`
146540
146523
  - call \`${t2("report_progress")}\` with the PR link or the exact error if push/PR failed
146541
146524
 
146542
- ${learningsStep(t2, 6)}
146543
-
146544
146525
  ### Notes
146545
146526
 
146546
146527
  For simple, well-defined tasks, skip the plan phase and go straight to build.`
@@ -146568,9 +146549,7 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
146568
146549
  - confirm a clean working tree, then push via \`${t2("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*)
146569
146550
  - reply to each comment using \`${t2("reply_to_review_comment")}\`
146570
146551
  - resolve addressed threads via \`${t2("resolve_review_thread")}\`
146571
- - call \`${t2("report_progress")}\` with a brief summary (or the exact push error if push failed)
146572
-
146573
- ${learningsStep(t2, 6)}`
146552
+ - call \`${t2("report_progress")}\` with a brief summary (or the exact push error if push failed)`
146574
146553
  },
146575
146554
  // Review and IncrementalReview use the multi-lens orchestrator pattern
146576
146555
  // (canonical source: .claude/commands/anneal.md). The orchestrator does
@@ -146741,9 +146720,7 @@ ${PR_SUMMARY_FORMAT}`
146741
146720
 
146742
146721
  2. Produce a structured, actionable plan with clear milestones.
146743
146722
 
146744
- 3. Call \`${t2("report_progress")}\` with the plan.
146745
-
146746
- ${learningsStep(t2, 4)}`
146723
+ 3. Call \`${t2("report_progress")}\` with the plan.`
146747
146724
  },
146748
146725
  {
146749
146726
  name: "Fix",
@@ -146765,9 +146742,7 @@ ${learningsStep(t2, 4)}`
146765
146742
 
146766
146743
  5. Finalize:
146767
146744
  - confirm a clean working tree, then push via \`${t2("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*)
146768
- - call \`${t2("report_progress")}\` with the diagnosis and fix summary (or the exact push error if push failed)
146769
-
146770
- ${learningsStep(t2, 6)}`
146745
+ - call \`${t2("report_progress")}\` with the diagnosis and fix summary (or the exact push error if push failed)`
146771
146746
  },
146772
146747
  {
146773
146748
  name: "ResolveConflicts",
@@ -146811,9 +146786,7 @@ ${learningsStep(t2, 6)}`
146811
146786
  3. Finalize:
146812
146787
  - if code changes were made, push to a pull request (new or existing) using \`${t2("push_branch")}\` and \`${t2("create_pull_request")}\` as needed. \`git status\` must be clean before you finish (see *SYSTEM* Git rules if push fails).
146813
146788
  - call \`${t2("report_progress")}\` once with results \u2014 include exact tool errors if push or PR creation failed
146814
- - if the task involved labeling, commenting, or other GitHub operations, perform those directly
146815
-
146816
- ${learningsStep(t2, 4)}`
146789
+ - if the task involved labeling, commenting, or other GitHub operations, perform those directly`
146817
146790
  }
146818
146791
  ];
146819
146792
  }
@@ -146913,6 +146886,17 @@ async function installFromNpmTarball(params) {
146913
146886
  // utils/providerErrors.ts
146914
146887
  var statusKey = `\\b(?:status[_ ]?code|http[_ ]?status|status)["']?\\s*[:=]\\s*["']?`;
146915
146888
  var PROVIDER_ERROR_PATTERNS = [
146889
+ // auth patterns must come BEFORE rate-limit patterns. OpenRouter 401 error
146890
+ // payloads carry `x-ratelimit-*` response headers in the dump, and the
146891
+ // free-form rate-limit regex below would otherwise win on word-boundary
146892
+ // matches inside header names. canonical 401 messages: OpenRouter returns
146893
+ // `{"error":{"message":"User not found","code":401}}` for disabled or
146894
+ // invalid keys (https://openai.luzhipeng.com/docs/api/reference/errors-and-debugging).
146895
+ { regex: new RegExp(`${statusKey}401\\b`, "i"), label: "auth error (401)" },
146896
+ { regex: new RegExp(`${statusKey}403\\b`, "i"), label: "auth error (403)" },
146897
+ { regex: /\bUser not found\b/i, label: "auth error (invalid/disabled key)" },
146898
+ { regex: /\bInvalid authentication\b/i, label: "auth error (invalid credentials)" },
146899
+ { regex: /\bNo auth credentials found\b/i, label: "auth error (missing credentials)" },
146916
146900
  { regex: new RegExp(`${statusKey}429\\b`, "i"), label: "rate limited (429)" },
146917
146901
  { regex: new RegExp(`${statusKey}500\\b`, "i"), label: "provider 500 error" },
146918
146902
  { regex: new RegExp(`${statusKey}503\\b`, "i"), label: "provider unavailable (503)" },
@@ -146976,7 +146960,7 @@ function installBundledSkills(params) {
146976
146960
  writeFileSync6(join9(skillDir, "SKILL.md"), content);
146977
146961
  }
146978
146962
  }
146979
- log.info(`installed bundled skills: ${BUNDLED_SKILL_NAMES.join(", ")}`);
146963
+ log.success(`installed bundled skills: ${BUNDLED_SKILL_NAMES.join(", ")}`);
146980
146964
  }
146981
146965
  function addSkill(params) {
146982
146966
  const result = spawnSync5(
@@ -147001,7 +146985,7 @@ function addSkill(params) {
147001
146985
  }
147002
146986
  );
147003
146987
  if (result.status === 0) {
147004
- log.info(`installed ${params.skill} skill (${params.agent})`);
146988
+ log.success(`installed ${params.skill} skill (${params.agent})`);
147005
146989
  } else {
147006
146990
  const stderr = (result.stderr?.toString() || "").trim();
147007
146991
  const errorMsg = result.error ? result.error.message : stderr;
@@ -147135,18 +147119,17 @@ function buildPostRunPrompt(issues) {
147135
147119
  if (issues.summaryStale) parts.push(buildSummaryStalePrompt(issues.summaryStale.filePath));
147136
147120
  return parts.join("\n\n---\n\n");
147137
147121
  }
147138
- function buildLearningsReflectionPrompt(agentId) {
147139
- const t2 = (name) => formatMcpToolRef(agentId, name);
147122
+ function buildLearningsReflectionPrompt(filePath) {
147140
147123
  return [
147141
- `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?`,
147124
+ `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?`,
147142
147125
  "",
147143
- `if so, call \`${t2("update_learnings")}\` to persist it.`,
147126
+ `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.`,
147144
147127
  "",
147145
- `rules:`,
147146
- `- only call \`${t2("update_learnings")}\` when the finding is high-confidence and broadly useful. skip if unsure, speculative, or one-off.`,
147147
- `- pass the FULL merged list: existing learnings from the original prompt + your new discoveries. one fact per bullet, lines starting with \`- \`.`,
147148
- `- deduplicate, and drop bullets that are clearly wrong or no longer relevant to the current codebase.`,
147149
- `- if you already called \`${t2("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.`
147128
+ `keep the file healthy:`,
147129
+ `- only add bullets when the finding is high-confidence AND broadly useful. skip speculative, one-off, or "maybe" findings.`,
147130
+ `- prune bullets that are clearly wrong, no longer relevant, or low-signal (rarely useful). a focused, accurate file beats a long stale one.`,
147131
+ `- 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.`,
147132
+ `- 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.`
147150
147133
  ].join("\n");
147151
147134
  }
147152
147135
  async function runPostRunRetryLoop(params) {
@@ -147729,7 +147712,7 @@ var claude = agent({
147729
147712
  stopScript: ctx.stopScript,
147730
147713
  summaryFilePath: ctx.summaryFilePath,
147731
147714
  summarySeed: ctx.summarySeed,
147732
- reflectionPrompt: buildLearningsReflectionPrompt("claude"),
147715
+ reflectionPrompt: ctx.learningsFilePath ? buildLearningsReflectionPrompt(ctx.learningsFilePath) : void 0,
147733
147716
  canResume: (r) => Boolean(r.sessionId),
147734
147717
  resume: async (c2) => {
147735
147718
  const sessionId = c2.previousResult.sessionId;
@@ -147745,9 +147728,92 @@ var claude = agent({
147745
147728
 
147746
147729
  // agents/opencode.ts
147747
147730
  import { execFileSync as execFileSync4 } from "node:child_process";
147748
- import { mkdirSync as mkdirSync5 } from "node:fs";
147731
+ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "node:fs";
147749
147732
  import { join as join11 } from "node:path";
147750
147733
  import { performance as performance7 } from "node:perf_hooks";
147734
+
147735
+ // agents/opencodePlugin.ts
147736
+ var PULLFROG_BUS_EVENT_TYPE = "pullfrog_bus_event";
147737
+ var PULLFROG_OPENCODE_PLUGIN_FILENAME = "pullfrog-events.ts";
147738
+ var PULLFROG_OPENCODE_PLUGIN_SOURCE = `// AUTOGENERATED by Pullfrog. do not edit; it'll be overwritten on the next run.
147739
+ // surfaces opencode subagent activity that the CLI's run-loop discards. see
147740
+ // action/agents/opencodePlugin.ts in pullfrog/app for why this exists. lives
147741
+ // inside the per-run tmpdir (XDG_CONFIG_HOME/opencode/plugin/), never inside
147742
+ // the user's working tree.
147743
+
147744
+ const PULLFROG_BUS_EVENT_TYPE = ${JSON.stringify(PULLFROG_BUS_EVENT_TYPE)};
147745
+
147746
+ // the first sessionID we see on a message.part.updated event is the
147747
+ // orchestrator \u2014 opencode's run command creates exactly one top-level session
147748
+ // before any subagent is dispatched, and the user-prompt text part fires
147749
+ // before the first task tool_use. we lock that sessionID in here and use it
147750
+ // to filter: the orchestrator's events are already streamed by the CLI's
147751
+ // run-loop, so we only forward (a) all subagent events, and (b) the
147752
+ // orchestrator's task tool dispatches at status="running". the CLI only
147753
+ // emits task tool_use at status=completed (after the subagent finishes), so
147754
+ // without the early announce the parent's labeler binds subagent sessions
147755
+ // before recordTaskDispatch fires and the lens label is lost.
147756
+ let orchestratorSessionID: string | undefined;
147757
+
147758
+ function isOrchestratorTaskDispatch(part: {
147759
+ type?: string;
147760
+ tool?: string;
147761
+ state?: { status?: string };
147762
+ }): boolean {
147763
+ if (part.type !== "tool") return false;
147764
+ if (part.tool !== "task") return false;
147765
+ // only forward at status="running" (not "pending"). at pending the
147766
+ // state.input is still {} \u2014 the orchestrator has emitted the part shell
147767
+ // but the LLM hasn't filled in description/subagent_type/prompt yet. by
147768
+ // running, input is populated and recordTaskDispatch can derive the lens
147769
+ // label correctly.
147770
+ return part.state?.status === "running";
147771
+ }
147772
+
147773
+ export default async function pullfrogEventsPlugin() {
147774
+ return {
147775
+ event: async (input: {
147776
+ event: {
147777
+ type: string;
147778
+ properties?: {
147779
+ part?: {
147780
+ sessionID?: string;
147781
+ type?: string;
147782
+ tool?: string;
147783
+ state?: { status?: string };
147784
+ };
147785
+ };
147786
+ };
147787
+ }) => {
147788
+ const event = input.event;
147789
+ if (!event || typeof event !== "object") return;
147790
+ if (event.type !== "message.part.updated") return;
147791
+ const part = event.properties?.part;
147792
+ const sessionID = part?.sessionID;
147793
+ if (typeof sessionID !== "string" || sessionID.length === 0) return;
147794
+ if (orchestratorSessionID === undefined) orchestratorSessionID = sessionID;
147795
+
147796
+ if (sessionID === orchestratorSessionID) {
147797
+ // skip orchestrator events EXCEPT early task dispatches.
147798
+ if (!part || !isOrchestratorTaskDispatch(part)) return;
147799
+ }
147800
+
147801
+ try {
147802
+ const line = JSON.stringify({
147803
+ type: PULLFROG_BUS_EVENT_TYPE,
147804
+ bus_event: event,
147805
+ });
147806
+ process.stdout.write(line + "\\n");
147807
+ } catch {
147808
+ // a circular reference or BigInt etc. would throw; swallow rather
147809
+ // than letting a single bad event take down the plugin.
147810
+ }
147811
+ },
147812
+ };
147813
+ }
147814
+ `;
147815
+
147816
+ // agents/opencode.ts
147751
147817
  async function installOpencodeCli() {
147752
147818
  return await installFromNpmTarball({
147753
147819
  packageName: "opencode-ai",
@@ -147849,9 +147915,6 @@ async function runOpenCode(params) {
147849
147915
  const taskDispatchByCallID = /* @__PURE__ */ new Map();
147850
147916
  const pendingTaskDispatches = [];
147851
147917
  const knownNonTaskCallIDs = /* @__PURE__ */ new Set();
147852
- function isSubagentInFlight() {
147853
- return taskDispatchByCallID.size > 0 || pendingTaskDispatches.length > 0;
147854
- }
147855
147918
  function emitSubagentFinished(dispatch, status, output2, matchKind) {
147856
147919
  const subagentDuration = performance7.now() - dispatch.startedAt;
147857
147920
  const outputStr = typeof output2 === "string" ? output2 : "";
@@ -147970,18 +148033,20 @@ async function runOpenCode(params) {
147970
148033
  return;
147971
148034
  }
147972
148035
  if (toolName === "task") {
147973
- const taskInput = event.part?.state?.input ?? {};
147974
- const dispatchedLabel = labeler.recordTaskDispatch(taskInput);
147975
- const dispatch = {
147976
- label: dispatchedLabel,
147977
- startedAt: performance7.now(),
147978
- toolUseCallID: toolId
147979
- };
147980
- taskDispatchByCallID.set(toolId, dispatch);
147981
- pendingTaskDispatches.push(dispatch);
147982
- log.info(
147983
- `\xBB dispatching subagent: ${dispatchedLabel}` + (taskInput.subagent_type ? ` (subagent_type=${taskInput.subagent_type})` : "")
147984
- );
148036
+ if (!taskDispatchByCallID.has(toolId)) {
148037
+ const taskInput = event.part?.state?.input ?? {};
148038
+ const dispatchedLabel = labeler.recordTaskDispatch(taskInput);
148039
+ const dispatch = {
148040
+ label: dispatchedLabel,
148041
+ startedAt: performance7.now(),
148042
+ toolUseCallID: toolId
148043
+ };
148044
+ taskDispatchByCallID.set(toolId, dispatch);
148045
+ pendingTaskDispatches.push(dispatch);
148046
+ log.info(
148047
+ `\xBB dispatching subagent: ${dispatchedLabel}` + (taskInput.subagent_type ? ` (subagent_type=${taskInput.subagent_type})` : "")
148048
+ );
148049
+ }
147985
148050
  } else {
147986
148051
  knownNonTaskCallIDs.add(toolId);
147987
148052
  }
@@ -148002,6 +148067,10 @@ async function runOpenCode(params) {
148002
148067
  if (event.part?.state?.status === "completed" && event.part.state.output) {
148003
148068
  log.debug(withLabel(label, ` output: ${event.part.state.output}`));
148004
148069
  }
148070
+ if (event.part?.state?.status === "error") {
148071
+ const errorMsg = event.part.state.output ?? "(no error message)";
148072
+ log.info(withLabel(label, `\xBB tool call failed: ${errorMsg}`));
148073
+ }
148005
148074
  if (toolName.includes("report_progress") && params.todoTracker) {
148006
148075
  log.debug("\xBB report_progress detected, disabling todo tracking");
148007
148076
  params.todoTracker.cancel();
@@ -148088,6 +148157,53 @@ async function runOpenCode(params) {
148088
148157
  tokensLogged = true;
148089
148158
  }
148090
148159
  }
148160
+ },
148161
+ [PULLFROG_BUS_EVENT_TYPE]: async (event) => {
148162
+ const busEvent = event.bus_event;
148163
+ if (!busEvent || busEvent.type !== "message.part.updated") return;
148164
+ const part = busEvent.properties?.part;
148165
+ if (!part || typeof part.sessionID !== "string") return;
148166
+ const sessionID = part.sessionID;
148167
+ const partType = part.type;
148168
+ if (partType === "tool") {
148169
+ const status = part.state?.status;
148170
+ const partWithToolFields = part;
148171
+ const isOrchestratorTaskDispatch = partWithToolFields.tool === "task" && status === "running";
148172
+ if (isOrchestratorTaskDispatch) {
148173
+ const callID = partWithToolFields.callID;
148174
+ if (typeof callID === "string" && !taskDispatchByCallID.has(callID)) {
148175
+ const taskInput = partWithToolFields.state?.input ?? {};
148176
+ const dispatchedLabel = labeler.recordTaskDispatch(taskInput);
148177
+ const dispatch = {
148178
+ label: dispatchedLabel,
148179
+ startedAt: performance7.now(),
148180
+ toolUseCallID: callID
148181
+ };
148182
+ taskDispatchByCallID.set(callID, dispatch);
148183
+ pendingTaskDispatches.push(dispatch);
148184
+ log.info(
148185
+ `\xBB dispatching subagent: ${dispatchedLabel}` + (taskInput.subagent_type ? ` (subagent_type=${taskInput.subagent_type})` : "")
148186
+ );
148187
+ }
148188
+ return;
148189
+ }
148190
+ if (status !== "completed" && status !== "error") return;
148191
+ await handlers2.tool_use({
148192
+ type: "tool_use",
148193
+ sessionID,
148194
+ part
148195
+ });
148196
+ return;
148197
+ }
148198
+ if (partType === "step-start" || partType === "step-finish") return;
148199
+ if (partType === "text" && part.time?.end !== void 0) {
148200
+ await handlers2.text({
148201
+ type: "text",
148202
+ sessionID,
148203
+ part
148204
+ });
148205
+ return;
148206
+ }
148091
148207
  }
148092
148208
  };
148093
148209
  const recentStderr = [];
@@ -148111,13 +148227,13 @@ async function runOpenCode(params) {
148111
148227
  // never fires — producing zombie runs. detached + killGroup nukes the
148112
148228
  // whole tree.
148113
148229
  killGroup: true,
148114
- // suspend the inner activity timer while a `task` subagent is in flight.
148115
- // opencode's task tool encapsulates subagent execution in-process the
148116
- // subagent's internal events don't surface on the parent NDJSON stream,
148117
- // so without this the 5min timeout would falsely fire mid-subagent.
148118
- // suspend/resume is preferable to a heartbeat because there's no race
148119
- // between a periodic tick and a subagent finishing between ticks.
148120
- isPausedExternally: isSubagentInFlight,
148230
+ // NB: we used to pass `isPausedExternally: isSubagentInFlight` to suspend
148231
+ // the activity timer during subagent dispatches. unnecessary now that
148232
+ // our injected plugin (action/agents/opencodePlugin.ts) re-emits
148233
+ // subagent `message.part.updated` events on opencode's stdout those
148234
+ // arrive at child.stdout here, fire updateActivity(), and reset
148235
+ // lastActivityTime naturally. verified empirically in PR #634
148236
+ // (~3.3 plugin events/sec during a typical subagent run).
148121
148237
  onStdout: async (chunk) => {
148122
148238
  const text = chunk.toString();
148123
148239
  output += text;
@@ -148272,6 +148388,12 @@ var opencode = agent({
148272
148388
  XDG_CONFIG_HOME: join11(ctx.tmpdir, ".config")
148273
148389
  };
148274
148390
  mkdirSync5(join11(homeEnv.XDG_CONFIG_HOME, "opencode"), { recursive: true });
148391
+ const opencodePluginDir = join11(homeEnv.XDG_CONFIG_HOME, "opencode", "plugin");
148392
+ mkdirSync5(opencodePluginDir, { recursive: true });
148393
+ writeFileSync8(
148394
+ join11(opencodePluginDir, PULLFROG_OPENCODE_PLUGIN_FILENAME),
148395
+ PULLFROG_OPENCODE_PLUGIN_SOURCE
148396
+ );
148275
148397
  const agentBrowserVersion = getDevDependencyVersion("agent-browser");
148276
148398
  addSkill({
148277
148399
  ref: `vercel-labs/agent-browser@v${agentBrowserVersion}`,
@@ -148314,7 +148436,7 @@ var opencode = agent({
148314
148436
  stopScript: ctx.stopScript,
148315
148437
  summaryFilePath: ctx.summaryFilePath,
148316
148438
  summarySeed: ctx.summarySeed,
148317
- reflectionPrompt: buildLearningsReflectionPrompt("opencode"),
148439
+ reflectionPrompt: ctx.learningsFilePath ? buildLearningsReflectionPrompt(ctx.learningsFilePath) : void 0,
148318
148440
  resume: async (c2) => runOpenCode({
148319
148441
  ...runParams,
148320
148442
  args: [...baseArgs, "--continue", c2.prompt]
@@ -152527,7 +152649,7 @@ ${ctx.error}` : ctx.error;
152527
152649
 
152528
152650
  // utils/gitAuthServer.ts
152529
152651
  import { randomUUID as randomUUID3 } from "node:crypto";
152530
- import { writeFileSync as writeFileSync8 } from "node:fs";
152652
+ import { writeFileSync as writeFileSync9 } from "node:fs";
152531
152653
  import { createServer as createServer2 } from "node:http";
152532
152654
  import { join as join13 } from "node:path";
152533
152655
  var CODE_TTL_MS = 5 * 60 * 1e3;
@@ -152616,7 +152738,7 @@ async function startGitAuthServer(tmpdir3) {
152616
152738
  `try{require("fs").unlinkSync("${scriptPath.replace(/\\/g, "\\\\")}")}catch(e){}`,
152617
152739
  `})}).on("error",function(){process.exit(1)})}`
152618
152740
  ].join("\n");
152619
- writeFileSync8(scriptPath, content, { mode: 448 });
152741
+ writeFileSync9(scriptPath, content, { mode: 448 });
152620
152742
  return scriptPath;
152621
152743
  }
152622
152744
  async function close() {
@@ -152890,9 +153012,9 @@ function buildPromptContext(ctx) {
152890
153012
  };
152891
153013
  }
152892
153014
  function assembleFullPrompt(ctx) {
152893
- const learningsSection = ctx.learnings ? `************* LEARNINGS *************
153015
+ const learningsSection = ctx.learningsFilePath ? `************* LEARNINGS *************
152894
153016
 
152895
- ${ctx.learnings}` : "";
153017
+ 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.` : "";
152896
153018
  const runtimeSection = `************* RUNTIME *************
152897
153019
 
152898
153020
  ${ctx.runtime}`;
@@ -152919,8 +153041,8 @@ function resolveInstructions(ctx) {
152919
153041
  if (eventContext)
152920
153042
  tocEntries.push({ label: "EVENT CONTEXT", description: "related PR/issue data" });
152921
153043
  tocEntries.push({ label: "SYSTEM", description: "persona, security, tools, workflow rules" });
152922
- if (pctx.learnings)
152923
- tocEntries.push({ label: "LEARNINGS", description: "repo-specific knowledge" });
153044
+ if (pctx.learningsFilePath)
153045
+ tocEntries.push({ label: "LEARNINGS", description: "repo-specific knowledge file path" });
152924
153046
  tocEntries.push({ label: "RUNTIME", description: "environment metadata" });
152925
153047
  const toc = buildToc(tocEntries);
152926
153048
  const full = assembleFullPrompt({
@@ -152929,7 +153051,7 @@ function resolveInstructions(ctx) {
152929
153051
  procedure,
152930
153052
  eventContext,
152931
153053
  system,
152932
- learnings: pctx.learnings,
153054
+ learningsFilePath: pctx.learningsFilePath,
152933
153055
  runtime: pctx.runtime
152934
153056
  });
152935
153057
  const event = [pctx.eventTitle, pctx.eventMetadata].filter(Boolean).join("\n\n---\n\n");
@@ -152943,6 +153065,32 @@ function resolveInstructions(ctx) {
152943
153065
  };
152944
153066
  }
152945
153067
 
153068
+ // utils/learnings.ts
153069
+ import { mkdir, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
153070
+ import { dirname as dirname4, join as join14 } from "node:path";
153071
+ var LEARNINGS_FILE_NAME = "pullfrog-learnings.md";
153072
+ var MAX_LEARNINGS_LENGTH = 1e4;
153073
+ function learningsFilePath(tmpdir3) {
153074
+ return join14(tmpdir3, LEARNINGS_FILE_NAME);
153075
+ }
153076
+ async function seedLearningsFile(params) {
153077
+ const path3 = learningsFilePath(params.tmpdir);
153078
+ await mkdir(dirname4(path3), { recursive: true });
153079
+ await writeFile2(path3, params.current ?? "", "utf8");
153080
+ return path3;
153081
+ }
153082
+ async function readLearningsFile(path3) {
153083
+ let raw2;
153084
+ try {
153085
+ raw2 = await readFile2(path3, "utf8");
153086
+ } catch {
153087
+ return null;
153088
+ }
153089
+ const trimmed = raw2.trim();
153090
+ if (trimmed.length > MAX_LEARNINGS_LENGTH) return trimmed.slice(0, MAX_LEARNINGS_LENGTH);
153091
+ return trimmed;
153092
+ }
153093
+
152946
153094
  // utils/normalizeEnv.ts
152947
153095
  function maskValue(value2) {
152948
153096
  if (value2 && typeof value2 === "string" && value2.trim().length > 0) {
@@ -153118,8 +153266,8 @@ function resolvePayload(resolvedPromptInput, repoSettings) {
153118
153266
  }
153119
153267
 
153120
153268
  // utils/prSummary.ts
153121
- import { mkdir, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
153122
- import { dirname as dirname4, join as join14 } from "node:path";
153269
+ import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
153270
+ import { dirname as dirname5, join as join15 } from "node:path";
153123
153271
  var SUMMARY_FILE_NAME = "pullfrog-summary.md";
153124
153272
  var SUMMARY_SCAFFOLD = `# PR summary
153125
153273
 
@@ -153129,19 +153277,19 @@ var SUMMARY_SCAFFOLD = `# PR summary
153129
153277
  var MIN_SNAPSHOT_LENGTH = 60;
153130
153278
  var MAX_SNAPSHOT_LENGTH = 32768;
153131
153279
  function summaryFilePath(tmpdir3) {
153132
- return join14(tmpdir3, SUMMARY_FILE_NAME);
153280
+ return join15(tmpdir3, SUMMARY_FILE_NAME);
153133
153281
  }
153134
153282
  async function seedSummaryFile(params) {
153135
153283
  const path3 = summaryFilePath(params.tmpdir);
153136
- await mkdir(dirname4(path3), { recursive: true });
153284
+ await mkdir2(dirname5(path3), { recursive: true });
153137
153285
  const seed = params.previousSnapshot && params.previousSnapshot.trim().length >= MIN_SNAPSHOT_LENGTH ? params.previousSnapshot : SUMMARY_SCAFFOLD;
153138
- await writeFile2(path3, seed, "utf8");
153286
+ await writeFile3(path3, seed, "utf8");
153139
153287
  return path3;
153140
153288
  }
153141
153289
  async function readSummaryFile(path3) {
153142
153290
  let raw2;
153143
153291
  try {
153144
- raw2 = await readFile2(path3, "utf8");
153292
+ raw2 = await readFile3(path3, "utf8");
153145
153293
  } catch {
153146
153294
  return null;
153147
153295
  }
@@ -153359,9 +153507,9 @@ async function resolveRunContextData(params) {
153359
153507
  import { execFileSync as execFileSync5, execSync as execSync3 } from "node:child_process";
153360
153508
  import { mkdtempSync } from "node:fs";
153361
153509
  import { tmpdir as tmpdir2 } from "node:os";
153362
- import { join as join15 } from "node:path";
153510
+ import { join as join16 } from "node:path";
153363
153511
  function createTempDirectory() {
153364
- const sharedTempDir = mkdtempSync(join15(tmpdir2(), "pullfrog-"));
153512
+ const sharedTempDir = mkdtempSync(join16(tmpdir2(), "pullfrog-"));
153365
153513
  process.env.PULLFROG_TEMP_DIR = sharedTempDir;
153366
153514
  log.info(`\xBB created temp dir at ${sharedTempDir}`);
153367
153515
  return sharedTempDir;
@@ -153763,15 +153911,12 @@ function formatTransientErrorSummary(error49, owner) {
153763
153911
  }
153764
153912
  async function mintProxyKey(ctx) {
153765
153913
  try {
153766
- process.env.ACTIONS_ID_TOKEN_REQUEST_URL = ctx.oidcCredentials.requestUrl;
153767
- process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = ctx.oidcCredentials.requestToken;
153768
- const oidcToken = await core6.getIDToken("pullfrog-api");
153769
- delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
153770
- delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
153914
+ const headers = await buildProxyTokenHeaders(ctx);
153915
+ if (!headers) return null;
153771
153916
  const response = await apiFetch({
153772
153917
  path: "/api/proxy-token",
153773
153918
  method: "POST",
153774
- headers: { Authorization: `Bearer ${oidcToken}` }
153919
+ headers
153775
153920
  });
153776
153921
  if (response.status === 402) {
153777
153922
  const body = await response.json().catch(() => null);
@@ -153803,15 +153948,30 @@ async function mintProxyKey(ctx) {
153803
153948
  delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
153804
153949
  }
153805
153950
  }
153951
+ async function buildProxyTokenHeaders(ctx) {
153952
+ if (ctx.oidcCredentials) {
153953
+ process.env.ACTIONS_ID_TOKEN_REQUEST_URL = ctx.oidcCredentials.requestUrl;
153954
+ process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = ctx.oidcCredentials.requestToken;
153955
+ const oidcToken = await core6.getIDToken("pullfrog-api");
153956
+ delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
153957
+ delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
153958
+ return { Authorization: `Bearer ${oidcToken}` };
153959
+ }
153960
+ if (isLocalApiUrl()) {
153961
+ log.info(`\xBB proxy: dev bypass (x-dev-repo) for ${ctx.repo.owner}/${ctx.repo.name}`);
153962
+ return { "x-dev-repo": `${ctx.repo.owner}/${ctx.repo.name}` };
153963
+ }
153964
+ return null;
153965
+ }
153806
153966
  async function resolveProxyModel(ctx) {
153807
153967
  if (process.env.PULLFROG_MODEL?.trim()) return;
153808
153968
  const needsProxy = isInfraCovered({ isOss: ctx.oss, plan: ctx.plan }) && ctx.proxyModel;
153809
153969
  if (!needsProxy) return;
153810
- if (!ctx.oidcCredentials) {
153970
+ if (!ctx.oidcCredentials && !isLocalApiUrl()) {
153811
153971
  log.warning("\xBB proxy requested but no OIDC credentials available \u2014 skipping");
153812
153972
  return;
153813
153973
  }
153814
- const key = await mintProxyKey({ oidcCredentials: ctx.oidcCredentials });
153974
+ const key = await mintProxyKey({ oidcCredentials: ctx.oidcCredentials, repo: ctx.repo });
153815
153975
  if (!key) return;
153816
153976
  process.env.OPENROUTER_API_KEY = key;
153817
153977
  core6.setSecret(key);
@@ -153835,6 +153995,45 @@ async function fetchPreviousSnapshot(ctx, prNumber) {
153835
153995
  return null;
153836
153996
  }
153837
153997
  }
153998
+ async function persistLearnings(ctx) {
153999
+ const filePath = ctx.toolState.learningsFilePath;
154000
+ if (!filePath) return;
154001
+ if (ctx.toolState.learningsPersistAttempted) return;
154002
+ ctx.toolState.learningsPersistAttempted = true;
154003
+ const current = await readLearningsFile(filePath);
154004
+ if (current === null) {
154005
+ log.debug(`learnings tmpfile missing or unreadable at ${filePath} \u2014 skipping persist`);
154006
+ return;
154007
+ }
154008
+ const seed = ctx.toolState.learningsSeed?.trim() ?? "";
154009
+ if (current === seed) {
154010
+ log.debug("learnings tmpfile unchanged from seed \u2014 skipping persist");
154011
+ return;
154012
+ }
154013
+ try {
154014
+ const response = await apiFetch({
154015
+ path: `/api/repo/${ctx.repo.owner}/${ctx.repo.name}/learnings`,
154016
+ method: "PATCH",
154017
+ headers: {
154018
+ authorization: `Bearer ${ctx.apiToken}`,
154019
+ "content-type": "application/json"
154020
+ },
154021
+ body: JSON.stringify({
154022
+ learnings: current,
154023
+ model: ctx.toolState.model
154024
+ }),
154025
+ signal: AbortSignal.timeout(1e4)
154026
+ });
154027
+ if (!response.ok) {
154028
+ const error49 = await response.text().catch(() => "(no body)");
154029
+ log.debug(`learnings persist failed (${response.status}): ${error49}`);
154030
+ return;
154031
+ }
154032
+ log.info("\xBB learnings updated");
154033
+ } catch (err) {
154034
+ log.debug(`learnings persist failed: ${err instanceof Error ? err.message : String(err)}`);
154035
+ }
154036
+ }
153838
154037
  async function persistSummary(ctx) {
153839
154038
  const filePath = ctx.toolState.summaryFilePath;
153840
154039
  if (!filePath) return;
@@ -153916,7 +154115,8 @@ async function main() {
153916
154115
  oss: runContext.oss,
153917
154116
  plan: runContext.plan,
153918
154117
  proxyModel: runContext.proxyModel,
153919
- oidcCredentials
154118
+ oidcCredentials,
154119
+ repo: runContext.repo
153920
154120
  });
153921
154121
  } catch (error49) {
153922
154122
  if (error49 instanceof BillingError) {
@@ -154019,12 +154219,32 @@ async function main() {
154019
154219
  toolContext.mcpServerUrl = mcpHttpServer.url;
154020
154220
  log.info(`\xBB MCP server started at ${mcpHttpServer.url}`);
154021
154221
  timer.checkpoint("mcpServer");
154222
+ try {
154223
+ const learningsPath = await seedLearningsFile({
154224
+ tmpdir: tmpdir3,
154225
+ current: runContext.repoSettings.learnings
154226
+ });
154227
+ toolState.learningsFilePath = learningsPath;
154228
+ try {
154229
+ toolState.learningsSeed = await readFile4(learningsPath, "utf8");
154230
+ } catch {
154231
+ }
154232
+ log.info(
154233
+ `\xBB learnings seeded at ${learningsPath} (existing=${runContext.repoSettings.learnings ? "yes" : "no"})`
154234
+ );
154235
+ const ctxForExit = toolContext;
154236
+ onExitSignal(() => persistLearnings(ctxForExit));
154237
+ } catch (err) {
154238
+ log.warning(
154239
+ `\xBB learnings seed failed: ${err instanceof Error ? err.message : String(err)} \u2014 continuing without learnings file`
154240
+ );
154241
+ }
154022
154242
  if (payload.generateSummary && payload.event.is_pr && payload.event.issue_number) {
154023
154243
  const previousSnapshot = await fetchPreviousSnapshot(toolContext, payload.event.issue_number);
154024
154244
  const filePath = await seedSummaryFile({ tmpdir: tmpdir3, previousSnapshot });
154025
154245
  toolState.summaryFilePath = filePath;
154026
154246
  try {
154027
- toolState.summarySeed = await readFile3(filePath, "utf8");
154247
+ toolState.summarySeed = await readFile4(filePath, "utf8");
154028
154248
  } catch {
154029
154249
  }
154030
154250
  log.info(
@@ -154048,7 +154268,7 @@ async function main() {
154048
154268
  modes: modes2,
154049
154269
  agentId,
154050
154270
  outputSchema,
154051
- learnings: runContext.repoSettings.learnings
154271
+ learningsFilePath: toolState.learningsFilePath ?? null
154052
154272
  });
154053
154273
  const logParts = [
154054
154274
  instructions.eventInstructions ? `EVENT-LEVEL INSTRUCTIONS:
@@ -154064,7 +154284,7 @@ ${instructions.user}` : null,
154064
154284
  log.info(instructions.full);
154065
154285
  });
154066
154286
  if (agentId === "opencode") {
154067
- const pluginDir = join16(process.cwd(), ".opencode", "plugin");
154287
+ const pluginDir = join17(process.cwd(), ".opencode", "plugin");
154068
154288
  const hasPlugins = existsSync7(pluginDir) && readdirSync(pluginDir).some((f) => /\.[jt]sx?$/.test(f));
154069
154289
  if (hasPlugins && toolState.dependencyInstallation?.promise) {
154070
154290
  log.info(
@@ -154125,6 +154345,7 @@ ${instructions.user}` : null,
154125
154345
  stopScript: runContext.repoSettings.stopScript,
154126
154346
  summaryFilePath: toolState.summaryFilePath,
154127
154347
  summarySeed: toolState.summarySeed,
154348
+ learningsFilePath: toolState.learningsFilePath,
154128
154349
  onActivityTimeout: onInnerActivityTimeout,
154129
154350
  onToolUse: (event) => {
154130
154351
  const wasTracked = recordDiffReadFromToolUse({
@@ -154182,6 +154403,9 @@ ${instructions.user}` : null,
154182
154403
  if (toolContext) {
154183
154404
  await persistSummary(toolContext);
154184
154405
  }
154406
+ if (toolContext) {
154407
+ await persistLearnings(toolContext);
154408
+ }
154185
154409
  if (toolContext && toolState.progressComment && !toolState.finalSummaryWritten) {
154186
154410
  await deleteProgressComment(toolContext).catch((error49) => {
154187
154411
  log.debug(`stranded progress comment cleanup failed: ${error49}`);
@@ -154234,6 +154458,9 @@ ${errorMessage}
154234
154458
  if (toolContext) {
154235
154459
  await persistSummary(toolContext);
154236
154460
  }
154461
+ if (toolContext) {
154462
+ await persistLearnings(toolContext);
154463
+ }
154237
154464
  return {
154238
154465
  success: false,
154239
154466
  error: errorMessage
@@ -154266,7 +154493,7 @@ ${errorMessage}
154266
154493
  }
154267
154494
 
154268
154495
  // commands/gha.ts
154269
- process.env.PATH = `${dirname5(process.execPath)}:${process.env.PATH}`;
154496
+ process.env.PATH = `${dirname6(process.execPath)}:${process.env.PATH}`;
154270
154497
  var STATE_TOKEN = "token";
154271
154498
  async function runMain() {
154272
154499
  try {
@@ -156076,7 +156303,7 @@ async function run2() {
156076
156303
  }
156077
156304
 
156078
156305
  // cli.ts
156079
- var VERSION10 = "0.1.1";
156306
+ var VERSION10 = "0.1.2";
156080
156307
  var bin = basename2(process.argv[1] || "");
156081
156308
  var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
156082
156309
  var rawArgs = process.argv.slice(2);