pullfrog 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -108027,7 +108027,8 @@ var providers = {
108027
108027
  displayName: "Claude Opus",
108028
108028
  resolve: "anthropic/claude-opus-4-7",
108029
108029
  openRouterResolve: "openrouter/anthropic/claude-opus-4.7",
108030
- preferred: true
108030
+ preferred: true,
108031
+ subagentModel: "claude-sonnet"
108031
108032
  },
108032
108033
  "claude-sonnet": {
108033
108034
  displayName: "Claude Sonnet",
@@ -108049,12 +108050,23 @@ var providers = {
108049
108050
  displayName: "GPT",
108050
108051
  resolve: "openai/gpt-5.5",
108051
108052
  openRouterResolve: "openrouter/openai/gpt-5.5",
108052
- preferred: true
108053
+ preferred: true,
108054
+ subagentModel: "gpt-5.4"
108053
108055
  },
108054
108056
  "gpt-pro": {
108055
108057
  displayName: "GPT Pro",
108056
108058
  resolve: "openai/gpt-5.5-pro",
108057
- openRouterResolve: "openrouter/openai/gpt-5.5-pro"
108059
+ openRouterResolve: "openrouter/openai/gpt-5.5-pro",
108060
+ subagentModel: "gpt"
108061
+ },
108062
+ // hidden subagent target — `gpt` lenses run against this. surfacing
108063
+ // it in the picker would just confuse users (it's the prior-flagship,
108064
+ // and they already have `gpt` and `gpt-mini` to choose from).
108065
+ "gpt-5.4": {
108066
+ displayName: "GPT 5.4",
108067
+ resolve: "openai/gpt-5.4",
108068
+ openRouterResolve: "openrouter/openai/gpt-5.4",
108069
+ hidden: true
108058
108070
  },
108059
108071
  "gpt-mini": {
108060
108072
  displayName: "GPT Mini",
@@ -108092,7 +108104,8 @@ var providers = {
108092
108104
  displayName: "Gemini Pro",
108093
108105
  resolve: "google/gemini-3.1-pro-preview",
108094
108106
  openRouterResolve: "openrouter/google/gemini-3.1-pro-preview",
108095
- preferred: true
108107
+ preferred: true,
108108
+ subagentModel: "gemini-flash"
108096
108109
  },
108097
108110
  "gemini-flash": {
108098
108111
  displayName: "Gemini Flash",
@@ -108180,7 +108193,8 @@ var providers = {
108180
108193
  "claude-opus": {
108181
108194
  displayName: "Claude Opus",
108182
108195
  resolve: "opencode/claude-opus-4-7",
108183
- openRouterResolve: "openrouter/anthropic/claude-opus-4.7"
108196
+ openRouterResolve: "openrouter/anthropic/claude-opus-4.7",
108197
+ subagentModel: "claude-sonnet"
108184
108198
  },
108185
108199
  "claude-sonnet": {
108186
108200
  displayName: "Claude Sonnet",
@@ -108195,12 +108209,21 @@ var providers = {
108195
108209
  gpt: {
108196
108210
  displayName: "GPT",
108197
108211
  resolve: "opencode/gpt-5.5",
108198
- openRouterResolve: "openrouter/openai/gpt-5.5"
108212
+ openRouterResolve: "openrouter/openai/gpt-5.5",
108213
+ subagentModel: "gpt-5.4"
108199
108214
  },
108200
108215
  "gpt-pro": {
108201
108216
  displayName: "GPT Pro",
108202
108217
  resolve: "opencode/gpt-5.5-pro",
108203
- openRouterResolve: "openrouter/openai/gpt-5.5-pro"
108218
+ openRouterResolve: "openrouter/openai/gpt-5.5-pro",
108219
+ subagentModel: "gpt"
108220
+ },
108221
+ // hidden subagent target — see openai provider above for context.
108222
+ "gpt-5.4": {
108223
+ displayName: "GPT 5.4",
108224
+ resolve: "opencode/gpt-5.4",
108225
+ openRouterResolve: "openrouter/openai/gpt-5.4",
108226
+ hidden: true
108204
108227
  },
108205
108228
  "gpt-mini": {
108206
108229
  displayName: "GPT Mini",
@@ -108223,7 +108246,8 @@ var providers = {
108223
108246
  "gemini-pro": {
108224
108247
  displayName: "Gemini Pro",
108225
108248
  resolve: "opencode/gemini-3.1-pro",
108226
- openRouterResolve: "openrouter/google/gemini-3.1-pro-preview"
108249
+ openRouterResolve: "openrouter/google/gemini-3.1-pro-preview",
108250
+ subagentModel: "gemini-flash"
108227
108251
  },
108228
108252
  "gemini-flash": {
108229
108253
  displayName: "Gemini Flash",
@@ -108255,6 +108279,20 @@ var providers = {
108255
108279
  }
108256
108280
  }
108257
108281
  }),
108282
+ bedrock: provider({
108283
+ displayName: "Amazon Bedrock",
108284
+ envVars: ["AWS_BEARER_TOKEN_BEDROCK", "AWS_REGION", "BEDROCK_MODEL_ID"],
108285
+ models: {
108286
+ // single routing entry — the actual Bedrock model ID is read from
108287
+ // BEDROCK_MODEL_ID at run time. see ModelRouting docs for why we
108288
+ // don't catalog individual Bedrock models.
108289
+ byok: {
108290
+ displayName: "Amazon Bedrock",
108291
+ resolve: "bedrock",
108292
+ routing: "bedrock"
108293
+ }
108294
+ }
108295
+ }),
108258
108296
  openrouter: provider({
108259
108297
  displayName: "OpenRouter",
108260
108298
  envVars: ["OPENROUTER_API_KEY"],
@@ -108263,7 +108301,8 @@ var providers = {
108263
108301
  displayName: "Claude Opus",
108264
108302
  resolve: "openrouter/anthropic/claude-opus-4.7",
108265
108303
  openRouterResolve: "openrouter/anthropic/claude-opus-4.7",
108266
- preferred: true
108304
+ preferred: true,
108305
+ subagentModel: "claude-sonnet"
108267
108306
  },
108268
108307
  "claude-sonnet": {
108269
108308
  displayName: "Claude Sonnet",
@@ -108278,12 +108317,21 @@ var providers = {
108278
108317
  gpt: {
108279
108318
  displayName: "GPT",
108280
108319
  resolve: "openrouter/openai/gpt-5.5",
108281
- openRouterResolve: "openrouter/openai/gpt-5.5"
108320
+ openRouterResolve: "openrouter/openai/gpt-5.5",
108321
+ subagentModel: "gpt-5.4"
108282
108322
  },
108283
108323
  "gpt-pro": {
108284
108324
  displayName: "GPT Pro",
108285
108325
  resolve: "openrouter/openai/gpt-5.5-pro",
108286
- openRouterResolve: "openrouter/openai/gpt-5.5-pro"
108326
+ openRouterResolve: "openrouter/openai/gpt-5.5-pro",
108327
+ subagentModel: "gpt"
108328
+ },
108329
+ // hidden subagent target — see openai provider above for context.
108330
+ "gpt-5.4": {
108331
+ displayName: "GPT 5.4",
108332
+ resolve: "openrouter/openai/gpt-5.4",
108333
+ openRouterResolve: "openrouter/openai/gpt-5.4",
108334
+ hidden: true
108287
108335
  },
108288
108336
  "gpt-mini": {
108289
108337
  displayName: "GPT Mini",
@@ -108311,7 +108359,8 @@ var providers = {
108311
108359
  "gemini-pro": {
108312
108360
  displayName: "Gemini Pro",
108313
108361
  resolve: "openrouter/google/gemini-3.1-pro-preview",
108314
- openRouterResolve: "openrouter/google/gemini-3.1-pro-preview"
108362
+ openRouterResolve: "openrouter/google/gemini-3.1-pro-preview",
108363
+ subagentModel: "gemini-flash"
108315
108364
  },
108316
108365
  "gemini-flash": {
108317
108366
  displayName: "Gemini Flash",
@@ -108380,7 +108429,13 @@ var modelAliases = Object.entries(providers).flatMap(
108380
108429
  openRouterResolve: def.openRouterResolve,
108381
108430
  preferred: def.preferred ?? false,
108382
108431
  isFree: def.isFree ?? false,
108383
- fallback: def.fallback
108432
+ fallback: def.fallback,
108433
+ routing: def.routing,
108434
+ // subagentModel is stored as an alias key local to the provider; expand
108435
+ // here to a fully-qualified slug so callers can look up the target alias
108436
+ // directly without re-deriving the provider.
108437
+ subagentModel: def.subagentModel ? `${providerKey}/${def.subagentModel}` : void 0,
108438
+ hidden: def.hidden ?? false
108384
108439
  }))
108385
108440
  );
108386
108441
  var MAX_FALLBACK_DEPTH = 10;
@@ -108400,6 +108455,10 @@ function resolveDisplayAlias(slug2) {
108400
108455
  function resolveCliModel(slug2) {
108401
108456
  return resolveDisplayAlias(slug2)?.resolve;
108402
108457
  }
108458
+ var BEDROCK_MODEL_ID_ENV = "BEDROCK_MODEL_ID";
108459
+ function isBedrockAnthropicId(bedrockModelId) {
108460
+ return bedrockModelId.toLowerCase().split(/[./:]/).includes("anthropic");
108461
+ }
108403
108462
 
108404
108463
  // utils/buildPullfrogFooter.ts
108405
108464
  var PULLFROG_DIVIDER = "<!-- PULLFROG_DIVIDER_DO_NOT_REMOVE_PLZ -->";
@@ -109247,7 +109306,7 @@ var Comment = type({
109247
109306
  function CreateCommentTool(ctx) {
109248
109307
  return tool({
109249
109308
  name: "create_issue_comment",
109250
- 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.",
109309
+ description: "Create a comment on a GitHub issue or PR. Example: `create_issue_comment({ issueNumber: 1234, body: \"Thanks for the report.\" })`. For progress/plan updates on the current run use report_progress instead. Use type: 'Plan' for plan comments.",
109251
109310
  parameters: Comment,
109252
109311
  execute: execute(async ({ issueNumber, body, type: commentType }) => {
109253
109312
  const bodyWithFooter = addFooter(ctx, body);
@@ -109415,7 +109474,7 @@ async function reportProgress(ctx, params) {
109415
109474
  function ReportProgressTool(ctx) {
109416
109475
  return tool({
109417
109476
  name: "report_progress",
109418
- description: "Share progress on the associated GitHub issue/PR. The first call creates a comment; subsequent calls update it in place. Call this at the end of every run with a brief final summary (1-3 sentences) unless the mode guidance instructs otherwise. The current task list is automatically appended in a collapsible section \u2014 do not restate individual steps.",
109477
+ description: 'Share progress on the associated GitHub issue/PR. The first call creates a comment; subsequent calls update it in place. Example: `report_progress({ body: "Implemented the auth check and added tests." })`. Call this at the end of every run with a brief final summary (1-3 sentences) unless the mode guidance instructs otherwise. The current task list is automatically appended in a collapsible section \u2014 do not restate individual steps.',
109419
109478
  parameters: ReportProgress,
109420
109479
  execute: execute(async (params) => {
109421
109480
  let body = params.body;
@@ -109495,7 +109554,7 @@ function duplicateReplyDecision(params) {
109495
109554
  function ReplyToReviewCommentTool(ctx) {
109496
109555
  return tool({
109497
109556
  name: "reply_to_review_comment",
109498
- description: "Reply to a PR review comment thread (NOT issue comments \u2014 this only works for inline review comments on PR diffs). Call exactly ONCE per parent comment you address in AddressReviews mode \u2014 duplicate calls with the same body are a no-op. Keep replies extremely brief (1 sentence max).",
109557
+ description: 'Reply to a PR review comment thread (NOT issue comments \u2014 this only works for inline review comments on PR diffs). Example: `reply_to_review_comment({ pull_number: 1234, comment_id: 567890, body: "Fixed by adding a null check." })`. Call exactly ONCE per parent comment you address in AddressReviews mode \u2014 duplicate calls with the same body are a no-op. Keep replies extremely brief (1 sentence max).',
109499
109558
  parameters: ReplyToReviewComment,
109500
109559
  execute: execute(async ({ pull_number, comment_id, body }) => {
109501
109560
  const bodyWithFooter = addFooter(ctx, body);
@@ -142638,7 +142697,7 @@ var import_semver = __toESM(require_semver2(), 1);
142638
142697
  // package.json
142639
142698
  var package_default = {
142640
142699
  name: "pullfrog",
142641
- version: "0.1.6",
142700
+ version: "0.1.7",
142642
142701
  type: "module",
142643
142702
  bin: {
142644
142703
  pullfrog: "dist/cli.mjs",
@@ -143494,7 +143553,7 @@ function PushBranchTool(ctx) {
143494
143553
  const pushPermission = ctx.payload.push;
143495
143554
  return tool({
143496
143555
  name: "push_branch",
143497
- description: "Push the current branch to the remote repository. Omit branchName to push the current branch (recommended). If specifying branchName, use the LOCAL branch name (e.g., 'pr-1'), not the remote branch name. The correct remote and remote branch are determined automatically from branch config set by checkout_pr. Requires a clean working tree. Runs the repository prepush hook (if configured) before the network push \u2014 hook failure means tests/lint or similar in that script failed, not necessarily a Pullfrog timeout. Never force push unless explicitly requested. Pushes to the default branch are blocked in restricted mode.",
143556
+ description: "Push the current branch to the remote repository. Omit branchName to push the current branch (recommended). Example: `push_branch({})` to push the current branch. Example: `push_branch({ branchName: \"pr-1\" })` to push a specific local branch. If specifying branchName, use the LOCAL branch name (e.g., 'pr-1'), not the remote branch name. The correct remote and remote branch are determined automatically from branch config set by checkout_pr. Requires a clean working tree. Runs the repository prepush hook (if configured) before the network push \u2014 hook failure means tests/lint or similar in that script failed, not necessarily a Pullfrog timeout. Never force push unless explicitly requested. Pushes to the default branch are blocked in restricted mode. If the response reports a timeout, the underlying push may have actually succeeded \u2014 verify with `git log origin/<branch>` (or this tool with command 'log') before retrying, otherwise you'll push a duplicate.",
143498
143557
  parameters: PushBranch,
143499
143558
  execute: execute(async ({ branchName, force }) => {
143500
143559
  if (pushPermission === "disabled") {
@@ -143633,7 +143692,7 @@ var Git = type({
143633
143692
  function GitTool(ctx) {
143634
143693
  return tool({
143635
143694
  name: "git",
143636
- description: "Run git commands. For push/fetch, use the dedicated MCP tools (push_branch, git_fetch). git pull is not available \u2014 use git_fetch then this tool with command 'merge'.",
143695
+ description: 'Run a git subcommand. `command` is a single subcommand; flags and positional args go in `args`. Example: `git({ command: "log", args: ["--oneline", "-n", "20"] })`. Example: `git({ command: "diff", args: ["origin/main..HEAD"] })`. For push/fetch, use the dedicated MCP tools (push_branch, git_fetch). git pull is not available \u2014 use git_fetch then this tool with command \'merge\'.',
143637
143696
  parameters: Git,
143638
143697
  execute: execute(async (params) => {
143639
143698
  const command = params.command;
@@ -143683,7 +143742,7 @@ var DEEPEN_RETRY_DEPTH = 1e3;
143683
143742
  function GitFetchTool(ctx) {
143684
143743
  return tool({
143685
143744
  name: "git_fetch",
143686
- description: "Fetch refs from remote repository. Use this instead of git fetch directly.",
143745
+ description: 'Fetch refs from remote repository. Use this instead of git fetch directly. Example: `git_fetch({ ref: "main" })`. With depth: `git_fetch({ ref: "pull/1234/head", depth: 1 })`.',
143687
143746
  parameters: GitFetch,
143688
143747
  execute: execute(async (params) => {
143689
143748
  rejectIfLeadingDash(params.ref, "ref");
@@ -143917,13 +143976,15 @@ var CreatePullRequestReview = type({
143917
143976
  approved: type.boolean.describe(
143918
143977
  "Set to true to submit as an approval. Use for both 'no issues found' and informational `> [!NOTE]` reviews where the PR is mergeable as-is and nothing in the body warrants code changes \u2014 approving also suppresses the Fix-button footer affordance so users don't dispatch a fix run on non-actionable feedback. Reserve approved: false for `> [!IMPORTANT]` (recommended changes) and `> [!CAUTION]` (critical) reviews. Defaults to false (comment-only review). Rejections are not supported."
143919
143978
  ).optional(),
143920
- commit_id: type.string.describe("Optional SHA of the commit being reviewed. Defaults to latest.").optional(),
143979
+ commit_id: type.string.describe(
143980
+ "Optional SHA of the commit being reviewed. Defaults to latest. Must be the FULL 40-character SHA \u2014 abbreviated SHAs are rejected by GitHub with `422 Unprocessable Entity`. The PR-synchronize event payload's `head_sha` is already full-length."
143981
+ ).optional(),
143921
143982
  comments: type({
143922
143983
  path: type.string.describe(
143923
143984
  "The file path to comment on (relative to repo root). Must be a file that appears in the PR diff."
143924
143985
  ),
143925
143986
  line: type.number.describe(
143926
- "Line number to comment on. For multi-line ranges, this is the end line. Use NEW column from diff format."
143987
+ "Line number to comment on. For multi-line ranges, this is the end line. Use NEW column from diff format. Must sit inside a `@@` hunk in the PR diff \u2014 anchors on context-only or untouched lines are dropped silently (the rest of the review still posts; dropped entries are reported under `droppedComments` in the response)."
143927
143988
  ),
143928
143989
  side: type.enumerated("LEFT", "RIGHT").describe(
143929
143990
  "Side of the diff: LEFT (old code, lines starting with -) or RIGHT (new code, lines starting with + or unchanged). Defaults to RIGHT."
@@ -143933,7 +143994,7 @@ var CreatePullRequestReview = type({
143933
143994
  "Full replacement code for the line range [start_line, line]. MUST preserve the exact indentation of the original code."
143934
143995
  ).optional(),
143935
143996
  start_line: type.number.describe(
143936
- "Start line for multi-line comment ranges. Omit for single-line comments. The range [start_line, line] defines which lines a suggestion replaces."
143997
+ "Start line for multi-line comment ranges. Omit for single-line comments. The range [start_line, line] defines which lines a suggestion replaces. Both `start_line` and `line` must sit inside the same `@@` hunk \u2014 a `start_line` outside the hunk causes the whole comment to be dropped even when `line` is valid. If you need to comment on context just above/below a hunk, shrink the range to a single line that is provably modified."
143937
143998
  ).optional()
143938
143999
  }).array().describe(
143939
144000
  "Inline comments on lines within diff hunks. Feedback about code outside the diff goes in 'body' instead."
@@ -143942,7 +144003,7 @@ var CreatePullRequestReview = type({
143942
144003
  function CreatePullRequestReviewTool(ctx) {
143943
144004
  return tool({
143944
144005
  name: "create_pull_request_review",
143945
- description: `Submit a review for an existing pull request. Each call creates a permanent, visible review on the PR \u2014 NEVER submit test or diagnostic reviews. Reviews with no body AND no comments are silently skipped (nothing to post). IMPORTANT: 95%+ of feedback should be in 'comments' array with file paths and line numbers. Only use 'body' for a 1-2 sentence summary with urgency and critical callouts. Use 'suggestion' to propose replacement code - MUST preserve exact indentation of original code. The first submission may error once with a one-time diff-coverage nudge listing unread TOC regions \u2014 retry with the same arguments and the pre-flight will not block again. Example replacing lines 42-44 (3 lines) with 5 lines: { path: 'src/api.ts', start_line: 42, line: 44, suggestion: ' const result = await fetch(url);\\n if (!result.ok) {\\n log.error(result.status);\\n throw new Error("request failed");\\n }' } CONSTRAINT: Inline comments can ONLY target files and lines that appear in the PR diff. Comments anchored outside a diff hunk are dropped automatically (with a note appended to the review body) \u2014 the rest of the review still posts.`,
144006
+ description: `Submit a review for an existing pull request. Example: \`create_pull_request_review({ pull_number: 1234, body: "LGTM", approved: true, comments: [{ path: "src/api.ts", line: 42, body: "nit: rename" }] })\`. Each call creates a permanent, visible review on the PR \u2014 NEVER submit test or diagnostic reviews. Reviews with no body AND no comments are silently skipped (nothing to post). IMPORTANT: 95%+ of feedback should be in 'comments' array with file paths and line numbers. Only use 'body' for a 1-2 sentence summary with urgency and critical callouts. Use 'suggestion' to propose replacement code - MUST preserve exact indentation of original code. The first submission may error once with a one-time diff-coverage nudge listing unread TOC regions \u2014 retry with the same arguments and the pre-flight will not block again. Example replacing lines 42-44 (3 lines) with 5 lines: { path: 'src/api.ts', start_line: 42, line: 44, suggestion: ' const result = await fetch(url);\\n if (!result.ok) {\\n log.error(result.status);\\n throw new Error("request failed");\\n }' } CONSTRAINT: Inline comments can ONLY target files and lines that appear in the PR diff. Comments anchored outside a diff hunk are dropped automatically (with a note appended to the review body) \u2014 the rest of the review still posts.`,
143946
144007
  parameters: CreatePullRequestReview,
143947
144008
  execute: execute(async ({ pull_number, body, approved, commit_id, comments = [] }) => {
143948
144009
  if (body) body = fixDoubleEscapedString(body);
@@ -144171,7 +144232,7 @@ function runDiffCoveragePreflight(params) {
144171
144232
  );
144172
144233
  const unreadText = unread.map((entry) => `- ${entry.path} (${entry.unreadLines} lines, ${entry.ranges})`).join("\n");
144173
144234
  throw new Error(
144174
- `diff coverage pre-flight: some TOC regions were not read before review submission. this is a one-time nudge \u2014 optionally read the ranges below from ${coverageState.diffPath}, then call create_pull_request_review again with the same arguments. this pre-flight will not block again in this review session.
144235
+ `diff coverage pre-flight: some TOC regions were not read before review submission. this is a one-time nudge \u2014 read the ranges below from ${coverageState.diffPath} on a best-effort basis, then call create_pull_request_review again. you are NOT obligated to read generated artifacts (lockfiles like pnpm-lock.yaml / package-lock.json / yarn.lock / Cargo.lock; codegen output like *.gen.*, *.pb.go, *.generated.*; snapshot/fixture dirs like __snapshots__/; migration metadata like drizzle/meta/, prisma migration SQL). if every unread region is generated, retry immediately without reading. this pre-flight will not block again in this review session.
144175
144236
 
144176
144237
  unread TOC regions:
144177
144238
  ${unreadText}
@@ -144618,7 +144679,7 @@ async function checkoutPrBranch(pr, params) {
144618
144679
  function CheckoutPrTool(ctx) {
144619
144680
  return tool({
144620
144681
  name: "checkout_pr",
144621
- description: "Checkout a pull request branch locally. This fetches the PR branch and sets up push configuration for fork PRs. Returns diffPath pointing to the formatted diff file.",
144682
+ description: "Checkout a pull request branch locally. This fetches the PR branch and sets up push configuration for fork PRs. Returns diffPath pointing to the formatted diff file. Example: `checkout_pr({ pull_number: 1234 })`. Transient fetch timeouts are common \u2014 retry the same call up to a few times before treating the failure as terminal. If the error mentions `.git/shallow.lock: File exists` or `.git/index.lock: File exists`, that's a stale lock from a prior timed-out fetch \u2014 remove it via the shell tool (`rm -f .git/shallow.lock .git/index.lock`) and retry.",
144622
144683
  parameters: CheckoutPr,
144623
144684
  execute: execute(async ({ pull_number }) => {
144624
144685
  const prResponse = await ctx.octokit.rest.pulls.get({
@@ -144929,7 +144990,7 @@ var CommitInfo = type({
144929
144990
  function CommitInfoTool(ctx) {
144930
144991
  return tool({
144931
144992
  name: "get_commit_info",
144932
- description: "Retrieve commit metadata and diff via GitHub API. Use this instead of git show for reviewing commits - it works with shallow clones and shows the actual changes in the commit. Returns diffPath pointing to formatted diff file.",
144993
+ description: 'Retrieve commit metadata and diff via GitHub API. Use this instead of git show for reviewing commits - it works with shallow clones and shows the actual changes in the commit. Returns diffPath pointing to formatted diff file. Example: `get_commit_info({ sha: "2a6ab5d" })`.',
144933
144994
  parameters: CommitInfo,
144934
144995
  execute: execute(async ({ sha }) => {
144935
144996
  const response = await ctx.octokit.rest.repos.getCommit({
@@ -145020,7 +145081,7 @@ var GetIssueComments = type({
145020
145081
  function GetIssueCommentsTool(ctx) {
145021
145082
  return tool({
145022
145083
  name: "get_issue_comments",
145023
- description: "Get all comments for a GitHub issue. Returns all comments including the issue body and all subsequent discussion comments.",
145084
+ description: "Get all comments for a GitHub issue. Returns all comments including the issue body and all subsequent discussion comments. Example: `get_issue_comments({ issue_number: 1234 })`.",
145024
145085
  parameters: GetIssueComments,
145025
145086
  execute: execute(async ({ issue_number }) => {
145026
145087
  ctx.toolState.issueNumber = issue_number;
@@ -145121,7 +145182,7 @@ var IssueInfo = type({
145121
145182
  function IssueInfoTool(ctx) {
145122
145183
  return tool({
145123
145184
  name: "get_issue",
145124
- description: "Retrieve GitHub issue information by issue number",
145185
+ description: "Retrieve GitHub issue information by issue number. Example: `get_issue({ issue_number: 1234 })`.",
145125
145186
  parameters: IssueInfo,
145126
145187
  execute: execute(async ({ issue_number }) => {
145127
145188
  const issue3 = await ctx.octokit.rest.issues.get({
@@ -145363,7 +145424,7 @@ var PullRequestInfo = type({
145363
145424
  function PullRequestInfoTool(ctx) {
145364
145425
  return tool({
145365
145426
  name: "get_pull_request",
145366
- description: "Retrieve PR metadata (title, body, state, branches, author, labels, linked issues). To checkout a PR branch locally, use checkout_pr instead.",
145427
+ description: "Retrieve PR metadata (title, body, state, branches, author, labels, linked issues). Example: `get_pull_request({ pull_number: 1234 })`. To checkout a PR branch locally, use checkout_pr instead.",
145367
145428
  parameters: PullRequestInfo,
145368
145429
  execute: execute(async ({ pull_number }) => {
145369
145430
  const [restResponse, graphqlResponse] = await Promise.all([
@@ -145767,7 +145828,7 @@ async function getReviewData(input) {
145767
145828
  function GetReviewCommentsTool(ctx) {
145768
145829
  return tool({
145769
145830
  name: "get_review_comments",
145770
- description: "Get review comments for a pull request review with full thread context. Automatically filters to approved comments when applicable. Returns a TOC and commentsPath pointing to a markdown file with full comment details.",
145831
+ description: "Get review comments for a pull request review with full thread context. Example: `get_review_comments({ pull_number: 1234, review_id: 567890 })`. Automatically filters to approved comments when applicable. Returns a TOC and commentsPath pointing to a markdown file with full comment details.",
145771
145832
  parameters: GetReviewComments,
145772
145833
  execute: execute(async (params) => {
145773
145834
  const approvedBy = ctx.payload.event.trigger === "fix_review" && ctx.payload.event.approved_only ? ctx.payload.triggerer : void 0;
@@ -145817,7 +145878,7 @@ var ListPullRequestReviews = type({
145817
145878
  function ListPullRequestReviewsTool(ctx) {
145818
145879
  return tool({
145819
145880
  name: "list_pull_request_reviews",
145820
- description: "List all reviews for a pull request. Returns all reviews including approvals, request changes, and comments.",
145881
+ description: "List all reviews for a pull request. Returns all reviews including approvals, request changes, and comments. Example: `list_pull_request_reviews({ pull_number: 1234 })`.",
145821
145882
  parameters: ListPullRequestReviews,
145822
145883
  execute: execute(async (params) => {
145823
145884
  const reviews = await ctx.octokit.paginate(ctx.octokit.rest.pulls.listReviews, {
@@ -145967,7 +146028,7 @@ function SelectModeTool(ctx) {
145967
146028
  const overrides = buildModeOverrides(t2);
145968
146029
  return tool({
145969
146030
  name: "select_mode",
145970
- description: "Select a mode and receive step-by-step guidance on how to handle the task. Call this to understand the best workflow for the current mode.",
146031
+ description: 'Select a mode and receive step-by-step guidance on how to handle the task. Call this to understand the best workflow for the current mode. Example: `select_mode({ mode: "Review" })` or `select_mode({ mode: "Plan", issue_number: 1234 })`.',
145971
146032
  parameters: SelectModeParams,
145972
146033
  execute: execute(async (params) => {
145973
146034
  if (ctx.toolState.selectedMode) {
@@ -146028,7 +146089,9 @@ import { setTimeout as sleep2 } from "node:timers/promises";
146028
146089
  var ShellParams = type({
146029
146090
  command: "string",
146030
146091
  description: "string",
146031
- "timeout?": "number",
146092
+ "timeout?": type.number.describe(
146093
+ "Timeout in MILLISECONDS (not seconds). Default 30000 (30s), max 120000 (2m). e.g. timeout: 180000 for 3 minutes; timeout: 180 means 180ms and will kill the process almost immediately."
146094
+ ),
146032
146095
  "working_directory?": "string",
146033
146096
  "background?": "boolean"
146034
146097
  });
@@ -146147,6 +146210,8 @@ function ShellTool(ctx) {
146147
146210
  name: "shell",
146148
146211
  description: `Execute shell commands securely. Environment is filtered to remove API keys and secrets.
146149
146212
 
146213
+ Example: \`shell({ command: "pnpm test", description: "run the test suite" })\`.
146214
+
146150
146215
  Use this tool to:
146151
146216
  - Run shell commands (ls, cat, grep, find, etc.)
146152
146217
  - Execute build tools (npm, pnpm, cargo, make, etc.)
@@ -146664,18 +146729,24 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
146664
146729
  - resolve addressed threads via \`${t2("resolve_review_thread")}\`
146665
146730
  - call \`${t2("report_progress")}\` with a brief summary (or the exact push error if push failed)`
146666
146731
  },
146667
- // Review and IncrementalReview use the multi-lens orchestrator pattern
146668
- // (canonical source: .claude/commands/anneal.md). The orchestrator does
146669
- // triage parallel read-only subagent fan-out aggregate → draft comments
146670
- // submit. For someone else's PR, parallel lenses (correctness, security,
146671
- // research-validated claims, user-journey, etc.) provide breadth across
146672
- // angles that a single subagent can't carry coherently. Build mode keeps
146673
- // a single fresh-eyes subagent (different problem shape — orchestrator
146674
- // wrote the code and bias-mitigation comes from delegating to one
146675
- // subagent that doesn't share the implementation context).
146676
- // Deliberate omission vs canonical /anneal: severity categorization in the
146677
- // final message (the review body has its own CAUTION/IMPORTANT framing
146678
- // instead of a severity table).
146732
+ // Review and IncrementalReview use a 0-or-2+ lens pattern. The default is
146733
+ // 0 lenses (orchestrator handles the review solo). Multi-lens (2+
146734
+ // reviewfrog subagents in parallel) only fires for substantive PRs or
146735
+ // high-stakes-subsystem touches and when it fires, ALL lenses must
146736
+ // dispatch in a single assistant turn or the parallelism win disappears.
146737
+ // We never dispatch exactly one lens: a single lens is just a worse,
146738
+ // slower version of doing the work yourself.
146739
+ //
146740
+ // Build mode self-review is a different problem shape: the orchestrator
146741
+ // wrote the code, so bias-mitigation comes from delegating to one
146742
+ // fresh-eyes subagent that doesn't share the implementation context. A
146743
+ // single subagent there is appropriate; the 0-or-2+ rule applies only to
146744
+ // the Review/IncrementalReview lens fan-out where independence between
146745
+ // perspectives is what's being purchased.
146746
+ //
146747
+ // Deliberate omission vs canonical /anneal: severity categorization in
146748
+ // the final message (the review body has its own CAUTION/IMPORTANT
146749
+ // framing instead of a severity table).
146679
146750
  {
146680
146751
  name: "Review",
146681
146752
  description: "Review code, PRs, or implementations; provide feedback or suggestions; identify issues; or check code quality, style, and correctness",
@@ -146685,9 +146756,9 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
146685
146756
 
146686
146757
  2. **checkout**: call \`${t2("checkout_pr")}\` \u2014 this returns PR metadata and a \`diffPath\`. read the diff TOC end-to-end and treat its file line ranges as your coverage checklist.
146687
146758
 
146688
- 3. **triage**: orient yourself on the PR \u2014 identify *what kind of thing this is* (domain it touches, seams it crosses, external contracts it depends on, user-facing surfaces it changes). orientation only \u2014 defer specific defect-hunting to the subagents; pre-reviewing biases the lenses you pick. use \`${t2("get_pull_request")}\` and other read-only GitHub tools for additional context if needed.
146759
+ 3. **triage**: orient yourself on the PR \u2014 identify *what kind of thing this is* (domain it touches, seams it crosses, external contracts it depends on, user-facing surfaces it changes). pull as much context as you need to render a confident, well-grounded review: read related files, grep for callers of changed symbols, check tests that exercise the touched paths, fetch related GitHub state. **you are the synthesizer** \u2014 never delegate understanding to subagents.
146689
146760
 
146690
- if the PR is **genuinely trivial**, skip steps 4\u20135 entirely and submit a \`No new issues found.\` review per step 6. there's no value in dispatching even one lens for a typo.
146761
+ if the PR is **genuinely trivial**, skip the fan-out entirely and submit a \`No new issues found.\` review per step 7.
146691
146762
 
146692
146763
  "Genuinely trivial" (skip):
146693
146764
  - single-word doc typo, whitespace/format-only, comment-only across any number of files
@@ -146706,25 +146777,25 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
146706
146777
  - any "typo fix" in user-facing copy that changes meaning ("approved" \u2192 "denied")
146707
146778
  - mixed diffs where a semantic 1-liner is buried in whitespace/formatting changes
146708
146779
 
146709
- 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.
146780
+ 4. **lens decision \u2014 0 or 2+, NEVER 1**.
146710
146781
 
146711
- 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:
146782
+ The default is **0 lenses**: handle the review yourself end-to-end. Most PRs land here.
146712
146783
 
146713
- - **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)
146714
- - **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)
146715
- - **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)
146716
- - **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.
146784
+ Dispatch **2+ \`${REVIEWER_AGENT_NAME}\` lenses in parallel** ONLY when ALL of the following are true:
146785
+ - the PR is substantive (>5 files changed AND >200 net lines), OR touches a high-stakes subsystem (auth, billing, payments, schema migration, webhooks, secrets, RBAC, multi-tenant isolation, cron/scheduling)
146786
+ - you can name 2+ distinct concrete failure modes that warrant independent lenses (one lens per failure mode; orthogonal, not overlapping)
146787
+ - parallel-orchestrated independent perspectives meaningfully outperform what you'd find solo
146717
146788
 
146718
- **lens-add discipline.** Each lens needs to clear a specific bar before you dispatch it: name the concrete failure mode this lens would catch *that the diff plausibly introduces*, in one sentence. "Could apply", "good to have", "for completeness" do not qualify. If you can't name what the lens is going to find, drop it. The "when unsure, treat as non-trivial" rule above is for the trivial-vs-non-trivial gate at step 3 \u2014 it does not license expanding lens count without articulated risk. Every extra lens adds wall-time, log noise, and pulls subagent attention onto speculative angles, which biases the final review toward bloat-shaped findings.
146789
+ **NEVER dispatch exactly one lens.** A single lens is just a more expensive version of doing the work yourself with a worse model \u2014 it adds wall time and a context-handoff for no orthogonality benefit. Either you have at least two genuinely independent failure-mode hypotheses (dispatch all in one turn), or you don't (do the review yourself).
146719
146790
 
146720
- lenses come in two flavors, and you can mix them:
146791
+ When you do go multi-lens, lens framings come in two flavors:
146721
146792
  - **themed lenses** \u2014 a perspective applied across the whole diff (correctness, security, user-journey, performance, etc.).
146722
- - **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.
146793
+ - **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"). **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.
146723
146794
 
146724
146795
  starter menu (combine, omit, or invent your own):
146725
146796
  - **correctness & invariants** \u2014 bugs, races, error handling, edge cases, state-machine boundaries
146726
- - **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
146727
- - **research-validated assumptions** \u2014 third-party API contracts, SDK semantics, framework directives, version-gated behavior. **only pick when the PR's correctness depends on the contract behaving a specific way** \u2014 not when the API is merely used. An idempotency key as a backstop, a timeout as a hint, a retry as belt-and-suspenders: not load-bearing, skip this lens. The bar is "if the third-party contract differs from what the diff assumes, the PR is incorrect." When dispatched, the subagent must verify load-bearing claims via web search and quote source URLs.
146797
+ - **impact** \u2014 stale references in code/tests/docs/configs/UI after rename/remove
146798
+ - **research-validated assumptions** \u2014 third-party API contracts, SDK semantics, framework directives, version-gated behavior. **only pick when the PR's correctness depends on the contract behaving a specific way** \u2014 not when the API is merely used. The bar is "if the third-party contract differs from what the diff assumes, the PR is incorrect." When dispatched, the subagent must verify load-bearing claims via web search and quote source URLs.
146728
146799
  - **security** \u2014 new endpoints, authZ, input validation, secrets handling, replay/CSRF/injection, cross-tenant isolation
146729
146800
  - **user-journey** \u2014 UX-touching flows: walk through happy path and failure modes as a user
146730
146801
  - **operational readiness** \u2014 observability, alerting, migrations (forward + rollback), feature flags, on-call burden
@@ -146734,26 +146805,36 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
146734
146805
  - **holistic** \u2014 does the PR make sense as a whole? symmetric flows (delete for every create, rollback for every migration)?
146735
146806
  - **subsystem lenses** (invent as the PR demands) \u2014 auth, billing, payments, schema migration, webhooks, secrets, RBAC, multi-tenant isolation, cron/scheduling, etc.
146736
146807
 
146737
- 4. **fan out**: dispatch one \`${REVIEWER_AGENT_NAME}\` subagent per lens \u2014 its baked-in system prompt enforces the non-mutative + non-recursive contract (read-only file/search/web tools and read-only MCP queries; no writes, shell side effects, state-changing MCP calls, or nested subagent dispatch). when picking 2+ lenses, dispatch them in a **single assistant turn with multiple parallel subagent calls**; issuing one and awaiting reply before the next collapses the fan-out into a serial review. if a subagent errors out, times out, or returns nothing usable, retry once with the same lens; if it still fails, proceed with partial coverage and note the missing lens in the review body \u2014 do not skip step 4 entirely on a single subagent failure. each subagent gets:
146808
+ The only subagent type is \`${REVIEWER_AGENT_NAME}\` \u2014 used for lens judgment work ("is this safe / correct / well-tested?"), runs on a mid-tier model.
146809
+
146810
+ 5. **fan out (only if step 4 said 2+ lenses)**: dispatch every \`${REVIEWER_AGENT_NAME}\` subagent for this run **IN A SINGLE ASSISTANT TURN, AS MULTIPLE PARALLEL TASK TOOL_USE BLOCKS IN ONE MESSAGE.**
146811
+
146812
+ \u26A0\uFE0F CRITICAL \u2014 PARALLELISM IS THE ONLY REASON LENSES EXIST. \u26A0\uFE0F
146813
+ The default tool-call behavior of Claude Code (and most agent runtimes) is **serial dispatch**: emit one Task call, await result, emit next, await, etc. This collapses your fan-out into a sequential review where each lens adds N \xD7 (orchestrator-think-time + lens-execution-time) to wall time. **YOU MUST OVERRIDE THIS DEFAULT.** Emit ALL of your Task tool_use blocks in the SAME assistant message, BEFORE you read ANY result from ANY of them. If you find yourself emitting one Task call, then thinking about the result, then emitting another \u2014 STOP and re-issue them all together. The whole point of going multi-lens is the wall-clock speedup from parallel execution; serial dispatch defeats it entirely.
146814
+
146815
+ \u2705 Right pattern: one assistant turn with N Task tool_use blocks \u2192 wait \u2192 N results arrive together \u2192 aggregate.
146816
+ \u274C Wrong pattern: turn 1 = Task(lens A) \u2192 turn 2 (after A's result) = Task(lens B) \u2192 turn 3 (after B's result) = Task(lens C). This is the failure mode. Do not do this.
146817
+
146818
+ You can also include your own \`read\` / \`grep\` / \`webfetch\` calls in the SAME turn as the parallel \`${REVIEWER_AGENT_NAME}\` dispatches \u2014 concurrent context-pulling on the orchestrator side runs in parallel with the lens fan-out and costs zero extra wall time.
146819
+
146820
+ 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 the fan-out entirely on a single subagent failure. each subagent gets:
146738
146821
  - the diff path / target \u2014 reading the diff and the codebase is its job
146739
146822
  - **only one lens** \u2014 never a multi-section "review for X, Y, and Z" prompt
146740
146823
  - **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\`.
146741
- - 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.
146742
146824
  - 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."
146743
146825
  - 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.
146744
146826
 
146745
146827
  delegation discipline:
146746
- - 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)
146747
146828
  - do NOT summarize the PR for them (biases toward a validation frame)
146748
146829
  - do NOT hand them a curated reading list (let them discover scope)
146749
146830
  - do NOT pre-shape their output with a finding schema
146750
146831
  - do NOT mention the other lenses (independence is the point \u2014 overlapping findings are a strong signal)
146751
146832
 
146752
- 5. **aggregate & draft**: merge findings; de-dup overlaps (two lenses catching the same issue = higher-confidence signal); trace each finding yourself before accepting it. drop praise, style preferences, speculative/unverified claims, findings about pre-existing code unrelated to the PR (heuristic: if the finding's root cause lives in lines this PR added or modified, it's in scope; otherwise drop unless the PR plausibly introduced or amplified the regression), and anything not actionable. also drop **bloat-shaped findings** \u2014 proposed fixes that would add defensive checks for cases that can't happen, abstractions used once, comments restating obvious code, tests asserting tautologies, or "just-in-case" guards. subagents are fallible and bias toward recommending changes; the bar for an actionable inline comment is sound + correct + elegant. recommending a change that improves only one of the three (or worse, degrades elegance to nominally improve correctness) makes the codebase worse, not better.
146833
+ 6. **aggregate & draft**: when the fan-out lands, 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.
146753
146834
 
146754
146835
  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.
146755
146836
 
146756
- 6. **submit**: ALWAYS submit exactly one review via \`${t2("create_pull_request_review")}\`. Do NOT call \`report_progress\` \u2014 the review is the final record and the progress comment will be cleaned up automatically.
146837
+ 7. **submit**: ALWAYS submit exactly one review via \`${t2("create_pull_request_review")}\`. Do NOT call \`report_progress\` \u2014 the review is the final record and the progress comment will be cleaned up automatically.
146757
146838
 
146758
146839
  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.
146759
146840
 
@@ -146781,10 +146862,10 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
146781
146862
 
146782
146863
  ${PR_SUMMARY_FORMAT}`
146783
146864
  },
146784
- // IncrementalReview shares Review's multi-lens orchestrator pattern but
146785
- // scopes the target to the incremental diff. The "issues must be NEW
146786
- // since the last Pullfrog review" filter lives at aggregation time
146787
- // (step 6), NOT in the subagent prompt — pushing the filter into
146865
+ // IncrementalReview shares Review's 0-or-2+ lens pattern but scopes the
146866
+ // target to the incremental diff. The "issues must be NEW since the last
146867
+ // Pullfrog review" filter lives at aggregation time (step 8), NOT in the
146868
+ // subagent prompt — pushing the filter into
146788
146869
  // subagents matches the canonical anneal anti-pattern of "list known
146789
146870
  // pre-existing failures — don't flag these" and suppresses signal on
146790
146871
  // regressions the new commits amplified. The review body is just
@@ -146803,38 +146884,57 @@ ${PR_SUMMARY_FORMAT}`
146803
146884
 
146804
146885
  3. **incremental scope**: if \`incrementalDiffPath\` is present, read it to see what changed since the last review. this is a range-diff that isolates the net changes, filtering out base branch noise. if not present, fall back to reviewing the full PR diff and determine what changed since Pullfrog's most recent review.
146805
146886
 
146806
- 4. **prior feedback**: fetch previous reviews via \`${t2("list_pull_request_reviews")}\`. for the most recent Pullfrog review, call \`${t2("get_review_comments")}\` with the review ID to retrieve specific prior line-level feedback. you'll use this to filter your aggregation in step 6 \u2014 anything already flagged in a prior review and not changed by the new commits should not be re-raised. you do NOT need to render this in the review body; the rolling PR summary snapshot is the durable record of what's been addressed.
146887
+ 4. **prior feedback**: fetch previous reviews via \`${t2("list_pull_request_reviews")}\`. for the most recent Pullfrog review, call \`${t2("get_review_comments")}\` with the review ID to retrieve specific prior line-level feedback. you'll use this to filter your aggregation in step 8 \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.
146807
146888
 
146808
- 5. **triage & fan out**: orient on the *incremental* changes \u2014 domain, seams, external contracts, user-facing surfaces.
146889
+ 5. **triage**: orient on the *incremental* changes \u2014 domain, seams, external contracts, user-facing surfaces. pull as much context as you need to render a confident review: read related files, grep for callers of changed symbols, check tests that exercise the touched paths. **you are the synthesizer.**
146809
146890
 
146810
- if the incremental changes are **genuinely trivial**, skip the fan-out entirely and jump to step 8's non-substantive path (do NOT submit a review).
146891
+ if the incremental changes are **genuinely trivial**, skip the fan-out entirely and jump to step 10's non-substantive path (do NOT submit a review).
146811
146892
 
146812
146893
  "Genuinely trivial" (skip): formatting/comment tweaks, import reordering, lockfile regen, mechanical rename of import paths, whitespace-only.
146813
146894
  "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.
146814
146895
  When unsure, treat as non-trivial.
146815
146896
 
146816
- 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). same **lens-add discipline** as Review mode applies: each lens needs to name the concrete failure mode it would catch *that the new commits plausibly introduce* \u2014 "could apply" doesn't qualify, drop it. **research-validated assumptions** specifically: only pick when the new commits' correctness depends on a third-party contract behaving a specific way; merely using an API doesn't qualify. 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.
146897
+ 6. **lens decision \u2014 0 or 2+, NEVER 1**.
146898
+
146899
+ The default is **0 lenses**: handle the re-review yourself end-to-end. Most incremental reviews land here \u2014 especially thread-reply re-reviews where the user is asking "did you address X?" rather than "review the diff again."
146900
+
146901
+ Dispatch **2+ \`${REVIEWER_AGENT_NAME}\` lenses in parallel** ONLY when ALL of the following are true:
146902
+ - the incremental changes are substantive (>5 files changed AND >200 net new lines), OR touch a high-stakes subsystem (auth, billing, payments, schema migration, webhooks, secrets, RBAC, multi-tenant isolation, cron/scheduling)
146903
+ - you can name 2+ distinct concrete failure modes the new commits plausibly introduce that warrant independent lenses
146904
+ - parallel-orchestrated independent perspectives meaningfully outperform what you'd find solo
146905
+
146906
+ **NEVER dispatch exactly one lens.** Single-lens dispatch adds wall time and cost for no orthogonality benefit. Either go multi-lens (\u22652 in parallel) or do the re-review yourself.
146907
+
146908
+ Lens framing follows Review mode: themed lenses (correctness, security, etc.) and subsystem lenses (auth, billing, schema-migration, etc.) \u2014 for high-stakes domains lead with the subsystem lens.
146909
+
146910
+ 7. **fan out (only if step 6 said 2+ lenses)**: dispatch every \`${REVIEWER_AGENT_NAME}\` subagent for this run **IN A SINGLE ASSISTANT TURN, AS MULTIPLE PARALLEL TASK TOOL_USE BLOCKS IN ONE MESSAGE.**
146911
+
146912
+ \u26A0\uFE0F CRITICAL \u2014 PARALLELISM IS THE ONLY REASON LENSES EXIST. \u26A0\uFE0F
146913
+ Default tool-call behavior is **serial dispatch**: emit one Task call, await result, emit next, await, etc. This collapses your fan-out into a sequential review where each lens adds N \xD7 (orchestrator-think-time + lens-execution-time) to wall time. **YOU MUST OVERRIDE THIS DEFAULT.** Emit ALL of your Task tool_use blocks in the SAME assistant message, BEFORE you read ANY result from ANY of them.
146817
146914
 
146818
- dispatch one \`${REVIEWER_AGENT_NAME}\` subagent per lens \u2014 its baked-in system prompt enforces the non-mutative + non-recursive contract (read-only file/search/web tools and read-only MCP queries; no writes, shell side effects, state-changing MCP calls, or nested subagent dispatch). dispatch them in a **single assistant turn with multiple parallel subagent calls** (serial dispatch collapses the fan-out). if a subagent errors out, times out, or returns nothing usable, retry once with the same lens; if it still fails, proceed with partial coverage and note the missing lens in the review body \u2014 do not skip step 5 entirely on a single subagent failure. each subagent gets:
146819
- - the diff scope (incremental diff path if available, full diff otherwise). do NOT tell them to skip pre-existing issues \u2014 that suppresses regressions the new commits amplified; the "issues must be NEW" filter lives at aggregation time (step 6), not in the subagent prompt
146915
+ \u2705 Right pattern: one assistant turn with N Task tool_use blocks \u2192 wait \u2192 N results arrive together \u2192 aggregate.
146916
+ \u274C Wrong pattern: turn 1 = Task(lens A) \u2192 turn 2 (after A's result) = Task(lens B). This is the failure mode.
146917
+
146918
+ You can also include your own \`read\` / \`grep\` / \`webfetch\` calls in the SAME turn as the parallel \`${REVIEWER_AGENT_NAME}\` dispatches.
146919
+
146920
+ 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. each subagent gets:
146921
+ - 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 8), not in the subagent prompt
146820
146922
  - **only one lens** \u2014 never a multi-section "review for X, Y, and Z" prompt
146821
- - **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\`.
146822
- - 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.
146823
- - 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."
146923
+ - **a Task \`description\` set to the lens name** \u2014 the harness reads this field to label log lines so parallel runs can be told apart.
146924
+ - if the lens touches external contracts, instruct the subagent to verify load-bearing claims via web search and quote source URLs.
146824
146925
  - ask the subagent to report findings with file paths and NEW line numbers from the full PR diff so you can anchor inline comments.
146825
146926
 
146826
146927
  delegation discipline:
146827
- - do NOT lens-review the diff yourself in parallel with the subagents
146828
146928
  - do NOT summarize the changes for them (biases toward validation frame)
146829
146929
  - do NOT hand them a curated reading list (let them discover scope)
146830
146930
  - do NOT pre-shape their output with a finding schema
146831
146931
  - do NOT mention the other lenses (independence is the point)
146832
146932
 
146833
- 6. **aggregate, draft, self-critique**: merge findings; de-dup overlaps; trace each finding yourself. drop praise, style preferences, speculative/unverified claims, findings about pre-existing code unrelated to the new commits, anything not actionable, and anything that re-states prior review feedback (heuristic: if the finding's root cause lives in lines the *new commits* added or modified, it's in scope; otherwise drop). also drop **bloat-shaped findings** \u2014 proposed fixes that would add defensive checks for cases that can't happen, abstractions used once, comments restating obvious code, tests asserting tautologies, or "just-in-case" guards. subagents are fallible and bias toward recommending changes; the bar for an actionable inline comment is sound + correct + elegant. recommending a change that improves only one of the three (or degrades elegance to nominally improve correctness) makes the codebase worse, not better. To compute "lines the new commits added or modified": if \`incrementalDiffPath\` from step 2 is present, use it directly. Otherwise, take the prior Pullfrog review's \`commit_id\` (returned alongside each entry from \`${t2("list_pull_request_reviews")}\` in step 4) and run \`git diff <prior-review-sha>..HEAD\` to isolate the lines added since that review. draft inline comments with NEW line numbers from the full PR diff \u2014 every comment must be actionable, 2-3 sentences max.
146933
+ 8. **aggregate, draft, self-critique**: merge findings (yours + any subagent output if you went multi-lens); de-dup overlaps; trace each finding yourself. drop praise, style preferences, speculative/unverified claims, findings about pre-existing code unrelated to the new commits, anything not actionable, and anything that re-states prior review feedback (heuristic: if the finding's root cause lives in lines the *new commits* added or modified, it's in scope; otherwise drop). also drop **bloat-shaped findings** \u2014 proposed fixes that would add defensive checks for cases that can't happen, abstractions used once, comments restating obvious code, tests asserting tautologies, or "just-in-case" guards. subagents are fallible and bias toward recommending changes; the bar for an actionable inline comment is sound + correct + elegant. recommending a change that improves only one of the three (or degrades elegance to nominally improve correctness) makes the codebase worse, not better. To compute "lines the new commits added or modified": if \`incrementalDiffPath\` from step 2 is present, use it directly. Otherwise, take the prior Pullfrog review's \`commit_id\` (returned alongside each entry from \`${t2("list_pull_request_reviews")}\` in step 4) and run \`git diff <prior-review-sha>..HEAD\` to isolate the lines added since that review. draft inline comments with NEW line numbers from the full PR diff \u2014 every comment must be actionable, 2-3 sentences max.
146834
146934
 
146835
- 7. **build the review body** \u2014 a single "Reviewed changes" section: summarize at the logical-change level, not per-file. each bullet starts with a past-tense verb (e.g. \`- Extracted shared CLI runtime into a single module\`, \`- Renamed package to pullfrog\`). avoid file paths unless they add clarity. if the changes can be described in one sentence, use one sentence \u2014 no bullets needed. do NOT include a separate "Prior review feedback" checklist; that's tracked in the rolling PR summary snapshot for the next agent run, and surfacing it in the user-facing body is noise (changes that addressed prior feedback are already covered by the Reviewed-changes bullets). in some cases you may receive a complete diff for the whole pull request instead of an incremental one \u2014 when this happens, you will need to determine what changes have happened since Pullfrog's most recent review.
146935
+ 9. **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.
146836
146936
 
146837
- 8. Submit \u2014 every run must end with EXACTLY ONE of \`${t2("create_pull_request_review")}\` (substantive review) or \`${t2("report_progress")}\` (no-review acknowledgement). do NOT call \`create_issue_comment\` for review output.
146937
+ 10. Submit \u2014 every run must end with EXACTLY ONE of \`${t2("create_pull_request_review")}\` (substantive review) or \`${t2("report_progress")}\` (no-review acknowledgement). do NOT call \`create_issue_comment\` for review output.
146838
146938
 
146839
146939
  Same callout-intensity ladder as Review mode \u2014 \`[!CAUTION]\` (large red, "will break") \u2192 \`[!IMPORTANT]\` (large purple, "must address before merging") \u2192 \`[!NOTE]\` (small blue, "FYI") \u2192 no callout (plain text). And the same Fix-button lever: the footer renders a Fix button on every non-approving review, so \`approved: true\` suppresses it. Wrapping mergeable feedback in \`[!IMPORTANT]\` trains users to click Fix on reviews that don't need fixing \u2014 pick the tier the author's actual next action justifies.
146840
146940
 
@@ -147214,45 +147314,12 @@ var ThinkingTimer = class {
147214
147314
  import { readFile } from "node:fs/promises";
147215
147315
  function getUnsubmittedReview(toolState) {
147216
147316
  const mode = toolState.selectedMode;
147217
- if (mode !== "Review" && mode !== "IncrementalReview") return null;
147218
- if (toolState.review || toolState.finalSummaryWritten) return null;
147219
147317
  if (!toolState.hadProgressComment) return null;
147220
- return mode;
147221
- }
147222
- var MAX_HOOK_OUTPUT_CHARS = 4096;
147223
- function truncateHookOutput(raw2) {
147224
- if (raw2.length <= MAX_HOOK_OUTPUT_CHARS) return raw2;
147225
- return `...(truncated, showing last ${MAX_HOOK_OUTPUT_CHARS} chars)
147226
- ${raw2.slice(-MAX_HOOK_OUTPUT_CHARS)}`;
147227
- }
147228
- async function executeStopHook(script) {
147229
- log.info("\xBB executing stop hook...");
147230
- try {
147231
- const result = await spawn({
147232
- cmd: "bash",
147233
- args: ["-c", script],
147234
- env: process.env,
147235
- timeout: LIFECYCLE_HOOK_TIMEOUT_MS,
147236
- activityTimeout: 0,
147237
- onStdout: (chunk) => process.stdout.write(chunk),
147238
- onStderr: (chunk) => process.stderr.write(chunk)
147239
- });
147240
- if (result.exitCode === 0) {
147241
- log.info("\xBB stop hook passed");
147242
- return null;
147243
- }
147244
- const combined = [result.stderr.trim(), result.stdout.trim()].filter(Boolean).join("\n");
147245
- const output = truncateHookOutput(combined);
147246
- log.info(`\xBB stop hook failed with exit code ${result.exitCode}`);
147247
- return { exitCode: result.exitCode, output };
147248
- } catch (err) {
147249
- const isTimeout = err instanceof SpawnTimeoutError && (err.code === SPAWN_TIMEOUT_CODE || err.code === SPAWN_ACTIVITY_TIMEOUT_CODE);
147250
- const msg = err instanceof Error ? err.message : String(err);
147251
- log.warning(
147252
- `stop hook ${isTimeout ? "timed out" : "failed to spawn"}: ${msg} \u2014 skipping retry`
147253
- );
147254
- return null;
147318
+ if (mode === "Review") return toolState.review ? null : "Review";
147319
+ if (mode === "IncrementalReview") {
147320
+ return toolState.review || toolState.finalSummaryWritten ? null : "IncrementalReview";
147255
147321
  }
147322
+ return null;
147256
147323
  }
147257
147324
  function buildStopHookPrompt(failure) {
147258
147325
  return [
@@ -147302,10 +147369,6 @@ function buildUnsubmittedReviewPrompt(mode) {
147302
147369
  }
147303
147370
  async function collectPostRunIssues(ctx, options = {}) {
147304
147371
  const issues = {};
147305
- if (ctx.stopScript) {
147306
- const failure = await executeStopHook(ctx.stopScript);
147307
- if (failure) issues.stopHook = failure;
147308
- }
147309
147372
  const status = getGitStatus();
147310
147373
  const mode = ctx.toolState.selectedMode;
147311
147374
  if (status) {
@@ -147341,11 +147404,25 @@ function buildLearningsReflectionPrompt(filePath) {
147341
147404
  "",
147342
147405
  `the rolling learnings file is at \`${filePath}\`. read it first if you haven't already, then edit it in place using your native file tools. the server reads this file at end-of-run and persists any changes \u2014 there is no tool to call.`,
147343
147406
  "",
147344
- `keep the file healthy:`,
147345
- `- only add bullets when the finding is high-confidence AND broadly useful. skip speculative, one-off, or "maybe" findings.`,
147346
- `- prune bullets that are clearly wrong, no longer relevant, or low-signal (rarely useful). a focused, accurate file beats a long stale one.`,
147347
- `- format: flat bullet list, one fact per line starting with \`- \`. deduplicate against existing entries \u2014 if a bullet covers the same fact, update it in place instead of adding a duplicate.`,
147348
- `- leave the file alone if you have nothing substantively new to add and the existing entries still look healthy. silence is a valid outcome \u2014 just reply "done" and stop.`
147407
+ `structure:`,
147408
+ `- markdown hierarchy: \`## \` for top-level themes, \`### \` and deeper for sub-themes when a section grows. there is no fixed taxonomy \u2014 choose headings that fit THIS repo (e.g. for one repo \`## Migrations\` / \`## Local dev\` may make sense; for another, \`## API quirks\` / \`## Failure modes\`).`,
147409
+ `- **no section over ~300 lines.** when a section is approaching that, split it: introduce \`### \` subsections grouping related bullets, or hoist a coherent group into a new top-level \`## \` section. granular sections mean future runs read targeted line ranges instead of slurping the whole file. this is the most important hygiene rule on long-lived repos.`,
147410
+ `- if you find a flat unstructured list (legacy content from before this format), restructure it: read it, group related bullets, rewrite the file with \`## \` / \`### \` headings around them. don't preserve bad structure \u2014 fix it.`,
147411
+ "",
147412
+ `bullet hygiene:`,
147413
+ `- one fact per line starting with \`- \`. each bullet is ONE specific durable fact, not a paragraph or essay.`,
147414
+ `- aim for \u2264 240 chars per bullet. longer bullets are almost always mixing multiple facts that should be split, or burying the durable claim under PR-specific context that should be cut.`,
147415
+ `- only add bullets when the finding is high-confidence AND broadly useful AND will still be true in 3+ months. skip speculative, one-off, or "maybe" findings.`,
147416
+ `- prune bullets that are clearly wrong, no longer relevant, or low-signal. a focused, accurate file beats a long stale one. compressing two overlapping bullets into one tighter bullet counts as progress.`,
147417
+ `- deduplicate against existing entries (in any section) \u2014 if a bullet covers the same fact, update it in place instead of adding a duplicate.`,
147418
+ "",
147419
+ `do NOT add bullets for:`,
147420
+ `- pullfrog tool quirks (e.g. "\`shell\` timeout is in milliseconds", "\`git\` args must be a JSON array", "\`create_pull_request_review\` drops out-of-hunk comments", "\`push_branch\` may report timeout when push succeeded"). these are universal across repos and belong in the tool descriptions \u2014 flag the gap rather than hoarding the workaround per-repo.`,
147421
+ `- references to specific PR numbers, review IDs, commit SHAs, branch names, or person handles ("PR #595 introduced X", "flagged in review 12345", "as of commit abc123"). repo state changes; these decay into noise within weeks.`,
147422
+ `- dated assertions ("as of May 2026", "currently...", "for now..."). if a fact needs a date to be true, it isn't durable enough to belong here.`,
147423
+ `- play-by-play of what THIS run did. learnings are for the NEXT run, not a retrospective.`,
147424
+ "",
147425
+ `if you have nothing substantively new to add AND the existing entries still look healthy and well-structured, leave the file alone \u2014 just reply "done" and stop. silence is a valid outcome.`
147349
147426
  ].join("\n");
147350
147427
  }
147351
147428
  async function runPostRunRetryLoop(params) {
@@ -147549,8 +147626,9 @@ function writeMcpConfig(ctx) {
147549
147626
  function buildAgentsJson() {
147550
147627
  const agents2 = {
147551
147628
  [REVIEWER_AGENT_NAME]: {
147552
- description: "Read-only review subagent for self-review and lens-based code review. Reads only \u2014 no writes, no state-changing shell or MCP calls, no nested subagent dispatch.",
147553
- prompt: REVIEWER_SYSTEM_PROMPT
147629
+ description: "Read-only review subagent for lens-based code review (correctness, security, billing-subsystem, etc.). Reads only \u2014 no writes, no state-changing shell or MCP calls, no nested subagent dispatch.",
147630
+ prompt: REVIEWER_SYSTEM_PROMPT,
147631
+ model: "claude-sonnet-4-6"
147554
147632
  }
147555
147633
  };
147556
147634
  return JSON.stringify(agents2);
@@ -147746,6 +147824,7 @@ async function runClaude(params) {
147746
147824
  }
147747
147825
  };
147748
147826
  const recentStderr = [];
147827
+ const recentNonJsonStdout = [];
147749
147828
  let lastProviderError = null;
147750
147829
  const output = new TailBuffer(DEFAULT_MAX_RETAINED_BYTES);
147751
147830
  let stdoutBuffer = "";
@@ -147784,6 +147863,8 @@ async function runClaude(params) {
147784
147863
  event = JSON.parse(trimmed);
147785
147864
  } catch {
147786
147865
  log.debug(`\xBB non-JSON stdout line: ${trimmed.substring(0, 200)}`);
147866
+ recentNonJsonStdout.push(trimmed);
147867
+ if (recentNonJsonStdout.length > MAX_STDERR_LINES) recentNonJsonStdout.shift();
147787
147868
  continue;
147788
147869
  }
147789
147870
  eventCount++;
@@ -147849,7 +147930,8 @@ ${stderrContext}`);
147849
147930
  const stdoutSnapshot = output.toString();
147850
147931
  const stderrSnapshot = recentStderr.join("\n");
147851
147932
  const truncatedStdout = stdoutSnapshot ? tailLines(stdoutSnapshot, 2048) : "";
147852
- const errorMessage = lastResultError || stderrSnapshot || truncatedStdout || `unknown error - no output from Claude CLI${errorContext}`;
147933
+ const nonJsonStdoutSnapshot = recentNonJsonStdout.join("\n");
147934
+ const errorMessage = lastResultError || stderrSnapshot || nonJsonStdoutSnapshot || truncatedStdout || `unknown error - no output from Claude CLI${errorContext}`;
147853
147935
  log.error(
147854
147936
  `${params.label} exited with code ${result.exitCode}${errorContext}: ${errorMessage}`
147855
147937
  );
@@ -147950,7 +148032,9 @@ var claude = agent({
147950
148032
  run: async (ctx) => {
147951
148033
  const cliPath = await installClaudeCli();
147952
148034
  const specifier = ctx.payload.proxyModel ?? ctx.resolvedModel;
147953
- const model = specifier ? stripProviderPrefix(specifier) : void 0;
148035
+ const bedrockModelId = process.env[BEDROCK_MODEL_ID_ENV]?.trim();
148036
+ const isBedrockRoute = specifier !== void 0 && bedrockModelId !== void 0 && bedrockModelId === specifier && isBedrockAnthropicId(specifier);
148037
+ const model = !specifier ? void 0 : isBedrockRoute ? specifier : stripProviderPrefix(specifier);
147954
148038
  const homeEnv = {
147955
148039
  HOME: ctx.tmpdir,
147956
148040
  XDG_CONFIG_HOME: join10(ctx.tmpdir, ".config")
@@ -147989,6 +148073,9 @@ var claude = agent({
147989
148073
  ...process.env,
147990
148074
  ...homeEnv
147991
148075
  };
148076
+ if (isBedrockRoute) {
148077
+ env2.CLAUDE_CODE_USE_BEDROCK = "1";
148078
+ }
147992
148079
  const repoDir = process.cwd();
147993
148080
  log.info(`\xBB effort: ${effort}`);
147994
148081
  log.debug(`\xBB starting Pullfrog (Claude Code): node ${baseArgs.join(" ")}`);
@@ -148110,6 +148197,22 @@ export default async function pullfrogEventsPlugin() {
148110
148197
  }
148111
148198
  `;
148112
148199
 
148200
+ // agents/subagentModels.ts
148201
+ function deriveSubagentModels(orchestratorSpec) {
148202
+ if (!orchestratorSpec) return { reviewer: void 0 };
148203
+ for (const source of modelAliases) {
148204
+ const matchedDirect = source.resolve === orchestratorSpec;
148205
+ const matchedOR = source.openRouterResolve === orchestratorSpec;
148206
+ if (!matchedDirect && !matchedOR) continue;
148207
+ if (!source.subagentModel) return { reviewer: void 0 };
148208
+ const target = modelAliases.find((a) => a.slug === source.subagentModel);
148209
+ if (!target) return { reviewer: void 0 };
148210
+ const reviewer = matchedOR ? target.openRouterResolve : target.resolve;
148211
+ return { reviewer };
148212
+ }
148213
+ return { reviewer: void 0 };
148214
+ }
148215
+
148113
148216
  // agents/opencode.ts
148114
148217
  async function installOpencodeCli() {
148115
148218
  return await installFromNpmTarball({
@@ -148119,7 +148222,6 @@ async function installOpencodeCli() {
148119
148222
  installDependencies: true
148120
148223
  });
148121
148224
  }
148122
- var PULLFROG_OPENCODE_OUTPUT_LIMIT = 5e3;
148123
148225
  var GEMINI_3_DIRECT_THINKING_LEVEL = "medium";
148124
148226
  var GEMINI_3_DIRECT_API_IDS = ["gemini-3.1-pro-preview", "gemini-3-flash-preview"];
148125
148227
  function buildSecurityConfig(ctx, model) {
@@ -148135,7 +148237,12 @@ function buildSecurityConfig(ctx, model) {
148135
148237
  mcp: {
148136
148238
  [pullfrogMcpName]: { type: "remote", url: ctx.mcpServerUrl }
148137
148239
  },
148138
- agent: buildReviewerAgentConfig(),
148240
+ agent: (() => {
148241
+ const cfg = buildReviewerAgentConfig(model);
148242
+ const reviewerModel = cfg[REVIEWER_AGENT_NAME]?.model ?? "(inherit)";
148243
+ log.info(`\xBB subagent models: reviewfrog=${reviewerModel}`);
148244
+ return cfg;
148245
+ })(),
148139
148246
  // opt into opencode's experimental `batch` tool (added in
148140
148247
  // anomalyco/opencode PR #2983, opt-in via `experimental.batch_tool`). it
148141
148248
  // exposes a single `batch` tool that runs 1-25 independent tool calls
@@ -148169,12 +148276,14 @@ function buildSecurityConfig(ctx, model) {
148169
148276
  }
148170
148277
  return JSON.stringify(config3);
148171
148278
  }
148172
- function buildReviewerAgentConfig() {
148279
+ function buildReviewerAgentConfig(orchestratorModel) {
148280
+ const overrides = deriveSubagentModels(orchestratorModel);
148173
148281
  return {
148174
148282
  [REVIEWER_AGENT_NAME]: {
148175
- description: "Read-only review subagent for self-review and lens-based code review. Reads only \u2014 no writes, no state-changing shell or MCP calls, no nested subagent dispatch.",
148283
+ description: "Read-only review subagent for lens-based code review (correctness, security, billing-subsystem, etc.). Reads only \u2014 no writes, no state-changing shell or MCP calls, no nested subagent dispatch.",
148176
148284
  mode: "subagent",
148177
- prompt: REVIEWER_SYSTEM_PROMPT
148285
+ prompt: REVIEWER_SYSTEM_PROMPT,
148286
+ ...overrides.reviewer !== void 0 ? { model: overrides.reviewer } : {}
148178
148287
  }
148179
148288
  };
148180
148289
  }
@@ -148199,7 +148308,7 @@ function autoSelectModel(cliPath) {
148199
148308
  const availableSet = new Set(availableModels);
148200
148309
  if (availableSet.size > 0) {
148201
148310
  log.debug(`\xBB opencode models (${availableSet.size}): ${availableModels.join(", ")}`);
148202
- const match3 = modelAliases.find((a) => a.preferred && availableSet.has(a.resolve)) ?? modelAliases.find((a) => availableSet.has(a.resolve));
148311
+ const match3 = modelAliases.find((a) => !a.hidden && a.preferred && availableSet.has(a.resolve)) ?? modelAliases.find((a) => !a.hidden && availableSet.has(a.resolve));
148203
148312
  if (match3) {
148204
148313
  log.info(
148205
148314
  `\xBB model: ${match3.resolve} (auto-selected${match3.preferred ? " \u2014 preferred" : ""} curated match)`
@@ -148725,7 +148834,10 @@ var opencode = agent({
148725
148834
  install: installOpencodeCli,
148726
148835
  run: async (ctx) => {
148727
148836
  const cliPath = await installOpencodeCli();
148728
- const model = ctx.payload.proxyModel ?? ctx.resolvedModel ?? autoSelectModel(cliPath);
148837
+ const rawModel = ctx.payload.proxyModel ?? ctx.resolvedModel ?? autoSelectModel(cliPath);
148838
+ const bedrockModelId = process.env[BEDROCK_MODEL_ID_ENV]?.trim();
148839
+ const isBedrockRoute = rawModel !== void 0 && bedrockModelId !== void 0 && bedrockModelId === rawModel;
148840
+ const model = isBedrockRoute ? `amazon-bedrock/${rawModel}` : rawModel;
148729
148841
  const homeEnv = {
148730
148842
  HOME: ctx.tmpdir,
148731
148843
  XDG_CONFIG_HOME: join11(ctx.tmpdir, ".config")
@@ -148754,7 +148866,6 @@ var opencode = agent({
148754
148866
  ...homeEnv,
148755
148867
  OPENCODE_CONFIG_CONTENT: buildSecurityConfig(ctx, model),
148756
148868
  OPENCODE_PERMISSION: permissionOverride,
148757
- OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX: PULLFROG_OPENCODE_OUTPUT_LIMIT.toString(),
148758
148869
  GOOGLE_GENERATIVE_AI_API_KEY: process.env.GOOGLE_GENERATIVE_AI_API_KEY || process.env.GEMINI_API_KEY
148759
148870
  };
148760
148871
  const repoDir = process.cwd();
@@ -148797,13 +148908,29 @@ function hasEnvVar(name) {
148797
148908
  function hasClaudeCodeAuth() {
148798
148909
  return hasEnvVar("CLAUDE_CODE_OAUTH_TOKEN") || hasEnvVar("ANTHROPIC_API_KEY");
148799
148910
  }
148911
+ function hasBedrockAuth() {
148912
+ return hasEnvVar("AWS_BEARER_TOKEN_BEDROCK") || hasEnvVar("AWS_ACCESS_KEY_ID") && hasEnvVar("AWS_SECRET_ACCESS_KEY");
148913
+ }
148914
+ function resolveSlug(slug2) {
148915
+ const alias = resolveDisplayAlias(slug2);
148916
+ if (alias?.routing === "bedrock") {
148917
+ const bedrockId = process.env[BEDROCK_MODEL_ID_ENV]?.trim();
148918
+ if (!bedrockId) {
148919
+ throw new Error(
148920
+ `${BEDROCK_MODEL_ID_ENV} env var is required when the model is set to "${slug2}". set it to an AWS Bedrock model ID (e.g. "us.anthropic.claude-opus-4-7", "amazon.nova-pro-v1:0"). see https://docs.pullfrog.com/bedrock for setup.`
148921
+ );
148922
+ }
148923
+ return bedrockId;
148924
+ }
148925
+ return resolveCliModel(slug2);
148926
+ }
148800
148927
  function resolveModel(ctx) {
148801
148928
  const envModel = process.env.PULLFROG_MODEL?.trim();
148802
148929
  if (envModel) {
148803
- return resolveCliModel(envModel) ?? envModel;
148930
+ return resolveSlug(envModel) ?? envModel;
148804
148931
  }
148805
148932
  if (ctx.slug) {
148806
- const resolved = resolveCliModel(ctx.slug);
148933
+ const resolved = resolveSlug(ctx.slug);
148807
148934
  if (resolved) {
148808
148935
  return resolved;
148809
148936
  }
@@ -148819,6 +148946,9 @@ function resolveAgent(ctx) {
148819
148946
  }
148820
148947
  log.warning(`\xBB unknown PULLFROG_AGENT="${envAgent}" \u2014 falling through to auto-select`);
148821
148948
  }
148949
+ if (ctx.model && hasBedrockAuth() && process.env[BEDROCK_MODEL_ID_ENV]?.trim() === ctx.model) {
148950
+ return isBedrockAnthropicId(ctx.model) ? agents.claude : agents.opencode;
148951
+ }
148822
148952
  if (ctx.model) {
148823
148953
  try {
148824
148954
  const provider2 = getModelProvider(ctx.model);
@@ -148833,31 +148963,56 @@ function resolveAgent(ctx) {
148833
148963
 
148834
148964
  // utils/apiKeys.ts
148835
148965
  var knownApiKeys = new Set(Object.values(providers).flatMap((p2) => [...p2.envVars]));
148966
+ var MISSING_KEY_MARKER = "no API key found";
148836
148967
  function buildMissingApiKeyError(params) {
148837
- const apiUrl = getApiUrl();
148838
- const settingsUrl = `${apiUrl}/console/${params.owner}/${params.name}`;
148839
- const githubRepoUrl = `https://github.com/${params.owner}/${params.name}`;
148840
- const githubSecretsUrl = `${githubRepoUrl}/settings/secrets/actions`;
148841
- return `no API key found. Pullfrog requires at least one LLM provider API key.
148968
+ const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
148969
+ const settingsUrl = `${getApiUrl()}/console/${params.owner}/${params.name}`;
148970
+ return [
148971
+ `**${MISSING_KEY_MARKER}** \u2014 Pullfrog needs at least one LLM provider API key (e.g. \`ANTHROPIC_API_KEY\`, \`OPENAI_API_KEY\`, \`GEMINI_API_KEY\`) configured as a GitHub Actions secret.`,
148972
+ "",
148973
+ `[Open repo secrets \u2192](${githubSecretsUrl}) \xB7 [Configure model \u2192](${settingsUrl}) \xB7 [Setup docs \u2192](https://docs.pullfrog.com/keys) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
148974
+ ].join("\n");
148975
+ }
148976
+ function buildBedrockSetupError(params) {
148977
+ const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
148978
+ return `Bedrock model selected but required configuration is missing: ${params.missing.join(", ")}.
148842
148979
 
148843
- to fix this, add the required secret to your GitHub repository:
148980
+ add the missing secret(s) to your GitHub repository at ${githubSecretsUrl}, then reference them in your workflow's \`env:\` block:
148844
148981
 
148845
- 1. go to: ${githubSecretsUrl}
148846
- 2. click "New repository secret"
148847
- 3. set the name to your provider's key (e.g., \`ANTHROPIC_API_KEY\`, \`OPENAI_API_KEY\`, \`GEMINI_API_KEY\`)
148848
- 4. set the value to your API key
148849
- 5. click "Add secret"
148982
+ AWS_BEARER_TOKEN_BEDROCK: \${{ secrets.AWS_BEARER_TOKEN_BEDROCK }}
148983
+ AWS_REGION: \${{ secrets.AWS_REGION }}
148984
+ ${BEDROCK_MODEL_ID_ENV}: \${{ secrets.${BEDROCK_MODEL_ID_ENV} }}
148850
148985
 
148851
- configure your model at ${settingsUrl}
148986
+ \`AWS_BEARER_TOKEN_BEDROCK\` may be substituted with \`AWS_ACCESS_KEY_ID\` + \`AWS_SECRET_ACCESS_KEY\` (and optional \`AWS_SESSION_TOKEN\`) if you prefer access keys.
148852
148987
 
148853
- for full setup instructions, see https://docs.pullfrog.com/keys`;
148988
+ for full setup instructions, see https://docs.pullfrog.com/bedrock`;
148854
148989
  }
148855
148990
  function hasEnvVar2(name) {
148856
148991
  const value2 = process.env[name];
148857
148992
  return typeof value2 === "string" && value2.length > 0;
148858
148993
  }
148994
+ function validateBedrockSetup(params) {
148995
+ const hasAuth = hasEnvVar2("AWS_BEARER_TOKEN_BEDROCK") || hasEnvVar2("AWS_ACCESS_KEY_ID") && hasEnvVar2("AWS_SECRET_ACCESS_KEY");
148996
+ const missing = [];
148997
+ if (!hasAuth)
148998
+ missing.push("AWS_BEARER_TOKEN_BEDROCK (or AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY)");
148999
+ if (!hasEnvVar2("AWS_REGION")) missing.push("AWS_REGION");
149000
+ if (!hasEnvVar2(BEDROCK_MODEL_ID_ENV)) missing.push(BEDROCK_MODEL_ID_ENV);
149001
+ if (missing.length > 0) {
149002
+ throw new Error(buildBedrockSetupError({ owner: params.owner, name: params.name, missing }));
149003
+ }
149004
+ }
148859
149005
  function validateAgentApiKey(params) {
148860
149006
  if (params.model) {
149007
+ const alias = resolveDisplayAlias(params.model);
149008
+ if (alias?.routing === "bedrock") {
149009
+ validateBedrockSetup({ owner: params.owner, name: params.name });
149010
+ return;
149011
+ }
149012
+ if (!params.model.includes("/")) {
149013
+ validateBedrockSetup({ owner: params.owner, name: params.name });
149014
+ return;
149015
+ }
148861
149016
  const requiredVars = getModelEnvVars(params.model);
148862
149017
  if (requiredVars.length === 0) return;
148863
149018
  if (requiredVars.some((v) => hasEnvVar2(v))) return;
@@ -148868,6 +149023,22 @@ function validateAgentApiKey(params) {
148868
149023
  throw new Error(buildMissingApiKeyError({ owner: params.owner, name: params.name }));
148869
149024
  }
148870
149025
  }
149026
+ function isApiKeyAuthError(text) {
149027
+ if (!text) return false;
149028
+ return text.includes(MISSING_KEY_MARKER) || /Invalid API key/i.test(text) || /\bUser not found\b/i.test(text) || /\bInvalid authentication\b/i.test(text);
149029
+ }
149030
+ function formatApiKeyErrorSummary(params) {
149031
+ if (params.raw.includes(MISSING_KEY_MARKER)) {
149032
+ return buildMissingApiKeyError({ owner: params.owner, name: params.name });
149033
+ }
149034
+ const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
149035
+ const settingsUrl = `${getApiUrl()}/console/${params.owner}/${params.name}`;
149036
+ return [
149037
+ `**Your LLM provider API key was rejected (401).** Rotate the key in your provider dashboard, then update the matching GitHub Actions secret.`,
149038
+ "",
149039
+ `[Update repo secret \u2192](${githubSecretsUrl}) \xB7 [Model settings \u2192](${settingsUrl}) \xB7 [Setup docs \u2192](https://docs.pullfrog.com/keys) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
149040
+ ].join("\n");
149041
+ }
148871
149042
 
148872
149043
  // utils/body.ts
148873
149044
  var import_turndown = __toESM(require_turndown_cjs(), 1);
@@ -153392,10 +153563,31 @@ function buildPromptContext(ctx) {
153392
153563
  userQuoted: user ? user.split("\n").map((line) => `> ${line}`).join("\n") : ""
153393
153564
  };
153394
153565
  }
153395
- function assembleFullPrompt(ctx) {
153396
- const learningsSection = ctx.learningsFilePath ? `************* LEARNINGS *************
153566
+ function renderLearningsToc(headings) {
153567
+ if (headings.length === 0) return "";
153568
+ const rootDepth = Math.min(...headings.map((h) => h.depth));
153569
+ return headings.map((h) => {
153570
+ const indent2 = " ".repeat((h.depth - rootDepth) * 2);
153571
+ return `${indent2}- ${h.title} (L${h.startLine}-L${h.endLine})`;
153572
+ }).join("\n");
153573
+ }
153574
+ function buildLearningsSection(ctx) {
153575
+ if (!ctx.filePath) return "";
153576
+ const intro = `Repo-level learnings accumulated by previous agent runs live at \`${ctx.filePath}\`. Use this file as durable context (test commands, conventions, gotchas, architecture notes).`;
153577
+ const tocBody = ctx.headings.length === 0 ? "(no headings yet \u2014 file is empty or a flat list. read the whole file. during the post-run reflection turn, structure it with `## ` / `### ` headings so future runs can read targeted ranges.)" : `Read targeted line ranges via your native file tool \u2014 do NOT slurp the whole file. Each range starts at the section heading line, so reading the range gives you heading + body together.
153397
153578
 
153398
- Repo-level learnings accumulated by previous agent runs live at \`${ctx.learningsFilePath}\`. Read this file early and let the entries inform your approach (test commands, conventions, gotchas, etc.). The file may be empty if no learnings have been collected yet.` : "";
153579
+ ${renderLearningsToc(ctx.headings)}`;
153580
+ return `************* LEARNINGS *************
153581
+
153582
+ ${intro}
153583
+
153584
+ ${tocBody}`;
153585
+ }
153586
+ function assembleFullPrompt(ctx) {
153587
+ const learningsSection = buildLearningsSection({
153588
+ filePath: ctx.learningsFilePath,
153589
+ headings: ctx.learningsHeadings
153590
+ });
153399
153591
  const runtimeSection = `************* RUNTIME *************
153400
153592
 
153401
153593
  ${ctx.runtime}`;
@@ -153423,7 +153615,10 @@ function resolveInstructions(ctx) {
153423
153615
  tocEntries.push({ label: "EVENT CONTEXT", description: "related PR/issue data" });
153424
153616
  tocEntries.push({ label: "SYSTEM", description: "persona, security, tools, workflow rules" });
153425
153617
  if (pctx.learningsFilePath)
153426
- tocEntries.push({ label: "LEARNINGS", description: "repo-specific knowledge file path" });
153618
+ tocEntries.push({
153619
+ label: "LEARNINGS",
153620
+ description: "repo-specific knowledge file path + heading TOC"
153621
+ });
153427
153622
  tocEntries.push({ label: "RUNTIME", description: "environment metadata" });
153428
153623
  const toc = buildToc(tocEntries);
153429
153624
  const full = assembleFullPrompt({
@@ -153433,6 +153628,7 @@ function resolveInstructions(ctx) {
153433
153628
  eventContext,
153434
153629
  system,
153435
153630
  learningsFilePath: pctx.learningsFilePath,
153631
+ learningsHeadings: pctx.learningsHeadings,
153436
153632
  runtime: pctx.runtime
153437
153633
  });
153438
153634
  const event = [pctx.eventTitle, pctx.eventMetadata].filter(Boolean).join("\n\n---\n\n");
@@ -153450,7 +153646,7 @@ function resolveInstructions(ctx) {
153450
153646
  import { mkdir, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
153451
153647
  import { dirname as dirname4, join as join14 } from "node:path";
153452
153648
  var LEARNINGS_FILE_NAME = "pullfrog-learnings.md";
153453
- var MAX_LEARNINGS_LENGTH = 1e4;
153649
+ var MAX_LEARNINGS_LENGTH = 1e5;
153454
153650
  function learningsFilePath(tmpdir3) {
153455
153651
  return join14(tmpdir3, LEARNINGS_FILE_NAME);
153456
153652
  }
@@ -153460,6 +153656,15 @@ async function seedLearningsFile(params) {
153460
153656
  await writeFile2(path3, params.current ?? "", "utf8");
153461
153657
  return path3;
153462
153658
  }
153659
+ var TRUNCATION_LINE_BOUNDARY_TOLERANCE = 4096;
153660
+ function truncateAtLineBoundary(body, cap) {
153661
+ if (body.length <= cap) return body;
153662
+ const head = body.slice(0, cap);
153663
+ const lastNewline = head.lastIndexOf("\n");
153664
+ if (lastNewline <= 0) return head;
153665
+ if (cap - lastNewline > TRUNCATION_LINE_BOUNDARY_TOLERANCE) return head;
153666
+ return head.slice(0, lastNewline);
153667
+ }
153463
153668
  async function readLearningsFile(path3) {
153464
153669
  let raw2;
153465
153670
  try {
@@ -153467,9 +153672,7 @@ async function readLearningsFile(path3) {
153467
153672
  } catch {
153468
153673
  return null;
153469
153674
  }
153470
- const trimmed = raw2.trim();
153471
- if (trimmed.length > MAX_LEARNINGS_LENGTH) return trimmed.slice(0, MAX_LEARNINGS_LENGTH);
153472
- return trimmed;
153675
+ return truncateAtLineBoundary(raw2.trim(), MAX_LEARNINGS_LENGTH);
153473
153676
  }
153474
153677
 
153475
153678
  // utils/normalizeEnv.ts
@@ -153816,6 +154019,7 @@ var defaultSettings = {
153816
154019
  prApproveEnabled: false,
153817
154020
  modeInstructions: {},
153818
154021
  learnings: null,
154022
+ learningsHeadings: [],
153819
154023
  envAllowlist: null
153820
154024
  };
153821
154025
  var defaultRunContext = {
@@ -153856,7 +154060,8 @@ async function fetchRunContext(params) {
153856
154060
  setupScript: data.settings?.setupScript ?? null,
153857
154061
  postCheckoutScript: data.settings?.postCheckoutScript ?? null,
153858
154062
  prepushScript: data.settings?.prepushScript ?? null,
153859
- stopScript: data.settings?.stopScript ?? null
154063
+ stopScript: data.settings?.stopScript ?? null,
154064
+ learningsHeadings: data.settings?.learningsHeadings ?? []
153860
154065
  },
153861
154066
  apiToken: data.apiToken,
153862
154067
  oss: data.oss ?? false,
@@ -154622,10 +154827,7 @@ async function main() {
154622
154827
  current: runContext.repoSettings.learnings
154623
154828
  });
154624
154829
  toolState.learningsFilePath = learningsPath;
154625
- try {
154626
- toolState.learningsSeed = await readFile4(learningsPath, "utf8");
154627
- } catch {
154628
- }
154830
+ toolState.learningsSeed = (runContext.repoSettings.learnings ?? "").trim();
154629
154831
  log.info(
154630
154832
  `\xBB learnings seeded at ${learningsPath} (existing=${runContext.repoSettings.learnings ? "yes" : "no"})`
154631
154833
  );
@@ -154665,7 +154867,8 @@ async function main() {
154665
154867
  modes: modes2,
154666
154868
  agentId,
154667
154869
  outputSchema,
154668
- learningsFilePath: toolState.learningsFilePath ?? null
154870
+ learningsFilePath: toolState.learningsFilePath ?? null,
154871
+ learningsHeadings: runContext.repoSettings.learningsHeadings
154669
154872
  });
154670
154873
  const logParts = [
154671
154874
  instructions.eventInstructions ? `EVENT-LEVEL INSTRUCTIONS:
@@ -154802,10 +155005,13 @@ ${instructions.user}` : null,
154802
155005
  await persistLearnings(toolContext);
154803
155006
  }
154804
155007
  if (!result.success && toolContext && toolState.progressComment) {
154805
- await reportErrorToComment({
154806
- toolState,
154807
- error: result.error || "agent run failed"
154808
- }).catch((error49) => {
155008
+ const rawError = result.error || "agent run failed";
155009
+ const errorBody = isApiKeyAuthError(rawError) ? formatApiKeyErrorSummary({
155010
+ owner: runContext.repo.owner,
155011
+ name: runContext.repo.name,
155012
+ raw: rawError
155013
+ }) : rawError;
155014
+ await reportErrorToComment({ toolState, error: errorBody }).catch((error49) => {
154809
155015
  log.debug(`failure error report failed: ${error49}`);
154810
155016
  });
154811
155017
  }
@@ -154841,8 +155047,13 @@ ${instructions.user}` : null,
154841
155047
  killTrackedChildren();
154842
155048
  log.error(errorMessage);
154843
155049
  const billingError = isRouterKeylimitExhaustedError(errorMessage) ? new BillingError(errorMessage, { code: "router_keylimit_exhausted" }) : null;
155050
+ const apiKeyErrorSummary = !billingError && isApiKeyAuthError(errorMessage) ? formatApiKeyErrorSummary({
155051
+ owner: runContext.repo.owner,
155052
+ name: runContext.repo.name,
155053
+ raw: errorMessage
155054
+ }) : null;
154844
155055
  try {
154845
- const errorSummary = billingError ? formatBillingErrorSummary(billingError, runContext.repo.owner) : `### \u274C Pullfrog failed
155056
+ const errorSummary = billingError ? formatBillingErrorSummary(billingError, runContext.repo.owner) : apiKeyErrorSummary ?? `### \u274C Pullfrog failed
154846
155057
 
154847
155058
  \`\`\`
154848
155059
  ${errorMessage}
@@ -154853,7 +155064,7 @@ ${errorMessage}
154853
155064
  } catch {
154854
155065
  }
154855
155066
  try {
154856
- const commentBody = billingError ? formatBillingErrorSummary(billingError, runContext.repo.owner) : errorMessage;
155067
+ const commentBody = billingError ? formatBillingErrorSummary(billingError, runContext.repo.owner) : apiKeyErrorSummary ?? errorMessage;
154857
155068
  await reportErrorToComment({ toolState, error: commentBody });
154858
155069
  } catch {
154859
155070
  }
@@ -155996,8 +156207,10 @@ function link(text, url4) {
155996
156207
  return `\x1B]8;;${url4}\x07${text}\x1B]8;;\x07`;
155997
156208
  }
155998
156209
  function buildProviders() {
155999
- return Object.entries(providers).filter(([key]) => key !== "opencode" && key !== "openrouter").map(([key, config3]) => {
156000
- const aliases = modelAliases.filter((a) => a.provider === key && !a.fallback);
156210
+ return Object.entries(providers).filter(([key]) => key !== "opencode" && key !== "openrouter" && key !== "bedrock").map(([key, config3]) => {
156211
+ const aliases = modelAliases.filter(
156212
+ (a) => a.provider === key && !a.fallback && !a.routing && !a.hidden
156213
+ );
156001
156214
  const recommended = aliases.find((a) => a.preferred);
156002
156215
  const sorted = [...aliases].sort((a, b) => {
156003
156216
  if (a.preferred && !b.preferred) return -1;
@@ -156710,7 +156923,7 @@ async function run2() {
156710
156923
  }
156711
156924
 
156712
156925
  // cli.ts
156713
- var VERSION10 = "0.1.6";
156926
+ var VERSION10 = "0.1.7";
156714
156927
  var bin = basename2(process.argv[1] || "");
156715
156928
  var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
156716
156929
  var rawArgs = process.argv.slice(2);