pullfrog 0.1.34 → 0.1.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -27957,7 +27957,7 @@ var require_cross_spawn = __commonJS({
27957
27957
  var cp = __require("child_process");
27958
27958
  var parse5 = require_parse3();
27959
27959
  var enoent = require_enoent();
27960
- function spawn3(command, args2, options) {
27960
+ function spawn4(command, args2, options) {
27961
27961
  const parsed2 = parse5(command, args2, options);
27962
27962
  const spawned = cp.spawn(parsed2.command, parsed2.args, parsed2.options);
27963
27963
  enoent.hookChildProcess(spawned, parsed2);
@@ -27969,8 +27969,8 @@ var require_cross_spawn = __commonJS({
27969
27969
  result.error = result.error || enoent.verifyENOENTSync(result.status, parsed2);
27970
27970
  return result;
27971
27971
  }
27972
- module.exports = spawn3;
27973
- module.exports.spawn = spawn3;
27972
+ module.exports = spawn4;
27973
+ module.exports.spawn = spawn4;
27974
27974
  module.exports.sync = spawnSync6;
27975
27975
  module.exports._parse = parse5;
27976
27976
  module.exports._enoent = enoent;
@@ -95782,14 +95782,14 @@ var require_turndown_cjs = __commonJS({
95782
95782
  } else if (node2.nodeType === 1) {
95783
95783
  replacement = replacementForNode.call(self2, node2);
95784
95784
  }
95785
- return join25(output, replacement);
95785
+ return join26(output, replacement);
95786
95786
  }, "");
95787
95787
  }
95788
95788
  function postProcess(output) {
95789
95789
  var self2 = this;
95790
95790
  this.rules.forEach(function(rule) {
95791
95791
  if (typeof rule.append === "function") {
95792
- output = join25(output, rule.append(self2.options));
95792
+ output = join26(output, rule.append(self2.options));
95793
95793
  }
95794
95794
  });
95795
95795
  return output.replace(/^[\t\r\n]+/, "").replace(/[\t\r\n\s]+$/, "");
@@ -95801,7 +95801,7 @@ var require_turndown_cjs = __commonJS({
95801
95801
  if (whitespace.leading || whitespace.trailing) content = content.trim();
95802
95802
  return whitespace.leading + rule.replacement(content, node2, this.options) + whitespace.trailing;
95803
95803
  }
95804
- function join25(output, replacement) {
95804
+ function join26(output, replacement) {
95805
95805
  var s1 = trimTrailingNewlines(output);
95806
95806
  var s2 = trimLeadingNewlines(replacement);
95807
95807
  var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
@@ -99801,7 +99801,7 @@ var init_file_type = __esm({
99801
99801
  // main.ts
99802
99802
  import { existsSync as existsSync8, readdirSync as readdirSync2 } from "node:fs";
99803
99803
  import { readFile as readFile5 } from "node:fs/promises";
99804
- import { join as join24 } from "node:path";
99804
+ import { join as join25 } from "node:path";
99805
99805
 
99806
99806
  // agents/claude.ts
99807
99807
  import { execFileSync as execFileSync2 } from "node:child_process";
@@ -99859,37 +99859,40 @@ var providers = {
99859
99859
  managedCredentials: ["CODEX_AUTH_JSON"],
99860
99860
  models: {
99861
99861
  gpt: {
99862
- displayName: "GPT",
99863
- resolve: "openai/gpt-5.5",
99864
- openRouterResolve: "openrouter/openai/gpt-5.5",
99862
+ displayName: "GPT Sol",
99863
+ resolve: "openai/gpt-5.6-sol",
99864
+ openRouterResolve: "openrouter/openai/gpt-5.6-sol",
99865
99865
  preferred: true,
99866
- subagentModel: "gpt-5.4"
99866
+ subagentModel: "gpt-terra"
99867
99867
  },
99868
+ // Sol served at reasoning.mode=pro — same $/token as Sol, just more tokens
99869
+ // burned; not a pricier premium tier. models.dev has no -pro id, so direct-key
99870
+ // (BYOK) resolves to plain Sol; only the Router/OpenRouter path gets sol-pro.
99868
99871
  "gpt-pro": {
99869
- displayName: "GPT Pro",
99870
- resolve: "openai/gpt-5.5-pro",
99871
- openRouterResolve: "openrouter/openai/gpt-5.5-pro",
99872
+ displayName: "GPT Sol Pro",
99873
+ description: "Maximum reasoning effort",
99874
+ resolve: "openai/gpt-5.6-sol",
99875
+ openRouterResolve: "openrouter/openai/gpt-5.6-sol-pro",
99872
99876
  subagentModel: "gpt"
99873
99877
  },
99874
- // hidden subagent target `gpt` lenses run against this. surfacing
99875
- // it in the picker would just confuse users (it's the prior-flagship,
99876
- // and they already have `gpt` and `gpt-mini` to choose from).
99877
- "gpt-5.4": {
99878
- displayName: "GPT 5.4",
99879
- resolve: "openai/gpt-5.4",
99880
- openRouterResolve: "openrouter/openai/gpt-5.4",
99881
- hidden: true
99878
+ // gpt-5.6's balanced mid-tier (Sol/Terra/Luna are durable capability tiers,
99879
+ // not version bumps). selectable on its own and doubles as `gpt`'s cheaper
99880
+ // lens-fanout subagent replaces the old hidden gpt-5.4 subagent target.
99881
+ "gpt-terra": {
99882
+ displayName: "GPT Terra",
99883
+ resolve: "openai/gpt-5.6-terra",
99884
+ openRouterResolve: "openrouter/openai/gpt-5.6-terra"
99882
99885
  },
99883
99886
  "gpt-mini": {
99884
- displayName: "GPT Mini",
99885
- resolve: "openai/gpt-5.4-mini",
99886
- openRouterResolve: "openrouter/openai/gpt-5.4-mini"
99887
+ displayName: "GPT Luna",
99888
+ resolve: "openai/gpt-5.6-luna",
99889
+ openRouterResolve: "openrouter/openai/gpt-5.6-luna"
99887
99890
  },
99888
99891
  // legacy aliases — openai unified the codex line into the main GPT family
99889
99892
  // and is shutting down every "-codex" snapshot on 2026-07-23. transparently
99890
99893
  // upgrade existing users via the fallback chain. UI display sites resolve
99891
99894
  // to the terminal alias's label (so dropdown trigger + PR footers show
99892
- // "GPT" / "GPT Mini", not the historical name).
99895
+ // "GPT Sol" / "GPT Luna", not the historical name).
99893
99896
  "gpt-codex": {
99894
99897
  displayName: "GPT Codex",
99895
99898
  resolve: "openai/gpt-5.3-codex",
@@ -99902,6 +99905,13 @@ var providers = {
99902
99905
  openRouterResolve: "openrouter/openai/gpt-5.1-codex-mini",
99903
99906
  fallback: "openai/gpt-mini"
99904
99907
  },
99908
+ // dropped hidden subagent tier — folds stored pins forward to gpt (Sol).
99909
+ "gpt-5.4": {
99910
+ displayName: "GPT 5.4",
99911
+ resolve: "openai/gpt-5.4",
99912
+ openRouterResolve: "openrouter/openai/gpt-5.4",
99913
+ fallback: "openai/gpt"
99914
+ },
99905
99915
  o3: {
99906
99916
  displayName: "O3",
99907
99917
  resolve: "openai/o3",
@@ -100031,28 +100041,29 @@ var providers = {
100031
100041
  openRouterResolve: "openrouter/anthropic/claude-haiku-4.5"
100032
100042
  },
100033
100043
  gpt: {
100034
- displayName: "GPT",
100035
- resolve: "opencode/gpt-5.5",
100036
- openRouterResolve: "openrouter/openai/gpt-5.5",
100037
- subagentModel: "gpt-5.4"
100044
+ displayName: "GPT Sol",
100045
+ resolve: "opencode/gpt-5.6-sol",
100046
+ openRouterResolve: "openrouter/openai/gpt-5.6-sol",
100047
+ subagentModel: "gpt-terra"
100038
100048
  },
100049
+ // see openai/gpt-pro — Zen has no -pro id, so direct resolves to plain Sol.
100039
100050
  "gpt-pro": {
100040
- displayName: "GPT Pro",
100041
- resolve: "opencode/gpt-5.5-pro",
100042
- openRouterResolve: "openrouter/openai/gpt-5.5-pro",
100051
+ displayName: "GPT Sol Pro",
100052
+ description: "Maximum reasoning effort",
100053
+ resolve: "opencode/gpt-5.6-sol",
100054
+ openRouterResolve: "openrouter/openai/gpt-5.6-sol-pro",
100043
100055
  subagentModel: "gpt"
100044
100056
  },
100045
- // hidden subagent target — see openai provider above for context.
100046
- "gpt-5.4": {
100047
- displayName: "GPT 5.4",
100048
- resolve: "opencode/gpt-5.4",
100049
- openRouterResolve: "openrouter/openai/gpt-5.4",
100050
- hidden: true
100057
+ // gpt-5.6 balanced mid-tierselectable + `gpt`'s subagent. see openai above.
100058
+ "gpt-terra": {
100059
+ displayName: "GPT Terra",
100060
+ resolve: "opencode/gpt-5.6-terra",
100061
+ openRouterResolve: "openrouter/openai/gpt-5.6-terra"
100051
100062
  },
100052
100063
  "gpt-mini": {
100053
- displayName: "GPT Mini",
100054
- resolve: "opencode/gpt-5.4-mini",
100055
- openRouterResolve: "openrouter/openai/gpt-5.4-mini"
100064
+ displayName: "GPT Luna",
100065
+ resolve: "opencode/gpt-5.6-luna",
100066
+ openRouterResolve: "openrouter/openai/gpt-5.6-luna"
100056
100067
  },
100057
100068
  // legacy aliases — see openai provider above for context.
100058
100069
  "gpt-codex": {
@@ -100067,6 +100078,13 @@ var providers = {
100067
100078
  openRouterResolve: "openrouter/openai/gpt-5.1-codex-mini",
100068
100079
  fallback: "opencode/gpt-mini"
100069
100080
  },
100081
+ // dropped hidden subagent tier — folds stored pins forward to gpt (Sol).
100082
+ "gpt-5.4": {
100083
+ displayName: "GPT 5.4",
100084
+ resolve: "opencode/gpt-5.4",
100085
+ openRouterResolve: "openrouter/openai/gpt-5.4",
100086
+ fallback: "opencode/gpt"
100087
+ },
100070
100088
  "gemini-pro": {
100071
100089
  displayName: "Gemini Pro",
100072
100090
  resolve: "opencode/gemini-3.1-pro",
@@ -100183,29 +100201,34 @@ var providers = {
100183
100201
  resolve: "openrouter/~anthropic/claude-haiku-latest",
100184
100202
  openRouterResolve: "openrouter/~anthropic/claude-haiku-latest"
100185
100203
  },
100204
+ // pinned to the explicit gpt-5.6 tiers (not the ~openai/gpt-latest rolling
100205
+ // alias): after the Sol/Terra/Luna rename, ~gpt-mini-latest no longer maps
100206
+ // to Luna, so rolling aliases would silently diverge `gpt`/`gpt-mini` from
100207
+ // the chosen tiers across funding paths.
100186
100208
  gpt: {
100187
- displayName: "GPT",
100188
- resolve: "openrouter/~openai/gpt-latest",
100189
- openRouterResolve: "openrouter/~openai/gpt-latest",
100190
- subagentModel: "gpt-5.4"
100209
+ displayName: "GPT Sol",
100210
+ resolve: "openrouter/openai/gpt-5.6-sol",
100211
+ openRouterResolve: "openrouter/openai/gpt-5.6-sol",
100212
+ subagentModel: "gpt-terra"
100191
100213
  },
100214
+ // see openai/gpt-pro. openrouter serves sol-pro directly on both routes.
100192
100215
  "gpt-pro": {
100193
- displayName: "GPT Pro",
100194
- resolve: "openrouter/openai/gpt-5.5-pro",
100195
- openRouterResolve: "openrouter/openai/gpt-5.5-pro",
100216
+ displayName: "GPT Sol Pro",
100217
+ description: "Maximum reasoning effort",
100218
+ resolve: "openrouter/openai/gpt-5.6-sol-pro",
100219
+ openRouterResolve: "openrouter/openai/gpt-5.6-sol-pro",
100196
100220
  subagentModel: "gpt"
100197
100221
  },
100198
- // hidden subagent target — see openai provider above for context.
100199
- "gpt-5.4": {
100200
- displayName: "GPT 5.4",
100201
- resolve: "openrouter/openai/gpt-5.4",
100202
- openRouterResolve: "openrouter/openai/gpt-5.4",
100203
- hidden: true
100222
+ // gpt-5.6 balanced mid-tierselectable + `gpt`'s subagent. see openai above.
100223
+ "gpt-terra": {
100224
+ displayName: "GPT Terra",
100225
+ resolve: "openrouter/openai/gpt-5.6-terra",
100226
+ openRouterResolve: "openrouter/openai/gpt-5.6-terra"
100204
100227
  },
100205
100228
  "gpt-mini": {
100206
- displayName: "GPT Mini",
100207
- resolve: "openrouter/~openai/gpt-mini-latest",
100208
- openRouterResolve: "openrouter/~openai/gpt-mini-latest"
100229
+ displayName: "GPT Luna",
100230
+ resolve: "openrouter/openai/gpt-5.6-luna",
100231
+ openRouterResolve: "openrouter/openai/gpt-5.6-luna"
100209
100232
  },
100210
100233
  // legacy aliases — see openai provider for context.
100211
100234
  "gpt-codex": {
@@ -100220,6 +100243,13 @@ var providers = {
100220
100243
  openRouterResolve: "openrouter/openai/gpt-5.1-codex-mini",
100221
100244
  fallback: "openrouter/gpt-mini"
100222
100245
  },
100246
+ // dropped hidden subagent tier — folds stored pins forward to gpt (Sol).
100247
+ "gpt-5.4": {
100248
+ displayName: "GPT 5.4",
100249
+ resolve: "openrouter/openai/gpt-5.4",
100250
+ openRouterResolve: "openrouter/openai/gpt-5.4",
100251
+ fallback: "openrouter/gpt"
100252
+ },
100223
100253
  "o4-mini": {
100224
100254
  displayName: "O4 Mini",
100225
100255
  resolve: "openrouter/openai/o4-mini",
@@ -100299,6 +100329,7 @@ var modelAliases = Object.entries(providers).flatMap(
100299
100329
  slug: `${providerKey}/${modelId}`,
100300
100330
  provider: providerKey,
100301
100331
  displayName: def.displayName,
100332
+ description: def.description,
100302
100333
  resolve: def.resolve,
100303
100334
  openRouterResolve: def.openRouterResolve,
100304
100335
  preferred: def.preferred ?? false,
@@ -101041,7 +101072,7 @@ var import_semver = __toESM(require_semver2(), 1);
101041
101072
  // package.json
101042
101073
  var package_default = {
101043
101074
  name: "pullfrog",
101044
- version: "0.1.34",
101075
+ version: "0.1.36",
101045
101076
  type: "module",
101046
101077
  bin: {
101047
101078
  pullfrog: "dist/cli.mjs",
@@ -101715,8 +101746,9 @@ HARD CONSTRAINTS (non-negotiable, regardless of orchestrator instructions):
101715
101746
  - Your FIRST action MUST source the diff for review. If the orchestrator's dispatch names a diff PATH on disk (e.g. \`diffPath\` / \`incrementalDiffPath\` from a prior \`checkout_pr\` call), \`read\` that path \u2014 do not invoke git at all. The on-disk diff is the authoritative scope, and dispatches almost always include one; recomputing it via git also fails on shallow GitHub Actions checkouts where the base ref may be unfetched. When BOTH a diff path and a base branch appear in your dispatch, path always wins. When the dispatch names an \`incrementalDiffPath\` alongside \`diffPath\`, prefer the incremental path for scope and consult the full diff only for line-number anchoring.
101716
101747
  - If (and only if) NO diff path was provided, the dispatch names a base branch. Run \`git diff --merge-base origin/<base>\` (single MCP call, captures committed + staged + unstaged work, excludes commits landed on \`origin/<base>\` since your branch forked). The read-only \`git\` MCP tool is the right surface for this \u2014 \`--merge-base\` is a flag git accepts directly, so no shell substitution is needed. Do NOT run bare \`git diff origin/<base>\` or two-dot \`git diff origin/<base>..HEAD\`: those are symmetric diffs that include the inverse of every commit on \`<base>\` your branch is behind, which is pure noise (and the git tool will reject those forms when the divergence is detected). Do NOT try to expand \`$(...)\` subshell forms via the git tool \u2014 it runs git directly without shell interpolation. If \`git diff --merge-base origin/<base>\` fails with \`ambiguous argument 'origin/<base>'\` or \`no merge base\`, the runner is a shallow single-branch checkout AND the orchestrator failed to fetch the base ref before dispatching you. Surface that in one line (which ref is missing, and that the orchestrator needs to fetch it with \`git fetch --no-tags --deepen=1000 origin <base>:refs/remotes/origin/<base>\` before re-dispatching) and stop. Do NOT run \`git fetch\` yourself \u2014 your read-only contract below forbids mutating shell, and the \`git_fetch\` MCP tool is state-changing and therefore prohibited. Do NOT call \`checkout_pr\`, do NOT fetch alternative refs, do NOT list branches or all-refs looking for the work, do NOT run \`gh pr list\`. The orchestrator's dispatch is the source of truth for scope.
101717
101748
  - If the on-disk diff path you were given is empty (or unreadable), that is a checkout / formatting failure on the orchestrator side \u2014 reply EXACTLY: \`no changes in dispatched diff \u2014 scope appears empty; orchestrator should verify checkout_pr output\` (naming the path), do NOT fall through to running \`git diff\` against guessed refs. If the merge-base diff (the fallback path) returns empty AND the orchestrator's dispatch claims there are changes to review, the most likely cause is a pre-commit Build-mode self-review: the orchestrator dispatched you before committing AND there are no uncommitted edits either. Reply EXACTLY: \`no changes detected \u2014 likely pre-commit Build self-review; orchestrator should commit then re-dispatch\` and stop. Do NOT guess PR numbers (e.g. by extrapolating from \`git log\` output), do NOT check out other PRs, do NOT fetch from forks. The empty diff is the diagnosis \u2014 surface it; do not work around it.
101749
+ - Once the mandatory first diff read returns a non-empty scope, batch each next dependency layer: emit all independent read-only file reads, greps, globs, and directory listings together in one assistant turn before awaiting their results. Include MCP queries only when their contract explicitly guarantees they are read-only; a \`get_*\` or \`list_*\` name alone is not proof. Keep dependent calls in later turns, after the results they depend on are available.
101718
101750
  - Read-only tools only. Do NOT write or edit files. Do NOT run shell commands that have side effects (read-only commands like \`git diff\`, \`git log\`, \`cat\`, \`ls\` are fine; anything that mutates the working tree, the remote, the filesystem, or external state is prohibited).
101719
- - Do NOT call any state-changing MCP tool. State-changing means: posts a comment, pushes a branch, creates/updates a PR or issue, changes labels, resolves review threads, persists learnings, sets workflow output, installs dependencies, uploads files, kills processes, etc. Read-only MCP queries (\`get_*\`, \`list_*\`, the \`git\` tool for read-only subcommands like \`diff\`/\`log\`/\`merge-base\`, log inspection, diff retrieval) are fine.
101751
+ - Do NOT call any state-changing MCP tool. State-changing means: posts a comment, pushes a branch, creates/updates a PR or issue, changes labels, resolves review threads, persists learnings, sets workflow output, installs dependencies, uploads files, kills processes, etc. MCP tool names are not evidence of safety: only use queries whose contract explicitly guarantees no side effects. The \`git\` tool is fine only for read-only subcommands like \`diff\`/\`log\`/\`merge-base\`.
101720
101752
  - Do NOT spawn further subagents. You are a leaf reviewer; recursive dispatch pre-aggregates findings through an intermediate model and defeats the design.
101721
101753
  - Test for any tool call before invoking it: would this still be a no-op if reverted? If not, do not call it. Apply this test to tools added after this prompt was written \u2014 the rule is the invariant, not the enumeration.
101722
101754
 
@@ -101979,20 +102011,15 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
101979
102011
  - **immediately** call \`${t("resolve_review_thread")}\` with that thread's \`thread=\` value as \`thread_id\`. Resolve every thread where you (a) made the requested code change in full \u2014 partial fixes leave the thread open \u2014 OR (b) replied with a substantive answer the user explicitly asked for. Do NOT resolve threads where you pushed back on the request and the disagreement is unresolved; leave those open for the human to mediate.
101980
102012
  - call \`${t("report_progress")}\` with a brief summary`
101981
102013
  },
101982
- // Review and IncrementalReview use a 0-or-2+ lens pattern. The default is
101983
- // 0 lenses (orchestrator handles the review solo). Multi-lens (2+
101984
- // reviewfrog subagents in parallel) only fires for substantive PRs or
101985
- // high-stakes-subsystem touches — and when it fires, ALL lenses must
101986
- // dispatch in a single assistant turn or the parallelism win disappears.
101987
- // We never dispatch exactly one lens: a single lens is just a worse,
101988
- // slower version of doing the work yourself.
102014
+ // Review and IncrementalReview route the minimum reviewfrog specialists
102015
+ // needed to cover unresolved, disposition-changing hypotheses. Most runs
102016
+ // use zero or one; multiple orthogonal hypotheses dispatch in parallel.
101989
102017
  //
101990
102018
  // Build mode self-review is a different problem shape: the orchestrator
101991
102019
  // wrote the code, so bias-mitigation comes from delegating to one
101992
102020
  // fresh-eyes subagent that doesn't share the implementation context. A
101993
- // single subagent there is appropriate; the 0-or-2+ rule applies only to
101994
- // the Review/IncrementalReview lens fan-out where independence between
101995
- // perspectives is what's being purchased.
102021
+ // single subagent there is appropriate. Review-mode specialist routing
102022
+ // instead scales with the unresolved hypotheses in someone else's diff.
101996
102023
  //
101997
102024
  // Severity categorization is split across two surfaces: the opening
101998
102025
  // callout (CAUTION/IMPORTANT/ℹ️/✅) sets the review's overall tier, and
@@ -102007,11 +102034,11 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
102007
102034
 
102008
102035
  1. **task list**: create your task list for this run as your first action.
102009
102036
 
102010
- 2. **checkout**: call \`${t("checkout_pr")}\` \u2014 this returns PR metadata and a \`diffPath\`. read the diff TOC end-to-end and treat its file line ranges as your coverage checklist.
102037
+ 2. **checkout**: call \`${t("checkout_pr")}\` \u2014 this returns PR metadata, a \`diffPath\`, and a supplemental \`impactPath\` when change-impact extraction is enabled. read the complete raw diff end-to-end, beginning with the TOC and using its file line ranges as your coverage checklist. only after that, use \`impactPath\` as an explicitly incomplete list of reference leads; it never replaces raw-diff reading or establishes coverage.
102011
102038
 
102012
102039
  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.
102013
102040
 
102014
- if the PR is **genuinely trivial**, skip the fan-out entirely and submit a \`No new issues found.\` review per step 7.
102041
+ if the PR is **genuinely trivial**, skip specialists entirely and submit a \`No new issues found.\` review per step 7.
102015
102042
 
102016
102043
  "Genuinely trivial" (skip):
102017
102044
  - single-word doc typo, whitespace/format-only, comment-only across any number of files
@@ -102030,22 +102057,24 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
102030
102057
  - any "typo fix" in user-facing copy that changes meaning ("approved" \u2192 "denied")
102031
102058
  - mixed diffs where a semantic 1-liner is buried in whitespace/formatting changes
102032
102059
 
102033
- 4. **lens decision \u2014 0 or 2+, NEVER 1**.
102060
+ 4. **specialist decision \u2014 minimum hypothesis coverage**.
102034
102061
 
102035
- The default is **0 lenses**: handle the review yourself end-to-end. Most PRs land here.
102062
+ After full-diff coverage and triage, identify the load-bearing questions you still cannot resolve confidently yourself. A specialist hypothesis is load-bearing only when its answer could yield an actionable finding that changes the review disposition and warrants independent investigation, and falsifiable only when the specialist can return evidence that supports or refutes it. Generic requests for extra confidence, polish, or "another look" do not qualify.
102036
102063
 
102037
- Dispatch **2+ \`${REVIEWER_AGENT_NAME}\` lenses in parallel** ONLY when ALL of the following are true:
102038
- - 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)
102039
- - you can name 2+ distinct concrete failure modes that warrant independent lenses (one lens per failure mode; orthogonal, not overlapping)
102040
- - parallel-orchestrated independent perspectives meaningfully outperform what you'd find solo
102064
+ Route the **minimum number of \`${REVIEWER_AGENT_NAME}\` specialists** needed to cover those unresolved hypotheses. Most reviews need **0 or 1**:
102065
+ - dispatch 0 when you can resolve every disposition-changing question directly
102066
+ - dispatch 1 when exactly one falsifiable, load-bearing hypothesis warrants independent investigation
102067
+ - dispatch 2+ in parallel when multiple orthogonal load-bearing hypotheses remain, or when the user explicitly requests an exhaustive or multi-angle review
102041
102068
 
102042
- **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).
102069
+ **There is NO one-specialist cap or fixed maximum.** Cover every orthogonal load-bearing hypothesis that remains; do not collapse multiple real questions into one broad prompt just to reduce the count. There is also no file-count, line-count, schema, quota, or hard-budget threshold \u2014 diff size is not a proxy for review uncertainty.
102043
102070
 
102044
- When you do go multi-lens, lens framings come in two flavors:
102071
+ The primary reviewer remains responsible for reading the complete raw diff, investigating surrounding code, validating every returned finding, and synthesizing the final review. Specialist reads supplement that work; they never replace it or satisfy the primary's diff-coverage obligation.
102072
+
102073
+ Specialist hypotheses can draw on two kinds of framing:
102045
102074
  - **themed lenses** \u2014 a perspective applied across the whole diff (correctness, security, user-journey, performance, etc.).
102046
102075
  - **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.
102047
102076
 
102048
- starter menu (combine, omit, or invent your own):
102077
+ starter menu for identifying hypotheses (combine, omit, or invent your own; do not dispatch a bare menu label without a falsifiable question):
102049
102078
  - **correctness & invariants** \u2014 bugs, races, error handling, edge cases, state-machine boundaries
102050
102079
  - **impact** \u2014 stale references in code/tests/docs/configs/UI after rename/remove
102051
102080
  - **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.
@@ -102060,30 +102089,27 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
102060
102089
 
102061
102090
  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.
102062
102091
 
102063
- 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.**
102064
-
102065
- \u26A0\uFE0F CRITICAL \u2014 PARALLELISM IS THE ONLY REASON LENSES EXIST. \u26A0\uFE0F
102066
- 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.
102092
+ 5. **dispatch specialists (only if step 4 found unresolved hypotheses)**: dispatch one \`${REVIEWER_AGENT_NAME}\` for one hypothesis. For 2+ hypotheses, emit every Task tool_use block **IN A SINGLE ASSISTANT TURN** before reading any result so the investigations run in parallel rather than serially.
102067
102093
 
102068
- \u2705 Right pattern: one assistant turn with N Task tool_use blocks \u2192 wait \u2192 N results arrive together \u2192 aggregate.
102069
- \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.
102094
+ \u2705 Right multi-specialist pattern: one assistant turn with N Task tool_use blocks \u2192 wait \u2192 N results arrive together \u2192 aggregate.
102095
+ \u274C Wrong multi-specialist pattern: Task(hypothesis A) \u2192 wait for A \u2192 Task(hypothesis B).
102070
102096
 
102071
102097
  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.
102072
102098
 
102073
- 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:
102099
+ if a specialist errors out, times out, or returns nothing usable, retry it once with the same hypothesis. if it still fails, attempt to resolve the hypothesis yourself; if it remains disposition-changing and unresolved, surface the limitation and do not approve. each specialist gets:
102074
102100
  - **the absolute \`diffPath\` (and \`incrementalDiffPath\` if available) from step 2's \`${t("checkout_pr")}\` return, named verbatim in the dispatch prompt** (e.g. \`diffPath: /tmp/pullfrog-XXXX/pr-NNN-SHA.diff\`). the reviewer's baked-in system prompt selects its FIRST action on this token \u2014 paraphrasing ("review the diff", "look at this PR") sends it down the \`git diff origin/<base>\` fallback, which fails on shallow GHA checkouts. the subagent \`read\`s those files for scope; it must NOT re-derive the diff via \`git diff\` (bare \`git diff origin/<base>\` is symmetric and pulls in the inverse of any commits that landed on \`<base>\` since the branch forked \u2014 pure noise, and the git tool rejects it). reading and codebase exploration are still its job.
102075
- - **only one lens** \u2014 never a multi-section "review for X, Y, and Z" prompt
102076
- - **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\`.
102101
+ - **exactly one falsifiable hypothesis with explicit scope boundaries** \u2014 ask for evidence that supports or refutes it, never a broad "review for X, Y, and Z" prompt
102102
+ - **a Task \`description\` set to a short hypothesis label** (e.g. \`"webhook-replay"\`, \`"billing-rounding"\`) \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\`.
102077
102103
  - 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."
102078
- - 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.
102104
+ - ask the subagent to report findings with file paths and NEW line numbers from the diff so you can validate and anchor them. you must still read the complete diff yourself.
102079
102105
 
102080
102106
  delegation discipline:
102081
- - do NOT summarize the PR for them (biases toward a validation frame)
102107
+ - do NOT summarize the PR for them (a lossy summary biases toward a validation frame; the raw diff is the source)
102082
102108
  - do NOT hand them a curated reading list (let them discover scope)
102083
102109
  - do NOT pre-shape their output with a finding schema
102084
102110
  - do NOT mention the other lenses (independence is the point \u2014 overlapping findings are a strong signal)
102085
102111
 
102086
- 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.
102112
+ 6. **aggregate & draft**: when specialist results land, merge findings; de-dup overlaps (two specialists 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.
102087
102113
 
102088
102114
  **Hunt for non-anchored concerns before drafting.** After collecting your anchored findings, deliberately scan for concerns that have no specific line to point at \u2014 typically: deletion / cleanup plans for code the diff replaces or shadows; rollout sequencing (what happens to in-flight state during deploy / revert?); coverage gaps the diff implies but doesn't add; scope questions that only the human can answer (e.g. is the legacy path going away or is this a long-term dual track?); architectural risks the diff opens up that aren't a single-line bug. On substantial PRs (migrations, refactors, multi-file rewrites, version bumps that change runtime semantics), at least one such concern almost always exists; if you can't think of any, your bar is probably too high.
102089
102115
 
@@ -102117,10 +102143,10 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
102117
102143
 
102118
102144
  ${PR_SUMMARY_FORMAT}`
102119
102145
  },
102120
- // IncrementalReview shares Review's 0-or-2+ lens pattern AND its body
102121
- // format (PR_SUMMARY_FORMAT), scoped to the incremental delta against the
102122
- // prior pullfrog review. The "issues must be NEW since the last Pullfrog
102123
- // review" filter lives at aggregation time (step 8), NOT in the subagent
102146
+ // IncrementalReview shares Review's minimum hypothesis-covering specialist
102147
+ // routing and body format, scoped to the incremental delta against the
102148
+ // prior Pullfrog review. The "issues must be NEW since the last Pullfrog
102149
+ // review" filter lives at aggregation time, NOT in the subagent
102124
102150
  // prompt — pushing the filter into subagents matches the canonical anneal
102125
102151
  // anti-pattern of "list known pre-existing failures — don't flag these"
102126
102152
  // and suppresses signal on regressions the new commits amplified. A
@@ -102135,9 +102161,9 @@ ${PR_SUMMARY_FORMAT}`
102135
102161
 
102136
102162
  1. **task list**: create your task list for this run as your first action.
102137
102163
 
102138
- 2. **checkout**: call \`${t("checkout_pr")}\` \u2014 this returns PR metadata, \`diffPath\` (full diff), and \`incrementalDiffPath\` (changes since last reviewed version, if available). read the diff TOC first and use its line ranges as your coverage checklist.
102164
+ 2. **checkout**: call \`${t("checkout_pr")}\` \u2014 this returns PR metadata, \`diffPath\` (full diff), \`incrementalDiffPath\` (changes since last reviewed version, if available), and a supplemental \`impactPath\` when change-impact extraction is enabled.
102139
102165
 
102140
- 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.
102166
+ 3. **incremental scope**: if \`incrementalDiffPath\` is present, read it FIRST to see what changed since the last review. this is a range-diff that isolates the net changes, filtering out base branch noise. then read the authoritative full diff end-to-end, beginning with the TOC and using its line ranges as your coverage checklist. if no incremental diff is present, start with the full-diff TOC, determine what changed since Pullfrog's most recent review, and complete the raw-diff read. only after establishing that authoritative scope and completing raw-diff coverage, use \`impactPath\` as an explicitly incomplete list of reference leads; it never replaces raw-diff reading or establishes coverage.
102141
102167
 
102142
102168
  4. **prior feedback \u2014 read AND retire it**: fetch previous reviews via \`${t("list_pull_request_reviews")}\`, then call \`${t("get_review_comments")}\` on each prior Pullfrog review. Each thread renders as a section whose first line is a fenced tag \`comment author=<login> id=<fullDatabaseId> review=<reviewId> thread=<graphqlId>\`; section headers carry \`[RESOLVED]\` / \`[OUTDATED]\` when relevant. For every **open, Pullfrog-originated** thread, decide and act:
102143
102169
 
@@ -102151,49 +102177,48 @@ ${PR_SUMMARY_FORMAT}`
102151
102177
 
102152
102178
  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.**
102153
102179
 
102154
- 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).
102180
+ if the incremental changes are **genuinely trivial**, skip specialists entirely and jump to step 10's non-substantive path (do NOT submit a review).
102155
102181
 
102156
102182
  "Genuinely trivial" (skip): formatting/comment tweaks, import reordering, lockfile regen, mechanical rename of import paths, whitespace-only.
102157
102183
  "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.
102158
102184
  When unsure, treat as non-trivial.
102159
102185
 
102160
- 6. **lens decision \u2014 0 or 2+, NEVER 1**.
102186
+ 6. **specialist decision \u2014 minimum hypothesis coverage**.
102161
102187
 
102162
- 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."
102188
+ After full-diff coverage and triage, identify the load-bearing questions about the incremental changes that you still cannot resolve confidently yourself. A specialist hypothesis is load-bearing only when its answer could yield an actionable new finding that changes the review disposition and warrants independent investigation, and falsifiable only when the specialist can return evidence that supports or refutes it. Generic requests for extra confidence, polish, or "another look" do not qualify.
102163
102189
 
102164
- Dispatch **2+ \`${REVIEWER_AGENT_NAME}\` lenses in parallel** ONLY when ALL of the following are true:
102165
- - 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)
102166
- - you can name 2+ distinct concrete failure modes the new commits plausibly introduce that warrant independent lenses
102167
- - parallel-orchestrated independent perspectives meaningfully outperform what you'd find solo
102190
+ Route the **minimum number of \`${REVIEWER_AGENT_NAME}\` specialists** needed to cover those unresolved hypotheses. Most incremental reviews need **0 or 1**, especially thread-reply re-reviews:
102191
+ - dispatch 0 when you can resolve every disposition-changing question directly
102192
+ - dispatch 1 when exactly one falsifiable, load-bearing hypothesis warrants independent investigation
102193
+ - dispatch 2+ in parallel when multiple orthogonal load-bearing hypotheses remain, or when the user explicitly requests an exhaustive or multi-angle review
102168
102194
 
102169
- **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.
102195
+ **There is NO one-specialist cap or fixed maximum.** Cover every orthogonal load-bearing hypothesis that remains; do not collapse multiple real questions into one broad prompt just to reduce the count. There is also no file-count, line-count, schema, quota, or hard-budget threshold \u2014 diff size is not a proxy for review uncertainty.
102170
102196
 
102171
- 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.
102197
+ The primary reviewer remains responsible for reading the complete raw full diff plus the incremental diff, investigating surrounding code, validating every returned finding, and synthesizing the final review. Specialist reads supplement that work; they never replace it or satisfy the primary's diff-coverage obligation.
102172
102198
 
102173
- 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.**
102199
+ Specialist hypotheses can draw on Review mode's themed or subsystem framings, but every dispatch must turn the framing into one falsifiable question with explicit scope boundaries.
102174
102200
 
102175
- \u26A0\uFE0F CRITICAL \u2014 PARALLELISM IS THE ONLY REASON LENSES EXIST. \u26A0\uFE0F
102176
- 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.
102201
+ 7. **dispatch specialists (only if step 6 found unresolved hypotheses)**: dispatch one \`${REVIEWER_AGENT_NAME}\` for one hypothesis. For 2+ hypotheses, emit every Task tool_use block **IN A SINGLE ASSISTANT TURN** before reading any result so the investigations run in parallel rather than serially.
102177
102202
 
102178
- \u2705 Right pattern: one assistant turn with N Task tool_use blocks \u2192 wait \u2192 N results arrive together \u2192 aggregate.
102179
- \u274C Wrong pattern: turn 1 = Task(lens A) \u2192 turn 2 (after A's result) = Task(lens B). This is the failure mode.
102203
+ \u2705 Right multi-specialist pattern: one assistant turn with N Task tool_use blocks \u2192 wait \u2192 N results arrive together \u2192 aggregate.
102204
+ \u274C Wrong multi-specialist pattern: Task(hypothesis A) \u2192 wait for A \u2192 Task(hypothesis B).
102180
102205
 
102181
102206
  You can also include your own \`read\` / \`grep\` / \`webfetch\` calls in the SAME turn as the parallel \`${REVIEWER_AGENT_NAME}\` dispatches.
102182
102207
 
102183
- 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:
102208
+ if a specialist errors out, times out, or returns nothing usable, retry it once with the same hypothesis. if it still fails, attempt to resolve the hypothesis yourself; if it remains disposition-changing and unresolved, surface the limitation and do not approve. each specialist gets:
102184
102209
  - **the absolute diff path(s) from step 2's \`${t("checkout_pr")}\` return, named verbatim in the dispatch prompt.** when \`incrementalDiffPath\` is present, name BOTH (\`incrementalDiffPath: /tmp/.../pr-NNN-SHA-incremental.diff\` then \`diffPath: /tmp/.../pr-NNN-SHA.diff\`) \u2014 the reviewer's baked-in prompt reads incremental first and uses full for context; when only \`diffPath\` exists, name it alone. the subagent \`read\`s those files; it must NOT re-derive via \`git diff\` (bare \`git diff origin/<base>\` is symmetric and pulls in the inverse of base-branch progress \u2014 pure noise, and the git tool rejects it), and paraphrasing ("review the new commits") sends it down that fallback, which also fails on shallow GHA checkouts. 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.
102185
- - **only one lens** \u2014 never a multi-section "review for X, Y, and Z" prompt
102186
- - **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.
102210
+ - **exactly one falsifiable hypothesis with explicit scope boundaries** \u2014 ask for evidence that supports or refutes it, never a broad "review for X, Y, and Z" prompt
102211
+ - **a Task \`description\` set to a short hypothesis label** \u2014 the harness reads this field to label log lines so parallel runs can be told apart.
102187
102212
  - if the lens touches external contracts, instruct the subagent to verify load-bearing claims via web search and quote source URLs.
102188
- - ask the subagent to report findings with file paths and NEW line numbers from the full PR diff so you can anchor inline comments.
102213
+ - ask the subagent to report findings with file paths and NEW line numbers from the full PR diff so you can validate and anchor them. you must still read the complete incremental and full diff scope yourself.
102189
102214
 
102190
102215
  delegation discipline:
102191
- - do NOT summarize the changes for them (biases toward validation frame)
102216
+ - do NOT summarize the changes for them (a lossy summary biases toward a validation frame; the raw diff is the source)
102192
102217
  - do NOT hand them a curated reading list (let them discover scope)
102193
102218
  - do NOT pre-shape their output with a finding schema
102194
102219
  - do NOT mention the other lenses (independence is the point)
102195
102220
 
102196
- 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 \`${t("list_pull_request_reviews")}\` in step 4) and run \`git diff <prior-review-sha>..HEAD\` to isolate the lines added since that review.
102221
+ 8. **aggregate, draft, self-critique**: merge findings (yours + output from every specialist you dispatched); 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 \`${t("list_pull_request_reviews")}\` in step 4) and run \`git diff <prior-review-sha>..HEAD\` to isolate the lines added since that review.
102197
102222
 
102198
102223
  **Hunt for non-anchored concerns before drafting.** After collecting your anchored findings, deliberately scan for concerns that have no specific line to point at \u2014 typically: deletion / cleanup plans for code the new commits replace or shadow; rollout sequencing (what happens to in-flight state during deploy / revert?); coverage gaps the new commits imply but don't add; scope questions that only the human can answer (e.g. is the legacy path going away or is this a long-term dual track?); architectural risks the new commits open up that aren't a single-line bug. On substantial incremental diffs (migrations, refactors, multi-file rewrites, version bumps that change runtime semantics), at least one such concern almost always exists; if you can't think of any, your bar is probably too high.
102199
102224
 
@@ -102290,7 +102315,7 @@ ${PR_SUMMARY_FORMAT}`
102290
102315
 
102291
102316
  1. **task list**: create your task list for this run as your first action.
102292
102317
 
102293
- 2. Analyze the task. For simple operations (labeling, commenting, answering questions, running a single command), handle directly \u2014 but your answer only reaches the user through \`${t("report_progress")}\` (step 4); raw assistant text is discarded.
102318
+ 2. Analyze the task. For simple operations (labeling, answering questions, running a single command), handle directly \u2014 but your answer only reaches the user through \`${t("report_progress")}\` (step 4); raw assistant text is discarded. If a standalone comment on the current issue/PR is the task's sole requested deliverable, create that comment directly and skip \`${t("report_progress")}\`.
102294
102319
 
102295
102320
  3. For substantial work \u2014 code changes across multiple files, multi-step investigations:
102296
102321
  - plan your approach before starting
@@ -102300,8 +102325,8 @@ ${PR_SUMMARY_FORMAT}`
102300
102325
 
102301
102326
  4. Finalize:
102302
102327
  - if code changes were made, get them onto a pull request (new or existing) using ${signedCommits ? `\`${t("commit_changes")}\`` : `\`${t("push_branch")}\``} and \`${t("create_pull_request")}\` as needed. \`git status\` must be clean before you finish (see *SYSTEM* Git rules if this fails).
102303
- - call \`${t("report_progress")}\` once with results \u2014 include exact tool errors if push or PR creation failed
102304
- - if the task involved labeling, commenting, or other GitHub operations, perform those directly`
102328
+ - call \`${t("report_progress")}\` once with results \u2014 include exact tool errors if push or PR creation failed. skip this only when a standalone comment on the current target was the task's sole requested deliverable
102329
+ - if the task involved labeling or other GitHub operations, perform those directly`
102305
102330
  }
102306
102331
  ];
102307
102332
  }
@@ -102856,7 +102881,7 @@ async function runClaude(params) {
102856
102881
  }
102857
102882
  } else if (block.type === "tool_use") {
102858
102883
  const toolName = block.name || "unknown";
102859
- if (params.onToolUse) {
102884
+ if (params.onToolUse && label === ORCHESTRATOR_LABEL) {
102860
102885
  params.onToolUse({
102861
102886
  toolName,
102862
102887
  input: block.input
@@ -103248,7 +103273,7 @@ var claude = agent({
103248
103273
  const cliPath = await installClaudeCli();
103249
103274
  const specifier = ctx.payload.proxyModel ?? ctx.resolvedModel;
103250
103275
  const bedrockModelId = process.env[BEDROCK_MODEL_ID_ENV]?.trim();
103251
- const isBedrockRoute = specifier !== void 0 && bedrockModelId !== void 0 && bedrockModelId === specifier && isBedrockAnthropicId(specifier);
103276
+ const isBedrockRoute = specifier !== void 0 && bedrockModelId !== void 0 && bedrockModelId === specifier;
103252
103277
  const vertexModelId = process.env[VERTEX_MODEL_ID_ENV]?.trim();
103253
103278
  const isVertexRoute2 = specifier !== void 0 && vertexModelId !== void 0 && vertexModelId === specifier && isVertexAnthropicId(specifier);
103254
103279
  const model = !specifier ? void 0 : isBedrockRoute ? specifier : isVertexRoute2 ? void 0 : stripProviderPrefix(specifier);
@@ -103296,6 +103321,7 @@ var claude = agent({
103296
103321
  ...homeEnv,
103297
103322
  PWD: repoDir
103298
103323
  };
103324
+ env2.CLAUDE_CODE_AUTO_COMPACT_WINDOW ||= "500000";
103299
103325
  if (isBedrockRoute) {
103300
103326
  env2.CLAUDE_CODE_USE_BEDROCK = "1";
103301
103327
  }
@@ -142247,8 +142273,8 @@ function closeBrowserDaemon(toolState) {
142247
142273
 
142248
142274
  // mcp/checkout.ts
142249
142275
  import { createHash as createHash2 } from "node:crypto";
142250
- import { statSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync7 } from "node:fs";
142251
- import { join as join12 } from "node:path";
142276
+ import { statSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync8 } from "node:fs";
142277
+ import { join as join13 } from "node:path";
142252
142278
 
142253
142279
  // node_modules/.pnpm/@ark+schema@0.56.0/node_modules/@ark/schema/out/shared/errors.js
142254
142280
  var ArkError = class _ArkError extends CastableBase {
@@ -149717,6 +149743,367 @@ function ensureRepoState(toolState, init) {
149717
149743
  return created;
149718
149744
  }
149719
149745
 
149746
+ // utils/changeImpact.ts
149747
+ import { spawn as spawn2 } from "node:child_process";
149748
+ import { writeFileSync as writeFileSync6 } from "node:fs";
149749
+ import { join as join7 } from "node:path";
149750
+ var MAX_CANDIDATES = 24;
149751
+ var MAX_ATOM_LENGTH = 128;
149752
+ var MAX_ATOMS = 12;
149753
+ var MAX_REFERENCES = 6;
149754
+ var MAX_EXCERPT = 160;
149755
+ var MAX_BYTES = 16e3;
149756
+ function isSymbolShaped(atom) {
149757
+ return atom.length >= 4 && atom.length <= MAX_ATOM_LENGTH && /[A-Z_]/.test(atom);
149758
+ }
149759
+ function getAtomConfidence(params) {
149760
+ const before = params.text.slice(Math.max(0, params.index - 64), params.index);
149761
+ const after = params.text.slice(
149762
+ params.index + params.atom.length,
149763
+ params.index + params.atom.length + 16
149764
+ );
149765
+ if (/\b(?:class|interface|type|enum|function|def|func|fn|struct|trait)\s+$/.test(before))
149766
+ return 4;
149767
+ if (/\b(?:const|let|var)\s+$/.test(before) || /^\s*\??\s*[:(]/.test(after)) return 3;
149768
+ const quoted = /['"`]$/.test(before) && /^['"`]/.test(after);
149769
+ if (quoted || before.endsWith(".") || after.startsWith(".") || params.atom.includes("_"))
149770
+ return 2;
149771
+ return 1;
149772
+ }
149773
+ function getChangedAtoms(text) {
149774
+ const atoms = /* @__PURE__ */ new Map();
149775
+ for (const match3 of text.matchAll(/[A-Za-z_][A-Za-z0-9_]*/g)) {
149776
+ const atom = match3[0];
149777
+ if (!isSymbolShaped(atom)) continue;
149778
+ const confidence = getAtomConfidence({ text, atom, index: match3.index ?? 0 });
149779
+ atoms.set(atom, Math.max(atoms.get(atom) ?? 0, confidence));
149780
+ }
149781
+ return atoms;
149782
+ }
149783
+ function recordChange(params) {
149784
+ for (const [atom, confidence] of getChangedAtoms(params.text)) {
149785
+ const change = params.changes.get(atom) ?? { atom, confidence, added: [], removed: [] };
149786
+ change.confidence = Math.max(change.confidence, confidence);
149787
+ change[params.location.kind].push(params.location);
149788
+ params.changes.set(atom, change);
149789
+ }
149790
+ }
149791
+ function locationKey(location) {
149792
+ return `${location.path}\0${location.line}`;
149793
+ }
149794
+ function collectFileChanges(collection, file2) {
149795
+ if (!file2.patch) return;
149796
+ let oldLine = 0;
149797
+ let newLine = 0;
149798
+ for (const patchLine of file2.patch.split("\n")) {
149799
+ const hunk = patchLine.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
149800
+ if (hunk) {
149801
+ oldLine = Number.parseInt(hunk[1], 10);
149802
+ newLine = Number.parseInt(hunk[2], 10);
149803
+ } else if (patchLine.startsWith("+")) {
149804
+ const location = { path: file2.filename, line: newLine++, kind: "added" };
149805
+ collection.addedLines.add(locationKey(location));
149806
+ recordChange({ changes: collection.changes, location, text: patchLine.slice(1) });
149807
+ } else if (patchLine.startsWith("-")) {
149808
+ const oldPath = file2.previous_filename ?? file2.filename;
149809
+ recordChange({
149810
+ changes: collection.changes,
149811
+ location: { path: oldPath, line: oldLine++, kind: "removed" },
149812
+ text: patchLine.slice(1)
149813
+ });
149814
+ } else if (!patchLine.startsWith("\\")) {
149815
+ oldLine++;
149816
+ newLine++;
149817
+ }
149818
+ }
149819
+ }
149820
+ function compareChanges(a, b) {
149821
+ if (a.confidence !== b.confidence) return b.confidence - a.confidence;
149822
+ const removedDelta = Number(b.removed.length > 0) - Number(a.removed.length > 0);
149823
+ if (removedDelta !== 0) return removedDelta;
149824
+ const aDelta = Math.abs(a.added.length - a.removed.length);
149825
+ const bDelta = Math.abs(b.added.length - b.removed.length);
149826
+ if (aDelta !== bDelta) return bDelta - aDelta;
149827
+ if (a.atom.length !== b.atom.length) return b.atom.length - a.atom.length;
149828
+ return a.atom < b.atom ? -1 : Number(a.atom > b.atom);
149829
+ }
149830
+ function getCandidates(files) {
149831
+ const collection = {
149832
+ changes: /* @__PURE__ */ new Map(),
149833
+ addedLines: /* @__PURE__ */ new Set()
149834
+ };
149835
+ for (const file2 of files) collectFileChanges(collection, file2);
149836
+ const candidates = [...collection.changes.values()].filter((change) => change.added.length !== change.removed.length).sort(compareChanges);
149837
+ return {
149838
+ selected: candidates.slice(0, MAX_CANDIDATES),
149839
+ total: candidates.length,
149840
+ addedLines: collection.addedLines
149841
+ };
149842
+ }
149843
+ function emptyBuckets(candidates) {
149844
+ return new Map(candidates.map((candidate) => [candidate.atom, { count: 0, samples: [] }]));
149845
+ }
149846
+ function createGrepRecordState() {
149847
+ return {
149848
+ phase: "path",
149849
+ path: "",
149850
+ line: "",
149851
+ sample: "",
149852
+ truncated: false,
149853
+ searchTail: "",
149854
+ searchStart: 0,
149855
+ matches: /* @__PURE__ */ new Set()
149856
+ };
149857
+ }
149858
+ function resetGrepRecord(state) {
149859
+ state.phase = "path";
149860
+ state.path = "";
149861
+ state.line = "";
149862
+ state.sample = "";
149863
+ state.truncated = false;
149864
+ state.searchTail = "";
149865
+ state.searchStart = 0;
149866
+ state.matches.clear();
149867
+ }
149868
+ function isWordCharacter(value2) {
149869
+ return value2 !== void 0 && /[A-Za-z0-9_]/.test(value2);
149870
+ }
149871
+ function recordCandidateMatches(params) {
149872
+ for (const candidate of params.candidates) {
149873
+ if (params.matches.has(candidate.atom)) continue;
149874
+ let index = params.text.indexOf(candidate.atom, params.minStart);
149875
+ while (index >= 0 && index < params.maxStart) {
149876
+ const before = params.text[index - 1];
149877
+ const after = params.text[index + candidate.atom.length];
149878
+ if (!isWordCharacter(before) && !isWordCharacter(after)) {
149879
+ params.matches.add(candidate.atom);
149880
+ break;
149881
+ }
149882
+ index = params.text.indexOf(candidate.atom, index + 1);
149883
+ }
149884
+ }
149885
+ }
149886
+ function consumeReferenceText(params) {
149887
+ const remaining = MAX_EXCERPT - params.state.sample.length;
149888
+ params.state.sample += params.text.slice(0, Math.max(remaining, 0));
149889
+ if (params.text.length > remaining) params.state.truncated = true;
149890
+ const searchable = params.state.searchTail + params.text;
149891
+ const consumed = params.final ? searchable.length : Math.max(0, searchable.length - params.maxAtomLength - 2);
149892
+ recordCandidateMatches({
149893
+ text: searchable,
149894
+ minStart: params.state.searchStart,
149895
+ maxStart: consumed,
149896
+ candidates: params.candidates,
149897
+ matches: params.state.matches
149898
+ });
149899
+ if (params.final) {
149900
+ params.state.searchTail = "";
149901
+ params.state.searchStart = 0;
149902
+ return;
149903
+ }
149904
+ const tailStart = Math.max(0, consumed - 1);
149905
+ params.state.searchTail = searchable.slice(tailStart);
149906
+ params.state.searchStart = consumed > 0 ? 1 : params.state.searchStart;
149907
+ }
149908
+ function recordGrepReference(params) {
149909
+ const prefix = `${params.treeish}:`;
149910
+ const path4 = params.state.path.startsWith(prefix) ? params.state.path.slice(prefix.length) : params.state.path;
149911
+ const line = Number.parseInt(params.state.line, 10);
149912
+ if (!Number.isFinite(line)) return;
149913
+ const reference2 = {
149914
+ path: path4,
149915
+ line,
149916
+ text: params.state.sample,
149917
+ truncated: params.state.truncated
149918
+ };
149919
+ if (params.addedLines.has(locationKey(reference2))) return;
149920
+ for (const atom of params.state.matches) {
149921
+ const bucket = params.buckets.get(atom);
149922
+ if (!bucket) continue;
149923
+ bucket.count++;
149924
+ if (bucket.samples.length < MAX_REFERENCES) bucket.samples.push(reference2);
149925
+ }
149926
+ }
149927
+ function consumeGrepChunk(params) {
149928
+ let offset = 0;
149929
+ while (offset < params.chunk.length) {
149930
+ if (params.state.phase !== "text") {
149931
+ const end2 = params.chunk.indexOf("\0", offset);
149932
+ const value2 = end2 < 0 ? params.chunk.slice(offset) : params.chunk.slice(offset, end2);
149933
+ params.state[params.state.phase] += value2;
149934
+ if (end2 < 0) return;
149935
+ params.state.phase = params.state.phase === "path" ? "line" : "text";
149936
+ offset = end2 + 1;
149937
+ continue;
149938
+ }
149939
+ const end = params.chunk.indexOf("\n", offset);
149940
+ const final = end >= 0;
149941
+ const text = final ? params.chunk.slice(offset, end) : params.chunk.slice(offset);
149942
+ consumeReferenceText({
149943
+ state: params.state,
149944
+ text,
149945
+ candidates: params.candidates,
149946
+ maxAtomLength: params.maxAtomLength,
149947
+ final
149948
+ });
149949
+ if (!final) return;
149950
+ recordGrepReference(params);
149951
+ resetGrepRecord(params.state);
149952
+ offset = end + 1;
149953
+ }
149954
+ }
149955
+ async function lookupReferences(params) {
149956
+ const buckets = emptyBuckets(params.candidates);
149957
+ if (params.candidates.length === 0) return { buckets, error: void 0 };
149958
+ const patterns = params.candidates.flatMap((candidate) => ["-e", candidate.atom]);
149959
+ const child = spawn2(
149960
+ "git",
149961
+ [
149962
+ "grep",
149963
+ "--no-color",
149964
+ "--full-name",
149965
+ "-n",
149966
+ "-z",
149967
+ "-I",
149968
+ "-w",
149969
+ "-F",
149970
+ ...patterns,
149971
+ params.treeish,
149972
+ "--",
149973
+ ":(top)"
149974
+ ],
149975
+ { env: resolveEnv(void 0), stdio: ["ignore", "pipe", "pipe"] }
149976
+ );
149977
+ const state = createGrepRecordState();
149978
+ const maxAtomLength = Math.max(...params.candidates.map((candidate) => candidate.atom.length));
149979
+ let spawnError;
149980
+ child.stdout.setEncoding("utf8");
149981
+ child.stdout.on("data", (chunk) => {
149982
+ consumeGrepChunk({
149983
+ chunk,
149984
+ state,
149985
+ candidates: params.candidates,
149986
+ maxAtomLength,
149987
+ buckets,
149988
+ addedLines: params.addedLines,
149989
+ treeish: params.treeish
149990
+ });
149991
+ });
149992
+ child.stderr.resume();
149993
+ const status = await new Promise((resolve3) => {
149994
+ child.once("error", (error49) => {
149995
+ spawnError = error49.message;
149996
+ resolve3(null);
149997
+ });
149998
+ child.once("close", resolve3);
149999
+ });
150000
+ if (spawnError || status !== 0 && status !== 1) {
150001
+ const error49 = spawnError ?? `git grep exit ${status ?? "unknown"}`;
150002
+ log.warning(`\xBB change impact lookup failed: ${error49}`);
150003
+ return { buckets: emptyBuckets(params.candidates), error: error49 };
150004
+ }
150005
+ if (state.phase === "text") {
150006
+ consumeReferenceText({
150007
+ state,
150008
+ text: "",
150009
+ candidates: params.candidates,
150010
+ maxAtomLength,
150011
+ final: true
150012
+ });
150013
+ recordGrepReference({ state, buckets, addedLines: params.addedLines, treeish: params.treeish });
150014
+ } else if (state.phase !== "path" || state.path) {
150015
+ const error49 = "malformed git grep output";
150016
+ log.warning(`\xBB change impact lookup failed: ${error49}`);
150017
+ return { buckets: emptyBuckets(params.candidates), error: error49 };
150018
+ }
150019
+ return { buckets, error: void 0 };
150020
+ }
150021
+ function formatReference(reference2) {
150022
+ const text = reference2.text.trim();
150023
+ const excerpt = reference2.truncated ? `${text.slice(0, MAX_EXCERPT - 1)}\u2026` : text;
150024
+ return `- ${JSON.stringify(reference2.path)}:${reference2.line} ${JSON.stringify(excerpt)}`;
150025
+ }
150026
+ function formatChangedLocations(change) {
150027
+ const selected = [change.removed[0], change.added[0]].filter(
150028
+ (location) => location !== void 0
150029
+ );
150030
+ const rendered = selected.map((location) => `${location.kind} ${JSON.stringify(location.path)}:${location.line}`).join(", ");
150031
+ const omitted = change.removed.length + change.added.length - selected.length;
150032
+ return omitted > 0 ? `${rendered}; ${omitted} more changed location(s) omitted` : rendered;
150033
+ }
150034
+ function formatEntry(entry) {
150035
+ const count = entry.bucket.count;
150036
+ const sampleNote = count > entry.bucket.samples.length ? `${count} matched line(s); first ${entry.bucket.samples.length} shown.` : `${count} matched line(s).`;
150037
+ return [
150038
+ `## \`${entry.change.atom}\``,
150039
+ "",
150040
+ `Changed at: ${formatChangedLocations(entry.change)}`,
150041
+ `Tracked references: ${sampleNote}`,
150042
+ "",
150043
+ ...entry.bucket.samples.map(formatReference)
150044
+ ].join("\n");
150045
+ }
150046
+ function formatArtifact(params) {
150047
+ const lines = [
150048
+ "# Change impact leads",
150049
+ "",
150050
+ "> Supplemental and explicitly incomplete. Read the authoritative `diffPath` TOC and raw lines first. Absence here is not evidence of no impact.",
150051
+ "",
150052
+ `Scope: base ${params.baseSha}, head ${params.headSha}; ${params.fileCount} changed file(s).`,
150053
+ "Generated from changed symbol-shaped identifiers, then one exact-word lookup pinned to the checked-out head across Git-tracked text files. Changed added lines are excluded; snippets are capped at 160 characters.",
150054
+ "",
150055
+ `Candidate atoms searched: ${params.candidateCount}/${params.totalCandidateCount}.`
150056
+ ];
150057
+ if (params.lookupError) lines.push(`Tracked-file lookup unavailable: ${params.lookupError}.`);
150058
+ let content = `${lines.join("\n")}
150059
+ `;
150060
+ let renderedAtomCount = 0;
150061
+ let referenceCount = 0;
150062
+ for (const entry of params.entries.filter((candidate) => candidate.bucket.count > 0)) {
150063
+ if (renderedAtomCount >= MAX_ATOMS) break;
150064
+ const addition = `
150065
+ ${formatEntry(entry)}
150066
+ `;
150067
+ if (Buffer.byteLength(content + addition) > MAX_BYTES) break;
150068
+ content += addition;
150069
+ renderedAtomCount++;
150070
+ referenceCount += entry.bucket.count;
150071
+ }
150072
+ if (renderedAtomCount === 0) {
150073
+ const empty = "\nNo bounded tracked references found. This is not evidence of no wider impact.\n";
150074
+ if (Buffer.byteLength(content + empty) <= MAX_BYTES) content += empty;
150075
+ }
150076
+ return { content, renderedAtomCount, referenceCount };
150077
+ }
150078
+ async function createChangeImpactArtifact(ctx, params) {
150079
+ const candidates = getCandidates(params.files);
150080
+ const lookup2 = await lookupReferences({
150081
+ candidates: candidates.selected,
150082
+ addedLines: candidates.addedLines,
150083
+ treeish: params.headSha
150084
+ });
150085
+ const artifact = formatArtifact({
150086
+ entries: candidates.selected.map((change) => {
150087
+ return { change, bucket: lookup2.buckets.get(change.atom) ?? { count: 0, samples: [] } };
150088
+ }),
150089
+ candidateCount: candidates.selected.length,
150090
+ totalCandidateCount: candidates.total,
150091
+ fileCount: params.files.length,
150092
+ baseSha: params.baseSha,
150093
+ headSha: params.headSha,
150094
+ lookupError: lookup2.error
150095
+ });
150096
+ const path4 = join7(ctx.tmpdir, `pr-${params.pullNumber}-${params.headSha.slice(0, 7)}-impact.md`);
150097
+ writeFileSync6(path4, artifact.content);
150098
+ return {
150099
+ path: path4,
150100
+ candidateCount: candidates.selected.length,
150101
+ renderedAtomCount: artifact.renderedAtomCount,
150102
+ referenceCount: artifact.referenceCount,
150103
+ bytes: Buffer.byteLength(artifact.content)
150104
+ };
150105
+ }
150106
+
149720
150107
  // utils/diffCoverage.ts
149721
150108
  import { isAbsolute, normalize as normalize2, resolve } from "node:path";
149722
150109
  function countLines(params) {
@@ -149804,27 +150191,6 @@ function getDiffCoverageBreakdown(params) {
149804
150191
  files
149805
150192
  };
149806
150193
  }
149807
- function renderDiffCoverageBreakdown(params) {
149808
- const breakdown = params.breakdown;
149809
- const lines = [];
149810
- lines.push(`diff coverage report for \`${params.diffPath}\``);
149811
- lines.push(
149812
- `overall: ${breakdown.coveredLines}/${breakdown.totalLines} lines read (${breakdown.coveragePercent}%), unread: ${breakdown.unreadLines}`
149813
- );
149814
- lines.push(`covered ranges: ${formatRanges({ ranges: breakdown.coveredRanges })}`);
149815
- lines.push(`unread ranges: ${formatRanges({ ranges: breakdown.unreadRanges })}`);
149816
- lines.push("");
149817
- lines.push("per-file TOC coverage:");
149818
- for (const file2 of breakdown.files) {
149819
- const filePercent = file2.totalLines ? Number((file2.coveredLines / file2.totalLines * 100).toFixed(2)) : 100;
149820
- lines.push(
149821
- `- ${file2.filename} (toc lines ${file2.startLine}-${file2.endLine}): ${file2.coveredLines}/${file2.totalLines} lines read (${filePercent}%)`
149822
- );
149823
- lines.push(` read: ${formatRanges({ ranges: file2.coveredRanges })}`);
149824
- lines.push(` unread: ${formatRanges({ ranges: file2.unreadRanges })}`);
149825
- }
149826
- return lines.join("\n");
149827
- }
149828
150194
  function resolveOffsetBase(params) {
149829
150195
  const lower2 = params.toolName.toLowerCase();
149830
150196
  if (lower2 === "readfile" || lower2.endsWith(".readfile")) {
@@ -149955,10 +150321,6 @@ function countLinesInRanges(params) {
149955
150321
  }
149956
150322
  return total;
149957
150323
  }
149958
- function formatRanges(params) {
149959
- if (params.ranges.length === 0) return "none";
149960
- return params.ranges.map((range2) => `${range2.startLine}-${range2.endLine}`).join(", ");
149961
- }
149962
150324
  function clampLine(params) {
149963
150325
  if (params.value < 1) return 1;
149964
150326
  if (params.value > params.totalLines) return params.totalLines;
@@ -149985,7 +150347,7 @@ function readNumber(params) {
149985
150347
  import { execSync } from "node:child_process";
149986
150348
  import { createHash } from "node:crypto";
149987
150349
  import { readFileSync as readFileSync2, realpathSync, unlinkSync } from "node:fs";
149988
- import { join as join7 } from "node:path";
150350
+ import { join as join8 } from "node:path";
149989
150351
 
149990
150352
  // utils/shell.ts
149991
150353
  import { spawnSync as spawnSync4 } from "node:child_process";
@@ -150063,7 +150425,7 @@ function resolveHooksDir(cwd, gitPath) {
150063
150425
  cwd,
150064
150426
  log: false
150065
150427
  }).trim();
150066
- const hooksDir = join7(commonDir, "hooks");
150428
+ const hooksDir = join8(commonDir, "hooks");
150067
150429
  hooksDirCache.set(cwd, hooksDir);
150068
150430
  return hooksDir;
150069
150431
  }
@@ -151211,13 +151573,13 @@ function _op(fn2, options) {
151211
151573
 
151212
151574
  // mcp/git.ts
151213
151575
  import { randomUUID as randomUUID3 } from "node:crypto";
151214
- import { writeFileSync as writeFileSync6 } from "node:fs";
151215
- import { join as join10 } from "node:path";
151576
+ import { writeFileSync as writeFileSync7 } from "node:fs";
151577
+ import { join as join11 } from "node:path";
151216
151578
 
151217
151579
  // utils/apiCommit.ts
151218
151580
  import { execFileSync as execFileSync6 } from "node:child_process";
151219
151581
  import { lstat, readlink } from "node:fs/promises";
151220
- import { join as join8 } from "node:path";
151582
+ import { join as join9 } from "node:path";
151221
151583
  var GITHUB_API = "https://api.github.com";
151222
151584
  var MAX_BLOB_BYTES = 30 * 1024 * 1024;
151223
151585
  var BLOB_UPLOAD_CONCURRENCY = 8;
@@ -151282,7 +151644,7 @@ async function assertApiCommittable(files) {
151282
151644
  const root = getRepoRoot();
151283
151645
  const attrs = $(
151284
151646
  "git",
151285
- ["check-attr", "filter", "-z", "--", ...present.map((p) => join8(root, p))],
151647
+ ["check-attr", "filter", "-z", "--", ...present.map((p) => join9(root, p))],
151286
151648
  { log: false }
151287
151649
  );
151288
151650
  const parts = attrs.split("\0");
@@ -151294,7 +151656,7 @@ async function assertApiCommittable(files) {
151294
151656
  }
151295
151657
  }
151296
151658
  for (const path4 of present) {
151297
- const stat = await lstat(join8(root, path4));
151659
+ const stat = await lstat(join9(root, path4));
151298
151660
  if (stat.isDirectory()) {
151299
151661
  throw new Error(
151300
151662
  `'${path4}' is a directory (nested repository or submodule?) \u2014 signed commits only support files and symlinks.`
@@ -151303,7 +151665,7 @@ async function assertApiCommittable(files) {
151303
151665
  }
151304
151666
  }
151305
151667
  async function createBlobEntry(params) {
151306
- const absPath = join8(params.repoRoot, params.path);
151668
+ const absPath = join9(params.repoRoot, params.path);
151307
151669
  const stat = await lstat(absPath);
151308
151670
  if (stat.size > MAX_BLOB_BYTES) {
151309
151671
  throw new Error(
@@ -151483,7 +151845,7 @@ this usually means your local branch has commits that were never pushed \u2014 s
151483
151845
  var core3 = __toESM(require_core(), 1);
151484
151846
  import { createSign } from "node:crypto";
151485
151847
  import { rename, writeFile } from "node:fs/promises";
151486
- import { dirname as dirname3, join as join9 } from "node:path";
151848
+ import { dirname as dirname3, join as join10 } from "node:path";
151487
151849
 
151488
151850
  // node_modules/.pnpm/@octokit+plugin-throttling@11.0.3_@octokit+core@7.0.5/node_modules/@octokit/plugin-throttling/dist-bundle/index.js
151489
151851
  var import_light = __toESM(require_light(), 1);
@@ -155427,7 +155789,7 @@ function getGitHubUsageSummary() {
155427
155789
  }
155428
155790
  async function writeGitHubUsageSummaryToFile(path4) {
155429
155791
  const summary2 = getGitHubUsageSummary();
155430
- const tmpPath = join9(dirname3(path4), `.usage-summary-${process.pid}.tmp`);
155792
+ const tmpPath = join10(dirname3(path4), `.usage-summary-${process.pid}.tmp`);
155431
155793
  await writeFile(tmpPath, JSON.stringify(summary2));
155432
155794
  await rename(tmpPath, path4);
155433
155795
  }
@@ -156554,8 +156916,8 @@ function countAhead(head, base, cwd) {
156554
156916
  function spillGitOutput(params) {
156555
156917
  const tempDir = process.env.PULLFROG_TEMP_DIR;
156556
156918
  if (!tempDir) throw new Error("PULLFROG_TEMP_DIR not set");
156557
- const outputPath = join10(tempDir, `git-${params.command}-${randomUUID3().slice(0, 8)}.txt`);
156558
- writeFileSync6(outputPath, params.output);
156919
+ const outputPath = join11(tempDir, `git-${params.command}-${randomUUID3().slice(0, 8)}.txt`);
156920
+ writeFileSync7(outputPath, params.output);
156559
156921
  const previewByLines = params.output.split("\n").slice(0, OVERFLOW_PREVIEW_LINES).join("\n");
156560
156922
  const preview = previewByLines.length <= OVERFLOW_PREVIEW_MAX_CHARS ? previewByLines : `${previewByLines.slice(0, OVERFLOW_PREVIEW_MAX_CHARS)}\u2026`;
156561
156923
  log.info(
@@ -157187,13 +157549,15 @@ function addFooter(ctx, body) {
157187
157549
  var Comment = type({
157188
157550
  issueNumber: type.number.describe("the issue number to comment on"),
157189
157551
  body: type.string.describe("the comment body content"),
157190
- type: type.enumerated("Plan", "Comment").describe("Plan: record as the plan for this run. Comment: regular comment (default).").optional()
157552
+ type: type.enumerated("Plan", "Comment").describe(
157553
+ "Plan: standalone plan comment on another target. Comment: regular comment (default)."
157554
+ ).optional()
157191
157555
  });
157192
157556
  function CreateCommentTool(ctx) {
157193
157557
  return tool({
157194
157558
  name: "create_issue_comment",
157195
157559
  mutates: true,
157196
- 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 \u2014 plan output (initial post AND revisions) is always posted via report_progress, never via this tool.',
157560
+ description: "Create a comment on a GitHub issue or PR. Example: `create_issue_comment({ issueNumber: 1234, body: \"Thanks for the report.\" })`. For the current run's answer, progress, or plan use report_progress instead. Use this on the current target only when the task explicitly requests a standalone comment. Skip report_progress only when that current-target comment is the task's sole requested deliverable.",
157197
157561
  parameters: Comment,
157198
157562
  execute: execute(async ({ issueNumber, body, type: commentType }) => {
157199
157563
  const bodyWithFooter = addFooter(ctx, body);
@@ -157205,9 +157569,6 @@ function CreateCommentTool(ctx) {
157205
157569
  });
157206
157570
  ctx.toolState.wasUpdated = true;
157207
157571
  log.info(`\xBB created comment ${result.data.id}`);
157208
- if (commentType !== "Plan" && issueNumber === ctx.payload.event.issue_number) {
157209
- ctx.toolState.answerCommentPosted = true;
157210
- }
157211
157572
  if (commentType === "Plan") {
157212
157573
  if (result.data.node_id) {
157213
157574
  await patchWorkflowRunFields(ctx, { planCommentNodeId: result.data.node_id });
@@ -158013,10 +158374,6 @@ function runDiffCoveragePreflight(params) {
158013
158374
  unread.push({ path: file2.filename, ranges: rangesText, unreadLines: fileUnreadLines });
158014
158375
  unreadLines += fileUnreadLines;
158015
158376
  }
158016
- coverageState.lastBreakdown = renderDiffCoverageBreakdown({
158017
- diffPath: coverageState.diffPath,
158018
- breakdown
158019
- });
158020
158377
  log.debug(
158021
158378
  `diff coverage pre-flight breakdown: coveredLines=${breakdown.coveredLines}, unreadLines=${unreadLines}`
158022
158379
  );
@@ -158032,9 +158389,7 @@ function runDiffCoveragePreflight(params) {
158032
158389
  `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.
158033
158390
 
158034
158391
  unread TOC regions:
158035
- ${unreadText}
158036
-
158037
- ${coverageState.lastBreakdown}`
158392
+ ${unreadText}`
158038
158393
  );
158039
158394
  }
158040
158395
  async function clearStrandedPendingReview(ctx, params) {
@@ -158156,9 +158511,9 @@ async function reportReviewNodeId(ctx, params) {
158156
158511
  import { execFileSync as execFileSync7, execSync as execSync2 } from "node:child_process";
158157
158512
  import { mkdtempSync, readdirSync, realpathSync as realpathSync2, unlinkSync as unlinkSync2 } from "node:fs";
158158
158513
  import { tmpdir as tmpdir2 } from "node:os";
158159
- import { join as join11 } from "node:path";
158514
+ import { join as join12 } from "node:path";
158160
158515
  function createTempDirectory() {
158161
- const sharedTempDir = mkdtempSync(join11(tmpdir2(), "pullfrog-"));
158516
+ const sharedTempDir = mkdtempSync(join12(tmpdir2(), "pullfrog-"));
158162
158517
  process.env.PULLFROG_TEMP_DIR = sharedTempDir;
158163
158518
  log.info(`\xBB created temp dir at ${sharedTempDir}`);
158164
158519
  return sharedTempDir;
@@ -158203,13 +158558,13 @@ function wipeRunnerLeakSurface() {
158203
158558
  return [];
158204
158559
  }
158205
158560
  };
158206
- const fileCommandsDir = join11(runnerTemp, "_runner_file_commands");
158561
+ const fileCommandsDir = join12(runnerTemp, "_runner_file_commands");
158207
158562
  for (const entry of listDir(fileCommandsDir)) {
158208
- tryUnlink(join11(fileCommandsDir, entry));
158563
+ tryUnlink(join12(fileCommandsDir, entry));
158209
158564
  }
158210
158565
  for (const entry of listDir(runnerTemp)) {
158211
158566
  if (entry.endsWith(".sh") || /^git-credentials-.*\.config$/.test(entry)) {
158212
- tryUnlink(join11(runnerTemp, entry));
158567
+ tryUnlink(join12(runnerTemp, entry));
158213
158568
  }
158214
158569
  }
158215
158570
  if (wiped.length > 0) {
@@ -158715,21 +159070,23 @@ function CheckoutPrTool(ctx) {
158715
159070
  "PULLFROG_TEMP_DIR not set - checkout_pr must run in pullfrog action context"
158716
159071
  );
158717
159072
  }
158718
- const headShort = primary.checkoutSha.slice(0, 7);
159073
+ const checkoutSha = primary.checkoutSha;
159074
+ if (!checkoutSha) throw new Error("checkout completed without a resolved HEAD SHA");
159075
+ const headShort = checkoutSha.slice(0, 7);
158719
159076
  let incrementalDiffPath;
158720
- if (primary.beforeSha && primary.checkoutSha) {
159077
+ if (primary.beforeSha) {
158721
159078
  const beforeShort = primary.beforeSha.slice(0, 7);
158722
159079
  const incremental = computeIncrementalDiff({
158723
159080
  baseBranch: pr.baseRef,
158724
159081
  beforeSha: primary.beforeSha,
158725
- headSha: primary.checkoutSha
159082
+ headSha: checkoutSha
158726
159083
  });
158727
159084
  if (incremental) {
158728
- incrementalDiffPath = join12(
159085
+ incrementalDiffPath = join13(
158729
159086
  tempDir,
158730
159087
  `pr-${pull_number}-${beforeShort}-${headShort}-incremental.diff`
158731
159088
  );
158732
- writeFileSync7(incrementalDiffPath, incremental);
159089
+ writeFileSync8(incrementalDiffPath, incremental);
158733
159090
  log.info(
158734
159091
  `\xBB incremental diff computed (${incremental.length} bytes) \u2192 ${incrementalDiffPath}`
158735
159092
  );
@@ -158739,8 +159096,8 @@ function CheckoutPrTool(ctx) {
158739
159096
  const diffPreview = formatResult.content.split("\n").slice(0, 100).join("\n");
158740
159097
  log.debug(`formatted diff preview (first 100 lines):
158741
159098
  ${diffPreview}`);
158742
- const diffPath = join12(tempDir, `pr-${pull_number}-${headShort}.diff`);
158743
- writeFileSync7(diffPath, formatResult.content);
159099
+ const diffPath = join13(tempDir, `pr-${pull_number}-${headShort}.diff`);
159100
+ writeFileSync8(diffPath, formatResult.content);
158744
159101
  log.debug(`wrote diff to ${diffPath} (${formatResult.content.length} bytes)`);
158745
159102
  primary.diffCoverage = createDiffCoverageState({
158746
159103
  diffPath,
@@ -158751,14 +159108,56 @@ ${diffPreview}`);
158751
159108
  log.debug(
158752
159109
  `\xBB diff coverage initialized: diffPath=${diffPath}, totalLines=${primary.diffCoverage.totalLines}, tocEntries=${primary.diffCoverage.tocEntries.length}`
158753
159110
  );
159111
+ let impactPath;
159112
+ if (process.env.PULLFROG_DISABLE_CHANGE_IMPACT === "1") {
159113
+ log.info("\xBB change impact disabled by PULLFROG_DISABLE_CHANGE_IMPACT");
159114
+ } else {
159115
+ let revisionVerified = false;
159116
+ let revisionFailure;
159117
+ try {
159118
+ const latest = await ctx.octokit.rest.pulls.get({
159119
+ owner: ctx.repo.owner,
159120
+ repo: ctx.repo.name,
159121
+ pull_number
159122
+ });
159123
+ revisionVerified = latest.data.head.sha === checkoutSha && latest.data.base.sha === prResponse.data.base.sha;
159124
+ } catch (error49) {
159125
+ revisionFailure = error49 instanceof Error ? error49.message : String(error49);
159126
+ }
159127
+ if (revisionFailure !== void 0) {
159128
+ log.warning(
159129
+ `\xBB change impact skipped: PR revision could not be verified (${revisionFailure})`
159130
+ );
159131
+ } else if (!revisionVerified) {
159132
+ log.warning("\xBB change impact skipped: PR revision changed while the diff was fetched");
159133
+ } else {
159134
+ try {
159135
+ const impact = await createChangeImpactArtifact(ctx, {
159136
+ files: formatResult.files,
159137
+ pullNumber: pull_number,
159138
+ baseSha: prResponse.data.base.sha,
159139
+ headSha: checkoutSha
159140
+ });
159141
+ impactPath = impact.path;
159142
+ log.info(
159143
+ `\xBB change impact: ${impact.candidateCount} candidate atom(s), ${impact.renderedAtomCount} detailed atom(s), ${impact.referenceCount} tracked reference(s), ${impact.bytes} bytes \u2192 ${impact.path}`
159144
+ );
159145
+ } catch (error49) {
159146
+ log.warning(
159147
+ `\xBB change impact skipped: generation failed (${error49 instanceof Error ? error49.message : String(error49)})`
159148
+ );
159149
+ }
159150
+ }
159151
+ }
158754
159152
  const cached4 = /* @__PURE__ */ new Map();
158755
159153
  for (const file2 of formatResult.files) {
158756
159154
  cached4.set(file2.filename, commentableLinesForFile(file2.patch));
158757
159155
  }
158758
159156
  primary.commentableLinesByFile = cached4;
158759
159157
  primary.commentableLinesPullNumber = pull_number;
158760
- primary.commentableLinesCheckoutSha = primary.checkoutSha;
158761
- const incrementalInstructions = incrementalDiffPath ? ` IMPORTANT: incrementalDiffPath contains ONLY the changes since the last reviewed version (computed via range-diff). you MUST read incrementalDiffPath FIRST to understand what changed, then use diffPath for full PR context. do NOT skip the incremental diff.` : "";
159158
+ primary.commentableLinesCheckoutSha = checkoutSha;
159159
+ const incrementalInstructions = incrementalDiffPath ? `IMPORTANT: incrementalDiffPath contains ONLY the changes since the last reviewed version (computed via range-diff). you MUST read incrementalDiffPath FIRST to understand what changed, then use diffPath for full PR context. do NOT skip the incremental diff. ` : "";
159160
+ const impactInstructions = impactPath ? ` impactPath contains bounded, deterministic reference leads for semantic atoms changed by the PR. read the authoritative diff TOC and relevant raw diff lines FIRST; only then use impactPath as a supplemental, explicitly incomplete lead list. impactPath never establishes review coverage and an empty artifact is not evidence of no wider impact.` : "";
158762
159161
  const COMMIT_LOG_MAX = 200;
158763
159162
  const baseRange = `origin/${pr.baseRef}..HEAD`;
158764
159163
  let commitCount = 0;
@@ -158794,6 +159193,7 @@ ${diffPreview}`);
158794
159193
  url: prResponse.data.html_url,
158795
159194
  headRepo: pr.headRepoFullName,
158796
159195
  diffPath,
159196
+ impactPath,
158797
159197
  incrementalDiffPath,
158798
159198
  toc: formatResult.toc,
158799
159199
  commitCount,
@@ -158801,7 +159201,7 @@ ${diffPreview}`);
158801
159201
  commitLogTruncated,
158802
159202
  commitLogUnavailable,
158803
159203
  hookWarning: checkoutResult.hookWarning,
158804
- instructions: `the diff file at diffPath contains a table of contents (TOC) at the top listing every changed file with its line range. use the TOC line ranges as your checklist and read specific files from the diff instead of reading the entire file. for example, if the TOC says "src/foo.ts \u2192 lines 5-42", read lines 5-42 from diffPath to see that file's changes. review files selectively based on relevance rather than reading everything sequentially. to inspect the PR's changed files, use diffPath \u2014 do NOT run \`git diff\` to re-derive what's already in diffPath. the formatted diff with line numbers is authoritative. if you ever do need a branch-vs-base diff via the git tool, use \`git diff --merge-base <base>\` (single call, includes uncommitted edits) or three-dot \`git diff <base>...HEAD\` (committed-only). bare \`<base>\` and two-dot \`<base>..HEAD\` are symmetric and pull in the inverse of every commit landed on \`<base>\` since the branch forked \u2014 the git tool will reject those forms when the divergence is detected. \`$(...)\` subshells are NOT expanded by the git tool. \`git log\` and \`git diff --stat\` are fine for commit-range overview, and \`git diff\` / \`git diff --cached\` are fine for inspecting *your own* uncommitted changes \u2014 but PR review content MUST come from diffPath. before your review is submitted, a one-time coverage pre-flight may error listing unread TOC regions. retry the same create_pull_request_review call to proceed \u2014 optionally after reading the listed ranges. the pre-flight will not block again this session. the local branch is 'localBranch' (pr-{number}), not the remote branch name. when pushing, omit branchName to use the current branch. do not use remoteBranch as a local branch name.` + incrementalInstructions + hookWarningInstructions + commitLogInstructions
159204
+ instructions: incrementalInstructions + `the diff file at diffPath contains a table of contents (TOC) at the top listing every changed file with its line range. use the TOC line ranges as your checklist and read specific files from the diff instead of reading the entire file. for example, if the TOC says "src/foo.ts \u2192 lines 5-42", read lines 5-42 from diffPath to see that file's changes. review files selectively based on relevance rather than reading everything sequentially. to inspect the PR's changed files, use diffPath \u2014 do NOT run \`git diff\` to re-derive what's already in diffPath. the formatted diff with line numbers is authoritative. if you ever do need a branch-vs-base diff via the git tool, use \`git diff --merge-base <base>\` (single call, includes uncommitted edits) or three-dot \`git diff <base>...HEAD\` (committed-only). bare \`<base>\` and two-dot \`<base>..HEAD\` are symmetric and pull in the inverse of every commit landed on \`<base>\` since the branch forked \u2014 the git tool will reject those forms when the divergence is detected. \`$(...)\` subshells are NOT expanded by the git tool. \`git log\` and \`git diff --stat\` are fine for commit-range overview, and \`git diff\` / \`git diff --cached\` are fine for inspecting *your own* uncommitted changes \u2014 but PR review content MUST come from diffPath. before your review is submitted, a one-time coverage pre-flight may error listing unread TOC regions. retry the same create_pull_request_review call to proceed \u2014 optionally after reading the listed ranges. the pre-flight will not block again this session. the local branch is 'localBranch' (pr-{number}), not the remote branch name. when pushing, omit branchName to use the current branch. do not use remoteBranch as a local branch name.` + impactInstructions + hookWarningInstructions + commitLogInstructions
158805
159205
  };
158806
159206
  };
158807
159207
  return tool({
@@ -158848,8 +159248,8 @@ ${dirty}`
158848
159248
  }
158849
159249
 
158850
159250
  // mcp/checkSuite.ts
158851
- import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "node:fs";
158852
- import { join as join13 } from "node:path";
159251
+ import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync9 } from "node:fs";
159252
+ import { join as join14 } from "node:path";
158853
159253
  var GetCheckSuiteLogs = type({
158854
159254
  check_suite_id: type.number.describe("the id from check_suite.id")
158855
159255
  });
@@ -158945,7 +159345,7 @@ function GetCheckSuiteLogsTool(ctx) {
158945
159345
  if (!tempDir) {
158946
159346
  throw new Error("PULLFROG_TEMP_DIR not set");
158947
159347
  }
158948
- const logsDir = join13(tempDir, "ci-logs");
159348
+ const logsDir = join14(tempDir, "ci-logs");
158949
159349
  mkdirSync7(logsDir, { recursive: true });
158950
159350
  const jobResults = [];
158951
159351
  for (const run of failedRuns) {
@@ -158972,8 +159372,8 @@ function GetCheckSuiteLogsTool(ctx) {
158972
159372
  );
158973
159373
  }
158974
159374
  const logsText = await logsResult.text();
158975
- const logPath = join13(logsDir, `job-${job.id}.log`);
158976
- writeFileSync8(logPath, logsText);
159375
+ const logPath = join14(logsDir, `job-${job.id}.log`);
159376
+ writeFileSync9(logPath, logsText);
158977
159377
  const analysis = analyzeLog(logsText, 80);
158978
159378
  const failedSteps = job.steps?.filter((s) => s.conclusion === "failure").map((s) => `Step ${s.number}: ${s.name}`) ?? [];
158979
159379
  jobResults.push({
@@ -159021,8 +159421,8 @@ function GetCheckSuiteLogsTool(ctx) {
159021
159421
  }
159022
159422
 
159023
159423
  // mcp/commitInfo.ts
159024
- import { writeFileSync as writeFileSync9 } from "node:fs";
159025
- import { join as join14 } from "node:path";
159424
+ import { writeFileSync as writeFileSync10 } from "node:fs";
159425
+ import { join as join15 } from "node:path";
159026
159426
  var CommitInfo = type({
159027
159427
  sha: type.string.describe("the commit SHA (full or abbreviated) to fetch")
159028
159428
  });
@@ -159046,8 +159446,8 @@ function CommitInfoTool(ctx) {
159046
159446
  "PULLFROG_TEMP_DIR not set - get_commit_info must run in pullfrog action context"
159047
159447
  );
159048
159448
  }
159049
- const diffFile = join14(tempDir, `commit-${sha.slice(0, 7)}.diff`);
159050
- writeFileSync9(diffFile, formatResult.content);
159449
+ const diffFile = join15(tempDir, `commit-${sha.slice(0, 7)}.diff`);
159450
+ writeFileSync10(diffFile, formatResult.content);
159051
159451
  log.debug(`wrote commit diff to ${diffFile} (${formatResult.content.length} bytes)`);
159052
159452
  return {
159053
159453
  sha: data.sha,
@@ -159074,7 +159474,7 @@ import { performance as performance8 } from "node:perf_hooks";
159074
159474
 
159075
159475
  // prep/installNodeDependencies.ts
159076
159476
  import { existsSync as existsSync6 } from "node:fs";
159077
- import { join as join16 } from "node:path";
159477
+ import { join as join17 } from "node:path";
159078
159478
 
159079
159479
  // node_modules/.pnpm/package-manager-detector@1.6.0/node_modules/package-manager-detector/dist/commands.mjs
159080
159480
  function dashDashArg(agent2, agentCommand) {
@@ -159373,7 +159773,7 @@ function isMetadataYarnClassic(metadataPath) {
159373
159773
  var import_semver3 = __toESM(require_semver2(), 1);
159374
159774
  import { existsSync as existsSync5 } from "node:fs";
159375
159775
  import { mkdir, readFile as readFile2 } from "node:fs/promises";
159376
- import { delimiter, join as join15 } from "node:path";
159776
+ import { delimiter, join as join16 } from "node:path";
159377
159777
  var SUPPORTED_NAMES = ["npm", "pnpm", "yarn", "bun"];
159378
159778
  var COREPACK_MANAGED = ["pnpm", "yarn"];
159379
159779
  function isSupported(name) {
@@ -159411,7 +159811,7 @@ function parseDevEnginesField(field) {
159411
159811
  };
159412
159812
  }
159413
159813
  async function resolvePackageManagerSpec(cwd) {
159414
- const pkgPath = join15(cwd, "package.json");
159814
+ const pkgPath = join16(cwd, "package.json");
159415
159815
  if (!existsSync5(pkgPath)) return null;
159416
159816
  let pkg;
159417
159817
  try {
@@ -159474,7 +159874,7 @@ async function currentVersion(name) {
159474
159874
  return result.stdout.trim();
159475
159875
  }
159476
159876
  function packageManagerBinDir(tmpdir3) {
159477
- return join15(tmpdir3, "pm-bin");
159877
+ return join16(tmpdir3, "pm-bin");
159478
159878
  }
159479
159879
  async function ensurePackageManager(params) {
159480
159880
  const spec = params.spec;
@@ -159545,7 +159945,7 @@ async function installFallback(name, installSpec) {
159545
159945
  return result.stderr || `failed to install ${name}`;
159546
159946
  }
159547
159947
  if (name === "deno") {
159548
- const denoPath = join16(process.env.HOME || "", ".deno", "bin");
159948
+ const denoPath = join17(process.env.HOME || "", ".deno", "bin");
159549
159949
  process.env.PATH = `${denoPath}:${process.env.PATH}`;
159550
159950
  }
159551
159951
  log.info(`\xBB installed ${name}`);
@@ -159554,7 +159954,7 @@ async function installFallback(name, installSpec) {
159554
159954
  var installNodeDependencies = {
159555
159955
  name: "installNodeDependencies",
159556
159956
  shouldRun: () => {
159557
- const packageJsonPath = join16(process.cwd(), "package.json");
159957
+ const packageJsonPath = join17(process.cwd(), "package.json");
159558
159958
  return existsSync6(packageJsonPath);
159559
159959
  },
159560
159960
  run: async (options) => {
@@ -159653,12 +160053,12 @@ ${errorMessage}`]
159653
160053
 
159654
160054
  // prep/installPythonDependencies.ts
159655
160055
  import { existsSync as existsSync7, readFileSync as readFileSync4 } from "node:fs";
159656
- import { join as join17 } from "node:path";
160056
+ import { join as join18 } from "node:path";
159657
160057
  function declaresBuildSystem(path4) {
159658
160058
  return /^\s*\[\s*build-system\s*\]/m.test(readFileSync4(path4, "utf8"));
159659
160059
  }
159660
160060
  function configApplies(config3, cwd) {
159661
- const path4 = join17(cwd, config3.file);
160061
+ const path4 = join18(cwd, config3.file);
159662
160062
  return existsSync7(path4) && (config3.applies?.(path4) ?? true);
159663
160063
  }
159664
160064
  var PYTHON_CONFIGS = [
@@ -160794,8 +161194,8 @@ function PullRequestInfoTool(ctx) {
160794
161194
  }
160795
161195
 
160796
161196
  // mcp/reviewComments.ts
160797
- import { writeFileSync as writeFileSync10 } from "node:fs";
160798
- import { join as join18 } from "node:path";
161197
+ import { writeFileSync as writeFileSync11 } from "node:fs";
161198
+ import { join as join19 } from "node:path";
160799
161199
  var REVIEW_THREADS_QUERY = `
160800
161200
  query ($owner: String!, $name: String!, $prNumber: Int!) {
160801
161201
  repository(owner: $owner, name: $name) {
@@ -161211,8 +161611,8 @@ function GetReviewCommentsTool(ctx) {
161211
161611
  throw new Error("PULLFROG_TEMP_DIR not set");
161212
161612
  }
161213
161613
  const filename = `review-${params.review_id}-threads.md`;
161214
- const commentsPath = join18(tempDir, filename);
161215
- writeFileSync10(commentsPath, formatted.content);
161614
+ const commentsPath = join19(tempDir, filename);
161615
+ writeFileSync11(commentsPath, formatted.content);
161216
161616
  log.debug(`wrote ${threadBlocks.length} threads to ${commentsPath}`);
161217
161617
  return {
161218
161618
  review_id: params.review_id,
@@ -161447,11 +161847,11 @@ ${summaryAddendum}`,
161447
161847
  }
161448
161848
 
161449
161849
  // mcp/shell.ts
161450
- import { spawn as spawn2, spawnSync as spawnSync5 } from "node:child_process";
161850
+ import { spawn as spawn3, spawnSync as spawnSync5 } from "node:child_process";
161451
161851
  import { randomUUID as randomUUID4 } from "node:crypto";
161452
- import { closeSync, openSync, writeFileSync as writeFileSync11 } from "node:fs";
161852
+ import { closeSync, openSync, writeFileSync as writeFileSync12 } from "node:fs";
161453
161853
  import { userInfo as userInfo2 } from "node:os";
161454
- import { join as join19 } from "node:path";
161854
+ import { join as join20 } from "node:path";
161455
161855
  import { setTimeout as sleep2 } from "node:timers/promises";
161456
161856
  var ShellParams = type({
161457
161857
  command: "string",
@@ -161549,7 +161949,7 @@ function spawnShell(params) {
161549
161949
  const repoRoot = resolveRepoRoot();
161550
161950
  const fsMounts = buildFsMounts(repoRoot);
161551
161951
  if (sandboxMethod === "unshare") {
161552
- return spawn2(
161952
+ return spawn3(
161553
161953
  "unshare",
161554
161954
  [
161555
161955
  "--pid",
@@ -161573,7 +161973,7 @@ function spawnShell(params) {
161573
161973
  const pathRestore = 'export PATH="${SANDBOX_PATH:-$PATH}"; ';
161574
161974
  const escaped = (pathRestore + params.command).replace(/'/g, "'\\''");
161575
161975
  envArgs.push(`SANDBOX_PATH=${params.env.PATH ?? ""}`);
161576
- return spawn2(
161976
+ return spawn3(
161577
161977
  "sudo",
161578
161978
  [
161579
161979
  "env",
@@ -161589,7 +161989,7 @@ function spawnShell(params) {
161589
161989
  { ...spawnOpts, env: {} }
161590
161990
  );
161591
161991
  }
161592
- return spawn2("bash", ["-c", params.command], spawnOpts);
161992
+ return spawn3("bash", ["-c", params.command], spawnOpts);
161593
161993
  }
161594
161994
  async function killProcessGroup(proc) {
161595
161995
  if (!proc.pid) return;
@@ -161614,8 +162014,8 @@ function getTempDir() {
161614
162014
  var MAX_OUTPUT_CHARS = 5e3;
161615
162015
  function capOutput(output) {
161616
162016
  if (output.length <= MAX_OUTPUT_CHARS) return output;
161617
- const fullPath = join19(getTempDir(), `shell-${randomUUID4().slice(0, 8)}.log`);
161618
- writeFileSync11(fullPath, output);
162017
+ const fullPath = join20(getTempDir(), `shell-${randomUUID4().slice(0, 8)}.log`);
162018
+ writeFileSync12(fullPath, output);
161619
162019
  const elided = output.length - MAX_OUTPUT_CHARS;
161620
162020
  return `... [${elided} chars truncated; full output saved to ${fullPath}] ...
161621
162021
  ${output.slice(-MAX_OUTPUT_CHARS)}`;
@@ -161669,8 +162069,8 @@ Do NOT use this tool for git commands \u2014 use the dedicated git tools instead
161669
162069
  if (params.background) {
161670
162070
  const tempDir = getTempDir();
161671
162071
  const handle = `bg-${randomUUID4().slice(0, 8)}`;
161672
- const outputPath = join19(tempDir, `${handle}.log`);
161673
- const pidPath = join19(tempDir, `${handle}.pid`);
162072
+ const outputPath = join20(tempDir, `${handle}.log`);
162073
+ const pidPath = join20(tempDir, `${handle}.pid`);
161674
162074
  const logFd = openSync(outputPath, "a");
161675
162075
  let proc2;
161676
162076
  try {
@@ -161687,7 +162087,7 @@ Do NOT use this tool for git commands \u2014 use the dedicated git tools instead
161687
162087
  throw new Error("failed to start background process");
161688
162088
  }
161689
162089
  proc2.unref();
161690
- writeFileSync11(pidPath, `${proc2.pid}
162090
+ writeFileSync12(pidPath, `${proc2.pid}
161691
162091
  `);
161692
162092
  ctx.toolState.backgroundProcesses.set(handle, { pid: proc2.pid, outputPath, pidPath });
161693
162093
  return {
@@ -161837,7 +162237,7 @@ function UploadFileTool(ctx) {
161837
162237
 
161838
162238
  // mcp/xrepo.ts
161839
162239
  import { mkdirSync as mkdirSync8, rmSync as rmSync2 } from "node:fs";
161840
- import { join as join20 } from "node:path";
162240
+ import { join as join21 } from "node:path";
161841
162241
  function assertValidRepoName(name) {
161842
162242
  if (!/^[A-Za-z0-9._-]+$/.test(name) || name.startsWith("-") || name.includes("..")) {
161843
162243
  throw new Error(
@@ -161855,6 +162255,7 @@ var ListRepos = type({});
161855
162255
  function ListReposTool(ctx) {
161856
162256
  return tool({
161857
162257
  name: "list_repos",
162258
+ annotations: { readOnlyHint: true },
161858
162259
  description: "List the repositories available for cross-repo (`--xrepo`) work in this run, with each repo's access tier (primary, write, or read) and whether it's already checked out. Use this before `checkout_repo` to discover what you can reference or edit. Returns an empty set on single-repo runs.",
161859
162260
  parameters: ListRepos,
161860
162261
  execute: execute(async () => {
@@ -161916,7 +162317,7 @@ function CheckoutRepoTool(ctx) {
161916
162317
  return { path: existing.dir, access: existing.access, note: "already checked out." };
161917
162318
  }
161918
162319
  const access = accessFor(ctx, repo);
161919
- const dir = join20(ctx.tmpdir, "xrepo", repo);
162320
+ const dir = join21(ctx.tmpdir, "xrepo", repo);
161920
162321
  mkdirSync8(dir, { recursive: true });
161921
162322
  const state = ensureRepoState(ctx.toolState, { owner, name: repo, dir, access });
161922
162323
  const rc = resolveRepoCtx(ctx, repo);
@@ -162366,9 +162767,9 @@ function formatApiKeyErrorSummary(params) {
162366
162767
 
162367
162768
  // utils/gitAuthServer.ts
162368
162769
  import { randomUUID as randomUUID5 } from "node:crypto";
162369
- import { writeFileSync as writeFileSync12 } from "node:fs";
162770
+ import { writeFileSync as writeFileSync13 } from "node:fs";
162370
162771
  import { createServer as createServer3 } from "node:http";
162371
- import { join as join21 } from "node:path";
162772
+ import { join as join22 } from "node:path";
162372
162773
  var REVOKED_TRAP_MS = 6e4;
162373
162774
  function revokeGitHubToken(token) {
162374
162775
  fetch("https://api.github.com/installation/token", {
@@ -162437,7 +162838,7 @@ async function startGitAuthServer(tmpdir3) {
162437
162838
  function writeAskpassScript(code) {
162438
162839
  const scriptId = randomUUID5();
162439
162840
  const scriptName = `askpass-${scriptId}.js`;
162440
- const scriptPath = join21(tmpdir3, scriptName);
162841
+ const scriptPath = join22(tmpdir3, scriptName);
162441
162842
  const content = [
162442
162843
  `#!/usr/bin/env node`,
162443
162844
  `var a=process.argv[2]||"";`,
@@ -162450,7 +162851,7 @@ async function startGitAuthServer(tmpdir3) {
162450
162851
  `r.on("end",function(){process.stdout.write(d+"\\n")})`,
162451
162852
  `}).on("error",function(){process.exit(1)})}`
162452
162853
  ].join("\n");
162453
- writeFileSync12(scriptPath, content, { mode: 448 });
162854
+ writeFileSync13(scriptPath, content, { mode: 448 });
162454
162855
  return scriptPath;
162455
162856
  }
162456
162857
  async function close() {
@@ -162771,9 +163172,9 @@ When embedding images (e.g. uploaded screenshots) in comments or PR bodies, alwa
162771
163172
 
162772
163173
  **Your raw assistant messages are never delivered** \u2014 they exist only in the run logs. Anything the user is meant to see (an answer to a question, a mention reply, a result) MUST go through \`report_progress\` (or another ${pullfrogMcpName} write tool). Do not rely on returning the answer as plain text \u2014 the harness makes a best-effort attempt to recover it into the progress comment, but that is a safety net, not a substitute for calling \`report_progress\`.
162773
163174
 
162774
- **\`report_progress\`**: call this exactly once at the end of every run with a brief final summary (1-3 sentences) unless the mode guidance instructs otherwise. Never call it for intermediate status updates (e.g., "Checking for changes...", "Starting review...") \u2014 the task list handles live progress automatically. Calling \`report_progress\` replaces the task list with your summary and preserves the current task list in a collapsible section. Keep the summary concise \u2014 do not repeat what the task list already shows. Focus on the outcome (what was accomplished, links to artifacts) rather than listing individual steps. If something failed, include the tool's error text even when that makes the summary longer.
163175
+ **\`report_progress\`**: call this exactly once at the end of every run with a brief final summary (1-3 sentences) unless the mode guidance instructs otherwise or a standalone comment on the current target is the task's sole requested deliverable. Never call it for intermediate status updates (e.g., "Checking for changes...", "Starting review...") \u2014 the task list handles live progress automatically. Calling \`report_progress\` replaces the task list with your summary and preserves the current task list in a collapsible section. Keep the summary concise \u2014 do not repeat what the task list already shows. Focus on the outcome (what was accomplished, links to artifacts) rather than listing individual steps. If something failed, include the tool's error text even when that makes the summary longer.
162775
163176
 
162776
- Never use \`create_issue_comment\` for task progress \u2014 that creates duplicate comments and leaves the progress comment stuck in its initial state. \`create_issue_comment\` is only for standalone comments unrelated to your current task. Plan output (initial post AND revisions) goes through \`report_progress\` \u2014 see the Plan mode guidance for details.
163177
+ Never use \`create_issue_comment\` for task progress or for the answer that \`report_progress\` should deliver. Use it only when the task explicitly requests a separate standalone comment or a comment on a different target. Skip \`report_progress\` only when a standalone comment on the current target is the task's sole requested deliverable; successful-run cleanup then removes the progress chrome. Plan output (initial post AND revisions) goes through \`report_progress\` \u2014 see the Plan mode guidance for details.
162777
163178
 
162778
163179
  ### If you get stuck
162779
163180
 
@@ -162907,7 +163308,7 @@ function resolveInstructions(ctx) {
162907
163308
 
162908
163309
  // utils/learnings.ts
162909
163310
  import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
162910
- import { dirname as dirname4, join as join22 } from "node:path";
163311
+ import { dirname as dirname4, join as join23 } from "node:path";
162911
163312
 
162912
163313
  // utils/learningsTruncate.ts
162913
163314
  var MAX_LEARNINGS_LENGTH = 1e5;
@@ -162925,10 +163326,10 @@ function truncateAtLineBoundary(body, cap) {
162925
163326
  var LEARNINGS_FILE_NAME = "pullfrog-learnings.md";
162926
163327
  var XREPO_LEARNINGS_FILE_NAME = "pullfrog-xrepo-learnings.md";
162927
163328
  function learningsFilePath(tmpdir3) {
162928
- return join22(tmpdir3, LEARNINGS_FILE_NAME);
163329
+ return join23(tmpdir3, LEARNINGS_FILE_NAME);
162929
163330
  }
162930
163331
  function xrepoLearningsFilePath(tmpdir3) {
162931
- return join22(tmpdir3, XREPO_LEARNINGS_FILE_NAME);
163332
+ return join23(tmpdir3, XREPO_LEARNINGS_FILE_NAME);
162932
163333
  }
162933
163334
  async function seedLearningsFile(params) {
162934
163335
  const path4 = learningsFilePath(params.tmpdir);
@@ -163668,7 +164069,7 @@ async function runProxyResolution(ctx) {
163668
164069
 
163669
164070
  // utils/prSummary.ts
163670
164071
  import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
163671
- import { dirname as dirname5, join as join23 } from "node:path";
164072
+ import { dirname as dirname5, join as join24 } from "node:path";
163672
164073
  var SUMMARY_FILE_NAME = "pullfrog-summary.md";
163673
164074
  var SUMMARY_SCAFFOLD = `# PR summary
163674
164075
 
@@ -163678,7 +164079,7 @@ var SUMMARY_SCAFFOLD = `# PR summary
163678
164079
  var MIN_SNAPSHOT_LENGTH = 60;
163679
164080
  var MAX_SNAPSHOT_LENGTH = 32768;
163680
164081
  function summaryFilePath(tmpdir3) {
163681
- return join23(tmpdir3, SUMMARY_FILE_NAME);
164082
+ return join24(tmpdir3, SUMMARY_FILE_NAME);
163682
164083
  }
163683
164084
  async function seedSummaryFile(params) {
163684
164085
  const path4 = summaryFilePath(params.tmpdir);
@@ -164395,7 +164796,7 @@ async function finalizeSuccessRun(input) {
164395
164796
  log.debug(`failure error report failed: ${error49}`);
164396
164797
  });
164397
164798
  }
164398
- if (input.result.success && input.toolState.progressComment && input.toolState.wasUpdated && (!input.toolState.finalSummaryWritten || input.toolState.answerCommentPosted)) {
164799
+ if (input.result.success && input.toolState.progressComment && input.toolState.wasUpdated && !input.toolState.finalSummaryWritten) {
164399
164800
  await deleteProgressComment(input.toolContext).catch((error49) => {
164400
164801
  log.debug(`stranded progress comment cleanup failed: ${error49}`);
164401
164802
  });
@@ -164961,7 +165362,7 @@ ${instructions.user}` : null,
164961
165362
  log.info(instructions.full);
164962
165363
  });
164963
165364
  if (agentId === "opencode") {
164964
- const pluginDir = join24(process.cwd(), ".opencode", "plugin");
165365
+ const pluginDir = join25(process.cwd(), ".opencode", "plugin");
164965
165366
  const hasPlugins = existsSync8(pluginDir) && readdirSync2(pluginDir).some((f) => /\.[jt]sx?$/.test(f));
164966
165367
  if (hasPlugins && toolState.dependencyInstallation?.promise) {
164967
165368
  log.info(