pullfrog 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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,
@@ -109979,6 +109993,7 @@ async function spawn(options) {
109979
109993
  const startTime = performance3.now();
109980
109994
  let stdoutBuffer = "";
109981
109995
  let stderrBuffer = "";
109996
+ const killGroup = options.killGroup ?? false;
109982
109997
  return new Promise((resolve3, reject) => {
109983
109998
  const child = nodeSpawn(options.cmd, options.args, {
109984
109999
  env: options.env || {
@@ -109986,9 +110001,20 @@ async function spawn(options) {
109986
110001
  HOME: process.env.HOME || ""
109987
110002
  },
109988
110003
  stdio: options.stdio || ["pipe", "pipe", "pipe"],
109989
- cwd: options.cwd || process.cwd()
110004
+ cwd: options.cwd || process.cwd(),
110005
+ detached: killGroup
109990
110006
  });
109991
- trackChild({ child });
110007
+ const killSelf = (signal) => {
110008
+ if (killGroup && child.pid) {
110009
+ try {
110010
+ process.kill(-child.pid, signal);
110011
+ return;
110012
+ } catch {
110013
+ }
110014
+ }
110015
+ child.kill(signal);
110016
+ };
110017
+ trackChild({ child, killGroup });
109992
110018
  let timeoutId;
109993
110019
  let sigkillEscalatorId;
109994
110020
  let activityCheckIntervalId;
@@ -109999,10 +110025,10 @@ async function spawn(options) {
109999
110025
  if (options.timeout) {
110000
110026
  timeoutId = setTimeout(() => {
110001
110027
  isTimedOut = true;
110002
- child.kill("SIGTERM");
110028
+ killSelf("SIGTERM");
110003
110029
  sigkillEscalatorId = setTimeout(() => {
110004
110030
  if (!child.killed) {
110005
- child.kill("SIGKILL");
110031
+ killSelf("SIGKILL");
110006
110032
  }
110007
110033
  }, 5e3);
110008
110034
  }, options.timeout);
@@ -110021,9 +110047,9 @@ async function spawn(options) {
110021
110047
  killedAtIdleMs = idleMs;
110022
110048
  const idleSec = Math.round(idleMs / 1e3);
110023
110049
  log.info(
110024
- `no output for ${idleSec}s from pid=${child.pid} (${options.cmd}), killing process`
110050
+ `no output for ${idleSec}s from pid=${child.pid} (${options.cmd}), killing process${killGroup ? " group" : ""}`
110025
110051
  );
110026
- child.kill("SIGKILL");
110052
+ killSelf("SIGKILL");
110027
110053
  clearInterval(activityCheckIntervalId);
110028
110054
  try {
110029
110055
  options.onActivityTimeout?.();
@@ -142532,7 +142558,7 @@ var import_semver = __toESM(require_semver2(), 1);
142532
142558
  // package.json
142533
142559
  var package_default = {
142534
142560
  name: "pullfrog",
142535
- version: "0.1.0",
142561
+ version: "0.1.2",
142536
142562
  type: "module",
142537
142563
  bin: {
142538
142564
  pullfrog: "dist/cli.mjs",
@@ -143476,6 +143502,10 @@ ${integrateStep}
143476
143502
  if (!pushed) {
143477
143503
  throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
143478
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
+ );
143479
143509
  return {
143480
143510
  success: true,
143481
143511
  branch,
@@ -143624,6 +143654,7 @@ function DeleteBranchTool(ctx) {
143624
143654
  await $git("push", ["origin", "--delete", `refs/heads/${params.branchName}`], {
143625
143655
  token: ctx.gitToken
143626
143656
  });
143657
+ log.info(`\xBB deleted branch ${params.branchName}`);
143627
143658
  return { success: true, deleted: params.branchName };
143628
143659
  })
143629
143660
  });
@@ -143649,6 +143680,7 @@ function PushTagsTool(ctx) {
143649
143680
  await $git("push", pushArgs, {
143650
143681
  token: ctx.gitToken
143651
143682
  });
143683
+ log.info(`\xBB pushed tag ${params.tag}`);
143652
143684
  return { success: true, tag: params.tag };
143653
143685
  })
143654
143686
  });
@@ -143973,6 +144005,7 @@ function CreatePullRequestReviewTool(ctx) {
143973
144005
  }
143974
144006
  const reviewId = result.data.id;
143975
144007
  const reviewNodeId = result.data.node_id;
144008
+ log.info(`\xBB created review ${reviewId} on pull request #${pull_number}`);
143976
144009
  const actuallyReviewedSha = ctx.toolState.checkoutSha ?? params.commit_id;
143977
144010
  ctx.toolState.review = {
143978
144011
  id: reviewId,
@@ -144837,6 +144870,7 @@ function IssueTool(ctx) {
144837
144870
  labels: params.labels ?? [],
144838
144871
  assignees: params.assignees ?? []
144839
144872
  });
144873
+ log.info(`\xBB created issue #${result.data.number} (id ${result.data.id})`);
144840
144874
  const nodeId = result.data.node_id;
144841
144875
  if (typeof nodeId === "string" && nodeId.length > 0) {
144842
144876
  await patchWorkflowRunFields(ctx, {
@@ -145028,6 +145062,7 @@ function AddLabelsTool(ctx) {
145028
145062
  issue_number,
145029
145063
  labels
145030
145064
  });
145065
+ log.info(`\xBB added labels [${labels.join(", ")}] to issue #${issue_number}`);
145031
145066
  return {
145032
145067
  success: true,
145033
145068
  labels: result.data.map((label) => label.name)
@@ -145036,40 +145071,6 @@ function AddLabelsTool(ctx) {
145036
145071
  });
145037
145072
  }
145038
145073
 
145039
- // mcp/learnings.ts
145040
- var UpdateLearningsParams = type({
145041
- learnings: type.string.describe(
145042
- "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."
145043
- )
145044
- });
145045
- function UpdateLearningsTool(ctx) {
145046
- return tool({
145047
- name: "update_learnings",
145048
- 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.",
145049
- parameters: UpdateLearningsParams,
145050
- execute: execute(async (params) => {
145051
- const response = await apiFetch({
145052
- path: `/api/repo/${ctx.repo.owner}/${ctx.repo.name}/learnings`,
145053
- method: "PATCH",
145054
- headers: {
145055
- authorization: `Bearer ${ctx.apiToken}`,
145056
- "content-type": "application/json"
145057
- },
145058
- body: JSON.stringify({
145059
- learnings: params.learnings,
145060
- model: ctx.toolState.model
145061
- }),
145062
- signal: AbortSignal.timeout(1e4)
145063
- });
145064
- if (!response.ok) {
145065
- const error49 = await response.text();
145066
- throw new Error(`failed to update learnings: ${error49}`);
145067
- }
145068
- return { success: true };
145069
- })
145070
- });
145071
- }
145072
-
145073
145074
  // mcp/output.ts
145074
145075
  var import_ajv3 = __toESM(require_ajv(), 1);
145075
145076
  var SetOutputParams = type({
@@ -145163,6 +145164,7 @@ function UpdatePullRequestBodyTool(ctx) {
145163
145164
  pull_number: params.pull_number,
145164
145165
  body: bodyWithFooter
145165
145166
  });
145167
+ log.info(`\xBB updated pull request #${result.data.number}`);
145166
145168
  ctx.toolState.wasUpdated = true;
145167
145169
  return {
145168
145170
  success: true,
@@ -145190,6 +145192,7 @@ function CreatePullRequestTool(ctx) {
145190
145192
  base: params.base,
145191
145193
  draft: params.draft ?? false
145192
145194
  });
145195
+ log.info(`\xBB created pull request #${result.data.number} (id ${result.data.id})`);
145193
145196
  const reviewer = ctx.payload.triggerer;
145194
145197
  if (reviewer) {
145195
145198
  try {
@@ -145741,7 +145744,7 @@ function ResolveReviewThreadTool(ctx) {
145741
145744
  threadId: params.thread_id
145742
145745
  });
145743
145746
  const thread = response.resolveReviewThread.thread;
145744
- log.debug(`resolved thread ${thread.id}, isResolved=${thread.isResolved}`);
145747
+ log.info(`\xBB resolved review thread ${thread.id}`);
145745
145748
  return {
145746
145749
  thread_id: thread.id,
145747
145750
  is_resolved: thread.isResolved,
@@ -146213,6 +146216,7 @@ function UploadFileTool(ctx) {
146213
146216
  if (!uploadResponse.ok) {
146214
146217
  throw new Error(`failed to upload file: ${uploadResponse.statusText}`);
146215
146218
  }
146219
+ log.info(`\xBB uploaded file ${publicUrl}`);
146216
146220
  return { success: true, publicUrl, filename, contentLength, contentType };
146217
146221
  })
146218
146222
  });
@@ -146306,8 +146310,7 @@ function buildOrchestratorTools(ctx, outputSchema) {
146306
146310
  PushTagsTool(ctx),
146307
146311
  DeleteBranchTool(ctx),
146308
146312
  CreatePullRequestTool(ctx),
146309
- UpdatePullRequestBodyTool(ctx),
146310
- UpdateLearningsTool(ctx)
146313
+ UpdatePullRequestBodyTool(ctx)
146311
146314
  ];
146312
146315
  }
146313
146316
  async function tryStartMcpServer(ctx, tools, port) {
@@ -146464,9 +146467,6 @@ Rules:
146464
146467
  - Do NOT include a changelog section \u2014 the key changes list serves this purpose
146465
146468
  - Focus on *intent*, not *what* \u2014 the diff already shows what changed
146466
146469
  - Get the file count and commit count from the checkout_pr metadata, not by counting manually`;
146467
- function learningsStep(t2, n) {
146468
- 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.`;
146469
- }
146470
146470
  function computeModes(agentId) {
146471
146471
  const t2 = (toolName) => formatMcpToolRef(agentId, toolName);
146472
146472
  return [
@@ -146522,8 +146522,6 @@ function computeModes(agentId) {
146522
146522
  - create a PR via \`${t2("create_pull_request")}\`
146523
146523
  - call \`${t2("report_progress")}\` with the PR link or the exact error if push/PR failed
146524
146524
 
146525
- ${learningsStep(t2, 6)}
146526
-
146527
146525
  ### Notes
146528
146526
 
146529
146527
  For simple, well-defined tasks, skip the plan phase and go straight to build.`
@@ -146551,9 +146549,7 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
146551
146549
  - confirm a clean working tree, then push via \`${t2("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*)
146552
146550
  - reply to each comment using \`${t2("reply_to_review_comment")}\`
146553
146551
  - resolve addressed threads via \`${t2("resolve_review_thread")}\`
146554
- - call \`${t2("report_progress")}\` with a brief summary (or the exact push error if push failed)
146555
-
146556
- ${learningsStep(t2, 6)}`
146552
+ - call \`${t2("report_progress")}\` with a brief summary (or the exact push error if push failed)`
146557
146553
  },
146558
146554
  // Review and IncrementalReview use the multi-lens orchestrator pattern
146559
146555
  // (canonical source: .claude/commands/anneal.md). The orchestrator does
@@ -146724,9 +146720,7 @@ ${PR_SUMMARY_FORMAT}`
146724
146720
 
146725
146721
  2. Produce a structured, actionable plan with clear milestones.
146726
146722
 
146727
- 3. Call \`${t2("report_progress")}\` with the plan.
146728
-
146729
- ${learningsStep(t2, 4)}`
146723
+ 3. Call \`${t2("report_progress")}\` with the plan.`
146730
146724
  },
146731
146725
  {
146732
146726
  name: "Fix",
@@ -146748,9 +146742,7 @@ ${learningsStep(t2, 4)}`
146748
146742
 
146749
146743
  5. Finalize:
146750
146744
  - confirm a clean working tree, then push via \`${t2("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*)
146751
- - call \`${t2("report_progress")}\` with the diagnosis and fix summary (or the exact push error if push failed)
146752
-
146753
- ${learningsStep(t2, 6)}`
146745
+ - call \`${t2("report_progress")}\` with the diagnosis and fix summary (or the exact push error if push failed)`
146754
146746
  },
146755
146747
  {
146756
146748
  name: "ResolveConflicts",
@@ -146794,9 +146786,7 @@ ${learningsStep(t2, 6)}`
146794
146786
  3. Finalize:
146795
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).
146796
146788
  - call \`${t2("report_progress")}\` once with results \u2014 include exact tool errors if push or PR creation failed
146797
- - if the task involved labeling, commenting, or other GitHub operations, perform those directly
146798
-
146799
- ${learningsStep(t2, 4)}`
146789
+ - if the task involved labeling, commenting, or other GitHub operations, perform those directly`
146800
146790
  }
146801
146791
  ];
146802
146792
  }
@@ -146896,6 +146886,17 @@ async function installFromNpmTarball(params) {
146896
146886
  // utils/providerErrors.ts
146897
146887
  var statusKey = `\\b(?:status[_ ]?code|http[_ ]?status|status)["']?\\s*[:=]\\s*["']?`;
146898
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)" },
146899
146900
  { regex: new RegExp(`${statusKey}429\\b`, "i"), label: "rate limited (429)" },
146900
146901
  { regex: new RegExp(`${statusKey}500\\b`, "i"), label: "provider 500 error" },
146901
146902
  { regex: new RegExp(`${statusKey}503\\b`, "i"), label: "provider unavailable (503)" },
@@ -146959,7 +146960,7 @@ function installBundledSkills(params) {
146959
146960
  writeFileSync6(join9(skillDir, "SKILL.md"), content);
146960
146961
  }
146961
146962
  }
146962
- log.info(`installed bundled skills: ${BUNDLED_SKILL_NAMES.join(", ")}`);
146963
+ log.success(`installed bundled skills: ${BUNDLED_SKILL_NAMES.join(", ")}`);
146963
146964
  }
146964
146965
  function addSkill(params) {
146965
146966
  const result = spawnSync5(
@@ -146984,7 +146985,7 @@ function addSkill(params) {
146984
146985
  }
146985
146986
  );
146986
146987
  if (result.status === 0) {
146987
- log.info(`installed ${params.skill} skill (${params.agent})`);
146988
+ log.success(`installed ${params.skill} skill (${params.agent})`);
146988
146989
  } else {
146989
146990
  const stderr = (result.stderr?.toString() || "").trim();
146990
146991
  const errorMsg = result.error ? result.error.message : stderr;
@@ -147118,18 +147119,17 @@ function buildPostRunPrompt(issues) {
147118
147119
  if (issues.summaryStale) parts.push(buildSummaryStalePrompt(issues.summaryStale.filePath));
147119
147120
  return parts.join("\n\n---\n\n");
147120
147121
  }
147121
- function buildLearningsReflectionPrompt(agentId) {
147122
- const t2 = (name) => formatMcpToolRef(agentId, name);
147122
+ function buildLearningsReflectionPrompt(filePath) {
147123
147123
  return [
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 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?`,
147125
147125
  "",
147126
- `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.`,
147127
147127
  "",
147128
- `rules:`,
147129
- `- only call \`${t2("update_learnings")}\` when the finding is high-confidence and broadly useful. skip if unsure, speculative, or one-off.`,
147130
- `- pass the FULL merged list: existing learnings from the original prompt + your new discoveries. one fact per bullet, lines starting with \`- \`.`,
147131
- `- deduplicate, and drop bullets that are clearly wrong or no longer relevant to the current codebase.`,
147132
- `- 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.`
147133
147133
  ].join("\n");
147134
147134
  }
147135
147135
  async function runPostRunRetryLoop(params) {
@@ -147467,6 +147467,12 @@ async function runClaude(params) {
147467
147467
  activityTimeout: 3e5,
147468
147468
  onActivityTimeout: params.onActivityTimeout,
147469
147469
  stdio: ["ignore", "pipe", "pipe"],
147470
+ // run claude in its own process group so SIGKILL on activity timeout /
147471
+ // outer cancellation reaches any subprocesses it spawns (rg, file
147472
+ // watchers, mcp transports, etc). claude itself is a node bundle so
147473
+ // there's no shim-orphan issue like opencode-ai/bin/opencode, but
147474
+ // detached + killGroup is the right default for any agent runtime.
147475
+ killGroup: true,
147470
147476
  onStdout: async (chunk) => {
147471
147477
  const text = chunk.toString();
147472
147478
  output += text;
@@ -147706,7 +147712,7 @@ var claude = agent({
147706
147712
  stopScript: ctx.stopScript,
147707
147713
  summaryFilePath: ctx.summaryFilePath,
147708
147714
  summarySeed: ctx.summarySeed,
147709
- reflectionPrompt: buildLearningsReflectionPrompt("claude"),
147715
+ reflectionPrompt: ctx.learningsFilePath ? buildLearningsReflectionPrompt(ctx.learningsFilePath) : void 0,
147710
147716
  canResume: (r) => Boolean(r.sessionId),
147711
147717
  resume: async (c2) => {
147712
147718
  const sessionId = c2.previousResult.sessionId;
@@ -147722,9 +147728,92 @@ var claude = agent({
147722
147728
 
147723
147729
  // agents/opencode.ts
147724
147730
  import { execFileSync as execFileSync4 } from "node:child_process";
147725
- import { mkdirSync as mkdirSync5 } from "node:fs";
147731
+ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "node:fs";
147726
147732
  import { join as join11 } from "node:path";
147727
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
147728
147817
  async function installOpencodeCli() {
147729
147818
  return await installFromNpmTarball({
147730
147819
  packageName: "opencode-ai",
@@ -147944,18 +148033,20 @@ async function runOpenCode(params) {
147944
148033
  return;
147945
148034
  }
147946
148035
  if (toolName === "task") {
147947
- const taskInput = event.part?.state?.input ?? {};
147948
- const dispatchedLabel = labeler.recordTaskDispatch(taskInput);
147949
- const dispatch = {
147950
- label: dispatchedLabel,
147951
- startedAt: performance7.now(),
147952
- toolUseCallID: toolId
147953
- };
147954
- taskDispatchByCallID.set(toolId, dispatch);
147955
- pendingTaskDispatches.push(dispatch);
147956
- log.info(
147957
- `\xBB dispatching subagent: ${dispatchedLabel}` + (taskInput.subagent_type ? ` (subagent_type=${taskInput.subagent_type})` : "")
147958
- );
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
+ }
147959
148050
  } else {
147960
148051
  knownNonTaskCallIDs.add(toolId);
147961
148052
  }
@@ -147976,6 +148067,10 @@ async function runOpenCode(params) {
147976
148067
  if (event.part?.state?.status === "completed" && event.part.state.output) {
147977
148068
  log.debug(withLabel(label, ` output: ${event.part.state.output}`));
147978
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
+ }
147979
148074
  if (toolName.includes("report_progress") && params.todoTracker) {
147980
148075
  log.debug("\xBB report_progress detected, disabling todo tracking");
147981
148076
  params.todoTracker.cancel();
@@ -148062,6 +148157,53 @@ async function runOpenCode(params) {
148062
148157
  tokensLogged = true;
148063
148158
  }
148064
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
+ }
148065
148207
  }
148066
148208
  };
148067
148209
  const recentStderr = [];
@@ -148078,6 +148220,20 @@ async function runOpenCode(params) {
148078
148220
  activityTimeout: 3e5,
148079
148221
  onActivityTimeout: params.onActivityTimeout,
148080
148222
  stdio: ["ignore", "pipe", "pipe"],
148223
+ // node_modules/opencode-ai/bin/opencode is a Node shim that spawnSyncs
148224
+ // the native opencode-<plat>-<arch> binary with stdio:"inherit". without
148225
+ // a process-group kill, SIGKILL hits only the shim, the native binary
148226
+ // is reparented to PID 1, holds our stdout pipe open, and `child.close`
148227
+ // never fires — producing zombie runs. detached + killGroup nukes the
148228
+ // whole tree.
148229
+ killGroup: true,
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).
148081
148237
  onStdout: async (chunk) => {
148082
148238
  const text = chunk.toString();
148083
148239
  output += text;
@@ -148232,6 +148388,12 @@ var opencode = agent({
148232
148388
  XDG_CONFIG_HOME: join11(ctx.tmpdir, ".config")
148233
148389
  };
148234
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
+ );
148235
148397
  const agentBrowserVersion = getDevDependencyVersion("agent-browser");
148236
148398
  addSkill({
148237
148399
  ref: `vercel-labs/agent-browser@v${agentBrowserVersion}`,
@@ -148274,7 +148436,7 @@ var opencode = agent({
148274
148436
  stopScript: ctx.stopScript,
148275
148437
  summaryFilePath: ctx.summaryFilePath,
148276
148438
  summarySeed: ctx.summarySeed,
148277
- reflectionPrompt: buildLearningsReflectionPrompt("opencode"),
148439
+ reflectionPrompt: ctx.learningsFilePath ? buildLearningsReflectionPrompt(ctx.learningsFilePath) : void 0,
148278
148440
  resume: async (c2) => runOpenCode({
148279
148441
  ...runParams,
148280
148442
  args: [...baseArgs, "--continue", c2.prompt]
@@ -152487,7 +152649,7 @@ ${ctx.error}` : ctx.error;
152487
152649
 
152488
152650
  // utils/gitAuthServer.ts
152489
152651
  import { randomUUID as randomUUID3 } from "node:crypto";
152490
- import { writeFileSync as writeFileSync8 } from "node:fs";
152652
+ import { writeFileSync as writeFileSync9 } from "node:fs";
152491
152653
  import { createServer as createServer2 } from "node:http";
152492
152654
  import { join as join13 } from "node:path";
152493
152655
  var CODE_TTL_MS = 5 * 60 * 1e3;
@@ -152576,7 +152738,7 @@ async function startGitAuthServer(tmpdir3) {
152576
152738
  `try{require("fs").unlinkSync("${scriptPath.replace(/\\/g, "\\\\")}")}catch(e){}`,
152577
152739
  `})}).on("error",function(){process.exit(1)})}`
152578
152740
  ].join("\n");
152579
- writeFileSync8(scriptPath, content, { mode: 448 });
152741
+ writeFileSync9(scriptPath, content, { mode: 448 });
152580
152742
  return scriptPath;
152581
152743
  }
152582
152744
  async function close() {
@@ -152850,9 +153012,9 @@ function buildPromptContext(ctx) {
152850
153012
  };
152851
153013
  }
152852
153014
  function assembleFullPrompt(ctx) {
152853
- const learningsSection = ctx.learnings ? `************* LEARNINGS *************
153015
+ const learningsSection = ctx.learningsFilePath ? `************* LEARNINGS *************
152854
153016
 
152855
- ${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.` : "";
152856
153018
  const runtimeSection = `************* RUNTIME *************
152857
153019
 
152858
153020
  ${ctx.runtime}`;
@@ -152879,8 +153041,8 @@ function resolveInstructions(ctx) {
152879
153041
  if (eventContext)
152880
153042
  tocEntries.push({ label: "EVENT CONTEXT", description: "related PR/issue data" });
152881
153043
  tocEntries.push({ label: "SYSTEM", description: "persona, security, tools, workflow rules" });
152882
- if (pctx.learnings)
152883
- tocEntries.push({ label: "LEARNINGS", description: "repo-specific knowledge" });
153044
+ if (pctx.learningsFilePath)
153045
+ tocEntries.push({ label: "LEARNINGS", description: "repo-specific knowledge file path" });
152884
153046
  tocEntries.push({ label: "RUNTIME", description: "environment metadata" });
152885
153047
  const toc = buildToc(tocEntries);
152886
153048
  const full = assembleFullPrompt({
@@ -152889,7 +153051,7 @@ function resolveInstructions(ctx) {
152889
153051
  procedure,
152890
153052
  eventContext,
152891
153053
  system,
152892
- learnings: pctx.learnings,
153054
+ learningsFilePath: pctx.learningsFilePath,
152893
153055
  runtime: pctx.runtime
152894
153056
  });
152895
153057
  const event = [pctx.eventTitle, pctx.eventMetadata].filter(Boolean).join("\n\n---\n\n");
@@ -152903,6 +153065,32 @@ function resolveInstructions(ctx) {
152903
153065
  };
152904
153066
  }
152905
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
+
152906
153094
  // utils/normalizeEnv.ts
152907
153095
  function maskValue(value2) {
152908
153096
  if (value2 && typeof value2 === "string" && value2.trim().length > 0) {
@@ -153078,8 +153266,8 @@ function resolvePayload(resolvedPromptInput, repoSettings) {
153078
153266
  }
153079
153267
 
153080
153268
  // utils/prSummary.ts
153081
- import { mkdir, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
153082
- 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";
153083
153271
  var SUMMARY_FILE_NAME = "pullfrog-summary.md";
153084
153272
  var SUMMARY_SCAFFOLD = `# PR summary
153085
153273
 
@@ -153089,19 +153277,19 @@ var SUMMARY_SCAFFOLD = `# PR summary
153089
153277
  var MIN_SNAPSHOT_LENGTH = 60;
153090
153278
  var MAX_SNAPSHOT_LENGTH = 32768;
153091
153279
  function summaryFilePath(tmpdir3) {
153092
- return join14(tmpdir3, SUMMARY_FILE_NAME);
153280
+ return join15(tmpdir3, SUMMARY_FILE_NAME);
153093
153281
  }
153094
153282
  async function seedSummaryFile(params) {
153095
153283
  const path3 = summaryFilePath(params.tmpdir);
153096
- await mkdir(dirname4(path3), { recursive: true });
153284
+ await mkdir2(dirname5(path3), { recursive: true });
153097
153285
  const seed = params.previousSnapshot && params.previousSnapshot.trim().length >= MIN_SNAPSHOT_LENGTH ? params.previousSnapshot : SUMMARY_SCAFFOLD;
153098
- await writeFile2(path3, seed, "utf8");
153286
+ await writeFile3(path3, seed, "utf8");
153099
153287
  return path3;
153100
153288
  }
153101
153289
  async function readSummaryFile(path3) {
153102
153290
  let raw2;
153103
153291
  try {
153104
- raw2 = await readFile2(path3, "utf8");
153292
+ raw2 = await readFile3(path3, "utf8");
153105
153293
  } catch {
153106
153294
  return null;
153107
153295
  }
@@ -153319,9 +153507,9 @@ async function resolveRunContextData(params) {
153319
153507
  import { execFileSync as execFileSync5, execSync as execSync3 } from "node:child_process";
153320
153508
  import { mkdtempSync } from "node:fs";
153321
153509
  import { tmpdir as tmpdir2 } from "node:os";
153322
- import { join as join15 } from "node:path";
153510
+ import { join as join16 } from "node:path";
153323
153511
  function createTempDirectory() {
153324
- const sharedTempDir = mkdtempSync(join15(tmpdir2(), "pullfrog-"));
153512
+ const sharedTempDir = mkdtempSync(join16(tmpdir2(), "pullfrog-"));
153325
153513
  process.env.PULLFROG_TEMP_DIR = sharedTempDir;
153326
153514
  log.info(`\xBB created temp dir at ${sharedTempDir}`);
153327
153515
  return sharedTempDir;
@@ -153723,15 +153911,12 @@ function formatTransientErrorSummary(error49, owner) {
153723
153911
  }
153724
153912
  async function mintProxyKey(ctx) {
153725
153913
  try {
153726
- process.env.ACTIONS_ID_TOKEN_REQUEST_URL = ctx.oidcCredentials.requestUrl;
153727
- process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = ctx.oidcCredentials.requestToken;
153728
- const oidcToken = await core6.getIDToken("pullfrog-api");
153729
- delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
153730
- delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
153914
+ const headers = await buildProxyTokenHeaders(ctx);
153915
+ if (!headers) return null;
153731
153916
  const response = await apiFetch({
153732
153917
  path: "/api/proxy-token",
153733
153918
  method: "POST",
153734
- headers: { Authorization: `Bearer ${oidcToken}` }
153919
+ headers
153735
153920
  });
153736
153921
  if (response.status === 402) {
153737
153922
  const body = await response.json().catch(() => null);
@@ -153763,15 +153948,30 @@ async function mintProxyKey(ctx) {
153763
153948
  delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
153764
153949
  }
153765
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
+ }
153766
153966
  async function resolveProxyModel(ctx) {
153767
153967
  if (process.env.PULLFROG_MODEL?.trim()) return;
153768
153968
  const needsProxy = isInfraCovered({ isOss: ctx.oss, plan: ctx.plan }) && ctx.proxyModel;
153769
153969
  if (!needsProxy) return;
153770
- if (!ctx.oidcCredentials) {
153970
+ if (!ctx.oidcCredentials && !isLocalApiUrl()) {
153771
153971
  log.warning("\xBB proxy requested but no OIDC credentials available \u2014 skipping");
153772
153972
  return;
153773
153973
  }
153774
- const key = await mintProxyKey({ oidcCredentials: ctx.oidcCredentials });
153974
+ const key = await mintProxyKey({ oidcCredentials: ctx.oidcCredentials, repo: ctx.repo });
153775
153975
  if (!key) return;
153776
153976
  process.env.OPENROUTER_API_KEY = key;
153777
153977
  core6.setSecret(key);
@@ -153795,6 +153995,45 @@ async function fetchPreviousSnapshot(ctx, prNumber) {
153795
153995
  return null;
153796
153996
  }
153797
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
+ }
153798
154037
  async function persistSummary(ctx) {
153799
154038
  const filePath = ctx.toolState.summaryFilePath;
153800
154039
  if (!filePath) return;
@@ -153876,7 +154115,8 @@ async function main() {
153876
154115
  oss: runContext.oss,
153877
154116
  plan: runContext.plan,
153878
154117
  proxyModel: runContext.proxyModel,
153879
- oidcCredentials
154118
+ oidcCredentials,
154119
+ repo: runContext.repo
153880
154120
  });
153881
154121
  } catch (error49) {
153882
154122
  if (error49 instanceof BillingError) {
@@ -153979,12 +154219,32 @@ async function main() {
153979
154219
  toolContext.mcpServerUrl = mcpHttpServer.url;
153980
154220
  log.info(`\xBB MCP server started at ${mcpHttpServer.url}`);
153981
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
+ }
153982
154242
  if (payload.generateSummary && payload.event.is_pr && payload.event.issue_number) {
153983
154243
  const previousSnapshot = await fetchPreviousSnapshot(toolContext, payload.event.issue_number);
153984
154244
  const filePath = await seedSummaryFile({ tmpdir: tmpdir3, previousSnapshot });
153985
154245
  toolState.summaryFilePath = filePath;
153986
154246
  try {
153987
- toolState.summarySeed = await readFile3(filePath, "utf8");
154247
+ toolState.summarySeed = await readFile4(filePath, "utf8");
153988
154248
  } catch {
153989
154249
  }
153990
154250
  log.info(
@@ -154008,7 +154268,7 @@ async function main() {
154008
154268
  modes: modes2,
154009
154269
  agentId,
154010
154270
  outputSchema,
154011
- learnings: runContext.repoSettings.learnings
154271
+ learningsFilePath: toolState.learningsFilePath ?? null
154012
154272
  });
154013
154273
  const logParts = [
154014
154274
  instructions.eventInstructions ? `EVENT-LEVEL INSTRUCTIONS:
@@ -154024,7 +154284,7 @@ ${instructions.user}` : null,
154024
154284
  log.info(instructions.full);
154025
154285
  });
154026
154286
  if (agentId === "opencode") {
154027
- const pluginDir = join16(process.cwd(), ".opencode", "plugin");
154287
+ const pluginDir = join17(process.cwd(), ".opencode", "plugin");
154028
154288
  const hasPlugins = existsSync7(pluginDir) && readdirSync(pluginDir).some((f) => /\.[jt]sx?$/.test(f));
154029
154289
  if (hasPlugins && toolState.dependencyInstallation?.promise) {
154030
154290
  log.info(
@@ -154085,6 +154345,7 @@ ${instructions.user}` : null,
154085
154345
  stopScript: runContext.repoSettings.stopScript,
154086
154346
  summaryFilePath: toolState.summaryFilePath,
154087
154347
  summarySeed: toolState.summarySeed,
154348
+ learningsFilePath: toolState.learningsFilePath,
154088
154349
  onActivityTimeout: onInnerActivityTimeout,
154089
154350
  onToolUse: (event) => {
154090
154351
  const wasTracked = recordDiffReadFromToolUse({
@@ -154142,6 +154403,9 @@ ${instructions.user}` : null,
154142
154403
  if (toolContext) {
154143
154404
  await persistSummary(toolContext);
154144
154405
  }
154406
+ if (toolContext) {
154407
+ await persistLearnings(toolContext);
154408
+ }
154145
154409
  if (toolContext && toolState.progressComment && !toolState.finalSummaryWritten) {
154146
154410
  await deleteProgressComment(toolContext).catch((error49) => {
154147
154411
  log.debug(`stranded progress comment cleanup failed: ${error49}`);
@@ -154194,6 +154458,9 @@ ${errorMessage}
154194
154458
  if (toolContext) {
154195
154459
  await persistSummary(toolContext);
154196
154460
  }
154461
+ if (toolContext) {
154462
+ await persistLearnings(toolContext);
154463
+ }
154197
154464
  return {
154198
154465
  success: false,
154199
154466
  error: errorMessage
@@ -154226,7 +154493,7 @@ ${errorMessage}
154226
154493
  }
154227
154494
 
154228
154495
  // commands/gha.ts
154229
- process.env.PATH = `${dirname5(process.execPath)}:${process.env.PATH}`;
154496
+ process.env.PATH = `${dirname6(process.execPath)}:${process.env.PATH}`;
154230
154497
  var STATE_TOKEN = "token";
154231
154498
  async function runMain() {
154232
154499
  try {
@@ -156036,7 +156303,7 @@ async function run2() {
156036
156303
  }
156037
156304
 
156038
156305
  // cli.ts
156039
- var VERSION10 = "0.1.0";
156306
+ var VERSION10 = "0.1.2";
156040
156307
  var bin = basename2(process.argv[1] || "");
156041
156308
  var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
156042
156309
  var rawArgs = process.argv.slice(2);