pullfrog 0.1.35 → 0.1.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -28230,7 +28230,7 @@ var require_cross_spawn = __commonJS({
28230
28230
  var cp = __require("child_process");
28231
28231
  var parse5 = require_parse3();
28232
28232
  var enoent = require_enoent();
28233
- function spawn5(command, args2, options) {
28233
+ function spawn6(command, args2, options) {
28234
28234
  const parsed2 = parse5(command, args2, options);
28235
28235
  const spawned = cp.spawn(parsed2.command, parsed2.args, parsed2.options);
28236
28236
  enoent.hookChildProcess(spawned, parsed2);
@@ -28242,8 +28242,8 @@ var require_cross_spawn = __commonJS({
28242
28242
  result.error = result.error || enoent.verifyENOENTSync(result.status, parsed2);
28243
28243
  return result;
28244
28244
  }
28245
- module.exports = spawn5;
28246
- module.exports.spawn = spawn5;
28245
+ module.exports = spawn6;
28246
+ module.exports.spawn = spawn6;
28247
28247
  module.exports.sync = spawnSync6;
28248
28248
  module.exports._parse = parse5;
28249
28249
  module.exports._enoent = enoent;
@@ -96055,14 +96055,14 @@ var require_turndown_cjs = __commonJS({
96055
96055
  } else if (node2.nodeType === 1) {
96056
96056
  replacement = replacementForNode.call(self2, node2);
96057
96057
  }
96058
- return join26(output, replacement);
96058
+ return join27(output, replacement);
96059
96059
  }, "");
96060
96060
  }
96061
96061
  function postProcess(output) {
96062
96062
  var self2 = this;
96063
96063
  this.rules.forEach(function(rule) {
96064
96064
  if (typeof rule.append === "function") {
96065
- output = join26(output, rule.append(self2.options));
96065
+ output = join27(output, rule.append(self2.options));
96066
96066
  }
96067
96067
  });
96068
96068
  return output.replace(/^[\t\r\n]+/, "").replace(/[\t\r\n\s]+$/, "");
@@ -96074,7 +96074,7 @@ var require_turndown_cjs = __commonJS({
96074
96074
  if (whitespace.leading || whitespace.trailing) content = content.trim();
96075
96075
  return whitespace.leading + rule.replacement(content, node2, this.options) + whitespace.trailing;
96076
96076
  }
96077
- function join26(output, replacement) {
96077
+ function join27(output, replacement) {
96078
96078
  var s1 = trimTrailingNewlines(output);
96079
96079
  var s2 = trimLeadingNewlines(replacement);
96080
96080
  var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
@@ -101730,7 +101730,7 @@ import { dirname as dirname6 } from "node:path";
101730
101730
  // main.ts
101731
101731
  import { existsSync as existsSync8, readdirSync as readdirSync2 } from "node:fs";
101732
101732
  import { readFile as readFile5 } from "node:fs/promises";
101733
- import { join as join25 } from "node:path";
101733
+ import { join as join26 } from "node:path";
101734
101734
 
101735
101735
  // agents/claude.ts
101736
101736
  import { execFileSync as execFileSync3 } from "node:child_process";
@@ -102064,7 +102064,7 @@ var providers = {
102064
102064
  envVars: ["OPENCODE_API_KEY"],
102065
102065
  models: {
102066
102066
  "glm-5.1": {
102067
- displayName: "GLM 5.1",
102067
+ displayName: "GLM 5.2",
102068
102068
  resolve: "opencode-go/glm-5.2",
102069
102069
  openRouterResolve: "openrouter/z-ai/glm-5.2",
102070
102070
  preferred: true
@@ -103001,7 +103001,7 @@ var import_semver = __toESM(require_semver2(), 1);
103001
103001
  // package.json
103002
103002
  var package_default = {
103003
103003
  name: "pullfrog",
103004
- version: "0.1.35",
103004
+ version: "0.1.37",
103005
103005
  type: "module",
103006
103006
  bin: {
103007
103007
  pullfrog: "dist/cli.mjs",
@@ -103675,8 +103675,9 @@ HARD CONSTRAINTS (non-negotiable, regardless of orchestrator instructions):
103675
103675
  - 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.
103676
103676
  - 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.
103677
103677
  - 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.
103678
+ - 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.
103678
103679
  - 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).
103679
- - 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.
103680
+ - 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\`.
103680
103681
  - Do NOT spawn further subagents. You are a leaf reviewer; recursive dispatch pre-aggregates findings through an intermediate model and defeats the design.
103681
103682
  - 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.
103682
103683
 
@@ -103939,20 +103940,15 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
103939
103940
  - **immediately** call \`${t2("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.
103940
103941
  - call \`${t2("report_progress")}\` with a brief summary`
103941
103942
  },
103942
- // Review and IncrementalReview use a 0-or-2+ lens pattern. The default is
103943
- // 0 lenses (orchestrator handles the review solo). Multi-lens (2+
103944
- // reviewfrog subagents in parallel) only fires for substantive PRs or
103945
- // high-stakes-subsystem touches — and when it fires, ALL lenses must
103946
- // dispatch in a single assistant turn or the parallelism win disappears.
103947
- // We never dispatch exactly one lens: a single lens is just a worse,
103948
- // slower version of doing the work yourself.
103943
+ // Review and IncrementalReview route the minimum reviewfrog specialists
103944
+ // needed to cover unresolved, disposition-changing hypotheses. Most runs
103945
+ // use zero or one; multiple orthogonal hypotheses dispatch in parallel.
103949
103946
  //
103950
103947
  // Build mode self-review is a different problem shape: the orchestrator
103951
103948
  // wrote the code, so bias-mitigation comes from delegating to one
103952
103949
  // fresh-eyes subagent that doesn't share the implementation context. A
103953
- // single subagent there is appropriate; the 0-or-2+ rule applies only to
103954
- // the Review/IncrementalReview lens fan-out where independence between
103955
- // perspectives is what's being purchased.
103950
+ // single subagent there is appropriate. Review-mode specialist routing
103951
+ // instead scales with the unresolved hypotheses in someone else's diff.
103956
103952
  //
103957
103953
  // Severity categorization is split across two surfaces: the opening
103958
103954
  // callout (CAUTION/IMPORTANT/ℹ️/✅) sets the review's overall tier, and
@@ -103967,11 +103963,11 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
103967
103963
 
103968
103964
  1. **task list**: create your task list for this run as your first action.
103969
103965
 
103970
- 2. **checkout**: call \`${t2("checkout_pr")}\` \u2014 this returns PR metadata and a \`diffPath\`. read the diff TOC end-to-end and treat its file line ranges as your coverage checklist.
103966
+ 2. **checkout**: call \`${t2("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.
103971
103967
 
103972
103968
  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.
103973
103969
 
103974
- if the PR is **genuinely trivial**, skip the fan-out entirely and submit a \`No new issues found.\` review per step 7.
103970
+ if the PR is **genuinely trivial**, skip specialists entirely and submit a \`No new issues found.\` review per step 7.
103975
103971
 
103976
103972
  "Genuinely trivial" (skip):
103977
103973
  - single-word doc typo, whitespace/format-only, comment-only across any number of files
@@ -103990,22 +103986,24 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
103990
103986
  - any "typo fix" in user-facing copy that changes meaning ("approved" \u2192 "denied")
103991
103987
  - mixed diffs where a semantic 1-liner is buried in whitespace/formatting changes
103992
103988
 
103993
- 4. **lens decision \u2014 0 or 2+, NEVER 1**.
103989
+ 4. **specialist decision \u2014 minimum hypothesis coverage**.
103994
103990
 
103995
- The default is **0 lenses**: handle the review yourself end-to-end. Most PRs land here.
103991
+ 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.
103996
103992
 
103997
- Dispatch **2+ \`${REVIEWER_AGENT_NAME}\` lenses in parallel** ONLY when ALL of the following are true:
103998
- - 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)
103999
- - you can name 2+ distinct concrete failure modes that warrant independent lenses (one lens per failure mode; orthogonal, not overlapping)
104000
- - parallel-orchestrated independent perspectives meaningfully outperform what you'd find solo
103993
+ Route the **minimum number of \`${REVIEWER_AGENT_NAME}\` specialists** needed to cover those unresolved hypotheses. Most reviews need **0 or 1**:
103994
+ - dispatch 0 when you can resolve every disposition-changing question directly
103995
+ - dispatch 1 when exactly one falsifiable, load-bearing hypothesis warrants independent investigation
103996
+ - dispatch 2+ in parallel when multiple orthogonal load-bearing hypotheses remain, or when the user explicitly requests an exhaustive or multi-angle review
104001
103997
 
104002
- **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).
103998
+ **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.
104003
103999
 
104004
- When you do go multi-lens, lens framings come in two flavors:
104000
+ 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.
104001
+
104002
+ Specialist hypotheses can draw on two kinds of framing:
104005
104003
  - **themed lenses** \u2014 a perspective applied across the whole diff (correctness, security, user-journey, performance, etc.).
104006
104004
  - **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.
104007
104005
 
104008
- starter menu (combine, omit, or invent your own):
104006
+ starter menu for identifying hypotheses (combine, omit, or invent your own; do not dispatch a bare menu label without a falsifiable question):
104009
104007
  - **correctness & invariants** \u2014 bugs, races, error handling, edge cases, state-machine boundaries
104010
104008
  - **impact** \u2014 stale references in code/tests/docs/configs/UI after rename/remove
104011
104009
  - **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.
@@ -104020,30 +104018,27 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
104020
104018
 
104021
104019
  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.
104022
104020
 
104023
- 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.**
104024
-
104025
- \u26A0\uFE0F CRITICAL \u2014 PARALLELISM IS THE ONLY REASON LENSES EXIST. \u26A0\uFE0F
104026
- 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.
104021
+ 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.
104027
104022
 
104028
- \u2705 Right pattern: one assistant turn with N Task tool_use blocks \u2192 wait \u2192 N results arrive together \u2192 aggregate.
104029
- \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.
104023
+ \u2705 Right multi-specialist pattern: one assistant turn with N Task tool_use blocks \u2192 wait \u2192 N results arrive together \u2192 aggregate.
104024
+ \u274C Wrong multi-specialist pattern: Task(hypothesis A) \u2192 wait for A \u2192 Task(hypothesis B).
104030
104025
 
104031
104026
  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.
104032
104027
 
104033
- 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:
104028
+ 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:
104034
104029
  - **the absolute \`diffPath\` (and \`incrementalDiffPath\` if available) from step 2's \`${t2("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.
104035
- - **only one lens** \u2014 never a multi-section "review for X, Y, and Z" prompt
104036
- - **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\`.
104030
+ - **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
104031
+ - **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\`.
104037
104032
  - 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."
104038
- - 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.
104033
+ - 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.
104039
104034
 
104040
104035
  delegation discipline:
104041
- - do NOT summarize the PR for them (biases toward a validation frame)
104036
+ - do NOT summarize the PR for them (a lossy summary biases toward a validation frame; the raw diff is the source)
104042
104037
  - do NOT hand them a curated reading list (let them discover scope)
104043
104038
  - do NOT pre-shape their output with a finding schema
104044
104039
  - do NOT mention the other lenses (independence is the point \u2014 overlapping findings are a strong signal)
104045
104040
 
104046
- 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.
104041
+ 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.
104047
104042
 
104048
104043
  **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.
104049
104044
 
@@ -104077,10 +104072,10 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
104077
104072
 
104078
104073
  ${PR_SUMMARY_FORMAT}`
104079
104074
  },
104080
- // IncrementalReview shares Review's 0-or-2+ lens pattern AND its body
104081
- // format (PR_SUMMARY_FORMAT), scoped to the incremental delta against the
104082
- // prior pullfrog review. The "issues must be NEW since the last Pullfrog
104083
- // review" filter lives at aggregation time (step 8), NOT in the subagent
104075
+ // IncrementalReview shares Review's minimum hypothesis-covering specialist
104076
+ // routing and body format, scoped to the incremental delta against the
104077
+ // prior Pullfrog review. The "issues must be NEW since the last Pullfrog
104078
+ // review" filter lives at aggregation time, NOT in the subagent
104084
104079
  // prompt — pushing the filter into subagents matches the canonical anneal
104085
104080
  // anti-pattern of "list known pre-existing failures — don't flag these"
104086
104081
  // and suppresses signal on regressions the new commits amplified. A
@@ -104095,9 +104090,9 @@ ${PR_SUMMARY_FORMAT}`
104095
104090
 
104096
104091
  1. **task list**: create your task list for this run as your first action.
104097
104092
 
104098
- 2. **checkout**: call \`${t2("checkout_pr")}\` \u2014 this returns PR metadata, \`diffPath\` (full diff), and \`incrementalDiffPath\` (changes since last reviewed version, if available). read the diff TOC first and use its line ranges as your coverage checklist.
104093
+ 2. **checkout**: call \`${t2("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.
104099
104094
 
104100
- 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.
104095
+ 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.
104101
104096
 
104102
104097
  4. **prior feedback \u2014 read AND retire it**: fetch previous reviews via \`${t2("list_pull_request_reviews")}\`, then call \`${t2("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:
104103
104098
 
@@ -104111,49 +104106,48 @@ ${PR_SUMMARY_FORMAT}`
104111
104106
 
104112
104107
  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.**
104113
104108
 
104114
- 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).
104109
+ if the incremental changes are **genuinely trivial**, skip specialists entirely and jump to step 10's non-substantive path (do NOT submit a review).
104115
104110
 
104116
104111
  "Genuinely trivial" (skip): formatting/comment tweaks, import reordering, lockfile regen, mechanical rename of import paths, whitespace-only.
104117
104112
  "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.
104118
104113
  When unsure, treat as non-trivial.
104119
104114
 
104120
- 6. **lens decision \u2014 0 or 2+, NEVER 1**.
104115
+ 6. **specialist decision \u2014 minimum hypothesis coverage**.
104121
104116
 
104122
- 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."
104117
+ 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.
104123
104118
 
104124
- Dispatch **2+ \`${REVIEWER_AGENT_NAME}\` lenses in parallel** ONLY when ALL of the following are true:
104125
- - 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)
104126
- - you can name 2+ distinct concrete failure modes the new commits plausibly introduce that warrant independent lenses
104127
- - parallel-orchestrated independent perspectives meaningfully outperform what you'd find solo
104119
+ 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:
104120
+ - dispatch 0 when you can resolve every disposition-changing question directly
104121
+ - dispatch 1 when exactly one falsifiable, load-bearing hypothesis warrants independent investigation
104122
+ - dispatch 2+ in parallel when multiple orthogonal load-bearing hypotheses remain, or when the user explicitly requests an exhaustive or multi-angle review
104128
104123
 
104129
- **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.
104124
+ **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.
104130
104125
 
104131
- 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.
104126
+ 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.
104132
104127
 
104133
- 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.**
104128
+ 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.
104134
104129
 
104135
- \u26A0\uFE0F CRITICAL \u2014 PARALLELISM IS THE ONLY REASON LENSES EXIST. \u26A0\uFE0F
104136
- 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.
104130
+ 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.
104137
104131
 
104138
- \u2705 Right pattern: one assistant turn with N Task tool_use blocks \u2192 wait \u2192 N results arrive together \u2192 aggregate.
104139
- \u274C Wrong pattern: turn 1 = Task(lens A) \u2192 turn 2 (after A's result) = Task(lens B). This is the failure mode.
104132
+ \u2705 Right multi-specialist pattern: one assistant turn with N Task tool_use blocks \u2192 wait \u2192 N results arrive together \u2192 aggregate.
104133
+ \u274C Wrong multi-specialist pattern: Task(hypothesis A) \u2192 wait for A \u2192 Task(hypothesis B).
104140
104134
 
104141
104135
  You can also include your own \`read\` / \`grep\` / \`webfetch\` calls in the SAME turn as the parallel \`${REVIEWER_AGENT_NAME}\` dispatches.
104142
104136
 
104143
- 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:
104137
+ 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:
104144
104138
  - **the absolute diff path(s) from step 2's \`${t2("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.
104145
- - **only one lens** \u2014 never a multi-section "review for X, Y, and Z" prompt
104146
- - **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.
104139
+ - **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
104140
+ - **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.
104147
104141
  - if the lens touches external contracts, instruct the subagent to verify load-bearing claims via web search and quote source URLs.
104148
- - ask the subagent to report findings with file paths and NEW line numbers from the full PR diff so you can anchor inline comments.
104142
+ - 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.
104149
104143
 
104150
104144
  delegation discipline:
104151
- - do NOT summarize the changes for them (biases toward validation frame)
104145
+ - do NOT summarize the changes for them (a lossy summary biases toward a validation frame; the raw diff is the source)
104152
104146
  - do NOT hand them a curated reading list (let them discover scope)
104153
104147
  - do NOT pre-shape their output with a finding schema
104154
104148
  - do NOT mention the other lenses (independence is the point)
104155
104149
 
104156
- 8. **aggregate, draft, self-critique**: merge findings (yours + any subagent output if you went multi-lens); de-dup overlaps; trace each finding yourself. drop praise, style preferences, speculative/unverified claims, findings about pre-existing code unrelated to the new commits, anything not actionable, and anything that re-states prior review feedback (heuristic: if the finding's root cause lives in lines the *new commits* added or modified, it's in scope; otherwise drop). also drop **bloat-shaped findings** \u2014 proposed fixes that would add defensive checks for cases that can't happen, abstractions used once, comments restating obvious code, tests asserting tautologies, or "just-in-case" guards. subagents are fallible and bias toward recommending changes; the bar for an actionable inline comment is sound + correct + elegant. recommending a change that improves only one of the three (or degrades elegance to nominally improve correctness) makes the codebase worse, not better. To compute "lines the new commits added or modified": if \`incrementalDiffPath\` from step 2 is present, use it directly. Otherwise, take the prior Pullfrog review's \`commit_id\` (returned alongside each entry from \`${t2("list_pull_request_reviews")}\` in step 4) and run \`git diff <prior-review-sha>..HEAD\` to isolate the lines added since that review.
104150
+ 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 \`${t2("list_pull_request_reviews")}\` in step 4) and run \`git diff <prior-review-sha>..HEAD\` to isolate the lines added since that review.
104157
104151
 
104158
104152
  **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.
104159
104153
 
@@ -104250,7 +104244,7 @@ ${PR_SUMMARY_FORMAT}`
104250
104244
 
104251
104245
  1. **task list**: create your task list for this run as your first action.
104252
104246
 
104253
- 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 \`${t2("report_progress")}\` (step 4); raw assistant text is discarded.
104247
+ 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 \`${t2("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 \`${t2("report_progress")}\`.
104254
104248
 
104255
104249
  3. For substantial work \u2014 code changes across multiple files, multi-step investigations:
104256
104250
  - plan your approach before starting
@@ -104260,8 +104254,8 @@ ${PR_SUMMARY_FORMAT}`
104260
104254
 
104261
104255
  4. Finalize:
104262
104256
  - if code changes were made, get them onto a pull request (new or existing) using ${signedCommits ? `\`${t2("commit_changes")}\`` : `\`${t2("push_branch")}\``} and \`${t2("create_pull_request")}\` as needed. \`git status\` must be clean before you finish (see *SYSTEM* Git rules if this fails).
104263
- - call \`${t2("report_progress")}\` once with results \u2014 include exact tool errors if push or PR creation failed
104264
- - if the task involved labeling, commenting, or other GitHub operations, perform those directly`
104257
+ - call \`${t2("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
104258
+ - if the task involved labeling or other GitHub operations, perform those directly`
104265
104259
  }
104266
104260
  ];
104267
104261
  }
@@ -104816,7 +104810,7 @@ async function runClaude2(params) {
104816
104810
  }
104817
104811
  } else if (block.type === "tool_use") {
104818
104812
  const toolName = block.name || "unknown";
104819
- if (params.onToolUse) {
104813
+ if (params.onToolUse && label === ORCHESTRATOR_LABEL) {
104820
104814
  params.onToolUse({
104821
104815
  toolName,
104822
104816
  input: block.input
@@ -105208,7 +105202,7 @@ var claude = agent({
105208
105202
  const cliPath = await installClaudeCli();
105209
105203
  const specifier = ctx.payload.proxyModel ?? ctx.resolvedModel;
105210
105204
  const bedrockModelId = process.env[BEDROCK_MODEL_ID_ENV]?.trim();
105211
- const isBedrockRoute = specifier !== void 0 && bedrockModelId !== void 0 && bedrockModelId === specifier && isBedrockAnthropicId(specifier);
105205
+ const isBedrockRoute = specifier !== void 0 && bedrockModelId !== void 0 && bedrockModelId === specifier;
105212
105206
  const vertexModelId = process.env[VERTEX_MODEL_ID_ENV]?.trim();
105213
105207
  const isVertexRoute2 = specifier !== void 0 && vertexModelId !== void 0 && vertexModelId === specifier && isVertexAnthropicId(specifier);
105214
105208
  const model = !specifier ? void 0 : isBedrockRoute ? specifier : isVertexRoute2 ? void 0 : stripProviderPrefix(specifier);
@@ -105256,6 +105250,7 @@ var claude = agent({
105256
105250
  ...homeEnv,
105257
105251
  PWD: repoDir
105258
105252
  };
105253
+ env2.CLAUDE_CODE_AUTO_COMPACT_WINDOW ||= "500000";
105259
105254
  if (isBedrockRoute) {
105260
105255
  env2.CLAUDE_CODE_USE_BEDROCK = "1";
105261
105256
  }
@@ -144165,8 +144160,8 @@ function closeBrowserDaemon(toolState) {
144165
144160
 
144166
144161
  // mcp/checkout.ts
144167
144162
  import { createHash as createHash2 } from "node:crypto";
144168
- import { statSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync8 } from "node:fs";
144169
- import { join as join13 } from "node:path";
144163
+ import { statSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync9 } from "node:fs";
144164
+ import { join as join14 } from "node:path";
144170
144165
 
144171
144166
  // node_modules/.pnpm/@ark+schema@0.56.0/node_modules/@ark/schema/out/shared/errors.js
144172
144167
  var ArkError = class _ArkError extends CastableBase {
@@ -151600,6 +151595,7 @@ function initToolState(params) {
151600
151595
  progressComment: resolved,
151601
151596
  hadProgressComment: !!resolved,
151602
151597
  prepushFailureCount: 0,
151598
+ noopReviewSubmissions: 0,
151603
151599
  backgroundProcesses: /* @__PURE__ */ new Map(),
151604
151600
  usageEntries: []
151605
151601
  };
@@ -151635,6 +151631,367 @@ function ensureRepoState(toolState, init) {
151635
151631
  return created;
151636
151632
  }
151637
151633
 
151634
+ // utils/changeImpact.ts
151635
+ import { spawn as spawn4 } from "node:child_process";
151636
+ import { writeFileSync as writeFileSync7 } from "node:fs";
151637
+ import { join as join8 } from "node:path";
151638
+ var MAX_CANDIDATES = 24;
151639
+ var MAX_ATOM_LENGTH = 128;
151640
+ var MAX_ATOMS = 12;
151641
+ var MAX_REFERENCES = 6;
151642
+ var MAX_EXCERPT = 160;
151643
+ var MAX_BYTES = 16e3;
151644
+ function isSymbolShaped(atom) {
151645
+ return atom.length >= 4 && atom.length <= MAX_ATOM_LENGTH && /[A-Z_]/.test(atom);
151646
+ }
151647
+ function getAtomConfidence(params) {
151648
+ const before = params.text.slice(Math.max(0, params.index - 64), params.index);
151649
+ const after = params.text.slice(
151650
+ params.index + params.atom.length,
151651
+ params.index + params.atom.length + 16
151652
+ );
151653
+ if (/\b(?:class|interface|type|enum|function|def|func|fn|struct|trait)\s+$/.test(before))
151654
+ return 4;
151655
+ if (/\b(?:const|let|var)\s+$/.test(before) || /^\s*\??\s*[:(]/.test(after)) return 3;
151656
+ const quoted = /['"`]$/.test(before) && /^['"`]/.test(after);
151657
+ if (quoted || before.endsWith(".") || after.startsWith(".") || params.atom.includes("_"))
151658
+ return 2;
151659
+ return 1;
151660
+ }
151661
+ function getChangedAtoms(text) {
151662
+ const atoms = /* @__PURE__ */ new Map();
151663
+ for (const match3 of text.matchAll(/[A-Za-z_][A-Za-z0-9_]*/g)) {
151664
+ const atom = match3[0];
151665
+ if (!isSymbolShaped(atom)) continue;
151666
+ const confidence = getAtomConfidence({ text, atom, index: match3.index ?? 0 });
151667
+ atoms.set(atom, Math.max(atoms.get(atom) ?? 0, confidence));
151668
+ }
151669
+ return atoms;
151670
+ }
151671
+ function recordChange(params) {
151672
+ for (const [atom, confidence] of getChangedAtoms(params.text)) {
151673
+ const change = params.changes.get(atom) ?? { atom, confidence, added: [], removed: [] };
151674
+ change.confidence = Math.max(change.confidence, confidence);
151675
+ change[params.location.kind].push(params.location);
151676
+ params.changes.set(atom, change);
151677
+ }
151678
+ }
151679
+ function locationKey(location) {
151680
+ return `${location.path}\0${location.line}`;
151681
+ }
151682
+ function collectFileChanges(collection, file2) {
151683
+ if (!file2.patch) return;
151684
+ let oldLine = 0;
151685
+ let newLine = 0;
151686
+ for (const patchLine of file2.patch.split("\n")) {
151687
+ const hunk = patchLine.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
151688
+ if (hunk) {
151689
+ oldLine = Number.parseInt(hunk[1], 10);
151690
+ newLine = Number.parseInt(hunk[2], 10);
151691
+ } else if (patchLine.startsWith("+")) {
151692
+ const location = { path: file2.filename, line: newLine++, kind: "added" };
151693
+ collection.addedLines.add(locationKey(location));
151694
+ recordChange({ changes: collection.changes, location, text: patchLine.slice(1) });
151695
+ } else if (patchLine.startsWith("-")) {
151696
+ const oldPath = file2.previous_filename ?? file2.filename;
151697
+ recordChange({
151698
+ changes: collection.changes,
151699
+ location: { path: oldPath, line: oldLine++, kind: "removed" },
151700
+ text: patchLine.slice(1)
151701
+ });
151702
+ } else if (!patchLine.startsWith("\\")) {
151703
+ oldLine++;
151704
+ newLine++;
151705
+ }
151706
+ }
151707
+ }
151708
+ function compareChanges(a, b) {
151709
+ if (a.confidence !== b.confidence) return b.confidence - a.confidence;
151710
+ const removedDelta = Number(b.removed.length > 0) - Number(a.removed.length > 0);
151711
+ if (removedDelta !== 0) return removedDelta;
151712
+ const aDelta = Math.abs(a.added.length - a.removed.length);
151713
+ const bDelta = Math.abs(b.added.length - b.removed.length);
151714
+ if (aDelta !== bDelta) return bDelta - aDelta;
151715
+ if (a.atom.length !== b.atom.length) return b.atom.length - a.atom.length;
151716
+ return a.atom < b.atom ? -1 : Number(a.atom > b.atom);
151717
+ }
151718
+ function getCandidates(files) {
151719
+ const collection = {
151720
+ changes: /* @__PURE__ */ new Map(),
151721
+ addedLines: /* @__PURE__ */ new Set()
151722
+ };
151723
+ for (const file2 of files) collectFileChanges(collection, file2);
151724
+ const candidates = [...collection.changes.values()].filter((change) => change.added.length !== change.removed.length).sort(compareChanges);
151725
+ return {
151726
+ selected: candidates.slice(0, MAX_CANDIDATES),
151727
+ total: candidates.length,
151728
+ addedLines: collection.addedLines
151729
+ };
151730
+ }
151731
+ function emptyBuckets(candidates) {
151732
+ return new Map(candidates.map((candidate) => [candidate.atom, { count: 0, samples: [] }]));
151733
+ }
151734
+ function createGrepRecordState() {
151735
+ return {
151736
+ phase: "path",
151737
+ path: "",
151738
+ line: "",
151739
+ sample: "",
151740
+ truncated: false,
151741
+ searchTail: "",
151742
+ searchStart: 0,
151743
+ matches: /* @__PURE__ */ new Set()
151744
+ };
151745
+ }
151746
+ function resetGrepRecord(state) {
151747
+ state.phase = "path";
151748
+ state.path = "";
151749
+ state.line = "";
151750
+ state.sample = "";
151751
+ state.truncated = false;
151752
+ state.searchTail = "";
151753
+ state.searchStart = 0;
151754
+ state.matches.clear();
151755
+ }
151756
+ function isWordCharacter(value2) {
151757
+ return value2 !== void 0 && /[A-Za-z0-9_]/.test(value2);
151758
+ }
151759
+ function recordCandidateMatches(params) {
151760
+ for (const candidate of params.candidates) {
151761
+ if (params.matches.has(candidate.atom)) continue;
151762
+ let index = params.text.indexOf(candidate.atom, params.minStart);
151763
+ while (index >= 0 && index < params.maxStart) {
151764
+ const before = params.text[index - 1];
151765
+ const after = params.text[index + candidate.atom.length];
151766
+ if (!isWordCharacter(before) && !isWordCharacter(after)) {
151767
+ params.matches.add(candidate.atom);
151768
+ break;
151769
+ }
151770
+ index = params.text.indexOf(candidate.atom, index + 1);
151771
+ }
151772
+ }
151773
+ }
151774
+ function consumeReferenceText(params) {
151775
+ const remaining = MAX_EXCERPT - params.state.sample.length;
151776
+ params.state.sample += params.text.slice(0, Math.max(remaining, 0));
151777
+ if (params.text.length > remaining) params.state.truncated = true;
151778
+ const searchable = params.state.searchTail + params.text;
151779
+ const consumed = params.final ? searchable.length : Math.max(0, searchable.length - params.maxAtomLength - 2);
151780
+ recordCandidateMatches({
151781
+ text: searchable,
151782
+ minStart: params.state.searchStart,
151783
+ maxStart: consumed,
151784
+ candidates: params.candidates,
151785
+ matches: params.state.matches
151786
+ });
151787
+ if (params.final) {
151788
+ params.state.searchTail = "";
151789
+ params.state.searchStart = 0;
151790
+ return;
151791
+ }
151792
+ const tailStart = Math.max(0, consumed - 1);
151793
+ params.state.searchTail = searchable.slice(tailStart);
151794
+ params.state.searchStart = consumed > 0 ? 1 : params.state.searchStart;
151795
+ }
151796
+ function recordGrepReference(params) {
151797
+ const prefix = `${params.treeish}:`;
151798
+ const path4 = params.state.path.startsWith(prefix) ? params.state.path.slice(prefix.length) : params.state.path;
151799
+ const line = Number.parseInt(params.state.line, 10);
151800
+ if (!Number.isFinite(line)) return;
151801
+ const reference2 = {
151802
+ path: path4,
151803
+ line,
151804
+ text: params.state.sample,
151805
+ truncated: params.state.truncated
151806
+ };
151807
+ if (params.addedLines.has(locationKey(reference2))) return;
151808
+ for (const atom of params.state.matches) {
151809
+ const bucket = params.buckets.get(atom);
151810
+ if (!bucket) continue;
151811
+ bucket.count++;
151812
+ if (bucket.samples.length < MAX_REFERENCES) bucket.samples.push(reference2);
151813
+ }
151814
+ }
151815
+ function consumeGrepChunk(params) {
151816
+ let offset = 0;
151817
+ while (offset < params.chunk.length) {
151818
+ if (params.state.phase !== "text") {
151819
+ const end2 = params.chunk.indexOf("\0", offset);
151820
+ const value2 = end2 < 0 ? params.chunk.slice(offset) : params.chunk.slice(offset, end2);
151821
+ params.state[params.state.phase] += value2;
151822
+ if (end2 < 0) return;
151823
+ params.state.phase = params.state.phase === "path" ? "line" : "text";
151824
+ offset = end2 + 1;
151825
+ continue;
151826
+ }
151827
+ const end = params.chunk.indexOf("\n", offset);
151828
+ const final = end >= 0;
151829
+ const text = final ? params.chunk.slice(offset, end) : params.chunk.slice(offset);
151830
+ consumeReferenceText({
151831
+ state: params.state,
151832
+ text,
151833
+ candidates: params.candidates,
151834
+ maxAtomLength: params.maxAtomLength,
151835
+ final
151836
+ });
151837
+ if (!final) return;
151838
+ recordGrepReference(params);
151839
+ resetGrepRecord(params.state);
151840
+ offset = end + 1;
151841
+ }
151842
+ }
151843
+ async function lookupReferences(params) {
151844
+ const buckets = emptyBuckets(params.candidates);
151845
+ if (params.candidates.length === 0) return { buckets, error: void 0 };
151846
+ const patterns = params.candidates.flatMap((candidate) => ["-e", candidate.atom]);
151847
+ const child = spawn4(
151848
+ "git",
151849
+ [
151850
+ "grep",
151851
+ "--no-color",
151852
+ "--full-name",
151853
+ "-n",
151854
+ "-z",
151855
+ "-I",
151856
+ "-w",
151857
+ "-F",
151858
+ ...patterns,
151859
+ params.treeish,
151860
+ "--",
151861
+ ":(top)"
151862
+ ],
151863
+ { env: resolveEnv(void 0), stdio: ["ignore", "pipe", "pipe"] }
151864
+ );
151865
+ const state = createGrepRecordState();
151866
+ const maxAtomLength = Math.max(...params.candidates.map((candidate) => candidate.atom.length));
151867
+ let spawnError;
151868
+ child.stdout.setEncoding("utf8");
151869
+ child.stdout.on("data", (chunk) => {
151870
+ consumeGrepChunk({
151871
+ chunk,
151872
+ state,
151873
+ candidates: params.candidates,
151874
+ maxAtomLength,
151875
+ buckets,
151876
+ addedLines: params.addedLines,
151877
+ treeish: params.treeish
151878
+ });
151879
+ });
151880
+ child.stderr.resume();
151881
+ const status = await new Promise((resolve3) => {
151882
+ child.once("error", (error49) => {
151883
+ spawnError = error49.message;
151884
+ resolve3(null);
151885
+ });
151886
+ child.once("close", resolve3);
151887
+ });
151888
+ if (spawnError || status !== 0 && status !== 1) {
151889
+ const error49 = spawnError ?? `git grep exit ${status ?? "unknown"}`;
151890
+ log.warning(`\xBB change impact lookup failed: ${error49}`);
151891
+ return { buckets: emptyBuckets(params.candidates), error: error49 };
151892
+ }
151893
+ if (state.phase === "text") {
151894
+ consumeReferenceText({
151895
+ state,
151896
+ text: "",
151897
+ candidates: params.candidates,
151898
+ maxAtomLength,
151899
+ final: true
151900
+ });
151901
+ recordGrepReference({ state, buckets, addedLines: params.addedLines, treeish: params.treeish });
151902
+ } else if (state.phase !== "path" || state.path) {
151903
+ const error49 = "malformed git grep output";
151904
+ log.warning(`\xBB change impact lookup failed: ${error49}`);
151905
+ return { buckets: emptyBuckets(params.candidates), error: error49 };
151906
+ }
151907
+ return { buckets, error: void 0 };
151908
+ }
151909
+ function formatReference(reference2) {
151910
+ const text = reference2.text.trim();
151911
+ const excerpt = reference2.truncated ? `${text.slice(0, MAX_EXCERPT - 1)}\u2026` : text;
151912
+ return `- ${JSON.stringify(reference2.path)}:${reference2.line} ${JSON.stringify(excerpt)}`;
151913
+ }
151914
+ function formatChangedLocations(change) {
151915
+ const selected = [change.removed[0], change.added[0]].filter(
151916
+ (location) => location !== void 0
151917
+ );
151918
+ const rendered = selected.map((location) => `${location.kind} ${JSON.stringify(location.path)}:${location.line}`).join(", ");
151919
+ const omitted = change.removed.length + change.added.length - selected.length;
151920
+ return omitted > 0 ? `${rendered}; ${omitted} more changed location(s) omitted` : rendered;
151921
+ }
151922
+ function formatEntry(entry) {
151923
+ const count = entry.bucket.count;
151924
+ const sampleNote = count > entry.bucket.samples.length ? `${count} matched line(s); first ${entry.bucket.samples.length} shown.` : `${count} matched line(s).`;
151925
+ return [
151926
+ `## \`${entry.change.atom}\``,
151927
+ "",
151928
+ `Changed at: ${formatChangedLocations(entry.change)}`,
151929
+ `Tracked references: ${sampleNote}`,
151930
+ "",
151931
+ ...entry.bucket.samples.map(formatReference)
151932
+ ].join("\n");
151933
+ }
151934
+ function formatArtifact(params) {
151935
+ const lines = [
151936
+ "# Change impact leads",
151937
+ "",
151938
+ "> Supplemental and explicitly incomplete. Read the authoritative `diffPath` TOC and raw lines first. Absence here is not evidence of no impact.",
151939
+ "",
151940
+ `Scope: base ${params.baseSha}, head ${params.headSha}; ${params.fileCount} changed file(s).`,
151941
+ "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.",
151942
+ "",
151943
+ `Candidate atoms searched: ${params.candidateCount}/${params.totalCandidateCount}.`
151944
+ ];
151945
+ if (params.lookupError) lines.push(`Tracked-file lookup unavailable: ${params.lookupError}.`);
151946
+ let content = `${lines.join("\n")}
151947
+ `;
151948
+ let renderedAtomCount = 0;
151949
+ let referenceCount = 0;
151950
+ for (const entry of params.entries.filter((candidate) => candidate.bucket.count > 0)) {
151951
+ if (renderedAtomCount >= MAX_ATOMS) break;
151952
+ const addition = `
151953
+ ${formatEntry(entry)}
151954
+ `;
151955
+ if (Buffer.byteLength(content + addition) > MAX_BYTES) break;
151956
+ content += addition;
151957
+ renderedAtomCount++;
151958
+ referenceCount += entry.bucket.count;
151959
+ }
151960
+ if (renderedAtomCount === 0) {
151961
+ const empty = "\nNo bounded tracked references found. This is not evidence of no wider impact.\n";
151962
+ if (Buffer.byteLength(content + empty) <= MAX_BYTES) content += empty;
151963
+ }
151964
+ return { content, renderedAtomCount, referenceCount };
151965
+ }
151966
+ async function createChangeImpactArtifact(ctx, params) {
151967
+ const candidates = getCandidates(params.files);
151968
+ const lookup2 = await lookupReferences({
151969
+ candidates: candidates.selected,
151970
+ addedLines: candidates.addedLines,
151971
+ treeish: params.headSha
151972
+ });
151973
+ const artifact = formatArtifact({
151974
+ entries: candidates.selected.map((change) => {
151975
+ return { change, bucket: lookup2.buckets.get(change.atom) ?? { count: 0, samples: [] } };
151976
+ }),
151977
+ candidateCount: candidates.selected.length,
151978
+ totalCandidateCount: candidates.total,
151979
+ fileCount: params.files.length,
151980
+ baseSha: params.baseSha,
151981
+ headSha: params.headSha,
151982
+ lookupError: lookup2.error
151983
+ });
151984
+ const path4 = join8(ctx.tmpdir, `pr-${params.pullNumber}-${params.headSha.slice(0, 7)}-impact.md`);
151985
+ writeFileSync7(path4, artifact.content);
151986
+ return {
151987
+ path: path4,
151988
+ candidateCount: candidates.selected.length,
151989
+ renderedAtomCount: artifact.renderedAtomCount,
151990
+ referenceCount: artifact.referenceCount,
151991
+ bytes: Buffer.byteLength(artifact.content)
151992
+ };
151993
+ }
151994
+
151638
151995
  // utils/diffCoverage.ts
151639
151996
  import { isAbsolute, normalize as normalize2, resolve } from "node:path";
151640
151997
  function countLines(params) {
@@ -151722,27 +152079,6 @@ function getDiffCoverageBreakdown(params) {
151722
152079
  files
151723
152080
  };
151724
152081
  }
151725
- function renderDiffCoverageBreakdown(params) {
151726
- const breakdown = params.breakdown;
151727
- const lines = [];
151728
- lines.push(`diff coverage report for \`${params.diffPath}\``);
151729
- lines.push(
151730
- `overall: ${breakdown.coveredLines}/${breakdown.totalLines} lines read (${breakdown.coveragePercent}%), unread: ${breakdown.unreadLines}`
151731
- );
151732
- lines.push(`covered ranges: ${formatRanges({ ranges: breakdown.coveredRanges })}`);
151733
- lines.push(`unread ranges: ${formatRanges({ ranges: breakdown.unreadRanges })}`);
151734
- lines.push("");
151735
- lines.push("per-file TOC coverage:");
151736
- for (const file2 of breakdown.files) {
151737
- const filePercent = file2.totalLines ? Number((file2.coveredLines / file2.totalLines * 100).toFixed(2)) : 100;
151738
- lines.push(
151739
- `- ${file2.filename} (toc lines ${file2.startLine}-${file2.endLine}): ${file2.coveredLines}/${file2.totalLines} lines read (${filePercent}%)`
151740
- );
151741
- lines.push(` read: ${formatRanges({ ranges: file2.coveredRanges })}`);
151742
- lines.push(` unread: ${formatRanges({ ranges: file2.unreadRanges })}`);
151743
- }
151744
- return lines.join("\n");
151745
- }
151746
152082
  function resolveOffsetBase(params) {
151747
152083
  const lower2 = params.toolName.toLowerCase();
151748
152084
  if (lower2 === "readfile" || lower2.endsWith(".readfile")) {
@@ -151873,10 +152209,6 @@ function countLinesInRanges(params) {
151873
152209
  }
151874
152210
  return total;
151875
152211
  }
151876
- function formatRanges(params) {
151877
- if (params.ranges.length === 0) return "none";
151878
- return params.ranges.map((range2) => `${range2.startLine}-${range2.endLine}`).join(", ");
151879
- }
151880
152212
  function clampLine(params) {
151881
152213
  if (params.value < 1) return 1;
151882
152214
  if (params.value > params.totalLines) return params.totalLines;
@@ -151903,7 +152235,7 @@ function readNumber(params) {
151903
152235
  import { execSync } from "node:child_process";
151904
152236
  import { createHash } from "node:crypto";
151905
152237
  import { readFileSync as readFileSync3, realpathSync, unlinkSync } from "node:fs";
151906
- import { join as join8 } from "node:path";
152238
+ import { join as join9 } from "node:path";
151907
152239
 
151908
152240
  // utils/shell.ts
151909
152241
  import { spawnSync as spawnSync4 } from "node:child_process";
@@ -151981,7 +152313,7 @@ function resolveHooksDir(cwd, gitPath) {
151981
152313
  cwd,
151982
152314
  log: false
151983
152315
  }).trim();
151984
- const hooksDir = join8(commonDir, "hooks");
152316
+ const hooksDir = join9(commonDir, "hooks");
151985
152317
  hooksDirCache.set(cwd, hooksDir);
151986
152318
  return hooksDir;
151987
152319
  }
@@ -153129,13 +153461,13 @@ function _op(fn2, options) {
153129
153461
 
153130
153462
  // mcp/git.ts
153131
153463
  import { randomUUID as randomUUID3 } from "node:crypto";
153132
- import { writeFileSync as writeFileSync7 } from "node:fs";
153133
- import { join as join11 } from "node:path";
153464
+ import { writeFileSync as writeFileSync8 } from "node:fs";
153465
+ import { join as join12 } from "node:path";
153134
153466
 
153135
153467
  // utils/apiCommit.ts
153136
153468
  import { execFileSync as execFileSync7 } from "node:child_process";
153137
153469
  import { lstat, readlink } from "node:fs/promises";
153138
- import { join as join9 } from "node:path";
153470
+ import { join as join10 } from "node:path";
153139
153471
  var GITHUB_API = "https://api.github.com";
153140
153472
  var MAX_BLOB_BYTES = 30 * 1024 * 1024;
153141
153473
  var BLOB_UPLOAD_CONCURRENCY = 8;
@@ -153200,7 +153532,7 @@ async function assertApiCommittable(files) {
153200
153532
  const root = getRepoRoot();
153201
153533
  const attrs = $2(
153202
153534
  "git",
153203
- ["check-attr", "filter", "-z", "--", ...present.map((p2) => join9(root, p2))],
153535
+ ["check-attr", "filter", "-z", "--", ...present.map((p2) => join10(root, p2))],
153204
153536
  { log: false }
153205
153537
  );
153206
153538
  const parts = attrs.split("\0");
@@ -153212,7 +153544,7 @@ async function assertApiCommittable(files) {
153212
153544
  }
153213
153545
  }
153214
153546
  for (const path4 of present) {
153215
- const stat = await lstat(join9(root, path4));
153547
+ const stat = await lstat(join10(root, path4));
153216
153548
  if (stat.isDirectory()) {
153217
153549
  throw new Error(
153218
153550
  `'${path4}' is a directory (nested repository or submodule?) \u2014 signed commits only support files and symlinks.`
@@ -153221,7 +153553,7 @@ async function assertApiCommittable(files) {
153221
153553
  }
153222
153554
  }
153223
153555
  async function createBlobEntry(params) {
153224
- const absPath = join9(params.repoRoot, params.path);
153556
+ const absPath = join10(params.repoRoot, params.path);
153225
153557
  const stat = await lstat(absPath);
153226
153558
  if (stat.size > MAX_BLOB_BYTES) {
153227
153559
  throw new Error(
@@ -153401,7 +153733,7 @@ this usually means your local branch has commits that were never pushed \u2014 s
153401
153733
  var core3 = __toESM(require_core(), 1);
153402
153734
  import { createSign } from "node:crypto";
153403
153735
  import { rename, writeFile } from "node:fs/promises";
153404
- import { dirname as dirname3, join as join10 } from "node:path";
153736
+ import { dirname as dirname3, join as join11 } from "node:path";
153405
153737
 
153406
153738
  // node_modules/.pnpm/@octokit+plugin-throttling@11.0.3_@octokit+core@7.0.5/node_modules/@octokit/plugin-throttling/dist-bundle/index.js
153407
153739
  var import_light = __toESM(require_light(), 1);
@@ -157345,7 +157677,7 @@ function getGitHubUsageSummary() {
157345
157677
  }
157346
157678
  async function writeGitHubUsageSummaryToFile(path4) {
157347
157679
  const summary2 = getGitHubUsageSummary();
157348
- const tmpPath = join10(dirname3(path4), `.usage-summary-${process.pid}.tmp`);
157680
+ const tmpPath = join11(dirname3(path4), `.usage-summary-${process.pid}.tmp`);
157349
157681
  await writeFile(tmpPath, JSON.stringify(summary2));
157350
157682
  await rename(tmpPath, path4);
157351
157683
  }
@@ -158472,8 +158804,8 @@ function countAhead(head, base, cwd) {
158472
158804
  function spillGitOutput(params) {
158473
158805
  const tempDir = process.env.PULLFROG_TEMP_DIR;
158474
158806
  if (!tempDir) throw new Error("PULLFROG_TEMP_DIR not set");
158475
- const outputPath = join11(tempDir, `git-${params.command}-${randomUUID3().slice(0, 8)}.txt`);
158476
- writeFileSync7(outputPath, params.output);
158807
+ const outputPath = join12(tempDir, `git-${params.command}-${randomUUID3().slice(0, 8)}.txt`);
158808
+ writeFileSync8(outputPath, params.output);
158477
158809
  const previewByLines = params.output.split("\n").slice(0, OVERFLOW_PREVIEW_LINES).join("\n");
158478
158810
  const preview = previewByLines.length <= OVERFLOW_PREVIEW_MAX_CHARS ? previewByLines : `${previewByLines.slice(0, OVERFLOW_PREVIEW_MAX_CHARS)}\u2026`;
158479
158811
  log.info(
@@ -158702,7 +159034,10 @@ function formatModelLabel(params) {
158702
159034
  modelAliases.find((a) => a.resolve === params.model || a.openRouterResolve === params.model);
158703
159035
  const displayName = alias?.displayName ?? params.model;
158704
159036
  if (params.oss) {
158705
- return `\`${displayName}\` (free via [Pullfrog for OSS](https://pullfrog.com/for-oss))`;
159037
+ const ossBase = `\`${displayName}\` (free via [Pullfrog for OSS](https://pullfrog.com/for-oss))`;
159038
+ if (params.clamped?.reason !== "oss") return ossBase;
159039
+ const configured = isAutoTier(params.clamped.from) ? "the intelligent tier" : `\`${resolveDisplayAlias(params.clamped.from)?.displayName ?? params.clamped.from}\``;
159040
+ return `${ossBase} (${configured} not used \u2014 the program covers this model; add its [provider key](https://docs.pullfrog.com/models) to run your pick)`;
158706
159041
  }
158707
159042
  const base = alias?.isFree ? `\`${displayName}\` (free)` : `\`${displayName}\``;
158708
159043
  if (params.fallbackFrom) {
@@ -159105,13 +159440,15 @@ function addFooter(ctx, body) {
159105
159440
  var Comment = type({
159106
159441
  issueNumber: type.number.describe("the issue number to comment on"),
159107
159442
  body: type.string.describe("the comment body content"),
159108
- type: type.enumerated("Plan", "Comment").describe("Plan: record as the plan for this run. Comment: regular comment (default).").optional()
159443
+ type: type.enumerated("Plan", "Comment").describe(
159444
+ "Plan: standalone plan comment on another target. Comment: regular comment (default)."
159445
+ ).optional()
159109
159446
  });
159110
159447
  function CreateCommentTool(ctx) {
159111
159448
  return tool({
159112
159449
  name: "create_issue_comment",
159113
159450
  mutates: true,
159114
- 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.',
159451
+ 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.",
159115
159452
  parameters: Comment,
159116
159453
  execute: execute(async ({ issueNumber, body, type: commentType }) => {
159117
159454
  const bodyWithFooter = addFooter(ctx, body);
@@ -159123,9 +159460,6 @@ function CreateCommentTool(ctx) {
159123
159460
  });
159124
159461
  ctx.toolState.wasUpdated = true;
159125
159462
  log.info(`\xBB created comment ${result.data.id}`);
159126
- if (commentType !== "Plan" && issueNumber === ctx.payload.event.issue_number) {
159127
- ctx.toolState.answerCommentPosted = true;
159128
- }
159129
159463
  if (commentType === "Plan") {
159130
159464
  if (result.data.node_id) {
159131
159465
  await patchWorkflowRunFields(ctx, { planCommentNodeId: result.data.node_id });
@@ -159614,12 +159948,23 @@ function duplicateReviewDecision(params) {
159614
159948
  reason: `review ${existing.id} was already submitted in this session; ignoring duplicate call (call \`checkout_pr\` again first if new commits were pushed)`
159615
159949
  };
159616
159950
  }
159951
+ var VERDICT_MARKERS = ["> \u2705", "> \u2139\uFE0F", "> [!IMPORTANT]", "> [!CAUTION]"];
159952
+ var MIN_UNMARKED_BODY_LENGTH = 20;
159953
+ var PLACEHOLDER_PATTERN = /\b(test|testing|placeholder|temp|foo|bar|asdf|dummy|sample)\b/i;
159954
+ var PLACEHOLDER_SCAN_LENGTH = 50;
159955
+ function isDegenerateReviewBody(body) {
159956
+ const trimmed = body.trim();
159957
+ if (VERDICT_MARKERS.some((marker) => trimmed.startsWith(marker))) return false;
159958
+ if (trimmed.length < MIN_UNMARKED_BODY_LENGTH) return true;
159959
+ return trimmed.length < PLACEHOLDER_SCAN_LENGTH && PLACEHOLDER_PATTERN.test(trimmed);
159960
+ }
159961
+ var MAX_CONSECUTIVE_NOOP_SUBMISSIONS = 4;
159617
159962
  function reviewSkipDecision(params) {
159618
159963
  if (params.body || params.hasComments) return null;
159619
159964
  if (!params.approved) {
159620
159965
  return {
159621
159966
  kind: "no-issues",
159622
- reason: params.requestChanges ? "request_changes with no body or comments \u2014 nothing to block on" : "no issues found \u2014 nothing to post"
159967
+ reason: params.requestChanges ? "request_changes with no body or comments \u2014 nothing to block on" : "this call carried neither `body` nor `comments`, so nothing was posted. if you found no issues, you are done \u2014 do not call this tool again. if you meant to submit a review, the `body` parameter was missing from your tool call: resend it with `body` set to the full review text. if a resend drops `body` again, do not keep retrying and do not probe with placeholder text \u2014 write the review into `create_issue_comment` instead, which takes the same text under a smaller schema and is editable after posting. every successful call here posts a permanent, publicly visible review."
159623
159968
  };
159624
159969
  }
159625
159970
  if (!params.prApproveEnabled) {
@@ -159647,9 +159992,15 @@ function formatDroppedCommentsNote(dropped) {
159647
159992
  }
159648
159993
  var CreatePullRequestReview = type({
159649
159994
  pull_number: type.number.describe("The pull request number to review"),
159995
+ // REQUIRED on purpose, not because an empty review is invalid — pass "" for
159996
+ // that. models that emit tool calls without schema-constrained decoding drop
159997
+ // optional parameters and keep required ones; in the incident log every
159998
+ // required parameter survived and this one, when optional, vanished ~15 times
159999
+ // running. required-ness is the only pressure that empirically held.
160000
+ // see wiki/review-approval.md.
159650
160001
  body: type.string.describe(
159651
- "1-2 sentence high-level summary with urgency level, critical callouts, and feedback about code outside the diff. Specific feedback on diff lines goes in 'comments' array."
159652
- ).optional(),
160002
+ `1-2 sentence high-level summary with urgency level, critical callouts, and feedback about code outside the diff. Specific feedback on diff lines goes in 'comments' array. ALWAYS pass this parameter \u2014 pass an empty string "" when approving with no commentary, never omit it.`
160003
+ ),
159653
160004
  approved: type.boolean.describe(
159654
160005
  "Set to true to submit as an approval. Use for `> \u2705 No new issues found.` reviews where the PR is mergeable as-is and nothing in the body warrants code changes \u2014 approving also suppresses the Fix-button footer affordance so users don't dispatch a fix run on non-actionable feedback. Reserve approved: false for `> \u2139\uFE0F ...` (minor suggestions inline), `> [!IMPORTANT]` (recommended changes), and `> [!CAUTION]` (critical) reviews. Defaults to false (comment-only review). Mutually exclusive with request_changes. Approval is REJECTED while any unresolved Pullfrog review thread remains open on the PR (not just the latest commit's diff): resolve the threads the current code addresses (reply + resolve_review_thread) first, or submit a non-approving review if a real issue remains."
159655
160006
  ).optional(),
@@ -159694,6 +160045,11 @@ function CreatePullRequestReviewTool(ctx) {
159694
160045
  );
159695
160046
  }
159696
160047
  if (body) body = fixDoubleEscapedString(body);
160048
+ if (body && isDegenerateReviewBody(body)) {
160049
+ throw new Error(
160050
+ `refusing to submit a review whose body looks like placeholder text: ${JSON.stringify(body)}. every submitted review is permanent and publicly visible on the contributor's PR \u2014 never probe this tool with test content. if the tool has been rejecting your submissions, the cause is your payload, not the tool: re-send with \`body\` set to the full review text, opening with a verdict marker (${VERDICT_MARKERS.join(", ")}).`
160051
+ );
160052
+ }
159697
160053
  const primary = primaryRepoState(ctx.toolState);
159698
160054
  primary.issueNumber = pull_number;
159699
160055
  const dup = duplicateReviewDecision({
@@ -159738,9 +160094,16 @@ function CreatePullRequestReviewTool(ctx) {
159738
160094
  prApproveEnabled: ctx.prApproveEnabled && !selfAuthored
159739
160095
  });
159740
160096
  if (skip) {
160097
+ ctx.toolState.noopReviewSubmissions += 1;
159741
160098
  log.info(`skipping review submission: ${skip.reason}`);
160099
+ if (ctx.toolState.noopReviewSubmissions >= MAX_CONSECUTIVE_NOOP_SUBMISSIONS) {
160100
+ throw new Error(
160101
+ `${ctx.toolState.noopReviewSubmissions} consecutive review submissions posted nothing \u2014 every one carried an empty payload. stop calling this tool: the run will report the review as unsubmitted. if you have review feedback, your tool calls are dropping the \`body\` parameter \u2014 write the review into \`create_issue_comment\` instead, which takes the same text and is recoverable if it goes wrong.`
160102
+ );
160103
+ }
159742
160104
  return { success: true, skipped: true, reason: skip.reason };
159743
160105
  }
160106
+ ctx.toolState.noopReviewSubmissions = 0;
159744
160107
  let event = approved ? "APPROVE" : request_changes ? "REQUEST_CHANGES" : "COMMENT";
159745
160108
  if (event === "APPROVE" || event === "REQUEST_CHANGES") {
159746
160109
  if (!ctx.prApproveEnabled) {
@@ -159931,10 +160294,6 @@ function runDiffCoveragePreflight(params) {
159931
160294
  unread.push({ path: file2.filename, ranges: rangesText, unreadLines: fileUnreadLines });
159932
160295
  unreadLines += fileUnreadLines;
159933
160296
  }
159934
- coverageState.lastBreakdown = renderDiffCoverageBreakdown({
159935
- diffPath: coverageState.diffPath,
159936
- breakdown
159937
- });
159938
160297
  log.debug(
159939
160298
  `diff coverage pre-flight breakdown: coveredLines=${breakdown.coveredLines}, unreadLines=${unreadLines}`
159940
160299
  );
@@ -159950,9 +160309,7 @@ function runDiffCoveragePreflight(params) {
159950
160309
  `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.
159951
160310
 
159952
160311
  unread TOC regions:
159953
- ${unreadText}
159954
-
159955
- ${coverageState.lastBreakdown}`
160312
+ ${unreadText}`
159956
160313
  );
159957
160314
  }
159958
160315
  async function clearStrandedPendingReview(ctx, params) {
@@ -160074,9 +160431,9 @@ async function reportReviewNodeId(ctx, params) {
160074
160431
  import { execFileSync as execFileSync8, execSync as execSync2 } from "node:child_process";
160075
160432
  import { mkdtempSync as mkdtempSync2, readdirSync, realpathSync as realpathSync2, unlinkSync as unlinkSync2 } from "node:fs";
160076
160433
  import { tmpdir as tmpdir3 } from "node:os";
160077
- import { join as join12 } from "node:path";
160434
+ import { join as join13 } from "node:path";
160078
160435
  function createTempDirectory() {
160079
- const sharedTempDir = mkdtempSync2(join12(tmpdir3(), "pullfrog-"));
160436
+ const sharedTempDir = mkdtempSync2(join13(tmpdir3(), "pullfrog-"));
160080
160437
  process.env.PULLFROG_TEMP_DIR = sharedTempDir;
160081
160438
  log.info(`\xBB created temp dir at ${sharedTempDir}`);
160082
160439
  return sharedTempDir;
@@ -160121,13 +160478,13 @@ function wipeRunnerLeakSurface() {
160121
160478
  return [];
160122
160479
  }
160123
160480
  };
160124
- const fileCommandsDir = join12(runnerTemp, "_runner_file_commands");
160481
+ const fileCommandsDir = join13(runnerTemp, "_runner_file_commands");
160125
160482
  for (const entry of listDir(fileCommandsDir)) {
160126
- tryUnlink(join12(fileCommandsDir, entry));
160483
+ tryUnlink(join13(fileCommandsDir, entry));
160127
160484
  }
160128
160485
  for (const entry of listDir(runnerTemp)) {
160129
160486
  if (entry.endsWith(".sh") || /^git-credentials-.*\.config$/.test(entry)) {
160130
- tryUnlink(join12(runnerTemp, entry));
160487
+ tryUnlink(join13(runnerTemp, entry));
160131
160488
  }
160132
160489
  }
160133
160490
  if (wiped.length > 0) {
@@ -160633,21 +160990,23 @@ function CheckoutPrTool(ctx) {
160633
160990
  "PULLFROG_TEMP_DIR not set - checkout_pr must run in pullfrog action context"
160634
160991
  );
160635
160992
  }
160636
- const headShort = primary.checkoutSha.slice(0, 7);
160993
+ const checkoutSha = primary.checkoutSha;
160994
+ if (!checkoutSha) throw new Error("checkout completed without a resolved HEAD SHA");
160995
+ const headShort = checkoutSha.slice(0, 7);
160637
160996
  let incrementalDiffPath;
160638
- if (primary.beforeSha && primary.checkoutSha) {
160997
+ if (primary.beforeSha) {
160639
160998
  const beforeShort = primary.beforeSha.slice(0, 7);
160640
160999
  const incremental = computeIncrementalDiff({
160641
161000
  baseBranch: pr.baseRef,
160642
161001
  beforeSha: primary.beforeSha,
160643
- headSha: primary.checkoutSha
161002
+ headSha: checkoutSha
160644
161003
  });
160645
161004
  if (incremental) {
160646
- incrementalDiffPath = join13(
161005
+ incrementalDiffPath = join14(
160647
161006
  tempDir,
160648
161007
  `pr-${pull_number}-${beforeShort}-${headShort}-incremental.diff`
160649
161008
  );
160650
- writeFileSync8(incrementalDiffPath, incremental);
161009
+ writeFileSync9(incrementalDiffPath, incremental);
160651
161010
  log.info(
160652
161011
  `\xBB incremental diff computed (${incremental.length} bytes) \u2192 ${incrementalDiffPath}`
160653
161012
  );
@@ -160657,8 +161016,8 @@ function CheckoutPrTool(ctx) {
160657
161016
  const diffPreview = formatResult.content.split("\n").slice(0, 100).join("\n");
160658
161017
  log.debug(`formatted diff preview (first 100 lines):
160659
161018
  ${diffPreview}`);
160660
- const diffPath = join13(tempDir, `pr-${pull_number}-${headShort}.diff`);
160661
- writeFileSync8(diffPath, formatResult.content);
161019
+ const diffPath = join14(tempDir, `pr-${pull_number}-${headShort}.diff`);
161020
+ writeFileSync9(diffPath, formatResult.content);
160662
161021
  log.debug(`wrote diff to ${diffPath} (${formatResult.content.length} bytes)`);
160663
161022
  primary.diffCoverage = createDiffCoverageState({
160664
161023
  diffPath,
@@ -160669,14 +161028,56 @@ ${diffPreview}`);
160669
161028
  log.debug(
160670
161029
  `\xBB diff coverage initialized: diffPath=${diffPath}, totalLines=${primary.diffCoverage.totalLines}, tocEntries=${primary.diffCoverage.tocEntries.length}`
160671
161030
  );
161031
+ let impactPath;
161032
+ if (process.env.PULLFROG_DISABLE_CHANGE_IMPACT === "1") {
161033
+ log.info("\xBB change impact disabled by PULLFROG_DISABLE_CHANGE_IMPACT");
161034
+ } else {
161035
+ let revisionVerified = false;
161036
+ let revisionFailure;
161037
+ try {
161038
+ const latest = await ctx.octokit.rest.pulls.get({
161039
+ owner: ctx.repo.owner,
161040
+ repo: ctx.repo.name,
161041
+ pull_number
161042
+ });
161043
+ revisionVerified = latest.data.head.sha === checkoutSha && latest.data.base.sha === prResponse.data.base.sha;
161044
+ } catch (error49) {
161045
+ revisionFailure = error49 instanceof Error ? error49.message : String(error49);
161046
+ }
161047
+ if (revisionFailure !== void 0) {
161048
+ log.warning(
161049
+ `\xBB change impact skipped: PR revision could not be verified (${revisionFailure})`
161050
+ );
161051
+ } else if (!revisionVerified) {
161052
+ log.warning("\xBB change impact skipped: PR revision changed while the diff was fetched");
161053
+ } else {
161054
+ try {
161055
+ const impact = await createChangeImpactArtifact(ctx, {
161056
+ files: formatResult.files,
161057
+ pullNumber: pull_number,
161058
+ baseSha: prResponse.data.base.sha,
161059
+ headSha: checkoutSha
161060
+ });
161061
+ impactPath = impact.path;
161062
+ log.info(
161063
+ `\xBB change impact: ${impact.candidateCount} candidate atom(s), ${impact.renderedAtomCount} detailed atom(s), ${impact.referenceCount} tracked reference(s), ${impact.bytes} bytes \u2192 ${impact.path}`
161064
+ );
161065
+ } catch (error49) {
161066
+ log.warning(
161067
+ `\xBB change impact skipped: generation failed (${error49 instanceof Error ? error49.message : String(error49)})`
161068
+ );
161069
+ }
161070
+ }
161071
+ }
160672
161072
  const cached4 = /* @__PURE__ */ new Map();
160673
161073
  for (const file2 of formatResult.files) {
160674
161074
  cached4.set(file2.filename, commentableLinesForFile(file2.patch));
160675
161075
  }
160676
161076
  primary.commentableLinesByFile = cached4;
160677
161077
  primary.commentableLinesPullNumber = pull_number;
160678
- primary.commentableLinesCheckoutSha = primary.checkoutSha;
160679
- 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.` : "";
161078
+ primary.commentableLinesCheckoutSha = checkoutSha;
161079
+ 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. ` : "";
161080
+ 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.` : "";
160680
161081
  const COMMIT_LOG_MAX = 200;
160681
161082
  const baseRange = `origin/${pr.baseRef}..HEAD`;
160682
161083
  let commitCount = 0;
@@ -160712,6 +161113,7 @@ ${diffPreview}`);
160712
161113
  url: prResponse.data.html_url,
160713
161114
  headRepo: pr.headRepoFullName,
160714
161115
  diffPath,
161116
+ impactPath,
160715
161117
  incrementalDiffPath,
160716
161118
  toc: formatResult.toc,
160717
161119
  commitCount,
@@ -160719,7 +161121,7 @@ ${diffPreview}`);
160719
161121
  commitLogTruncated,
160720
161122
  commitLogUnavailable,
160721
161123
  hookWarning: checkoutResult.hookWarning,
160722
- 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
161124
+ 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
160723
161125
  };
160724
161126
  };
160725
161127
  return tool({
@@ -160766,8 +161168,8 @@ ${dirty}`
160766
161168
  }
160767
161169
 
160768
161170
  // mcp/checkSuite.ts
160769
- import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync9 } from "node:fs";
160770
- import { join as join14 } from "node:path";
161171
+ import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync10 } from "node:fs";
161172
+ import { join as join15 } from "node:path";
160771
161173
  var GetCheckSuiteLogs = type({
160772
161174
  check_suite_id: type.number.describe("the id from check_suite.id")
160773
161175
  });
@@ -160863,7 +161265,7 @@ function GetCheckSuiteLogsTool(ctx) {
160863
161265
  if (!tempDir) {
160864
161266
  throw new Error("PULLFROG_TEMP_DIR not set");
160865
161267
  }
160866
- const logsDir = join14(tempDir, "ci-logs");
161268
+ const logsDir = join15(tempDir, "ci-logs");
160867
161269
  mkdirSync7(logsDir, { recursive: true });
160868
161270
  const jobResults = [];
160869
161271
  for (const run4 of failedRuns) {
@@ -160890,8 +161292,8 @@ function GetCheckSuiteLogsTool(ctx) {
160890
161292
  );
160891
161293
  }
160892
161294
  const logsText = await logsResult.text();
160893
- const logPath = join14(logsDir, `job-${job.id}.log`);
160894
- writeFileSync9(logPath, logsText);
161295
+ const logPath = join15(logsDir, `job-${job.id}.log`);
161296
+ writeFileSync10(logPath, logsText);
160895
161297
  const analysis = analyzeLog(logsText, 80);
160896
161298
  const failedSteps = job.steps?.filter((s) => s.conclusion === "failure").map((s) => `Step ${s.number}: ${s.name}`) ?? [];
160897
161299
  jobResults.push({
@@ -160939,8 +161341,8 @@ function GetCheckSuiteLogsTool(ctx) {
160939
161341
  }
160940
161342
 
160941
161343
  // mcp/commitInfo.ts
160942
- import { writeFileSync as writeFileSync10 } from "node:fs";
160943
- import { join as join15 } from "node:path";
161344
+ import { writeFileSync as writeFileSync11 } from "node:fs";
161345
+ import { join as join16 } from "node:path";
160944
161346
  var CommitInfo = type({
160945
161347
  sha: type.string.describe("the commit SHA (full or abbreviated) to fetch")
160946
161348
  });
@@ -160964,8 +161366,8 @@ function CommitInfoTool(ctx) {
160964
161366
  "PULLFROG_TEMP_DIR not set - get_commit_info must run in pullfrog action context"
160965
161367
  );
160966
161368
  }
160967
- const diffFile = join15(tempDir, `commit-${sha.slice(0, 7)}.diff`);
160968
- writeFileSync10(diffFile, formatResult.content);
161369
+ const diffFile = join16(tempDir, `commit-${sha.slice(0, 7)}.diff`);
161370
+ writeFileSync11(diffFile, formatResult.content);
160969
161371
  log.debug(`wrote commit diff to ${diffFile} (${formatResult.content.length} bytes)`);
160970
161372
  return {
160971
161373
  sha: data.sha,
@@ -160992,7 +161394,7 @@ import { performance as performance8 } from "node:perf_hooks";
160992
161394
 
160993
161395
  // prep/installNodeDependencies.ts
160994
161396
  import { existsSync as existsSync6 } from "node:fs";
160995
- import { join as join17 } from "node:path";
161397
+ import { join as join18 } from "node:path";
160996
161398
 
160997
161399
  // node_modules/.pnpm/package-manager-detector@1.6.0/node_modules/package-manager-detector/dist/commands.mjs
160998
161400
  function dashDashArg(agent2, agentCommand) {
@@ -161291,7 +161693,7 @@ function isMetadataYarnClassic(metadataPath) {
161291
161693
  var import_semver3 = __toESM(require_semver2(), 1);
161292
161694
  import { existsSync as existsSync5 } from "node:fs";
161293
161695
  import { mkdir, readFile as readFile2 } from "node:fs/promises";
161294
- import { delimiter, join as join16 } from "node:path";
161696
+ import { delimiter, join as join17 } from "node:path";
161295
161697
  var SUPPORTED_NAMES = ["npm", "pnpm", "yarn", "bun"];
161296
161698
  var COREPACK_MANAGED = ["pnpm", "yarn"];
161297
161699
  function isSupported(name) {
@@ -161329,7 +161731,7 @@ function parseDevEnginesField(field) {
161329
161731
  };
161330
161732
  }
161331
161733
  async function resolvePackageManagerSpec(cwd) {
161332
- const pkgPath = join16(cwd, "package.json");
161734
+ const pkgPath = join17(cwd, "package.json");
161333
161735
  if (!existsSync5(pkgPath)) return null;
161334
161736
  let pkg;
161335
161737
  try {
@@ -161392,7 +161794,7 @@ async function currentVersion(name) {
161392
161794
  return result.stdout.trim();
161393
161795
  }
161394
161796
  function packageManagerBinDir(tmpdir4) {
161395
- return join16(tmpdir4, "pm-bin");
161797
+ return join17(tmpdir4, "pm-bin");
161396
161798
  }
161397
161799
  async function ensurePackageManager(params) {
161398
161800
  const spec = params.spec;
@@ -161463,7 +161865,7 @@ async function installFallback(name, installSpec) {
161463
161865
  return result.stderr || `failed to install ${name}`;
161464
161866
  }
161465
161867
  if (name === "deno") {
161466
- const denoPath = join17(process.env.HOME || "", ".deno", "bin");
161868
+ const denoPath = join18(process.env.HOME || "", ".deno", "bin");
161467
161869
  process.env.PATH = `${denoPath}:${process.env.PATH}`;
161468
161870
  }
161469
161871
  log.info(`\xBB installed ${name}`);
@@ -161472,7 +161874,7 @@ async function installFallback(name, installSpec) {
161472
161874
  var installNodeDependencies = {
161473
161875
  name: "installNodeDependencies",
161474
161876
  shouldRun: () => {
161475
- const packageJsonPath = join17(process.cwd(), "package.json");
161877
+ const packageJsonPath = join18(process.cwd(), "package.json");
161476
161878
  return existsSync6(packageJsonPath);
161477
161879
  },
161478
161880
  run: async (options) => {
@@ -161571,12 +161973,12 @@ ${errorMessage}`]
161571
161973
 
161572
161974
  // prep/installPythonDependencies.ts
161573
161975
  import { existsSync as existsSync7, readFileSync as readFileSync5 } from "node:fs";
161574
- import { join as join18 } from "node:path";
161976
+ import { join as join19 } from "node:path";
161575
161977
  function declaresBuildSystem(path4) {
161576
161978
  return /^\s*\[\s*build-system\s*\]/m.test(readFileSync5(path4, "utf8"));
161577
161979
  }
161578
161980
  function configApplies(config3, cwd) {
161579
- const path4 = join18(cwd, config3.file);
161981
+ const path4 = join19(cwd, config3.file);
161580
161982
  return existsSync7(path4) && (config3.applies?.(path4) ?? true);
161581
161983
  }
161582
161984
  var PYTHON_CONFIGS = [
@@ -162712,8 +163114,8 @@ function PullRequestInfoTool(ctx) {
162712
163114
  }
162713
163115
 
162714
163116
  // mcp/reviewComments.ts
162715
- import { writeFileSync as writeFileSync11 } from "node:fs";
162716
- import { join as join19 } from "node:path";
163117
+ import { writeFileSync as writeFileSync12 } from "node:fs";
163118
+ import { join as join20 } from "node:path";
162717
163119
  var REVIEW_THREADS_QUERY = `
162718
163120
  query ($owner: String!, $name: String!, $prNumber: Int!) {
162719
163121
  repository(owner: $owner, name: $name) {
@@ -163129,8 +163531,8 @@ function GetReviewCommentsTool(ctx) {
163129
163531
  throw new Error("PULLFROG_TEMP_DIR not set");
163130
163532
  }
163131
163533
  const filename = `review-${params.review_id}-threads.md`;
163132
- const commentsPath = join19(tempDir, filename);
163133
- writeFileSync11(commentsPath, formatted.content);
163534
+ const commentsPath = join20(tempDir, filename);
163535
+ writeFileSync12(commentsPath, formatted.content);
163134
163536
  log.debug(`wrote ${threadBlocks.length} threads to ${commentsPath}`);
163135
163537
  return {
163136
163538
  review_id: params.review_id,
@@ -163365,11 +163767,11 @@ ${summaryAddendum}`,
163365
163767
  }
163366
163768
 
163367
163769
  // mcp/shell.ts
163368
- import { spawn as spawn4, spawnSync as spawnSync5 } from "node:child_process";
163770
+ import { spawn as spawn5, spawnSync as spawnSync5 } from "node:child_process";
163369
163771
  import { randomUUID as randomUUID4 } from "node:crypto";
163370
- import { closeSync, openSync, writeFileSync as writeFileSync12 } from "node:fs";
163772
+ import { closeSync, openSync, writeFileSync as writeFileSync13 } from "node:fs";
163371
163773
  import { userInfo as userInfo2 } from "node:os";
163372
- import { join as join20 } from "node:path";
163774
+ import { join as join21 } from "node:path";
163373
163775
  import { setTimeout as sleep2 } from "node:timers/promises";
163374
163776
  var ShellParams = type({
163375
163777
  command: "string",
@@ -163467,7 +163869,7 @@ function spawnShell(params) {
163467
163869
  const repoRoot = resolveRepoRoot();
163468
163870
  const fsMounts = buildFsMounts(repoRoot);
163469
163871
  if (sandboxMethod === "unshare") {
163470
- return spawn4(
163872
+ return spawn5(
163471
163873
  "unshare",
163472
163874
  [
163473
163875
  "--pid",
@@ -163491,7 +163893,7 @@ function spawnShell(params) {
163491
163893
  const pathRestore = 'export PATH="${SANDBOX_PATH:-$PATH}"; ';
163492
163894
  const escaped = (pathRestore + params.command).replace(/'/g, "'\\''");
163493
163895
  envArgs.push(`SANDBOX_PATH=${params.env.PATH ?? ""}`);
163494
- return spawn4(
163896
+ return spawn5(
163495
163897
  "sudo",
163496
163898
  [
163497
163899
  "env",
@@ -163507,7 +163909,7 @@ function spawnShell(params) {
163507
163909
  { ...spawnOpts, env: {} }
163508
163910
  );
163509
163911
  }
163510
- return spawn4("bash", ["-c", params.command], spawnOpts);
163912
+ return spawn5("bash", ["-c", params.command], spawnOpts);
163511
163913
  }
163512
163914
  async function killProcessGroup(proc) {
163513
163915
  if (!proc.pid) return;
@@ -163532,8 +163934,8 @@ function getTempDir() {
163532
163934
  var MAX_OUTPUT_CHARS = 5e3;
163533
163935
  function capOutput(output) {
163534
163936
  if (output.length <= MAX_OUTPUT_CHARS) return output;
163535
- const fullPath = join20(getTempDir(), `shell-${randomUUID4().slice(0, 8)}.log`);
163536
- writeFileSync12(fullPath, output);
163937
+ const fullPath = join21(getTempDir(), `shell-${randomUUID4().slice(0, 8)}.log`);
163938
+ writeFileSync13(fullPath, output);
163537
163939
  const elided = output.length - MAX_OUTPUT_CHARS;
163538
163940
  return `... [${elided} chars truncated; full output saved to ${fullPath}] ...
163539
163941
  ${output.slice(-MAX_OUTPUT_CHARS)}`;
@@ -163587,8 +163989,8 @@ Do NOT use this tool for git commands \u2014 use the dedicated git tools instead
163587
163989
  if (params.background) {
163588
163990
  const tempDir = getTempDir();
163589
163991
  const handle = `bg-${randomUUID4().slice(0, 8)}`;
163590
- const outputPath = join20(tempDir, `${handle}.log`);
163591
- const pidPath = join20(tempDir, `${handle}.pid`);
163992
+ const outputPath = join21(tempDir, `${handle}.log`);
163993
+ const pidPath = join21(tempDir, `${handle}.pid`);
163592
163994
  const logFd = openSync(outputPath, "a");
163593
163995
  let proc2;
163594
163996
  try {
@@ -163605,7 +164007,7 @@ Do NOT use this tool for git commands \u2014 use the dedicated git tools instead
163605
164007
  throw new Error("failed to start background process");
163606
164008
  }
163607
164009
  proc2.unref();
163608
- writeFileSync12(pidPath, `${proc2.pid}
164010
+ writeFileSync13(pidPath, `${proc2.pid}
163609
164011
  `);
163610
164012
  ctx.toolState.backgroundProcesses.set(handle, { pid: proc2.pid, outputPath, pidPath });
163611
164013
  return {
@@ -163755,7 +164157,7 @@ function UploadFileTool(ctx) {
163755
164157
 
163756
164158
  // mcp/xrepo.ts
163757
164159
  import { mkdirSync as mkdirSync8, rmSync as rmSync3 } from "node:fs";
163758
- import { join as join21 } from "node:path";
164160
+ import { join as join22 } from "node:path";
163759
164161
  function assertValidRepoName(name) {
163760
164162
  if (!/^[A-Za-z0-9._-]+$/.test(name) || name.startsWith("-") || name.includes("..")) {
163761
164163
  throw new Error(
@@ -163773,6 +164175,7 @@ var ListRepos = type({});
163773
164175
  function ListReposTool(ctx) {
163774
164176
  return tool({
163775
164177
  name: "list_repos",
164178
+ annotations: { readOnlyHint: true },
163776
164179
  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.",
163777
164180
  parameters: ListRepos,
163778
164181
  execute: execute(async () => {
@@ -163834,7 +164237,7 @@ function CheckoutRepoTool(ctx) {
163834
164237
  return { path: existing.dir, access: existing.access, note: "already checked out." };
163835
164238
  }
163836
164239
  const access = accessFor(ctx, repo);
163837
- const dir = join21(ctx.tmpdir, "xrepo", repo);
164240
+ const dir = join22(ctx.tmpdir, "xrepo", repo);
163838
164241
  mkdirSync8(dir, { recursive: true });
163839
164242
  const state = ensureRepoState(ctx.toolState, { owner, name: repo, dir, access });
163840
164243
  const rc = resolveRepoCtx(ctx, repo);
@@ -164284,9 +164687,9 @@ function formatApiKeyErrorSummary(params) {
164284
164687
 
164285
164688
  // utils/gitAuthServer.ts
164286
164689
  import { randomUUID as randomUUID5 } from "node:crypto";
164287
- import { writeFileSync as writeFileSync13 } from "node:fs";
164690
+ import { writeFileSync as writeFileSync14 } from "node:fs";
164288
164691
  import { createServer as createServer3 } from "node:http";
164289
- import { join as join22 } from "node:path";
164692
+ import { join as join23 } from "node:path";
164290
164693
  var REVOKED_TRAP_MS = 6e4;
164291
164694
  function revokeGitHubToken(token) {
164292
164695
  fetch("https://api.github.com/installation/token", {
@@ -164355,7 +164758,7 @@ async function startGitAuthServer(tmpdir4) {
164355
164758
  function writeAskpassScript(code) {
164356
164759
  const scriptId = randomUUID5();
164357
164760
  const scriptName = `askpass-${scriptId}.js`;
164358
- const scriptPath = join22(tmpdir4, scriptName);
164761
+ const scriptPath = join23(tmpdir4, scriptName);
164359
164762
  const content = [
164360
164763
  `#!/usr/bin/env node`,
164361
164764
  `var a=process.argv[2]||"";`,
@@ -164368,7 +164771,7 @@ async function startGitAuthServer(tmpdir4) {
164368
164771
  `r.on("end",function(){process.stdout.write(d+"\\n")})`,
164369
164772
  `}).on("error",function(){process.exit(1)})}`
164370
164773
  ].join("\n");
164371
- writeFileSync13(scriptPath, content, { mode: 448 });
164774
+ writeFileSync14(scriptPath, content, { mode: 448 });
164372
164775
  return scriptPath;
164373
164776
  }
164374
164777
  async function close() {
@@ -164689,9 +165092,9 @@ When embedding images (e.g. uploaded screenshots) in comments or PR bodies, alwa
164689
165092
 
164690
165093
  **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\`.
164691
165094
 
164692
- **\`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.
165095
+ **\`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.
164693
165096
 
164694
- 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.
165097
+ 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.
164695
165098
 
164696
165099
  ### If you get stuck
164697
165100
 
@@ -164825,7 +165228,7 @@ function resolveInstructions(ctx) {
164825
165228
 
164826
165229
  // utils/learnings.ts
164827
165230
  import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
164828
- import { dirname as dirname4, join as join23 } from "node:path";
165231
+ import { dirname as dirname4, join as join24 } from "node:path";
164829
165232
 
164830
165233
  // utils/learningsTruncate.ts
164831
165234
  var MAX_LEARNINGS_LENGTH = 1e5;
@@ -164843,10 +165246,10 @@ function truncateAtLineBoundary(body, cap) {
164843
165246
  var LEARNINGS_FILE_NAME = "pullfrog-learnings.md";
164844
165247
  var XREPO_LEARNINGS_FILE_NAME = "pullfrog-xrepo-learnings.md";
164845
165248
  function learningsFilePath(tmpdir4) {
164846
- return join23(tmpdir4, LEARNINGS_FILE_NAME);
165249
+ return join24(tmpdir4, LEARNINGS_FILE_NAME);
164847
165250
  }
164848
165251
  function xrepoLearningsFilePath(tmpdir4) {
164849
- return join23(tmpdir4, XREPO_LEARNINGS_FILE_NAME);
165252
+ return join24(tmpdir4, XREPO_LEARNINGS_FILE_NAME);
164850
165253
  }
164851
165254
  async function seedLearningsFile(params) {
164852
165255
  const path4 = learningsFilePath(params.tmpdir);
@@ -165531,8 +165934,13 @@ async function resolveProxyModel(ctx) {
165531
165934
  `\xBB no model selected \u2014 using the cost-optimized default (${ctx.proxyModel}); pick a model in your Pullfrog repo settings for stronger reviews.`
165532
165935
  );
165533
165936
  }
165534
- if (!ctx.oss && ctx.payload.model && ctx.proxyModel === DEFAULT_PROXY_MODEL && resolveOpenRouterModel(ctx.payload.model) !== DEFAULT_PROXY_MODEL) {
165535
- if (isCardGatedModel(ctx.payload.model)) {
165937
+ if (ctx.payload.model && ctx.proxyModel === DEFAULT_PROXY_MODEL && resolveOpenRouterModel(ctx.payload.model) !== DEFAULT_PROXY_MODEL) {
165938
+ if (ctx.oss) {
165939
+ ctx.toolState.modelClamped = { from: ctx.payload.model, reason: "oss" };
165940
+ log.info(
165941
+ `\xBB ${ctx.payload.model} overridden \u2014 Pullfrog for OSS covers ${ctx.proxyModel}; add a provider key in your Pullfrog settings to run your configured model.`
165942
+ );
165943
+ } else if (isCardGatedModel(ctx.payload.model)) {
165536
165944
  ctx.toolState.modelClamped = { from: ctx.payload.model, reason: "card" };
165537
165945
  log.warning(
165538
165946
  `\xBB ${ctx.payload.model} needs a card on file \u2014 using the efficient default (${ctx.proxyModel}); add a card in your Pullfrog org billing settings.`
@@ -165586,7 +165994,7 @@ async function runProxyResolution(ctx) {
165586
165994
 
165587
165995
  // utils/prSummary.ts
165588
165996
  import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
165589
- import { dirname as dirname5, join as join24 } from "node:path";
165997
+ import { dirname as dirname5, join as join25 } from "node:path";
165590
165998
  var SUMMARY_FILE_NAME = "pullfrog-summary.md";
165591
165999
  var SUMMARY_SCAFFOLD = `# PR summary
165592
166000
 
@@ -165596,7 +166004,7 @@ var SUMMARY_SCAFFOLD = `# PR summary
165596
166004
  var MIN_SNAPSHOT_LENGTH = 60;
165597
166005
  var MAX_SNAPSHOT_LENGTH = 32768;
165598
166006
  function summaryFilePath(tmpdir4) {
165599
- return join24(tmpdir4, SUMMARY_FILE_NAME);
166007
+ return join25(tmpdir4, SUMMARY_FILE_NAME);
165600
166008
  }
165601
166009
  async function seedSummaryFile(params) {
165602
166010
  const path4 = summaryFilePath(params.tmpdir);
@@ -166313,7 +166721,7 @@ async function finalizeSuccessRun(input) {
166313
166721
  log.debug(`failure error report failed: ${error49}`);
166314
166722
  });
166315
166723
  }
166316
- if (input.result.success && input.toolState.progressComment && input.toolState.wasUpdated && (!input.toolState.finalSummaryWritten || input.toolState.answerCommentPosted)) {
166724
+ if (input.result.success && input.toolState.progressComment && input.toolState.wasUpdated && !input.toolState.finalSummaryWritten) {
166317
166725
  await deleteProgressComment(input.toolContext).catch((error49) => {
166318
166726
  log.debug(`stranded progress comment cleanup failed: ${error49}`);
166319
166727
  });
@@ -166879,7 +167287,7 @@ ${instructions.user}` : null,
166879
167287
  log.info(instructions.full);
166880
167288
  });
166881
167289
  if (agentId === "opencode") {
166882
- const pluginDir = join25(process.cwd(), ".opencode", "plugin");
167290
+ const pluginDir = join26(process.cwd(), ".opencode", "plugin");
166883
167291
  const hasPlugins = existsSync8(pluginDir) && readdirSync2(pluginDir).some((f) => /\.[jt]sx?$/.test(f));
166884
167292
  if (hasPlugins && toolState.dependencyInstallation?.promise) {
166885
167293
  log.info(
@@ -168053,7 +168461,7 @@ async function runCli4(input) {
168053
168461
  }
168054
168462
 
168055
168463
  // cli.ts
168056
- var VERSION10 = "0.1.35";
168464
+ var VERSION10 = "0.1.37";
168057
168465
  var bin = basename2(process.argv[1] || "");
168058
168466
  var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
168059
168467
  var rawArgs = process.argv.slice(2);