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