pullfrog 0.1.35 → 0.1.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/reviewer.d.ts +1 -1
- package/dist/cli.mjs +557 -187
- package/dist/index.js +556 -186
- package/dist/internal.js +55 -62
- package/dist/mcp/checkout.d.ts +1 -0
- package/dist/mcp/shared.d.ts +3 -1
- package/dist/toolState.d.ts +0 -1
- package/dist/utils/changeImpact.d.ts +18 -0
- package/dist/utils/diffCoverage.d.ts +0 -5
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -27957,7 +27957,7 @@ var require_cross_spawn = __commonJS({
|
|
|
27957
27957
|
var cp = __require("child_process");
|
|
27958
27958
|
var parse5 = require_parse3();
|
|
27959
27959
|
var enoent = require_enoent();
|
|
27960
|
-
function
|
|
27960
|
+
function spawn4(command, args2, options) {
|
|
27961
27961
|
const parsed2 = parse5(command, args2, options);
|
|
27962
27962
|
const spawned = cp.spawn(parsed2.command, parsed2.args, parsed2.options);
|
|
27963
27963
|
enoent.hookChildProcess(spawned, parsed2);
|
|
@@ -27969,8 +27969,8 @@ var require_cross_spawn = __commonJS({
|
|
|
27969
27969
|
result.error = result.error || enoent.verifyENOENTSync(result.status, parsed2);
|
|
27970
27970
|
return result;
|
|
27971
27971
|
}
|
|
27972
|
-
module.exports =
|
|
27973
|
-
module.exports.spawn =
|
|
27972
|
+
module.exports = spawn4;
|
|
27973
|
+
module.exports.spawn = spawn4;
|
|
27974
27974
|
module.exports.sync = spawnSync6;
|
|
27975
27975
|
module.exports._parse = parse5;
|
|
27976
27976
|
module.exports._enoent = enoent;
|
|
@@ -95782,14 +95782,14 @@ var require_turndown_cjs = __commonJS({
|
|
|
95782
95782
|
} else if (node2.nodeType === 1) {
|
|
95783
95783
|
replacement = replacementForNode.call(self2, node2);
|
|
95784
95784
|
}
|
|
95785
|
-
return
|
|
95785
|
+
return join26(output, replacement);
|
|
95786
95786
|
}, "");
|
|
95787
95787
|
}
|
|
95788
95788
|
function postProcess(output) {
|
|
95789
95789
|
var self2 = this;
|
|
95790
95790
|
this.rules.forEach(function(rule) {
|
|
95791
95791
|
if (typeof rule.append === "function") {
|
|
95792
|
-
output =
|
|
95792
|
+
output = join26(output, rule.append(self2.options));
|
|
95793
95793
|
}
|
|
95794
95794
|
});
|
|
95795
95795
|
return output.replace(/^[\t\r\n]+/, "").replace(/[\t\r\n\s]+$/, "");
|
|
@@ -95801,7 +95801,7 @@ var require_turndown_cjs = __commonJS({
|
|
|
95801
95801
|
if (whitespace.leading || whitespace.trailing) content = content.trim();
|
|
95802
95802
|
return whitespace.leading + rule.replacement(content, node2, this.options) + whitespace.trailing;
|
|
95803
95803
|
}
|
|
95804
|
-
function
|
|
95804
|
+
function join26(output, replacement) {
|
|
95805
95805
|
var s1 = trimTrailingNewlines(output);
|
|
95806
95806
|
var s2 = trimLeadingNewlines(replacement);
|
|
95807
95807
|
var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
|
|
@@ -99801,7 +99801,7 @@ var init_file_type = __esm({
|
|
|
99801
99801
|
// main.ts
|
|
99802
99802
|
import { existsSync as existsSync8, readdirSync as readdirSync2 } from "node:fs";
|
|
99803
99803
|
import { readFile as readFile5 } from "node:fs/promises";
|
|
99804
|
-
import { join as
|
|
99804
|
+
import { join as join25 } from "node:path";
|
|
99805
99805
|
|
|
99806
99806
|
// agents/claude.ts
|
|
99807
99807
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
@@ -101072,7 +101072,7 @@ var import_semver = __toESM(require_semver2(), 1);
|
|
|
101072
101072
|
// package.json
|
|
101073
101073
|
var package_default = {
|
|
101074
101074
|
name: "pullfrog",
|
|
101075
|
-
version: "0.1.
|
|
101075
|
+
version: "0.1.36",
|
|
101076
101076
|
type: "module",
|
|
101077
101077
|
bin: {
|
|
101078
101078
|
pullfrog: "dist/cli.mjs",
|
|
@@ -101746,8 +101746,9 @@ HARD CONSTRAINTS (non-negotiable, regardless of orchestrator instructions):
|
|
|
101746
101746
|
- Your FIRST action MUST source the diff for review. If the orchestrator's dispatch names a diff PATH on disk (e.g. \`diffPath\` / \`incrementalDiffPath\` from a prior \`checkout_pr\` call), \`read\` that path \u2014 do not invoke git at all. The on-disk diff is the authoritative scope, and dispatches almost always include one; recomputing it via git also fails on shallow GitHub Actions checkouts where the base ref may be unfetched. When BOTH a diff path and a base branch appear in your dispatch, path always wins. When the dispatch names an \`incrementalDiffPath\` alongside \`diffPath\`, prefer the incremental path for scope and consult the full diff only for line-number anchoring.
|
|
101747
101747
|
- If (and only if) NO diff path was provided, the dispatch names a base branch. Run \`git diff --merge-base origin/<base>\` (single MCP call, captures committed + staged + unstaged work, excludes commits landed on \`origin/<base>\` since your branch forked). The read-only \`git\` MCP tool is the right surface for this \u2014 \`--merge-base\` is a flag git accepts directly, so no shell substitution is needed. Do NOT run bare \`git diff origin/<base>\` or two-dot \`git diff origin/<base>..HEAD\`: those are symmetric diffs that include the inverse of every commit on \`<base>\` your branch is behind, which is pure noise (and the git tool will reject those forms when the divergence is detected). Do NOT try to expand \`$(...)\` subshell forms via the git tool \u2014 it runs git directly without shell interpolation. If \`git diff --merge-base origin/<base>\` fails with \`ambiguous argument 'origin/<base>'\` or \`no merge base\`, the runner is a shallow single-branch checkout AND the orchestrator failed to fetch the base ref before dispatching you. Surface that in one line (which ref is missing, and that the orchestrator needs to fetch it with \`git fetch --no-tags --deepen=1000 origin <base>:refs/remotes/origin/<base>\` before re-dispatching) and stop. Do NOT run \`git fetch\` yourself \u2014 your read-only contract below forbids mutating shell, and the \`git_fetch\` MCP tool is state-changing and therefore prohibited. Do NOT call \`checkout_pr\`, do NOT fetch alternative refs, do NOT list branches or all-refs looking for the work, do NOT run \`gh pr list\`. The orchestrator's dispatch is the source of truth for scope.
|
|
101748
101748
|
- If the on-disk diff path you were given is empty (or unreadable), that is a checkout / formatting failure on the orchestrator side \u2014 reply EXACTLY: \`no changes in dispatched diff \u2014 scope appears empty; orchestrator should verify checkout_pr output\` (naming the path), do NOT fall through to running \`git diff\` against guessed refs. If the merge-base diff (the fallback path) returns empty AND the orchestrator's dispatch claims there are changes to review, the most likely cause is a pre-commit Build-mode self-review: the orchestrator dispatched you before committing AND there are no uncommitted edits either. Reply EXACTLY: \`no changes detected \u2014 likely pre-commit Build self-review; orchestrator should commit then re-dispatch\` and stop. Do NOT guess PR numbers (e.g. by extrapolating from \`git log\` output), do NOT check out other PRs, do NOT fetch from forks. The empty diff is the diagnosis \u2014 surface it; do not work around it.
|
|
101749
|
+
- Once the mandatory first diff read returns a non-empty scope, batch each next dependency layer: emit all independent read-only file reads, greps, globs, and directory listings together in one assistant turn before awaiting their results. Include MCP queries only when their contract explicitly guarantees they are read-only; a \`get_*\` or \`list_*\` name alone is not proof. Keep dependent calls in later turns, after the results they depend on are available.
|
|
101749
101750
|
- Read-only tools only. Do NOT write or edit files. Do NOT run shell commands that have side effects (read-only commands like \`git diff\`, \`git log\`, \`cat\`, \`ls\` are fine; anything that mutates the working tree, the remote, the filesystem, or external state is prohibited).
|
|
101750
|
-
- 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.
|
|
101751
|
+
- Do NOT call any state-changing MCP tool. State-changing means: posts a comment, pushes a branch, creates/updates a PR or issue, changes labels, resolves review threads, persists learnings, sets workflow output, installs dependencies, uploads files, kills processes, etc. MCP tool names are not evidence of safety: only use queries whose contract explicitly guarantees no side effects. The \`git\` tool is fine only for read-only subcommands like \`diff\`/\`log\`/\`merge-base\`.
|
|
101751
101752
|
- Do NOT spawn further subagents. You are a leaf reviewer; recursive dispatch pre-aggregates findings through an intermediate model and defeats the design.
|
|
101752
101753
|
- Test for any tool call before invoking it: would this still be a no-op if reverted? If not, do not call it. Apply this test to tools added after this prompt was written \u2014 the rule is the invariant, not the enumeration.
|
|
101753
101754
|
|
|
@@ -102010,20 +102011,15 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
102010
102011
|
- **immediately** call \`${t("resolve_review_thread")}\` with that thread's \`thread=\` value as \`thread_id\`. Resolve every thread where you (a) made the requested code change in full \u2014 partial fixes leave the thread open \u2014 OR (b) replied with a substantive answer the user explicitly asked for. Do NOT resolve threads where you pushed back on the request and the disagreement is unresolved; leave those open for the human to mediate.
|
|
102011
102012
|
- call \`${t("report_progress")}\` with a brief summary`
|
|
102012
102013
|
},
|
|
102013
|
-
// Review and IncrementalReview
|
|
102014
|
-
//
|
|
102015
|
-
//
|
|
102016
|
-
// high-stakes-subsystem touches — and when it fires, ALL lenses must
|
|
102017
|
-
// dispatch in a single assistant turn or the parallelism win disappears.
|
|
102018
|
-
// We never dispatch exactly one lens: a single lens is just a worse,
|
|
102019
|
-
// slower version of doing the work yourself.
|
|
102014
|
+
// Review and IncrementalReview route the minimum reviewfrog specialists
|
|
102015
|
+
// needed to cover unresolved, disposition-changing hypotheses. Most runs
|
|
102016
|
+
// use zero or one; multiple orthogonal hypotheses dispatch in parallel.
|
|
102020
102017
|
//
|
|
102021
102018
|
// Build mode self-review is a different problem shape: the orchestrator
|
|
102022
102019
|
// wrote the code, so bias-mitigation comes from delegating to one
|
|
102023
102020
|
// fresh-eyes subagent that doesn't share the implementation context. A
|
|
102024
|
-
// single subagent there is appropriate
|
|
102025
|
-
// the
|
|
102026
|
-
// perspectives is what's being purchased.
|
|
102021
|
+
// single subagent there is appropriate. Review-mode specialist routing
|
|
102022
|
+
// instead scales with the unresolved hypotheses in someone else's diff.
|
|
102027
102023
|
//
|
|
102028
102024
|
// Severity categorization is split across two surfaces: the opening
|
|
102029
102025
|
// callout (CAUTION/IMPORTANT/ℹ️/✅) sets the review's overall tier, and
|
|
@@ -102038,11 +102034,11 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
102038
102034
|
|
|
102039
102035
|
1. **task list**: create your task list for this run as your first action.
|
|
102040
102036
|
|
|
102041
|
-
2. **checkout**: call \`${t("checkout_pr")}\` \u2014 this returns PR metadata and a \`
|
|
102037
|
+
2. **checkout**: call \`${t("checkout_pr")}\` \u2014 this returns PR metadata, a \`diffPath\`, and a supplemental \`impactPath\` when change-impact extraction is enabled. read the complete raw diff end-to-end, beginning with the TOC and using its file line ranges as your coverage checklist. only after that, use \`impactPath\` as an explicitly incomplete list of reference leads; it never replaces raw-diff reading or establishes coverage.
|
|
102042
102038
|
|
|
102043
102039
|
3. **triage**: orient yourself on the PR \u2014 identify *what kind of thing this is* (domain it touches, seams it crosses, external contracts it depends on, user-facing surfaces it changes). pull as much context as you need to render a confident, well-grounded review: read related files, grep for callers of changed symbols, check tests that exercise the touched paths, fetch related GitHub state. **you are the synthesizer** \u2014 never delegate understanding to subagents.
|
|
102044
102040
|
|
|
102045
|
-
if the PR is **genuinely trivial**, skip
|
|
102041
|
+
if the PR is **genuinely trivial**, skip specialists entirely and submit a \`No new issues found.\` review per step 7.
|
|
102046
102042
|
|
|
102047
102043
|
"Genuinely trivial" (skip):
|
|
102048
102044
|
- single-word doc typo, whitespace/format-only, comment-only across any number of files
|
|
@@ -102061,22 +102057,24 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
102061
102057
|
- any "typo fix" in user-facing copy that changes meaning ("approved" \u2192 "denied")
|
|
102062
102058
|
- mixed diffs where a semantic 1-liner is buried in whitespace/formatting changes
|
|
102063
102059
|
|
|
102064
|
-
4. **
|
|
102060
|
+
4. **specialist decision \u2014 minimum hypothesis coverage**.
|
|
102065
102061
|
|
|
102066
|
-
|
|
102062
|
+
After full-diff coverage and triage, identify the load-bearing questions you still cannot resolve confidently yourself. A specialist hypothesis is load-bearing only when its answer could yield an actionable finding that changes the review disposition and warrants independent investigation, and falsifiable only when the specialist can return evidence that supports or refutes it. Generic requests for extra confidence, polish, or "another look" do not qualify.
|
|
102067
102063
|
|
|
102068
|
-
|
|
102069
|
-
-
|
|
102070
|
-
-
|
|
102071
|
-
- parallel-
|
|
102064
|
+
Route the **minimum number of \`${REVIEWER_AGENT_NAME}\` specialists** needed to cover those unresolved hypotheses. Most reviews need **0 or 1**:
|
|
102065
|
+
- dispatch 0 when you can resolve every disposition-changing question directly
|
|
102066
|
+
- dispatch 1 when exactly one falsifiable, load-bearing hypothesis warrants independent investigation
|
|
102067
|
+
- dispatch 2+ in parallel when multiple orthogonal load-bearing hypotheses remain, or when the user explicitly requests an exhaustive or multi-angle review
|
|
102072
102068
|
|
|
102073
|
-
**
|
|
102069
|
+
**There is NO one-specialist cap or fixed maximum.** Cover every orthogonal load-bearing hypothesis that remains; do not collapse multiple real questions into one broad prompt just to reduce the count. There is also no file-count, line-count, schema, quota, or hard-budget threshold \u2014 diff size is not a proxy for review uncertainty.
|
|
102074
102070
|
|
|
102075
|
-
|
|
102071
|
+
The primary reviewer remains responsible for reading the complete raw diff, investigating surrounding code, validating every returned finding, and synthesizing the final review. Specialist reads supplement that work; they never replace it or satisfy the primary's diff-coverage obligation.
|
|
102072
|
+
|
|
102073
|
+
Specialist hypotheses can draw on two kinds of framing:
|
|
102076
102074
|
- **themed lenses** \u2014 a perspective applied across the whole diff (correctness, security, user-journey, performance, etc.).
|
|
102077
102075
|
- **subsystem lenses** \u2014 a domain-scoped frame for high-stakes subsystems the PR touches (e.g. "the auth lens", "the billing lens", "the schema-migration lens"). **for high-stakes domains, lead with the subsystem lens rather than the generic themed equivalent** \u2014 "billing-subsystem" outperforms "correctness on billing code" because the framing primes the subagent to remember domain-specific failure modes (double-charges, refund races, currency rounding, dispute flows) the generic lens misses.
|
|
102078
102076
|
|
|
102079
|
-
starter menu (combine, omit, or invent your own):
|
|
102077
|
+
starter menu for identifying hypotheses (combine, omit, or invent your own; do not dispatch a bare menu label without a falsifiable question):
|
|
102080
102078
|
- **correctness & invariants** \u2014 bugs, races, error handling, edge cases, state-machine boundaries
|
|
102081
102079
|
- **impact** \u2014 stale references in code/tests/docs/configs/UI after rename/remove
|
|
102082
102080
|
- **research-validated assumptions** \u2014 third-party API contracts, SDK semantics, framework directives, version-gated behavior. **only pick when the PR's correctness depends on the contract behaving a specific way** \u2014 not when the API is merely used. The bar is "if the third-party contract differs from what the diff assumes, the PR is incorrect." When dispatched, the subagent must verify load-bearing claims via web search and quote source URLs.
|
|
@@ -102091,30 +102089,27 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
102091
102089
|
|
|
102092
102090
|
The only subagent type is \`${REVIEWER_AGENT_NAME}\` \u2014 used for lens judgment work ("is this safe / correct / well-tested?"), runs on a mid-tier model.
|
|
102093
102091
|
|
|
102094
|
-
5. **
|
|
102095
|
-
|
|
102096
|
-
\u26A0\uFE0F CRITICAL \u2014 PARALLELISM IS THE ONLY REASON LENSES EXIST. \u26A0\uFE0F
|
|
102097
|
-
The default tool-call behavior of Claude Code (and most agent runtimes) is **serial dispatch**: emit one Task call, await result, emit next, await, etc. This collapses your fan-out into a sequential review where each lens adds N \xD7 (orchestrator-think-time + lens-execution-time) to wall time. **YOU MUST OVERRIDE THIS DEFAULT.** Emit ALL of your Task tool_use blocks in the SAME assistant message, BEFORE you read ANY result from ANY of them. If you find yourself emitting one Task call, then thinking about the result, then emitting another \u2014 STOP and re-issue them all together. The whole point of going multi-lens is the wall-clock speedup from parallel execution; serial dispatch defeats it entirely.
|
|
102092
|
+
5. **dispatch specialists (only if step 4 found unresolved hypotheses)**: dispatch one \`${REVIEWER_AGENT_NAME}\` for one hypothesis. For 2+ hypotheses, emit every Task tool_use block **IN A SINGLE ASSISTANT TURN** before reading any result so the investigations run in parallel rather than serially.
|
|
102098
102093
|
|
|
102099
|
-
\u2705 Right pattern: one assistant turn with N Task tool_use blocks \u2192 wait \u2192 N results arrive together \u2192 aggregate.
|
|
102100
|
-
\u274C Wrong pattern:
|
|
102094
|
+
\u2705 Right multi-specialist pattern: one assistant turn with N Task tool_use blocks \u2192 wait \u2192 N results arrive together \u2192 aggregate.
|
|
102095
|
+
\u274C Wrong multi-specialist pattern: Task(hypothesis A) \u2192 wait for A \u2192 Task(hypothesis B).
|
|
102101
102096
|
|
|
102102
102097
|
You can also include your own \`read\` / \`grep\` / \`webfetch\` calls in the SAME turn as the parallel \`${REVIEWER_AGENT_NAME}\` dispatches \u2014 concurrent context-pulling on the orchestrator side runs in parallel with the lens fan-out and costs zero extra wall time.
|
|
102103
102098
|
|
|
102104
|
-
if a
|
|
102099
|
+
if a specialist errors out, times out, or returns nothing usable, retry it once with the same hypothesis. if it still fails, attempt to resolve the hypothesis yourself; if it remains disposition-changing and unresolved, surface the limitation and do not approve. each specialist gets:
|
|
102105
102100
|
- **the absolute \`diffPath\` (and \`incrementalDiffPath\` if available) from step 2's \`${t("checkout_pr")}\` return, named verbatim in the dispatch prompt** (e.g. \`diffPath: /tmp/pullfrog-XXXX/pr-NNN-SHA.diff\`). the reviewer's baked-in system prompt selects its FIRST action on this token \u2014 paraphrasing ("review the diff", "look at this PR") sends it down the \`git diff origin/<base>\` fallback, which fails on shallow GHA checkouts. the subagent \`read\`s those files for scope; it must NOT re-derive the diff via \`git diff\` (bare \`git diff origin/<base>\` is symmetric and pulls in the inverse of any commits that landed on \`<base>\` since the branch forked \u2014 pure noise, and the git tool rejects it). reading and codebase exploration are still its job.
|
|
102106
|
-
- **
|
|
102107
|
-
- **a Task \`description\` set to
|
|
102101
|
+
- **exactly one falsifiable hypothesis with explicit scope boundaries** \u2014 ask for evidence that supports or refutes it, never a broad "review for X, Y, and Z" prompt
|
|
102102
|
+
- **a Task \`description\` set to a short hypothesis label** (e.g. \`"webhook-replay"\`, \`"billing-rounding"\`) \u2014 the harness reads this field to label the subagent's log lines so parallel runs can be told apart in CI output. without it, every subagent shows up as \`subagent#N\`.
|
|
102108
102103
|
- if the lens touches external contracts, instruct the subagent to verify load-bearing claims via web search rather than trust training data, and to quote source URLs in its reasoning. action runs are non-interactive \u2014 there's no human in the loop to catch "I'm pretty sure Stripe does X."
|
|
102109
|
-
- ask the subagent to report findings with file paths and NEW line numbers from the diff so you can anchor
|
|
102104
|
+
- ask the subagent to report findings with file paths and NEW line numbers from the diff so you can validate and anchor them. you must still read the complete diff yourself.
|
|
102110
102105
|
|
|
102111
102106
|
delegation discipline:
|
|
102112
|
-
- do NOT summarize the PR for them (biases toward a validation frame)
|
|
102107
|
+
- do NOT summarize the PR for them (a lossy summary biases toward a validation frame; the raw diff is the source)
|
|
102113
102108
|
- do NOT hand them a curated reading list (let them discover scope)
|
|
102114
102109
|
- do NOT pre-shape their output with a finding schema
|
|
102115
102110
|
- do NOT mention the other lenses (independence is the point \u2014 overlapping findings are a strong signal)
|
|
102116
102111
|
|
|
102117
|
-
6. **aggregate & draft**: when
|
|
102112
|
+
6. **aggregate & draft**: when specialist results land, merge findings; de-dup overlaps (two specialists catching the same issue = higher-confidence signal); trace each finding yourself before accepting it. drop praise, style preferences, speculative/unverified claims, findings about pre-existing code unrelated to the PR (heuristic: if the finding's root cause lives in lines this PR added or modified, it's in scope; otherwise drop unless the PR plausibly introduced or amplified the regression), and anything not actionable. also drop **bloat-shaped findings** \u2014 proposed fixes that would add defensive checks for cases that can't happen, abstractions used once, comments restating obvious code, tests asserting tautologies, or "just-in-case" guards. subagents are fallible and bias toward recommending changes; the bar for an actionable inline comment is sound + correct + elegant. recommending a change that improves only one of the three (or worse, degrades elegance to nominally improve correctness) makes the codebase worse, not better.
|
|
102118
102113
|
|
|
102119
102114
|
**Hunt for non-anchored concerns before drafting.** After collecting your anchored findings, deliberately scan for concerns that have no specific line to point at \u2014 typically: deletion / cleanup plans for code the diff replaces or shadows; rollout sequencing (what happens to in-flight state during deploy / revert?); coverage gaps the diff implies but doesn't add; scope questions that only the human can answer (e.g. is the legacy path going away or is this a long-term dual track?); architectural risks the diff opens up that aren't a single-line bug. On substantial PRs (migrations, refactors, multi-file rewrites, version bumps that change runtime semantics), at least one such concern almost always exists; if you can't think of any, your bar is probably too high.
|
|
102120
102115
|
|
|
@@ -102148,10 +102143,10 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
102148
102143
|
|
|
102149
102144
|
${PR_SUMMARY_FORMAT}`
|
|
102150
102145
|
},
|
|
102151
|
-
// IncrementalReview shares Review's
|
|
102152
|
-
// format
|
|
102153
|
-
// prior
|
|
102154
|
-
// review" filter lives at aggregation time
|
|
102146
|
+
// IncrementalReview shares Review's minimum hypothesis-covering specialist
|
|
102147
|
+
// routing and body format, scoped to the incremental delta against the
|
|
102148
|
+
// prior Pullfrog review. The "issues must be NEW since the last Pullfrog
|
|
102149
|
+
// review" filter lives at aggregation time, NOT in the subagent
|
|
102155
102150
|
// prompt — pushing the filter into subagents matches the canonical anneal
|
|
102156
102151
|
// anti-pattern of "list known pre-existing failures — don't flag these"
|
|
102157
102152
|
// and suppresses signal on regressions the new commits amplified. A
|
|
@@ -102166,9 +102161,9 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
102166
102161
|
|
|
102167
102162
|
1. **task list**: create your task list for this run as your first action.
|
|
102168
102163
|
|
|
102169
|
-
2. **checkout**: call \`${t("checkout_pr")}\` \u2014 this returns PR metadata, \`diffPath\` (full diff),
|
|
102164
|
+
2. **checkout**: call \`${t("checkout_pr")}\` \u2014 this returns PR metadata, \`diffPath\` (full diff), \`incrementalDiffPath\` (changes since last reviewed version, if available), and a supplemental \`impactPath\` when change-impact extraction is enabled.
|
|
102170
102165
|
|
|
102171
|
-
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.
|
|
102166
|
+
3. **incremental scope**: if \`incrementalDiffPath\` is present, read it FIRST to see what changed since the last review. this is a range-diff that isolates the net changes, filtering out base branch noise. then read the authoritative full diff end-to-end, beginning with the TOC and using its line ranges as your coverage checklist. if no incremental diff is present, start with the full-diff TOC, determine what changed since Pullfrog's most recent review, and complete the raw-diff read. only after establishing that authoritative scope and completing raw-diff coverage, use \`impactPath\` as an explicitly incomplete list of reference leads; it never replaces raw-diff reading or establishes coverage.
|
|
102172
102167
|
|
|
102173
102168
|
4. **prior feedback \u2014 read AND retire it**: fetch previous reviews via \`${t("list_pull_request_reviews")}\`, then call \`${t("get_review_comments")}\` on each prior Pullfrog review. Each thread renders as a section whose first line is a fenced tag \`comment author=<login> id=<fullDatabaseId> review=<reviewId> thread=<graphqlId>\`; section headers carry \`[RESOLVED]\` / \`[OUTDATED]\` when relevant. For every **open, Pullfrog-originated** thread, decide and act:
|
|
102174
102169
|
|
|
@@ -102182,49 +102177,48 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
102182
102177
|
|
|
102183
102178
|
5. **triage**: orient on the *incremental* changes \u2014 domain, seams, external contracts, user-facing surfaces. pull as much context as you need to render a confident review: read related files, grep for callers of changed symbols, check tests that exercise the touched paths. **you are the synthesizer.**
|
|
102184
102179
|
|
|
102185
|
-
if the incremental changes are **genuinely trivial**, skip
|
|
102180
|
+
if the incremental changes are **genuinely trivial**, skip specialists entirely and jump to step 10's non-substantive path (do NOT submit a review).
|
|
102186
102181
|
|
|
102187
102182
|
"Genuinely trivial" (skip): formatting/comment tweaks, import reordering, lockfile regen, mechanical rename of import paths, whitespace-only.
|
|
102188
102183
|
"Looks trivial but isn't" (do NOT skip \u2014 same anti-patterns as Review mode): 1-line changes to SQL/regex/auth/billing/permissions/signature-verification code; flipping feature-flag defaults or retry/timeout constants; money/tax/HTTP-method/redirect changes; tightening or loosening a comparison operator; mixed diffs with a semantic line buried in formatting.
|
|
102189
102184
|
When unsure, treat as non-trivial.
|
|
102190
102185
|
|
|
102191
|
-
6. **
|
|
102186
|
+
6. **specialist decision \u2014 minimum hypothesis coverage**.
|
|
102192
102187
|
|
|
102193
|
-
|
|
102188
|
+
After full-diff coverage and triage, identify the load-bearing questions about the incremental changes that you still cannot resolve confidently yourself. A specialist hypothesis is load-bearing only when its answer could yield an actionable new finding that changes the review disposition and warrants independent investigation, and falsifiable only when the specialist can return evidence that supports or refutes it. Generic requests for extra confidence, polish, or "another look" do not qualify.
|
|
102194
102189
|
|
|
102195
|
-
|
|
102196
|
-
-
|
|
102197
|
-
-
|
|
102198
|
-
- parallel-
|
|
102190
|
+
Route the **minimum number of \`${REVIEWER_AGENT_NAME}\` specialists** needed to cover those unresolved hypotheses. Most incremental reviews need **0 or 1**, especially thread-reply re-reviews:
|
|
102191
|
+
- dispatch 0 when you can resolve every disposition-changing question directly
|
|
102192
|
+
- dispatch 1 when exactly one falsifiable, load-bearing hypothesis warrants independent investigation
|
|
102193
|
+
- dispatch 2+ in parallel when multiple orthogonal load-bearing hypotheses remain, or when the user explicitly requests an exhaustive or multi-angle review
|
|
102199
102194
|
|
|
102200
|
-
**
|
|
102195
|
+
**There is NO one-specialist cap or fixed maximum.** Cover every orthogonal load-bearing hypothesis that remains; do not collapse multiple real questions into one broad prompt just to reduce the count. There is also no file-count, line-count, schema, quota, or hard-budget threshold \u2014 diff size is not a proxy for review uncertainty.
|
|
102201
102196
|
|
|
102202
|
-
|
|
102197
|
+
The primary reviewer remains responsible for reading the complete raw full diff plus the incremental diff, investigating surrounding code, validating every returned finding, and synthesizing the final review. Specialist reads supplement that work; they never replace it or satisfy the primary's diff-coverage obligation.
|
|
102203
102198
|
|
|
102204
|
-
|
|
102199
|
+
Specialist hypotheses can draw on Review mode's themed or subsystem framings, but every dispatch must turn the framing into one falsifiable question with explicit scope boundaries.
|
|
102205
102200
|
|
|
102206
|
-
|
|
102207
|
-
Default tool-call behavior is **serial dispatch**: emit one Task call, await result, emit next, await, etc. This collapses your fan-out into a sequential review where each lens adds N \xD7 (orchestrator-think-time + lens-execution-time) to wall time. **YOU MUST OVERRIDE THIS DEFAULT.** Emit ALL of your Task tool_use blocks in the SAME assistant message, BEFORE you read ANY result from ANY of them.
|
|
102201
|
+
7. **dispatch specialists (only if step 6 found unresolved hypotheses)**: dispatch one \`${REVIEWER_AGENT_NAME}\` for one hypothesis. For 2+ hypotheses, emit every Task tool_use block **IN A SINGLE ASSISTANT TURN** before reading any result so the investigations run in parallel rather than serially.
|
|
102208
102202
|
|
|
102209
|
-
\u2705 Right pattern: one assistant turn with N Task tool_use blocks \u2192 wait \u2192 N results arrive together \u2192 aggregate.
|
|
102210
|
-
\u274C Wrong pattern:
|
|
102203
|
+
\u2705 Right multi-specialist pattern: one assistant turn with N Task tool_use blocks \u2192 wait \u2192 N results arrive together \u2192 aggregate.
|
|
102204
|
+
\u274C Wrong multi-specialist pattern: Task(hypothesis A) \u2192 wait for A \u2192 Task(hypothesis B).
|
|
102211
102205
|
|
|
102212
102206
|
You can also include your own \`read\` / \`grep\` / \`webfetch\` calls in the SAME turn as the parallel \`${REVIEWER_AGENT_NAME}\` dispatches.
|
|
102213
102207
|
|
|
102214
|
-
if a
|
|
102208
|
+
if a specialist errors out, times out, or returns nothing usable, retry it once with the same hypothesis. if it still fails, attempt to resolve the hypothesis yourself; if it remains disposition-changing and unresolved, surface the limitation and do not approve. each specialist gets:
|
|
102215
102209
|
- **the absolute diff path(s) from step 2's \`${t("checkout_pr")}\` return, named verbatim in the dispatch prompt.** when \`incrementalDiffPath\` is present, name BOTH (\`incrementalDiffPath: /tmp/.../pr-NNN-SHA-incremental.diff\` then \`diffPath: /tmp/.../pr-NNN-SHA.diff\`) \u2014 the reviewer's baked-in prompt reads incremental first and uses full for context; when only \`diffPath\` exists, name it alone. the subagent \`read\`s those files; it must NOT re-derive via \`git diff\` (bare \`git diff origin/<base>\` is symmetric and pulls in the inverse of base-branch progress \u2014 pure noise, and the git tool rejects it), and paraphrasing ("review the new commits") sends it down that fallback, which also fails on shallow GHA checkouts. do NOT tell them to skip pre-existing issues \u2014 that suppresses regressions the new commits amplified; the "issues must be NEW" filter lives at aggregation time (step 8), not in the subagent prompt.
|
|
102216
|
-
- **
|
|
102217
|
-
- **a Task \`description\` set to
|
|
102210
|
+
- **exactly one falsifiable hypothesis with explicit scope boundaries** \u2014 ask for evidence that supports or refutes it, never a broad "review for X, Y, and Z" prompt
|
|
102211
|
+
- **a Task \`description\` set to a short hypothesis label** \u2014 the harness reads this field to label log lines so parallel runs can be told apart.
|
|
102218
102212
|
- if the lens touches external contracts, instruct the subagent to verify load-bearing claims via web search and quote source URLs.
|
|
102219
|
-
- ask the subagent to report findings with file paths and NEW line numbers from the full PR diff so you can anchor
|
|
102213
|
+
- ask the subagent to report findings with file paths and NEW line numbers from the full PR diff so you can validate and anchor them. you must still read the complete incremental and full diff scope yourself.
|
|
102220
102214
|
|
|
102221
102215
|
delegation discipline:
|
|
102222
|
-
- do NOT summarize the changes for them (biases toward validation frame)
|
|
102216
|
+
- do NOT summarize the changes for them (a lossy summary biases toward a validation frame; the raw diff is the source)
|
|
102223
102217
|
- do NOT hand them a curated reading list (let them discover scope)
|
|
102224
102218
|
- do NOT pre-shape their output with a finding schema
|
|
102225
102219
|
- do NOT mention the other lenses (independence is the point)
|
|
102226
102220
|
|
|
102227
|
-
8. **aggregate, draft, self-critique**: merge findings (yours +
|
|
102221
|
+
8. **aggregate, draft, self-critique**: merge findings (yours + output from every specialist you dispatched); de-dup overlaps; trace each finding yourself. drop praise, style preferences, speculative/unverified claims, findings about pre-existing code unrelated to the new commits, anything not actionable, and anything that re-states prior review feedback (heuristic: if the finding's root cause lives in lines the *new commits* added or modified, it's in scope; otherwise drop). also drop **bloat-shaped findings** \u2014 proposed fixes that would add defensive checks for cases that can't happen, abstractions used once, comments restating obvious code, tests asserting tautologies, or "just-in-case" guards. subagents are fallible and bias toward recommending changes; the bar for an actionable inline comment is sound + correct + elegant. recommending a change that improves only one of the three (or degrades elegance to nominally improve correctness) makes the codebase worse, not better. To compute "lines the new commits added or modified": if \`incrementalDiffPath\` from step 2 is present, use it directly. Otherwise, take the prior Pullfrog review's \`commit_id\` (returned alongside each entry from \`${t("list_pull_request_reviews")}\` in step 4) and run \`git diff <prior-review-sha>..HEAD\` to isolate the lines added since that review.
|
|
102228
102222
|
|
|
102229
102223
|
**Hunt for non-anchored concerns before drafting.** After collecting your anchored findings, deliberately scan for concerns that have no specific line to point at \u2014 typically: deletion / cleanup plans for code the new commits replace or shadow; rollout sequencing (what happens to in-flight state during deploy / revert?); coverage gaps the new commits imply but don't add; scope questions that only the human can answer (e.g. is the legacy path going away or is this a long-term dual track?); architectural risks the new commits open up that aren't a single-line bug. On substantial incremental diffs (migrations, refactors, multi-file rewrites, version bumps that change runtime semantics), at least one such concern almost always exists; if you can't think of any, your bar is probably too high.
|
|
102230
102224
|
|
|
@@ -102321,7 +102315,7 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
102321
102315
|
|
|
102322
102316
|
1. **task list**: create your task list for this run as your first action.
|
|
102323
102317
|
|
|
102324
|
-
2. Analyze the task. For simple operations (labeling,
|
|
102318
|
+
2. Analyze the task. For simple operations (labeling, answering questions, running a single command), handle directly \u2014 but your answer only reaches the user through \`${t("report_progress")}\` (step 4); raw assistant text is discarded. If a standalone comment on the current issue/PR is the task's sole requested deliverable, create that comment directly and skip \`${t("report_progress")}\`.
|
|
102325
102319
|
|
|
102326
102320
|
3. For substantial work \u2014 code changes across multiple files, multi-step investigations:
|
|
102327
102321
|
- plan your approach before starting
|
|
@@ -102331,8 +102325,8 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
102331
102325
|
|
|
102332
102326
|
4. Finalize:
|
|
102333
102327
|
- if code changes were made, get them onto a pull request (new or existing) using ${signedCommits ? `\`${t("commit_changes")}\`` : `\`${t("push_branch")}\``} and \`${t("create_pull_request")}\` as needed. \`git status\` must be clean before you finish (see *SYSTEM* Git rules if this fails).
|
|
102334
|
-
- call \`${t("report_progress")}\` once with results \u2014 include exact tool errors if push or PR creation failed
|
|
102335
|
-
- if the task involved labeling
|
|
102328
|
+
- call \`${t("report_progress")}\` once with results \u2014 include exact tool errors if push or PR creation failed. skip this only when a standalone comment on the current target was the task's sole requested deliverable
|
|
102329
|
+
- if the task involved labeling or other GitHub operations, perform those directly`
|
|
102336
102330
|
}
|
|
102337
102331
|
];
|
|
102338
102332
|
}
|
|
@@ -102887,7 +102881,7 @@ async function runClaude(params) {
|
|
|
102887
102881
|
}
|
|
102888
102882
|
} else if (block.type === "tool_use") {
|
|
102889
102883
|
const toolName = block.name || "unknown";
|
|
102890
|
-
if (params.onToolUse) {
|
|
102884
|
+
if (params.onToolUse && label === ORCHESTRATOR_LABEL) {
|
|
102891
102885
|
params.onToolUse({
|
|
102892
102886
|
toolName,
|
|
102893
102887
|
input: block.input
|
|
@@ -103279,7 +103273,7 @@ var claude = agent({
|
|
|
103279
103273
|
const cliPath = await installClaudeCli();
|
|
103280
103274
|
const specifier = ctx.payload.proxyModel ?? ctx.resolvedModel;
|
|
103281
103275
|
const bedrockModelId = process.env[BEDROCK_MODEL_ID_ENV]?.trim();
|
|
103282
|
-
const isBedrockRoute = specifier !== void 0 && bedrockModelId !== void 0 && bedrockModelId === specifier
|
|
103276
|
+
const isBedrockRoute = specifier !== void 0 && bedrockModelId !== void 0 && bedrockModelId === specifier;
|
|
103283
103277
|
const vertexModelId = process.env[VERTEX_MODEL_ID_ENV]?.trim();
|
|
103284
103278
|
const isVertexRoute2 = specifier !== void 0 && vertexModelId !== void 0 && vertexModelId === specifier && isVertexAnthropicId(specifier);
|
|
103285
103279
|
const model = !specifier ? void 0 : isBedrockRoute ? specifier : isVertexRoute2 ? void 0 : stripProviderPrefix(specifier);
|
|
@@ -103327,6 +103321,7 @@ var claude = agent({
|
|
|
103327
103321
|
...homeEnv,
|
|
103328
103322
|
PWD: repoDir
|
|
103329
103323
|
};
|
|
103324
|
+
env2.CLAUDE_CODE_AUTO_COMPACT_WINDOW ||= "500000";
|
|
103330
103325
|
if (isBedrockRoute) {
|
|
103331
103326
|
env2.CLAUDE_CODE_USE_BEDROCK = "1";
|
|
103332
103327
|
}
|
|
@@ -142278,8 +142273,8 @@ function closeBrowserDaemon(toolState) {
|
|
|
142278
142273
|
|
|
142279
142274
|
// mcp/checkout.ts
|
|
142280
142275
|
import { createHash as createHash2 } from "node:crypto";
|
|
142281
|
-
import { statSync, unlinkSync as unlinkSync3, writeFileSync as
|
|
142282
|
-
import { join as
|
|
142276
|
+
import { statSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync8 } from "node:fs";
|
|
142277
|
+
import { join as join13 } from "node:path";
|
|
142283
142278
|
|
|
142284
142279
|
// node_modules/.pnpm/@ark+schema@0.56.0/node_modules/@ark/schema/out/shared/errors.js
|
|
142285
142280
|
var ArkError = class _ArkError extends CastableBase {
|
|
@@ -149748,6 +149743,367 @@ function ensureRepoState(toolState, init) {
|
|
|
149748
149743
|
return created;
|
|
149749
149744
|
}
|
|
149750
149745
|
|
|
149746
|
+
// utils/changeImpact.ts
|
|
149747
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
149748
|
+
import { writeFileSync as writeFileSync6 } from "node:fs";
|
|
149749
|
+
import { join as join7 } from "node:path";
|
|
149750
|
+
var MAX_CANDIDATES = 24;
|
|
149751
|
+
var MAX_ATOM_LENGTH = 128;
|
|
149752
|
+
var MAX_ATOMS = 12;
|
|
149753
|
+
var MAX_REFERENCES = 6;
|
|
149754
|
+
var MAX_EXCERPT = 160;
|
|
149755
|
+
var MAX_BYTES = 16e3;
|
|
149756
|
+
function isSymbolShaped(atom) {
|
|
149757
|
+
return atom.length >= 4 && atom.length <= MAX_ATOM_LENGTH && /[A-Z_]/.test(atom);
|
|
149758
|
+
}
|
|
149759
|
+
function getAtomConfidence(params) {
|
|
149760
|
+
const before = params.text.slice(Math.max(0, params.index - 64), params.index);
|
|
149761
|
+
const after = params.text.slice(
|
|
149762
|
+
params.index + params.atom.length,
|
|
149763
|
+
params.index + params.atom.length + 16
|
|
149764
|
+
);
|
|
149765
|
+
if (/\b(?:class|interface|type|enum|function|def|func|fn|struct|trait)\s+$/.test(before))
|
|
149766
|
+
return 4;
|
|
149767
|
+
if (/\b(?:const|let|var)\s+$/.test(before) || /^\s*\??\s*[:(]/.test(after)) return 3;
|
|
149768
|
+
const quoted = /['"`]$/.test(before) && /^['"`]/.test(after);
|
|
149769
|
+
if (quoted || before.endsWith(".") || after.startsWith(".") || params.atom.includes("_"))
|
|
149770
|
+
return 2;
|
|
149771
|
+
return 1;
|
|
149772
|
+
}
|
|
149773
|
+
function getChangedAtoms(text) {
|
|
149774
|
+
const atoms = /* @__PURE__ */ new Map();
|
|
149775
|
+
for (const match3 of text.matchAll(/[A-Za-z_][A-Za-z0-9_]*/g)) {
|
|
149776
|
+
const atom = match3[0];
|
|
149777
|
+
if (!isSymbolShaped(atom)) continue;
|
|
149778
|
+
const confidence = getAtomConfidence({ text, atom, index: match3.index ?? 0 });
|
|
149779
|
+
atoms.set(atom, Math.max(atoms.get(atom) ?? 0, confidence));
|
|
149780
|
+
}
|
|
149781
|
+
return atoms;
|
|
149782
|
+
}
|
|
149783
|
+
function recordChange(params) {
|
|
149784
|
+
for (const [atom, confidence] of getChangedAtoms(params.text)) {
|
|
149785
|
+
const change = params.changes.get(atom) ?? { atom, confidence, added: [], removed: [] };
|
|
149786
|
+
change.confidence = Math.max(change.confidence, confidence);
|
|
149787
|
+
change[params.location.kind].push(params.location);
|
|
149788
|
+
params.changes.set(atom, change);
|
|
149789
|
+
}
|
|
149790
|
+
}
|
|
149791
|
+
function locationKey(location) {
|
|
149792
|
+
return `${location.path}\0${location.line}`;
|
|
149793
|
+
}
|
|
149794
|
+
function collectFileChanges(collection, file2) {
|
|
149795
|
+
if (!file2.patch) return;
|
|
149796
|
+
let oldLine = 0;
|
|
149797
|
+
let newLine = 0;
|
|
149798
|
+
for (const patchLine of file2.patch.split("\n")) {
|
|
149799
|
+
const hunk = patchLine.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
149800
|
+
if (hunk) {
|
|
149801
|
+
oldLine = Number.parseInt(hunk[1], 10);
|
|
149802
|
+
newLine = Number.parseInt(hunk[2], 10);
|
|
149803
|
+
} else if (patchLine.startsWith("+")) {
|
|
149804
|
+
const location = { path: file2.filename, line: newLine++, kind: "added" };
|
|
149805
|
+
collection.addedLines.add(locationKey(location));
|
|
149806
|
+
recordChange({ changes: collection.changes, location, text: patchLine.slice(1) });
|
|
149807
|
+
} else if (patchLine.startsWith("-")) {
|
|
149808
|
+
const oldPath = file2.previous_filename ?? file2.filename;
|
|
149809
|
+
recordChange({
|
|
149810
|
+
changes: collection.changes,
|
|
149811
|
+
location: { path: oldPath, line: oldLine++, kind: "removed" },
|
|
149812
|
+
text: patchLine.slice(1)
|
|
149813
|
+
});
|
|
149814
|
+
} else if (!patchLine.startsWith("\\")) {
|
|
149815
|
+
oldLine++;
|
|
149816
|
+
newLine++;
|
|
149817
|
+
}
|
|
149818
|
+
}
|
|
149819
|
+
}
|
|
149820
|
+
function compareChanges(a, b) {
|
|
149821
|
+
if (a.confidence !== b.confidence) return b.confidence - a.confidence;
|
|
149822
|
+
const removedDelta = Number(b.removed.length > 0) - Number(a.removed.length > 0);
|
|
149823
|
+
if (removedDelta !== 0) return removedDelta;
|
|
149824
|
+
const aDelta = Math.abs(a.added.length - a.removed.length);
|
|
149825
|
+
const bDelta = Math.abs(b.added.length - b.removed.length);
|
|
149826
|
+
if (aDelta !== bDelta) return bDelta - aDelta;
|
|
149827
|
+
if (a.atom.length !== b.atom.length) return b.atom.length - a.atom.length;
|
|
149828
|
+
return a.atom < b.atom ? -1 : Number(a.atom > b.atom);
|
|
149829
|
+
}
|
|
149830
|
+
function getCandidates(files) {
|
|
149831
|
+
const collection = {
|
|
149832
|
+
changes: /* @__PURE__ */ new Map(),
|
|
149833
|
+
addedLines: /* @__PURE__ */ new Set()
|
|
149834
|
+
};
|
|
149835
|
+
for (const file2 of files) collectFileChanges(collection, file2);
|
|
149836
|
+
const candidates = [...collection.changes.values()].filter((change) => change.added.length !== change.removed.length).sort(compareChanges);
|
|
149837
|
+
return {
|
|
149838
|
+
selected: candidates.slice(0, MAX_CANDIDATES),
|
|
149839
|
+
total: candidates.length,
|
|
149840
|
+
addedLines: collection.addedLines
|
|
149841
|
+
};
|
|
149842
|
+
}
|
|
149843
|
+
function emptyBuckets(candidates) {
|
|
149844
|
+
return new Map(candidates.map((candidate) => [candidate.atom, { count: 0, samples: [] }]));
|
|
149845
|
+
}
|
|
149846
|
+
function createGrepRecordState() {
|
|
149847
|
+
return {
|
|
149848
|
+
phase: "path",
|
|
149849
|
+
path: "",
|
|
149850
|
+
line: "",
|
|
149851
|
+
sample: "",
|
|
149852
|
+
truncated: false,
|
|
149853
|
+
searchTail: "",
|
|
149854
|
+
searchStart: 0,
|
|
149855
|
+
matches: /* @__PURE__ */ new Set()
|
|
149856
|
+
};
|
|
149857
|
+
}
|
|
149858
|
+
function resetGrepRecord(state) {
|
|
149859
|
+
state.phase = "path";
|
|
149860
|
+
state.path = "";
|
|
149861
|
+
state.line = "";
|
|
149862
|
+
state.sample = "";
|
|
149863
|
+
state.truncated = false;
|
|
149864
|
+
state.searchTail = "";
|
|
149865
|
+
state.searchStart = 0;
|
|
149866
|
+
state.matches.clear();
|
|
149867
|
+
}
|
|
149868
|
+
function isWordCharacter(value2) {
|
|
149869
|
+
return value2 !== void 0 && /[A-Za-z0-9_]/.test(value2);
|
|
149870
|
+
}
|
|
149871
|
+
function recordCandidateMatches(params) {
|
|
149872
|
+
for (const candidate of params.candidates) {
|
|
149873
|
+
if (params.matches.has(candidate.atom)) continue;
|
|
149874
|
+
let index = params.text.indexOf(candidate.atom, params.minStart);
|
|
149875
|
+
while (index >= 0 && index < params.maxStart) {
|
|
149876
|
+
const before = params.text[index - 1];
|
|
149877
|
+
const after = params.text[index + candidate.atom.length];
|
|
149878
|
+
if (!isWordCharacter(before) && !isWordCharacter(after)) {
|
|
149879
|
+
params.matches.add(candidate.atom);
|
|
149880
|
+
break;
|
|
149881
|
+
}
|
|
149882
|
+
index = params.text.indexOf(candidate.atom, index + 1);
|
|
149883
|
+
}
|
|
149884
|
+
}
|
|
149885
|
+
}
|
|
149886
|
+
function consumeReferenceText(params) {
|
|
149887
|
+
const remaining = MAX_EXCERPT - params.state.sample.length;
|
|
149888
|
+
params.state.sample += params.text.slice(0, Math.max(remaining, 0));
|
|
149889
|
+
if (params.text.length > remaining) params.state.truncated = true;
|
|
149890
|
+
const searchable = params.state.searchTail + params.text;
|
|
149891
|
+
const consumed = params.final ? searchable.length : Math.max(0, searchable.length - params.maxAtomLength - 2);
|
|
149892
|
+
recordCandidateMatches({
|
|
149893
|
+
text: searchable,
|
|
149894
|
+
minStart: params.state.searchStart,
|
|
149895
|
+
maxStart: consumed,
|
|
149896
|
+
candidates: params.candidates,
|
|
149897
|
+
matches: params.state.matches
|
|
149898
|
+
});
|
|
149899
|
+
if (params.final) {
|
|
149900
|
+
params.state.searchTail = "";
|
|
149901
|
+
params.state.searchStart = 0;
|
|
149902
|
+
return;
|
|
149903
|
+
}
|
|
149904
|
+
const tailStart = Math.max(0, consumed - 1);
|
|
149905
|
+
params.state.searchTail = searchable.slice(tailStart);
|
|
149906
|
+
params.state.searchStart = consumed > 0 ? 1 : params.state.searchStart;
|
|
149907
|
+
}
|
|
149908
|
+
function recordGrepReference(params) {
|
|
149909
|
+
const prefix = `${params.treeish}:`;
|
|
149910
|
+
const path4 = params.state.path.startsWith(prefix) ? params.state.path.slice(prefix.length) : params.state.path;
|
|
149911
|
+
const line = Number.parseInt(params.state.line, 10);
|
|
149912
|
+
if (!Number.isFinite(line)) return;
|
|
149913
|
+
const reference2 = {
|
|
149914
|
+
path: path4,
|
|
149915
|
+
line,
|
|
149916
|
+
text: params.state.sample,
|
|
149917
|
+
truncated: params.state.truncated
|
|
149918
|
+
};
|
|
149919
|
+
if (params.addedLines.has(locationKey(reference2))) return;
|
|
149920
|
+
for (const atom of params.state.matches) {
|
|
149921
|
+
const bucket = params.buckets.get(atom);
|
|
149922
|
+
if (!bucket) continue;
|
|
149923
|
+
bucket.count++;
|
|
149924
|
+
if (bucket.samples.length < MAX_REFERENCES) bucket.samples.push(reference2);
|
|
149925
|
+
}
|
|
149926
|
+
}
|
|
149927
|
+
function consumeGrepChunk(params) {
|
|
149928
|
+
let offset = 0;
|
|
149929
|
+
while (offset < params.chunk.length) {
|
|
149930
|
+
if (params.state.phase !== "text") {
|
|
149931
|
+
const end2 = params.chunk.indexOf("\0", offset);
|
|
149932
|
+
const value2 = end2 < 0 ? params.chunk.slice(offset) : params.chunk.slice(offset, end2);
|
|
149933
|
+
params.state[params.state.phase] += value2;
|
|
149934
|
+
if (end2 < 0) return;
|
|
149935
|
+
params.state.phase = params.state.phase === "path" ? "line" : "text";
|
|
149936
|
+
offset = end2 + 1;
|
|
149937
|
+
continue;
|
|
149938
|
+
}
|
|
149939
|
+
const end = params.chunk.indexOf("\n", offset);
|
|
149940
|
+
const final = end >= 0;
|
|
149941
|
+
const text = final ? params.chunk.slice(offset, end) : params.chunk.slice(offset);
|
|
149942
|
+
consumeReferenceText({
|
|
149943
|
+
state: params.state,
|
|
149944
|
+
text,
|
|
149945
|
+
candidates: params.candidates,
|
|
149946
|
+
maxAtomLength: params.maxAtomLength,
|
|
149947
|
+
final
|
|
149948
|
+
});
|
|
149949
|
+
if (!final) return;
|
|
149950
|
+
recordGrepReference(params);
|
|
149951
|
+
resetGrepRecord(params.state);
|
|
149952
|
+
offset = end + 1;
|
|
149953
|
+
}
|
|
149954
|
+
}
|
|
149955
|
+
async function lookupReferences(params) {
|
|
149956
|
+
const buckets = emptyBuckets(params.candidates);
|
|
149957
|
+
if (params.candidates.length === 0) return { buckets, error: void 0 };
|
|
149958
|
+
const patterns = params.candidates.flatMap((candidate) => ["-e", candidate.atom]);
|
|
149959
|
+
const child = spawn2(
|
|
149960
|
+
"git",
|
|
149961
|
+
[
|
|
149962
|
+
"grep",
|
|
149963
|
+
"--no-color",
|
|
149964
|
+
"--full-name",
|
|
149965
|
+
"-n",
|
|
149966
|
+
"-z",
|
|
149967
|
+
"-I",
|
|
149968
|
+
"-w",
|
|
149969
|
+
"-F",
|
|
149970
|
+
...patterns,
|
|
149971
|
+
params.treeish,
|
|
149972
|
+
"--",
|
|
149973
|
+
":(top)"
|
|
149974
|
+
],
|
|
149975
|
+
{ env: resolveEnv(void 0), stdio: ["ignore", "pipe", "pipe"] }
|
|
149976
|
+
);
|
|
149977
|
+
const state = createGrepRecordState();
|
|
149978
|
+
const maxAtomLength = Math.max(...params.candidates.map((candidate) => candidate.atom.length));
|
|
149979
|
+
let spawnError;
|
|
149980
|
+
child.stdout.setEncoding("utf8");
|
|
149981
|
+
child.stdout.on("data", (chunk) => {
|
|
149982
|
+
consumeGrepChunk({
|
|
149983
|
+
chunk,
|
|
149984
|
+
state,
|
|
149985
|
+
candidates: params.candidates,
|
|
149986
|
+
maxAtomLength,
|
|
149987
|
+
buckets,
|
|
149988
|
+
addedLines: params.addedLines,
|
|
149989
|
+
treeish: params.treeish
|
|
149990
|
+
});
|
|
149991
|
+
});
|
|
149992
|
+
child.stderr.resume();
|
|
149993
|
+
const status = await new Promise((resolve3) => {
|
|
149994
|
+
child.once("error", (error49) => {
|
|
149995
|
+
spawnError = error49.message;
|
|
149996
|
+
resolve3(null);
|
|
149997
|
+
});
|
|
149998
|
+
child.once("close", resolve3);
|
|
149999
|
+
});
|
|
150000
|
+
if (spawnError || status !== 0 && status !== 1) {
|
|
150001
|
+
const error49 = spawnError ?? `git grep exit ${status ?? "unknown"}`;
|
|
150002
|
+
log.warning(`\xBB change impact lookup failed: ${error49}`);
|
|
150003
|
+
return { buckets: emptyBuckets(params.candidates), error: error49 };
|
|
150004
|
+
}
|
|
150005
|
+
if (state.phase === "text") {
|
|
150006
|
+
consumeReferenceText({
|
|
150007
|
+
state,
|
|
150008
|
+
text: "",
|
|
150009
|
+
candidates: params.candidates,
|
|
150010
|
+
maxAtomLength,
|
|
150011
|
+
final: true
|
|
150012
|
+
});
|
|
150013
|
+
recordGrepReference({ state, buckets, addedLines: params.addedLines, treeish: params.treeish });
|
|
150014
|
+
} else if (state.phase !== "path" || state.path) {
|
|
150015
|
+
const error49 = "malformed git grep output";
|
|
150016
|
+
log.warning(`\xBB change impact lookup failed: ${error49}`);
|
|
150017
|
+
return { buckets: emptyBuckets(params.candidates), error: error49 };
|
|
150018
|
+
}
|
|
150019
|
+
return { buckets, error: void 0 };
|
|
150020
|
+
}
|
|
150021
|
+
function formatReference(reference2) {
|
|
150022
|
+
const text = reference2.text.trim();
|
|
150023
|
+
const excerpt = reference2.truncated ? `${text.slice(0, MAX_EXCERPT - 1)}\u2026` : text;
|
|
150024
|
+
return `- ${JSON.stringify(reference2.path)}:${reference2.line} ${JSON.stringify(excerpt)}`;
|
|
150025
|
+
}
|
|
150026
|
+
function formatChangedLocations(change) {
|
|
150027
|
+
const selected = [change.removed[0], change.added[0]].filter(
|
|
150028
|
+
(location) => location !== void 0
|
|
150029
|
+
);
|
|
150030
|
+
const rendered = selected.map((location) => `${location.kind} ${JSON.stringify(location.path)}:${location.line}`).join(", ");
|
|
150031
|
+
const omitted = change.removed.length + change.added.length - selected.length;
|
|
150032
|
+
return omitted > 0 ? `${rendered}; ${omitted} more changed location(s) omitted` : rendered;
|
|
150033
|
+
}
|
|
150034
|
+
function formatEntry(entry) {
|
|
150035
|
+
const count = entry.bucket.count;
|
|
150036
|
+
const sampleNote = count > entry.bucket.samples.length ? `${count} matched line(s); first ${entry.bucket.samples.length} shown.` : `${count} matched line(s).`;
|
|
150037
|
+
return [
|
|
150038
|
+
`## \`${entry.change.atom}\``,
|
|
150039
|
+
"",
|
|
150040
|
+
`Changed at: ${formatChangedLocations(entry.change)}`,
|
|
150041
|
+
`Tracked references: ${sampleNote}`,
|
|
150042
|
+
"",
|
|
150043
|
+
...entry.bucket.samples.map(formatReference)
|
|
150044
|
+
].join("\n");
|
|
150045
|
+
}
|
|
150046
|
+
function formatArtifact(params) {
|
|
150047
|
+
const lines = [
|
|
150048
|
+
"# Change impact leads",
|
|
150049
|
+
"",
|
|
150050
|
+
"> Supplemental and explicitly incomplete. Read the authoritative `diffPath` TOC and raw lines first. Absence here is not evidence of no impact.",
|
|
150051
|
+
"",
|
|
150052
|
+
`Scope: base ${params.baseSha}, head ${params.headSha}; ${params.fileCount} changed file(s).`,
|
|
150053
|
+
"Generated from changed symbol-shaped identifiers, then one exact-word lookup pinned to the checked-out head across Git-tracked text files. Changed added lines are excluded; snippets are capped at 160 characters.",
|
|
150054
|
+
"",
|
|
150055
|
+
`Candidate atoms searched: ${params.candidateCount}/${params.totalCandidateCount}.`
|
|
150056
|
+
];
|
|
150057
|
+
if (params.lookupError) lines.push(`Tracked-file lookup unavailable: ${params.lookupError}.`);
|
|
150058
|
+
let content = `${lines.join("\n")}
|
|
150059
|
+
`;
|
|
150060
|
+
let renderedAtomCount = 0;
|
|
150061
|
+
let referenceCount = 0;
|
|
150062
|
+
for (const entry of params.entries.filter((candidate) => candidate.bucket.count > 0)) {
|
|
150063
|
+
if (renderedAtomCount >= MAX_ATOMS) break;
|
|
150064
|
+
const addition = `
|
|
150065
|
+
${formatEntry(entry)}
|
|
150066
|
+
`;
|
|
150067
|
+
if (Buffer.byteLength(content + addition) > MAX_BYTES) break;
|
|
150068
|
+
content += addition;
|
|
150069
|
+
renderedAtomCount++;
|
|
150070
|
+
referenceCount += entry.bucket.count;
|
|
150071
|
+
}
|
|
150072
|
+
if (renderedAtomCount === 0) {
|
|
150073
|
+
const empty = "\nNo bounded tracked references found. This is not evidence of no wider impact.\n";
|
|
150074
|
+
if (Buffer.byteLength(content + empty) <= MAX_BYTES) content += empty;
|
|
150075
|
+
}
|
|
150076
|
+
return { content, renderedAtomCount, referenceCount };
|
|
150077
|
+
}
|
|
150078
|
+
async function createChangeImpactArtifact(ctx, params) {
|
|
150079
|
+
const candidates = getCandidates(params.files);
|
|
150080
|
+
const lookup2 = await lookupReferences({
|
|
150081
|
+
candidates: candidates.selected,
|
|
150082
|
+
addedLines: candidates.addedLines,
|
|
150083
|
+
treeish: params.headSha
|
|
150084
|
+
});
|
|
150085
|
+
const artifact = formatArtifact({
|
|
150086
|
+
entries: candidates.selected.map((change) => {
|
|
150087
|
+
return { change, bucket: lookup2.buckets.get(change.atom) ?? { count: 0, samples: [] } };
|
|
150088
|
+
}),
|
|
150089
|
+
candidateCount: candidates.selected.length,
|
|
150090
|
+
totalCandidateCount: candidates.total,
|
|
150091
|
+
fileCount: params.files.length,
|
|
150092
|
+
baseSha: params.baseSha,
|
|
150093
|
+
headSha: params.headSha,
|
|
150094
|
+
lookupError: lookup2.error
|
|
150095
|
+
});
|
|
150096
|
+
const path4 = join7(ctx.tmpdir, `pr-${params.pullNumber}-${params.headSha.slice(0, 7)}-impact.md`);
|
|
150097
|
+
writeFileSync6(path4, artifact.content);
|
|
150098
|
+
return {
|
|
150099
|
+
path: path4,
|
|
150100
|
+
candidateCount: candidates.selected.length,
|
|
150101
|
+
renderedAtomCount: artifact.renderedAtomCount,
|
|
150102
|
+
referenceCount: artifact.referenceCount,
|
|
150103
|
+
bytes: Buffer.byteLength(artifact.content)
|
|
150104
|
+
};
|
|
150105
|
+
}
|
|
150106
|
+
|
|
149751
150107
|
// utils/diffCoverage.ts
|
|
149752
150108
|
import { isAbsolute, normalize as normalize2, resolve } from "node:path";
|
|
149753
150109
|
function countLines(params) {
|
|
@@ -149835,27 +150191,6 @@ function getDiffCoverageBreakdown(params) {
|
|
|
149835
150191
|
files
|
|
149836
150192
|
};
|
|
149837
150193
|
}
|
|
149838
|
-
function renderDiffCoverageBreakdown(params) {
|
|
149839
|
-
const breakdown = params.breakdown;
|
|
149840
|
-
const lines = [];
|
|
149841
|
-
lines.push(`diff coverage report for \`${params.diffPath}\``);
|
|
149842
|
-
lines.push(
|
|
149843
|
-
`overall: ${breakdown.coveredLines}/${breakdown.totalLines} lines read (${breakdown.coveragePercent}%), unread: ${breakdown.unreadLines}`
|
|
149844
|
-
);
|
|
149845
|
-
lines.push(`covered ranges: ${formatRanges({ ranges: breakdown.coveredRanges })}`);
|
|
149846
|
-
lines.push(`unread ranges: ${formatRanges({ ranges: breakdown.unreadRanges })}`);
|
|
149847
|
-
lines.push("");
|
|
149848
|
-
lines.push("per-file TOC coverage:");
|
|
149849
|
-
for (const file2 of breakdown.files) {
|
|
149850
|
-
const filePercent = file2.totalLines ? Number((file2.coveredLines / file2.totalLines * 100).toFixed(2)) : 100;
|
|
149851
|
-
lines.push(
|
|
149852
|
-
`- ${file2.filename} (toc lines ${file2.startLine}-${file2.endLine}): ${file2.coveredLines}/${file2.totalLines} lines read (${filePercent}%)`
|
|
149853
|
-
);
|
|
149854
|
-
lines.push(` read: ${formatRanges({ ranges: file2.coveredRanges })}`);
|
|
149855
|
-
lines.push(` unread: ${formatRanges({ ranges: file2.unreadRanges })}`);
|
|
149856
|
-
}
|
|
149857
|
-
return lines.join("\n");
|
|
149858
|
-
}
|
|
149859
150194
|
function resolveOffsetBase(params) {
|
|
149860
150195
|
const lower2 = params.toolName.toLowerCase();
|
|
149861
150196
|
if (lower2 === "readfile" || lower2.endsWith(".readfile")) {
|
|
@@ -149986,10 +150321,6 @@ function countLinesInRanges(params) {
|
|
|
149986
150321
|
}
|
|
149987
150322
|
return total;
|
|
149988
150323
|
}
|
|
149989
|
-
function formatRanges(params) {
|
|
149990
|
-
if (params.ranges.length === 0) return "none";
|
|
149991
|
-
return params.ranges.map((range2) => `${range2.startLine}-${range2.endLine}`).join(", ");
|
|
149992
|
-
}
|
|
149993
150324
|
function clampLine(params) {
|
|
149994
150325
|
if (params.value < 1) return 1;
|
|
149995
150326
|
if (params.value > params.totalLines) return params.totalLines;
|
|
@@ -150016,7 +150347,7 @@ function readNumber(params) {
|
|
|
150016
150347
|
import { execSync } from "node:child_process";
|
|
150017
150348
|
import { createHash } from "node:crypto";
|
|
150018
150349
|
import { readFileSync as readFileSync2, realpathSync, unlinkSync } from "node:fs";
|
|
150019
|
-
import { join as
|
|
150350
|
+
import { join as join8 } from "node:path";
|
|
150020
150351
|
|
|
150021
150352
|
// utils/shell.ts
|
|
150022
150353
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
@@ -150094,7 +150425,7 @@ function resolveHooksDir(cwd, gitPath) {
|
|
|
150094
150425
|
cwd,
|
|
150095
150426
|
log: false
|
|
150096
150427
|
}).trim();
|
|
150097
|
-
const hooksDir =
|
|
150428
|
+
const hooksDir = join8(commonDir, "hooks");
|
|
150098
150429
|
hooksDirCache.set(cwd, hooksDir);
|
|
150099
150430
|
return hooksDir;
|
|
150100
150431
|
}
|
|
@@ -151242,13 +151573,13 @@ function _op(fn2, options) {
|
|
|
151242
151573
|
|
|
151243
151574
|
// mcp/git.ts
|
|
151244
151575
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
151245
|
-
import { writeFileSync as
|
|
151246
|
-
import { join as
|
|
151576
|
+
import { writeFileSync as writeFileSync7 } from "node:fs";
|
|
151577
|
+
import { join as join11 } from "node:path";
|
|
151247
151578
|
|
|
151248
151579
|
// utils/apiCommit.ts
|
|
151249
151580
|
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
151250
151581
|
import { lstat, readlink } from "node:fs/promises";
|
|
151251
|
-
import { join as
|
|
151582
|
+
import { join as join9 } from "node:path";
|
|
151252
151583
|
var GITHUB_API = "https://api.github.com";
|
|
151253
151584
|
var MAX_BLOB_BYTES = 30 * 1024 * 1024;
|
|
151254
151585
|
var BLOB_UPLOAD_CONCURRENCY = 8;
|
|
@@ -151313,7 +151644,7 @@ async function assertApiCommittable(files) {
|
|
|
151313
151644
|
const root = getRepoRoot();
|
|
151314
151645
|
const attrs = $(
|
|
151315
151646
|
"git",
|
|
151316
|
-
["check-attr", "filter", "-z", "--", ...present.map((p) =>
|
|
151647
|
+
["check-attr", "filter", "-z", "--", ...present.map((p) => join9(root, p))],
|
|
151317
151648
|
{ log: false }
|
|
151318
151649
|
);
|
|
151319
151650
|
const parts = attrs.split("\0");
|
|
@@ -151325,7 +151656,7 @@ async function assertApiCommittable(files) {
|
|
|
151325
151656
|
}
|
|
151326
151657
|
}
|
|
151327
151658
|
for (const path4 of present) {
|
|
151328
|
-
const stat = await lstat(
|
|
151659
|
+
const stat = await lstat(join9(root, path4));
|
|
151329
151660
|
if (stat.isDirectory()) {
|
|
151330
151661
|
throw new Error(
|
|
151331
151662
|
`'${path4}' is a directory (nested repository or submodule?) \u2014 signed commits only support files and symlinks.`
|
|
@@ -151334,7 +151665,7 @@ async function assertApiCommittable(files) {
|
|
|
151334
151665
|
}
|
|
151335
151666
|
}
|
|
151336
151667
|
async function createBlobEntry(params) {
|
|
151337
|
-
const absPath =
|
|
151668
|
+
const absPath = join9(params.repoRoot, params.path);
|
|
151338
151669
|
const stat = await lstat(absPath);
|
|
151339
151670
|
if (stat.size > MAX_BLOB_BYTES) {
|
|
151340
151671
|
throw new Error(
|
|
@@ -151514,7 +151845,7 @@ this usually means your local branch has commits that were never pushed \u2014 s
|
|
|
151514
151845
|
var core3 = __toESM(require_core(), 1);
|
|
151515
151846
|
import { createSign } from "node:crypto";
|
|
151516
151847
|
import { rename, writeFile } from "node:fs/promises";
|
|
151517
|
-
import { dirname as dirname3, join as
|
|
151848
|
+
import { dirname as dirname3, join as join10 } from "node:path";
|
|
151518
151849
|
|
|
151519
151850
|
// node_modules/.pnpm/@octokit+plugin-throttling@11.0.3_@octokit+core@7.0.5/node_modules/@octokit/plugin-throttling/dist-bundle/index.js
|
|
151520
151851
|
var import_light = __toESM(require_light(), 1);
|
|
@@ -155458,7 +155789,7 @@ function getGitHubUsageSummary() {
|
|
|
155458
155789
|
}
|
|
155459
155790
|
async function writeGitHubUsageSummaryToFile(path4) {
|
|
155460
155791
|
const summary2 = getGitHubUsageSummary();
|
|
155461
|
-
const tmpPath =
|
|
155792
|
+
const tmpPath = join10(dirname3(path4), `.usage-summary-${process.pid}.tmp`);
|
|
155462
155793
|
await writeFile(tmpPath, JSON.stringify(summary2));
|
|
155463
155794
|
await rename(tmpPath, path4);
|
|
155464
155795
|
}
|
|
@@ -156585,8 +156916,8 @@ function countAhead(head, base, cwd) {
|
|
|
156585
156916
|
function spillGitOutput(params) {
|
|
156586
156917
|
const tempDir = process.env.PULLFROG_TEMP_DIR;
|
|
156587
156918
|
if (!tempDir) throw new Error("PULLFROG_TEMP_DIR not set");
|
|
156588
|
-
const outputPath =
|
|
156589
|
-
|
|
156919
|
+
const outputPath = join11(tempDir, `git-${params.command}-${randomUUID3().slice(0, 8)}.txt`);
|
|
156920
|
+
writeFileSync7(outputPath, params.output);
|
|
156590
156921
|
const previewByLines = params.output.split("\n").slice(0, OVERFLOW_PREVIEW_LINES).join("\n");
|
|
156591
156922
|
const preview = previewByLines.length <= OVERFLOW_PREVIEW_MAX_CHARS ? previewByLines : `${previewByLines.slice(0, OVERFLOW_PREVIEW_MAX_CHARS)}\u2026`;
|
|
156592
156923
|
log.info(
|
|
@@ -157218,13 +157549,15 @@ function addFooter(ctx, body) {
|
|
|
157218
157549
|
var Comment = type({
|
|
157219
157550
|
issueNumber: type.number.describe("the issue number to comment on"),
|
|
157220
157551
|
body: type.string.describe("the comment body content"),
|
|
157221
|
-
type: type.enumerated("Plan", "Comment").describe(
|
|
157552
|
+
type: type.enumerated("Plan", "Comment").describe(
|
|
157553
|
+
"Plan: standalone plan comment on another target. Comment: regular comment (default)."
|
|
157554
|
+
).optional()
|
|
157222
157555
|
});
|
|
157223
157556
|
function CreateCommentTool(ctx) {
|
|
157224
157557
|
return tool({
|
|
157225
157558
|
name: "create_issue_comment",
|
|
157226
157559
|
mutates: true,
|
|
157227
|
-
description:
|
|
157560
|
+
description: "Create a comment on a GitHub issue or PR. Example: `create_issue_comment({ issueNumber: 1234, body: \"Thanks for the report.\" })`. For the current run's answer, progress, or plan use report_progress instead. Use this on the current target only when the task explicitly requests a standalone comment. Skip report_progress only when that current-target comment is the task's sole requested deliverable.",
|
|
157228
157561
|
parameters: Comment,
|
|
157229
157562
|
execute: execute(async ({ issueNumber, body, type: commentType }) => {
|
|
157230
157563
|
const bodyWithFooter = addFooter(ctx, body);
|
|
@@ -157236,9 +157569,6 @@ function CreateCommentTool(ctx) {
|
|
|
157236
157569
|
});
|
|
157237
157570
|
ctx.toolState.wasUpdated = true;
|
|
157238
157571
|
log.info(`\xBB created comment ${result.data.id}`);
|
|
157239
|
-
if (commentType !== "Plan" && issueNumber === ctx.payload.event.issue_number) {
|
|
157240
|
-
ctx.toolState.answerCommentPosted = true;
|
|
157241
|
-
}
|
|
157242
157572
|
if (commentType === "Plan") {
|
|
157243
157573
|
if (result.data.node_id) {
|
|
157244
157574
|
await patchWorkflowRunFields(ctx, { planCommentNodeId: result.data.node_id });
|
|
@@ -158044,10 +158374,6 @@ function runDiffCoveragePreflight(params) {
|
|
|
158044
158374
|
unread.push({ path: file2.filename, ranges: rangesText, unreadLines: fileUnreadLines });
|
|
158045
158375
|
unreadLines += fileUnreadLines;
|
|
158046
158376
|
}
|
|
158047
|
-
coverageState.lastBreakdown = renderDiffCoverageBreakdown({
|
|
158048
|
-
diffPath: coverageState.diffPath,
|
|
158049
|
-
breakdown
|
|
158050
|
-
});
|
|
158051
158377
|
log.debug(
|
|
158052
158378
|
`diff coverage pre-flight breakdown: coveredLines=${breakdown.coveredLines}, unreadLines=${unreadLines}`
|
|
158053
158379
|
);
|
|
@@ -158063,9 +158389,7 @@ function runDiffCoveragePreflight(params) {
|
|
|
158063
158389
|
`diff coverage pre-flight: some TOC regions were not read before review submission. this is a one-time nudge \u2014 read the ranges below from ${coverageState.diffPath} on a best-effort basis, then call create_pull_request_review again. you are NOT obligated to read generated artifacts (lockfiles like pnpm-lock.yaml / package-lock.json / yarn.lock / Cargo.lock; codegen output like *.gen.*, *.pb.go, *.generated.*; snapshot/fixture dirs like __snapshots__/; migration metadata like drizzle/meta/, prisma migration SQL). if every unread region is generated, retry immediately without reading. this pre-flight will not block again in this review session.
|
|
158064
158390
|
|
|
158065
158391
|
unread TOC regions:
|
|
158066
|
-
${unreadText}
|
|
158067
|
-
|
|
158068
|
-
${coverageState.lastBreakdown}`
|
|
158392
|
+
${unreadText}`
|
|
158069
158393
|
);
|
|
158070
158394
|
}
|
|
158071
158395
|
async function clearStrandedPendingReview(ctx, params) {
|
|
@@ -158187,9 +158511,9 @@ async function reportReviewNodeId(ctx, params) {
|
|
|
158187
158511
|
import { execFileSync as execFileSync7, execSync as execSync2 } from "node:child_process";
|
|
158188
158512
|
import { mkdtempSync, readdirSync, realpathSync as realpathSync2, unlinkSync as unlinkSync2 } from "node:fs";
|
|
158189
158513
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
158190
|
-
import { join as
|
|
158514
|
+
import { join as join12 } from "node:path";
|
|
158191
158515
|
function createTempDirectory() {
|
|
158192
|
-
const sharedTempDir = mkdtempSync(
|
|
158516
|
+
const sharedTempDir = mkdtempSync(join12(tmpdir2(), "pullfrog-"));
|
|
158193
158517
|
process.env.PULLFROG_TEMP_DIR = sharedTempDir;
|
|
158194
158518
|
log.info(`\xBB created temp dir at ${sharedTempDir}`);
|
|
158195
158519
|
return sharedTempDir;
|
|
@@ -158234,13 +158558,13 @@ function wipeRunnerLeakSurface() {
|
|
|
158234
158558
|
return [];
|
|
158235
158559
|
}
|
|
158236
158560
|
};
|
|
158237
|
-
const fileCommandsDir =
|
|
158561
|
+
const fileCommandsDir = join12(runnerTemp, "_runner_file_commands");
|
|
158238
158562
|
for (const entry of listDir(fileCommandsDir)) {
|
|
158239
|
-
tryUnlink(
|
|
158563
|
+
tryUnlink(join12(fileCommandsDir, entry));
|
|
158240
158564
|
}
|
|
158241
158565
|
for (const entry of listDir(runnerTemp)) {
|
|
158242
158566
|
if (entry.endsWith(".sh") || /^git-credentials-.*\.config$/.test(entry)) {
|
|
158243
|
-
tryUnlink(
|
|
158567
|
+
tryUnlink(join12(runnerTemp, entry));
|
|
158244
158568
|
}
|
|
158245
158569
|
}
|
|
158246
158570
|
if (wiped.length > 0) {
|
|
@@ -158746,21 +159070,23 @@ function CheckoutPrTool(ctx) {
|
|
|
158746
159070
|
"PULLFROG_TEMP_DIR not set - checkout_pr must run in pullfrog action context"
|
|
158747
159071
|
);
|
|
158748
159072
|
}
|
|
158749
|
-
const
|
|
159073
|
+
const checkoutSha = primary.checkoutSha;
|
|
159074
|
+
if (!checkoutSha) throw new Error("checkout completed without a resolved HEAD SHA");
|
|
159075
|
+
const headShort = checkoutSha.slice(0, 7);
|
|
158750
159076
|
let incrementalDiffPath;
|
|
158751
|
-
if (primary.beforeSha
|
|
159077
|
+
if (primary.beforeSha) {
|
|
158752
159078
|
const beforeShort = primary.beforeSha.slice(0, 7);
|
|
158753
159079
|
const incremental = computeIncrementalDiff({
|
|
158754
159080
|
baseBranch: pr.baseRef,
|
|
158755
159081
|
beforeSha: primary.beforeSha,
|
|
158756
|
-
headSha:
|
|
159082
|
+
headSha: checkoutSha
|
|
158757
159083
|
});
|
|
158758
159084
|
if (incremental) {
|
|
158759
|
-
incrementalDiffPath =
|
|
159085
|
+
incrementalDiffPath = join13(
|
|
158760
159086
|
tempDir,
|
|
158761
159087
|
`pr-${pull_number}-${beforeShort}-${headShort}-incremental.diff`
|
|
158762
159088
|
);
|
|
158763
|
-
|
|
159089
|
+
writeFileSync8(incrementalDiffPath, incremental);
|
|
158764
159090
|
log.info(
|
|
158765
159091
|
`\xBB incremental diff computed (${incremental.length} bytes) \u2192 ${incrementalDiffPath}`
|
|
158766
159092
|
);
|
|
@@ -158770,8 +159096,8 @@ function CheckoutPrTool(ctx) {
|
|
|
158770
159096
|
const diffPreview = formatResult.content.split("\n").slice(0, 100).join("\n");
|
|
158771
159097
|
log.debug(`formatted diff preview (first 100 lines):
|
|
158772
159098
|
${diffPreview}`);
|
|
158773
|
-
const diffPath =
|
|
158774
|
-
|
|
159099
|
+
const diffPath = join13(tempDir, `pr-${pull_number}-${headShort}.diff`);
|
|
159100
|
+
writeFileSync8(diffPath, formatResult.content);
|
|
158775
159101
|
log.debug(`wrote diff to ${diffPath} (${formatResult.content.length} bytes)`);
|
|
158776
159102
|
primary.diffCoverage = createDiffCoverageState({
|
|
158777
159103
|
diffPath,
|
|
@@ -158782,14 +159108,56 @@ ${diffPreview}`);
|
|
|
158782
159108
|
log.debug(
|
|
158783
159109
|
`\xBB diff coverage initialized: diffPath=${diffPath}, totalLines=${primary.diffCoverage.totalLines}, tocEntries=${primary.diffCoverage.tocEntries.length}`
|
|
158784
159110
|
);
|
|
159111
|
+
let impactPath;
|
|
159112
|
+
if (process.env.PULLFROG_DISABLE_CHANGE_IMPACT === "1") {
|
|
159113
|
+
log.info("\xBB change impact disabled by PULLFROG_DISABLE_CHANGE_IMPACT");
|
|
159114
|
+
} else {
|
|
159115
|
+
let revisionVerified = false;
|
|
159116
|
+
let revisionFailure;
|
|
159117
|
+
try {
|
|
159118
|
+
const latest = await ctx.octokit.rest.pulls.get({
|
|
159119
|
+
owner: ctx.repo.owner,
|
|
159120
|
+
repo: ctx.repo.name,
|
|
159121
|
+
pull_number
|
|
159122
|
+
});
|
|
159123
|
+
revisionVerified = latest.data.head.sha === checkoutSha && latest.data.base.sha === prResponse.data.base.sha;
|
|
159124
|
+
} catch (error49) {
|
|
159125
|
+
revisionFailure = error49 instanceof Error ? error49.message : String(error49);
|
|
159126
|
+
}
|
|
159127
|
+
if (revisionFailure !== void 0) {
|
|
159128
|
+
log.warning(
|
|
159129
|
+
`\xBB change impact skipped: PR revision could not be verified (${revisionFailure})`
|
|
159130
|
+
);
|
|
159131
|
+
} else if (!revisionVerified) {
|
|
159132
|
+
log.warning("\xBB change impact skipped: PR revision changed while the diff was fetched");
|
|
159133
|
+
} else {
|
|
159134
|
+
try {
|
|
159135
|
+
const impact = await createChangeImpactArtifact(ctx, {
|
|
159136
|
+
files: formatResult.files,
|
|
159137
|
+
pullNumber: pull_number,
|
|
159138
|
+
baseSha: prResponse.data.base.sha,
|
|
159139
|
+
headSha: checkoutSha
|
|
159140
|
+
});
|
|
159141
|
+
impactPath = impact.path;
|
|
159142
|
+
log.info(
|
|
159143
|
+
`\xBB change impact: ${impact.candidateCount} candidate atom(s), ${impact.renderedAtomCount} detailed atom(s), ${impact.referenceCount} tracked reference(s), ${impact.bytes} bytes \u2192 ${impact.path}`
|
|
159144
|
+
);
|
|
159145
|
+
} catch (error49) {
|
|
159146
|
+
log.warning(
|
|
159147
|
+
`\xBB change impact skipped: generation failed (${error49 instanceof Error ? error49.message : String(error49)})`
|
|
159148
|
+
);
|
|
159149
|
+
}
|
|
159150
|
+
}
|
|
159151
|
+
}
|
|
158785
159152
|
const cached4 = /* @__PURE__ */ new Map();
|
|
158786
159153
|
for (const file2 of formatResult.files) {
|
|
158787
159154
|
cached4.set(file2.filename, commentableLinesForFile(file2.patch));
|
|
158788
159155
|
}
|
|
158789
159156
|
primary.commentableLinesByFile = cached4;
|
|
158790
159157
|
primary.commentableLinesPullNumber = pull_number;
|
|
158791
|
-
primary.commentableLinesCheckoutSha =
|
|
158792
|
-
const incrementalInstructions = incrementalDiffPath ? `
|
|
159158
|
+
primary.commentableLinesCheckoutSha = checkoutSha;
|
|
159159
|
+
const incrementalInstructions = incrementalDiffPath ? `IMPORTANT: incrementalDiffPath contains ONLY the changes since the last reviewed version (computed via range-diff). you MUST read incrementalDiffPath FIRST to understand what changed, then use diffPath for full PR context. do NOT skip the incremental diff. ` : "";
|
|
159160
|
+
const impactInstructions = impactPath ? ` impactPath contains bounded, deterministic reference leads for semantic atoms changed by the PR. read the authoritative diff TOC and relevant raw diff lines FIRST; only then use impactPath as a supplemental, explicitly incomplete lead list. impactPath never establishes review coverage and an empty artifact is not evidence of no wider impact.` : "";
|
|
158793
159161
|
const COMMIT_LOG_MAX = 200;
|
|
158794
159162
|
const baseRange = `origin/${pr.baseRef}..HEAD`;
|
|
158795
159163
|
let commitCount = 0;
|
|
@@ -158825,6 +159193,7 @@ ${diffPreview}`);
|
|
|
158825
159193
|
url: prResponse.data.html_url,
|
|
158826
159194
|
headRepo: pr.headRepoFullName,
|
|
158827
159195
|
diffPath,
|
|
159196
|
+
impactPath,
|
|
158828
159197
|
incrementalDiffPath,
|
|
158829
159198
|
toc: formatResult.toc,
|
|
158830
159199
|
commitCount,
|
|
@@ -158832,7 +159201,7 @@ ${diffPreview}`);
|
|
|
158832
159201
|
commitLogTruncated,
|
|
158833
159202
|
commitLogUnavailable,
|
|
158834
159203
|
hookWarning: checkoutResult.hookWarning,
|
|
158835
|
-
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.` +
|
|
159204
|
+
instructions: incrementalInstructions + `the diff file at diffPath contains a table of contents (TOC) at the top listing every changed file with its line range. use the TOC line ranges as your checklist and read specific files from the diff instead of reading the entire file. for example, if the TOC says "src/foo.ts \u2192 lines 5-42", read lines 5-42 from diffPath to see that file's changes. review files selectively based on relevance rather than reading everything sequentially. to inspect the PR's changed files, use diffPath \u2014 do NOT run \`git diff\` to re-derive what's already in diffPath. the formatted diff with line numbers is authoritative. if you ever do need a branch-vs-base diff via the git tool, use \`git diff --merge-base <base>\` (single call, includes uncommitted edits) or three-dot \`git diff <base>...HEAD\` (committed-only). bare \`<base>\` and two-dot \`<base>..HEAD\` are symmetric and pull in the inverse of every commit landed on \`<base>\` since the branch forked \u2014 the git tool will reject those forms when the divergence is detected. \`$(...)\` subshells are NOT expanded by the git tool. \`git log\` and \`git diff --stat\` are fine for commit-range overview, and \`git diff\` / \`git diff --cached\` are fine for inspecting *your own* uncommitted changes \u2014 but PR review content MUST come from diffPath. before your review is submitted, a one-time coverage pre-flight may error listing unread TOC regions. retry the same create_pull_request_review call to proceed \u2014 optionally after reading the listed ranges. the pre-flight will not block again this session. the local branch is 'localBranch' (pr-{number}), not the remote branch name. when pushing, omit branchName to use the current branch. do not use remoteBranch as a local branch name.` + impactInstructions + hookWarningInstructions + commitLogInstructions
|
|
158836
159205
|
};
|
|
158837
159206
|
};
|
|
158838
159207
|
return tool({
|
|
@@ -158879,8 +159248,8 @@ ${dirty}`
|
|
|
158879
159248
|
}
|
|
158880
159249
|
|
|
158881
159250
|
// mcp/checkSuite.ts
|
|
158882
|
-
import { mkdirSync as mkdirSync7, writeFileSync as
|
|
158883
|
-
import { join as
|
|
159251
|
+
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync9 } from "node:fs";
|
|
159252
|
+
import { join as join14 } from "node:path";
|
|
158884
159253
|
var GetCheckSuiteLogs = type({
|
|
158885
159254
|
check_suite_id: type.number.describe("the id from check_suite.id")
|
|
158886
159255
|
});
|
|
@@ -158976,7 +159345,7 @@ function GetCheckSuiteLogsTool(ctx) {
|
|
|
158976
159345
|
if (!tempDir) {
|
|
158977
159346
|
throw new Error("PULLFROG_TEMP_DIR not set");
|
|
158978
159347
|
}
|
|
158979
|
-
const logsDir =
|
|
159348
|
+
const logsDir = join14(tempDir, "ci-logs");
|
|
158980
159349
|
mkdirSync7(logsDir, { recursive: true });
|
|
158981
159350
|
const jobResults = [];
|
|
158982
159351
|
for (const run of failedRuns) {
|
|
@@ -159003,8 +159372,8 @@ function GetCheckSuiteLogsTool(ctx) {
|
|
|
159003
159372
|
);
|
|
159004
159373
|
}
|
|
159005
159374
|
const logsText = await logsResult.text();
|
|
159006
|
-
const logPath =
|
|
159007
|
-
|
|
159375
|
+
const logPath = join14(logsDir, `job-${job.id}.log`);
|
|
159376
|
+
writeFileSync9(logPath, logsText);
|
|
159008
159377
|
const analysis = analyzeLog(logsText, 80);
|
|
159009
159378
|
const failedSteps = job.steps?.filter((s) => s.conclusion === "failure").map((s) => `Step ${s.number}: ${s.name}`) ?? [];
|
|
159010
159379
|
jobResults.push({
|
|
@@ -159052,8 +159421,8 @@ function GetCheckSuiteLogsTool(ctx) {
|
|
|
159052
159421
|
}
|
|
159053
159422
|
|
|
159054
159423
|
// mcp/commitInfo.ts
|
|
159055
|
-
import { writeFileSync as
|
|
159056
|
-
import { join as
|
|
159424
|
+
import { writeFileSync as writeFileSync10 } from "node:fs";
|
|
159425
|
+
import { join as join15 } from "node:path";
|
|
159057
159426
|
var CommitInfo = type({
|
|
159058
159427
|
sha: type.string.describe("the commit SHA (full or abbreviated) to fetch")
|
|
159059
159428
|
});
|
|
@@ -159077,8 +159446,8 @@ function CommitInfoTool(ctx) {
|
|
|
159077
159446
|
"PULLFROG_TEMP_DIR not set - get_commit_info must run in pullfrog action context"
|
|
159078
159447
|
);
|
|
159079
159448
|
}
|
|
159080
|
-
const diffFile =
|
|
159081
|
-
|
|
159449
|
+
const diffFile = join15(tempDir, `commit-${sha.slice(0, 7)}.diff`);
|
|
159450
|
+
writeFileSync10(diffFile, formatResult.content);
|
|
159082
159451
|
log.debug(`wrote commit diff to ${diffFile} (${formatResult.content.length} bytes)`);
|
|
159083
159452
|
return {
|
|
159084
159453
|
sha: data.sha,
|
|
@@ -159105,7 +159474,7 @@ import { performance as performance8 } from "node:perf_hooks";
|
|
|
159105
159474
|
|
|
159106
159475
|
// prep/installNodeDependencies.ts
|
|
159107
159476
|
import { existsSync as existsSync6 } from "node:fs";
|
|
159108
|
-
import { join as
|
|
159477
|
+
import { join as join17 } from "node:path";
|
|
159109
159478
|
|
|
159110
159479
|
// node_modules/.pnpm/package-manager-detector@1.6.0/node_modules/package-manager-detector/dist/commands.mjs
|
|
159111
159480
|
function dashDashArg(agent2, agentCommand) {
|
|
@@ -159404,7 +159773,7 @@ function isMetadataYarnClassic(metadataPath) {
|
|
|
159404
159773
|
var import_semver3 = __toESM(require_semver2(), 1);
|
|
159405
159774
|
import { existsSync as existsSync5 } from "node:fs";
|
|
159406
159775
|
import { mkdir, readFile as readFile2 } from "node:fs/promises";
|
|
159407
|
-
import { delimiter, join as
|
|
159776
|
+
import { delimiter, join as join16 } from "node:path";
|
|
159408
159777
|
var SUPPORTED_NAMES = ["npm", "pnpm", "yarn", "bun"];
|
|
159409
159778
|
var COREPACK_MANAGED = ["pnpm", "yarn"];
|
|
159410
159779
|
function isSupported(name) {
|
|
@@ -159442,7 +159811,7 @@ function parseDevEnginesField(field) {
|
|
|
159442
159811
|
};
|
|
159443
159812
|
}
|
|
159444
159813
|
async function resolvePackageManagerSpec(cwd) {
|
|
159445
|
-
const pkgPath =
|
|
159814
|
+
const pkgPath = join16(cwd, "package.json");
|
|
159446
159815
|
if (!existsSync5(pkgPath)) return null;
|
|
159447
159816
|
let pkg;
|
|
159448
159817
|
try {
|
|
@@ -159505,7 +159874,7 @@ async function currentVersion(name) {
|
|
|
159505
159874
|
return result.stdout.trim();
|
|
159506
159875
|
}
|
|
159507
159876
|
function packageManagerBinDir(tmpdir3) {
|
|
159508
|
-
return
|
|
159877
|
+
return join16(tmpdir3, "pm-bin");
|
|
159509
159878
|
}
|
|
159510
159879
|
async function ensurePackageManager(params) {
|
|
159511
159880
|
const spec = params.spec;
|
|
@@ -159576,7 +159945,7 @@ async function installFallback(name, installSpec) {
|
|
|
159576
159945
|
return result.stderr || `failed to install ${name}`;
|
|
159577
159946
|
}
|
|
159578
159947
|
if (name === "deno") {
|
|
159579
|
-
const denoPath =
|
|
159948
|
+
const denoPath = join17(process.env.HOME || "", ".deno", "bin");
|
|
159580
159949
|
process.env.PATH = `${denoPath}:${process.env.PATH}`;
|
|
159581
159950
|
}
|
|
159582
159951
|
log.info(`\xBB installed ${name}`);
|
|
@@ -159585,7 +159954,7 @@ async function installFallback(name, installSpec) {
|
|
|
159585
159954
|
var installNodeDependencies = {
|
|
159586
159955
|
name: "installNodeDependencies",
|
|
159587
159956
|
shouldRun: () => {
|
|
159588
|
-
const packageJsonPath =
|
|
159957
|
+
const packageJsonPath = join17(process.cwd(), "package.json");
|
|
159589
159958
|
return existsSync6(packageJsonPath);
|
|
159590
159959
|
},
|
|
159591
159960
|
run: async (options) => {
|
|
@@ -159684,12 +160053,12 @@ ${errorMessage}`]
|
|
|
159684
160053
|
|
|
159685
160054
|
// prep/installPythonDependencies.ts
|
|
159686
160055
|
import { existsSync as existsSync7, readFileSync as readFileSync4 } from "node:fs";
|
|
159687
|
-
import { join as
|
|
160056
|
+
import { join as join18 } from "node:path";
|
|
159688
160057
|
function declaresBuildSystem(path4) {
|
|
159689
160058
|
return /^\s*\[\s*build-system\s*\]/m.test(readFileSync4(path4, "utf8"));
|
|
159690
160059
|
}
|
|
159691
160060
|
function configApplies(config3, cwd) {
|
|
159692
|
-
const path4 =
|
|
160061
|
+
const path4 = join18(cwd, config3.file);
|
|
159693
160062
|
return existsSync7(path4) && (config3.applies?.(path4) ?? true);
|
|
159694
160063
|
}
|
|
159695
160064
|
var PYTHON_CONFIGS = [
|
|
@@ -160825,8 +161194,8 @@ function PullRequestInfoTool(ctx) {
|
|
|
160825
161194
|
}
|
|
160826
161195
|
|
|
160827
161196
|
// mcp/reviewComments.ts
|
|
160828
|
-
import { writeFileSync as
|
|
160829
|
-
import { join as
|
|
161197
|
+
import { writeFileSync as writeFileSync11 } from "node:fs";
|
|
161198
|
+
import { join as join19 } from "node:path";
|
|
160830
161199
|
var REVIEW_THREADS_QUERY = `
|
|
160831
161200
|
query ($owner: String!, $name: String!, $prNumber: Int!) {
|
|
160832
161201
|
repository(owner: $owner, name: $name) {
|
|
@@ -161242,8 +161611,8 @@ function GetReviewCommentsTool(ctx) {
|
|
|
161242
161611
|
throw new Error("PULLFROG_TEMP_DIR not set");
|
|
161243
161612
|
}
|
|
161244
161613
|
const filename = `review-${params.review_id}-threads.md`;
|
|
161245
|
-
const commentsPath =
|
|
161246
|
-
|
|
161614
|
+
const commentsPath = join19(tempDir, filename);
|
|
161615
|
+
writeFileSync11(commentsPath, formatted.content);
|
|
161247
161616
|
log.debug(`wrote ${threadBlocks.length} threads to ${commentsPath}`);
|
|
161248
161617
|
return {
|
|
161249
161618
|
review_id: params.review_id,
|
|
@@ -161478,11 +161847,11 @@ ${summaryAddendum}`,
|
|
|
161478
161847
|
}
|
|
161479
161848
|
|
|
161480
161849
|
// mcp/shell.ts
|
|
161481
|
-
import { spawn as
|
|
161850
|
+
import { spawn as spawn3, spawnSync as spawnSync5 } from "node:child_process";
|
|
161482
161851
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
161483
|
-
import { closeSync, openSync, writeFileSync as
|
|
161852
|
+
import { closeSync, openSync, writeFileSync as writeFileSync12 } from "node:fs";
|
|
161484
161853
|
import { userInfo as userInfo2 } from "node:os";
|
|
161485
|
-
import { join as
|
|
161854
|
+
import { join as join20 } from "node:path";
|
|
161486
161855
|
import { setTimeout as sleep2 } from "node:timers/promises";
|
|
161487
161856
|
var ShellParams = type({
|
|
161488
161857
|
command: "string",
|
|
@@ -161580,7 +161949,7 @@ function spawnShell(params) {
|
|
|
161580
161949
|
const repoRoot = resolveRepoRoot();
|
|
161581
161950
|
const fsMounts = buildFsMounts(repoRoot);
|
|
161582
161951
|
if (sandboxMethod === "unshare") {
|
|
161583
|
-
return
|
|
161952
|
+
return spawn3(
|
|
161584
161953
|
"unshare",
|
|
161585
161954
|
[
|
|
161586
161955
|
"--pid",
|
|
@@ -161604,7 +161973,7 @@ function spawnShell(params) {
|
|
|
161604
161973
|
const pathRestore = 'export PATH="${SANDBOX_PATH:-$PATH}"; ';
|
|
161605
161974
|
const escaped = (pathRestore + params.command).replace(/'/g, "'\\''");
|
|
161606
161975
|
envArgs.push(`SANDBOX_PATH=${params.env.PATH ?? ""}`);
|
|
161607
|
-
return
|
|
161976
|
+
return spawn3(
|
|
161608
161977
|
"sudo",
|
|
161609
161978
|
[
|
|
161610
161979
|
"env",
|
|
@@ -161620,7 +161989,7 @@ function spawnShell(params) {
|
|
|
161620
161989
|
{ ...spawnOpts, env: {} }
|
|
161621
161990
|
);
|
|
161622
161991
|
}
|
|
161623
|
-
return
|
|
161992
|
+
return spawn3("bash", ["-c", params.command], spawnOpts);
|
|
161624
161993
|
}
|
|
161625
161994
|
async function killProcessGroup(proc) {
|
|
161626
161995
|
if (!proc.pid) return;
|
|
@@ -161645,8 +162014,8 @@ function getTempDir() {
|
|
|
161645
162014
|
var MAX_OUTPUT_CHARS = 5e3;
|
|
161646
162015
|
function capOutput(output) {
|
|
161647
162016
|
if (output.length <= MAX_OUTPUT_CHARS) return output;
|
|
161648
|
-
const fullPath =
|
|
161649
|
-
|
|
162017
|
+
const fullPath = join20(getTempDir(), `shell-${randomUUID4().slice(0, 8)}.log`);
|
|
162018
|
+
writeFileSync12(fullPath, output);
|
|
161650
162019
|
const elided = output.length - MAX_OUTPUT_CHARS;
|
|
161651
162020
|
return `... [${elided} chars truncated; full output saved to ${fullPath}] ...
|
|
161652
162021
|
${output.slice(-MAX_OUTPUT_CHARS)}`;
|
|
@@ -161700,8 +162069,8 @@ Do NOT use this tool for git commands \u2014 use the dedicated git tools instead
|
|
|
161700
162069
|
if (params.background) {
|
|
161701
162070
|
const tempDir = getTempDir();
|
|
161702
162071
|
const handle = `bg-${randomUUID4().slice(0, 8)}`;
|
|
161703
|
-
const outputPath =
|
|
161704
|
-
const pidPath =
|
|
162072
|
+
const outputPath = join20(tempDir, `${handle}.log`);
|
|
162073
|
+
const pidPath = join20(tempDir, `${handle}.pid`);
|
|
161705
162074
|
const logFd = openSync(outputPath, "a");
|
|
161706
162075
|
let proc2;
|
|
161707
162076
|
try {
|
|
@@ -161718,7 +162087,7 @@ Do NOT use this tool for git commands \u2014 use the dedicated git tools instead
|
|
|
161718
162087
|
throw new Error("failed to start background process");
|
|
161719
162088
|
}
|
|
161720
162089
|
proc2.unref();
|
|
161721
|
-
|
|
162090
|
+
writeFileSync12(pidPath, `${proc2.pid}
|
|
161722
162091
|
`);
|
|
161723
162092
|
ctx.toolState.backgroundProcesses.set(handle, { pid: proc2.pid, outputPath, pidPath });
|
|
161724
162093
|
return {
|
|
@@ -161868,7 +162237,7 @@ function UploadFileTool(ctx) {
|
|
|
161868
162237
|
|
|
161869
162238
|
// mcp/xrepo.ts
|
|
161870
162239
|
import { mkdirSync as mkdirSync8, rmSync as rmSync2 } from "node:fs";
|
|
161871
|
-
import { join as
|
|
162240
|
+
import { join as join21 } from "node:path";
|
|
161872
162241
|
function assertValidRepoName(name) {
|
|
161873
162242
|
if (!/^[A-Za-z0-9._-]+$/.test(name) || name.startsWith("-") || name.includes("..")) {
|
|
161874
162243
|
throw new Error(
|
|
@@ -161886,6 +162255,7 @@ var ListRepos = type({});
|
|
|
161886
162255
|
function ListReposTool(ctx) {
|
|
161887
162256
|
return tool({
|
|
161888
162257
|
name: "list_repos",
|
|
162258
|
+
annotations: { readOnlyHint: true },
|
|
161889
162259
|
description: "List the repositories available for cross-repo (`--xrepo`) work in this run, with each repo's access tier (primary, write, or read) and whether it's already checked out. Use this before `checkout_repo` to discover what you can reference or edit. Returns an empty set on single-repo runs.",
|
|
161890
162260
|
parameters: ListRepos,
|
|
161891
162261
|
execute: execute(async () => {
|
|
@@ -161947,7 +162317,7 @@ function CheckoutRepoTool(ctx) {
|
|
|
161947
162317
|
return { path: existing.dir, access: existing.access, note: "already checked out." };
|
|
161948
162318
|
}
|
|
161949
162319
|
const access = accessFor(ctx, repo);
|
|
161950
|
-
const dir =
|
|
162320
|
+
const dir = join21(ctx.tmpdir, "xrepo", repo);
|
|
161951
162321
|
mkdirSync8(dir, { recursive: true });
|
|
161952
162322
|
const state = ensureRepoState(ctx.toolState, { owner, name: repo, dir, access });
|
|
161953
162323
|
const rc = resolveRepoCtx(ctx, repo);
|
|
@@ -162397,9 +162767,9 @@ function formatApiKeyErrorSummary(params) {
|
|
|
162397
162767
|
|
|
162398
162768
|
// utils/gitAuthServer.ts
|
|
162399
162769
|
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
162400
|
-
import { writeFileSync as
|
|
162770
|
+
import { writeFileSync as writeFileSync13 } from "node:fs";
|
|
162401
162771
|
import { createServer as createServer3 } from "node:http";
|
|
162402
|
-
import { join as
|
|
162772
|
+
import { join as join22 } from "node:path";
|
|
162403
162773
|
var REVOKED_TRAP_MS = 6e4;
|
|
162404
162774
|
function revokeGitHubToken(token) {
|
|
162405
162775
|
fetch("https://api.github.com/installation/token", {
|
|
@@ -162468,7 +162838,7 @@ async function startGitAuthServer(tmpdir3) {
|
|
|
162468
162838
|
function writeAskpassScript(code) {
|
|
162469
162839
|
const scriptId = randomUUID5();
|
|
162470
162840
|
const scriptName = `askpass-${scriptId}.js`;
|
|
162471
|
-
const scriptPath =
|
|
162841
|
+
const scriptPath = join22(tmpdir3, scriptName);
|
|
162472
162842
|
const content = [
|
|
162473
162843
|
`#!/usr/bin/env node`,
|
|
162474
162844
|
`var a=process.argv[2]||"";`,
|
|
@@ -162481,7 +162851,7 @@ async function startGitAuthServer(tmpdir3) {
|
|
|
162481
162851
|
`r.on("end",function(){process.stdout.write(d+"\\n")})`,
|
|
162482
162852
|
`}).on("error",function(){process.exit(1)})}`
|
|
162483
162853
|
].join("\n");
|
|
162484
|
-
|
|
162854
|
+
writeFileSync13(scriptPath, content, { mode: 448 });
|
|
162485
162855
|
return scriptPath;
|
|
162486
162856
|
}
|
|
162487
162857
|
async function close() {
|
|
@@ -162802,9 +163172,9 @@ When embedding images (e.g. uploaded screenshots) in comments or PR bodies, alwa
|
|
|
162802
163172
|
|
|
162803
163173
|
**Your raw assistant messages are never delivered** \u2014 they exist only in the run logs. Anything the user is meant to see (an answer to a question, a mention reply, a result) MUST go through \`report_progress\` (or another ${pullfrogMcpName} write tool). Do not rely on returning the answer as plain text \u2014 the harness makes a best-effort attempt to recover it into the progress comment, but that is a safety net, not a substitute for calling \`report_progress\`.
|
|
162804
163174
|
|
|
162805
|
-
**\`report_progress\`**: call this exactly once at the end of every run with a brief final summary (1-3 sentences) unless the mode guidance instructs otherwise. Never call it for intermediate status updates (e.g., "Checking for changes...", "Starting review...") \u2014 the task list handles live progress automatically. Calling \`report_progress\` replaces the task list with your summary and preserves the current task list in a collapsible section. Keep the summary concise \u2014 do not repeat what the task list already shows. Focus on the outcome (what was accomplished, links to artifacts) rather than listing individual steps. If something failed, include the tool's error text even when that makes the summary longer.
|
|
163175
|
+
**\`report_progress\`**: call this exactly once at the end of every run with a brief final summary (1-3 sentences) unless the mode guidance instructs otherwise or a standalone comment on the current target is the task's sole requested deliverable. Never call it for intermediate status updates (e.g., "Checking for changes...", "Starting review...") \u2014 the task list handles live progress automatically. Calling \`report_progress\` replaces the task list with your summary and preserves the current task list in a collapsible section. Keep the summary concise \u2014 do not repeat what the task list already shows. Focus on the outcome (what was accomplished, links to artifacts) rather than listing individual steps. If something failed, include the tool's error text even when that makes the summary longer.
|
|
162806
163176
|
|
|
162807
|
-
Never use \`create_issue_comment\` for task progress
|
|
163177
|
+
Never use \`create_issue_comment\` for task progress or for the answer that \`report_progress\` should deliver. Use it only when the task explicitly requests a separate standalone comment or a comment on a different target. Skip \`report_progress\` only when a standalone comment on the current target is the task's sole requested deliverable; successful-run cleanup then removes the progress chrome. Plan output (initial post AND revisions) goes through \`report_progress\` \u2014 see the Plan mode guidance for details.
|
|
162808
163178
|
|
|
162809
163179
|
### If you get stuck
|
|
162810
163180
|
|
|
@@ -162938,7 +163308,7 @@ function resolveInstructions(ctx) {
|
|
|
162938
163308
|
|
|
162939
163309
|
// utils/learnings.ts
|
|
162940
163310
|
import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
|
|
162941
|
-
import { dirname as dirname4, join as
|
|
163311
|
+
import { dirname as dirname4, join as join23 } from "node:path";
|
|
162942
163312
|
|
|
162943
163313
|
// utils/learningsTruncate.ts
|
|
162944
163314
|
var MAX_LEARNINGS_LENGTH = 1e5;
|
|
@@ -162956,10 +163326,10 @@ function truncateAtLineBoundary(body, cap) {
|
|
|
162956
163326
|
var LEARNINGS_FILE_NAME = "pullfrog-learnings.md";
|
|
162957
163327
|
var XREPO_LEARNINGS_FILE_NAME = "pullfrog-xrepo-learnings.md";
|
|
162958
163328
|
function learningsFilePath(tmpdir3) {
|
|
162959
|
-
return
|
|
163329
|
+
return join23(tmpdir3, LEARNINGS_FILE_NAME);
|
|
162960
163330
|
}
|
|
162961
163331
|
function xrepoLearningsFilePath(tmpdir3) {
|
|
162962
|
-
return
|
|
163332
|
+
return join23(tmpdir3, XREPO_LEARNINGS_FILE_NAME);
|
|
162963
163333
|
}
|
|
162964
163334
|
async function seedLearningsFile(params) {
|
|
162965
163335
|
const path4 = learningsFilePath(params.tmpdir);
|
|
@@ -163699,7 +164069,7 @@ async function runProxyResolution(ctx) {
|
|
|
163699
164069
|
|
|
163700
164070
|
// utils/prSummary.ts
|
|
163701
164071
|
import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
|
|
163702
|
-
import { dirname as dirname5, join as
|
|
164072
|
+
import { dirname as dirname5, join as join24 } from "node:path";
|
|
163703
164073
|
var SUMMARY_FILE_NAME = "pullfrog-summary.md";
|
|
163704
164074
|
var SUMMARY_SCAFFOLD = `# PR summary
|
|
163705
164075
|
|
|
@@ -163709,7 +164079,7 @@ var SUMMARY_SCAFFOLD = `# PR summary
|
|
|
163709
164079
|
var MIN_SNAPSHOT_LENGTH = 60;
|
|
163710
164080
|
var MAX_SNAPSHOT_LENGTH = 32768;
|
|
163711
164081
|
function summaryFilePath(tmpdir3) {
|
|
163712
|
-
return
|
|
164082
|
+
return join24(tmpdir3, SUMMARY_FILE_NAME);
|
|
163713
164083
|
}
|
|
163714
164084
|
async function seedSummaryFile(params) {
|
|
163715
164085
|
const path4 = summaryFilePath(params.tmpdir);
|
|
@@ -164426,7 +164796,7 @@ async function finalizeSuccessRun(input) {
|
|
|
164426
164796
|
log.debug(`failure error report failed: ${error49}`);
|
|
164427
164797
|
});
|
|
164428
164798
|
}
|
|
164429
|
-
if (input.result.success && input.toolState.progressComment && input.toolState.wasUpdated &&
|
|
164799
|
+
if (input.result.success && input.toolState.progressComment && input.toolState.wasUpdated && !input.toolState.finalSummaryWritten) {
|
|
164430
164800
|
await deleteProgressComment(input.toolContext).catch((error49) => {
|
|
164431
164801
|
log.debug(`stranded progress comment cleanup failed: ${error49}`);
|
|
164432
164802
|
});
|
|
@@ -164992,7 +165362,7 @@ ${instructions.user}` : null,
|
|
|
164992
165362
|
log.info(instructions.full);
|
|
164993
165363
|
});
|
|
164994
165364
|
if (agentId === "opencode") {
|
|
164995
|
-
const pluginDir =
|
|
165365
|
+
const pluginDir = join25(process.cwd(), ".opencode", "plugin");
|
|
164996
165366
|
const hasPlugins = existsSync8(pluginDir) && readdirSync2(pluginDir).some((f) => /\.[jt]sx?$/.test(f));
|
|
164997
165367
|
if (hasPlugins && toolState.dependencyInstallation?.promise) {
|
|
164998
165368
|
log.info(
|