pullfrog 0.0.204 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -18198,7 +18198,7 @@ var require_summary = __commonJS({
18198
18198
  exports.summary = exports.markdownSummary = exports.SUMMARY_DOCS_URL = exports.SUMMARY_ENV_VAR = void 0;
18199
18199
  var os_1 = __require("os");
18200
18200
  var fs_1 = __require("fs");
18201
- var { access, appendFile, writeFile: writeFile2 } = fs_1.promises;
18201
+ var { access, appendFile, writeFile: writeFile3 } = fs_1.promises;
18202
18202
  exports.SUMMARY_ENV_VAR = "GITHUB_STEP_SUMMARY";
18203
18203
  exports.SUMMARY_DOCS_URL = "https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary";
18204
18204
  var Summary = class {
@@ -18256,7 +18256,7 @@ var require_summary = __commonJS({
18256
18256
  return __awaiter(this, void 0, void 0, function* () {
18257
18257
  const overwrite = !!(options === null || options === void 0 ? void 0 : options.overwrite);
18258
18258
  const filePath = yield this.filePath();
18259
- const writeFunc = overwrite ? writeFile2 : appendFile;
18259
+ const writeFunc = overwrite ? writeFile3 : appendFile;
18260
18260
  yield writeFunc(filePath, this._buffer, { encoding: "utf8" });
18261
18261
  return this.emptyBuffer();
18262
18262
  });
@@ -62662,8 +62662,8 @@ var require_snapshot_utils = __commonJS({
62662
62662
  var require_snapshot_recorder = __commonJS({
62663
62663
  "node_modules/.pnpm/undici@7.22.0/node_modules/undici/lib/mock/snapshot-recorder.js"(exports, module) {
62664
62664
  "use strict";
62665
- var { writeFile: writeFile2, readFile, mkdir } = __require("node:fs/promises");
62666
- var { dirname: dirname4, resolve: resolve3 } = __require("node:path");
62665
+ var { writeFile: writeFile3, readFile: readFile4, mkdir: mkdir2 } = __require("node:fs/promises");
62666
+ var { dirname: dirname5, resolve: resolve3 } = __require("node:path");
62667
62667
  var { setTimeout: setTimeout2, clearTimeout: clearTimeout2 } = __require("node:timers");
62668
62668
  var { InvalidArgumentError, UndiciError } = require_errors4();
62669
62669
  var { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = require_snapshot_utils();
@@ -62864,7 +62864,7 @@ var require_snapshot_recorder = __commonJS({
62864
62864
  throw new InvalidArgumentError("Snapshot path is required");
62865
62865
  }
62866
62866
  try {
62867
- const data = await readFile(resolve3(path3), "utf8");
62867
+ const data = await readFile4(resolve3(path3), "utf8");
62868
62868
  const parsed2 = JSON.parse(data);
62869
62869
  if (Array.isArray(parsed2)) {
62870
62870
  this.#snapshots.clear();
@@ -62894,12 +62894,12 @@ var require_snapshot_recorder = __commonJS({
62894
62894
  throw new InvalidArgumentError("Snapshot path is required");
62895
62895
  }
62896
62896
  const resolvedPath = resolve3(path3);
62897
- await mkdir(dirname4(resolvedPath), { recursive: true });
62897
+ await mkdir2(dirname5(resolvedPath), { recursive: true });
62898
62898
  const data = Array.from(this.#snapshots.entries()).map(([hash2, snapshot2]) => ({
62899
62899
  hash: hash2,
62900
62900
  snapshot: snapshot2
62901
62901
  }));
62902
- await writeFile2(resolvedPath, JSON.stringify(data, null, 2), { flush: true });
62902
+ await writeFile3(resolvedPath, JSON.stringify(data, null, 2), { flush: true });
62903
62903
  }
62904
62904
  /**
62905
62905
  * Clears all recorded snapshots
@@ -97475,14 +97475,14 @@ var require_turndown_cjs = __commonJS({
97475
97475
  } else if (node2.nodeType === 1) {
97476
97476
  replacement = replacementForNode.call(self2, node2);
97477
97477
  }
97478
- return join16(output, replacement);
97478
+ return join17(output, replacement);
97479
97479
  }, "");
97480
97480
  }
97481
97481
  function postProcess(output) {
97482
97482
  var self2 = this;
97483
97483
  this.rules.forEach(function(rule) {
97484
97484
  if (typeof rule.append === "function") {
97485
- output = join16(output, rule.append(self2.options));
97485
+ output = join17(output, rule.append(self2.options));
97486
97486
  }
97487
97487
  });
97488
97488
  return output.replace(/^[\t\r\n]+/, "").replace(/[\t\r\n\s]+$/, "");
@@ -97494,7 +97494,7 @@ var require_turndown_cjs = __commonJS({
97494
97494
  if (whitespace.leading || whitespace.trailing) content = content.trim();
97495
97495
  return whitespace.leading + rule.replacement(content, node2, this.options) + whitespace.trailing;
97496
97496
  }
97497
- function join16(output, replacement) {
97497
+ function join17(output, replacement) {
97498
97498
  var s1 = trimTrailingNewlines(output);
97499
97499
  var s2 = trimLeadingNewlines(replacement);
97500
97500
  var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
@@ -98926,7 +98926,8 @@ var require_fast_content_type_parse = __commonJS({
98926
98926
  // main.ts
98927
98927
  var core6 = __toESM(require_core(), 1);
98928
98928
  import { existsSync as existsSync7, readdirSync } from "node:fs";
98929
- import { join as join15 } from "node:path";
98929
+ import { readFile as readFile3 } from "node:fs/promises";
98930
+ import { join as join16 } from "node:path";
98930
98931
 
98931
98932
  // node_modules/.pnpm/@ark+util@0.56.0/node_modules/@ark/util/out/arrays.js
98932
98933
  var liftArray = (data) => Array.isArray(data) ? data : [data];
@@ -107422,7 +107423,7 @@ function buildCommitPrompt(status) {
107422
107423
  ].join("\n");
107423
107424
  }
107424
107425
  function hasPostRunIssues(issues) {
107425
- return issues.stopHook !== void 0 || issues.dirtyTree !== void 0;
107426
+ return issues.stopHook !== void 0 || issues.dirtyTree !== void 0 || issues.summaryStale !== void 0;
107426
107427
  }
107427
107428
  var agent = (input) => {
107428
107429
  return {
@@ -108115,7 +108116,11 @@ function resolveCliModel(slug2) {
108115
108116
  var PULLFROG_DIVIDER = "<!-- PULLFROG_DIVIDER_DO_NOT_REMOVE_PLZ -->";
108116
108117
  var FROG_LOGO = `<a href="https://pullfrog.com"><picture><source media="(prefers-color-scheme: dark)" srcset="https://pullfrog.com/logos/frog-white-full-18px.png"><img src="https://pullfrog.com/logos/frog-green-full-18px.png" width="9px" height="9px" style="vertical-align: middle; " alt="Pullfrog"></picture></a>`;
108117
108118
  function formatModelLabel(slug2) {
108118
- const alias = resolveDisplayAlias(slug2);
108119
+ const alias = resolveDisplayAlias(slug2) ?? // reverse-lookup: when the caller passes an effective model (proxy or
108120
+ // resolved target like "openrouter/anthropic/claude-opus-4.7") instead of
108121
+ // a stored alias slug, find the alias whose resolve target matches so we
108122
+ // still render a friendly display name.
108123
+ modelAliases.find((a) => a.resolve === slug2 || a.openRouterResolve === slug2);
108119
108124
  if (!alias) return `\`${slug2}\``;
108120
108125
  return alias.isFree ? `\`${alias.displayName}\` (free)` : `\`${alias.displayName}\``;
108121
108126
  }
@@ -108190,10 +108195,13 @@ var defaultShouldRetry = (error49) => {
108190
108195
  return error49.name === "AbortError" || error49.message.includes("fetch failed") || error49.message.includes("ECONNRESET") || error49.message.includes("ETIMEDOUT");
108191
108196
  };
108192
108197
  async function retry(fn2, options = {}) {
108193
- const maxAttempts = options.maxAttempts ?? 3;
108194
- const delayMs = options.delayMs ?? 1e3;
108195
108198
  const shouldRetry = options.shouldRetry ?? defaultShouldRetry;
108196
108199
  const label = options.label ?? "operation";
108200
+ const delays = options.delaysMs ? Array.from(options.delaysMs) : Array.from(
108201
+ { length: (options.maxAttempts ?? 3) - 1 },
108202
+ (_, i) => (options.delayMs ?? 1e3) * (i + 1)
108203
+ );
108204
+ const maxAttempts = delays.length + 1;
108197
108205
  let lastError;
108198
108206
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
108199
108207
  try {
@@ -108203,7 +108211,7 @@ async function retry(fn2, options = {}) {
108203
108211
  if (attempt === maxAttempts || !shouldRetry(error49)) {
108204
108212
  throw error49;
108205
108213
  }
108206
- const delay2 = delayMs * attempt;
108214
+ const delay2 = delays[attempt - 1];
108207
108215
  log.info(`\xBB ${label} failed (attempt ${attempt}/${maxAttempts}), retrying in ${delay2}ms...`);
108208
108216
  await sleep(delay2);
108209
108217
  }
@@ -108217,7 +108225,7 @@ var STRING_KEYS = [
108217
108225
  "issueNodeId",
108218
108226
  "reviewNodeId",
108219
108227
  "planCommentNodeId",
108220
- "summaryCommentNodeId"
108228
+ "summarySnapshot"
108221
108229
  ];
108222
108230
  var NUMBER_KEYS = [
108223
108231
  "inputTokens",
@@ -108937,43 +108945,22 @@ function addFooter(ctx, body) {
108937
108945
  var Comment = type({
108938
108946
  issueNumber: type.number.describe("the issue number to comment on"),
108939
108947
  body: type.string.describe("the comment body content"),
108940
- type: type.enumerated("Plan", "Summary", "Comment").describe(
108941
- "Plan: record as the plan for this run. Summary: record as the PR summary comment (one per PR, updated in place). Comment: regular comment (default)."
108942
- ).optional()
108948
+ type: type.enumerated("Plan", "Comment").describe("Plan: record as the plan for this run. Comment: regular comment (default).").optional()
108943
108949
  });
108944
108950
  function CreateCommentTool(ctx) {
108945
108951
  return tool({
108946
108952
  name: "create_issue_comment",
108947
- description: "Create a comment on a GitHub issue or PR. For progress/plan updates on the current run use report_progress instead. Use type: 'Plan' for plan comments, type: 'Summary' for PR summary comments.",
108953
+ description: "Create a comment on a GitHub issue or PR. For progress/plan updates on the current run use report_progress instead. Use type: 'Plan' for plan comments.",
108948
108954
  parameters: Comment,
108949
108955
  execute: execute(async ({ issueNumber, body, type: commentType }) => {
108950
108956
  const bodyWithFooter = addFooter(ctx, body);
108951
- if (commentType === "Summary" && ctx.toolState.existingSummaryCommentId) {
108952
- log.info(
108953
- `\xBB redirecting create_issue_comment(Summary) to update existing comment ${ctx.toolState.existingSummaryCommentId}`
108954
- );
108955
- const result2 = await ctx.octokit.rest.issues.updateComment({
108956
- owner: ctx.repo.owner,
108957
- repo: ctx.repo.name,
108958
- comment_id: ctx.toolState.existingSummaryCommentId,
108959
- body: bodyWithFooter
108960
- });
108961
- if (result2.data.node_id) {
108962
- await patchWorkflowRunFields(ctx, { summaryCommentNodeId: result2.data.node_id });
108963
- }
108964
- return {
108965
- success: true,
108966
- commentId: result2.data.id,
108967
- url: result2.data.html_url,
108968
- body: result2.data.body
108969
- };
108970
- }
108971
108957
  const result = await ctx.octokit.rest.issues.createComment({
108972
108958
  owner: ctx.repo.owner,
108973
108959
  repo: ctx.repo.name,
108974
108960
  issue_number: issueNumber,
108975
108961
  body: bodyWithFooter
108976
108962
  });
108963
+ ctx.toolState.wasUpdated = true;
108977
108964
  if (commentType === "Plan") {
108978
108965
  if (result.data.node_id) {
108979
108966
  await patchWorkflowRunFields(ctx, { planCommentNodeId: result.data.node_id });
@@ -108994,9 +108981,6 @@ function CreateCommentTool(ctx) {
108994
108981
  body: updateResult.data.body
108995
108982
  };
108996
108983
  }
108997
- if (commentType === "Summary" && result.data.node_id) {
108998
- await patchWorkflowRunFields(ctx, { summaryCommentNodeId: result.data.node_id });
108999
- }
109000
108984
  return {
109001
108985
  success: true,
109002
108986
  commentId: result.data.id,
@@ -109152,15 +109136,15 @@ ${collapsible}`;
109152
109136
  reportParams.target_plan_comment = params.target_plan_comment;
109153
109137
  }
109154
109138
  const result = await reportProgress(ctx, reportParams);
109155
- if (!params.target_plan_comment) {
109156
- ctx.toolState.finalSummaryWritten = true;
109157
- }
109158
109139
  if (result.action === "skipped") {
109159
109140
  return {
109160
109141
  success: true,
109161
109142
  message: "progress recorded (no GitHub comment created - this may occur for workflow_dispatch events or when there is no associated issue/PR)"
109162
109143
  };
109163
109144
  }
109145
+ if (!params.target_plan_comment) {
109146
+ ctx.toolState.finalSummaryWritten = true;
109147
+ }
109164
109148
  return {
109165
109149
  success: true,
109166
109150
  ...result
@@ -142265,7 +142249,7 @@ var import_semver = __toESM(require_semver2(), 1);
142265
142249
  // package.json
142266
142250
  var package_default = {
142267
142251
  name: "pullfrog",
142268
- version: "0.0.204",
142252
+ version: "0.1.0",
142269
142253
  type: "module",
142270
142254
  bin: {
142271
142255
  pullfrog: "dist/cli.mjs",
@@ -142462,7 +142446,7 @@ function closeBrowserDaemon(toolState) {
142462
142446
 
142463
142447
  // mcp/checkout.ts
142464
142448
  import { createHash as createHash2 } from "node:crypto";
142465
- import { writeFileSync } from "node:fs";
142449
+ import { statSync, unlinkSync as unlinkSync2, writeFileSync } from "node:fs";
142466
142450
  import { join as join3 } from "node:path";
142467
142451
 
142468
142452
  // utils/diffCoverage.ts
@@ -142491,7 +142475,10 @@ function createDiffCoverageState(params) {
142491
142475
  totalLines: params.totalLines,
142492
142476
  tocEntries: parseDiffTocEntries({ toc: params.toc }),
142493
142477
  coveredRanges: [],
142494
- coveragePreflightRan: false
142478
+ // carry forward across checkout_pr refreshes so the nudge stays "once per
142479
+ // review session". coveredRanges are intentionally not carried because
142480
+ // line numbers are tied to the previous diff's content.
142481
+ coveragePreflightRan: params.previous?.coveragePreflightRan ?? false
142495
142482
  };
142496
142483
  }
142497
142484
  function recordDiffReadFromToolUse(params) {
@@ -143295,6 +143282,11 @@ var GitFetch = type({
143295
143282
  ref: type.string.describe("Ref to fetch: branch name, tag, or 'pull/N/head' for PRs"),
143296
143283
  depth: type.number.describe("Fetch depth (for shallow clones)").optional()
143297
143284
  });
143285
+ var SHALLOW_UNREACHABLE_PATTERNS = [
143286
+ /Could not read [a-f0-9]{40,64}/,
143287
+ /remote did not send all necessary objects/
143288
+ ];
143289
+ var DEEPEN_RETRY_DEPTH = 1e3;
143298
143290
  function GitFetchTool(ctx) {
143299
143291
  return tool({
143300
143292
  name: "git_fetch",
@@ -143306,9 +143298,20 @@ function GitFetchTool(ctx) {
143306
143298
  if (params.depth !== void 0) {
143307
143299
  fetchArgs.push(`--depth=${params.depth}`);
143308
143300
  }
143309
- await $git("fetch", fetchArgs, {
143310
- token: ctx.gitToken
143311
- });
143301
+ try {
143302
+ await $git("fetch", fetchArgs, { token: ctx.gitToken });
143303
+ } catch (err) {
143304
+ const msg = err instanceof Error ? err.message : String(err);
143305
+ const isShallowUnreachable = SHALLOW_UNREACHABLE_PATTERNS.some((p) => p.test(msg));
143306
+ const isShallow = isShallowUnreachable && $("git", ["rev-parse", "--is-shallow-repository"], { log: false }).trim() === "true";
143307
+ if (!isShallow) throw err;
143308
+ log.info(
143309
+ `\xBB git_fetch hit shallow-unreachable error, retrying with --deepen=${DEEPEN_RETRY_DEPTH}`
143310
+ );
143311
+ await $git("fetch", [`--deepen=${DEEPEN_RETRY_DEPTH}`, "--no-tags", "origin", params.ref], {
143312
+ token: ctx.gitToken
143313
+ });
143314
+ }
143312
143315
  return { success: true, ref: params.ref };
143313
143316
  })
143314
143317
  });
@@ -143374,6 +143377,12 @@ function getHttpStatus(err) {
143374
143377
  const status = err.status;
143375
143378
  return typeof status === "number" ? status : void 0;
143376
143379
  }
143380
+ function isTransientReviewError(err) {
143381
+ if (getHttpStatus(err) !== 422) return false;
143382
+ const msg = err instanceof Error ? err.message : String(err);
143383
+ return /internal error occurred, please try again/i.test(msg);
143384
+ }
143385
+ var TRANSIENT_REVIEW_RETRY_DELAYS_MS = [1e3, 3e3];
143377
143386
  function commentableLinesForFile(patch) {
143378
143387
  const right = /* @__PURE__ */ new Set();
143379
143388
  const left = /* @__PURE__ */ new Set();
@@ -143641,12 +143650,26 @@ function CreatePullRequestReviewTool(ctx) {
143641
143650
  }
143642
143651
  let result;
143643
143652
  try {
143644
- result = body ? await createAndSubmitWithFooter(ctx, params, {
143645
- body,
143646
- approved: approved ?? false,
143647
- hasComments: (params.comments?.length ?? 0) > 0
143648
- }) : await createReviewWithStrandedRecovery(ctx, params);
143653
+ result = await retry(
143654
+ () => body ? createAndSubmitWithFooter(ctx, params, {
143655
+ body,
143656
+ approved: approved ?? false,
143657
+ hasComments: (params.comments?.length ?? 0) > 0
143658
+ }) : createReviewWithStrandedRecovery(ctx, params),
143659
+ {
143660
+ delaysMs: TRANSIENT_REVIEW_RETRY_DELAYS_MS,
143661
+ shouldRetry: isTransientReviewError,
143662
+ label: "review submission"
143663
+ }
143664
+ );
143649
143665
  } catch (err) {
143666
+ if (isTransientReviewError(err)) {
143667
+ const rawMsg2 = err instanceof Error ? err.message : String(err);
143668
+ throw new Error(
143669
+ `GitHub returned a transient 422 "internal error" on the reviews endpoint after ${TRANSIENT_REVIEW_RETRY_DELAYS_MS.length + 1} attempts. This is a GitHub-side issue, not a problem with your review content. Do NOT modify or drop inline comments \u2014 their content is not the cause. Wait ~30 seconds and call this tool once more with the SAME arguments. If it still fails, submit a body-only review (move all inline feedback into \`body\` as text) so nothing is lost. GitHub said: ${rawMsg2}`,
143670
+ { cause: err }
143671
+ );
143672
+ }
143650
143673
  if (getHttpStatus(err) !== 422 || !params.comments?.length) throw err;
143651
143674
  const details = params.comments.map((c) => {
143652
143675
  const line = c.line ?? 0;
@@ -143673,6 +143696,7 @@ function CreatePullRequestReviewTool(ctx) {
143673
143696
  nodeId: reviewNodeId,
143674
143697
  reviewedSha: actuallyReviewedSha
143675
143698
  };
143699
+ ctx.toolState.wasUpdated = true;
143676
143700
  await deleteProgressComment(ctx).catch((err) => {
143677
143701
  log.debug(`progress comment cleanup after review failed: ${err}`);
143678
143702
  });
@@ -144024,11 +144048,38 @@ async function ensureBeforeShaReachable(params) {
144024
144048
  return false;
144025
144049
  }
144026
144050
  }
144051
+ var STALE_LOCK_AGE_MS = 3e4;
144052
+ var GIT_LOCK_PATHS = [
144053
+ ".git/shallow.lock",
144054
+ ".git/index.lock",
144055
+ ".git/objects/maintenance.lock"
144056
+ ];
144057
+ function cleanupStaleGitLocks() {
144058
+ const now = Date.now();
144059
+ for (const relPath of GIT_LOCK_PATHS) {
144060
+ let mtimeMs;
144061
+ try {
144062
+ mtimeMs = statSync(relPath).mtimeMs;
144063
+ } catch {
144064
+ continue;
144065
+ }
144066
+ if (now - mtimeMs < STALE_LOCK_AGE_MS) continue;
144067
+ try {
144068
+ unlinkSync2(relPath);
144069
+ log.warning(`\xBB removed stale ${relPath} from prior run`);
144070
+ } catch (e) {
144071
+ log.debug(
144072
+ `\xBB failed to remove stale ${relPath}: ${e instanceof Error ? e.message : String(e)}`
144073
+ );
144074
+ }
144075
+ }
144076
+ }
144027
144077
  async function checkoutPrBranch(pr, params) {
144028
144078
  const { octokit, owner, name, gitToken, toolState, beforeSha } = params;
144029
144079
  log.info(`\xBB checking out PR #${pr.number}...`);
144030
144080
  rejectIfLeadingDash(pr.baseRef, "PR base ref");
144031
144081
  rejectIfLeadingDash(pr.headRef, "PR head ref");
144082
+ cleanupStaleGitLocks();
144032
144083
  const isFork = pr.headRepoFullName !== pr.baseRepoFullName;
144033
144084
  const localBranch = `pr-${pr.number}`;
144034
144085
  const isShallow = $("git", ["rev-parse", "--is-shallow-repository"], { log: false }).trim() === "true";
@@ -144198,7 +144249,8 @@ ${diffPreview}`);
144198
144249
  ctx.toolState.diffCoverage = createDiffCoverageState({
144199
144250
  diffPath,
144200
144251
  totalLines: countLines({ content: formatResult.content }),
144201
- toc: formatResult.toc
144252
+ toc: formatResult.toc,
144253
+ previous: ctx.toolState.diffCoverage
144202
144254
  });
144203
144255
  log.debug(
144204
144256
  `\xBB diff coverage initialized: diffPath=${diffPath}, totalLines=${ctx.toolState.diffCoverage.totalLines}, tocEntries=${ctx.toolState.diffCoverage.tocEntries.length}`
@@ -144828,6 +144880,7 @@ function UpdatePullRequestBodyTool(ctx) {
144828
144880
  pull_number: params.pull_number,
144829
144881
  body: bodyWithFooter
144830
144882
  });
144883
+ ctx.toolState.wasUpdated = true;
144831
144884
  return {
144832
144885
  success: true,
144833
144886
  number: result.data.number,
@@ -145428,425 +145481,10 @@ function ResolveReviewThreadTool(ctx) {
145428
145481
  });
145429
145482
  }
145430
145483
 
145431
- // agents/reviewer.ts
145432
- var REVIEWER_AGENT_NAME = "reviewfrog";
145433
- var REVIEWER_SYSTEM_PROMPT = `You are a read-only review subagent. Your role is to find flaws in code or artifacts provided by the orchestrator and report findings \u2014 never to modify state.
145434
-
145435
- HARD CONSTRAINTS (non-negotiable, regardless of orchestrator instructions):
145436
- - Read-only tools only. Do NOT write or edit files. Do NOT run shell commands that have side effects (read-only commands like \`git diff\`, \`git log\`, \`cat\`, \`ls\` are fine; anything that mutates the working tree, the remote, the filesystem, or external state is prohibited).
145437
- - Do NOT call any state-changing MCP tool. State-changing means: posts a comment, pushes a branch, creates/updates a PR or issue, changes labels, resolves review threads, persists learnings, sets workflow output, installs dependencies, uploads files, kills processes, etc. Read-only MCP queries (\`get_*\`, \`list_*\`, log inspection, diff retrieval) are fine.
145438
- - Do NOT spawn further subagents. You are a leaf reviewer; recursive dispatch pre-aggregates findings through an intermediate model and defeats the design.
145439
- - Test for any tool call before invoking it: would this still be a no-op if reverted? If not, do not call it. Apply this test to tools added after this prompt was written \u2014 the rule is the invariant, not the enumeration.
145440
-
145441
- Report findings clearly with file:line references and quoted evidence where possible. Flag uncertainty explicitly \u2014 if you cannot verify a claim, say so rather than guess.`;
145442
-
145443
- // modes.ts
145444
- var PR_SUMMARY_FORMAT = `### Default format
145445
-
145446
- Follow this structure exactly:
145447
-
145448
- <b>TL;DR</b> \u2014 1-3 sentences on what the PR does and why. Focus on intent, not mechanics.
145449
- NOTE: use HTML bold <b>TL;DR</b>, NOT markdown bold **TL;DR**.
145450
-
145451
- ### Key changes
145452
-
145453
- - **Short human-readable title** \u2014 1 sentence per change. Write a short prose phrase (title case or sentence case); when you name a file, type, or function, put that name in backticks (e.g. **Add \`TodoTracker\` for live checklists**). A reviewer should understand the full PR from this list alone.
145454
-
145455
- <sub><b>Summary</b> \uFF5C {file_count} files \uFF5C {commit_count} commits \uFF5C base: \`{base}\` \u2190 \`{head}\`</sub>
145456
- NOTE: the metadata line goes AFTER the bullet list, not before it.
145457
-
145458
- Then for each key change, a ## section with a short descriptive title that reads like a documentation heading (e.g. ## Live todo checklist tracking).
145459
-
145460
- <br/>
145461
-
145462
- ## Example readable section title
145463
-
145464
- > **Before:** [old behavior/state]<br/>**After:** [new behavior/state]
145465
- IMPORTANT: Before and After MUST be on a SINGLE blockquote line with an inline <br/> between them. Two separate \`>\` lines creates a double line break.
145466
-
145467
- 1-2 sentences of explanation. Break up text with tables, blockquotes, or lists \u2014 NEVER 3+ plain paragraphs in a row.
145468
-
145469
- If a change warrants deeper explanation, use a blockquoted details/summary framed as a question:
145470
- > <details><summary>How does X work?</summary>
145471
- > Extended explanation here.
145472
- > </details>
145473
-
145474
- End each section with a file links trail (3-4 key files max):
145475
- [\`file.ts\`](https://github.com/{owner}/{repo}/pull/{number}/files#diff-{sha256hex_of_filepath}) \xB7 ...
145476
-
145477
- Single-feature PRs: skip the ## sections. Fold before/after and explanation into the header after key changes.
145478
-
145479
- CRITICAL \u2014 GitHub markdown rendering rule:
145480
- GitHub's markdown parser requires a blank line between ALL block-level elements. This includes transitions between: HTML tags (<br/>, <sub>, <details>, <b>, etc.) and markdown syntax (headings, lists, blockquotes, paragraphs). Without a blank line, GitHub treats the following content as a continuation of the HTML block and renders markdown syntax as literal text. ALWAYS separate block-level elements with a blank line.
145481
-
145482
- Rules:
145483
- - \`##\` titles and key-change bullet lead-ins are plain-language summaries; backtick only actual code tokens (files, types, functions) where they appear in the title
145484
- - ALL variable names, identifiers, and file names in body text must be in backticks
145485
- - ALL file references MUST link to the PR Files Changed view. Use the \`diff-<hex>\` anchor precomputed next to each filename in the \`checkout_pr\` TOC \u2014 do NOT run \`sha256sum\` or any other shell command to compute anchors. NEVER fabricate hex strings. If a file is not in the TOC, omit the \`#diff-\` anchor rather than guessing.
145486
- - Add <br/> before each ## heading for visual spacing. Do NOT use horizontal rules (---)
145487
- - Do NOT include raw diff stats like '+123 / -45' or line counts
145488
- - Do NOT include code blocks or repeat diff contents
145489
- - Do NOT include a changelog section \u2014 the key changes list serves this purpose
145490
- - Focus on *intent*, not *what* \u2014 the diff already shows what changed
145491
- - Get the file count and commit count from the checkout_pr metadata, not by counting manually`;
145492
- function learningsStep(t, n) {
145493
- return `${n}. **learnings** (only if high confidence): if you discovered something about repo setup, test commands, conventions, or patterns that you are confident is correct and would reliably help future runs, call \`${t("update_learnings")}\` to persist it. skip this step if you are unsure or the finding is speculative/one-off. format as a flat bullet list (\`- \` per line, one fact per bullet). merge with existing learnings from the prompt \u2014 pass the FULL merged list. deduplicate, and drop bullets that are clearly wrong or no longer relevant to the current codebase.`;
145494
- }
145495
- function computeModes(agentId) {
145496
- const t = (toolName) => formatMcpToolRef(agentId, toolName);
145497
- return [
145498
- {
145499
- name: "Build",
145500
- 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",
145501
- prompt: `### Checklist
145502
-
145503
- 1. **plan** (optional, for complex tasks): analyze requirements, read AGENTS.md and relevant code, produce a step-by-step implementation plan.
145504
-
145505
- 2. **setup**: checkout or create the branch:
145506
- - **PR event, modifying the existing PR**: call \`${t("checkout_pr")}\`
145507
- - **new branch**: use \`${t("git")}\` to create a branch (\`git checkout -b pullfrog/branch-name\`)
145508
-
145509
- 3. **build**: implement changes using your native file and shell tools:
145510
- - follow the plan (if you ran a plan phase)
145511
- - 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.
145512
- - run relevant tests/lints before committing
145513
-
145514
- 4. **self-review**: judgment call \u2014 does YOUR diff warrant a fresh-eyes pass?
145515
-
145516
- Skip self-review (commit directly) when the diff is **genuinely trivial**:
145517
- - doc typos, comment-only edits, whitespace/format-only, import reordering
145518
- - lockfile or generated-code regeneration, mechanical rename whose only effect is import-path updates (size of diff is irrelevant \u2014 read the *shape*, not the line count)
145519
- - low-risk dep patch bump from a trusted source
145520
-
145521
- Run self-review when the diff has **any behavioral surface, however small**:
145522
- - 1-line changes to SQL operators / comparison logic / regexes / redirects / HTTP methods / response codes
145523
- - any change to money / tax / currency / billing / fee / refund / payout calculations or constants
145524
- - any change to auth / permissions / roles / sessions / tokens / signature verification
145525
- - any change to feature-flag defaults, retry counts, timeouts, rate limits, batch sizes
145526
- - new endpoints, new code paths, new error branches \u2014 even small ones
145527
- - mixed diffs (whitespace + a single semantic line) \u2014 the semantic line still triggers self-review
145528
- - anything you're uncertain about
145529
-
145530
- Tie-breaker: when in doubt, run self-review. One false-positive subagent dispatch costs cents; one false-negative shipped bug costs much more. There's no value in dispatching for a typo, but there's also no excuse for skipping on a 1-line change to a billing path.
145531
-
145532
- Otherwise delegate the \`${REVIEWER_AGENT_NAME}\` subagent to review your diff with fresh eyes against YOUR TASK. The subagent's baked-in system prompt enforces a non-mutative + non-recursive contract: read-only file/search/web tools and read-only MCP queries only; no writes, shell side effects, state-changing MCP calls, or nested subagent dispatch. Enforcement is prose-only \u2014 restate the constraint in your dispatch instructions and do not relax it.
145533
-
145534
- Provide the subagent with YOUR TASK, the output of \`git diff\`, and a tight summary (not raw output) of any lint/typecheck/test failures you fixed during build \u2014 what broke, root cause, the fix \u2014 so it can check that fixes addressed root causes rather than suppressed symptoms; say "no build-phase failures" if the build path was clean. Instruct it to flag bugs, logic errors, missing edge cases, gaps between request and diff, and unintended changes.
145535
-
145536
- Delegation + research discipline (distilled from \`/anneal\` canonical \u2014 these are codified learnings from many review rounds, not theoretical best practices):
145537
- - Do NOT summarize what you implemented \u2014 that biases the subagent toward validating the shape of your solution rather than questioning it.
145538
- - Do NOT curate a reading list of files. Let the subagent discover scope from the diff and codebase.
145539
- - Do NOT pre-shape output with a severity / category schema. That leaks your hypotheses; severity is your call during evaluation.
145540
- - Do NOT defect-hunt the diff yourself in parallel with the subagent. Your role is dispatch + evaluation; doing the review yourself reintroduces the implementation bias the subagent is meant to mitigate.
145541
- - For diffs that rely on third-party API contracts, SDK semantics, framework directives, or DB engine specifics, instruct the subagent to verify load-bearing claims via web search and quote source URLs rather than trust training data \u2014 this is the single most common review-quality failure mode.
145542
-
145543
- 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 not enough \u2014 a fix that improves correctness while degrading elegance still degrades the codebase. 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 "..."\`).
145544
-
145545
- 5. **finalize**:
145546
- - confirm a clean working tree, then push via \`${t("push_branch")}\` (see *SYSTEM* Git rules if this fails \u2014 prepush errors are usually the repo's tests/lint, not infra timeouts)
145547
- - create a PR via \`${t("create_pull_request")}\`
145548
- - call \`${t("report_progress")}\` with the PR link or the exact error if push/PR failed
145549
-
145550
- ${learningsStep(t, 6)}
145551
-
145552
- ### Notes
145553
-
145554
- For simple, well-defined tasks, skip the plan phase and go straight to build.`
145555
- },
145556
- {
145557
- name: "AddressReviews",
145558
- description: "Address PR review feedback; respond to reviewer comments; make requested changes to an existing PR",
145559
- prompt: `### Checklist
145560
-
145561
- 1. Checkout the PR branch via \`${t("checkout_pr")}\`.
145562
-
145563
- 2. Fetch review comments via \`${t("get_review_comments")}\`.
145564
-
145565
- 3. For each comment:
145566
- - understand the feedback
145567
- - 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 not enough; improving correctness while degrading elegance still degrades the code.
145568
- - if the request stands, make the code change using your native tools; otherwise reply explaining why
145569
- - record what was done (or why nothing was done)
145570
-
145571
- 4. Quality check:
145572
- - 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
145573
- - commit locally via shell (\`git add . && git commit -m "..."\`)
145574
-
145575
- 5. Finalize:
145576
- - confirm a clean working tree, then push via \`${t("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*)
145577
- - reply to each comment using \`${t("reply_to_review_comment")}\`
145578
- - resolve addressed threads via \`${t("resolve_review_thread")}\`
145579
- - call \`${t("report_progress")}\` with a brief summary (or the exact push error if push failed)
145580
-
145581
- ${learningsStep(t, 6)}`
145582
- },
145583
- // Review and IncrementalReview use the multi-lens orchestrator pattern
145584
- // (canonical source: .claude/commands/anneal.md). The orchestrator does
145585
- // triage → parallel read-only subagent fan-out → aggregate → draft comments
145586
- // → submit. For someone else's PR, parallel lenses (correctness, security,
145587
- // research-validated claims, user-journey, etc.) provide breadth across
145588
- // angles that a single subagent can't carry coherently. Build mode keeps
145589
- // a single fresh-eyes subagent (different problem shape — orchestrator
145590
- // wrote the code and bias-mitigation comes from delegating to one
145591
- // subagent that doesn't share the implementation context).
145592
- // Deliberate omission vs canonical /anneal: severity categorization in the
145593
- // final message (the review body has its own CAUTION/IMPORTANT framing
145594
- // instead of a severity table).
145595
- {
145596
- name: "Review",
145597
- description: "Review code, PRs, or implementations; provide feedback or suggestions; identify issues; or check code quality, style, and correctness",
145598
- prompt: `### Checklist
145599
-
145600
- 1. **checkout**: call \`${t("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.
145601
-
145602
- 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 \`${t("get_pull_request")}\` and other read-only GitHub tools for additional context if needed.
145603
-
145604
- if the PR is **genuinely trivial**, skip steps 3\u20134 entirely and submit \`Reviewed \u2014 no issues found.\` per step 5. there's no value in dispatching even one lens for a typo.
145605
-
145606
- "Genuinely trivial" (skip):
145607
- - single-word doc typo, whitespace/format-only, comment-only across any number of files
145608
- - lockfile or generated-code regeneration (size of diff is irrelevant \u2014 read the *shape*)
145609
- - mechanical rename whose only effect is import-path updates
145610
- - low-risk dep patch bump
145611
-
145612
- "Looks trivial but isn't" (do **NOT** skip \u2014 small diff, big blast radius):
145613
- - any 1-line change to SQL / regex / auth / billing / permission / signature-verification code
145614
- - flipping a feature-flag default, default config value, or retry/timeout constant
145615
- - changing a money/tax/currency/fee constant by any amount
145616
- - changing an HTTP method, redirect URL, response code, or status enum
145617
- - tightening or loosening a comparison operator (\`<\` \u2194 \`<=\`, \`==\` \u2194 \`!=\`)
145618
- - renaming a public API surface (still trivial in shape, but needs an impact lens)
145619
- - adding a new direct dependency (supply-chain surface)
145620
- - any "typo fix" in user-facing copy that changes meaning ("approved" \u2192 "denied")
145621
- - mixed diffs where a semantic 1-liner is buried in whitespace/formatting changes
145622
-
145623
- When unsure, treat as non-trivial. The cost of one extra subagent is cents; the cost of a missed billing/auth/data bug is much more.
145624
-
145625
- otherwise pick lenses by where the PR concentrates risk \u2014 **there's no fixed count**. lens count is judgment, not a formula. concrete shapes to anchor against:
145626
-
145627
- - **1 lens** \u2014 pure refactor / mechanical rename across many files (impact); new test file with no source change (test-integrity); small isolated bug fix (correctness); doc-only PR with non-trivial technical content (research-validated or holistic)
145628
- - **2\u20133 lenses (most PRs land here)** \u2014 new CRUD endpoint (correctness + security + test-integrity); new UI flow (user-journey + correctness); a single bug fix in a non-critical subsystem (correctness + test-integrity); design doc covering one domain (research-validated + correctness or holistic)
145629
- - **4\u20135 lenses (high-stakes subsystem touches)** \u2014 any billing/payments change (billing-subsystem + correctness + security + operational-readiness); new auth flow (auth-subsystem + correctness + security + test-integrity); schema migration (schema-migration-subsystem + correctness + operational-readiness + impact); cross-subsystem PR that touches billing AND auth AND schema (one subsystem lens per domain + correctness)
145630
- - **6+ lenses** \u2014 almost always a smell; you're either covering overlapping ground or this PR should have been split. push back via the review body rather than expanding lens count.
145631
-
145632
- lenses come in two flavors, and you can mix them:
145633
- - **themed lenses** \u2014 a perspective applied across the whole diff (correctness, security, user-journey, performance, etc.).
145634
- - **subsystem lenses** \u2014 a domain-scoped frame for high-stakes subsystems the PR touches (e.g. "the auth lens", "the billing lens", "the schema-migration lens"). a subsystem lens is "review the PR specifically for what could go wrong in this subsystem" and naturally combines theme + scope. **for high-stakes domains, lead with the subsystem lens rather than the generic themed equivalent** \u2014 "billing-subsystem" outperforms "correctness on billing code" because the framing primes the subagent to remember domain-specific failure modes (double-charges, refund races, currency rounding, dispute flows) the generic lens misses.
145635
-
145636
- starter menu (combine, omit, or invent your own):
145637
- - **correctness & invariants** \u2014 bugs, races, error handling, edge cases, state-machine boundaries
145638
- - **impact** \u2014 when the PR removes features, deletes exports, renames identifiers, or changes architectural patterns: stale references in code, tests, docs (\`docs/\`, \`wiki/\`), comments, configs, UI
145639
- - **research-validated assumptions** \u2014 third-party API contracts, SDK semantics, framework directives, version-gated behavior. the subagent must verify load-bearing claims via web search and quote source URLs.
145640
- - **security** \u2014 new endpoints, authZ, input validation, secrets handling, replay/CSRF/injection, cross-tenant isolation
145641
- - **user-journey** \u2014 UX-touching flows: walk through happy path and failure modes as a user
145642
- - **operational readiness** \u2014 observability, alerting, migrations (forward + rollback), feature flags, on-call burden
145643
- - **integration & cross-cutting** \u2014 API contracts between modules, backward-compat of public surfaces, multi-service ordering
145644
- - **test integrity** \u2014 meaningful coverage for the changed behavior; deterministic; no shared-state pollution
145645
- - **performance** \u2014 N+1 queries, hot-path allocation, latency budgets, index coverage
145646
- - **holistic** \u2014 does the PR make sense as a whole? symmetric flows (delete for every create, rollback for every migration)?
145647
- - **subsystem lenses** (invent as the PR demands) \u2014 auth, billing, payments, schema migration, webhooks, secrets, RBAC, multi-tenant isolation, cron/scheduling, etc.
145648
-
145649
- 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:
145650
- - the diff path / target \u2014 reading the diff and the codebase is its job
145651
- - **only one lens** \u2014 never a multi-section "review for X, Y, and Z" prompt
145652
- - **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\`.
145653
- - 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.
145654
- - if the lens touches external contracts, instruct the subagent to verify load-bearing claims via web search rather than trust training data, and to quote source URLs in its reasoning. action runs are non-interactive \u2014 there's no human in the loop to catch "I'm pretty sure Stripe does X."
145655
- - ask the subagent to report findings with file paths and NEW line numbers from the diff so you can anchor inline comments without re-reading the entire diff.
145656
-
145657
- delegation discipline:
145658
- - do NOT lens-review the diff yourself in parallel with the subagents (your job is dispatch + comment-drafting; doing the lens work yourself reintroduces the bias the fan-out avoids)
145659
- - do NOT summarize the PR for them (biases toward a validation frame)
145660
- - do NOT hand them a curated reading list (let them discover scope)
145661
- - do NOT pre-shape their output with a finding schema
145662
- - do NOT mention the other lenses (independence is the point \u2014 overlapping findings are a strong signal)
145663
-
145664
- 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.
145665
-
145666
- 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.
145667
-
145668
- 5. **submit**: ALWAYS submit exactly one review via \`${t("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.
145669
-
145670
- 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.
145671
-
145672
- - **critical issues** (blocks merge \u2014 bugs, security, data loss):
145673
- \`approved: false\`. Body begins with a GitHub alert blockquote, e.g.:
145674
- \`> [!CAUTION]\\n> This PR introduces a race condition in ...\`
145675
- Follow with a brief summary if needed. Include all inline comments.
145676
- - **recommended changes** (non-critical):
145677
- \`approved: false\`. Body begins with a GitHub alert blockquote, e.g.:
145678
- \`> [!IMPORTANT]\\n> Consider adding input validation for ...\`
145679
- Follow with a brief summary if needed. Include all inline comments.
145680
- - **no actionable issues**:
145681
- \`approved: true\`, body: "Reviewed \u2014 no issues found."`
145682
- },
145683
- // IncrementalReview shares Review's multi-lens orchestrator pattern but
145684
- // scopes the target to the incremental diff and adds prior-review-feedback
145685
- // tracking. The "issues must be NEW since the last Pullfrog review" filter
145686
- // lives at aggregation time (step 5), NOT in the subagent prompt — pushing
145687
- // the filter into subagents matches the canonical anneal anti-pattern of
145688
- // "list known pre-existing failures — don't flag these" and suppresses
145689
- // signal on regressions the new commits amplified. The body-format rules
145690
- // (Reviewed changes / Prior review feedback) are unchanged from the prior
145691
- // version. Same severity-table omission as Review.
145692
- {
145693
- name: "IncrementalReview",
145694
- description: "Re-review a PR after new commits are pushed; focus on new changes since the last review",
145695
- prompt: `### Checklist
145696
-
145697
- 1. **checkout**: call \`${t("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.
145698
-
145699
- 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.
145700
-
145701
- 3. **prior feedback**: fetch previous reviews via \`${t("list_pull_request_reviews")}\`. for the most recent Pullfrog review, call \`${t("get_review_comments")}\` with the review ID to retrieve specific prior line-level feedback. you'll need this in step 6 to track which prior comments were addressed.
145702
-
145703
- 4. **triage & fan out**: orient on the *incremental* changes \u2014 domain, seams, external contracts, user-facing surfaces.
145704
-
145705
- 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).
145706
-
145707
- "Genuinely trivial" (skip): formatting/comment tweaks, import reordering, lockfile regen, mechanical rename of import paths, whitespace-only.
145708
- "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.
145709
- When unsure, treat as non-trivial.
145710
-
145711
- 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.
145712
-
145713
- 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:
145714
- - 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
145715
- - **only one lens** \u2014 never a multi-section "review for X, Y, and Z" prompt
145716
- - **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\`.
145717
- - 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.
145718
- - if the lens touches external contracts, instruct the subagent to verify load-bearing claims via web search and quote source URLs. action runs are non-interactive \u2014 there's no human to catch "I'm pretty sure Stripe does X."
145719
- - ask the subagent to report findings with file paths and NEW line numbers from the full PR diff so you can anchor inline comments.
145720
-
145721
- delegation discipline:
145722
- - do NOT lens-review the diff yourself in parallel with the subagents
145723
- - do NOT summarize the changes for them (biases toward validation frame)
145724
- - do NOT hand them a curated reading list (let them discover scope)
145725
- - do NOT pre-shape their output with a finding schema
145726
- - do NOT mention the other lenses (independence is the point)
145727
-
145728
- 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 \`${t("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.
145729
-
145730
- then check: which prior review comments were addressed by the new commits? track the addressed ones for step 6b.
145731
-
145732
- 6. **build the review body** \u2014 two distinct sections:
145733
- a. **Reviewed changes**: 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.
145734
- b. **Prior review feedback** (only if any were addressed): list only the prior review comments that WERE addressed by the new commits (\`- [x] safeParse instead of parse \u2014 addressed\`). omit unaddressed comments. omit this entire section if nothing was addressed. a change can appear in both sections.
145735
- - no headings, no tables, no prose paragraphs in either section \u2014 just bullets
145736
- - in some cases you may receive a complete diff for the whole pull request instead of an incremental one. when this happens, you will need to determine what changes have happened since Pullfrog's most recent review.
145737
-
145738
- 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. the review body always includes the reviewed changes from step 6a. append \`Prior review feedback:\\n\` with the checklist from step 6b only if any prior comments were addressed. Follow these rules:
145739
- - 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.
145740
- - 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.
145741
- - ELSE IF NEW CRITICAL ISSUES (blocks merge): call \`${t("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 and prior feedback (if any).
145742
- - ELSE IF NEW RECOMMENDED CHANGES (non-critical): call \`${t("create_pull_request_review")}\` with \`approved: false\`, all comments, and the review body. body opens with \`> [!IMPORTANT]\\n> ...\` alert, then the reviewed changes summary and prior feedback (if any).
145743
- - ELSE IF NO NEW ISSUES, SUBSTANTIVE CHANGES (new functionality, behavior changes, or fixes to prior review feedback): call \`${t("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 and prior feedback (if any).`
145744
- },
145745
- {
145746
- name: "Plan",
145747
- description: "Create plans, break down tasks, outline steps, analyze requirements, understand scope of work, or provide task breakdowns",
145748
- prompt: `### Checklist
145749
-
145750
- 1. Analyze the task and gather context:
145751
- - read AGENTS.md and relevant codebase files
145752
- - understand the architecture and constraints
145753
-
145754
- 2. Produce a structured, actionable plan with clear milestones.
145755
-
145756
- 3. Call \`${t("report_progress")}\` with the plan.
145757
-
145758
- ${learningsStep(t, 4)}`
145759
- },
145760
- {
145761
- name: "Fix",
145762
- description: "Fix CI failures; debug failing tests or builds; investigate and resolve check suite failures",
145763
- prompt: `### Checklist
145764
-
145765
- 1. Checkout the PR branch via \`${t("checkout_pr")}\`.
145766
-
145767
- 2. Fetch check suite logs via \`${t("get_check_suite_logs")}\`.
145768
-
145769
- 3. **CRITICAL**: verify the failure was INTRODUCED BY THIS PR before fixing. If unrelated, abort and report.
145770
-
145771
- 4. Diagnose and fix:
145772
- - read the workflow file, reproduce locally with the EXACT same commands CI runs
145773
- - fix the issue using your native file and shell tools
145774
- - verify the fix by re-running the exact CI command
145775
- - 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.
145776
- - commit locally via shell (\`git add . && git commit -m "..."\`)
145777
-
145778
- 5. Finalize:
145779
- - confirm a clean working tree, then push via \`${t("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*)
145780
- - call \`${t("report_progress")}\` with the diagnosis and fix summary (or the exact push error if push failed)
145781
-
145782
- ${learningsStep(t, 6)}`
145783
- },
145784
- {
145785
- name: "ResolveConflicts",
145786
- description: "Resolve merge conflicts in a PR branch against the base branch",
145787
- prompt: `### Checklist
145788
-
145789
- 1. **Setup**:
145790
- - Call \`${t("checkout_pr")}\` to get the PR branch.
145791
- - Call \`${t("get_pull_request")}\` to identify the base branch (e.g., 'main').
145792
- - Call \`${t("git_fetch")}\` to fetch the base branch.
145793
-
145794
- 2. **Merge Attempt**:
145795
- - Run \`git merge origin/<base_branch>\` via shell.
145796
- - If it succeeds automatically, confirm a clean working tree, push via \`${t("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*), and call \`${t("report_progress")}\` with a brief success note or the exact push error if push failed \u2014 **then stop; do not run steps 3\u20134.**
145797
- - If it fails (conflicts), resolve them manually (continue to steps 3\u20134).
145798
-
145799
- 3. **Resolve Conflicts**:
145800
- - Run \`git status\` or parse the merge output to find the list of conflicting files.
145801
- - 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.
145802
- - Verify the file syntax is correct after resolution.
145803
-
145804
- 4. **Finalize**:
145805
- - Run a final verification (build/test) to ensure the resolution works.
145806
- - \`git add . && git commit -m "resolve merge conflicts"\`
145807
- - confirm a clean working tree, then push via \`${t("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*)
145808
- - Call \`${t("report_progress")}\` with a summary of what was resolved (or the exact push error if push failed)`
145809
- },
145810
- {
145811
- name: "Task",
145812
- description: "General-purpose tasks that don't fit other modes: answering questions, adding comments, labeling, running ad-hoc commands, or any direct request",
145813
- prompt: `### Checklist
145814
-
145815
- 1. Analyze the task. For simple operations (labeling, commenting, answering questions, running a single command), handle directly.
145816
-
145817
- 2. For substantial work \u2014 code changes across multiple files, multi-step investigations:
145818
- - plan your approach before starting
145819
- - use native file and shell tools for local operations
145820
- - use ${pullfrogMcpName} MCP tools for GitHub/git operations
145821
- - 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
145822
-
145823
- 3. Finalize:
145824
- - if code changes were made, push to a pull request (new or existing) using \`${t("push_branch")}\` and \`${t("create_pull_request")}\` as needed. \`git status\` must be clean before you finish (see *SYSTEM* Git rules if push fails).
145825
- - call \`${t("report_progress")}\` once with results \u2014 include exact tool errors if push or PR creation failed
145826
- - if the task involved labeling, commenting, or other GitHub operations, perform those directly
145827
-
145828
- ${learningsStep(t, 4)}`
145829
- },
145830
- {
145831
- name: "Summarize",
145832
- description: "Summarize a PR with a structured comment that is updated in place on subsequent pushes",
145833
- prompt: `### Checklist
145834
-
145835
- 1. Checkout the PR via \`${t("checkout_pr")}\` \u2014 this returns PR metadata and a \`diffPath\`.
145836
- 2. Read the diff using the TOC to selectively read relevant sections (not the entire file). Produce a structured summary. If EVENT INSTRUCTIONS specify a custom format, follow that instead of the default format below.
145837
- 3. Call \`${t("create_issue_comment")}\` with \`type: "Summary"\` and the summary body.
145838
- 4. Call \`${t("report_progress")}\` with a brief note (e.g., "Posted PR summary.").
145839
-
145840
- ${PR_SUMMARY_FORMAT}`
145841
- }
145842
- ];
145843
- }
145844
- var modes = computeModes("opencode");
145845
-
145846
145484
  // mcp/selectMode.ts
145847
145485
  var SelectModeParams = type({
145848
145486
  mode: type.string.describe(
145849
- "the name of the mode to select (e.g., 'Build', 'Plan', 'Review', 'IncrementalReview', 'Fix', 'AddressReviews', 'Task', 'ResolveConflicts', 'Summarize')"
145487
+ "the name of the mode to select (e.g., 'Build', 'Plan', 'Review', 'IncrementalReview', 'Fix', 'AddressReviews', 'Task', 'ResolveConflicts')"
145850
145488
  ),
145851
145489
  "issue_number?": type("number").describe(
145852
145490
  "optional issue number; when provided with Plan mode, used to look up an existing plan comment for this issue (edit vs create)"
@@ -145867,18 +145505,7 @@ An existing plan comment was found for this issue. Update that comment with the
145867
145505
  - gather relevant codebase context (file paths, architecture notes from AGENTS.md)
145868
145506
  - produce a structured plan with clear milestones
145869
145507
  3. Call \`${t("report_progress")}\` with the full revised plan text and \`{ target_plan_comment: true }\` so it updates the existing plan comment (not the progress comment).
145870
- 4. Then post a short note to the progress comment (e.g. "Plan has been updated in the comment above.") via \`${t("report_progress")}\` so it is not left as "Leaping...".`,
145871
- SummaryUpdate: `### Checklist (updating existing summary)
145872
-
145873
- An existing summary comment was found for this PR. Update it rather than creating a new one.
145874
-
145875
- 1. Use \`previousSummaryBody\` from this response as the current summary to revise.
145876
- 2. Checkout the PR via \`${t("checkout_pr")}\` \u2014 this returns PR metadata and a \`diffPath\`.
145877
- 3. Read the diff using the TOC to selectively read relevant sections. Produce an updated summary reflecting the current state of the PR, using the existing summary (\`previousSummaryBody\`) as a starting point. If EVENT INSTRUCTIONS specify a custom format, follow that instead of the default format below.
145878
- 4. Call \`${t("edit_issue_comment")}\` with \`commentId: existingSummaryCommentId\` (from this response) and the updated summary body.
145879
- 5. Call \`${t("report_progress")}\` with a brief note (e.g., "Updated PR summary.").
145880
-
145881
- ${PR_SUMMARY_FORMAT}`
145508
+ 4. Then post a short note to the progress comment (e.g. "Plan has been updated in the comment above.") via \`${t("report_progress")}\` so it is not left as "Leaping...".`
145882
145509
  };
145883
145510
  }
145884
145511
  var modeInstructionParent = {
@@ -145911,30 +145538,22 @@ async function fetchExistingPlanComment(ctx, issueNumber) {
145911
145538
  return null;
145912
145539
  }
145913
145540
  }
145914
- async function fetchExistingSummaryComment(ctx, prNumber) {
145915
- if (!ctx.githubInstallationToken) {
145916
- log.warning("fetchExistingSummaryComment: no token, skipping");
145917
- return null;
145918
- }
145919
- const path3 = `/api/repo/${ctx.repo.owner}/${ctx.repo.name}/pr/${prNumber}/summary-comment`;
145920
- try {
145921
- const response = await apiFetch({
145922
- path: path3,
145923
- method: "GET",
145924
- headers: { authorization: `Bearer ${ctx.githubInstallationToken}` },
145925
- signal: AbortSignal.timeout(1e4)
145926
- });
145927
- const data = await response.json();
145928
- if (response.ok && "commentId" in data) {
145929
- return data;
145930
- }
145931
- const errMsg = "error" in data ? data.error : "(no error body)";
145932
- log.warning(`fetchExistingSummaryComment: ${response.status} ${path3} \u2014 ${errMsg}`);
145933
- return null;
145934
- } catch (error49) {
145935
- log.warning("fetchExistingSummaryComment failed:", error49);
145936
- return null;
145937
- }
145541
+ var SUMMARY_MODES = /* @__PURE__ */ new Set(["Review", "IncrementalReview", "Task"]);
145542
+ function buildSummaryAddendum(t, ctx) {
145543
+ const filePath = ctx.toolState.summaryFilePath;
145544
+ if (!filePath) return "";
145545
+ return `### PR summary snapshot \u2014 required step
145546
+
145547
+ A rolling PR summary lives at \`${filePath}\`. It is your durable cross-run agent context \u2014 a functional summary of what this PR does, the subsystems and files it touches, the material behavior of its changes, and any risks or open questions worth carrying forward. It is NOT a chronological log of past review runs; commit-level history can already be reconstructed from \`${t("list_pull_request_reviews")}\`.
145548
+
145549
+ How to use it:
145550
+
145551
+ - read \`${filePath}\` at the START of the run, alongside the diff. it represents what previous agent runs already understood about this PR \u2014 absorb it before picking lenses or crafting subagent dispatch prompts. if it's a fresh seed (file is one or two lines), this is a first review and you'll be filling it in from the diff.
145552
+ - let the snapshot inform triage and dispatch. when it already tracks a risk, your lens prompts to subagents are stronger when they reference that context (e.g. "the JSDoc explicitly scopes to code points \u2014 do not flag grapheme-cluster issues" if the snapshot already documents that contract). when something the snapshot tracks is now resolved by new commits, note that. when new commits introduce something the snapshot doesn't yet describe, that's exactly where your fan-out should focus.
145553
+ - update the file in place to reflect the PR's CURRENT state. revise stale claims, drop resolved risks, add new behavior or risks. accuracy over breadth \u2014 every claim must be grounded in the diff. write for the next agent run, not for a human.
145554
+ - structure however serves THIS PR. there is no required section template. a refactor might organize by renamed export and call-site impact; a feature by capability; a billing change by money path. a compact note of which commit ranges have been reviewed should always be present so future runs scope correctly, but the rest is your call. when the structure works across runs, keep it stable so range-diffs are clean; when the PR's character changes (e.g. scope expands), reshape.
145555
+
145556
+ Do NOT call \`${t("create_issue_comment")}\` for the summary \u2014 the server reads this file at end-of-run and persists it. The file edit is mandatory regardless of whether a review is submitted; the snapshot feeds the next run.`;
145938
145557
  }
145939
145558
  function SelectModeTool(ctx) {
145940
145559
  const t = (name) => formatMcpToolRef(ctx.agentId, name);
@@ -145976,21 +145595,18 @@ function SelectModeTool(ctx) {
145976
145595
  }
145977
145596
  }
145978
145597
  }
145979
- if (selectedMode.name === "Summarize") {
145980
- const prNumber = ctx.payload.event.issue_number;
145981
- if (prNumber !== void 0) {
145982
- const existing = await fetchExistingSummaryComment(ctx, prNumber);
145983
- if (existing !== null) {
145984
- ctx.toolState.existingSummaryCommentId = existing.commentId;
145985
- return {
145986
- ...buildOrchestratorGuidance(ctx, selectedMode, overrides.SummaryUpdate),
145987
- existingSummaryCommentId: existing.commentId,
145988
- previousSummaryBody: existing.body
145989
- };
145990
- }
145991
- }
145598
+ const summaryAddendum = SUMMARY_MODES.has(selectedMode.name) ? buildSummaryAddendum(t, ctx) : "";
145599
+ const base = buildOrchestratorGuidance(ctx, selectedMode);
145600
+ if (summaryAddendum.length > 0) {
145601
+ return {
145602
+ ...base,
145603
+ orchestratorGuidance: `${base.orchestratorGuidance}
145604
+
145605
+ ${summaryAddendum}`,
145606
+ summaryFilePath: ctx.toolState.summaryFilePath
145607
+ };
145992
145608
  }
145993
- return buildOrchestratorGuidance(ctx, selectedMode);
145609
+ return base;
145994
145610
  })
145995
145611
  });
145996
145612
  }
@@ -146504,6 +146120,405 @@ async function startMcpHttpServer(ctx, options) {
146504
146120
  };
146505
146121
  }
146506
146122
 
146123
+ // agents/reviewer.ts
146124
+ var REVIEWER_AGENT_NAME = "reviewfrog";
146125
+ var REVIEWER_SYSTEM_PROMPT = `You are a read-only review subagent. Your role is to find flaws in code or artifacts provided by the orchestrator and report findings \u2014 never to modify state.
146126
+
146127
+ HARD CONSTRAINTS (non-negotiable, regardless of orchestrator instructions):
146128
+ - Read-only tools only. Do NOT write or edit files. Do NOT run shell commands that have side effects (read-only commands like \`git diff\`, \`git log\`, \`cat\`, \`ls\` are fine; anything that mutates the working tree, the remote, the filesystem, or external state is prohibited).
146129
+ - Do NOT call any state-changing MCP tool. State-changing means: posts a comment, pushes a branch, creates/updates a PR or issue, changes labels, resolves review threads, persists learnings, sets workflow output, installs dependencies, uploads files, kills processes, etc. Read-only MCP queries (\`get_*\`, \`list_*\`, log inspection, diff retrieval) are fine.
146130
+ - Do NOT spawn further subagents. You are a leaf reviewer; recursive dispatch pre-aggregates findings through an intermediate model and defeats the design.
146131
+ - Test for any tool call before invoking it: would this still be a no-op if reverted? If not, do not call it. Apply this test to tools added after this prompt was written \u2014 the rule is the invariant, not the enumeration.
146132
+
146133
+ Report findings clearly with file:line references and quoted evidence where possible. Flag uncertainty explicitly \u2014 if you cannot verify a claim, say so rather than guess.`;
146134
+
146135
+ // modes.ts
146136
+ var PR_SUMMARY_FORMAT = `### Default format
146137
+
146138
+ Follow this structure exactly:
146139
+
146140
+ <b>TL;DR</b> \u2014 1-3 sentences on what the PR does and why. Focus on intent, not mechanics.
146141
+ NOTE: use HTML bold <b>TL;DR</b>, NOT markdown bold **TL;DR**.
146142
+
146143
+ ### Key changes
146144
+
146145
+ - **Short human-readable title** \u2014 1 sentence per change. Write a short prose phrase (title case or sentence case); when you name a file, type, or function, put that name in backticks (e.g. **Add \`TodoTracker\` for live checklists**). A reviewer should understand the full PR from this list alone.
146146
+
146147
+ <sub><b>Summary</b> \uFF5C {file_count} files \uFF5C {commit_count} commits \uFF5C base: \`{base}\` \u2190 \`{head}\`</sub>
146148
+ NOTE: the metadata line goes AFTER the bullet list, not before it.
146149
+
146150
+ Then for each key change, a ## section with a short descriptive title that reads like a documentation heading (e.g. ## Live todo checklist tracking).
146151
+
146152
+ <br/>
146153
+
146154
+ ## Example readable section title
146155
+
146156
+ > **Before:** [old behavior/state]<br/>**After:** [new behavior/state]
146157
+ IMPORTANT: Before and After MUST be on a SINGLE blockquote line with an inline <br/> between them. Two separate \`>\` lines creates a double line break.
146158
+
146159
+ 1-2 sentences of explanation. Break up text with tables, blockquotes, or lists \u2014 NEVER 3+ plain paragraphs in a row.
146160
+
146161
+ If a change warrants deeper explanation, use a blockquoted details/summary framed as a question:
146162
+ > <details><summary>How does X work?</summary>
146163
+ > Extended explanation here.
146164
+ > </details>
146165
+
146166
+ End each section with a file links trail (3-4 key files max):
146167
+ [\`file.ts\`](https://github.com/{owner}/{repo}/pull/{number}/files#diff-{sha256hex_of_filepath}) \xB7 ...
146168
+
146169
+ Single-feature PRs: skip the ## sections. Fold before/after and explanation into the header after key changes.
146170
+
146171
+ CRITICAL \u2014 GitHub markdown rendering rule:
146172
+ GitHub's markdown parser requires a blank line between ALL block-level elements. This includes transitions between: HTML tags (<br/>, <sub>, <details>, <b>, etc.) and markdown syntax (headings, lists, blockquotes, paragraphs). Without a blank line, GitHub treats the following content as a continuation of the HTML block and renders markdown syntax as literal text. ALWAYS separate block-level elements with a blank line.
146173
+
146174
+ Rules:
146175
+ - \`##\` titles and key-change bullet lead-ins are plain-language summaries; backtick only actual code tokens (files, types, functions) where they appear in the title
146176
+ - ALL variable names, identifiers, and file names in body text must be in backticks
146177
+ - ALL file references MUST link to the PR Files Changed view. Use the \`diff-<hex>\` anchor precomputed next to each filename in the \`checkout_pr\` TOC \u2014 do NOT run \`sha256sum\` or any other shell command to compute anchors. NEVER fabricate hex strings. If a file is not in the TOC, omit the \`#diff-\` anchor rather than guessing.
146178
+ - Add <br/> before each ## heading for visual spacing. Do NOT use horizontal rules (---)
146179
+ - Do NOT include raw diff stats like '+123 / -45' or line counts
146180
+ - Do NOT include code blocks or repeat diff contents
146181
+ - Do NOT include a changelog section \u2014 the key changes list serves this purpose
146182
+ - Focus on *intent*, not *what* \u2014 the diff already shows what changed
146183
+ - Get the file count and commit count from the checkout_pr metadata, not by counting manually`;
146184
+ function learningsStep(t, n) {
146185
+ return `${n}. **learnings** (only if high confidence): if you discovered something about repo setup, test commands, conventions, or patterns that you are confident is correct and would reliably help future runs, call \`${t("update_learnings")}\` to persist it. skip this step if you are unsure or the finding is speculative/one-off. format as a flat bullet list (\`- \` per line, one fact per bullet). merge with existing learnings from the prompt \u2014 pass the FULL merged list. deduplicate, and drop bullets that are clearly wrong or no longer relevant to the current codebase.`;
146186
+ }
146187
+ function computeModes(agentId) {
146188
+ const t = (toolName) => formatMcpToolRef(agentId, toolName);
146189
+ return [
146190
+ {
146191
+ name: "Build",
146192
+ 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",
146193
+ prompt: `### Checklist
146194
+
146195
+ 1. **plan** (optional, for complex tasks): analyze requirements, read AGENTS.md and relevant code, produce a step-by-step implementation plan.
146196
+
146197
+ 2. **setup**: checkout or create the branch:
146198
+ - **PR event, modifying the existing PR**: call \`${t("checkout_pr")}\`
146199
+ - **new branch**: use \`${t("git")}\` to create a branch (\`git checkout -b pullfrog/branch-name\`)
146200
+
146201
+ 3. **build**: implement changes using your native file and shell tools:
146202
+ - follow the plan (if you ran a plan phase)
146203
+ - 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.
146204
+ - run relevant tests/lints before committing
146205
+
146206
+ 4. **self-review**: judgment call \u2014 does YOUR diff warrant a fresh-eyes pass?
146207
+
146208
+ Skip self-review (commit directly) when the diff is **genuinely trivial**:
146209
+ - doc typos, comment-only edits, whitespace/format-only, import reordering
146210
+ - lockfile or generated-code regeneration, mechanical rename whose only effect is import-path updates (size of diff is irrelevant \u2014 read the *shape*, not the line count)
146211
+ - low-risk dep patch bump from a trusted source
146212
+
146213
+ Run self-review when the diff has **any behavioral surface, however small**:
146214
+ - 1-line changes to SQL operators / comparison logic / regexes / redirects / HTTP methods / response codes
146215
+ - any change to money / tax / currency / billing / fee / refund / payout calculations or constants
146216
+ - any change to auth / permissions / roles / sessions / tokens / signature verification
146217
+ - any change to feature-flag defaults, retry counts, timeouts, rate limits, batch sizes
146218
+ - new endpoints, new code paths, new error branches \u2014 even small ones
146219
+ - mixed diffs (whitespace + a single semantic line) \u2014 the semantic line still triggers self-review
146220
+ - anything you're uncertain about
146221
+
146222
+ Tie-breaker: when in doubt, run self-review. One false-positive subagent dispatch costs cents; one false-negative shipped bug costs much more. There's no value in dispatching for a typo, but there's also no excuse for skipping on a 1-line change to a billing path.
146223
+
146224
+ Otherwise delegate the \`${REVIEWER_AGENT_NAME}\` subagent to review your diff with fresh eyes against YOUR TASK. The subagent's baked-in system prompt enforces a non-mutative + non-recursive contract: read-only file/search/web tools and read-only MCP queries only; no writes, shell side effects, state-changing MCP calls, or nested subagent dispatch. Enforcement is prose-only \u2014 restate the constraint in your dispatch instructions and do not relax it.
146225
+
146226
+ Provide the subagent with YOUR TASK, the output of \`git diff\`, and a tight summary (not raw output) of any lint/typecheck/test failures you fixed during build \u2014 what broke, root cause, the fix \u2014 so it can check that fixes addressed root causes rather than suppressed symptoms; say "no build-phase failures" if the build path was clean. Instruct it to flag bugs, logic errors, missing edge cases, gaps between request and diff, and unintended changes.
146227
+
146228
+ Delegation + research discipline (distilled from \`/anneal\` canonical \u2014 these are codified learnings from many review rounds, not theoretical best practices):
146229
+ - Do NOT summarize what you implemented \u2014 that biases the subagent toward validating the shape of your solution rather than questioning it.
146230
+ - Do NOT curate a reading list of files. Let the subagent discover scope from the diff and codebase.
146231
+ - Do NOT pre-shape output with a severity / category schema. That leaks your hypotheses; severity is your call during evaluation.
146232
+ - Do NOT defect-hunt the diff yourself in parallel with the subagent. Your role is dispatch + evaluation; doing the review yourself reintroduces the implementation bias the subagent is meant to mitigate.
146233
+ - For diffs that rely on third-party API contracts, SDK semantics, framework directives, or DB engine specifics, instruct the subagent to verify load-bearing claims via web search and quote source URLs rather than trust training data \u2014 this is the single most common review-quality failure mode.
146234
+
146235
+ 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 "..."\`).
146236
+
146237
+ 5. **finalize**:
146238
+ - confirm a clean working tree, then push via \`${t("push_branch")}\` (see *SYSTEM* Git rules if this fails \u2014 prepush errors are usually the repo's tests/lint, not infra timeouts)
146239
+ - create a PR via \`${t("create_pull_request")}\`
146240
+ - call \`${t("report_progress")}\` with the PR link or the exact error if push/PR failed
146241
+
146242
+ ${learningsStep(t, 6)}
146243
+
146244
+ ### Notes
146245
+
146246
+ For simple, well-defined tasks, skip the plan phase and go straight to build.`
146247
+ },
146248
+ {
146249
+ name: "AddressReviews",
146250
+ description: "Address PR review feedback; respond to reviewer comments; make requested changes to an existing PR",
146251
+ prompt: `### Checklist
146252
+
146253
+ 1. Checkout the PR branch via \`${t("checkout_pr")}\`.
146254
+
146255
+ 2. Fetch review comments via \`${t("get_review_comments")}\`.
146256
+
146257
+ 3. For each comment:
146258
+ - understand the feedback
146259
+ - 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.
146260
+ - if the request stands, make the code change using your native tools; otherwise reply explaining why
146261
+ - record what was done (or why nothing was done)
146262
+
146263
+ 4. Quality check:
146264
+ - 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
146265
+ - commit locally via shell (\`git add . && git commit -m "..."\`)
146266
+
146267
+ 5. Finalize:
146268
+ - confirm a clean working tree, then push via \`${t("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*)
146269
+ - reply to each comment using \`${t("reply_to_review_comment")}\`
146270
+ - resolve addressed threads via \`${t("resolve_review_thread")}\`
146271
+ - call \`${t("report_progress")}\` with a brief summary (or the exact push error if push failed)
146272
+
146273
+ ${learningsStep(t, 6)}`
146274
+ },
146275
+ // Review and IncrementalReview use the multi-lens orchestrator pattern
146276
+ // (canonical source: .claude/commands/anneal.md). The orchestrator does
146277
+ // triage → parallel read-only subagent fan-out → aggregate → draft comments
146278
+ // → submit. For someone else's PR, parallel lenses (correctness, security,
146279
+ // research-validated claims, user-journey, etc.) provide breadth across
146280
+ // angles that a single subagent can't carry coherently. Build mode keeps
146281
+ // a single fresh-eyes subagent (different problem shape — orchestrator
146282
+ // wrote the code and bias-mitigation comes from delegating to one
146283
+ // subagent that doesn't share the implementation context).
146284
+ // Deliberate omission vs canonical /anneal: severity categorization in the
146285
+ // final message (the review body has its own CAUTION/IMPORTANT framing
146286
+ // instead of a severity table).
146287
+ {
146288
+ name: "Review",
146289
+ description: "Review code, PRs, or implementations; provide feedback or suggestions; identify issues; or check code quality, style, and correctness",
146290
+ prompt: `### Checklist
146291
+
146292
+ 1. **checkout**: call \`${t("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.
146293
+
146294
+ 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 \`${t("get_pull_request")}\` and other read-only GitHub tools for additional context if needed.
146295
+
146296
+ 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.
146297
+
146298
+ "Genuinely trivial" (skip):
146299
+ - single-word doc typo, whitespace/format-only, comment-only across any number of files
146300
+ - lockfile or generated-code regeneration (size of diff is irrelevant \u2014 read the *shape*)
146301
+ - mechanical rename whose only effect is import-path updates
146302
+ - low-risk dep patch bump
146303
+
146304
+ "Looks trivial but isn't" (do **NOT** skip \u2014 small diff, big blast radius):
146305
+ - any 1-line change to SQL / regex / auth / billing / permission / signature-verification code
146306
+ - flipping a feature-flag default, default config value, or retry/timeout constant
146307
+ - changing a money/tax/currency/fee constant by any amount
146308
+ - changing an HTTP method, redirect URL, response code, or status enum
146309
+ - tightening or loosening a comparison operator (\`<\` \u2194 \`<=\`, \`==\` \u2194 \`!=\`)
146310
+ - renaming a public API surface (still trivial in shape, but needs an impact lens)
146311
+ - adding a new direct dependency (supply-chain surface)
146312
+ - any "typo fix" in user-facing copy that changes meaning ("approved" \u2192 "denied")
146313
+ - mixed diffs where a semantic 1-liner is buried in whitespace/formatting changes
146314
+
146315
+ When unsure, treat as non-trivial. The cost of one extra subagent is cents; the cost of a missed billing/auth/data bug is much more.
146316
+
146317
+ otherwise pick lenses by where the PR concentrates risk \u2014 **there's no fixed count**. lens count is judgment, not a formula. concrete shapes to anchor against:
146318
+
146319
+ - **1 lens** \u2014 pure refactor / mechanical rename across many files (impact); new test file with no source change (test-integrity); small isolated bug fix (correctness); doc-only PR with non-trivial technical content (research-validated or holistic)
146320
+ - **2\u20133 lenses (most PRs land here)** \u2014 new CRUD endpoint (correctness + security + test-integrity); new UI flow (user-journey + correctness); a single bug fix in a non-critical subsystem (correctness + test-integrity); design doc covering one domain (research-validated + correctness or holistic)
146321
+ - **4\u20135 lenses (high-stakes subsystem touches)** \u2014 any billing/payments change (billing-subsystem + correctness + security + operational-readiness); new auth flow (auth-subsystem + correctness + security + test-integrity); schema migration (schema-migration-subsystem + correctness + operational-readiness + impact); cross-subsystem PR that touches billing AND auth AND schema (one subsystem lens per domain + correctness)
146322
+ - **6+ lenses** \u2014 almost always a smell; you're either covering overlapping ground or this PR should have been split. push back via the review body rather than expanding lens count.
146323
+
146324
+ lenses come in two flavors, and you can mix them:
146325
+ - **themed lenses** \u2014 a perspective applied across the whole diff (correctness, security, user-journey, performance, etc.).
146326
+ - **subsystem lenses** \u2014 a domain-scoped frame for high-stakes subsystems the PR touches (e.g. "the auth lens", "the billing lens", "the schema-migration lens"). a subsystem lens is "review the PR specifically for what could go wrong in this subsystem" and naturally combines theme + scope. **for high-stakes domains, lead with the subsystem lens rather than the generic themed equivalent** \u2014 "billing-subsystem" outperforms "correctness on billing code" because the framing primes the subagent to remember domain-specific failure modes (double-charges, refund races, currency rounding, dispute flows) the generic lens misses.
146327
+
146328
+ starter menu (combine, omit, or invent your own):
146329
+ - **correctness & invariants** \u2014 bugs, races, error handling, edge cases, state-machine boundaries
146330
+ - **impact** \u2014 when the PR removes features, deletes exports, renames identifiers, or changes architectural patterns: stale references in code, tests, docs (\`docs/\`, \`wiki/\`), comments, configs, UI
146331
+ - **research-validated assumptions** \u2014 third-party API contracts, SDK semantics, framework directives, version-gated behavior. the subagent must verify load-bearing claims via web search and quote source URLs.
146332
+ - **security** \u2014 new endpoints, authZ, input validation, secrets handling, replay/CSRF/injection, cross-tenant isolation
146333
+ - **user-journey** \u2014 UX-touching flows: walk through happy path and failure modes as a user
146334
+ - **operational readiness** \u2014 observability, alerting, migrations (forward + rollback), feature flags, on-call burden
146335
+ - **integration & cross-cutting** \u2014 API contracts between modules, backward-compat of public surfaces, multi-service ordering
146336
+ - **test integrity** \u2014 meaningful coverage for the changed behavior; deterministic; no shared-state pollution
146337
+ - **performance** \u2014 N+1 queries, hot-path allocation, latency budgets, index coverage
146338
+ - **holistic** \u2014 does the PR make sense as a whole? symmetric flows (delete for every create, rollback for every migration)?
146339
+ - **subsystem lenses** (invent as the PR demands) \u2014 auth, billing, payments, schema migration, webhooks, secrets, RBAC, multi-tenant isolation, cron/scheduling, etc.
146340
+
146341
+ 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:
146342
+ - the diff path / target \u2014 reading the diff and the codebase is its job
146343
+ - **only one lens** \u2014 never a multi-section "review for X, Y, and Z" prompt
146344
+ - **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\`.
146345
+ - 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.
146346
+ - if the lens touches external contracts, instruct the subagent to verify load-bearing claims via web search rather than trust training data, and to quote source URLs in its reasoning. action runs are non-interactive \u2014 there's no human in the loop to catch "I'm pretty sure Stripe does X."
146347
+ - ask the subagent to report findings with file paths and NEW line numbers from the diff so you can anchor inline comments without re-reading the entire diff.
146348
+
146349
+ delegation discipline:
146350
+ - do NOT lens-review the diff yourself in parallel with the subagents (your job is dispatch + comment-drafting; doing the lens work yourself reintroduces the bias the fan-out avoids)
146351
+ - do NOT summarize the PR for them (biases toward a validation frame)
146352
+ - do NOT hand them a curated reading list (let them discover scope)
146353
+ - do NOT pre-shape their output with a finding schema
146354
+ - do NOT mention the other lenses (independence is the point \u2014 overlapping findings are a strong signal)
146355
+
146356
+ 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.
146357
+
146358
+ 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.
146359
+
146360
+ 5. **submit**: ALWAYS submit exactly one review via \`${t("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.
146361
+
146362
+ 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.
146363
+
146364
+ 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.
146365
+
146366
+ - **critical issues** (blocks merge \u2014 bugs, security, data loss):
146367
+ \`approved: false\`. Body opens with \`> [!CAUTION]\\n> This PR introduces ...\`, followed by the PR summary. Include all inline comments via \`comments\`.
146368
+ - **recommended changes** (non-critical):
146369
+ \`approved: false\`. Body opens with \`> [!IMPORTANT]\\n> Consider ...\`, followed by the PR summary. Include all inline comments via \`comments\`.
146370
+ - **no actionable issues**:
146371
+ \`approved: true\`. Body opens with \`No new issues found.\` followed by the PR summary.
146372
+
146373
+ ${PR_SUMMARY_FORMAT}`
146374
+ },
146375
+ // IncrementalReview shares Review's multi-lens orchestrator pattern but
146376
+ // scopes the target to the incremental diff. The "issues must be NEW
146377
+ // since the last Pullfrog review" filter lives at aggregation time
146378
+ // (step 5), NOT in the subagent prompt — pushing the filter into
146379
+ // subagents matches the canonical anneal anti-pattern of "list known
146380
+ // pre-existing failures — don't flag these" and suppresses signal on
146381
+ // regressions the new commits amplified. The review body is just
146382
+ // "Reviewed changes" — a separate "Prior review feedback" checklist
146383
+ // would duplicate the rolling PR summary snapshot's record of what
146384
+ // earlier runs already addressed and add noise to the user-facing
146385
+ // body. Same severity-table omission as Review.
146386
+ {
146387
+ name: "IncrementalReview",
146388
+ description: "Re-review a PR after new commits are pushed; focus on new changes since the last review",
146389
+ prompt: `### Checklist
146390
+
146391
+ 1. **checkout**: call \`${t("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.
146392
+
146393
+ 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.
146394
+
146395
+ 3. **prior feedback**: fetch previous reviews via \`${t("list_pull_request_reviews")}\`. for the most recent Pullfrog review, call \`${t("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.
146396
+
146397
+ 4. **triage & fan out**: orient on the *incremental* changes \u2014 domain, seams, external contracts, user-facing surfaces.
146398
+
146399
+ 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).
146400
+
146401
+ "Genuinely trivial" (skip): formatting/comment tweaks, import reordering, lockfile regen, mechanical rename of import paths, whitespace-only.
146402
+ "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.
146403
+ When unsure, treat as non-trivial.
146404
+
146405
+ 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.
146406
+
146407
+ 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:
146408
+ - 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
146409
+ - **only one lens** \u2014 never a multi-section "review for X, Y, and Z" prompt
146410
+ - **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\`.
146411
+ - 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.
146412
+ - if the lens touches external contracts, instruct the subagent to verify load-bearing claims via web search and quote source URLs. action runs are non-interactive \u2014 there's no human to catch "I'm pretty sure Stripe does X."
146413
+ - ask the subagent to report findings with file paths and NEW line numbers from the full PR diff so you can anchor inline comments.
146414
+
146415
+ delegation discipline:
146416
+ - do NOT lens-review the diff yourself in parallel with the subagents
146417
+ - do NOT summarize the changes for them (biases toward validation frame)
146418
+ - do NOT hand them a curated reading list (let them discover scope)
146419
+ - do NOT pre-shape their output with a finding schema
146420
+ - do NOT mention the other lenses (independence is the point)
146421
+
146422
+ 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 \`${t("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.
146423
+
146424
+ 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.
146425
+
146426
+ 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:
146427
+ - 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.
146428
+ - 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.
146429
+ - ELSE IF NEW CRITICAL ISSUES (blocks merge): call \`${t("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.
146430
+ - ELSE IF NEW RECOMMENDED CHANGES (non-critical): call \`${t("create_pull_request_review")}\` with \`approved: false\`, all comments, and the review body. body opens with \`> [!IMPORTANT]\\n> ...\` alert, then the Reviewed-changes summary.
146431
+ - ELSE IF NO NEW ISSUES, SUBSTANTIVE CHANGES (new functionality, behavior changes, or fixes to prior review feedback): call \`${t("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.`
146432
+ },
146433
+ {
146434
+ name: "Plan",
146435
+ description: "Create plans, break down tasks, outline steps, analyze requirements, understand scope of work, or provide task breakdowns",
146436
+ prompt: `### Checklist
146437
+
146438
+ 1. Analyze the task and gather context:
146439
+ - read AGENTS.md and relevant codebase files
146440
+ - understand the architecture and constraints
146441
+
146442
+ 2. Produce a structured, actionable plan with clear milestones.
146443
+
146444
+ 3. Call \`${t("report_progress")}\` with the plan.
146445
+
146446
+ ${learningsStep(t, 4)}`
146447
+ },
146448
+ {
146449
+ name: "Fix",
146450
+ description: "Fix CI failures; debug failing tests or builds; investigate and resolve check suite failures",
146451
+ prompt: `### Checklist
146452
+
146453
+ 1. Checkout the PR branch via \`${t("checkout_pr")}\`.
146454
+
146455
+ 2. Fetch check suite logs via \`${t("get_check_suite_logs")}\`.
146456
+
146457
+ 3. **CRITICAL**: verify the failure was INTRODUCED BY THIS PR before fixing. If unrelated, abort and report.
146458
+
146459
+ 4. Diagnose and fix:
146460
+ - read the workflow file, reproduce locally with the EXACT same commands CI runs
146461
+ - fix the issue using your native file and shell tools
146462
+ - verify the fix by re-running the exact CI command
146463
+ - 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.
146464
+ - commit locally via shell (\`git add . && git commit -m "..."\`)
146465
+
146466
+ 5. Finalize:
146467
+ - confirm a clean working tree, then push via \`${t("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*)
146468
+ - call \`${t("report_progress")}\` with the diagnosis and fix summary (or the exact push error if push failed)
146469
+
146470
+ ${learningsStep(t, 6)}`
146471
+ },
146472
+ {
146473
+ name: "ResolveConflicts",
146474
+ description: "Resolve merge conflicts in a PR branch against the base branch",
146475
+ prompt: `### Checklist
146476
+
146477
+ 1. **Setup**:
146478
+ - Call \`${t("checkout_pr")}\` to get the PR branch.
146479
+ - Call \`${t("get_pull_request")}\` to identify the base branch (e.g., 'main').
146480
+ - Call \`${t("git_fetch")}\` to fetch the base branch.
146481
+
146482
+ 2. **Merge Attempt**:
146483
+ - Run \`git merge origin/<base_branch>\` via shell.
146484
+ - If it succeeds automatically, confirm a clean working tree, push via \`${t("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*), and call \`${t("report_progress")}\` with a brief success note or the exact push error if push failed \u2014 **then stop; do not run steps 3\u20134.**
146485
+ - If it fails (conflicts), resolve them manually (continue to steps 3\u20134).
146486
+
146487
+ 3. **Resolve Conflicts**:
146488
+ - Run \`git status\` or parse the merge output to find the list of conflicting files.
146489
+ - 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.
146490
+ - Verify the file syntax is correct after resolution.
146491
+
146492
+ 4. **Finalize**:
146493
+ - Run a final verification (build/test) to ensure the resolution works.
146494
+ - \`git add . && git commit -m "resolve merge conflicts"\`
146495
+ - confirm a clean working tree, then push via \`${t("push_branch")}\` (same push/prepush guidance as Build mode in *SYSTEM*)
146496
+ - Call \`${t("report_progress")}\` with a summary of what was resolved (or the exact push error if push failed)`
146497
+ },
146498
+ {
146499
+ name: "Task",
146500
+ description: "General-purpose tasks that don't fit other modes: answering questions, adding comments, labeling, running ad-hoc commands, or any direct request",
146501
+ prompt: `### Checklist
146502
+
146503
+ 1. Analyze the task. For simple operations (labeling, commenting, answering questions, running a single command), handle directly.
146504
+
146505
+ 2. For substantial work \u2014 code changes across multiple files, multi-step investigations:
146506
+ - plan your approach before starting
146507
+ - use native file and shell tools for local operations
146508
+ - use ${pullfrogMcpName} MCP tools for GitHub/git operations
146509
+ - 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
146510
+
146511
+ 3. Finalize:
146512
+ - if code changes were made, push to a pull request (new or existing) using \`${t("push_branch")}\` and \`${t("create_pull_request")}\` as needed. \`git status\` must be clean before you finish (see *SYSTEM* Git rules if push fails).
146513
+ - call \`${t("report_progress")}\` once with results \u2014 include exact tool errors if push or PR creation failed
146514
+ - if the task involved labeling, commenting, or other GitHub operations, perform those directly
146515
+
146516
+ ${learningsStep(t, 4)}`
146517
+ }
146518
+ ];
146519
+ }
146520
+ var modes = computeModes("opencode");
146521
+
146507
146522
  // agents/claude.ts
146508
146523
  import { execFileSync as execFileSync3 } from "node:child_process";
146509
146524
  import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync7 } from "node:fs";
@@ -146596,23 +146611,41 @@ async function installFromNpmTarball(params) {
146596
146611
  }
146597
146612
 
146598
146613
  // utils/providerErrors.ts
146614
+ var statusKey = `\\b(?:status[_ ]?code|http[_ ]?status|status)["']?\\s*[:=]\\s*["']?`;
146599
146615
  var PROVIDER_ERROR_PATTERNS = [
146600
- { pattern: "429", label: "rate limited (429)" },
146601
- { pattern: "RESOURCE_EXHAUSTED", label: "quota exhausted" },
146602
- { pattern: "quota", label: "quota error" },
146603
- { pattern: "status: 500", label: "provider 500 error" },
146604
- { pattern: "INTERNAL", label: "provider internal error" },
146605
- { pattern: "status: 503", label: "provider unavailable (503)" },
146606
- { pattern: "UNAVAILABLE", label: "provider unavailable" },
146607
- { pattern: "rate limit", label: "rate limited" },
146608
- { pattern: "limit: 0", label: "zero quota" }
146616
+ { regex: new RegExp(`${statusKey}429\\b`, "i"), label: "rate limited (429)" },
146617
+ { regex: new RegExp(`${statusKey}500\\b`, "i"), label: "provider 500 error" },
146618
+ { regex: new RegExp(`${statusKey}503\\b`, "i"), label: "provider unavailable (503)" },
146619
+ // matches `rate limit`, `rate limited`, `rate limits exceeded`,
146620
+ // `rate_limit_error`, `rate_limit_exceeded`. the leading `\b` + `[_ ]`
146621
+ // separator rejects `x-ratelimit-*` / `anthropic-ratelimit-*` response
146622
+ // headers (no separator between "rate" and "limit") which routinely
146623
+ // appear in dumped 401 / 4xx error JSON.
146624
+ { regex: /\brate[_ ]limit/i, label: "rate limited" },
146625
+ { regex: /\bRESOURCE_EXHAUSTED\b/, label: "quota exhausted" },
146626
+ // Google gRPC `INTERNAL` status. word-boundary anchors reject
146627
+ // `INTERNAL_SERVER_ERROR` (HTTP 500 message that may appear in unrelated
146628
+ // log lines) and identifiers like `INTERNALS`.
146629
+ { regex: /\bINTERNAL\b/, label: "provider internal error" },
146630
+ { regex: /\bUNAVAILABLE\b/, label: "provider unavailable" },
146631
+ // matches `quota`, `insufficient_quota`, `quota_exceeded`, `quotaExceeded`.
146632
+ // word-character lookarounds would reject `_quota` / `quotaX`; `quota` is
146633
+ // specific enough that a plain substring match is safe.
146634
+ { regex: /quota/i, label: "quota error" },
146635
+ // explicit zero-quota response, e.g. `{"limit": 0}`. the `\b` anchor
146636
+ // around `limit` rejects keys like `time_limit` or `field_limit`.
146637
+ { regex: /["']?\blimit\b["']?\s*:\s*0\b/, label: "zero quota" }
146609
146638
  ];
146610
146639
  function detectProviderError(text) {
146611
146640
  for (const entry of PROVIDER_ERROR_PATTERNS) {
146612
- if (text.includes(entry.pattern)) return entry.label;
146641
+ if (entry.regex.test(text)) return entry.label;
146613
146642
  }
146614
146643
  return null;
146615
146644
  }
146645
+ var ROUTER_KEYLIMIT_EXHAUSTED_PATTERN = /requires more credits.*?fewer max_tokens|requested up to \d+ tokens.*?can only afford/is;
146646
+ function isRouterKeylimitExhaustedError(text) {
146647
+ return ROUTER_KEYLIMIT_EXHAUSTED_PATTERN.test(text);
146648
+ }
146616
146649
 
146617
146650
  // utils/skills.ts
146618
146651
  import { spawnSync as spawnSync5 } from "node:child_process";
@@ -146719,6 +146752,7 @@ var ThinkingTimer = class {
146719
146752
  };
146720
146753
 
146721
146754
  // agents/postRun.ts
146755
+ import { readFile } from "node:fs/promises";
146722
146756
  var MAX_HOOK_OUTPUT_CHARS = 4096;
146723
146757
  function truncateHookOutput(raw2) {
146724
146758
  if (raw2.length <= MAX_HOOK_OUTPUT_CHARS) return raw2;
@@ -146763,6 +146797,23 @@ function buildStopHookPrompt(failure) {
146763
146797
  "```"
146764
146798
  ].join("\n");
146765
146799
  }
146800
+ async function isSummaryUnchanged(filePath, seed) {
146801
+ try {
146802
+ const current = await readFile(filePath, "utf8");
146803
+ return current === seed;
146804
+ } catch {
146805
+ return false;
146806
+ }
146807
+ }
146808
+ function buildSummaryStalePrompt(filePath) {
146809
+ return [
146810
+ `PR SUMMARY UNTOUCHED \u2014 the rolling PR summary file at \`${filePath}\` is byte-identical to its seed; this run did not edit it.`,
146811
+ "",
146812
+ "review the diff and update the file in place to reflect what changed in the PR. update intent, key changes, and any risks worth flagging \u2014 keep the existing section headings stable so incremental runs produce clean diffs.",
146813
+ "",
146814
+ "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."
146815
+ ].join("\n");
146816
+ }
146766
146817
  async function collectPostRunIssues(params) {
146767
146818
  const issues = {};
146768
146819
  if (params.stopScript) {
@@ -146771,12 +146822,17 @@ async function collectPostRunIssues(params) {
146771
146822
  }
146772
146823
  const status = getGitStatus();
146773
146824
  if (status) issues.dirtyTree = status;
146825
+ if (params.summaryFilePath && params.summarySeed !== void 0) {
146826
+ const stale = await isSummaryUnchanged(params.summaryFilePath, params.summarySeed);
146827
+ if (stale) issues.summaryStale = { filePath: params.summaryFilePath };
146828
+ }
146774
146829
  return issues;
146775
146830
  }
146776
146831
  function buildPostRunPrompt(issues) {
146777
146832
  const parts = [];
146778
146833
  if (issues.stopHook) parts.push(buildStopHookPrompt(issues.stopHook));
146779
146834
  if (issues.dirtyTree) parts.push(buildCommitPrompt(issues.dirtyTree));
146835
+ if (issues.summaryStale) parts.push(buildSummaryStalePrompt(issues.summaryStale.filePath));
146780
146836
  return parts.join("\n\n---\n\n");
146781
146837
  }
146782
146838
  function buildLearningsReflectionPrompt(agentId) {
@@ -146799,9 +146855,15 @@ async function runPostRunRetryLoop(params) {
146799
146855
  let finalIssues = {};
146800
146856
  let gateResumeCount = 0;
146801
146857
  let pendingReflection = params.reflectionPrompt;
146858
+ let summaryStaleNudged = false;
146802
146859
  while (gateResumeCount < MAX_POST_RUN_RETRIES) {
146803
146860
  if (!result.success) break;
146804
- const issues = await collectPostRunIssues({ stopScript: params.stopScript });
146861
+ const issues = await collectPostRunIssues({
146862
+ stopScript: params.stopScript,
146863
+ summaryFilePath: summaryStaleNudged ? void 0 : params.summaryFilePath,
146864
+ summarySeed: summaryStaleNudged ? void 0 : params.summarySeed
146865
+ });
146866
+ if (issues.summaryStale) summaryStaleNudged = true;
146805
146867
  finalIssues = issues;
146806
146868
  if (!hasPostRunIssues(issues)) {
146807
146869
  if (!pendingReflection) break;
@@ -146833,8 +146895,17 @@ async function runPostRunRetryLoop(params) {
146833
146895
  }
146834
146896
  log.info(`\xBB post-run retry (attempt ${gateResumeCount + 1}/${MAX_POST_RUN_RETRIES})`);
146835
146897
  const prompt = buildPostRunPrompt(issues);
146898
+ const onlySummaryStale = issues.summaryStale !== void 0 && issues.stopHook === void 0 && issues.dirtyTree === void 0;
146899
+ const preResume = result;
146836
146900
  result = await params.resume({ prompt, previousResult: result });
146837
146901
  aggregatedUsage = mergeAgentUsage(aggregatedUsage, result.usage);
146902
+ if (!result.success && onlySummaryStale) {
146903
+ log.warning(
146904
+ `\xBB summary-stale resume turn failed (${result.error ?? "unknown error"}), preserving prior successful result`
146905
+ );
146906
+ result = preResume;
146907
+ break;
146908
+ }
146838
146909
  gateResumeCount++;
146839
146910
  }
146840
146911
  if (gateResumeCount > 0 && result.success && hasPostRunIssues(finalIssues)) {
@@ -146971,6 +147042,7 @@ async function runClaude(params) {
146971
147042
  const thinkingTimer = new ThinkingTimer();
146972
147043
  let finalOutput = "";
146973
147044
  let sessionId;
147045
+ let resultErrorSubtype = null;
146974
147046
  let accumulatedTokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
146975
147047
  let accumulatedCostUsd = 0;
146976
147048
  let tokensLogged = false;
@@ -147074,9 +147146,14 @@ async function runClaude(params) {
147074
147146
  tokensLogged = true;
147075
147147
  }
147076
147148
  } else if (subtype === "error_max_turns") {
147149
+ resultErrorSubtype = subtype;
147077
147150
  log.info(`\xBB ${params.label} max turns reached: ${JSON.stringify(event)}`);
147078
147151
  } else if (subtype === "error_during_execution") {
147152
+ resultErrorSubtype = subtype;
147079
147153
  log.info(`\xBB ${params.label} execution error: ${JSON.stringify(event)}`);
147154
+ } else if (subtype.startsWith("error")) {
147155
+ resultErrorSubtype = subtype;
147156
+ log.info(`\xBB ${params.label} result: subtype=${subtype}, data=${JSON.stringify(event)}`);
147080
147157
  } else {
147081
147158
  log.info(`\xBB ${params.label} result: subtype=${subtype}, data=${JSON.stringify(event)}`);
147082
147159
  }
@@ -147207,6 +147284,15 @@ ${stderrContext}`);
147207
147284
  sessionId
147208
147285
  };
147209
147286
  }
147287
+ if (resultErrorSubtype) {
147288
+ return {
147289
+ success: false,
147290
+ output: finalOutput || output,
147291
+ error: `result subtype: ${resultErrorSubtype}`,
147292
+ usage,
147293
+ sessionId
147294
+ };
147295
+ }
147210
147296
  return { success: true, output: finalOutput || output, usage, sessionId };
147211
147297
  } catch (error49) {
147212
147298
  params.todoTracker?.cancel();
@@ -147335,6 +147421,8 @@ var claude = agent({
147335
147421
  initialResult: result,
147336
147422
  initialUsage: result.usage,
147337
147423
  stopScript: ctx.stopScript,
147424
+ summaryFilePath: ctx.summaryFilePath,
147425
+ summarySeed: ctx.summarySeed,
147338
147426
  reflectionPrompt: buildLearningsReflectionPrompt("claude"),
147339
147427
  canResume: (r) => Boolean(r.sessionId),
147340
147428
  resume: async (c) => {
@@ -147362,6 +147450,7 @@ async function installOpencodeCli() {
147362
147450
  installDependencies: true
147363
147451
  });
147364
147452
  }
147453
+ var PULLFROG_OPENCODE_OUTPUT_LIMIT = 5e3;
147365
147454
  function buildSecurityConfig(ctx, model) {
147366
147455
  const config3 = {
147367
147456
  permission: {
@@ -147668,6 +147757,12 @@ async function runOpenCode(params) {
147668
147757
  log.debug(withLabel(label, `tool output: ${outputStr}`));
147669
147758
  }
147670
147759
  },
147760
+ error: (event) => {
147761
+ agentErrorEvent = event;
147762
+ const errorName = event.error?.name || "unknown";
147763
+ const errorMessage = event.error?.data?.message || event.error?.name || JSON.stringify(event);
147764
+ log.info(`\xBB ${params.label} error event: ${errorName}: ${errorMessage}`);
147765
+ },
147671
147766
  result: async (event) => {
147672
147767
  const status = event.status || "unknown";
147673
147768
  const duration4 = event.stats?.duration_ms || 0;
@@ -147688,6 +147783,7 @@ async function runOpenCode(params) {
147688
147783
  };
147689
147784
  const recentStderr = [];
147690
147785
  let lastProviderError = null;
147786
+ let agentErrorEvent = null;
147691
147787
  let output = "";
147692
147788
  let stdoutBuffer = "";
147693
147789
  try {
@@ -147806,6 +147902,17 @@ ${stderrContext}`);
147806
147902
  usage
147807
147903
  };
147808
147904
  }
147905
+ if (agentErrorEvent) {
147906
+ const errorEvent = agentErrorEvent;
147907
+ const errorName = errorEvent.error?.name || "agent error";
147908
+ const errorMessage = errorEvent.error?.data?.message || errorEvent.error?.name || JSON.stringify(errorEvent);
147909
+ return {
147910
+ success: false,
147911
+ output: finalOutput || output,
147912
+ error: `${errorName}: ${errorMessage}`,
147913
+ usage
147914
+ };
147915
+ }
147809
147916
  return { success: true, output: finalOutput || output, usage };
147810
147917
  } catch (error49) {
147811
147918
  params.todoTracker?.cancel();
@@ -147859,6 +147966,7 @@ var opencode = agent({
147859
147966
  ...homeEnv,
147860
147967
  OPENCODE_CONFIG_CONTENT: buildSecurityConfig(ctx, model),
147861
147968
  OPENCODE_PERMISSION: permissionOverride,
147969
+ OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX: PULLFROG_OPENCODE_OUTPUT_LIMIT.toString(),
147862
147970
  GOOGLE_GENERATIVE_AI_API_KEY: process.env.GOOGLE_GENERATIVE_AI_API_KEY || process.env.GEMINI_API_KEY
147863
147971
  };
147864
147972
  const repoDir = process.cwd();
@@ -147881,6 +147989,8 @@ var opencode = agent({
147881
147989
  initialResult: result,
147882
147990
  initialUsage: result.usage,
147883
147991
  stopScript: ctx.stopScript,
147992
+ summaryFilePath: ctx.summaryFilePath,
147993
+ summarySeed: ctx.summarySeed,
147884
147994
  reflectionPrompt: buildLearningsReflectionPrompt("opencode"),
147885
147995
  resume: async (c) => runOpenCode({
147886
147996
  ...runParams,
@@ -152427,7 +152537,7 @@ When embedding images (e.g. uploaded screenshots) in comments or PR bodies, alwa
152427
152537
 
152428
152538
  **\`report_progress\`**: call this exactly once at the end of every run with a brief final summary (1-3 sentences) unless the mode guidance instructs otherwise. Never call it for intermediate status updates (e.g., "Checking for changes...", "Starting review...") \u2014 the task list handles live progress automatically. Calling \`report_progress\` replaces the task list with your summary and preserves the current task list in a collapsible section. Keep the summary concise \u2014 do not repeat what the task list already shows. Focus on the outcome (what was accomplished, links to artifacts) rather than listing individual steps. If something failed, include the tool's error text even when that makes the summary longer.
152429
152539
 
152430
- Never use \`create_issue_comment\` for task progress \u2014 that creates duplicate comments and leaves the progress comment stuck in its initial state. \`create_issue_comment\` is only for standalone comments unrelated to your current task (e.g., Plan comments, PR Summary comments).
152540
+ Never use \`create_issue_comment\` for task progress \u2014 that creates duplicate comments and leaves the progress comment stuck in its initial state. \`create_issue_comment\` is only for standalone comments unrelated to your current task (e.g., Plan comments).
152431
152541
 
152432
152542
  ### If you get stuck
152433
152543
 
@@ -152591,7 +152701,8 @@ var JsonPayload = type({
152591
152701
  "progressComment?": type({
152592
152702
  id: "string",
152593
152703
  type: "'issue' | 'review'"
152594
- }).or("undefined")
152704
+ }).or("undefined"),
152705
+ "generateSummary?": "boolean | undefined"
152595
152706
  });
152596
152707
  var COLLABORATOR_PERMISSIONS = ["admin", "maintain", "write"];
152597
152708
  function isCollaborator(event) {
@@ -152674,6 +152785,7 @@ function resolvePayload(resolvedPromptInput, repoSettings) {
152674
152785
  timeout: inputs.timeout ?? jsonPayload?.timeout,
152675
152786
  cwd: resolveCwd(inputs.cwd),
152676
152787
  progressComment: jsonPayload?.progressComment,
152788
+ generateSummary: jsonPayload?.generateSummary,
152677
152789
  // permissions: inputs > repoSettings > fallbacks
152678
152790
  push: inputs.push ?? repoSettings.push ?? "restricted",
152679
152791
  shell: resolvedShell,
@@ -152682,6 +152794,40 @@ function resolvePayload(resolvedPromptInput, repoSettings) {
152682
152794
  };
152683
152795
  }
152684
152796
 
152797
+ // utils/prSummary.ts
152798
+ import { mkdir, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
152799
+ import { dirname as dirname4, join as join14 } from "node:path";
152800
+ var SUMMARY_FILE_NAME = "pullfrog-summary.md";
152801
+ var SUMMARY_SCAFFOLD = `# PR summary
152802
+
152803
+ <!-- durable cross-run context. edit in place; the next agent run reads this
152804
+ before reviewing new commits. structure however serves the PR best. -->
152805
+ `;
152806
+ var MIN_SNAPSHOT_LENGTH = 60;
152807
+ var MAX_SNAPSHOT_LENGTH = 32768;
152808
+ function summaryFilePath(tmpdir3) {
152809
+ return join14(tmpdir3, SUMMARY_FILE_NAME);
152810
+ }
152811
+ async function seedSummaryFile(params) {
152812
+ const path3 = summaryFilePath(params.tmpdir);
152813
+ await mkdir(dirname4(path3), { recursive: true });
152814
+ const seed = params.previousSnapshot && params.previousSnapshot.trim().length >= MIN_SNAPSHOT_LENGTH ? params.previousSnapshot : SUMMARY_SCAFFOLD;
152815
+ await writeFile2(path3, seed, "utf8");
152816
+ return path3;
152817
+ }
152818
+ async function readSummaryFile(path3) {
152819
+ let raw2;
152820
+ try {
152821
+ raw2 = await readFile2(path3, "utf8");
152822
+ } catch {
152823
+ return null;
152824
+ }
152825
+ const trimmed = raw2.trim();
152826
+ if (trimmed.length < MIN_SNAPSHOT_LENGTH) return null;
152827
+ if (trimmed.length > MAX_SNAPSHOT_LENGTH) return trimmed.slice(0, MAX_SNAPSHOT_LENGTH);
152828
+ return trimmed;
152829
+ }
152830
+
152685
152831
  // utils/reviewCleanup.ts
152686
152832
  var RE_REVIEW_PREAMBLE = "Incrementally re-review the new commits on this pull request. Use the IncrementalReview mode.";
152687
152833
  async function postReviewCleanup(ctx) {
@@ -152741,11 +152887,16 @@ async function dispatchFollowUpReReview(ctx, reviewedSha) {
152741
152887
  await ctx.octokit.rest.actions.createWorkflowDispatch({
152742
152888
  owner: ctx.repo.owner,
152743
152889
  repo: ctx.repo.name,
152744
- workflow_id: "pullfrog.yml",
152890
+ workflow_id: getCurrentWorkflowFilename(),
152745
152891
  ref: pr.data.base.repo.default_branch,
152746
152892
  inputs: { prompt: JSON.stringify(payload) }
152747
152893
  });
152748
152894
  }
152895
+ function getCurrentWorkflowFilename() {
152896
+ const ref = process.env.GITHUB_WORKFLOW_REF ?? "";
152897
+ const match3 = ref.match(/\/([^/]+)@/);
152898
+ return match3?.[1] ?? "pullfrog.yml";
152899
+ }
152749
152900
 
152750
152901
  // utils/run.ts
152751
152902
  async function handleAgentResult(ctx) {
@@ -152885,9 +153036,9 @@ async function resolveRunContextData(params) {
152885
153036
  import { execFileSync as execFileSync5, execSync as execSync3 } from "node:child_process";
152886
153037
  import { mkdtempSync } from "node:fs";
152887
153038
  import { tmpdir as tmpdir2 } from "node:os";
152888
- import { join as join14 } from "node:path";
153039
+ import { join as join15 } from "node:path";
152889
153040
  function createTempDirectory() {
152890
- const sharedTempDir = mkdtempSync(join14(tmpdir2(), "pullfrog-"));
153041
+ const sharedTempDir = mkdtempSync(join15(tmpdir2(), "pullfrog-"));
152891
153042
  process.env.PULLFROG_TEMP_DIR = sharedTempDir;
152892
153043
  log.info(`\xBB created temp dir at ${sharedTempDir}`);
152893
153044
  return sharedTempDir;
@@ -153220,39 +153371,71 @@ var TransientError = class extends Error {
153220
153371
  this.name = "TransientError";
153221
153372
  }
153222
153373
  };
153223
- function formatBillingErrorSummary(error49) {
153374
+ function billingConsoleUrl(owner, anchor) {
153375
+ return `https://pullfrog.com/console/${encodeURIComponent(owner)}#${anchor}`;
153376
+ }
153377
+ function formatBillingErrorSummary(error49, owner) {
153224
153378
  if (error49.code === "router_requires_card") {
153225
153379
  return [
153226
- "### \u26D4 Pullfrog Router requires a card",
153380
+ "**Add a card to start using Pullfrog Router.**",
153381
+ "",
153382
+ "Router proxies OpenRouter at raw cost \u2014 no platform markup, and your first $20 of usage is on us.",
153383
+ "",
153384
+ `[Add a card \u2192](${billingConsoleUrl(owner, "model-access")})`
153385
+ ].join("\n");
153386
+ }
153387
+ if (error49.code === "router_balance_exhausted") {
153388
+ return [
153389
+ "**Your Pullfrog Router balance is exhausted.**",
153227
153390
  "",
153228
- "This run was going to use Pullfrog Router, which bills at raw OpenRouter cost and needs a card on file. Runs won't proceed until a card is added.",
153391
+ "You have a card on file but auto-reload is disabled, so runs paused once your balance went past the overdraft buffer.",
153229
153392
  "",
153230
- "[Add a card \u2192](https://pullfrog.com/console#model-access) \u2014 your first $20 of Router usage is free."
153393
+ `[Top up balance \u2192](${billingConsoleUrl(owner, "billing")}) \xB7 [Enable auto-reload \u2192](${billingConsoleUrl(owner, "model-access")})`
153394
+ ].join("\n");
153395
+ }
153396
+ if (error49.code === "router_keylimit_exhausted") {
153397
+ return [
153398
+ "**This run was cut short \u2014 your Pullfrog Router balance ran out mid-run.**",
153399
+ "",
153400
+ "OpenRouter stopped the agent because the per-run budget was exhausted. Your wallet is now negative; top up or enable auto-reload to keep runs flowing.",
153401
+ "",
153402
+ `[Top up balance \u2192](${billingConsoleUrl(owner, "billing")}) \xB7 [Enable auto-reload \u2192](${billingConsoleUrl(owner, "model-access")})`
153231
153403
  ].join("\n");
153232
153404
  }
153233
153405
  if (error49.needsReauthentication) {
153406
+ const code = error49.declineCode ?? "authentication_required";
153234
153407
  return [
153235
- "### \u274C Pullfrog billing error \u2014 card requires 3DS on every charge",
153408
+ `**Your card issuer requires 3D Secure on every charge** (\`${code}\`).`,
153236
153409
  "",
153237
- `Your card issuer requires a 3D Secure challenge on each off-session charge (\`${error49.declineCode ?? "authentication_required"}\`), which we can't run from the agent. Top up your Router credit balance manually \u2014 3DS runs interactively in Stripe Checkout, and subsequent runs draw from the prepaid balance without triggering another off-session charge.`,
153410
+ "Pullfrog can't complete a 3DS challenge from inside a workflow. Top up your Router balance once in Stripe Checkout \u2014 subsequent runs draw from the prepaid balance without re-triggering 3DS.",
153238
153411
  "",
153239
- "[Top up your Router credit balance \u2192](https://pullfrog.com/console)"
153412
+ `[Top up balance \u2192](${billingConsoleUrl(owner, "billing")})`
153240
153413
  ].join("\n");
153241
153414
  }
153242
- const codeSuffix = error49.declineCode ? ` (\`${error49.declineCode}\`)` : "";
153243
- return `### \u274C Pullfrog billing error
153244
-
153245
- ${error49.message}${codeSuffix}
153246
-
153247
- [Manage billing \u2192](https://pullfrog.com/console)`;
153415
+ if (error49.declineCode) {
153416
+ return [
153417
+ `**Your card was declined** (\`${error49.declineCode}\`).`,
153418
+ "",
153419
+ "Update your payment method and Pullfrog will retry on the next run.",
153420
+ "",
153421
+ `[Update payment method \u2192](${billingConsoleUrl(owner, "billing")})`
153422
+ ].join("\n");
153423
+ }
153424
+ return [
153425
+ "**Your Pullfrog balance is empty.**",
153426
+ "",
153427
+ "Top up your balance or enable auto-reload to keep runs flowing.",
153428
+ "",
153429
+ `[Manage billing \u2192](${billingConsoleUrl(owner, "billing")})`
153430
+ ].join("\n");
153248
153431
  }
153249
- function formatTransientErrorSummary(error49) {
153432
+ function formatTransientErrorSummary(error49, owner) {
153250
153433
  return [
153251
- "### \u26A0\uFE0F Pullfrog temporarily unavailable",
153434
+ "**Pullfrog billing is temporarily unavailable.**",
153252
153435
  "",
153253
153436
  error49.message,
153254
153437
  "",
153255
- "This is typically transient \u2014 the next dispatch should succeed. If it persists, check [status.pullfrog.com](https://status.pullfrog.com)."
153438
+ `Usually transient \u2014 the next dispatch should succeed. If it persists, check [status.pullfrog.com](https://status.pullfrog.com) or [your console](${billingConsoleUrl(owner, "billing")}).`
153256
153439
  ].join("\n");
153257
153440
  }
153258
153441
  async function mintProxyKey(ctx) {
@@ -153313,6 +153496,43 @@ async function resolveProxyModel(ctx) {
153313
153496
  const label = ctx.oss ? "oss" : "router";
153314
153497
  log.info(`\xBB proxy: ${label} \u2192 ${ctx.proxyModel}`);
153315
153498
  }
153499
+ async function fetchPreviousSnapshot(ctx, prNumber) {
153500
+ if (!ctx.githubInstallationToken) return null;
153501
+ try {
153502
+ const response = await apiFetch({
153503
+ path: `/api/repo/${ctx.repo.owner}/${ctx.repo.name}/pr/${prNumber}/summary-comment`,
153504
+ method: "GET",
153505
+ headers: { authorization: `Bearer ${ctx.githubInstallationToken}` },
153506
+ signal: AbortSignal.timeout(1e4)
153507
+ });
153508
+ if (!response.ok) return null;
153509
+ const data = await response.json();
153510
+ return typeof data.snapshot === "string" && data.snapshot.length > 0 ? data.snapshot : null;
153511
+ } catch {
153512
+ return null;
153513
+ }
153514
+ }
153515
+ async function persistSummary(ctx) {
153516
+ const filePath = ctx.toolState.summaryFilePath;
153517
+ if (!filePath) return;
153518
+ if (ctx.toolState.summaryPersistAttempted) return;
153519
+ ctx.toolState.summaryPersistAttempted = true;
153520
+ const snapshot2 = await readSummaryFile(filePath);
153521
+ if (!snapshot2) {
153522
+ log.debug(`pr summary tmpfile missing or invalid at ${filePath} \u2014 skipping persist`);
153523
+ return;
153524
+ }
153525
+ const seed = ctx.toolState.summarySeed?.trim();
153526
+ if (seed !== void 0 && snapshot2 === seed) {
153527
+ log.warning(
153528
+ "\xBB pr summary tmpfile unchanged from seed \u2014 skipping persist (agent did not edit it)"
153529
+ );
153530
+ return;
153531
+ }
153532
+ await patchWorkflowRunFields(ctx, { summarySnapshot: snapshot2 }).catch((err) => {
153533
+ log.debug(`pr summary persist failed: ${err instanceof Error ? err.message : String(err)}`);
153534
+ });
153535
+ }
153316
153536
  async function writeJobSummary(toolState) {
153317
153537
  const usageSummary = formatUsageSummary(toolState.usageEntries);
153318
153538
  const summaryParts = [toolState.lastProgressBody, usageSummary].filter(Boolean);
@@ -153377,7 +153597,7 @@ async function main() {
153377
153597
  });
153378
153598
  } catch (error49) {
153379
153599
  if (error49 instanceof BillingError) {
153380
- const summary2 = formatBillingErrorSummary(error49);
153600
+ const summary2 = formatBillingErrorSummary(error49, runContext.repo.owner);
153381
153601
  await writeSummary(summary2).catch(() => {
153382
153602
  });
153383
153603
  await reportErrorToComment({ toolState, error: summary2 }).catch(() => {
@@ -153385,7 +153605,7 @@ async function main() {
153385
153605
  throw error49;
153386
153606
  }
153387
153607
  if (error49 instanceof TransientError) {
153388
- const summary2 = formatTransientErrorSummary(error49);
153608
+ const summary2 = formatTransientErrorSummary(error49, runContext.repo.owner);
153389
153609
  await writeSummary(summary2).catch(() => {
153390
153610
  });
153391
153611
  await reportErrorToComment({ toolState, error: summary2 }).catch(() => {
@@ -153422,6 +153642,7 @@ async function main() {
153422
153642
  setGitAuthServer(gitAuthServer);
153423
153643
  const resolvedModel = payload.proxyModel ? void 0 : resolveModel({ slug: payload.model });
153424
153644
  const agent2 = resolveAgent({ model: resolvedModel });
153645
+ toolState.model = payload.proxyModel ?? resolvedModel ?? payload.model;
153425
153646
  validateAgentApiKey({
153426
153647
  agent: agent2,
153427
153648
  model: payload.proxyModel ?? resolvedModel ?? payload.model,
@@ -153475,6 +153696,20 @@ async function main() {
153475
153696
  toolContext.mcpServerUrl = mcpHttpServer.url;
153476
153697
  log.info(`\xBB MCP server started at ${mcpHttpServer.url}`);
153477
153698
  timer.checkpoint("mcpServer");
153699
+ if (payload.generateSummary && payload.event.is_pr && payload.event.issue_number) {
153700
+ const previousSnapshot = await fetchPreviousSnapshot(toolContext, payload.event.issue_number);
153701
+ const filePath = await seedSummaryFile({ tmpdir: tmpdir3, previousSnapshot });
153702
+ toolState.summaryFilePath = filePath;
153703
+ try {
153704
+ toolState.summarySeed = await readFile3(filePath, "utf8");
153705
+ } catch {
153706
+ }
153707
+ log.info(
153708
+ `\xBB summary snapshot seeded at ${filePath} (previous=${previousSnapshot ? "yes" : "no"})`
153709
+ );
153710
+ const ctxForExit = toolContext;
153711
+ onExitSignal(() => persistSummary(ctxForExit));
153712
+ }
153478
153713
  startInstallation(toolContext);
153479
153714
  const modelForLog = resolveModelForLog({ payload, resolvedModel });
153480
153715
  const agentForLog = resolveAgentForLog({ agentName: agent2.name, resolvedModel });
@@ -153506,7 +153741,7 @@ ${instructions.user}` : null,
153506
153741
  log.info(instructions.full);
153507
153742
  });
153508
153743
  if (agentId === "opencode") {
153509
- const pluginDir = join15(process.cwd(), ".opencode", "plugin");
153744
+ const pluginDir = join16(process.cwd(), ".opencode", "plugin");
153510
153745
  const hasPlugins = existsSync7(pluginDir) && readdirSync(pluginDir).some((f) => /\.[jt]sx?$/.test(f));
153511
153746
  if (hasPlugins && toolState.dependencyInstallation?.promise) {
153512
153747
  log.info(
@@ -153565,6 +153800,8 @@ ${instructions.user}` : null,
153565
153800
  instructions,
153566
153801
  todoTracker,
153567
153802
  stopScript: runContext.repoSettings.stopScript,
153803
+ summaryFilePath: toolState.summaryFilePath,
153804
+ summarySeed: toolState.summarySeed,
153568
153805
  onActivityTimeout: onInnerActivityTimeout,
153569
153806
  onToolUse: (event) => {
153570
153807
  const wasTracked = recordDiffReadFromToolUse({
@@ -153619,8 +153856,10 @@ ${instructions.user}` : null,
153619
153856
  log.debug(`post-review cleanup failed: ${error49}`);
153620
153857
  });
153621
153858
  }
153622
- const trackerWasLastWriter = todoTracker?.hasPublished && !toolState.finalSummaryWritten;
153623
- if (toolContext && toolState.progressComment && (!toolState.wasUpdated || trackerWasLastWriter)) {
153859
+ if (toolContext) {
153860
+ await persistSummary(toolContext);
153861
+ }
153862
+ if (toolContext && toolState.progressComment && !toolState.finalSummaryWritten) {
153624
153863
  await deleteProgressComment(toolContext).catch((error49) => {
153625
153864
  log.debug(`stranded progress comment cleanup failed: ${error49}`);
153626
153865
  });
@@ -153647,8 +153886,9 @@ ${instructions.user}` : null,
153647
153886
  todoTracker?.cancel();
153648
153887
  killTrackedChildren();
153649
153888
  log.error(errorMessage);
153889
+ const billingError = isRouterKeylimitExhaustedError(errorMessage) ? new BillingError(errorMessage, { code: "router_keylimit_exhausted" }) : null;
153650
153890
  try {
153651
- const errorSummary = `### \u274C Pullfrog failed
153891
+ const errorSummary = billingError ? formatBillingErrorSummary(billingError, runContext.repo.owner) : `### \u274C Pullfrog failed
153652
153892
 
153653
153893
  \`\`\`
153654
153894
  ${errorMessage}
@@ -153659,7 +153899,8 @@ ${errorMessage}
153659
153899
  } catch {
153660
153900
  }
153661
153901
  try {
153662
- await reportErrorToComment({ toolState, error: errorMessage });
153902
+ const commentBody = billingError ? formatBillingErrorSummary(billingError, runContext.repo.owner) : errorMessage;
153903
+ await reportErrorToComment({ toolState, error: commentBody });
153663
153904
  } catch {
153664
153905
  }
153665
153906
  if (toolContext) {
@@ -153667,6 +153908,9 @@ ${errorMessage}
153667
153908
  log.debug(`post-review cleanup failed: ${error50}`);
153668
153909
  });
153669
153910
  }
153911
+ if (toolContext) {
153912
+ await persistSummary(toolContext);
153913
+ }
153670
153914
  return {
153671
153915
  success: false,
153672
153916
  error: errorMessage