pullfrog 0.1.1 → 0.1.3

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];
@@ -107706,7 +107706,7 @@ function buildCommitPrompt(status) {
107706
107706
  ].join("\n");
107707
107707
  }
107708
107708
  function hasPostRunIssues(issues) {
107709
- return issues.stopHook !== void 0 || issues.dirtyTree !== void 0 || issues.summaryStale !== void 0;
107709
+ return issues.stopHook !== void 0 || issues.dirtyTree !== void 0 || issues.summaryStale !== void 0 || issues.unsubmittedReview !== void 0;
107710
107710
  }
107711
107711
  var agent = (input) => {
107712
107712
  return {
@@ -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
  }
@@ -109461,13 +109474,38 @@ var ReplyToReviewComment = type({
109461
109474
  "extremely brief reply (1 sentence max) explaining what was fixed, e.g. 'Fixed by renaming to X' or 'Added null check'"
109462
109475
  )
109463
109476
  });
109477
+ function duplicateReplyDecision(params) {
109478
+ const existing = params.existing;
109479
+ if (!existing) return null;
109480
+ if (existing.bodyWithFooter !== params.bodyWithFooter) return null;
109481
+ return {
109482
+ kind: "already-replied",
109483
+ commentId: existing.commentId,
109484
+ url: existing.url,
109485
+ reason: `reply ${existing.commentId} with identical body was already posted in this session; ignoring duplicate call`
109486
+ };
109487
+ }
109464
109488
  function ReplyToReviewCommentTool(ctx) {
109465
109489
  return tool({
109466
109490
  name: "reply_to_review_comment",
109467
- description: "Reply to a PR review comment thread (NOT issue comments \u2014 this only works for inline review comments on PR diffs). Call this for EACH comment you address in AddressReviews mode. Keep replies extremely brief (1 sentence max).",
109491
+ description: "Reply to a PR review comment thread (NOT issue comments \u2014 this only works for inline review comments on PR diffs). Call exactly ONCE per parent comment you address in AddressReviews mode \u2014 duplicate calls with the same body are a no-op. Keep replies extremely brief (1 sentence max).",
109468
109492
  parameters: ReplyToReviewComment,
109469
109493
  execute: execute(async ({ pull_number, comment_id, body }) => {
109470
109494
  const bodyWithFooter = addFooter(ctx, body);
109495
+ const dup = duplicateReplyDecision({
109496
+ existing: ctx.toolState.reviewReplies?.get(comment_id),
109497
+ bodyWithFooter
109498
+ });
109499
+ if (dup) {
109500
+ log.info(`skipping duplicate review reply: ${dup.reason}`);
109501
+ return {
109502
+ success: true,
109503
+ skipped: true,
109504
+ reason: dup.reason,
109505
+ commentId: dup.commentId,
109506
+ url: dup.url
109507
+ };
109508
+ }
109471
109509
  const result = await ctx.octokit.rest.pulls.createReplyForReviewComment({
109472
109510
  owner: ctx.repo.owner,
109473
109511
  repo: ctx.repo.name,
@@ -109475,7 +109513,14 @@ function ReplyToReviewCommentTool(ctx) {
109475
109513
  comment_id,
109476
109514
  body: bodyWithFooter
109477
109515
  });
109516
+ log.info(`\xBB created review comment ${result.data.id} (in reply to ${comment_id})`);
109478
109517
  ctx.toolState.wasUpdated = true;
109518
+ ctx.toolState.reviewReplies ??= /* @__PURE__ */ new Map();
109519
+ ctx.toolState.reviewReplies.set(comment_id, {
109520
+ commentId: result.data.id,
109521
+ url: result.data.html_url,
109522
+ bodyWithFooter
109523
+ });
109479
109524
  return {
109480
109525
  success: true,
109481
109526
  commentId: result.data.id,
@@ -110024,11 +110069,6 @@ async function spawn(options) {
110024
110069
  `spawn activity timer: pid=${child.pid} cmd=${options.cmd} timeout=${activityTimeoutMs}ms`
110025
110070
  );
110026
110071
  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
110072
  const idleMs = performance3.now() - lastActivityTime;
110033
110073
  log.debug(
110034
110074
  `spawn activity check: pid=${child.pid} idle=${Math.round(idleMs)}ms / ${activityTimeoutMs}ms`
@@ -110227,13 +110267,13 @@ var installNodeDependencies = {
110227
110267
  };
110228
110268
  }
110229
110269
  }
110230
- const resolved = resolveCommand(agent2, "frozen", []) || resolveCommand(agent2, "install", []);
110270
+ const resolved = resolveCommand(agent2, "frozen", []);
110231
110271
  if (!resolved) {
110232
110272
  return {
110233
110273
  language: "node",
110234
110274
  packageManager,
110235
110275
  dependenciesInstalled: false,
110236
- issues: [`no install command found for ${agent2}`]
110276
+ issues: [`no frozen-install command available for ${agent2}`]
110237
110277
  };
110238
110278
  }
110239
110279
  if (options.ignoreScripts) {
@@ -142549,7 +142589,7 @@ var import_semver = __toESM(require_semver2(), 1);
142549
142589
  // package.json
142550
142590
  var package_default = {
142551
142591
  name: "pullfrog",
142552
- version: "0.1.1",
142592
+ version: "0.1.3",
142553
142593
  type: "module",
142554
142594
  bin: {
142555
142595
  pullfrog: "dist/cli.mjs",
@@ -143493,6 +143533,10 @@ ${integrateStep}
143493
143533
  if (!pushed) {
143494
143534
  throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
143495
143535
  }
143536
+ const pushedSha = $("git", ["rev-parse", "HEAD"], { log: false }).trim();
143537
+ log.info(
143538
+ `\xBB pushed branch ${branch} to ${pushDest.remoteName}/${pushDest.remoteBranch} (sha ${pushedSha})`
143539
+ );
143496
143540
  return {
143497
143541
  success: true,
143498
143542
  branch,
@@ -143641,6 +143685,7 @@ function DeleteBranchTool(ctx) {
143641
143685
  await $git("push", ["origin", "--delete", `refs/heads/${params.branchName}`], {
143642
143686
  token: ctx.gitToken
143643
143687
  });
143688
+ log.info(`\xBB deleted branch ${params.branchName}`);
143644
143689
  return { success: true, deleted: params.branchName };
143645
143690
  })
143646
143691
  });
@@ -143666,6 +143711,7 @@ function PushTagsTool(ctx) {
143666
143711
  await $git("push", pushArgs, {
143667
143712
  token: ctx.gitToken
143668
143713
  });
143714
+ log.info(`\xBB pushed tag ${params.tag}`);
143669
143715
  return { success: true, tag: params.tag };
143670
143716
  })
143671
143717
  });
@@ -143820,7 +143866,7 @@ var CreatePullRequestReview = type({
143820
143866
  "1-2 sentence high-level summary with urgency level, critical callouts, and feedback about code outside the diff. Specific feedback on diff lines goes in 'comments' array."
143821
143867
  ).optional(),
143822
143868
  approved: type.boolean.describe(
143823
- "Set to true to submit as an approval. ONLY when the review contains no actionable feedback \u2014 neither inline comments nor actionable content in the body. Defaults to false (comment-only review). Rejections are not supported."
143869
+ "Set to true to submit as an approval. Use for both 'no issues found' and informational `> [!NOTE]` reviews where the PR is mergeable as-is and nothing in the body warrants code changes \u2014 approving also suppresses the Fix-button footer affordance so users don't dispatch a fix run on non-actionable feedback. Reserve approved: false for `> [!IMPORTANT]` (recommended changes) and `> [!CAUTION]` (critical) reviews. Defaults to false (comment-only review). Rejections are not supported."
143824
143870
  ).optional(),
143825
143871
  commit_id: type.string.describe("Optional SHA of the commit being reviewed. Defaults to latest.").optional(),
143826
143872
  comments: type({
@@ -143990,6 +144036,7 @@ function CreatePullRequestReviewTool(ctx) {
143990
144036
  }
143991
144037
  const reviewId = result.data.id;
143992
144038
  const reviewNodeId = result.data.node_id;
144039
+ log.info(`\xBB created review ${reviewId} on pull request #${pull_number}`);
143993
144040
  const actuallyReviewedSha = ctx.toolState.checkoutSha ?? params.commit_id;
143994
144041
  ctx.toolState.review = {
143995
144042
  id: reviewId,
@@ -144349,6 +144396,8 @@ async function ensureBeforeShaReachable(params) {
144349
144396
  }
144350
144397
  }
144351
144398
  var STALE_LOCK_AGE_MS = 3e4;
144399
+ var PULL_REF_RETRY_DELAYS_MS = [2e3, 5e3, 1e4];
144400
+ var PULL_REF_MISSING_PATTERN = /couldn't find remote ref pull\/\d+\/head/i;
144352
144401
  var GIT_LOCK_PATHS = [
144353
144402
  ".git/shallow.lock",
144354
144403
  ".git/index.lock",
@@ -144374,6 +144423,27 @@ function cleanupStaleGitLocks() {
144374
144423
  }
144375
144424
  }
144376
144425
  }
144426
+ async function isPullRequestStillDispatchable(args2) {
144427
+ try {
144428
+ const { data } = await args2.octokit.rest.pulls.get({
144429
+ owner: args2.owner,
144430
+ repo: args2.repo,
144431
+ pull_number: args2.pr.number
144432
+ });
144433
+ if (data.state !== "open") return false;
144434
+ if (data.head.sha !== args2.pr.headSha) return false;
144435
+ return true;
144436
+ } catch {
144437
+ return true;
144438
+ }
144439
+ }
144440
+ async function abortIfPullRequestMoved(args2) {
144441
+ const stillValid = await isPullRequestStillDispatchable(args2);
144442
+ if (stillValid) return;
144443
+ throw new Error(
144444
+ `PR #${args2.pr.number} is no longer in the state it was at dispatch (likely closed, merged, or force-pushed between webhook fire and run start). aborting checkout \u2014 re-trigger the run if this PR is still active.`
144445
+ );
144446
+ }
144377
144447
  async function checkoutPrBranch(pr, params) {
144378
144448
  const { octokit, owner, name, gitToken, toolState, beforeSha } = params;
144379
144449
  log.info(`\xBB checking out PR #${pr.number}...`);
@@ -144390,9 +144460,26 @@ async function checkoutPrBranch(pr, params) {
144390
144460
  if (!alreadyOnBranch) {
144391
144461
  $("git", ["checkout", "-B", pr.baseRef, `origin/${pr.baseRef}`], { log: false });
144392
144462
  log.debug(`\xBB fetching PR #${pr.number} (${localBranch})...`);
144393
- await $git("fetch", ["--no-tags", "origin", `+pull/${pr.number}/head:${localBranch}`], {
144394
- token: gitToken
144395
- });
144463
+ await retry(
144464
+ async () => {
144465
+ try {
144466
+ await $git("fetch", ["--no-tags", "origin", `+pull/${pr.number}/head:${localBranch}`], {
144467
+ token: gitToken
144468
+ });
144469
+ } catch (e) {
144470
+ const msg = e instanceof Error ? e.message : String(e);
144471
+ if (PULL_REF_MISSING_PATTERN.test(msg)) {
144472
+ await abortIfPullRequestMoved({ octokit, owner, repo: name, pr });
144473
+ }
144474
+ throw e;
144475
+ }
144476
+ },
144477
+ {
144478
+ delaysMs: PULL_REF_RETRY_DELAYS_MS,
144479
+ label: `pull/${pr.number}/head fetch`,
144480
+ shouldRetry: (e) => PULL_REF_MISSING_PATTERN.test(e instanceof Error ? e.message : String(e))
144481
+ }
144482
+ );
144396
144483
  $("git", ["checkout", localBranch], { log: false });
144397
144484
  log.debug(`\xBB checked out PR #${pr.number}`);
144398
144485
  toolState.checkoutSha = $("git", ["rev-parse", "HEAD"], { log: false }).trim();
@@ -144854,6 +144941,7 @@ function IssueTool(ctx) {
144854
144941
  labels: params.labels ?? [],
144855
144942
  assignees: params.assignees ?? []
144856
144943
  });
144944
+ log.info(`\xBB created issue #${result.data.number} (id ${result.data.id})`);
144857
144945
  const nodeId = result.data.node_id;
144858
144946
  if (typeof nodeId === "string" && nodeId.length > 0) {
144859
144947
  await patchWorkflowRunFields(ctx, {
@@ -145045,6 +145133,7 @@ function AddLabelsTool(ctx) {
145045
145133
  issue_number,
145046
145134
  labels
145047
145135
  });
145136
+ log.info(`\xBB added labels [${labels.join(", ")}] to issue #${issue_number}`);
145048
145137
  return {
145049
145138
  success: true,
145050
145139
  labels: result.data.map((label) => label.name)
@@ -145053,40 +145142,6 @@ function AddLabelsTool(ctx) {
145053
145142
  });
145054
145143
  }
145055
145144
 
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
145145
  // mcp/output.ts
145091
145146
  var import_ajv3 = __toESM(require_ajv(), 1);
145092
145147
  var SetOutputParams = type({
@@ -145180,6 +145235,7 @@ function UpdatePullRequestBodyTool(ctx) {
145180
145235
  pull_number: params.pull_number,
145181
145236
  body: bodyWithFooter
145182
145237
  });
145238
+ log.info(`\xBB updated pull request #${result.data.number}`);
145183
145239
  ctx.toolState.wasUpdated = true;
145184
145240
  return {
145185
145241
  success: true,
@@ -145207,6 +145263,7 @@ function CreatePullRequestTool(ctx) {
145207
145263
  base: params.base,
145208
145264
  draft: params.draft ?? false
145209
145265
  });
145266
+ log.info(`\xBB created pull request #${result.data.number} (id ${result.data.id})`);
145210
145267
  const reviewer = ctx.payload.triggerer;
145211
145268
  if (reviewer) {
145212
145269
  try {
@@ -145758,7 +145815,7 @@ function ResolveReviewThreadTool(ctx) {
145758
145815
  threadId: params.thread_id
145759
145816
  });
145760
145817
  const thread = response.resolveReviewThread.thread;
145761
- log.debug(`resolved thread ${thread.id}, isResolved=${thread.isResolved}`);
145818
+ log.info(`\xBB resolved review thread ${thread.id}`);
145762
145819
  return {
145763
145820
  thread_id: thread.id,
145764
145821
  is_resolved: thread.isResolved,
@@ -145799,13 +145856,14 @@ function buildModeOverrides(t2) {
145799
145856
 
145800
145857
  An existing plan comment was found for this issue. Update that comment with the revised plan \u2014 do not create a new plan comment.
145801
145858
 
145802
- 1. Use \`previousPlanBody\` from this response as the plan to revise; do not call \`get_issue\` or \`get_issue_comments\`.
145803
- 2. Revise the plan based on the user's request:
145859
+ 1. **task list**: create your task list for this run as your first action.
145860
+ 2. Use \`previousPlanBody\` from this response as the plan to revise; do not call \`get_issue\` or \`get_issue_comments\`.
145861
+ 3. Revise the plan based on the user's request:
145804
145862
  - incorporate the current plan (\`previousPlanBody\`) and the user's revision request
145805
145863
  - gather relevant codebase context (file paths, architecture notes from AGENTS.md)
145806
145864
  - produce a structured plan with clear milestones
145807
- 3. Call \`${t2("report_progress")}\` with the full revised plan text and \`{ target_plan_comment: true }\` so it updates the existing plan comment (not the progress comment).
145808
- 4. Then post a short note to the progress comment (e.g. "Plan has been updated in the comment above.") via \`${t2("report_progress")}\` so it is not left as "Leaping...".`
145865
+ 4. Call \`${t2("report_progress")}\` with the full revised plan text and \`{ target_plan_comment: true }\` so it updates the existing plan comment (not the progress comment).
145866
+ 5. Then post a short note to the progress comment (e.g. "Plan has been updated in the comment above.") via \`${t2("report_progress")}\` so it is not left as "Leaping...".`
145809
145867
  };
145810
145868
  }
145811
145869
  var modeInstructionParent = {
@@ -146230,24 +146288,13 @@ function UploadFileTool(ctx) {
146230
146288
  if (!uploadResponse.ok) {
146231
146289
  throw new Error(`failed to upload file: ${uploadResponse.statusText}`);
146232
146290
  }
146291
+ log.info(`\xBB uploaded file ${publicUrl}`);
146233
146292
  return { success: true, publicUrl, filename, contentLength, contentType };
146234
146293
  })
146235
146294
  });
146236
146295
  }
146237
146296
 
146238
146297
  // mcp/server.ts
146239
- function initToolState(params) {
146240
- const resolved = parseProgressComment(params.progressComment);
146241
- if (resolved) {
146242
- log.info(`\xBB using pre-created progress comment: ${resolved.id} (${resolved.type})`);
146243
- }
146244
- return {
146245
- progressComment: resolved,
146246
- hadProgressComment: !!resolved,
146247
- backgroundProcesses: /* @__PURE__ */ new Map(),
146248
- usageEntries: []
146249
- };
146250
- }
146251
146298
  var mcpPortStart = 3764;
146252
146299
  var mcpPortAttempts = 100;
146253
146300
  var mcpHost = "127.0.0.1";
@@ -146323,8 +146370,7 @@ function buildOrchestratorTools(ctx, outputSchema) {
146323
146370
  PushTagsTool(ctx),
146324
146371
  DeleteBranchTool(ctx),
146325
146372
  CreatePullRequestTool(ctx),
146326
- UpdatePullRequestBodyTool(ctx),
146327
- UpdateLearningsTool(ctx)
146373
+ UpdatePullRequestBodyTool(ctx)
146328
146374
  ];
146329
146375
  }
146330
146376
  async function tryStartMcpServer(ctx, tools, port) {
@@ -146481,9 +146527,6 @@ Rules:
146481
146527
  - Do NOT include a changelog section \u2014 the key changes list serves this purpose
146482
146528
  - Focus on *intent*, not *what* \u2014 the diff already shows what changed
146483
146529
  - 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
146530
  function computeModes(agentId) {
146488
146531
  const t2 = (toolName) => formatMcpToolRef(agentId, toolName);
146489
146532
  return [
@@ -146492,18 +146535,20 @@ function computeModes(agentId) {
146492
146535
  description: "Implement, build, create, or develop code changes; make specific changes to files or features; execute a plan; or handle tasks with specific implementation details",
146493
146536
  prompt: `### Checklist
146494
146537
 
146495
- 1. **plan** (optional, for complex tasks): analyze requirements, read AGENTS.md and relevant code, produce a step-by-step implementation plan.
146538
+ 1. **task list**: create your task list for this run as your first action.
146496
146539
 
146497
- 2. **setup**: checkout or create the branch:
146540
+ 2. **plan** (optional, for complex tasks): analyze requirements, read AGENTS.md and relevant code, produce a step-by-step implementation plan.
146541
+
146542
+ 3. **setup**: checkout or create the branch:
146498
146543
  - **PR event, modifying the existing PR**: call \`${t2("checkout_pr")}\`
146499
146544
  - **new branch**: use \`${t2("git")}\` to create a branch (\`git checkout -b pullfrog/branch-name\`)
146500
146545
 
146501
- 3. **build**: implement changes using your native file and shell tools:
146546
+ 4. **build**: implement changes using your native file and shell tools:
146502
146547
  - follow the plan (if you ran a plan phase)
146503
146548
  - plan your approach before writing code: identify which files need to change, key design decisions, and edge cases. for non-trivial changes, consider whether there's a more elegant approach.
146504
146549
  - run relevant tests/lints before committing
146505
146550
 
146506
- 4. **self-review**: judgment call \u2014 does YOUR diff warrant a fresh-eyes pass?
146551
+ 5. **self-review**: judgment call \u2014 does YOUR diff warrant a fresh-eyes pass?
146507
146552
 
146508
146553
  Skip self-review (commit directly) when the diff is **genuinely trivial**:
146509
146554
  - doc typos, comment-only edits, whitespace/format-only, import reordering
@@ -146534,13 +146579,11 @@ function computeModes(agentId) {
146534
146579
 
146535
146580
  Review the findings, address valid points, and discard nitpicks or false positives. The reviewer is fallible \u2014 it biases toward *recommending additions* (defensive checks for impossible cases, extra logging, new abstractions used once, comments restating code, tests asserting tautologies, "just-in-case" guards). For each finding, ask: would applying it leave the code more sound, correct, AND elegant? Two-out-of-three is usually a signal to look harder for a fix that gets all three before settling for one that trades elegance for correctness. Reject bloat-shaped findings without applying them, and after applying the rest re-read your diff and be discerning about what *you just changed*: if any fix turned out to be bloat in context, revert it. The goal is code that is sound and correct *while remaining elegant*; the smallest diff that fixes the real defect almost always wins. Then verify only intended changes are present, no debug artifacts or commented-out code remain, no unrelated files were modified. Commit locally via shell (\`git add . && git commit -m "..."\`).
146536
146581
 
146537
- 5. **finalize**:
146582
+ 6. **finalize**:
146538
146583
  - confirm a clean working tree, then push via \`${t2("push_branch")}\` (see *SYSTEM* Git rules if this fails \u2014 prepush errors are usually the repo's tests/lint, not infra timeouts)
146539
146584
  - create a PR via \`${t2("create_pull_request")}\`
146540
146585
  - call \`${t2("report_progress")}\` with the PR link or the exact error if push/PR failed
146541
146586
 
146542
- ${learningsStep(t2, 6)}
146543
-
146544
146587
  ### Notes
146545
146588
 
146546
146589
  For simple, well-defined tasks, skip the plan phase and go straight to build.`
@@ -146550,27 +146593,27 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
146550
146593
  description: "Address PR review feedback; respond to reviewer comments; make requested changes to an existing PR",
146551
146594
  prompt: `### Checklist
146552
146595
 
146553
- 1. Checkout the PR branch via \`${t2("checkout_pr")}\`.
146596
+ 1. **task list**: create your task list for this run as your first action.
146554
146597
 
146555
- 2. Fetch review comments via \`${t2("get_review_comments")}\`.
146598
+ 2. Checkout the PR branch via \`${t2("checkout_pr")}\`.
146556
146599
 
146557
- 3. For each comment:
146600
+ 3. Fetch review comments via \`${t2("get_review_comments")}\`.
146601
+
146602
+ 4. For each comment:
146558
146603
  - understand the feedback
146559
146604
  - evaluate whether applying it would leave the code more **sound, correct, AND elegant**. reviewers are fallible and bias toward *recommending additions* (defensive checks for impossible cases, extra abstractions, comments restating obvious code, tests asserting tautologies, "just-in-case" guards). if a request would add bloat \u2014 ceremony without commensurate correctness benefit \u2014 push back in your reply rather than mechanically applying it. two-out-of-three is usually a signal to look harder for a fix that gets all three before settling.
146560
146605
  - if the request stands, make the code change using your native tools; otherwise reply explaining why
146561
146606
  - record what was done (or why nothing was done)
146562
146607
 
146563
- 4. Quality check:
146608
+ 5. Quality check:
146564
146609
  - test changes, then review the diff before committing \u2014 verify only intended changes are present, no debug artifacts remain, no fix turned out to be bloat in context (revert any that did), and the changes are clean enough that a senior engineer would approve without hesitation
146565
146610
  - commit locally via shell (\`git add . && git commit -m "..."\`)
146566
146611
 
146567
- 5. Finalize:
146612
+ 6. Finalize:
146568
146613
  - confirm a clean working tree, then push via \`${t2("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*)
146569
- - reply to each comment using \`${t2("reply_to_review_comment")}\`
146614
+ - reply to each comment **exactly once** using \`${t2("reply_to_review_comment")}\` \u2014 do not re-emit the same call (the runtime dedupes identical bodies and the second call is wasted)
146570
146615
  - 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)}`
146616
+ - call \`${t2("report_progress")}\` with a brief summary (or the exact push error if push failed)`
146574
146617
  },
146575
146618
  // Review and IncrementalReview use the multi-lens orchestrator pattern
146576
146619
  // (canonical source: .claude/commands/anneal.md). The orchestrator does
@@ -146589,11 +146632,13 @@ ${learningsStep(t2, 6)}`
146589
146632
  description: "Review code, PRs, or implementations; provide feedback or suggestions; identify issues; or check code quality, style, and correctness",
146590
146633
  prompt: `### Checklist
146591
146634
 
146592
- 1. **checkout**: call \`${t2("checkout_pr")}\` \u2014 this returns PR metadata and a \`diffPath\`. read the diff TOC end-to-end and treat its file line ranges as your coverage checklist.
146635
+ 1. **task list**: create your task list for this run as your first action.
146593
146636
 
146594
- 2. **triage**: orient yourself on the PR \u2014 identify *what kind of thing this is* (domain it touches, seams it crosses, external contracts it depends on, user-facing surfaces it changes). orientation only \u2014 defer specific defect-hunting to the subagents; pre-reviewing biases the lenses you pick. use \`${t2("get_pull_request")}\` and other read-only GitHub tools for additional context if needed.
146637
+ 2. **checkout**: call \`${t2("checkout_pr")}\` \u2014 this returns PR metadata and a \`diffPath\`. read the diff TOC end-to-end and treat its file line ranges as your coverage checklist.
146595
146638
 
146596
- if the PR is **genuinely trivial**, skip steps 3\u20134 entirely and submit a \`No new issues found.\` review per step 5. there's no value in dispatching even one lens for a typo.
146639
+ 3. **triage**: orient yourself on the PR \u2014 identify *what kind of thing this is* (domain it touches, seams it crosses, external contracts it depends on, user-facing surfaces it changes). orientation only \u2014 defer specific defect-hunting to the subagents; pre-reviewing biases the lenses you pick. use \`${t2("get_pull_request")}\` and other read-only GitHub tools for additional context if needed.
146640
+
146641
+ if the PR is **genuinely trivial**, skip steps 4\u20135 entirely and submit a \`No new issues found.\` review per step 6. there's no value in dispatching even one lens for a typo.
146597
146642
 
146598
146643
  "Genuinely trivial" (skip):
146599
146644
  - single-word doc typo, whitespace/format-only, comment-only across any number of files
@@ -146638,7 +146683,7 @@ ${learningsStep(t2, 6)}`
146638
146683
  - **holistic** \u2014 does the PR make sense as a whole? symmetric flows (delete for every create, rollback for every migration)?
146639
146684
  - **subsystem lenses** (invent as the PR demands) \u2014 auth, billing, payments, schema migration, webhooks, secrets, RBAC, multi-tenant isolation, cron/scheduling, etc.
146640
146685
 
146641
- 3. **fan out**: dispatch one \`${REVIEWER_AGENT_NAME}\` subagent per lens \u2014 its baked-in system prompt enforces the non-mutative + non-recursive contract (read-only file/search/web tools and read-only MCP queries; no writes, shell side effects, state-changing MCP calls, or nested subagent dispatch). when picking 2+ lenses, dispatch them in a **single assistant turn with multiple parallel subagent calls**; issuing one and awaiting reply before the next collapses the fan-out into a serial review. if a subagent errors out, times out, or returns nothing usable, retry once with the same lens; if it still fails, proceed with partial coverage and note the missing lens in the review body \u2014 do not skip step 3 entirely on a single subagent failure. each subagent gets:
146686
+ 4. **fan out**: dispatch one \`${REVIEWER_AGENT_NAME}\` subagent per lens \u2014 its baked-in system prompt enforces the non-mutative + non-recursive contract (read-only file/search/web tools and read-only MCP queries; no writes, shell side effects, state-changing MCP calls, or nested subagent dispatch). when picking 2+ lenses, dispatch them in a **single assistant turn with multiple parallel subagent calls**; issuing one and awaiting reply before the next collapses the fan-out into a serial review. if a subagent errors out, times out, or returns nothing usable, retry once with the same lens; if it still fails, proceed with partial coverage and note the missing lens in the review body \u2014 do not skip step 4 entirely on a single subagent failure. each subagent gets:
146642
146687
  - the diff path / target \u2014 reading the diff and the codebase is its job
146643
146688
  - **only one lens** \u2014 never a multi-section "review for X, Y, and Z" prompt
146644
146689
  - **a Task \`description\` set to the lens name** (e.g. \`"security"\`, \`"correctness"\`, \`"billing-subsystem"\`) \u2014 the harness reads this field to label the subagent's log lines so parallel runs can be told apart in CI output. without it, every subagent shows up as \`subagent#N\`.
@@ -146653,20 +146698,33 @@ ${learningsStep(t2, 6)}`
146653
146698
  - do NOT pre-shape their output with a finding schema
146654
146699
  - do NOT mention the other lenses (independence is the point \u2014 overlapping findings are a strong signal)
146655
146700
 
146656
- 4. **aggregate & draft**: merge findings; de-dup overlaps (two lenses catching the same issue = higher-confidence signal); trace each finding yourself before accepting it. drop praise, style preferences, speculative/unverified claims, findings about pre-existing code unrelated to the PR (heuristic: if the finding's root cause lives in lines this PR added or modified, it's in scope; otherwise drop unless the PR plausibly introduced or amplified the regression), and anything not actionable. also drop **bloat-shaped findings** \u2014 proposed fixes that would add defensive checks for cases that can't happen, abstractions used once, comments restating obvious code, tests asserting tautologies, or "just-in-case" guards. subagents are fallible and bias toward recommending changes; the bar for an actionable inline comment is sound + correct + elegant. recommending a change that improves only one of the three (or worse, degrades elegance to nominally improve correctness) makes the codebase worse, not better.
146701
+ 5. **aggregate & draft**: merge findings; de-dup overlaps (two lenses catching the same issue = higher-confidence signal); trace each finding yourself before accepting it. drop praise, style preferences, speculative/unverified claims, findings about pre-existing code unrelated to the PR (heuristic: if the finding's root cause lives in lines this PR added or modified, it's in scope; otherwise drop unless the PR plausibly introduced or amplified the regression), and anything not actionable. also drop **bloat-shaped findings** \u2014 proposed fixes that would add defensive checks for cases that can't happen, abstractions used once, comments restating obvious code, tests asserting tautologies, or "just-in-case" guards. subagents are fallible and bias toward recommending changes; the bar for an actionable inline comment is sound + correct + elegant. recommending a change that improves only one of the three (or worse, degrades elegance to nominally improve correctness) makes the codebase worse, not better.
146657
146702
 
146658
146703
  for surviving findings, draft inline comments with NEW line numbers from the diff. every comment must be actionable, 2-3 sentences max. use GitHub permalink format for code references. for impact-analysis findings (stale references after rename/remove), report them in the review body ordered by severity (runtime breakage > incorrect docs > stale comments) rather than as inline comments unless they're anchored to a specific line.
146659
146704
 
146660
- 5. **submit**: ALWAYS submit exactly one review via \`${t2("create_pull_request_review")}\`. Do NOT call \`report_progress\` \u2014 the review is the final record and the progress comment will be cleaned up automatically.
146705
+ 6. **submit**: ALWAYS submit exactly one review via \`${t2("create_pull_request_review")}\`. Do NOT call \`report_progress\` \u2014 the review is the final record and the progress comment will be cleaned up automatically.
146661
146706
 
146662
146707
  note: the first create_pull_request_review submission may error with a one-time diff-coverage nudge listing unread TOC regions. retry the same call to proceed \u2014 optionally after reading the listed ranges. the pre-flight will not block again this session.
146663
146708
 
146664
146709
  The review body is structured as: \`[optional alert blockquote]\` \u2192 \`[PR summary using the default format below]\`. Inline comments are passed via the \`comments\` parameter, not in the body.
146665
146710
 
146666
- - **critical issues** (blocks merge \u2014 bugs, security, data loss):
146711
+ GitHub alert blockquotes render at four visual intensities \u2014 the callout is what the author sees first, so pick the one that matches what you want them to do:
146712
+
146713
+ - \`[!CAUTION]\` \u2014 large red banner. Reads as "this will break something."
146714
+ - \`[!IMPORTANT]\` \u2014 large purple banner. Reads as "you need to look at this before merging."
146715
+ - \`[!NOTE]\` \u2014 small blue inline callout. Reads as "FYI, here's something worth noting."
146716
+ - no callout \u2014 plain text. Reads as routine review output.
146717
+
146718
+ Two reinforcing levers: callout intensity (above) and \`approved\` (which gates the footer Fix-button affordance \u2014 Fix renders on every non-approving review, so \`approved: true\` suppresses it). Wrapping mergeable feedback in \`[!IMPORTANT]\` trains users to click Fix on reviews that don't need fixing. Pick the tier the author's actual next action justifies.
146719
+
146720
+ - **critical issues** (blocks merge \u2014 bugs, security, data loss, broken core flows):
146667
146721
  \`approved: false\`. Body opens with \`> [!CAUTION]\\n> This PR introduces ...\`, followed by the PR summary. Include all inline comments via \`comments\`.
146668
- - **recommended changes** (non-critical):
146669
- \`approved: false\`. Body opens with \`> [!IMPORTANT]\\n> Consider ...\`, followed by the PR summary. Include all inline comments via \`comments\`.
146722
+ - **must-address non-critical findings** (real consequences if shipped \u2014 incorrect behavior in non-critical paths, missing validation on user input, regressions the author should fix before merge):
146723
+ \`approved: false\`. Body opens with \`> [!IMPORTANT]\\n> ...\`, followed by the PR summary. Reserve this tier for findings with concrete fallout \u2014 do NOT use \`[!IMPORTANT]\` for nits, style preferences, or "consider also" suggestions. Include all inline comments via \`comments\`.
146724
+ - **minor suggestions only** (single-line nits, doc/comment polish, defer-able observations, "rough edges"):
146725
+ \`approved: false\`. NO alert blockquote. Body opens directly with the PR summary. Include all inline comments via \`comments\`.
146726
+ - **informational observations** (mergeable as-is, nothing actionable \u2014 e.g. prior feedback addressed cleanly, surfacing a minor stale doc reference, calling out something noteworthy without recommending a change):
146727
+ \`approved: true\`. Body opens with \`> [!NOTE]\\n> ...\`, followed by the PR summary. Do NOT include inline \`comments\` \u2014 \`[!NOTE]\` signals "no action needed", which contradicts an actionable anchor; if a point is concrete enough to anchor to a line, downgrade the whole review to "minor suggestions only" (\`approved: false\`) instead.
146670
146728
  - **no actionable issues**:
146671
146729
  \`approved: true\`. Body opens with \`No new issues found.\` followed by the PR summary.
146672
146730
 
@@ -146675,7 +146733,7 @@ ${PR_SUMMARY_FORMAT}`
146675
146733
  // IncrementalReview shares Review's multi-lens orchestrator pattern but
146676
146734
  // scopes the target to the incremental diff. The "issues must be NEW
146677
146735
  // since the last Pullfrog review" filter lives at aggregation time
146678
- // (step 5), NOT in the subagent prompt — pushing the filter into
146736
+ // (step 6), NOT in the subagent prompt — pushing the filter into
146679
146737
  // subagents matches the canonical anneal anti-pattern of "list known
146680
146738
  // pre-existing failures — don't flag these" and suppresses signal on
146681
146739
  // regressions the new commits amplified. The review body is just
@@ -146688,15 +146746,17 @@ ${PR_SUMMARY_FORMAT}`
146688
146746
  description: "Re-review a PR after new commits are pushed; focus on new changes since the last review",
146689
146747
  prompt: `### Checklist
146690
146748
 
146691
- 1. **checkout**: call \`${t2("checkout_pr")}\` \u2014 this returns PR metadata, \`diffPath\` (full diff), and \`incrementalDiffPath\` (changes since last reviewed version, if available). read the diff TOC first and use its line ranges as your coverage checklist.
146749
+ 1. **task list**: create your task list for this run as your first action.
146750
+
146751
+ 2. **checkout**: call \`${t2("checkout_pr")}\` \u2014 this returns PR metadata, \`diffPath\` (full diff), and \`incrementalDiffPath\` (changes since last reviewed version, if available). read the diff TOC first and use its line ranges as your coverage checklist.
146692
146752
 
146693
- 2. **incremental scope**: if \`incrementalDiffPath\` is present, read it to see what changed since the last review. this is a range-diff that isolates the net changes, filtering out base branch noise. if not present, fall back to reviewing the full PR diff and determine what changed since Pullfrog's most recent review.
146753
+ 3. **incremental scope**: if \`incrementalDiffPath\` is present, read it to see what changed since the last review. this is a range-diff that isolates the net changes, filtering out base branch noise. if not present, fall back to reviewing the full PR diff and determine what changed since Pullfrog's most recent review.
146694
146754
 
146695
- 3. **prior feedback**: fetch previous reviews via \`${t2("list_pull_request_reviews")}\`. for the most recent Pullfrog review, call \`${t2("get_review_comments")}\` with the review ID to retrieve specific prior line-level feedback. you'll use this to filter your aggregation in step 5 \u2014 anything already flagged in a prior review and not changed by the new commits should not be re-raised. you do NOT need to render this in the review body; the rolling PR summary snapshot is the durable record of what's been addressed.
146755
+ 4. **prior feedback**: fetch previous reviews via \`${t2("list_pull_request_reviews")}\`. for the most recent Pullfrog review, call \`${t2("get_review_comments")}\` with the review ID to retrieve specific prior line-level feedback. you'll use this to filter your aggregation in step 6 \u2014 anything already flagged in a prior review and not changed by the new commits should not be re-raised. you do NOT need to render this in the review body; the rolling PR summary snapshot is the durable record of what's been addressed.
146696
146756
 
146697
- 4. **triage & fan out**: orient on the *incremental* changes \u2014 domain, seams, external contracts, user-facing surfaces.
146757
+ 5. **triage & fan out**: orient on the *incremental* changes \u2014 domain, seams, external contracts, user-facing surfaces.
146698
146758
 
146699
- if the incremental changes are **genuinely trivial**, skip the fan-out entirely and jump to step 7's non-substantive path (do NOT submit a review).
146759
+ if the incremental changes are **genuinely trivial**, skip the fan-out entirely and jump to step 8's non-substantive path (do NOT submit a review).
146700
146760
 
146701
146761
  "Genuinely trivial" (skip): formatting/comment tweaks, import reordering, lockfile regen, mechanical rename of import paths, whitespace-only.
146702
146762
  "Looks trivial but isn't" (do NOT skip \u2014 same anti-patterns as Review mode): 1-line changes to SQL/regex/auth/billing/permissions/signature-verification code; flipping feature-flag defaults or retry/timeout constants; money/tax/HTTP-method/redirect changes; tightening or loosening a comparison operator; mixed diffs with a semantic line buried in formatting.
@@ -146704,8 +146764,8 @@ ${PR_SUMMARY_FORMAT}`
146704
146764
 
146705
146765
  otherwise pick lenses by where the new commits concentrate risk \u2014 **there's no fixed count**, same calibration as Review mode (1 lens for pure refactor / isolated fix; 2\u20133 for typical features; 4\u20135 for high-stakes subsystem touches; 6+ is a smell). lens framing follows Review mode: themed lenses (correctness & invariants, impact when new commits remove/rename/deprecate things, research-validated assumptions, security, user-journey, operational readiness, integration & cross-cutting, test integrity, performance, holistic) and subsystem lenses (auth, billing, schema migration, etc.) \u2014 for high-stakes domains lead with the subsystem lens rather than the generic themed equivalent.
146706
146766
 
146707
- dispatch one \`${REVIEWER_AGENT_NAME}\` subagent per lens \u2014 its baked-in system prompt enforces the non-mutative + non-recursive contract (read-only file/search/web tools and read-only MCP queries; no writes, shell side effects, state-changing MCP calls, or nested subagent dispatch). dispatch them in a **single assistant turn with multiple parallel subagent calls** (serial dispatch collapses the fan-out). if a subagent errors out, times out, or returns nothing usable, retry once with the same lens; if it still fails, proceed with partial coverage and note the missing lens in the review body \u2014 do not skip step 4 entirely on a single subagent failure. each subagent gets:
146708
- - the diff scope (incremental diff path if available, full diff otherwise). do NOT tell them to skip pre-existing issues \u2014 that suppresses regressions the new commits amplified; the "issues must be NEW" filter lives at aggregation time (step 5), not in the subagent prompt
146767
+ dispatch one \`${REVIEWER_AGENT_NAME}\` subagent per lens \u2014 its baked-in system prompt enforces the non-mutative + non-recursive contract (read-only file/search/web tools and read-only MCP queries; no writes, shell side effects, state-changing MCP calls, or nested subagent dispatch). dispatch them in a **single assistant turn with multiple parallel subagent calls** (serial dispatch collapses the fan-out). if a subagent errors out, times out, or returns nothing usable, retry once with the same lens; if it still fails, proceed with partial coverage and note the missing lens in the review body \u2014 do not skip step 5 entirely on a single subagent failure. each subagent gets:
146768
+ - the diff scope (incremental diff path if available, full diff otherwise). do NOT tell them to skip pre-existing issues \u2014 that suppresses regressions the new commits amplified; the "issues must be NEW" filter lives at aggregation time (step 6), not in the subagent prompt
146709
146769
  - **only one lens** \u2014 never a multi-section "review for X, Y, and Z" prompt
146710
146770
  - **a Task \`description\` set to the lens name** (e.g. \`"security"\`, \`"correctness"\`, \`"billing-subsystem"\`) \u2014 the harness reads this field to label the subagent's log lines so parallel runs can be told apart in CI output. without it, every subagent shows up as \`subagent#N\`.
146711
146771
  - the read-only contract restated in your dispatch instructions so the rule is present twice (the subagent's system prompt also enforces it). The test: would this call still be a no-op if reverted? If not (PR comments, branch pushes, issue updates, set_output, label changes, dependency installs, etc.), don't make it.
@@ -146719,15 +146779,21 @@ ${PR_SUMMARY_FORMAT}`
146719
146779
  - do NOT pre-shape their output with a finding schema
146720
146780
  - do NOT mention the other lenses (independence is the point)
146721
146781
 
146722
- 5. **aggregate, draft, self-critique**: merge findings; de-dup overlaps; trace each finding yourself. drop praise, style preferences, speculative/unverified claims, findings about pre-existing code unrelated to the new commits, anything not actionable, and anything that re-states prior review feedback (heuristic: if the finding's root cause lives in lines the *new commits* added or modified, it's in scope; otherwise drop). also drop **bloat-shaped findings** \u2014 proposed fixes that would add defensive checks for cases that can't happen, abstractions used once, comments restating obvious code, tests asserting tautologies, or "just-in-case" guards. subagents are fallible and bias toward recommending changes; the bar for an actionable inline comment is sound + correct + elegant. recommending a change that improves only one of the three (or degrades elegance to nominally improve correctness) makes the codebase worse, not better. To compute "lines the new commits added or modified": if \`incrementalDiffPath\` from step 1 is present, use it directly. Otherwise, take the prior Pullfrog review's \`commit_id\` (returned alongside each entry from \`${t2("list_pull_request_reviews")}\` in step 3) and run \`git diff <prior-review-sha>..HEAD\` to isolate the lines added since that review. draft inline comments with NEW line numbers from the full PR diff \u2014 every comment must be actionable, 2-3 sentences max.
146782
+ 6. **aggregate, draft, self-critique**: merge findings; de-dup overlaps; trace each finding yourself. drop praise, style preferences, speculative/unverified claims, findings about pre-existing code unrelated to the new commits, anything not actionable, and anything that re-states prior review feedback (heuristic: if the finding's root cause lives in lines the *new commits* added or modified, it's in scope; otherwise drop). also drop **bloat-shaped findings** \u2014 proposed fixes that would add defensive checks for cases that can't happen, abstractions used once, comments restating obvious code, tests asserting tautologies, or "just-in-case" guards. subagents are fallible and bias toward recommending changes; the bar for an actionable inline comment is sound + correct + elegant. recommending a change that improves only one of the three (or degrades elegance to nominally improve correctness) makes the codebase worse, not better. To compute "lines the new commits added or modified": if \`incrementalDiffPath\` from step 2 is present, use it directly. Otherwise, take the prior Pullfrog review's \`commit_id\` (returned alongside each entry from \`${t2("list_pull_request_reviews")}\` in step 4) and run \`git diff <prior-review-sha>..HEAD\` to isolate the lines added since that review. draft inline comments with NEW line numbers from the full PR diff \u2014 every comment must be actionable, 2-3 sentences max.
146723
146783
 
146724
- 6. **build the review body** \u2014 a single "Reviewed changes" section: summarize at the logical-change level, not per-file. each bullet starts with a past-tense verb (e.g. \`- Extracted shared CLI runtime into a single module\`, \`- Renamed package to pullfrog\`). avoid file paths unless they add clarity. if the changes can be described in one sentence, use one sentence \u2014 no bullets needed. do NOT include a separate "Prior review feedback" checklist; that's tracked in the rolling PR summary snapshot for the next agent run, and surfacing it in the user-facing body is noise (changes that addressed prior feedback are already covered by the Reviewed-changes bullets). in some cases you may receive a complete diff for the whole pull request instead of an incremental one \u2014 when this happens, you will need to determine what changes have happened since Pullfrog's most recent review.
146784
+ 7. **build the review body** \u2014 a single "Reviewed changes" section: summarize at the logical-change level, not per-file. each bullet starts with a past-tense verb (e.g. \`- Extracted shared CLI runtime into a single module\`, \`- Renamed package to pullfrog\`). avoid file paths unless they add clarity. if the changes can be described in one sentence, use one sentence \u2014 no bullets needed. do NOT include a separate "Prior review feedback" checklist; that's tracked in the rolling PR summary snapshot for the next agent run, and surfacing it in the user-facing body is noise (changes that addressed prior feedback are already covered by the Reviewed-changes bullets). in some cases you may receive a complete diff for the whole pull request instead of an incremental one \u2014 when this happens, you will need to determine what changes have happened since Pullfrog's most recent review.
146725
146785
 
146726
- 7. Submit \u2014 Do NOT call \`report_progress\` or \`create_issue_comment\` \u2014 the review is the final record and the progress comment will be cleaned up automatically. Follow these rules:
146786
+ 8. Submit \u2014 every run must end with EXACTLY ONE of \`${t2("create_pull_request_review")}\` (substantive review) or \`${t2("report_progress")}\` (no-review acknowledgement). do NOT call \`create_issue_comment\` for review output.
146787
+
146788
+ Same callout-intensity ladder as Review mode \u2014 \`[!CAUTION]\` (large red, "will break") \u2192 \`[!IMPORTANT]\` (large purple, "must address before merging") \u2192 \`[!NOTE]\` (small blue, "FYI") \u2192 no callout (plain text). And the same Fix-button lever: the footer renders a Fix button on every non-approving review, so \`approved: true\` suppresses it. Wrapping mergeable feedback in \`[!IMPORTANT]\` trains users to click Fix on reviews that don't need fixing \u2014 pick the tier the author's actual next action justifies.
146789
+
146790
+ Follow these rules:
146727
146791
  - note: the first create_pull_request_review submission may error with a one-time diff-coverage nudge listing unread TOC regions. retry the same call to proceed \u2014 optionally after reading the listed ranges. the pre-flight will not block again this session.
146728
- - IF NO NEW ISSUES, NON-SUBSTANTIVE CHANGES ONLY (trivial formatting, import reordering, comment tweaks): do NOT submit a review. Do NOT call \`report_progress\`. Exit \u2014 the progress comment will be cleaned up automatically.
146729
- - ELSE IF NEW CRITICAL ISSUES (blocks merge): call \`${t2("create_pull_request_review")}\` with \`approved: false\`, all comments, and the review body. body opens with a GitHub alert blockquote (e.g. \`> [!CAUTION]\\n> This PR introduces ...\`), then the Reviewed-changes summary.
146730
- - ELSE IF NEW RECOMMENDED CHANGES (non-critical): call \`${t2("create_pull_request_review")}\` with \`approved: false\`, all comments, and the review body. body opens with \`> [!IMPORTANT]\\n> ...\` alert, then the Reviewed-changes summary.
146792
+ - IF NO NEW ISSUES, NON-SUBSTANTIVE CHANGES ONLY (trivial formatting, import reordering, comment tweaks): do NOT submit a review. Instead call \`${t2("report_progress")}\` with a 1-2 sentence note explaining no review was warranted (e.g. "No new issues. Changes since last review are formatting-only."). this leaves a visible signal that the run completed.
146793
+ - ELSE IF NEW CRITICAL ISSUES (blocks merge \u2014 bugs, security, data loss, broken core flows): call \`${t2("create_pull_request_review")}\` with \`approved: false\`, all comments, and the review body. body opens with \`> [!CAUTION]\\n> This PR introduces ...\`, then the Reviewed-changes summary.
146794
+ - ELSE IF NEW MUST-ADDRESS NON-CRITICAL FINDINGS (real consequences if shipped \u2014 incorrect behavior, missing validation, regressions the author should fix before merge): call \`${t2("create_pull_request_review")}\` with \`approved: false\`, all comments, and the review body. body opens with \`> [!IMPORTANT]\\n> ...\`, then the Reviewed-changes summary. Do NOT use this tier for nits, style preferences, or "consider also" suggestions.
146795
+ - ELSE IF NEW MINOR SUGGESTIONS ONLY (single-line nits, doc/comment polish, defer-able observations, "rough edges"): call \`${t2("create_pull_request_review")}\` with \`approved: false\`, all comments, and the review body. body opens directly with \`Reviewed the following changes:\\n\` (NO alert blockquote), then the Reviewed-changes summary.
146796
+ - ELSE IF INFORMATIONAL OBSERVATIONS (mergeable as-is, but worth surfacing \u2014 e.g. prior feedback addressed cleanly with one minor stale doc reference, or a noteworthy positive observation): call \`${t2("create_pull_request_review")}\` with \`approved: true\`, NO inline comments, and the review body. body opens with \`> [!NOTE]\\n> ...\` alert, then the Reviewed-changes summary. If a point is concrete enough to anchor to a line, downgrade the whole review to "minor suggestions only" (\`approved: false\`) instead \u2014 \`[!NOTE]\` and inline comments don't mix.
146731
146797
  - ELSE IF NO NEW ISSUES, SUBSTANTIVE CHANGES (new functionality, behavior changes, or fixes to prior review feedback): call \`${t2("create_pull_request_review")}\` to create a PR review. If all previous reviews have been properly addressed and no new issues were discovered, you can set \`approved: true\`. body opens with \`No new issues. Reviewed the following changes:\\n\`, then the Reviewed-changes summary.`
146732
146798
  },
146733
146799
  {
@@ -146735,61 +146801,63 @@ ${PR_SUMMARY_FORMAT}`
146735
146801
  description: "Create plans, break down tasks, outline steps, analyze requirements, understand scope of work, or provide task breakdowns",
146736
146802
  prompt: `### Checklist
146737
146803
 
146738
- 1. Analyze the task and gather context:
146804
+ 1. **task list**: create your task list for this run as your first action.
146805
+
146806
+ 2. Analyze the task and gather context:
146739
146807
  - read AGENTS.md and relevant codebase files
146740
146808
  - understand the architecture and constraints
146741
146809
 
146742
- 2. Produce a structured, actionable plan with clear milestones.
146743
-
146744
- 3. Call \`${t2("report_progress")}\` with the plan.
146810
+ 3. Produce a structured, actionable plan with clear milestones.
146745
146811
 
146746
- ${learningsStep(t2, 4)}`
146812
+ 4. Call \`${t2("report_progress")}\` with the plan.`
146747
146813
  },
146748
146814
  {
146749
146815
  name: "Fix",
146750
146816
  description: "Fix CI failures; debug failing tests or builds; investigate and resolve check suite failures",
146751
146817
  prompt: `### Checklist
146752
146818
 
146753
- 1. Checkout the PR branch via \`${t2("checkout_pr")}\`.
146819
+ 1. **task list**: create your task list for this run as your first action.
146754
146820
 
146755
- 2. Fetch check suite logs via \`${t2("get_check_suite_logs")}\`.
146821
+ 2. Checkout the PR branch via \`${t2("checkout_pr")}\`.
146756
146822
 
146757
- 3. **CRITICAL**: verify the failure was INTRODUCED BY THIS PR before fixing. If unrelated, abort and report.
146823
+ 3. Fetch check suite logs via \`${t2("get_check_suite_logs")}\`.
146758
146824
 
146759
- 4. Diagnose and fix:
146825
+ 4. **CRITICAL**: verify the failure was INTRODUCED BY THIS PR before fixing. If unrelated, abort and report.
146826
+
146827
+ 5. Diagnose and fix:
146760
146828
  - read the workflow file, reproduce locally with the EXACT same commands CI runs
146761
146829
  - fix the issue using your native file and shell tools
146762
146830
  - verify the fix by re-running the exact CI command
146763
146831
  - review the diff before committing \u2014 verify only the fix is present, no debug artifacts, no unrelated changes. the fix should be clean enough that a senior engineer would approve without hesitation.
146764
146832
  - commit locally via shell (\`git add . && git commit -m "..."\`)
146765
146833
 
146766
- 5. Finalize:
146834
+ 6. Finalize:
146767
146835
  - 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)}`
146836
+ - call \`${t2("report_progress")}\` with the diagnosis and fix summary (or the exact push error if push failed)`
146771
146837
  },
146772
146838
  {
146773
146839
  name: "ResolveConflicts",
146774
146840
  description: "Resolve merge conflicts in a PR branch against the base branch",
146775
146841
  prompt: `### Checklist
146776
146842
 
146777
- 1. **Setup**:
146843
+ 1. **task list**: create your task list for this run as your first action.
146844
+
146845
+ 2. **Setup**:
146778
146846
  - Call \`${t2("checkout_pr")}\` to get the PR branch.
146779
146847
  - Call \`${t2("get_pull_request")}\` to identify the base branch (e.g., 'main').
146780
146848
  - Call \`${t2("git_fetch")}\` to fetch the base branch.
146781
146849
 
146782
- 2. **Merge Attempt**:
146850
+ 3. **Merge Attempt**:
146783
146851
  - Run \`git merge origin/<base_branch>\` via shell.
146784
- - If it succeeds automatically, confirm a clean working tree, push via \`${t2("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*), and call \`${t2("report_progress")}\` with a brief success note or the exact push error if push failed \u2014 **then stop; do not run steps 3\u20134.**
146785
- - If it fails (conflicts), resolve them manually (continue to steps 3\u20134).
146852
+ - If it succeeds automatically, confirm a clean working tree, push via \`${t2("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*), and call \`${t2("report_progress")}\` with a brief success note or the exact push error if push failed \u2014 **then stop; do not run steps 4\u20135.**
146853
+ - If it fails (conflicts), resolve them manually (continue to steps 4\u20135).
146786
146854
 
146787
- 3. **Resolve Conflicts**:
146855
+ 4. **Resolve Conflicts**:
146788
146856
  - Run \`git status\` or parse the merge output to find the list of conflicting files.
146789
146857
  - For each conflicting file: read it, find the conflict markers (\`<<<<<<<\`, \`=======\`, \`>>>>>>>\`), understand the code context, and rewrite the file with the correct resolution. Remove all markers.
146790
146858
  - Verify the file syntax is correct after resolution.
146791
146859
 
146792
- 4. **Finalize**:
146860
+ 5. **Finalize**:
146793
146861
  - Run a final verification (build/test) to ensure the resolution works.
146794
146862
  - \`git add . && git commit -m "resolve merge conflicts"\`
146795
146863
  - confirm a clean working tree, then push via \`${t2("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*)
@@ -146800,24 +146868,43 @@ ${learningsStep(t2, 6)}`
146800
146868
  description: "General-purpose tasks that don't fit other modes: answering questions, adding comments, labeling, running ad-hoc commands, or any direct request",
146801
146869
  prompt: `### Checklist
146802
146870
 
146803
- 1. Analyze the task. For simple operations (labeling, commenting, answering questions, running a single command), handle directly.
146871
+ 1. **task list**: create your task list for this run as your first action.
146872
+
146873
+ 2. Analyze the task. For simple operations (labeling, commenting, answering questions, running a single command), handle directly.
146804
146874
 
146805
- 2. For substantial work \u2014 code changes across multiple files, multi-step investigations:
146875
+ 3. For substantial work \u2014 code changes across multiple files, multi-step investigations:
146806
146876
  - plan your approach before starting
146807
146877
  - use native file and shell tools for local operations
146808
146878
  - use ${pullfrogMcpName} MCP tools for GitHub/git operations
146809
146879
  - if code changes are needed: review your own diff before committing \u2014 verify only intended changes are present, no debug artifacts remain, and the changes are clean enough that a senior engineer would approve without hesitation
146810
146880
 
146811
- 3. Finalize:
146881
+ 4. Finalize:
146812
146882
  - 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
146883
  - 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)}`
146884
+ - if the task involved labeling, commenting, or other GitHub operations, perform those directly`
146817
146885
  }
146818
146886
  ];
146819
146887
  }
146820
146888
  var modes = computeModes("opencode");
146889
+ var NON_COMMITTING_MODES = /* @__PURE__ */ new Set([
146890
+ "Review",
146891
+ "IncrementalReview",
146892
+ "Plan"
146893
+ ]);
146894
+
146895
+ // toolState.ts
146896
+ function initToolState(params) {
146897
+ const resolved = parseProgressComment(params.progressComment);
146898
+ if (resolved) {
146899
+ log.info(`\xBB using pre-created progress comment: ${resolved.id} (${resolved.type})`);
146900
+ }
146901
+ return {
146902
+ progressComment: resolved,
146903
+ hadProgressComment: !!resolved,
146904
+ backgroundProcesses: /* @__PURE__ */ new Map(),
146905
+ usageEntries: []
146906
+ };
146907
+ }
146821
146908
 
146822
146909
  // agents/claude.ts
146823
146910
  import { execFileSync as execFileSync3 } from "node:child_process";
@@ -146913,6 +147000,17 @@ async function installFromNpmTarball(params) {
146913
147000
  // utils/providerErrors.ts
146914
147001
  var statusKey = `\\b(?:status[_ ]?code|http[_ ]?status|status)["']?\\s*[:=]\\s*["']?`;
146915
147002
  var PROVIDER_ERROR_PATTERNS = [
147003
+ // auth patterns must come BEFORE rate-limit patterns. OpenRouter 401 error
147004
+ // payloads carry `x-ratelimit-*` response headers in the dump, and the
147005
+ // free-form rate-limit regex below would otherwise win on word-boundary
147006
+ // matches inside header names. canonical 401 messages: OpenRouter returns
147007
+ // `{"error":{"message":"User not found","code":401}}` for disabled or
147008
+ // invalid keys (https://openai.luzhipeng.com/docs/api/reference/errors-and-debugging).
147009
+ { regex: new RegExp(`${statusKey}401\\b`, "i"), label: "auth error (401)" },
147010
+ { regex: new RegExp(`${statusKey}403\\b`, "i"), label: "auth error (403)" },
147011
+ { regex: /\bUser not found\b/i, label: "auth error (invalid/disabled key)" },
147012
+ { regex: /\bInvalid authentication\b/i, label: "auth error (invalid credentials)" },
147013
+ { regex: /\bNo auth credentials found\b/i, label: "auth error (missing credentials)" },
146916
147014
  { regex: new RegExp(`${statusKey}429\\b`, "i"), label: "rate limited (429)" },
146917
147015
  { regex: new RegExp(`${statusKey}500\\b`, "i"), label: "provider 500 error" },
146918
147016
  { regex: new RegExp(`${statusKey}503\\b`, "i"), label: "provider unavailable (503)" },
@@ -146976,7 +147074,7 @@ function installBundledSkills(params) {
146976
147074
  writeFileSync6(join9(skillDir, "SKILL.md"), content);
146977
147075
  }
146978
147076
  }
146979
- log.info(`installed bundled skills: ${BUNDLED_SKILL_NAMES.join(", ")}`);
147077
+ log.success(`installed bundled skills: ${BUNDLED_SKILL_NAMES.join(", ")}`);
146980
147078
  }
146981
147079
  function addSkill(params) {
146982
147080
  const result = spawnSync5(
@@ -147001,7 +147099,7 @@ function addSkill(params) {
147001
147099
  }
147002
147100
  );
147003
147101
  if (result.status === 0) {
147004
- log.info(`installed ${params.skill} skill (${params.agent})`);
147102
+ log.success(`installed ${params.skill} skill (${params.agent})`);
147005
147103
  } else {
147006
147104
  const stderr = (result.stderr?.toString() || "").trim();
147007
147105
  const errorMsg = result.error ? result.error.message : stderr;
@@ -147053,6 +147151,13 @@ var ThinkingTimer = class {
147053
147151
 
147054
147152
  // agents/postRun.ts
147055
147153
  import { readFile } from "node:fs/promises";
147154
+ function getUnsubmittedReview(toolState) {
147155
+ const mode = toolState.selectedMode;
147156
+ if (mode !== "Review" && mode !== "IncrementalReview") return null;
147157
+ if (toolState.review || toolState.finalSummaryWritten) return null;
147158
+ if (!toolState.hadProgressComment) return null;
147159
+ return mode;
147160
+ }
147056
147161
  var MAX_HOOK_OUTPUT_CHARS = 4096;
147057
147162
  function truncateHookOutput(raw2) {
147058
147163
  if (raw2.length <= MAX_HOOK_OUTPUT_CHARS) return raw2;
@@ -147114,39 +147219,72 @@ function buildSummaryStalePrompt(filePath) {
147114
147219
  "if the diff is genuinely too small or noisy to warrant rewriting (e.g. a one-line typo fix, a comment tweak, a formatting-only change), it's fine to leave the structure as-is \u2014 but at minimum confirm you considered it by appending one line to the appropriate section noting the run. silence is not an option; the snapshot is what the next review run reads as context."
147115
147220
  ].join("\n");
147116
147221
  }
147117
- async function collectPostRunIssues(params) {
147222
+ function buildUnsubmittedReviewPrompt(mode) {
147223
+ if (mode === "Review") {
147224
+ return [
147225
+ `MISSING REVIEW OUTPUT \u2014 you selected Review mode but stopped without calling \`create_pull_request_review\`. the user has no visible signal that this run produced anything; the progress comment will be deleted on exit and no review will appear on the PR.`,
147226
+ "",
147227
+ "call `create_pull_request_review` now with your aggregated review (body + inline comments). pick the tier per the mode prompt \u2014 Review mode has no no-submit exit, so even informational `> [!NOTE]` reviews and `No new issues found.` reviews must be submitted (both use `approved: true`). the first call may error once with a diff-coverage nudge \u2014 retry the same call to proceed.",
147228
+ "",
147229
+ "do NOT stop again until `create_pull_request_review` has been called successfully."
147230
+ ].join("\n");
147231
+ }
147232
+ return [
147233
+ `MISSING REVIEW OUTPUT \u2014 you selected IncrementalReview mode but stopped without calling \`create_pull_request_review\` or \`report_progress\`. the user has no visible signal that this run produced anything; the progress comment will be deleted on exit and no review will appear on the PR.`,
147234
+ "",
147235
+ "do exactly one of:",
147236
+ "- if you have findings: call `create_pull_request_review` now with your aggregated review (body + inline comments). the first call may error once with a diff-coverage nudge \u2014 retry the same call to proceed.",
147237
+ "- if there are genuinely no actionable findings since the last review (e.g. only formatting / comment / lockfile changes): call `report_progress` with a 1-2 sentence summary explaining that no review was warranted.",
147238
+ "",
147239
+ "do NOT stop again until one of those tools has been called successfully."
147240
+ ].join("\n");
147241
+ }
147242
+ async function collectPostRunIssues(ctx, options = {}) {
147118
147243
  const issues = {};
147119
- if (params.stopScript) {
147120
- const failure = await executeStopHook(params.stopScript);
147244
+ if (ctx.stopScript) {
147245
+ const failure = await executeStopHook(ctx.stopScript);
147121
147246
  if (failure) issues.stopHook = failure;
147122
147247
  }
147123
147248
  const status = getGitStatus();
147124
- if (status) issues.dirtyTree = status;
147125
- if (params.summaryFilePath && params.summarySeed !== void 0) {
147126
- const stale = await isSummaryUnchanged(params.summaryFilePath, params.summarySeed);
147127
- if (stale) issues.summaryStale = { filePath: params.summaryFilePath };
147249
+ const mode = ctx.toolState.selectedMode;
147250
+ if (status) {
147251
+ if (mode && NON_COMMITTING_MODES.has(mode)) {
147252
+ log.info(`\xBB dirty-tree gate suppressed: mode \`${mode}\` does not commit`);
147253
+ } else {
147254
+ issues.dirtyTree = status;
147255
+ }
147128
147256
  }
147257
+ const summaryFilePath2 = ctx.toolState.summaryFilePath;
147258
+ const summarySeed = ctx.toolState.summarySeed;
147259
+ if (!options.skipSummaryStale && summaryFilePath2 && summarySeed !== void 0) {
147260
+ const stale = await isSummaryUnchanged(summaryFilePath2, summarySeed);
147261
+ if (stale) issues.summaryStale = { filePath: summaryFilePath2 };
147262
+ }
147263
+ const unsubmittedMode = getUnsubmittedReview(ctx.toolState);
147264
+ if (unsubmittedMode) issues.unsubmittedReview = unsubmittedMode;
147129
147265
  return issues;
147130
147266
  }
147131
147267
  function buildPostRunPrompt(issues) {
147132
147268
  const parts = [];
147133
147269
  if (issues.stopHook) parts.push(buildStopHookPrompt(issues.stopHook));
147270
+ if (issues.unsubmittedReview) {
147271
+ parts.push(buildUnsubmittedReviewPrompt(issues.unsubmittedReview));
147272
+ }
147134
147273
  if (issues.dirtyTree) parts.push(buildCommitPrompt(issues.dirtyTree));
147135
147274
  if (issues.summaryStale) parts.push(buildSummaryStalePrompt(issues.summaryStale.filePath));
147136
147275
  return parts.join("\n\n---\n\n");
147137
147276
  }
147138
- function buildLearningsReflectionPrompt(agentId) {
147139
- const t2 = (name) => formatMcpToolRef(agentId, name);
147277
+ function buildLearningsReflectionPrompt(filePath) {
147140
147278
  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?`,
147279
+ `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
147280
  "",
147143
- `if so, call \`${t2("update_learnings")}\` to persist it.`,
147281
+ `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
147282
  "",
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.`
147283
+ `keep the file healthy:`,
147284
+ `- only add bullets when the finding is high-confidence AND broadly useful. skip speculative, one-off, or "maybe" findings.`,
147285
+ `- prune bullets that are clearly wrong, no longer relevant, or low-signal (rarely useful). a focused, accurate file beats a long stale one.`,
147286
+ `- 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.`,
147287
+ `- 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
147288
  ].join("\n");
147151
147289
  }
147152
147290
  async function runPostRunRetryLoop(params) {
@@ -147158,10 +147296,8 @@ async function runPostRunRetryLoop(params) {
147158
147296
  let summaryStaleNudged = false;
147159
147297
  while (gateResumeCount < MAX_POST_RUN_RETRIES) {
147160
147298
  if (!result.success) break;
147161
- const issues = await collectPostRunIssues({
147162
- stopScript: params.stopScript,
147163
- summaryFilePath: summaryStaleNudged ? void 0 : params.summaryFilePath,
147164
- summarySeed: summaryStaleNudged ? void 0 : params.summarySeed
147299
+ const issues = await collectPostRunIssues(params.ctx, {
147300
+ skipSummaryStale: summaryStaleNudged
147165
147301
  });
147166
147302
  if (issues.summaryStale) summaryStaleNudged = true;
147167
147303
  finalIssues = issues;
@@ -147209,7 +147345,7 @@ async function runPostRunRetryLoop(params) {
147209
147345
  gateResumeCount++;
147210
147346
  }
147211
147347
  if (gateResumeCount > 0 && result.success && hasPostRunIssues(finalIssues)) {
147212
- finalIssues = await collectPostRunIssues({ stopScript: params.stopScript });
147348
+ finalIssues = await collectPostRunIssues(params.ctx, { skipSummaryStale: true });
147213
147349
  }
147214
147350
  if (result.success && finalIssues.stopHook) {
147215
147351
  const retryNote = gateResumeCount > 0 ? ` after ${gateResumeCount} retry ${gateResumeCount === 1 ? "attempt" : "attempts"}` : "";
@@ -147220,6 +147356,16 @@ async function runPostRunRetryLoop(params) {
147220
147356
  usage: aggregatedUsage
147221
147357
  };
147222
147358
  }
147359
+ if (result.success && finalIssues.unsubmittedReview) {
147360
+ const retryNote = gateResumeCount > 0 ? ` after ${gateResumeCount} retry ${gateResumeCount === 1 ? "attempt" : "attempts"}` : "";
147361
+ const expected = finalIssues.unsubmittedReview === "Review" ? "create_pull_request_review" : "create_pull_request_review or report_progress";
147362
+ return {
147363
+ ...result,
147364
+ success: false,
147365
+ error: `${finalIssues.unsubmittedReview} mode finished without calling ${expected}${retryNote}`,
147366
+ usage: aggregatedUsage
147367
+ };
147368
+ }
147223
147369
  return { ...result, usage: aggregatedUsage };
147224
147370
  }
147225
147371
 
@@ -147336,6 +147482,12 @@ function resolveEffort(model) {
147336
147482
  if (model?.includes("opus")) return "max";
147337
147483
  return "high";
147338
147484
  }
147485
+ function tailLines(text, maxCodeUnits) {
147486
+ if (text.length <= maxCodeUnits) return text;
147487
+ const tail = text.slice(-maxCodeUnits);
147488
+ const firstNewline = tail.indexOf("\n");
147489
+ return firstNewline > 0 && firstNewline < tail.length - 1 ? tail.slice(firstNewline + 1) : tail;
147490
+ }
147339
147491
  async function runClaude(params) {
147340
147492
  const startTime = performance6.now();
147341
147493
  let eventCount = 0;
@@ -147343,6 +147495,8 @@ async function runClaude(params) {
147343
147495
  let finalOutput = "";
147344
147496
  let sessionId;
147345
147497
  let resultErrorSubtype = null;
147498
+ let lastResultError = null;
147499
+ let syntheticStopFailure = false;
147346
147500
  let accumulatedTokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
147347
147501
  let accumulatedCostUsd = 0;
147348
147502
  let tokensLogged = false;
@@ -147425,6 +147579,16 @@ async function runClaude(params) {
147425
147579
  if (event.session_id) sessionId = event.session_id;
147426
147580
  const subtype = event.subtype || "unknown";
147427
147581
  const numTurns = event.num_turns || 0;
147582
+ if (event.is_error === true && subtype === "success") {
147583
+ const apiStatus = event.api_error_status;
147584
+ lastResultError = event.result?.trim() || `claude reported is_error=true with no result text (api_error_status=${apiStatus ?? "unknown"})`;
147585
+ resultErrorSubtype = subtype;
147586
+ syntheticStopFailure = true;
147587
+ log.info(
147588
+ `\xBB ${params.label} result error: subtype=${subtype}, api_error_status=${apiStatus ?? "unknown"}, message=${lastResultError}`
147589
+ );
147590
+ return;
147591
+ }
147428
147592
  if (subtype === "success") {
147429
147593
  const usage = event.usage;
147430
147594
  const inputTokens = usage?.input_tokens || 0;
@@ -147447,12 +147611,15 @@ async function runClaude(params) {
147447
147611
  }
147448
147612
  } else if (subtype === "error_max_turns") {
147449
147613
  resultErrorSubtype = subtype;
147614
+ lastResultError = event.errors?.join("\n").trim() || null;
147450
147615
  log.info(`\xBB ${params.label} max turns reached: ${JSON.stringify(event)}`);
147451
147616
  } else if (subtype === "error_during_execution") {
147452
147617
  resultErrorSubtype = subtype;
147618
+ lastResultError = event.errors?.join("\n").trim() || null;
147453
147619
  log.info(`\xBB ${params.label} execution error: ${JSON.stringify(event)}`);
147454
147620
  } else if (subtype.startsWith("error")) {
147455
147621
  resultErrorSubtype = subtype;
147622
+ lastResultError = event.errors?.join("\n").trim() || null;
147456
147623
  log.info(`\xBB ${params.label} result: subtype=${subtype}, data=${JSON.stringify(event)}`);
147457
147624
  } else {
147458
147625
  log.info(`\xBB ${params.label} result: subtype=${subtype}, data=${JSON.stringify(event)}`);
@@ -147560,14 +147727,15 @@ async function runClaude(params) {
147560
147727
  if (stderrContext) log.info(`\xBB last stderr output:
147561
147728
  ${stderrContext}`);
147562
147729
  }
147563
- if (!tokensLogged && (accumulatedTokens.input > 0 || accumulatedTokens.output > 0 || accumulatedTokens.cacheRead > 0 || accumulatedTokens.cacheWrite > 0)) {
147730
+ if (!tokensLogged && !syntheticStopFailure && (accumulatedTokens.input > 0 || accumulatedTokens.output > 0 || accumulatedTokens.cacheRead > 0 || accumulatedTokens.cacheWrite > 0)) {
147564
147731
  logTokenTable({ ...accumulatedTokens, costUsd: accumulatedCostUsd });
147565
147732
  tokensLogged = true;
147566
147733
  }
147567
147734
  const usage = buildUsage();
147568
147735
  if (result.exitCode !== 0) {
147569
147736
  const errorContext = lastProviderError ? ` (${lastProviderError})` : "";
147570
- const errorMessage = result.stderr || result.stdout || `unknown error - no output from Claude CLI${errorContext}`;
147737
+ const truncatedStdout = result.stdout ? tailLines(result.stdout, 2048) : "";
147738
+ const errorMessage = lastResultError || result.stderr || truncatedStdout || `unknown error - no output from Claude CLI${errorContext}`;
147571
147739
  log.error(
147572
147740
  `${params.label} exited with code ${result.exitCode}${errorContext}: ${errorMessage}`
147573
147741
  );
@@ -147594,7 +147762,7 @@ ${stderrContext}`);
147594
147762
  return {
147595
147763
  success: false,
147596
147764
  output: finalOutput || output,
147597
- error: `result subtype: ${resultErrorSubtype}`,
147765
+ error: lastResultError || `result subtype: ${resultErrorSubtype}`,
147598
147766
  usage,
147599
147767
  sessionId
147600
147768
  };
@@ -147724,12 +147892,10 @@ var claude = agent({
147724
147892
  args: [...baseArgs, "-p", ctx.instructions.full]
147725
147893
  });
147726
147894
  return runPostRunRetryLoop({
147895
+ ctx,
147727
147896
  initialResult: result,
147728
147897
  initialUsage: result.usage,
147729
- stopScript: ctx.stopScript,
147730
- summaryFilePath: ctx.summaryFilePath,
147731
- summarySeed: ctx.summarySeed,
147732
- reflectionPrompt: buildLearningsReflectionPrompt("claude"),
147898
+ reflectionPrompt: ctx.toolState.learningsFilePath ? buildLearningsReflectionPrompt(ctx.toolState.learningsFilePath) : void 0,
147733
147899
  canResume: (r) => Boolean(r.sessionId),
147734
147900
  resume: async (c2) => {
147735
147901
  const sessionId = c2.previousResult.sessionId;
@@ -147745,9 +147911,92 @@ var claude = agent({
147745
147911
 
147746
147912
  // agents/opencode.ts
147747
147913
  import { execFileSync as execFileSync4 } from "node:child_process";
147748
- import { mkdirSync as mkdirSync5 } from "node:fs";
147914
+ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "node:fs";
147749
147915
  import { join as join11 } from "node:path";
147750
147916
  import { performance as performance7 } from "node:perf_hooks";
147917
+
147918
+ // agents/opencodePlugin.ts
147919
+ var PULLFROG_BUS_EVENT_TYPE = "pullfrog_bus_event";
147920
+ var PULLFROG_OPENCODE_PLUGIN_FILENAME = "pullfrog-events.ts";
147921
+ var PULLFROG_OPENCODE_PLUGIN_SOURCE = `// AUTOGENERATED by Pullfrog. do not edit; it'll be overwritten on the next run.
147922
+ // surfaces opencode subagent activity that the CLI's run-loop discards. see
147923
+ // action/agents/opencodePlugin.ts in pullfrog/app for why this exists. lives
147924
+ // inside the per-run tmpdir (XDG_CONFIG_HOME/opencode/plugin/), never inside
147925
+ // the user's working tree.
147926
+
147927
+ const PULLFROG_BUS_EVENT_TYPE = ${JSON.stringify(PULLFROG_BUS_EVENT_TYPE)};
147928
+
147929
+ // the first sessionID we see on a message.part.updated event is the
147930
+ // orchestrator \u2014 opencode's run command creates exactly one top-level session
147931
+ // before any subagent is dispatched, and the user-prompt text part fires
147932
+ // before the first task tool_use. we lock that sessionID in here and use it
147933
+ // to filter: the orchestrator's events are already streamed by the CLI's
147934
+ // run-loop, so we only forward (a) all subagent events, and (b) the
147935
+ // orchestrator's task tool dispatches at status="running". the CLI only
147936
+ // emits task tool_use at status=completed (after the subagent finishes), so
147937
+ // without the early announce the parent's labeler binds subagent sessions
147938
+ // before recordTaskDispatch fires and the lens label is lost.
147939
+ let orchestratorSessionID: string | undefined;
147940
+
147941
+ function isOrchestratorTaskDispatch(part: {
147942
+ type?: string;
147943
+ tool?: string;
147944
+ state?: { status?: string };
147945
+ }): boolean {
147946
+ if (part.type !== "tool") return false;
147947
+ if (part.tool !== "task") return false;
147948
+ // only forward at status="running" (not "pending"). at pending the
147949
+ // state.input is still {} \u2014 the orchestrator has emitted the part shell
147950
+ // but the LLM hasn't filled in description/subagent_type/prompt yet. by
147951
+ // running, input is populated and recordTaskDispatch can derive the lens
147952
+ // label correctly.
147953
+ return part.state?.status === "running";
147954
+ }
147955
+
147956
+ export default async function pullfrogEventsPlugin() {
147957
+ return {
147958
+ event: async (input: {
147959
+ event: {
147960
+ type: string;
147961
+ properties?: {
147962
+ part?: {
147963
+ sessionID?: string;
147964
+ type?: string;
147965
+ tool?: string;
147966
+ state?: { status?: string };
147967
+ };
147968
+ };
147969
+ };
147970
+ }) => {
147971
+ const event = input.event;
147972
+ if (!event || typeof event !== "object") return;
147973
+ if (event.type !== "message.part.updated") return;
147974
+ const part = event.properties?.part;
147975
+ const sessionID = part?.sessionID;
147976
+ if (typeof sessionID !== "string" || sessionID.length === 0) return;
147977
+ if (orchestratorSessionID === undefined) orchestratorSessionID = sessionID;
147978
+
147979
+ if (sessionID === orchestratorSessionID) {
147980
+ // skip orchestrator events EXCEPT early task dispatches.
147981
+ if (!part || !isOrchestratorTaskDispatch(part)) return;
147982
+ }
147983
+
147984
+ try {
147985
+ const line = JSON.stringify({
147986
+ type: PULLFROG_BUS_EVENT_TYPE,
147987
+ bus_event: event,
147988
+ });
147989
+ process.stdout.write(line + "\\n");
147990
+ } catch {
147991
+ // a circular reference or BigInt etc. would throw; swallow rather
147992
+ // than letting a single bad event take down the plugin.
147993
+ }
147994
+ },
147995
+ };
147996
+ }
147997
+ `;
147998
+
147999
+ // agents/opencode.ts
147751
148000
  async function installOpencodeCli() {
147752
148001
  return await installFromNpmTarball({
147753
148002
  packageName: "opencode-ai",
@@ -147757,6 +148006,8 @@ async function installOpencodeCli() {
147757
148006
  });
147758
148007
  }
147759
148008
  var PULLFROG_OPENCODE_OUTPUT_LIMIT = 5e3;
148009
+ var GEMINI_3_DIRECT_THINKING_LEVEL = "medium";
148010
+ var GEMINI_3_DIRECT_API_IDS = ["gemini-3.1-pro-preview", "gemini-3-flash-preview"];
147760
148011
  function buildSecurityConfig(ctx, model) {
147761
148012
  const config3 = {
147762
148013
  permission: {
@@ -147770,7 +148021,21 @@ function buildSecurityConfig(ctx, model) {
147770
148021
  mcp: {
147771
148022
  [pullfrogMcpName]: { type: "remote", url: ctx.mcpServerUrl }
147772
148023
  },
147773
- agent: buildReviewerAgentConfig()
148024
+ agent: buildReviewerAgentConfig(),
148025
+ provider: {
148026
+ google: {
148027
+ models: Object.fromEntries(
148028
+ GEMINI_3_DIRECT_API_IDS.map((id) => [
148029
+ id,
148030
+ {
148031
+ options: {
148032
+ thinkingConfig: { thinkingLevel: GEMINI_3_DIRECT_THINKING_LEVEL }
148033
+ }
148034
+ }
148035
+ ])
148036
+ )
148037
+ }
148038
+ }
147774
148039
  };
147775
148040
  if (model) {
147776
148041
  config3.model = model;
@@ -147849,9 +148114,6 @@ async function runOpenCode(params) {
147849
148114
  const taskDispatchByCallID = /* @__PURE__ */ new Map();
147850
148115
  const pendingTaskDispatches = [];
147851
148116
  const knownNonTaskCallIDs = /* @__PURE__ */ new Set();
147852
- function isSubagentInFlight() {
147853
- return taskDispatchByCallID.size > 0 || pendingTaskDispatches.length > 0;
147854
- }
147855
148117
  function emitSubagentFinished(dispatch, status, output2, matchKind) {
147856
148118
  const subagentDuration = performance7.now() - dispatch.startedAt;
147857
148119
  const outputStr = typeof output2 === "string" ? output2 : "";
@@ -147970,18 +148232,20 @@ async function runOpenCode(params) {
147970
148232
  return;
147971
148233
  }
147972
148234
  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
- );
148235
+ if (!taskDispatchByCallID.has(toolId)) {
148236
+ const taskInput = event.part?.state?.input ?? {};
148237
+ const dispatchedLabel = labeler.recordTaskDispatch(taskInput);
148238
+ const dispatch = {
148239
+ label: dispatchedLabel,
148240
+ startedAt: performance7.now(),
148241
+ toolUseCallID: toolId
148242
+ };
148243
+ taskDispatchByCallID.set(toolId, dispatch);
148244
+ pendingTaskDispatches.push(dispatch);
148245
+ log.info(
148246
+ `\xBB dispatching subagent: ${dispatchedLabel}` + (taskInput.subagent_type ? ` (subagent_type=${taskInput.subagent_type})` : "")
148247
+ );
148248
+ }
147985
148249
  } else {
147986
148250
  knownNonTaskCallIDs.add(toolId);
147987
148251
  }
@@ -148002,6 +148266,10 @@ async function runOpenCode(params) {
148002
148266
  if (event.part?.state?.status === "completed" && event.part.state.output) {
148003
148267
  log.debug(withLabel(label, ` output: ${event.part.state.output}`));
148004
148268
  }
148269
+ if (event.part?.state?.status === "error") {
148270
+ const errorMsg = event.part.state.output ?? "(no error message)";
148271
+ log.info(withLabel(label, `\xBB tool call failed: ${errorMsg}`));
148272
+ }
148005
148273
  if (toolName.includes("report_progress") && params.todoTracker) {
148006
148274
  log.debug("\xBB report_progress detected, disabling todo tracking");
148007
148275
  params.todoTracker.cancel();
@@ -148088,6 +148356,53 @@ async function runOpenCode(params) {
148088
148356
  tokensLogged = true;
148089
148357
  }
148090
148358
  }
148359
+ },
148360
+ [PULLFROG_BUS_EVENT_TYPE]: async (event) => {
148361
+ const busEvent = event.bus_event;
148362
+ if (!busEvent || busEvent.type !== "message.part.updated") return;
148363
+ const part = busEvent.properties?.part;
148364
+ if (!part || typeof part.sessionID !== "string") return;
148365
+ const sessionID = part.sessionID;
148366
+ const partType = part.type;
148367
+ if (partType === "tool") {
148368
+ const status = part.state?.status;
148369
+ const partWithToolFields = part;
148370
+ const isOrchestratorTaskDispatch = partWithToolFields.tool === "task" && status === "running";
148371
+ if (isOrchestratorTaskDispatch) {
148372
+ const callID = partWithToolFields.callID;
148373
+ if (typeof callID === "string" && !taskDispatchByCallID.has(callID)) {
148374
+ const taskInput = partWithToolFields.state?.input ?? {};
148375
+ const dispatchedLabel = labeler.recordTaskDispatch(taskInput);
148376
+ const dispatch = {
148377
+ label: dispatchedLabel,
148378
+ startedAt: performance7.now(),
148379
+ toolUseCallID: callID
148380
+ };
148381
+ taskDispatchByCallID.set(callID, dispatch);
148382
+ pendingTaskDispatches.push(dispatch);
148383
+ log.info(
148384
+ `\xBB dispatching subagent: ${dispatchedLabel}` + (taskInput.subagent_type ? ` (subagent_type=${taskInput.subagent_type})` : "")
148385
+ );
148386
+ }
148387
+ return;
148388
+ }
148389
+ if (status !== "completed" && status !== "error") return;
148390
+ await handlers2.tool_use({
148391
+ type: "tool_use",
148392
+ sessionID,
148393
+ part
148394
+ });
148395
+ return;
148396
+ }
148397
+ if (partType === "step-start" || partType === "step-finish") return;
148398
+ if (partType === "text" && part.time?.end !== void 0) {
148399
+ await handlers2.text({
148400
+ type: "text",
148401
+ sessionID,
148402
+ part
148403
+ });
148404
+ return;
148405
+ }
148091
148406
  }
148092
148407
  };
148093
148408
  const recentStderr = [];
@@ -148111,13 +148426,13 @@ async function runOpenCode(params) {
148111
148426
  // never fires — producing zombie runs. detached + killGroup nukes the
148112
148427
  // whole tree.
148113
148428
  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,
148429
+ // NB: we used to pass `isPausedExternally: isSubagentInFlight` to suspend
148430
+ // the activity timer during subagent dispatches. unnecessary now that
148431
+ // our injected plugin (action/agents/opencodePlugin.ts) re-emits
148432
+ // subagent `message.part.updated` events on opencode's stdout those
148433
+ // arrive at child.stdout here, fire updateActivity(), and reset
148434
+ // lastActivityTime naturally. verified empirically in PR #634
148435
+ // (~3.3 plugin events/sec during a typical subagent run).
148121
148436
  onStdout: async (chunk) => {
148122
148437
  const text = chunk.toString();
148123
148438
  output += text;
@@ -148272,6 +148587,12 @@ var opencode = agent({
148272
148587
  XDG_CONFIG_HOME: join11(ctx.tmpdir, ".config")
148273
148588
  };
148274
148589
  mkdirSync5(join11(homeEnv.XDG_CONFIG_HOME, "opencode"), { recursive: true });
148590
+ const opencodePluginDir = join11(homeEnv.XDG_CONFIG_HOME, "opencode", "plugin");
148591
+ mkdirSync5(opencodePluginDir, { recursive: true });
148592
+ writeFileSync8(
148593
+ join11(opencodePluginDir, PULLFROG_OPENCODE_PLUGIN_FILENAME),
148594
+ PULLFROG_OPENCODE_PLUGIN_SOURCE
148595
+ );
148275
148596
  const agentBrowserVersion = getDevDependencyVersion("agent-browser");
148276
148597
  addSkill({
148277
148598
  ref: `vercel-labs/agent-browser@v${agentBrowserVersion}`,
@@ -148309,12 +148630,10 @@ var opencode = agent({
148309
148630
  args: [...baseArgs, ctx.instructions.full]
148310
148631
  });
148311
148632
  return runPostRunRetryLoop({
148633
+ ctx,
148312
148634
  initialResult: result,
148313
148635
  initialUsage: result.usage,
148314
- stopScript: ctx.stopScript,
148315
- summaryFilePath: ctx.summaryFilePath,
148316
- summarySeed: ctx.summarySeed,
148317
- reflectionPrompt: buildLearningsReflectionPrompt("opencode"),
148636
+ reflectionPrompt: ctx.toolState.learningsFilePath ? buildLearningsReflectionPrompt(ctx.toolState.learningsFilePath) : void 0,
148318
148637
  resume: async (c2) => runOpenCode({
148319
148638
  ...runParams,
148320
148639
  args: [...baseArgs, "--continue", c2.prompt]
@@ -152240,8 +152559,10 @@ var checkRepositoryAccess = async (token, repoOwner, repoName) => {
152240
152559
  const response = await githubRequest("/installation/repositories", {
152241
152560
  headers: { Authorization: `token ${token}` }
152242
152561
  });
152562
+ const ownerLower = repoOwner.toLowerCase();
152563
+ const nameLower = repoName.toLowerCase();
152243
152564
  return response.repositories.some(
152244
- (repo) => repo.owner.login === repoOwner && repo.name === repoName
152565
+ (repo) => repo.owner.login.toLowerCase() === ownerLower && repo.name.toLowerCase() === nameLower
152245
152566
  );
152246
152567
  } catch {
152247
152568
  return false;
@@ -152527,7 +152848,7 @@ ${ctx.error}` : ctx.error;
152527
152848
 
152528
152849
  // utils/gitAuthServer.ts
152529
152850
  import { randomUUID as randomUUID3 } from "node:crypto";
152530
- import { writeFileSync as writeFileSync8 } from "node:fs";
152851
+ import { writeFileSync as writeFileSync9 } from "node:fs";
152531
152852
  import { createServer as createServer2 } from "node:http";
152532
152853
  import { join as join13 } from "node:path";
152533
152854
  var CODE_TTL_MS = 5 * 60 * 1e3;
@@ -152616,7 +152937,7 @@ async function startGitAuthServer(tmpdir3) {
152616
152937
  `try{require("fs").unlinkSync("${scriptPath.replace(/\\/g, "\\\\")}")}catch(e){}`,
152617
152938
  `})}).on("error",function(){process.exit(1)})}`
152618
152939
  ].join("\n");
152619
- writeFileSync8(scriptPath, content, { mode: 448 });
152940
+ writeFileSync9(scriptPath, content, { mode: 448 });
152620
152941
  return scriptPath;
152621
152942
  }
152622
152943
  async function close() {
@@ -152890,9 +153211,9 @@ function buildPromptContext(ctx) {
152890
153211
  };
152891
153212
  }
152892
153213
  function assembleFullPrompt(ctx) {
152893
- const learningsSection = ctx.learnings ? `************* LEARNINGS *************
153214
+ const learningsSection = ctx.learningsFilePath ? `************* LEARNINGS *************
152894
153215
 
152895
- ${ctx.learnings}` : "";
153216
+ 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
153217
  const runtimeSection = `************* RUNTIME *************
152897
153218
 
152898
153219
  ${ctx.runtime}`;
@@ -152919,8 +153240,8 @@ function resolveInstructions(ctx) {
152919
153240
  if (eventContext)
152920
153241
  tocEntries.push({ label: "EVENT CONTEXT", description: "related PR/issue data" });
152921
153242
  tocEntries.push({ label: "SYSTEM", description: "persona, security, tools, workflow rules" });
152922
- if (pctx.learnings)
152923
- tocEntries.push({ label: "LEARNINGS", description: "repo-specific knowledge" });
153243
+ if (pctx.learningsFilePath)
153244
+ tocEntries.push({ label: "LEARNINGS", description: "repo-specific knowledge file path" });
152924
153245
  tocEntries.push({ label: "RUNTIME", description: "environment metadata" });
152925
153246
  const toc = buildToc(tocEntries);
152926
153247
  const full = assembleFullPrompt({
@@ -152929,7 +153250,7 @@ function resolveInstructions(ctx) {
152929
153250
  procedure,
152930
153251
  eventContext,
152931
153252
  system,
152932
- learnings: pctx.learnings,
153253
+ learningsFilePath: pctx.learningsFilePath,
152933
153254
  runtime: pctx.runtime
152934
153255
  });
152935
153256
  const event = [pctx.eventTitle, pctx.eventMetadata].filter(Boolean).join("\n\n---\n\n");
@@ -152943,6 +153264,32 @@ function resolveInstructions(ctx) {
152943
153264
  };
152944
153265
  }
152945
153266
 
153267
+ // utils/learnings.ts
153268
+ import { mkdir, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
153269
+ import { dirname as dirname4, join as join14 } from "node:path";
153270
+ var LEARNINGS_FILE_NAME = "pullfrog-learnings.md";
153271
+ var MAX_LEARNINGS_LENGTH = 1e4;
153272
+ function learningsFilePath(tmpdir3) {
153273
+ return join14(tmpdir3, LEARNINGS_FILE_NAME);
153274
+ }
153275
+ async function seedLearningsFile(params) {
153276
+ const path3 = learningsFilePath(params.tmpdir);
153277
+ await mkdir(dirname4(path3), { recursive: true });
153278
+ await writeFile2(path3, params.current ?? "", "utf8");
153279
+ return path3;
153280
+ }
153281
+ async function readLearningsFile(path3) {
153282
+ let raw2;
153283
+ try {
153284
+ raw2 = await readFile2(path3, "utf8");
153285
+ } catch {
153286
+ return null;
153287
+ }
153288
+ const trimmed = raw2.trim();
153289
+ if (trimmed.length > MAX_LEARNINGS_LENGTH) return trimmed.slice(0, MAX_LEARNINGS_LENGTH);
153290
+ return trimmed;
153291
+ }
153292
+
152946
153293
  // utils/normalizeEnv.ts
152947
153294
  function maskValue(value2) {
152948
153295
  if (value2 && typeof value2 === "string" && value2.trim().length > 0) {
@@ -153118,8 +153465,8 @@ function resolvePayload(resolvedPromptInput, repoSettings) {
153118
153465
  }
153119
153466
 
153120
153467
  // 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";
153468
+ import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
153469
+ import { dirname as dirname5, join as join15 } from "node:path";
153123
153470
  var SUMMARY_FILE_NAME = "pullfrog-summary.md";
153124
153471
  var SUMMARY_SCAFFOLD = `# PR summary
153125
153472
 
@@ -153129,19 +153476,19 @@ var SUMMARY_SCAFFOLD = `# PR summary
153129
153476
  var MIN_SNAPSHOT_LENGTH = 60;
153130
153477
  var MAX_SNAPSHOT_LENGTH = 32768;
153131
153478
  function summaryFilePath(tmpdir3) {
153132
- return join14(tmpdir3, SUMMARY_FILE_NAME);
153479
+ return join15(tmpdir3, SUMMARY_FILE_NAME);
153133
153480
  }
153134
153481
  async function seedSummaryFile(params) {
153135
153482
  const path3 = summaryFilePath(params.tmpdir);
153136
- await mkdir(dirname4(path3), { recursive: true });
153483
+ await mkdir2(dirname5(path3), { recursive: true });
153137
153484
  const seed = params.previousSnapshot && params.previousSnapshot.trim().length >= MIN_SNAPSHOT_LENGTH ? params.previousSnapshot : SUMMARY_SCAFFOLD;
153138
- await writeFile2(path3, seed, "utf8");
153485
+ await writeFile3(path3, seed, "utf8");
153139
153486
  return path3;
153140
153487
  }
153141
153488
  async function readSummaryFile(path3) {
153142
153489
  let raw2;
153143
153490
  try {
153144
- raw2 = await readFile2(path3, "utf8");
153491
+ raw2 = await readFile3(path3, "utf8");
153145
153492
  } catch {
153146
153493
  return null;
153147
153494
  }
@@ -153359,9 +153706,9 @@ async function resolveRunContextData(params) {
153359
153706
  import { execFileSync as execFileSync5, execSync as execSync3 } from "node:child_process";
153360
153707
  import { mkdtempSync } from "node:fs";
153361
153708
  import { tmpdir as tmpdir2 } from "node:os";
153362
- import { join as join15 } from "node:path";
153709
+ import { join as join16 } from "node:path";
153363
153710
  function createTempDirectory() {
153364
- const sharedTempDir = mkdtempSync(join15(tmpdir2(), "pullfrog-"));
153711
+ const sharedTempDir = mkdtempSync(join16(tmpdir2(), "pullfrog-"));
153365
153712
  process.env.PULLFROG_TEMP_DIR = sharedTempDir;
153366
153713
  log.info(`\xBB created temp dir at ${sharedTempDir}`);
153367
153714
  return sharedTempDir;
@@ -153763,15 +154110,12 @@ function formatTransientErrorSummary(error49, owner) {
153763
154110
  }
153764
154111
  async function mintProxyKey(ctx) {
153765
154112
  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;
154113
+ const headers = await buildProxyTokenHeaders(ctx);
154114
+ if (!headers) return null;
153771
154115
  const response = await apiFetch({
153772
154116
  path: "/api/proxy-token",
153773
154117
  method: "POST",
153774
- headers: { Authorization: `Bearer ${oidcToken}` }
154118
+ headers
153775
154119
  });
153776
154120
  if (response.status === 402) {
153777
154121
  const body = await response.json().catch(() => null);
@@ -153803,15 +154147,30 @@ async function mintProxyKey(ctx) {
153803
154147
  delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
153804
154148
  }
153805
154149
  }
154150
+ async function buildProxyTokenHeaders(ctx) {
154151
+ if (ctx.oidcCredentials) {
154152
+ process.env.ACTIONS_ID_TOKEN_REQUEST_URL = ctx.oidcCredentials.requestUrl;
154153
+ process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = ctx.oidcCredentials.requestToken;
154154
+ const oidcToken = await core6.getIDToken("pullfrog-api");
154155
+ delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
154156
+ delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
154157
+ return { Authorization: `Bearer ${oidcToken}` };
154158
+ }
154159
+ if (isLocalApiUrl()) {
154160
+ log.info(`\xBB proxy: dev bypass (x-dev-repo) for ${ctx.repo.owner}/${ctx.repo.name}`);
154161
+ return { "x-dev-repo": `${ctx.repo.owner}/${ctx.repo.name}` };
154162
+ }
154163
+ return null;
154164
+ }
153806
154165
  async function resolveProxyModel(ctx) {
153807
154166
  if (process.env.PULLFROG_MODEL?.trim()) return;
153808
154167
  const needsProxy = isInfraCovered({ isOss: ctx.oss, plan: ctx.plan }) && ctx.proxyModel;
153809
154168
  if (!needsProxy) return;
153810
- if (!ctx.oidcCredentials) {
154169
+ if (!ctx.oidcCredentials && !isLocalApiUrl()) {
153811
154170
  log.warning("\xBB proxy requested but no OIDC credentials available \u2014 skipping");
153812
154171
  return;
153813
154172
  }
153814
- const key = await mintProxyKey({ oidcCredentials: ctx.oidcCredentials });
154173
+ const key = await mintProxyKey({ oidcCredentials: ctx.oidcCredentials, repo: ctx.repo });
153815
154174
  if (!key) return;
153816
154175
  process.env.OPENROUTER_API_KEY = key;
153817
154176
  core6.setSecret(key);
@@ -153835,6 +154194,45 @@ async function fetchPreviousSnapshot(ctx, prNumber) {
153835
154194
  return null;
153836
154195
  }
153837
154196
  }
154197
+ async function persistLearnings(ctx) {
154198
+ const filePath = ctx.toolState.learningsFilePath;
154199
+ if (!filePath) return;
154200
+ if (ctx.toolState.learningsPersistAttempted) return;
154201
+ ctx.toolState.learningsPersistAttempted = true;
154202
+ const current = await readLearningsFile(filePath);
154203
+ if (current === null) {
154204
+ log.debug(`learnings tmpfile missing or unreadable at ${filePath} \u2014 skipping persist`);
154205
+ return;
154206
+ }
154207
+ const seed = ctx.toolState.learningsSeed?.trim() ?? "";
154208
+ if (current === seed) {
154209
+ log.debug("learnings tmpfile unchanged from seed \u2014 skipping persist");
154210
+ return;
154211
+ }
154212
+ try {
154213
+ const response = await apiFetch({
154214
+ path: `/api/repo/${ctx.repo.owner}/${ctx.repo.name}/learnings`,
154215
+ method: "PATCH",
154216
+ headers: {
154217
+ authorization: `Bearer ${ctx.apiToken}`,
154218
+ "content-type": "application/json"
154219
+ },
154220
+ body: JSON.stringify({
154221
+ learnings: current,
154222
+ model: ctx.toolState.model
154223
+ }),
154224
+ signal: AbortSignal.timeout(1e4)
154225
+ });
154226
+ if (!response.ok) {
154227
+ const error49 = await response.text().catch(() => "(no body)");
154228
+ log.debug(`learnings persist failed (${response.status}): ${error49}`);
154229
+ return;
154230
+ }
154231
+ log.info("\xBB learnings updated");
154232
+ } catch (err) {
154233
+ log.debug(`learnings persist failed: ${err instanceof Error ? err.message : String(err)}`);
154234
+ }
154235
+ }
153838
154236
  async function persistSummary(ctx) {
153839
154237
  const filePath = ctx.toolState.summaryFilePath;
153840
154238
  if (!filePath) return;
@@ -153856,9 +154254,10 @@ async function persistSummary(ctx) {
153856
154254
  log.debug(`pr summary persist failed: ${err instanceof Error ? err.message : String(err)}`);
153857
154255
  });
153858
154256
  }
153859
- async function writeJobSummary(toolState) {
154257
+ async function writeJobSummary(toolState, finalOutput) {
153860
154258
  const usageSummary = formatUsageSummary(toolState.usageEntries);
153861
- const summaryParts = [toolState.lastProgressBody, usageSummary].filter(Boolean);
154259
+ const body = toolState.lastProgressBody || finalOutput;
154260
+ const summaryParts = [body, usageSummary].filter(Boolean);
153862
154261
  if (summaryParts.length > 0) {
153863
154262
  await writeSummary(summaryParts.join("\n\n"));
153864
154263
  }
@@ -153916,7 +154315,8 @@ async function main() {
153916
154315
  oss: runContext.oss,
153917
154316
  plan: runContext.plan,
153918
154317
  proxyModel: runContext.proxyModel,
153919
- oidcCredentials
154318
+ oidcCredentials,
154319
+ repo: runContext.repo
153920
154320
  });
153921
154321
  } catch (error49) {
153922
154322
  if (error49 instanceof BillingError) {
@@ -154019,12 +154419,32 @@ async function main() {
154019
154419
  toolContext.mcpServerUrl = mcpHttpServer.url;
154020
154420
  log.info(`\xBB MCP server started at ${mcpHttpServer.url}`);
154021
154421
  timer.checkpoint("mcpServer");
154422
+ try {
154423
+ const learningsPath = await seedLearningsFile({
154424
+ tmpdir: tmpdir3,
154425
+ current: runContext.repoSettings.learnings
154426
+ });
154427
+ toolState.learningsFilePath = learningsPath;
154428
+ try {
154429
+ toolState.learningsSeed = await readFile4(learningsPath, "utf8");
154430
+ } catch {
154431
+ }
154432
+ log.info(
154433
+ `\xBB learnings seeded at ${learningsPath} (existing=${runContext.repoSettings.learnings ? "yes" : "no"})`
154434
+ );
154435
+ const ctxForExit = toolContext;
154436
+ onExitSignal(() => persistLearnings(ctxForExit));
154437
+ } catch (err) {
154438
+ log.warning(
154439
+ `\xBB learnings seed failed: ${err instanceof Error ? err.message : String(err)} \u2014 continuing without learnings file`
154440
+ );
154441
+ }
154022
154442
  if (payload.generateSummary && payload.event.is_pr && payload.event.issue_number) {
154023
154443
  const previousSnapshot = await fetchPreviousSnapshot(toolContext, payload.event.issue_number);
154024
154444
  const filePath = await seedSummaryFile({ tmpdir: tmpdir3, previousSnapshot });
154025
154445
  toolState.summaryFilePath = filePath;
154026
154446
  try {
154027
- toolState.summarySeed = await readFile3(filePath, "utf8");
154447
+ toolState.summarySeed = await readFile4(filePath, "utf8");
154028
154448
  } catch {
154029
154449
  }
154030
154450
  log.info(
@@ -154048,7 +154468,7 @@ async function main() {
154048
154468
  modes: modes2,
154049
154469
  agentId,
154050
154470
  outputSchema,
154051
- learnings: runContext.repoSettings.learnings
154471
+ learningsFilePath: toolState.learningsFilePath ?? null
154052
154472
  });
154053
154473
  const logParts = [
154054
154474
  instructions.eventInstructions ? `EVENT-LEVEL INSTRUCTIONS:
@@ -154064,7 +154484,7 @@ ${instructions.user}` : null,
154064
154484
  log.info(instructions.full);
154065
154485
  });
154066
154486
  if (agentId === "opencode") {
154067
- const pluginDir = join16(process.cwd(), ".opencode", "plugin");
154487
+ const pluginDir = join17(process.cwd(), ".opencode", "plugin");
154068
154488
  const hasPlugins = existsSync7(pluginDir) && readdirSync(pluginDir).some((f) => /\.[jt]sx?$/.test(f));
154069
154489
  if (hasPlugins && toolState.dependencyInstallation?.promise) {
154070
154490
  log.info(
@@ -154123,8 +154543,7 @@ ${instructions.user}` : null,
154123
154543
  instructions,
154124
154544
  todoTracker,
154125
154545
  stopScript: runContext.repoSettings.stopScript,
154126
- summaryFilePath: toolState.summaryFilePath,
154127
- summarySeed: toolState.summarySeed,
154546
+ toolState,
154128
154547
  onActivityTimeout: onInnerActivityTimeout,
154129
154548
  onToolUse: (event) => {
154130
154549
  const wasTracked = recordDiffReadFromToolUse({
@@ -154182,12 +154601,27 @@ ${instructions.user}` : null,
154182
154601
  if (toolContext) {
154183
154602
  await persistSummary(toolContext);
154184
154603
  }
154185
- if (toolContext && toolState.progressComment && !toolState.finalSummaryWritten) {
154604
+ if (toolContext) {
154605
+ await persistLearnings(toolContext);
154606
+ }
154607
+ if (!result.success && toolContext && toolState.progressComment) {
154608
+ await reportErrorToComment({
154609
+ toolState,
154610
+ error: result.error || "agent run failed"
154611
+ }).catch((error49) => {
154612
+ log.debug(`failure error report failed: ${error49}`);
154613
+ });
154614
+ }
154615
+ if (toolContext && result.success && toolState.progressComment && !toolState.finalSummaryWritten) {
154186
154616
  await deleteProgressComment(toolContext).catch((error49) => {
154187
154617
  log.debug(`stranded progress comment cleanup failed: ${error49}`);
154188
154618
  });
154189
154619
  }
154190
- await writeJobSummary(toolState);
154620
+ try {
154621
+ await writeJobSummary(toolState, result.output);
154622
+ } catch (error49) {
154623
+ log.debug(`job summary write failed: ${error49}`);
154624
+ }
154191
154625
  if (toolState.output) {
154192
154626
  log.info(`::pullfrog-output::${Buffer.from(toolState.output).toString("base64")}`);
154193
154627
  core6.setOutput("result", toolState.output);
@@ -154234,6 +154668,9 @@ ${errorMessage}
154234
154668
  if (toolContext) {
154235
154669
  await persistSummary(toolContext);
154236
154670
  }
154671
+ if (toolContext) {
154672
+ await persistLearnings(toolContext);
154673
+ }
154237
154674
  return {
154238
154675
  success: false,
154239
154676
  error: errorMessage
@@ -154266,7 +154703,7 @@ ${errorMessage}
154266
154703
  }
154267
154704
 
154268
154705
  // commands/gha.ts
154269
- process.env.PATH = `${dirname5(process.execPath)}:${process.env.PATH}`;
154706
+ process.env.PATH = `${dirname6(process.execPath)}:${process.env.PATH}`;
154270
154707
  var STATE_TOKEN = "token";
154271
154708
  async function runMain() {
154272
154709
  try {
@@ -156076,7 +156513,7 @@ async function run2() {
156076
156513
  }
156077
156514
 
156078
156515
  // cli.ts
156079
- var VERSION10 = "0.1.1";
156516
+ var VERSION10 = "0.1.3";
156080
156517
  var bin = basename2(process.argv[1] || "");
156081
156518
  var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
156082
156519
  var rawArgs = process.argv.slice(2);