pullfrog 0.1.96 → 0.1.97
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/opencodePlugin.d.ts +4 -2
- package/dist/cli.mjs +714 -526
- package/dist/index.js +718 -539
- package/dist/internal.js +60 -23
- package/dist/mcp/checkout.d.ts +2 -0
- package/dist/utils/apiKeys.d.ts +26 -0
- package/dist/utils/changeImpact.d.ts +1 -0
- package/dist/utils/codexHome.d.ts +4 -6
- package/dist/utils/gitAuth.d.ts +1 -0
- package/dist/utils/oauthWriteback.d.ts +5 -0
- package/dist/utils/openCodeModels.d.ts +19 -2
- package/dist/utils/providerErrors.d.ts +4 -1
- package/dist/utils/runErrorRenderer.d.ts +25 -9
- package/dist/utils/subprocess.d.ts +1 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -98088,14 +98088,14 @@ var require_turndown_cjs = __commonJS({
|
|
|
98088
98088
|
} else if (node2.nodeType === 1) {
|
|
98089
98089
|
replacement = replacementForNode.call(self2, node2);
|
|
98090
98090
|
}
|
|
98091
|
-
return
|
|
98091
|
+
return join29(output, replacement);
|
|
98092
98092
|
}, "");
|
|
98093
98093
|
}
|
|
98094
98094
|
function postProcess(output) {
|
|
98095
98095
|
var self2 = this;
|
|
98096
98096
|
this.rules.forEach(function(rule) {
|
|
98097
98097
|
if (typeof rule.append === "function") {
|
|
98098
|
-
output =
|
|
98098
|
+
output = join29(output, rule.append(self2.options));
|
|
98099
98099
|
}
|
|
98100
98100
|
});
|
|
98101
98101
|
return output.replace(/^[\t\r\n]+/, "").replace(/[\t\r\n\s]+$/, "");
|
|
@@ -98107,7 +98107,7 @@ var require_turndown_cjs = __commonJS({
|
|
|
98107
98107
|
if (whitespace.leading || whitespace.trailing) content = content.trim();
|
|
98108
98108
|
return whitespace.leading + rule.replacement(content, node2, this.options) + whitespace.trailing;
|
|
98109
98109
|
}
|
|
98110
|
-
function
|
|
98110
|
+
function join29(output, replacement) {
|
|
98111
98111
|
var s1 = trimTrailingNewlines(output);
|
|
98112
98112
|
var s2 = trimLeadingNewlines(replacement);
|
|
98113
98113
|
var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
|
|
@@ -102898,9 +102898,9 @@ var init_file_type = __esm({
|
|
|
102898
102898
|
});
|
|
102899
102899
|
|
|
102900
102900
|
// main.ts
|
|
102901
|
-
import { existsSync as
|
|
102901
|
+
import { existsSync as existsSync11, readdirSync as readdirSync2 } from "node:fs";
|
|
102902
102902
|
import { readFile as readFile5 } from "node:fs/promises";
|
|
102903
|
-
import { join as
|
|
102903
|
+
import { join as join28 } from "node:path";
|
|
102904
102904
|
|
|
102905
102905
|
// node_modules/.pnpm/package-manager-detector@1.8.0/node_modules/package-manager-detector/dist/commands.mjs
|
|
102906
102906
|
function splitRunArgs(args2, valueFlags = []) {
|
|
@@ -103909,6 +103909,15 @@ var providers = {
|
|
|
103909
103909
|
openRouterEffort: [],
|
|
103910
103910
|
openRouterResolve: "openrouter/qwen/qwen3.8-flash"
|
|
103911
103911
|
},
|
|
103912
|
+
// Zen lists Mistral Large 4 at half its list price for launch, level with
|
|
103913
|
+
// the routers. hidden until a published `pullfrog` resolves it, as there.
|
|
103914
|
+
"mistral-large": {
|
|
103915
|
+
displayName: "Mistral Large",
|
|
103916
|
+
resolve: "opencode/mistral-large-4",
|
|
103917
|
+
effort: ["none", "high"],
|
|
103918
|
+
openRouterResolve: "openrouter/mistralai/mistral-large-4-0",
|
|
103919
|
+
hidden: true
|
|
103920
|
+
},
|
|
103912
103921
|
"gpt-5-nano": {
|
|
103913
103922
|
displayName: "GPT Nano",
|
|
103914
103923
|
resolve: "opencode/gpt-5.4-nano",
|
|
@@ -103957,12 +103966,12 @@ var providers = {
|
|
|
103957
103966
|
fallback: "opencode/big-pickle"
|
|
103958
103967
|
},
|
|
103959
103968
|
// Zen's FREE stealth preview (listed 2026-09-23), the third after
|
|
103960
|
-
// big-pickle and union-alpha.
|
|
103961
|
-
//
|
|
103962
|
-
//
|
|
103969
|
+
// big-pickle and union-alpha. Zen's docs: zero retention, no training.
|
|
103970
|
+
// unlike the first two it publishes an effort ladder, so the gate
|
|
103971
|
+
// mirrors one here.
|
|
103963
103972
|
"space-bunny": {
|
|
103964
103973
|
displayName: "Space Bunny",
|
|
103965
|
-
description: "Stealth preview
|
|
103974
|
+
description: "Stealth preview, free for a limited time",
|
|
103966
103975
|
resolve: "opencode/space-bunny-free",
|
|
103967
103976
|
effort: ["low", "medium", "high", "xhigh", "max"],
|
|
103968
103977
|
// free to run, still gated on the provider's own OPENCODE_API_KEY —
|
|
@@ -103980,17 +103989,30 @@ var providers = {
|
|
|
103980
103989
|
// see the big-pickle note above (#1077).
|
|
103981
103990
|
isFree: true
|
|
103982
103991
|
},
|
|
103983
|
-
// Zen's FREE stealth preview (listed 2026-10-01), the fifth promo. Zen
|
|
103984
|
-
//
|
|
103992
|
+
// Zen's FREE stealth preview (listed 2026-10-01), the fifth promo. Zen's
|
|
103993
|
+
// docs: data may be used to improve the model during the free period.
|
|
103985
103994
|
"fledge-alpha": {
|
|
103986
103995
|
displayName: "Fledge Alpha",
|
|
103987
|
-
description: "Stealth preview
|
|
103996
|
+
description: "Stealth preview; data may be used to improve it",
|
|
103988
103997
|
resolve: "opencode/fledge-alpha-free",
|
|
103989
103998
|
effort: ["low", "high", "max"],
|
|
103990
103999
|
// free to run, still gated on the provider's own OPENCODE_API_KEY —
|
|
103991
104000
|
// see the big-pickle note above (#1077).
|
|
103992
104001
|
isFree: true
|
|
103993
104002
|
},
|
|
104003
|
+
// Zen's FREE preview (listed 2026-10-06), the sixth promo, on Fledge
|
|
104004
|
+
// Alpha's terms. hidden until a published `pullfrog` resolves it.
|
|
104005
|
+
exo: {
|
|
104006
|
+
displayName: "Exo",
|
|
104007
|
+
description: "Preview; data may be used to improve it",
|
|
104008
|
+
resolve: "opencode/exo-free",
|
|
104009
|
+
// Zen publishes a single rung.
|
|
104010
|
+
effort: ["high"],
|
|
104011
|
+
// free to run, still gated on the provider's own OPENCODE_API_KEY —
|
|
104012
|
+
// see the big-pickle note above (#1077).
|
|
104013
|
+
isFree: true,
|
|
104014
|
+
hidden: true
|
|
104015
|
+
},
|
|
103994
104016
|
// Zen's live free MiMo, and the second free row in a menu that big-pickle
|
|
103995
104017
|
// was alone in since `mimo-v2-pro-free` lost its model.
|
|
103996
104018
|
mimo: {
|
|
@@ -104446,6 +104468,17 @@ var providers = {
|
|
|
104446
104468
|
resolve: "openrouter/meta/muse-spark-1.3-contributor",
|
|
104447
104469
|
effort: ["minimal", "low", "medium", "high", "xhigh", "max"],
|
|
104448
104470
|
openRouterResolve: "openrouter/meta/muse-spark-1.3-contributor"
|
|
104471
|
+
},
|
|
104472
|
+
// Mistral's flagship (public preview, 2026-10-06), on the routers only:
|
|
104473
|
+
// the catalog carries no direct Mistral provider. out of the pickers
|
|
104474
|
+
// until a published `pullfrog` resolves the slug; see "A new alias
|
|
104475
|
+
// reaches the picker at merge".
|
|
104476
|
+
"mistral-large": {
|
|
104477
|
+
displayName: "Mistral Large",
|
|
104478
|
+
resolve: "openrouter/mistralai/mistral-large-4-0",
|
|
104479
|
+
effort: ["none", "high"],
|
|
104480
|
+
openRouterResolve: "openrouter/mistralai/mistral-large-4-0",
|
|
104481
|
+
hidden: true
|
|
104449
104482
|
}
|
|
104450
104483
|
}
|
|
104451
104484
|
}),
|
|
@@ -104537,6 +104570,13 @@ var providers = {
|
|
|
104537
104570
|
displayName: "Muse Spark",
|
|
104538
104571
|
resolve: "vercel/meta/muse-spark-1.3",
|
|
104539
104572
|
effort: ["minimal", "low", "medium", "high", "xhigh"]
|
|
104573
|
+
},
|
|
104574
|
+
// hidden until a published `pullfrog` resolves it, as on OpenRouter.
|
|
104575
|
+
"mistral-large": {
|
|
104576
|
+
displayName: "Mistral Large",
|
|
104577
|
+
resolve: "vercel/mistral/mistral-large-4",
|
|
104578
|
+
effort: ["none", "low", "medium", "high"],
|
|
104579
|
+
hidden: true
|
|
104540
104580
|
}
|
|
104541
104581
|
}
|
|
104542
104582
|
})
|
|
@@ -105417,7 +105457,13 @@ var PROVIDER_ERROR_PATTERNS = [
|
|
|
105417
105457
|
// …/settings/credits`) matched neither this nor `requires more credits`, so
|
|
105418
105458
|
// the one run in that episode that was a genuinely empty wallet rendered with
|
|
105419
105459
|
// no CTA at all while runs 1-6 rendered correctly.
|
|
105420
|
-
|
|
105460
|
+
// `account funds` is Zen's (#1474).
|
|
105461
|
+
{
|
|
105462
|
+
regex: /Insufficient (?:balance|credits|account funds)/i,
|
|
105463
|
+
label: PROVIDER_BILLING_EXHAUSTED_LABEL
|
|
105464
|
+
},
|
|
105465
|
+
// OpenAI's `You have no credits remaining` (#1474).
|
|
105466
|
+
{ regex: /no credits remaining/i, label: PROVIDER_BILLING_EXHAUSTED_LABEL },
|
|
105421
105467
|
{ regex: /credit balance is too low/i, label: PROVIDER_BILLING_EXHAUSTED_LABEL },
|
|
105422
105468
|
// "spending cap" (Gemini) and "spending limit" (xAI, #1076) are the same
|
|
105423
105469
|
// condition in two house styles — a configured ceiling was reached.
|
|
@@ -105538,6 +105584,9 @@ function isRouterKeylimitExhaustedError(text) {
|
|
|
105538
105584
|
function isProviderBillingExhausted(text) {
|
|
105539
105585
|
return findProviderErrorMatch(text)?.label === PROVIDER_BILLING_EXHAUSTED_LABEL;
|
|
105540
105586
|
}
|
|
105587
|
+
function isProviderUsageLimit(text) {
|
|
105588
|
+
return findProviderErrorMatch(text)?.label === PROVIDER_USAGE_LIMIT_LABEL;
|
|
105589
|
+
}
|
|
105541
105590
|
function isProviderNoRoutableEndpoints(text) {
|
|
105542
105591
|
return findProviderErrorMatch(text)?.label === PROVIDER_NO_ENDPOINTS_LABEL;
|
|
105543
105592
|
}
|
|
@@ -105609,7 +105658,7 @@ var import_semver = __toESM(require_semver2(), 1);
|
|
|
105609
105658
|
// package.json
|
|
105610
105659
|
var package_default = {
|
|
105611
105660
|
name: "pullfrog",
|
|
105612
|
-
version: "0.1.
|
|
105661
|
+
version: "0.1.97",
|
|
105613
105662
|
cliContractVersion: "1",
|
|
105614
105663
|
type: "module",
|
|
105615
105664
|
bin: {
|
|
@@ -105906,6 +105955,7 @@ ${this.buffer}`;
|
|
|
105906
105955
|
};
|
|
105907
105956
|
async function spawn(options) {
|
|
105908
105957
|
const activityTimeoutMs = options.activityTimeout ?? DEFAULT_ACTIVITY_TIMEOUT_MS;
|
|
105958
|
+
options.signal?.throwIfAborted();
|
|
105909
105959
|
installSignalHandler();
|
|
105910
105960
|
const startTime = performance3.now();
|
|
105911
105961
|
const retain = options.retain ?? "tail";
|
|
@@ -105941,17 +105991,18 @@ async function spawn(options) {
|
|
|
105941
105991
|
let isActivityTimedOut = false;
|
|
105942
105992
|
let lastActivityTime = performance3.now();
|
|
105943
105993
|
let killedAtIdleMs;
|
|
105994
|
+
const terminate = () => {
|
|
105995
|
+
if (sigkillEscalatorId) return;
|
|
105996
|
+
killSelf("SIGTERM");
|
|
105997
|
+
sigkillEscalatorId = setTimeout(() => killSelf("SIGKILL"), 5e3);
|
|
105998
|
+
};
|
|
105944
105999
|
if (options.timeout) {
|
|
105945
106000
|
timeoutId = setTimeout(() => {
|
|
105946
106001
|
isTimedOut = true;
|
|
105947
|
-
|
|
105948
|
-
sigkillEscalatorId = setTimeout(() => {
|
|
105949
|
-
if (!child.killed) {
|
|
105950
|
-
killSelf("SIGKILL");
|
|
105951
|
-
}
|
|
105952
|
-
}, 5e3);
|
|
106002
|
+
terminate();
|
|
105953
106003
|
}, options.timeout);
|
|
105954
106004
|
}
|
|
106005
|
+
options.signal?.addEventListener("abort", terminate, { once: true });
|
|
105955
106006
|
if (activityTimeoutMs > 0) {
|
|
105956
106007
|
log.debug(
|
|
105957
106008
|
`spawn activity timer: pid=${child.pid} cmd=${options.cmd} timeout=${activityTimeoutMs}ms`
|
|
@@ -106016,6 +106067,11 @@ async function spawn(options) {
|
|
|
106016
106067
|
if (timeoutId) clearTimeout(timeoutId);
|
|
106017
106068
|
if (sigkillEscalatorId) clearTimeout(sigkillEscalatorId);
|
|
106018
106069
|
if (activityCheckIntervalId) clearInterval(activityCheckIntervalId);
|
|
106070
|
+
options.signal?.removeEventListener("abort", terminate);
|
|
106071
|
+
if (options.signal?.aborted) {
|
|
106072
|
+
reject(options.signal.reason);
|
|
106073
|
+
return;
|
|
106074
|
+
}
|
|
106019
106075
|
if (isTimedOut) {
|
|
106020
106076
|
reject(
|
|
106021
106077
|
new SpawnTimeoutError(`process timed out after ${options.timeout}ms`, SPAWN_TIMEOUT_CODE)
|
|
@@ -106054,6 +106110,7 @@ ${killMsg}` : killMsg;
|
|
|
106054
106110
|
if (timeoutId) clearTimeout(timeoutId);
|
|
106055
106111
|
if (sigkillEscalatorId) clearTimeout(sigkillEscalatorId);
|
|
106056
106112
|
if (activityCheckIntervalId) clearInterval(activityCheckIntervalId);
|
|
106113
|
+
options.signal?.removeEventListener("abort", terminate);
|
|
106057
106114
|
const errMsg = `[spawn] ${options.cmd}: ${error49.message}`;
|
|
106058
106115
|
console.error(errMsg);
|
|
106059
106116
|
const existingStderr = stderrBuffer?.toString() ?? "";
|
|
@@ -106295,17 +106352,16 @@ Report findings clearly with file:line references and quoted evidence where poss
|
|
|
106295
106352
|
// modes.ts
|
|
106296
106353
|
var PR_SUMMARY_FORMAT = `### Default format
|
|
106297
106354
|
|
|
106298
|
-
The body has at most
|
|
106355
|
+
The body has at most two parts, in this order:
|
|
106299
106356
|
|
|
106300
106357
|
1. **Reviewed changes preamble** \u2014 a bolded \`**Reviewed changes**\` lead-in with one sentence on what was reviewed in this run (for \`IncrementalReview\`: what changed since the prior pullfrog review), then a bullet list of the substantive changes \u2014 short bolded title, one sentence each. A reviewer should understand the full reviewed scope from this list alone. Close the preamble with the metadata comment below.
|
|
106301
106358
|
2. **Cross-cutting issue sections** (zero or more) \u2014 one \`### {emoji} {what's wrong, not what to do}\` heading per concern.
|
|
106302
|
-
3. **\`### \u2139\uFE0F Nitpicks\`** at the very bottom, if any \u2014 a flat bullet list, no technical-details block. Only for what has no runtime effect: a stale or wrong comment or doc, a misleading name, a typo, dead code, a missing test. Never formatting or whitespace.
|
|
106303
106359
|
|
|
106304
106360
|
**Inline vs. body.** Concerns that anchor to a specific line go inline (the \`comments\` parameter), even when their implications are broad. Body \`### \` sections are reserved for concerns that have **no line to anchor to** \u2014 *absence* (something the diff should have done but didn't), *sequencing* (rollout / deletion / migration order), *design decisions only the human can make*, or *scope questions the diff raises but doesn't address*. With no non-anchorable concerns, the body is just the preamble + metadata.
|
|
106305
106361
|
|
|
106306
106362
|
**Severity emoji** on every \`### \` heading, and nowhere else: \u{1F6A8} critical (blocks merge \u2014 data loss, security, broken core flow) \xB7 \u26A0\uFE0F important (must address before merging) \xB7 \u2139\uFE0F informational (mergeable as-is).
|
|
106307
106363
|
|
|
106308
|
-
**Behavior decides placement, not fix size.** A finding that changes what the code does \u2014 a wrong value or message, a crash, a race, a leak, a missed cleanup, a slow path \u2014 gets its own inline comment (or a \`### \` section if no line anchors it) at its real severity, however small the fix.
|
|
106364
|
+
**Behavior decides placement, not fix size.** A finding that changes what the code does \u2014 a wrong value or message, a crash, a race, a leak, a missed cleanup, a slow path \u2014 gets its own inline comment (or a \`### \` section if no line anchors it) at its real severity, however small the fix.
|
|
106309
106365
|
|
|
106310
106366
|
**Blank line between every block-level element.** GitHub's markdown parser requires one before and after HTML tags (\`<details>\`, \`<summary>\`, \`<sub>\`, \`<br/>\`) \u2014 without it GitHub treats what follows as a continuation of the HTML block and renders your markdown as literal text. This is a parser quirk, not a style preference, and it permanently breaks the posted review.
|
|
106311
106367
|
|
|
@@ -106454,8 +106510,6 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
106454
106510
|
|
|
106455
106511
|
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.
|
|
106456
106512
|
|
|
106457
|
-
when the diff adds or changes a test, check that it can actually fail: a test that would still pass with the bug present is theatre, not coverage. the usual tell is a loose assertion standing where an exact one belongs \u2014 \`>=\` or a truthiness check over an expected value, or a snapshot that absorbs whatever it is handed. read the assertion against the behavior it claims to pin, not against whether it currently passes.
|
|
106458
|
-
|
|
106459
106513
|
skip the deeper pass and submit a \`No new issues found.\` review per step 7 only when the diff has **no behavioral surface at all** \u2014 doc typos, whitespace/formatting, lockfile or generated-code regeneration, a mechanical rename whose only effect is import-path updates. line count is not the signal: a one-line change to auth, money, SQL, a comparison operator, a redirect, or a config default is not trivial.
|
|
106460
106514
|
|
|
106461
106515
|
4. **specialist decision**: after reading the complete diff, name the questions you still cannot answer confidently yourself, and dispatch one \`${REVIEWER_AGENT_NAME}\` specialist per question. a question qualifies only when a specialist could return evidence that **changes your disposition** on the PR \u2014 generic requests for another look, extra confidence, or polish do not. most reviews need zero or one; some need several.
|
|
@@ -106479,8 +106533,6 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
106479
106533
|
|
|
106480
106534
|
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.
|
|
106481
106535
|
|
|
106482
|
-
**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?); 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.
|
|
106483
|
-
|
|
106484
106536
|
for surviving findings, draft inline comments with NEW line numbers from the diff \u2014 attach a \`<details>Technical details</details>\` block to any inline comment whose fix is non-trivial or has cross-file implications (see Inline technical details in the format below). every comment must be actionable, 2-3 sentences max in the visible part, one per root cause \u2014 name its other sites in that comment rather than repeating it. use GitHub permalink format for code references. for impact-analysis findings (stale references after rename/remove), report them in the review body ordered by severity (runtime breakage > incorrect docs > stale comments) rather than as inline comments unless they're anchored to a specific line.
|
|
106485
106537
|
|
|
106486
106538
|
7. **submit**: ALWAYS submit exactly one review via \`${t("create_pull_request_review")}\`. Do NOT call \`report_progress\` \u2014 the review is the final record and the progress comment will be cleaned up automatically.
|
|
@@ -106502,7 +106554,7 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
106502
106554
|
\`approved: false\`. Body opens with \`> [!CAUTION]\\n> This PR introduces ...\`, followed by the PR summary. Include all inline comments via \`comments\`.
|
|
106503
106555
|
- **must-address non-critical findings** (real consequences if shipped \u2014 incorrect behavior in non-critical paths, missing validation on user input, regressions the author should fix before merge):
|
|
106504
106556
|
\`approved: false\`. Body opens with \`> [!IMPORTANT]\\n> ...\`, followed by the PR summary. Reserve this tier for findings with concrete fallout \u2014 do NOT use \`[!IMPORTANT]\` for nits, style preferences, or "consider also" suggestions. Include all inline comments via \`comments\`.
|
|
106505
|
-
- **minor suggestions only** (low-stakes findings
|
|
106557
|
+
- **minor suggestions only** (low-stakes findings):
|
|
106506
106558
|
\`approved: false\`. Body opens with \`> \u2139\uFE0F No critical issues \u2014 minor suggestions inline.\\n\\n\` followed by the PR summary. Include all inline comments via \`comments\`. Vary the wording after the emoji to fit the review (e.g. "Minor suggestions only.", "Two rough edges worth a look."), but always keep the \u2139\uFE0F prefix and keep it short.
|
|
106507
106559
|
- **informational observations** (mergeable as-is, nothing actionable \u2014 e.g. prior feedback addressed cleanly, calling out something noteworthy without recommending a change):
|
|
106508
106560
|
\`approved: true\`. Body opens with \`> \u2705 No new issues found.\\n\\n\` followed by the PR summary. Do NOT include inline \`comments\` \u2014 the \u2705 signals "no action needed", which contradicts an actionable anchor; if a point is concrete enough to anchor to a line, downgrade the whole review to "minor suggestions only" (\`approved: false\`) instead.
|
|
@@ -106545,8 +106597,6 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
106545
106597
|
|
|
106546
106598
|
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.**
|
|
106547
106599
|
|
|
106548
|
-
a test added or changed in this delta must be able to fail \u2014 one that would still pass with the bug present is theatre, not coverage. the tell is a loose assertion where an exact one belongs (\`>=\` or a truthiness check over an expected value, a snapshot that absorbs whatever it is handed).
|
|
106549
|
-
|
|
106550
106600
|
skip the deeper pass and jump to step 10's non-substantive path (do NOT submit a review) only when the incremental changes have **no behavioral surface at all** \u2014 formatting, comment tweaks, import reordering, lockfile regen, a mechanical rename of import paths. line count is not the signal: a one-line change to auth, money, SQL, a comparison operator, a redirect, or a config default is not trivial.
|
|
106551
106601
|
|
|
106552
106602
|
6. **specialist decision**: after covering the incremental and full diffs, name the questions about the new changes that you still cannot answer confidently yourself, and dispatch one \`${REVIEWER_AGENT_NAME}\` specialist per question. a question qualifies only when a specialist could return evidence that **changes your disposition** on the PR \u2014 generic requests for another look, extra confidence, or polish do not. most incremental reviews need zero or one, especially thread-reply re-reviews; some need several.
|
|
@@ -106570,11 +106620,9 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
106570
106620
|
|
|
106571
106621
|
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.
|
|
106572
106622
|
|
|
106573
|
-
**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?); 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.
|
|
106574
|
-
|
|
106575
106623
|
draft inline comments with NEW line numbers from the full PR diff \u2014 attach a \`<details>Technical details</details>\` block to any inline comment whose fix is non-trivial or has cross-file implications (see Inline technical details in the format below). every comment must be actionable, 2-3 sentences max in the visible part, one per root cause \u2014 name its other sites in that comment rather than repeating it.
|
|
106576
106624
|
|
|
106577
|
-
9. **build the review body**: use the same default format as Review mode (preamble + optional cross-cutting \`### \` sections
|
|
106625
|
+
9. **build the review body**: use the same default format as Review mode (preamble + optional cross-cutting \`### \` sections) \u2014 scoped to the **incremental delta**, not the full PR. The "Reviewed changes" bullets describe what changed since the prior pullfrog review (each bullet starts with a past-tense verb, e.g. \`- Extracted shared CLI runtime into a single module\`). Do NOT include a separate "Prior review feedback" checklist \u2014 that's tracked in the rolling PR summary snapshot for the next agent run, and surfacing it in the user-facing body is noise (changes that addressed prior feedback are already covered by the Reviewed-changes bullets). In some cases you may receive a complete diff for the whole PR instead of an incremental one; when this happens, determine what changed since Pullfrog's most recent review yourself before drafting bullets.
|
|
106578
106626
|
|
|
106579
106627
|
10. Submit \u2014 every run must end with EXACTLY ONE of \`${t("create_pull_request_review")}\` (substantive review) or \`${t("report_progress")}\` (no-review acknowledgement). do NOT call \`create_issue_comment\` for review output.
|
|
106580
106628
|
|
|
@@ -106585,7 +106633,7 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
106585
106633
|
- IF NO NEW ISSUES, NON-SUBSTANTIVE CHANGES ONLY (trivial formatting, import reordering, comment tweaks): do NOT submit a review. Instead call \`${t("report_progress")}\` with \`no_review_needed: true\` and a 1-2 sentence note explaining no review was warranted (e.g. "No new issues. Changes since last review are formatting-only."). this leaves a visible signal that the run completed.
|
|
106586
106634
|
- ELSE IF NEW CRITICAL ISSUES (blocks merge \u2014 bugs, security, data loss, broken core flows): call \`${t("create_pull_request_review")}\` with \`approved: false\`, all comments, and the review body. body opens with \`> [!CAUTION]\\n> This PR introduces ...\`, followed by the PR summary using the default format below.
|
|
106587
106635
|
- ELSE IF NEW MUST-ADDRESS NON-CRITICAL FINDINGS (real consequences if shipped \u2014 incorrect behavior, missing validation, regressions the author should fix before merge): call \`${t("create_pull_request_review")}\` with \`approved: false\`, all comments, and the review body. body opens with \`> [!IMPORTANT]\\n> ...\`, followed by the PR summary using the default format below. Do NOT use this tier for nits, style preferences, or "consider also" suggestions.
|
|
106588
|
-
- ELSE IF NEW MINOR SUGGESTIONS ONLY (low-stakes findings
|
|
106636
|
+
- ELSE IF NEW MINOR SUGGESTIONS ONLY (low-stakes findings): call \`${t("create_pull_request_review")}\` with \`approved: false\`, all comments, and the review body. body opens with \`> \u2139\uFE0F No critical issues \u2014 minor suggestions inline.\\n\\n\` (vary the wording after \u2139\uFE0F to fit the review), followed by the PR summary using the default format below.
|
|
106589
106637
|
- ELSE IF INFORMATIONAL OBSERVATIONS (mergeable as-is, but worth surfacing \u2014 e.g. prior feedback addressed cleanly, or a noteworthy positive observation): call \`${t("create_pull_request_review")}\` with \`approved: true\`, NO inline comments, and the review body. body opens with \`> \u2705 No new issues found.\\n\\n\` (or similar friendly green opener), followed by the PR summary using the default format below. If a point is concrete enough to anchor to a line, downgrade the whole review to "minor suggestions only" (\`approved: false\`) instead \u2014 the \u2705 signals "no action needed", which contradicts an actionable anchor.
|
|
106590
106638
|
- ELSE IF NO NEW ISSUES, SUBSTANTIVE CHANGES (new functionality, behavior changes, or fixes to prior review feedback): call \`${t("create_pull_request_review")}\` to create a PR review. If all previous reviews have been properly addressed and no new issues were discovered, set \`approved: true\`. body opens with \`> \u2705 No new issues found.\\n\\n\`, followed by the PR summary using the default format below.
|
|
106591
106639
|
|
|
@@ -107802,7 +107850,7 @@ import { performance as performance6 } from "node:perf_hooks";
|
|
|
107802
107850
|
|
|
107803
107851
|
// utils/codexHome.ts
|
|
107804
107852
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
107805
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync2, writeFileSync as writeFileSync4 } from "node:fs";
|
|
107853
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync5, mkdtempSync, readFileSync as readFileSync2, writeFileSync as writeFileSync4 } from "node:fs";
|
|
107806
107854
|
import { homedir as homedir2, userInfo } from "node:os";
|
|
107807
107855
|
import { join as join5 } from "node:path";
|
|
107808
107856
|
|
|
@@ -107853,205 +107901,6 @@ function stringifyCodexAuthBody(body) {
|
|
|
107853
107901
|
`;
|
|
107854
107902
|
}
|
|
107855
107903
|
|
|
107856
|
-
// utils/xaiOAuth.ts
|
|
107857
|
-
function parseXaiAuthBody(raw2) {
|
|
107858
|
-
let parsed2;
|
|
107859
|
-
try {
|
|
107860
|
-
parsed2 = JSON.parse(raw2);
|
|
107861
|
-
} catch {
|
|
107862
|
-
return null;
|
|
107863
|
-
}
|
|
107864
|
-
if (!parsed2 || typeof parsed2 !== "object") return null;
|
|
107865
|
-
const v = parsed2;
|
|
107866
|
-
if (v.auth_mode !== "grok") return null;
|
|
107867
|
-
const tokens = v.tokens;
|
|
107868
|
-
if (!tokens || typeof tokens !== "object") return null;
|
|
107869
|
-
const t = tokens;
|
|
107870
|
-
if (typeof t.access_token !== "string" || t.access_token.length === 0) return null;
|
|
107871
|
-
if (typeof t.refresh_token !== "string" || t.refresh_token.length === 0) return null;
|
|
107872
|
-
return {
|
|
107873
|
-
auth_mode: "grok",
|
|
107874
|
-
...typeof v.refresh_rejected_at === "string" ? { refresh_rejected_at: v.refresh_rejected_at } : {},
|
|
107875
|
-
tokens: { access_token: t.access_token, refresh_token: t.refresh_token },
|
|
107876
|
-
...typeof v.last_refresh === "string" ? { last_refresh: v.last_refresh } : {}
|
|
107877
|
-
};
|
|
107878
|
-
}
|
|
107879
|
-
|
|
107880
|
-
// utils/codexHome.ts
|
|
107881
|
-
var CODEX_AUTH_ENV = "CODEX_AUTH_JSON";
|
|
107882
|
-
var XAI_AUTH_ENV = "GROK_AUTH_JSON";
|
|
107883
|
-
var installedSubscriptionPaths = /* @__PURE__ */ new Map();
|
|
107884
|
-
function clearInstalledSubscription(name) {
|
|
107885
|
-
const path4 = installedSubscriptionPaths.get(name);
|
|
107886
|
-
if (!path4 || !existsSync4(path4)) return;
|
|
107887
|
-
const value2 = JSON.parse(readFileSync2(path4, "utf8"));
|
|
107888
|
-
if (!value2 || typeof value2 !== "object") return;
|
|
107889
|
-
if (name === "CODEX_AUTH_JSON" && "openai" in value2) delete value2.openai;
|
|
107890
|
-
if (name === "GROK_AUTH_JSON" && "xai" in value2) delete value2.xai;
|
|
107891
|
-
writeFileSync4(path4, `${JSON.stringify(value2)}
|
|
107892
|
-
`, { mode: 384 });
|
|
107893
|
-
installedSubscriptionPaths.delete(name);
|
|
107894
|
-
}
|
|
107895
|
-
var PULLFROG_DATA_DIR = "/var/lib/pullfrog";
|
|
107896
|
-
function isRejectedChain(body) {
|
|
107897
|
-
if (!body.refresh_rejected_at) return false;
|
|
107898
|
-
log.warning(
|
|
107899
|
-
`\xBB ${CODEX_AUTH_ENV} was rejected by OpenAI at ${body.refresh_rejected_at} and cannot be refreshed \u2014 re-run \`npx pullfrog auth codex\`. skipping it for this run.`
|
|
107900
|
-
);
|
|
107901
|
-
return true;
|
|
107902
|
-
}
|
|
107903
|
-
function installCodexAuth() {
|
|
107904
|
-
const raw2 = process.env[CODEX_AUTH_ENV];
|
|
107905
|
-
if (!raw2) return null;
|
|
107906
|
-
const body = parseCodexAuthBody(raw2);
|
|
107907
|
-
if (!body) {
|
|
107908
|
-
log.warning(`\xBB ${CODEX_AUTH_ENV} present but malformed; ignoring`);
|
|
107909
|
-
return null;
|
|
107910
|
-
}
|
|
107911
|
-
if (isRejectedChain(body)) return null;
|
|
107912
|
-
const expiresMs = decodeJwtExpMs(body.tokens.access_token) ?? 0;
|
|
107913
|
-
const xdgDataHome = resolveDataHome();
|
|
107914
|
-
if (!xdgDataHome) return null;
|
|
107915
|
-
const opencodeDir = join5(xdgDataHome, "opencode");
|
|
107916
|
-
const authPath = join5(opencodeDir, "auth.json");
|
|
107917
|
-
writeOpenCodeAuthEntry({
|
|
107918
|
-
opencodeDir,
|
|
107919
|
-
authPath,
|
|
107920
|
-
provider: "openai",
|
|
107921
|
-
entry: {
|
|
107922
|
-
type: "oauth",
|
|
107923
|
-
refresh: body.tokens.refresh_token,
|
|
107924
|
-
access: body.tokens.access_token,
|
|
107925
|
-
expires: expiresMs,
|
|
107926
|
-
...body.tokens.account_id ? { accountId: body.tokens.account_id } : {}
|
|
107927
|
-
}
|
|
107928
|
-
});
|
|
107929
|
-
process.env.XDG_DATA_HOME = xdgDataHome;
|
|
107930
|
-
log.info(`\xBB installed Codex auth at ${authPath}`);
|
|
107931
|
-
installedSubscriptionPaths.set(CODEX_AUTH_ENV, authPath);
|
|
107932
|
-
return {
|
|
107933
|
-
authPath,
|
|
107934
|
-
xdgDataHome,
|
|
107935
|
-
originalRefresh: body.tokens.refresh_token,
|
|
107936
|
-
originalIdToken: body.tokens.id_token
|
|
107937
|
-
};
|
|
107938
|
-
}
|
|
107939
|
-
function writeOpenCodeAuthEntry(params) {
|
|
107940
|
-
let existing = {};
|
|
107941
|
-
if (existsSync4(params.authPath)) {
|
|
107942
|
-
try {
|
|
107943
|
-
const parsed2 = JSON.parse(readFileSync2(params.authPath, "utf8"));
|
|
107944
|
-
if (parsed2 && typeof parsed2 === "object") existing = parsed2;
|
|
107945
|
-
} catch {
|
|
107946
|
-
log.warning(`\xBB ${params.authPath} was unreadable; rewriting it`);
|
|
107947
|
-
}
|
|
107948
|
-
}
|
|
107949
|
-
const merged = { ...existing, [params.provider]: params.entry };
|
|
107950
|
-
mkdirSync5(params.opencodeDir, { recursive: true });
|
|
107951
|
-
writeFileSync4(params.authPath, `${JSON.stringify(merged, null, 2)}
|
|
107952
|
-
`, { mode: 384 });
|
|
107953
|
-
}
|
|
107954
|
-
function installXaiAuth() {
|
|
107955
|
-
const raw2 = process.env[XAI_AUTH_ENV];
|
|
107956
|
-
if (!raw2) return null;
|
|
107957
|
-
const body = parseXaiAuthBody(raw2);
|
|
107958
|
-
if (!body) {
|
|
107959
|
-
log.warning(`\xBB ${XAI_AUTH_ENV} present but malformed; ignoring`);
|
|
107960
|
-
return null;
|
|
107961
|
-
}
|
|
107962
|
-
if (isXaiRejectedChain(body)) return null;
|
|
107963
|
-
const xdgDataHome = resolveDataHome();
|
|
107964
|
-
if (!xdgDataHome) return null;
|
|
107965
|
-
const opencodeDir = join5(xdgDataHome, "opencode");
|
|
107966
|
-
const authPath = join5(opencodeDir, "auth.json");
|
|
107967
|
-
writeOpenCodeAuthEntry({
|
|
107968
|
-
opencodeDir,
|
|
107969
|
-
authPath,
|
|
107970
|
-
provider: "xai",
|
|
107971
|
-
// opencode refreshes when `expires` is inside its skew window, and falls
|
|
107972
|
-
// back to the JWT's own exp when the stored value is absent. Decoding it
|
|
107973
|
-
// here means the first request uses the token we already hold instead of
|
|
107974
|
-
// burning a rotation on startup.
|
|
107975
|
-
entry: {
|
|
107976
|
-
type: "oauth",
|
|
107977
|
-
refresh: body.tokens.refresh_token,
|
|
107978
|
-
access: body.tokens.access_token,
|
|
107979
|
-
expires: decodeJwtExpMs(body.tokens.access_token) ?? 0
|
|
107980
|
-
}
|
|
107981
|
-
});
|
|
107982
|
-
process.env.XDG_DATA_HOME = xdgDataHome;
|
|
107983
|
-
log.info(`\xBB installed Grok auth at ${authPath}`);
|
|
107984
|
-
installedSubscriptionPaths.set(XAI_AUTH_ENV, authPath);
|
|
107985
|
-
return { authPath, xdgDataHome, originalRefresh: body.tokens.refresh_token };
|
|
107986
|
-
}
|
|
107987
|
-
function isXaiRejectedChain(body) {
|
|
107988
|
-
if (!body.refresh_rejected_at) return false;
|
|
107989
|
-
log.warning(
|
|
107990
|
-
`\xBB ${XAI_AUTH_ENV} was rejected by xAI at ${body.refresh_rejected_at} and cannot be refreshed \u2014 re-run \`npx pullfrog auth grok\`. skipping it for this run.`
|
|
107991
|
-
);
|
|
107992
|
-
return true;
|
|
107993
|
-
}
|
|
107994
|
-
function installCodexHome() {
|
|
107995
|
-
const raw2 = process.env[CODEX_AUTH_ENV];
|
|
107996
|
-
if (!raw2) return null;
|
|
107997
|
-
const body = parseCodexAuthBody(raw2);
|
|
107998
|
-
if (!body) {
|
|
107999
|
-
log.warning(`\xBB ${CODEX_AUTH_ENV} present but malformed; ignoring`);
|
|
108000
|
-
return null;
|
|
108001
|
-
}
|
|
108002
|
-
if (isRejectedChain(body)) return null;
|
|
108003
|
-
if (!body.tokens.id_token) {
|
|
108004
|
-
log.warning(
|
|
108005
|
-
`\xBB ${CODEX_AUTH_ENV} carries no id_token \u2014 the codex CLI cannot load it. re-run \`npx pullfrog auth codex\` to mint a complete credential.`
|
|
108006
|
-
);
|
|
108007
|
-
return null;
|
|
108008
|
-
}
|
|
108009
|
-
const root = resolveDataHome();
|
|
108010
|
-
if (!root) return null;
|
|
108011
|
-
const codexHome = join5(root, "codex");
|
|
108012
|
-
const authPath = join5(codexHome, "auth.json");
|
|
108013
|
-
mkdirSync5(codexHome, { recursive: true });
|
|
108014
|
-
writeFileSync4(authPath, stringifyCodexAuthBody(body), { mode: 384 });
|
|
108015
|
-
log.info(`\xBB installed Codex auth at ${authPath}`);
|
|
108016
|
-
return { codexHome, authPath, originalRefresh: body.tokens.refresh_token };
|
|
108017
|
-
}
|
|
108018
|
-
var dataHome;
|
|
108019
|
-
var unhostedSubscriptions = /* @__PURE__ */ new Set();
|
|
108020
|
-
function canInstallSubscription(name) {
|
|
108021
|
-
return name !== CODEX_AUTH_ENV && name !== XAI_AUTH_ENV || resolveDataHome() !== null;
|
|
108022
|
-
}
|
|
108023
|
-
function resolveDataHome() {
|
|
108024
|
-
if (process.env.CI !== "true") return join5(homedir2(), ".local", "share");
|
|
108025
|
-
if (dataHome === void 0) dataHome = bootstrapPullfrogDataDir();
|
|
108026
|
-
return dataHome;
|
|
108027
|
-
}
|
|
108028
|
-
function bootstrapPullfrogDataDir() {
|
|
108029
|
-
const user = userInfo().username;
|
|
108030
|
-
let primaryGroup;
|
|
108031
|
-
try {
|
|
108032
|
-
primaryGroup = execFileSync3("id", ["-gn", user], { stdio: "pipe", encoding: "utf-8" }).trim();
|
|
108033
|
-
} catch {
|
|
108034
|
-
primaryGroup = user;
|
|
108035
|
-
}
|
|
108036
|
-
try {
|
|
108037
|
-
execFileSync3("sudo", ["-n", "mkdir", "-p", PULLFROG_DATA_DIR], { stdio: "pipe" });
|
|
108038
|
-
execFileSync3("sudo", ["-n", "chown", `${user}:${primaryGroup}`, PULLFROG_DATA_DIR], {
|
|
108039
|
-
stdio: "pipe"
|
|
108040
|
-
});
|
|
108041
|
-
execFileSync3("sudo", ["-n", "chmod", "700", PULLFROG_DATA_DIR], { stdio: "pipe" });
|
|
108042
|
-
} catch (err) {
|
|
108043
|
-
log.warning(
|
|
108044
|
-
`\xBB subscription auth skipped: could not create ${PULLFROG_DATA_DIR} (${err instanceof Error ? err.message : String(err)}). it must live outside the agent sandbox's reach, which needs passwordless sudo on this runner; runs on a model that needs the subscription will report no key.`
|
|
108045
|
-
);
|
|
108046
|
-
for (const name of [CODEX_AUTH_ENV, XAI_AUTH_ENV]) {
|
|
108047
|
-
if (process.env[name]) unhostedSubscriptions.add(name);
|
|
108048
|
-
delete process.env[name];
|
|
108049
|
-
}
|
|
108050
|
-
return null;
|
|
108051
|
-
}
|
|
108052
|
-
return PULLFROG_DATA_DIR;
|
|
108053
|
-
}
|
|
108054
|
-
|
|
108055
107904
|
// utils/oauthWriteback.ts
|
|
108056
107905
|
var core2 = __toESM(require_core(), 1);
|
|
108057
107906
|
|
|
@@ -110281,6 +110130,7 @@ async function apiFetch(options) {
|
|
|
110281
110130
|
|
|
110282
110131
|
// utils/oauthWriteback.ts
|
|
110283
110132
|
var OAUTH_WRITEBACK_STATE = "oauth_writeback";
|
|
110133
|
+
var SUBSCRIPTION_DATA_DIR_STATE = "subscription_data_dir";
|
|
110284
110134
|
|
|
110285
110135
|
// utils/secretCommands.ts
|
|
110286
110136
|
var core3 = __toESM(require_core(), 1);
|
|
@@ -110291,6 +110141,212 @@ function saveSecretState(name, value2) {
|
|
|
110291
110141
|
if (isGitHubActions) core3.saveState(name, value2);
|
|
110292
110142
|
}
|
|
110293
110143
|
|
|
110144
|
+
// utils/xaiOAuth.ts
|
|
110145
|
+
function parseXaiAuthBody(raw2) {
|
|
110146
|
+
let parsed2;
|
|
110147
|
+
try {
|
|
110148
|
+
parsed2 = JSON.parse(raw2);
|
|
110149
|
+
} catch {
|
|
110150
|
+
return null;
|
|
110151
|
+
}
|
|
110152
|
+
if (!parsed2 || typeof parsed2 !== "object") return null;
|
|
110153
|
+
const v = parsed2;
|
|
110154
|
+
if (v.auth_mode !== "grok") return null;
|
|
110155
|
+
const tokens = v.tokens;
|
|
110156
|
+
if (!tokens || typeof tokens !== "object") return null;
|
|
110157
|
+
const t = tokens;
|
|
110158
|
+
if (typeof t.access_token !== "string" || t.access_token.length === 0) return null;
|
|
110159
|
+
if (typeof t.refresh_token !== "string" || t.refresh_token.length === 0) return null;
|
|
110160
|
+
return {
|
|
110161
|
+
auth_mode: "grok",
|
|
110162
|
+
...typeof v.refresh_rejected_at === "string" ? { refresh_rejected_at: v.refresh_rejected_at } : {},
|
|
110163
|
+
tokens: { access_token: t.access_token, refresh_token: t.refresh_token },
|
|
110164
|
+
...typeof v.last_refresh === "string" ? { last_refresh: v.last_refresh } : {}
|
|
110165
|
+
};
|
|
110166
|
+
}
|
|
110167
|
+
|
|
110168
|
+
// utils/codexHome.ts
|
|
110169
|
+
var CODEX_AUTH_ENV = "CODEX_AUTH_JSON";
|
|
110170
|
+
var XAI_AUTH_ENV = "GROK_AUTH_JSON";
|
|
110171
|
+
var installedSubscriptionPaths = /* @__PURE__ */ new Map();
|
|
110172
|
+
var installedDataHome;
|
|
110173
|
+
function getSubscriptionDataHome() {
|
|
110174
|
+
return installedDataHome;
|
|
110175
|
+
}
|
|
110176
|
+
function clearInstalledSubscription(name) {
|
|
110177
|
+
const path4 = installedSubscriptionPaths.get(name);
|
|
110178
|
+
if (!path4 || !existsSync4(path4)) return;
|
|
110179
|
+
const value2 = JSON.parse(readFileSync2(path4, "utf8"));
|
|
110180
|
+
if (!value2 || typeof value2 !== "object") return;
|
|
110181
|
+
if (name === "CODEX_AUTH_JSON" && "openai" in value2) delete value2.openai;
|
|
110182
|
+
if (name === "GROK_AUTH_JSON" && "xai" in value2) delete value2.xai;
|
|
110183
|
+
writeFileSync4(path4, `${JSON.stringify(value2)}
|
|
110184
|
+
`, { mode: 384 });
|
|
110185
|
+
installedSubscriptionPaths.delete(name);
|
|
110186
|
+
}
|
|
110187
|
+
var PULLFROG_DATA_DIR = "/var/lib/pullfrog";
|
|
110188
|
+
function isRejectedChain(body) {
|
|
110189
|
+
if (!body.refresh_rejected_at) return false;
|
|
110190
|
+
log.warning(
|
|
110191
|
+
`\xBB ${CODEX_AUTH_ENV} was rejected by OpenAI at ${body.refresh_rejected_at} and cannot be refreshed \u2014 re-run \`npx pullfrog auth codex\`. skipping it for this run.`
|
|
110192
|
+
);
|
|
110193
|
+
return true;
|
|
110194
|
+
}
|
|
110195
|
+
function installCodexAuth() {
|
|
110196
|
+
const raw2 = process.env[CODEX_AUTH_ENV];
|
|
110197
|
+
if (!raw2) return null;
|
|
110198
|
+
const body = parseCodexAuthBody(raw2);
|
|
110199
|
+
if (!body) {
|
|
110200
|
+
log.warning(`\xBB ${CODEX_AUTH_ENV} present but malformed; ignoring`);
|
|
110201
|
+
return null;
|
|
110202
|
+
}
|
|
110203
|
+
if (isRejectedChain(body)) return null;
|
|
110204
|
+
const expiresMs = decodeJwtExpMs(body.tokens.access_token) ?? 0;
|
|
110205
|
+
const xdgDataHome = resolveDataHome();
|
|
110206
|
+
if (!xdgDataHome) return null;
|
|
110207
|
+
const opencodeDir = join5(xdgDataHome, "opencode");
|
|
110208
|
+
const authPath = join5(opencodeDir, "auth.json");
|
|
110209
|
+
writeOpenCodeAuthEntry({
|
|
110210
|
+
opencodeDir,
|
|
110211
|
+
authPath,
|
|
110212
|
+
provider: "openai",
|
|
110213
|
+
entry: {
|
|
110214
|
+
type: "oauth",
|
|
110215
|
+
refresh: body.tokens.refresh_token,
|
|
110216
|
+
access: body.tokens.access_token,
|
|
110217
|
+
expires: expiresMs,
|
|
110218
|
+
...body.tokens.account_id ? { accountId: body.tokens.account_id } : {}
|
|
110219
|
+
}
|
|
110220
|
+
});
|
|
110221
|
+
installedDataHome = xdgDataHome;
|
|
110222
|
+
log.info(`\xBB installed Codex auth at ${authPath}`);
|
|
110223
|
+
installedSubscriptionPaths.set(CODEX_AUTH_ENV, authPath);
|
|
110224
|
+
return {
|
|
110225
|
+
authPath,
|
|
110226
|
+
originalRefresh: body.tokens.refresh_token,
|
|
110227
|
+
originalIdToken: body.tokens.id_token
|
|
110228
|
+
};
|
|
110229
|
+
}
|
|
110230
|
+
function writeOpenCodeAuthEntry(params) {
|
|
110231
|
+
let existing = {};
|
|
110232
|
+
if (existsSync4(params.authPath)) {
|
|
110233
|
+
try {
|
|
110234
|
+
const parsed2 = JSON.parse(readFileSync2(params.authPath, "utf8"));
|
|
110235
|
+
if (parsed2 && typeof parsed2 === "object") existing = parsed2;
|
|
110236
|
+
} catch {
|
|
110237
|
+
log.warning(`\xBB ${params.authPath} was unreadable; rewriting it`);
|
|
110238
|
+
}
|
|
110239
|
+
}
|
|
110240
|
+
const merged = { ...existing, [params.provider]: params.entry };
|
|
110241
|
+
mkdirSync5(params.opencodeDir, { recursive: true });
|
|
110242
|
+
writeFileSync4(params.authPath, `${JSON.stringify(merged, null, 2)}
|
|
110243
|
+
`, { mode: 384 });
|
|
110244
|
+
}
|
|
110245
|
+
function installXaiAuth() {
|
|
110246
|
+
const raw2 = process.env[XAI_AUTH_ENV];
|
|
110247
|
+
if (!raw2) return null;
|
|
110248
|
+
const body = parseXaiAuthBody(raw2);
|
|
110249
|
+
if (!body) {
|
|
110250
|
+
log.warning(`\xBB ${XAI_AUTH_ENV} present but malformed; ignoring`);
|
|
110251
|
+
return null;
|
|
110252
|
+
}
|
|
110253
|
+
if (isXaiRejectedChain(body)) return null;
|
|
110254
|
+
const xdgDataHome = resolveDataHome();
|
|
110255
|
+
if (!xdgDataHome) return null;
|
|
110256
|
+
const opencodeDir = join5(xdgDataHome, "opencode");
|
|
110257
|
+
const authPath = join5(opencodeDir, "auth.json");
|
|
110258
|
+
writeOpenCodeAuthEntry({
|
|
110259
|
+
opencodeDir,
|
|
110260
|
+
authPath,
|
|
110261
|
+
provider: "xai",
|
|
110262
|
+
// opencode refreshes when `expires` is inside its skew window, and falls
|
|
110263
|
+
// back to the JWT's own exp when the stored value is absent. Decoding it
|
|
110264
|
+
// here means the first request uses the token we already hold instead of
|
|
110265
|
+
// burning a rotation on startup.
|
|
110266
|
+
entry: {
|
|
110267
|
+
type: "oauth",
|
|
110268
|
+
refresh: body.tokens.refresh_token,
|
|
110269
|
+
access: body.tokens.access_token,
|
|
110270
|
+
expires: decodeJwtExpMs(body.tokens.access_token) ?? 0
|
|
110271
|
+
}
|
|
110272
|
+
});
|
|
110273
|
+
installedDataHome = xdgDataHome;
|
|
110274
|
+
log.info(`\xBB installed Grok auth at ${authPath}`);
|
|
110275
|
+
installedSubscriptionPaths.set(XAI_AUTH_ENV, authPath);
|
|
110276
|
+
return { authPath, originalRefresh: body.tokens.refresh_token };
|
|
110277
|
+
}
|
|
110278
|
+
function isXaiRejectedChain(body) {
|
|
110279
|
+
if (!body.refresh_rejected_at) return false;
|
|
110280
|
+
log.warning(
|
|
110281
|
+
`\xBB ${XAI_AUTH_ENV} was rejected by xAI at ${body.refresh_rejected_at} and cannot be refreshed \u2014 re-run \`npx pullfrog auth grok\`. skipping it for this run.`
|
|
110282
|
+
);
|
|
110283
|
+
return true;
|
|
110284
|
+
}
|
|
110285
|
+
function installCodexHome() {
|
|
110286
|
+
const raw2 = process.env[CODEX_AUTH_ENV];
|
|
110287
|
+
if (!raw2) return null;
|
|
110288
|
+
const body = parseCodexAuthBody(raw2);
|
|
110289
|
+
if (!body) {
|
|
110290
|
+
log.warning(`\xBB ${CODEX_AUTH_ENV} present but malformed; ignoring`);
|
|
110291
|
+
return null;
|
|
110292
|
+
}
|
|
110293
|
+
if (isRejectedChain(body)) return null;
|
|
110294
|
+
if (!body.tokens.id_token) {
|
|
110295
|
+
log.warning(
|
|
110296
|
+
`\xBB ${CODEX_AUTH_ENV} carries no id_token \u2014 the codex CLI cannot load it. re-run \`npx pullfrog auth codex\` to mint a complete credential.`
|
|
110297
|
+
);
|
|
110298
|
+
return null;
|
|
110299
|
+
}
|
|
110300
|
+
const root = resolveDataHome();
|
|
110301
|
+
if (!root) return null;
|
|
110302
|
+
const codexHome = join5(root, "codex");
|
|
110303
|
+
const authPath = join5(codexHome, "auth.json");
|
|
110304
|
+
mkdirSync5(codexHome, { recursive: true });
|
|
110305
|
+
writeFileSync4(authPath, stringifyCodexAuthBody(body), { mode: 384 });
|
|
110306
|
+
log.info(`\xBB installed Codex auth at ${authPath}`);
|
|
110307
|
+
return { codexHome, authPath, originalRefresh: body.tokens.refresh_token };
|
|
110308
|
+
}
|
|
110309
|
+
var dataHome;
|
|
110310
|
+
var unhostedSubscriptions = /* @__PURE__ */ new Set();
|
|
110311
|
+
function canInstallSubscription(name) {
|
|
110312
|
+
return name !== CODEX_AUTH_ENV && name !== XAI_AUTH_ENV || resolveDataHome() !== null;
|
|
110313
|
+
}
|
|
110314
|
+
function resolveDataHome() {
|
|
110315
|
+
if (process.env.CI !== "true") return join5(homedir2(), ".local", "share");
|
|
110316
|
+
if (dataHome === void 0) dataHome = bootstrapPullfrogDataDir();
|
|
110317
|
+
return dataHome;
|
|
110318
|
+
}
|
|
110319
|
+
function bootstrapPullfrogDataDir() {
|
|
110320
|
+
const user = userInfo().username;
|
|
110321
|
+
let primaryGroup;
|
|
110322
|
+
try {
|
|
110323
|
+
primaryGroup = execFileSync3("id", ["-gn", user], { stdio: "pipe", encoding: "utf-8" }).trim();
|
|
110324
|
+
} catch {
|
|
110325
|
+
primaryGroup = user;
|
|
110326
|
+
}
|
|
110327
|
+
try {
|
|
110328
|
+
execFileSync3("sudo", ["-n", "mkdir", "-p", PULLFROG_DATA_DIR], { stdio: "pipe" });
|
|
110329
|
+
execFileSync3("sudo", ["-n", "chown", `${user}:${primaryGroup}`, PULLFROG_DATA_DIR], {
|
|
110330
|
+
stdio: "pipe"
|
|
110331
|
+
});
|
|
110332
|
+
execFileSync3("sudo", ["-n", "chmod", "700", PULLFROG_DATA_DIR], { stdio: "pipe" });
|
|
110333
|
+
} catch (err) {
|
|
110334
|
+
log.warning(
|
|
110335
|
+
`\xBB subscription auth skipped: could not create ${PULLFROG_DATA_DIR} (${err instanceof Error ? err.message : String(err)}). it must live outside the agent sandbox's reach, which needs passwordless sudo on this runner; runs on a model that needs the subscription will report no key.`
|
|
110336
|
+
);
|
|
110337
|
+
for (const name of [CODEX_AUTH_ENV, XAI_AUTH_ENV]) {
|
|
110338
|
+
if (process.env[name]) unhostedSubscriptions.add(name);
|
|
110339
|
+
delete process.env[name];
|
|
110340
|
+
}
|
|
110341
|
+
return null;
|
|
110342
|
+
}
|
|
110343
|
+
const runDir = mkdtempSync(join5(PULLFROG_DATA_DIR, "run-"));
|
|
110344
|
+
if (process.env.RUNNER_ENVIRONMENT !== "github-hosted") {
|
|
110345
|
+
saveSecretState(SUBSCRIPTION_DATA_DIR_STATE, runDir);
|
|
110346
|
+
}
|
|
110347
|
+
return runDir;
|
|
110348
|
+
}
|
|
110349
|
+
|
|
110294
110350
|
// utils/secrets.ts
|
|
110295
110351
|
var SENSITIVE_PATTERNS = [
|
|
110296
110352
|
/_KEY$/i,
|
|
@@ -110937,9 +110993,8 @@ var codex = agent({
|
|
|
110937
110993
|
initialUsage: initial.usage,
|
|
110938
110994
|
reflectionPrompt: buildReflectionPrompt(ctx.toolState),
|
|
110939
110995
|
// a resume needs the thread codex recorded. without one there is nothing
|
|
110940
|
-
// to continue, and `resume --last`
|
|
110941
|
-
//
|
|
110942
|
-
// runner with a shared CODEX_HOME is somebody else's run.
|
|
110996
|
+
// to continue, and `resume --last` could pick another run's session off CI,
|
|
110997
|
+
// where CODEX_HOME is the shared ~/.local/share/codex (in CI it is per run).
|
|
110943
110998
|
canResume: (r) => r.threadId !== void 0,
|
|
110944
110999
|
resume: async (c) => runCodex({
|
|
110945
111000
|
...runnerArgs,
|
|
@@ -110966,7 +111021,7 @@ var codex = agent({
|
|
|
110966
111021
|
import { spawn as nodeSpawn2 } from "node:child_process";
|
|
110967
111022
|
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync6 } from "node:fs";
|
|
110968
111023
|
import { totalmem } from "node:os";
|
|
110969
|
-
import { join as
|
|
111024
|
+
import { join as join8 } from "node:path";
|
|
110970
111025
|
import { performance as performance7 } from "node:perf_hooks";
|
|
110971
111026
|
|
|
110972
111027
|
// node_modules/.pnpm/@opencode-ai+sdk@1.18.29/node_modules/@opencode-ai/sdk/dist/v2/gen/core/serverSentEvents.gen.js
|
|
@@ -116375,200 +116430,27 @@ var import_cross_spawn = __toESM(require_cross_spawn(), 1);
|
|
|
116375
116430
|
// agents/opencode.ts
|
|
116376
116431
|
var import_undici = __toESM(require_undici2(), 1);
|
|
116377
116432
|
|
|
116378
|
-
// utils/worktree.ts
|
|
116379
|
-
async function dirtyTrackedPaths(params = {}) {
|
|
116380
|
-
const result = await spawn({
|
|
116381
|
-
cmd: "git",
|
|
116382
|
-
args: ["diff", "--name-only", "HEAD"],
|
|
116383
|
-
env: process.env,
|
|
116384
|
-
cwd: params.cwd ?? process.cwd(),
|
|
116385
|
-
activityTimeout: 0
|
|
116386
|
-
});
|
|
116387
|
-
if (result.exitCode !== 0) {
|
|
116388
|
-
throw new Error(
|
|
116389
|
-
`git diff --name-only HEAD failed (exit ${result.exitCode}): ${result.stderr.trim() || "(no stderr)"}`
|
|
116390
|
-
);
|
|
116391
|
-
}
|
|
116392
|
-
return new Set(result.stdout.split("\n").filter(Boolean));
|
|
116393
|
-
}
|
|
116394
|
-
async function restoreDirtiedSince(params) {
|
|
116395
|
-
const dirtied = [...await dirtyTrackedPaths({ cwd: params.cwd ?? process.cwd() })].filter(
|
|
116396
|
-
(path4) => !params.before.has(path4)
|
|
116397
|
-
);
|
|
116398
|
-
if (dirtied.length === 0) return;
|
|
116399
|
-
const result = await spawn({
|
|
116400
|
-
cmd: "git",
|
|
116401
|
-
args: ["restore", "--staged", "--worktree", "--", ...dirtied],
|
|
116402
|
-
env: process.env,
|
|
116403
|
-
cwd: params.cwd ?? process.cwd(),
|
|
116404
|
-
activityTimeout: 0
|
|
116405
|
-
});
|
|
116406
|
-
if (result.exitCode !== 0) {
|
|
116407
|
-
log.warning(
|
|
116408
|
-
`\xBB failed to restore ${dirtied.length} tracked file(s) modified by ${params.actor}: ${result.stderr.trim() || "(no stderr)"}`
|
|
116409
|
-
);
|
|
116410
|
-
return;
|
|
116411
|
-
}
|
|
116412
|
-
log.info(
|
|
116413
|
-
`\xBB restored ${dirtied.length} tracked file(s) modified by ${params.actor}: ${dirtied.join(", ")}`
|
|
116414
|
-
);
|
|
116415
|
-
}
|
|
116416
|
-
|
|
116417
|
-
// agents/opencodePlugin.ts
|
|
116418
|
-
var PULLFROG_BUS_EVENT_TYPE = "pullfrog_bus_event";
|
|
116419
|
-
var PULLFROG_OPENCODE_GATE_PLUGIN_FILENAME = "pullfrog-subagent-gate.ts";
|
|
116420
|
-
var PULLFROG_OPENCODE_PLUGIN_SOURCE = `// AUTOGENERATED by Pullfrog. do not edit; it'll be overwritten on the next run.
|
|
116421
|
-
// surfaces opencode subagent activity that the CLI's run-loop discards. see
|
|
116422
|
-
// action/agents/opencodePlugin.ts in pullfrog/app for why this exists. lives
|
|
116423
|
-
// inside the per-run tmpdir (XDG_CONFIG_HOME/opencode/plugin/), never inside
|
|
116424
|
-
// the user's working tree. the subagent gate ships as a separate plugin
|
|
116425
|
-
// (pullfrog-subagent-gate.ts).
|
|
116426
|
-
|
|
116427
|
-
const PULLFROG_BUS_EVENT_TYPE = ${JSON.stringify(PULLFROG_BUS_EVENT_TYPE)};
|
|
116428
|
-
|
|
116429
|
-
// orchestratorSessionID locks to the first sessionID we observe on the bus
|
|
116430
|
-
// (\`message.part.updated\`). opencode's run command creates exactly one
|
|
116431
|
-
// top-level session before any subagent is dispatched, and the orchestrator
|
|
116432
|
-
// emits at least one event before any subagent does \u2014 so first-seen-wins
|
|
116433
|
-
// captures the orchestrator; every subagent part has a non-matching
|
|
116434
|
-
// sessionID.
|
|
116435
|
-
let orchestratorSessionID: string | undefined;
|
|
116436
|
-
|
|
116437
|
-
function isOrchestratorTaskDispatch(part: {
|
|
116438
|
-
type?: string;
|
|
116439
|
-
tool?: string;
|
|
116440
|
-
state?: { status?: string };
|
|
116441
|
-
} | undefined): boolean {
|
|
116442
|
-
if (!part || part.type !== "tool") return false;
|
|
116443
|
-
if (part.tool !== "task") return false;
|
|
116444
|
-
// only forward at status="running" (not "pending"). at pending the
|
|
116445
|
-
// state.input is still {} \u2014 the orchestrator has emitted the part shell
|
|
116446
|
-
// but the LLM hasn't filled in description/subagent_type/prompt yet. by
|
|
116447
|
-
// running, input is populated and recordTaskDispatch can derive the lens
|
|
116448
|
-
// label correctly.
|
|
116449
|
-
return part.state?.status === "running";
|
|
116450
|
-
}
|
|
116451
|
-
|
|
116452
|
-
export default async function pullfrogEventsPlugin() {
|
|
116453
|
-
return {
|
|
116454
|
-
event: async (input: {
|
|
116455
|
-
event: {
|
|
116456
|
-
type: string;
|
|
116457
|
-
properties?: {
|
|
116458
|
-
part?: {
|
|
116459
|
-
sessionID?: string;
|
|
116460
|
-
type?: string;
|
|
116461
|
-
tool?: string;
|
|
116462
|
-
state?: { status?: string };
|
|
116463
|
-
};
|
|
116464
|
-
};
|
|
116465
|
-
};
|
|
116466
|
-
}) => {
|
|
116467
|
-
const event = input?.event;
|
|
116468
|
-
if (!event || typeof event !== "object") return;
|
|
116469
|
-
if (event.type !== "message.part.updated") return;
|
|
116470
|
-
const part = event.properties?.part;
|
|
116471
|
-
const sessionID = part?.sessionID;
|
|
116472
|
-
if (typeof sessionID !== "string" || sessionID.length === 0) return;
|
|
116473
|
-
if (orchestratorSessionID === undefined) orchestratorSessionID = sessionID;
|
|
116474
|
-
|
|
116475
|
-
if (sessionID === orchestratorSessionID) {
|
|
116476
|
-
// skip orchestrator events EXCEPT early task dispatches.
|
|
116477
|
-
if (!isOrchestratorTaskDispatch(part)) return;
|
|
116478
|
-
}
|
|
116479
|
-
|
|
116480
|
-
try {
|
|
116481
|
-
const line = JSON.stringify({
|
|
116482
|
-
type: PULLFROG_BUS_EVENT_TYPE,
|
|
116483
|
-
bus_event: event,
|
|
116484
|
-
});
|
|
116485
|
-
process.stdout.write(line + "\\n");
|
|
116486
|
-
} catch {
|
|
116487
|
-
// a circular reference or BigInt etc. would throw; swallow rather
|
|
116488
|
-
// than letting a single bad event take down the plugin.
|
|
116489
|
-
}
|
|
116490
|
-
},
|
|
116491
|
-
};
|
|
116492
|
-
}
|
|
116493
|
-
`;
|
|
116494
|
-
function buildOpencodeSubagentGateSource(deniedTools) {
|
|
116495
|
-
return `// AUTOGENERATED by Pullfrog. do not edit; it'll be overwritten on the next run.
|
|
116496
|
-
// hard-blocks state-mutating MCP tool calls from opencode subagents (PR
|
|
116497
|
-
// following pullfrog/app#796 \u2014 see action/agents/subagentToolGates.ts). see
|
|
116498
|
-
// action/agents/opencodePlugin.ts in pullfrog/app for why this exists. lives
|
|
116499
|
-
// inside the per-run tmpdir (XDG_CONFIG_HOME/opencode/plugin/), never inside
|
|
116500
|
-
// the user's working tree.
|
|
116501
|
-
|
|
116502
|
-
// embedded at template-render time so the plugin source has no imports. the
|
|
116503
|
-
// denied set is derived from each tool's \`mutates\` flag \u2014 see
|
|
116504
|
-
// action/agents/subagentToolGates.ts.
|
|
116505
|
-
const SUBAGENT_DENIED_TOOLS = new Set(${JSON.stringify(deniedTools)});
|
|
116506
|
-
|
|
116507
|
-
// opencode presents MCP tools as \`pullfrog_<bare>\`; strip that to compare
|
|
116508
|
-
// against the canonical bare names in SUBAGENT_DENIED_TOOLS. native tools
|
|
116509
|
-
// (read, write, bash, task, etc.) lack the prefix and never match the deny
|
|
116510
|
-
// list anyway.
|
|
116511
|
-
function stripPullfrogPrefix(toolName: string): string {
|
|
116512
|
-
if (toolName.startsWith("pullfrog_")) return toolName.slice("pullfrog_".length);
|
|
116513
|
-
return toolName;
|
|
116514
|
-
}
|
|
116515
|
-
|
|
116516
|
-
// orchestratorSessionID locks to the first sessionID we observe on
|
|
116517
|
-
// \`tool.execute.before\`. the orchestrator must invoke a tool (e.g. its
|
|
116518
|
-
// \`task\` dispatch) before any subagent session exists, so first-seen-wins
|
|
116519
|
-
// always captures the orchestrator; every subagent call therefore has a
|
|
116520
|
-
// non-matching sessionID and is subject to the deny check.
|
|
116521
|
-
let orchestratorSessionID: string | undefined;
|
|
116522
|
-
|
|
116523
|
-
export default async function pullfrogSubagentGatePlugin() {
|
|
116524
|
-
return {
|
|
116525
|
-
"tool.execute.before": async (
|
|
116526
|
-
input: { tool?: string; sessionID?: string; callID?: string },
|
|
116527
|
-
_output: { args?: Record<string, unknown> }
|
|
116528
|
-
) => {
|
|
116529
|
-
// input: { tool, sessionID, callID } \u2014 confirmed against
|
|
116530
|
-
// anomalyco/opencode packages/opencode/src/session/tools.ts which
|
|
116531
|
-
// calls plugin.trigger("tool.execute.before", { tool, sessionID,
|
|
116532
|
-
// callID }, { args }). throwing here surfaces as a tool error to the
|
|
116533
|
-
// model (Effect's run.promise propagates the rejection through the
|
|
116534
|
-
// AI SDK's tool-runtime), so the subagent sees a denial and can
|
|
116535
|
-
// pick a different action.
|
|
116536
|
-
const sessionID = typeof input?.sessionID === "string" ? input.sessionID : "";
|
|
116537
|
-
const tool = typeof input?.tool === "string" ? input.tool : "";
|
|
116538
|
-
// above the sessionID check: the bash block must never depend on a
|
|
116539
|
-
// session id being present.
|
|
116540
|
-
if (tool === "bash") {
|
|
116541
|
-
throw new Error("the native bash tool is disabled in Pullfrog runs.");
|
|
116542
|
-
}
|
|
116543
|
-
if (!sessionID || !tool) return;
|
|
116544
|
-
if (orchestratorSessionID === undefined) orchestratorSessionID = sessionID;
|
|
116545
|
-
if (sessionID === orchestratorSessionID) return;
|
|
116546
|
-
const bare = stripPullfrogPrefix(tool);
|
|
116547
|
-
if (!SUBAGENT_DENIED_TOOLS.has(bare)) return;
|
|
116548
|
-
throw new Error(
|
|
116549
|
-
"subagent attempted to call denied tool '" + bare + "'. " +
|
|
116550
|
-
"subagents share the orchestrator's in-process working tree and toolState; " +
|
|
116551
|
-
"state-changing MCP tools (checkout_pr, push_branch, create_pull_request_review, " +
|
|
116552
|
-
"report_progress, etc.) are reserved for the orchestrator. " +
|
|
116553
|
-
"report findings back to the orchestrator and let it perform the mutation."
|
|
116554
|
-
);
|
|
116555
|
-
},
|
|
116556
|
-
};
|
|
116557
|
-
}
|
|
116558
|
-
`;
|
|
116559
|
-
}
|
|
116560
|
-
|
|
116561
116433
|
// utils/openCodeModels.ts
|
|
116562
116434
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
116435
|
+
import { join as join7 } from "node:path";
|
|
116563
116436
|
var baseline;
|
|
116564
116437
|
var authorized;
|
|
116565
116438
|
var failure;
|
|
116566
116439
|
var ANSI_PATTERN = /\[[0-9;]*m/g;
|
|
116567
|
-
function
|
|
116568
|
-
|
|
116440
|
+
function openCodeHomeEnv(tmpdir3) {
|
|
116441
|
+
return {
|
|
116442
|
+
HOME: tmpdir3,
|
|
116443
|
+
XDG_CONFIG_HOME: join7(tmpdir3, ".config"),
|
|
116444
|
+
XDG_DATA_HOME: getSubscriptionDataHome() ?? join7(tmpdir3, ".local", "share"),
|
|
116445
|
+
XDG_CACHE_HOME: join7(tmpdir3, ".cache"),
|
|
116446
|
+
XDG_STATE_HOME: join7(tmpdir3, ".local", "state")
|
|
116447
|
+
};
|
|
116448
|
+
}
|
|
116449
|
+
function readModels(params) {
|
|
116450
|
+
const result = spawnSync3(params.cliPath, ["models"], {
|
|
116569
116451
|
encoding: "utf-8",
|
|
116570
116452
|
timeout: 3e4,
|
|
116571
|
-
env: process.env,
|
|
116453
|
+
env: { ...process.env, ...openCodeHomeEnv(params.tmpdir) },
|
|
116572
116454
|
stdio: ["ignore", "pipe", "pipe"]
|
|
116573
116455
|
});
|
|
116574
116456
|
if (result.status !== 0) {
|
|
@@ -116585,12 +116467,12 @@ function readModels(cliPath) {
|
|
|
116585
116467
|
function getModelsFailure() {
|
|
116586
116468
|
return failure;
|
|
116587
116469
|
}
|
|
116588
|
-
function captureBaselineModels(
|
|
116589
|
-
baseline = readModels(
|
|
116470
|
+
function captureBaselineModels(params) {
|
|
116471
|
+
baseline = readModels(params);
|
|
116590
116472
|
log.debug(`\xBB opencode baseline: ${baseline.size} models`);
|
|
116591
116473
|
}
|
|
116592
|
-
function captureAuthorizedModels(
|
|
116593
|
-
authorized = readModels(
|
|
116474
|
+
function captureAuthorizedModels(params) {
|
|
116475
|
+
authorized = readModels(params);
|
|
116594
116476
|
const base = baseline;
|
|
116595
116477
|
if (base) {
|
|
116596
116478
|
const diff = [...authorized].filter((m) => !base.has(m));
|
|
@@ -116610,6 +116492,190 @@ function getAuthorizedModels() {
|
|
|
116610
116492
|
return authorized;
|
|
116611
116493
|
}
|
|
116612
116494
|
|
|
116495
|
+
// utils/worktree.ts
|
|
116496
|
+
async function dirtyTrackedPaths(params = {}) {
|
|
116497
|
+
const result = await spawn({
|
|
116498
|
+
cmd: "git",
|
|
116499
|
+
args: ["diff", "--name-only", "HEAD"],
|
|
116500
|
+
env: process.env,
|
|
116501
|
+
cwd: params.cwd ?? process.cwd(),
|
|
116502
|
+
activityTimeout: 0
|
|
116503
|
+
});
|
|
116504
|
+
if (result.exitCode !== 0) {
|
|
116505
|
+
throw new Error(
|
|
116506
|
+
`git diff --name-only HEAD failed (exit ${result.exitCode}): ${result.stderr.trim() || "(no stderr)"}`
|
|
116507
|
+
);
|
|
116508
|
+
}
|
|
116509
|
+
return new Set(result.stdout.split("\n").filter(Boolean));
|
|
116510
|
+
}
|
|
116511
|
+
async function restoreDirtiedSince(params) {
|
|
116512
|
+
const dirtied = [...await dirtyTrackedPaths({ cwd: params.cwd ?? process.cwd() })].filter(
|
|
116513
|
+
(path4) => !params.before.has(path4)
|
|
116514
|
+
);
|
|
116515
|
+
if (dirtied.length === 0) return;
|
|
116516
|
+
const result = await spawn({
|
|
116517
|
+
cmd: "git",
|
|
116518
|
+
args: ["restore", "--staged", "--worktree", "--", ...dirtied],
|
|
116519
|
+
env: process.env,
|
|
116520
|
+
cwd: params.cwd ?? process.cwd(),
|
|
116521
|
+
activityTimeout: 0
|
|
116522
|
+
});
|
|
116523
|
+
if (result.exitCode !== 0) {
|
|
116524
|
+
log.warning(
|
|
116525
|
+
`\xBB failed to restore ${dirtied.length} tracked file(s) modified by ${params.actor}: ${result.stderr.trim() || "(no stderr)"}`
|
|
116526
|
+
);
|
|
116527
|
+
return;
|
|
116528
|
+
}
|
|
116529
|
+
log.info(
|
|
116530
|
+
`\xBB restored ${dirtied.length} tracked file(s) modified by ${params.actor}: ${dirtied.join(", ")}`
|
|
116531
|
+
);
|
|
116532
|
+
}
|
|
116533
|
+
|
|
116534
|
+
// agents/opencodePlugin.ts
|
|
116535
|
+
var PULLFROG_BUS_EVENT_TYPE = "pullfrog_bus_event";
|
|
116536
|
+
var PULLFROG_OPENCODE_GATE_PLUGIN_FILENAME = "pullfrog-subagent-gate.ts";
|
|
116537
|
+
var PULLFROG_OPENCODE_PLUGIN_SOURCE = `// AUTOGENERATED by Pullfrog. do not edit; it'll be overwritten on the next run.
|
|
116538
|
+
// surfaces opencode subagent activity that the CLI's run-loop discards. see
|
|
116539
|
+
// action/agents/opencodePlugin.ts in pullfrog/app for why this exists. lives
|
|
116540
|
+
// inside the per-run tmpdir (XDG_CONFIG_HOME/opencode/plugin/), never inside
|
|
116541
|
+
// the user's working tree. the subagent gate ships as a separate plugin
|
|
116542
|
+
// (pullfrog-subagent-gate.ts).
|
|
116543
|
+
|
|
116544
|
+
const PULLFROG_BUS_EVENT_TYPE = ${JSON.stringify(PULLFROG_BUS_EVENT_TYPE)};
|
|
116545
|
+
|
|
116546
|
+
// orchestratorSessionID locks to the first sessionID we observe on the bus
|
|
116547
|
+
// (\`message.part.updated\`). opencode's run command creates exactly one
|
|
116548
|
+
// top-level session before any subagent is dispatched, and the orchestrator
|
|
116549
|
+
// emits at least one event before any subagent does \u2014 so first-seen-wins
|
|
116550
|
+
// captures the orchestrator; every subagent part has a non-matching
|
|
116551
|
+
// sessionID.
|
|
116552
|
+
let orchestratorSessionID: string | undefined;
|
|
116553
|
+
|
|
116554
|
+
function isOrchestratorTaskDispatch(part: {
|
|
116555
|
+
type?: string;
|
|
116556
|
+
tool?: string;
|
|
116557
|
+
state?: { status?: string };
|
|
116558
|
+
} | undefined): boolean {
|
|
116559
|
+
if (!part || part.type !== "tool") return false;
|
|
116560
|
+
if (part.tool !== "task") return false;
|
|
116561
|
+
// only forward at status="running" (not "pending"). at pending the
|
|
116562
|
+
// state.input is still {} \u2014 the orchestrator has emitted the part shell
|
|
116563
|
+
// but the LLM hasn't filled in description/subagent_type/prompt yet. by
|
|
116564
|
+
// running, input is populated and recordTaskDispatch can derive the lens
|
|
116565
|
+
// label correctly.
|
|
116566
|
+
return part.state?.status === "running";
|
|
116567
|
+
}
|
|
116568
|
+
|
|
116569
|
+
export default async function pullfrogEventsPlugin() {
|
|
116570
|
+
return {
|
|
116571
|
+
event: async (input: {
|
|
116572
|
+
event: {
|
|
116573
|
+
type: string;
|
|
116574
|
+
properties?: {
|
|
116575
|
+
part?: {
|
|
116576
|
+
sessionID?: string;
|
|
116577
|
+
type?: string;
|
|
116578
|
+
tool?: string;
|
|
116579
|
+
state?: { status?: string };
|
|
116580
|
+
};
|
|
116581
|
+
};
|
|
116582
|
+
};
|
|
116583
|
+
}) => {
|
|
116584
|
+
const event = input?.event;
|
|
116585
|
+
if (!event || typeof event !== "object") return;
|
|
116586
|
+
if (event.type !== "message.part.updated") return;
|
|
116587
|
+
const part = event.properties?.part;
|
|
116588
|
+
const sessionID = part?.sessionID;
|
|
116589
|
+
if (typeof sessionID !== "string" || sessionID.length === 0) return;
|
|
116590
|
+
if (orchestratorSessionID === undefined) orchestratorSessionID = sessionID;
|
|
116591
|
+
|
|
116592
|
+
if (sessionID === orchestratorSessionID) {
|
|
116593
|
+
// skip orchestrator events EXCEPT early task dispatches.
|
|
116594
|
+
if (!isOrchestratorTaskDispatch(part)) return;
|
|
116595
|
+
}
|
|
116596
|
+
|
|
116597
|
+
try {
|
|
116598
|
+
const line = JSON.stringify({
|
|
116599
|
+
type: PULLFROG_BUS_EVENT_TYPE,
|
|
116600
|
+
bus_event: event,
|
|
116601
|
+
});
|
|
116602
|
+
process.stdout.write(line + "\\n");
|
|
116603
|
+
} catch {
|
|
116604
|
+
// a circular reference or BigInt etc. would throw; swallow rather
|
|
116605
|
+
// than letting a single bad event take down the plugin.
|
|
116606
|
+
}
|
|
116607
|
+
},
|
|
116608
|
+
};
|
|
116609
|
+
}
|
|
116610
|
+
`;
|
|
116611
|
+
function buildOpencodeSubagentGateSource(ctx) {
|
|
116612
|
+
const bashRefusal = ctx.payload.shell === "disabled" ? "shell execution is disabled for this run and no shell tool exists \u2014 use read, grep and glob instead." : `native bash is disabled in Pullfrog runs \u2014 call ${formatMcpToolRef("opencode", "shell")} with the same command instead.`;
|
|
116613
|
+
return `// AUTOGENERATED by Pullfrog. do not edit; it'll be overwritten on the next run.
|
|
116614
|
+
// hard-blocks state-mutating MCP tool calls from opencode subagents (PR
|
|
116615
|
+
// following pullfrog/app#796 \u2014 see action/agents/subagentToolGates.ts). see
|
|
116616
|
+
// action/agents/opencodePlugin.ts in pullfrog/app for why this exists. lives
|
|
116617
|
+
// inside the per-run tmpdir (XDG_CONFIG_HOME/opencode/plugin/), never inside
|
|
116618
|
+
// the user's working tree.
|
|
116619
|
+
|
|
116620
|
+
// embedded at template-render time so the plugin source has no imports. the
|
|
116621
|
+
// denied set is derived from each tool's \`mutates\` flag \u2014 see
|
|
116622
|
+
// action/agents/subagentToolGates.ts.
|
|
116623
|
+
const SUBAGENT_DENIED_TOOLS = new Set(${JSON.stringify(ctx.subagentDeniedTools)});
|
|
116624
|
+
|
|
116625
|
+
// opencode presents MCP tools as \`pullfrog_<bare>\`; strip that to compare
|
|
116626
|
+
// against the canonical bare names in SUBAGENT_DENIED_TOOLS. native tools
|
|
116627
|
+
// (read, write, bash, task, etc.) lack the prefix and never match the deny
|
|
116628
|
+
// list anyway.
|
|
116629
|
+
function stripPullfrogPrefix(toolName: string): string {
|
|
116630
|
+
if (toolName.startsWith("pullfrog_")) return toolName.slice("pullfrog_".length);
|
|
116631
|
+
return toolName;
|
|
116632
|
+
}
|
|
116633
|
+
|
|
116634
|
+
// orchestratorSessionID locks to the first sessionID we observe on
|
|
116635
|
+
// \`tool.execute.before\`. the orchestrator must invoke a tool (e.g. its
|
|
116636
|
+
// \`task\` dispatch) before any subagent session exists, so first-seen-wins
|
|
116637
|
+
// always captures the orchestrator; every subagent call therefore has a
|
|
116638
|
+
// non-matching sessionID and is subject to the deny check.
|
|
116639
|
+
let orchestratorSessionID: string | undefined;
|
|
116640
|
+
|
|
116641
|
+
export default async function pullfrogSubagentGatePlugin() {
|
|
116642
|
+
return {
|
|
116643
|
+
"tool.execute.before": async (
|
|
116644
|
+
input: { tool?: string; sessionID?: string; callID?: string },
|
|
116645
|
+
_output: { args?: Record<string, unknown> }
|
|
116646
|
+
) => {
|
|
116647
|
+
// input: { tool, sessionID, callID } \u2014 confirmed against
|
|
116648
|
+
// anomalyco/opencode packages/opencode/src/session/tools.ts which
|
|
116649
|
+
// calls plugin.trigger("tool.execute.before", { tool, sessionID,
|
|
116650
|
+
// callID }, { args }). throwing here surfaces as a tool error to the
|
|
116651
|
+
// model (Effect's run.promise propagates the rejection through the
|
|
116652
|
+
// AI SDK's tool-runtime), so the subagent sees a denial and can
|
|
116653
|
+
// pick a different action.
|
|
116654
|
+
const sessionID = typeof input?.sessionID === "string" ? input.sessionID : "";
|
|
116655
|
+
const tool = typeof input?.tool === "string" ? input.tool : "";
|
|
116656
|
+
// above the sessionID check: the bash block must never depend on a
|
|
116657
|
+
// session id being present.
|
|
116658
|
+
if (tool === "bash") {
|
|
116659
|
+
throw new Error(${JSON.stringify(bashRefusal)});
|
|
116660
|
+
}
|
|
116661
|
+
if (!sessionID || !tool) return;
|
|
116662
|
+
if (orchestratorSessionID === undefined) orchestratorSessionID = sessionID;
|
|
116663
|
+
if (sessionID === orchestratorSessionID) return;
|
|
116664
|
+
const bare = stripPullfrogPrefix(tool);
|
|
116665
|
+
if (!SUBAGENT_DENIED_TOOLS.has(bare)) return;
|
|
116666
|
+
throw new Error(
|
|
116667
|
+
"subagent attempted to call denied tool '" + bare + "'. " +
|
|
116668
|
+
"subagents share the orchestrator's in-process working tree and toolState; " +
|
|
116669
|
+
"state-changing MCP tools (checkout_pr, push_branch, create_pull_request_review, " +
|
|
116670
|
+
"report_progress, etc.) are reserved for the orchestrator. " +
|
|
116671
|
+
"report findings back to the orchestrator and let it perform the mutation."
|
|
116672
|
+
);
|
|
116673
|
+
},
|
|
116674
|
+
};
|
|
116675
|
+
}
|
|
116676
|
+
`;
|
|
116677
|
+
}
|
|
116678
|
+
|
|
116613
116679
|
// agents/subagentModels.ts
|
|
116614
116680
|
function deriveSubagentModels(orchestratorSpec) {
|
|
116615
116681
|
if (!orchestratorSpec) return { reviewer: void 0 };
|
|
@@ -116984,6 +117050,7 @@ async function dispatchEvent(ctx, event) {
|
|
|
116984
117050
|
const err = event.properties.error;
|
|
116985
117051
|
const message = err ? extractErrorMessage(err) : "(no error payload)";
|
|
116986
117052
|
log.info(`\xBB ${ctx.label} session error: ${message}`);
|
|
117053
|
+
if (err) ctx.lastSessionError = message;
|
|
116987
117054
|
return;
|
|
116988
117055
|
}
|
|
116989
117056
|
}
|
|
@@ -117122,6 +117189,7 @@ async function runPromptTurn(ctx, params) {
|
|
|
117122
117189
|
ctx.promptMessageID = void 0;
|
|
117123
117190
|
ctx.diagnostic.sawModelOutput = false;
|
|
117124
117191
|
ctx.diagnostic.idleSec = void 0;
|
|
117192
|
+
ctx.lastSessionError = void 0;
|
|
117125
117193
|
let assistant;
|
|
117126
117194
|
let returnedParts;
|
|
117127
117195
|
let networkError = null;
|
|
@@ -117166,11 +117234,12 @@ async function runPromptTurn(ctx, params) {
|
|
|
117166
117234
|
}
|
|
117167
117235
|
const cause = ctx.serverExitCause() ?? ctx.diagnostic.lastProviderError;
|
|
117168
117236
|
const diagnosis = cause ? ` \u2014 likely cause: ${cause}` : "";
|
|
117237
|
+
const promptCause = ctx.serverExitCause() ?? (ctx.diagnostic.sawModelOutput ? void 0 : ctx.lastSessionError) ?? ctx.diagnostic.lastProviderError;
|
|
117169
117238
|
if (networkError) {
|
|
117170
117239
|
return {
|
|
117171
117240
|
success: false,
|
|
117172
117241
|
output: finalText,
|
|
117173
|
-
error: params.signal.aborted ? `activity timeout: the model went silent and the turn was aborted by the activity watchdog (${networkError})${diagnosis}` : `opencode prompt failed: ${networkError}${
|
|
117242
|
+
error: params.signal.aborted ? `activity timeout: the model went silent and the turn was aborted by the activity watchdog (${networkError})${diagnosis}` : `opencode prompt failed: ${networkError}${promptCause ? ` \u2014 likely cause: ${promptCause}` : ""}`,
|
|
117174
117243
|
usage
|
|
117175
117244
|
};
|
|
117176
117245
|
}
|
|
@@ -117362,15 +117431,14 @@ var opencode = agent({
|
|
|
117362
117431
|
const isBedrockRoute = rawModel !== void 0 && bedrockModelId !== void 0 && bedrockModelId === rawModel;
|
|
117363
117432
|
const vertexModel = resolveVertexOpenCodeModel(rawModel);
|
|
117364
117433
|
const model = vertexModel ?? (isBedrockRoute ? `amazon-bedrock/${rawModel}` : rawModel);
|
|
117365
|
-
const
|
|
117366
|
-
|
|
117367
|
-
|
|
117368
|
-
|
|
117369
|
-
const opencodePluginDir = join7(homeEnv.XDG_CONFIG_HOME, "opencode", "plugin");
|
|
117434
|
+
const codexAuth = installCodexAuth();
|
|
117435
|
+
const xaiAuth = installXaiAuth();
|
|
117436
|
+
const homeEnv = openCodeHomeEnv(ctx.tmpdir);
|
|
117437
|
+
const opencodePluginDir = join8(homeEnv.XDG_CONFIG_HOME, "opencode", "plugin");
|
|
117370
117438
|
mkdirSync7(opencodePluginDir, { recursive: true });
|
|
117371
117439
|
writeFileSync6(
|
|
117372
|
-
|
|
117373
|
-
buildOpencodeSubagentGateSource(ctx
|
|
117440
|
+
join8(opencodePluginDir, PULLFROG_OPENCODE_GATE_PLUGIN_FILENAME),
|
|
117441
|
+
buildOpencodeSubagentGateSource(ctx)
|
|
117374
117442
|
);
|
|
117375
117443
|
const agentBrowserVersion = getDevDependencyVersion("agent-browser");
|
|
117376
117444
|
addSkill({
|
|
@@ -117380,8 +117448,6 @@ var opencode = agent({
|
|
|
117380
117448
|
agent: "opencode"
|
|
117381
117449
|
});
|
|
117382
117450
|
installBundledSkills({ home: homeEnv.HOME });
|
|
117383
|
-
const codexAuth = installCodexAuth();
|
|
117384
|
-
const xaiAuth = installXaiAuth();
|
|
117385
117451
|
const permissionOverride = JSON.stringify({
|
|
117386
117452
|
external_directory: { "*": "deny", "/tmp/*": "allow" },
|
|
117387
117453
|
read: { "*": "allow", ...GIT_NATIVE_READ_DENY_OPENCODE },
|
|
@@ -117400,7 +117466,6 @@ var opencode = agent({
|
|
|
117400
117466
|
delete env2.OPENCODE_EXPERIMENTAL_CODE_MODE;
|
|
117401
117467
|
const writebacks = [];
|
|
117402
117468
|
if (codexAuth) {
|
|
117403
|
-
env2.XDG_DATA_HOME = codexAuth.xdgDataHome;
|
|
117404
117469
|
delete env2.OPENAI_API_KEY;
|
|
117405
117470
|
writebacks.push({
|
|
117406
117471
|
secretName: "CODEX_AUTH_JSON",
|
|
@@ -117411,7 +117476,6 @@ var opencode = agent({
|
|
|
117411
117476
|
});
|
|
117412
117477
|
}
|
|
117413
117478
|
if (xaiAuth) {
|
|
117414
|
-
env2.XDG_DATA_HOME = xaiAuth.xdgDataHome;
|
|
117415
117479
|
delete env2.XAI_API_KEY;
|
|
117416
117480
|
writebacks.push({
|
|
117417
117481
|
secretName: "GROK_AUTH_JSON",
|
|
@@ -117484,6 +117548,7 @@ var opencode = agent({
|
|
|
117484
117548
|
mcpToolCalls: 0,
|
|
117485
117549
|
recentStderr: server.recentStderr,
|
|
117486
117550
|
serverExitCause: server.exitCause,
|
|
117551
|
+
lastSessionError: void 0,
|
|
117487
117552
|
diagnostic: {
|
|
117488
117553
|
label: "Pullfrog",
|
|
117489
117554
|
recentStderr: server.recentStderr,
|
|
@@ -172402,7 +172467,7 @@ function closeBrowserDaemon(toolState) {
|
|
|
172402
172467
|
// mcp/checkout.ts
|
|
172403
172468
|
import { createHash as createHash2 } from "node:crypto";
|
|
172404
172469
|
import { statSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync10 } from "node:fs";
|
|
172405
|
-
import { join as
|
|
172470
|
+
import { join as join16 } from "node:path";
|
|
172406
172471
|
|
|
172407
172472
|
// node_modules/.pnpm/@ark+schema@0.56.0/node_modules/@ark/schema/out/shared/errors.js
|
|
172408
172473
|
var ArkError = class _ArkError extends CastableBase {
|
|
@@ -180773,13 +180838,14 @@ function ensureRepoState(toolState, init) {
|
|
|
180773
180838
|
// utils/changeImpact.ts
|
|
180774
180839
|
import { spawn as spawn2 } from "node:child_process";
|
|
180775
180840
|
import { writeFileSync as writeFileSync7 } from "node:fs";
|
|
180776
|
-
import { join as
|
|
180841
|
+
import { join as join9 } from "node:path";
|
|
180777
180842
|
var MAX_CANDIDATES = 24;
|
|
180778
180843
|
var MAX_ATOM_LENGTH = 128;
|
|
180779
180844
|
var MAX_ATOMS = 12;
|
|
180780
180845
|
var MAX_REFERENCES = 6;
|
|
180781
180846
|
var MAX_EXCERPT = 160;
|
|
180782
180847
|
var MAX_BYTES = 16e3;
|
|
180848
|
+
var LOOKUP_BUDGET_MS = 6e4;
|
|
180783
180849
|
function isSymbolShaped(atom) {
|
|
180784
180850
|
return atom.length >= 4 && atom.length <= MAX_ATOM_LENGTH && /[A-Z_]/.test(atom);
|
|
180785
180851
|
}
|
|
@@ -180982,7 +181048,7 @@ function consumeGrepChunk(params) {
|
|
|
180982
181048
|
async function lookupReferences(params) {
|
|
180983
181049
|
const buckets = emptyBuckets(params.candidates);
|
|
180984
181050
|
if (params.candidates.length === 0) return { buckets, error: void 0 };
|
|
180985
|
-
const
|
|
181051
|
+
const pattern = params.candidates.map((candidate) => candidate.atom).join("|");
|
|
180986
181052
|
const child = spawn2(
|
|
180987
181053
|
"git",
|
|
180988
181054
|
[
|
|
@@ -180993,13 +181059,14 @@ async function lookupReferences(params) {
|
|
|
180993
181059
|
"-z",
|
|
180994
181060
|
"-I",
|
|
180995
181061
|
"-w",
|
|
180996
|
-
"-
|
|
180997
|
-
|
|
181062
|
+
"-E",
|
|
181063
|
+
"-e",
|
|
181064
|
+
pattern,
|
|
180998
181065
|
params.treeish,
|
|
180999
181066
|
"--",
|
|
181000
181067
|
":(top)"
|
|
181001
181068
|
],
|
|
181002
|
-
{ env: resolveEnv(void 0), stdio: ["ignore", "pipe", "pipe"] }
|
|
181069
|
+
{ env: resolveEnv(void 0), stdio: ["ignore", "pipe", "pipe"], signal: params.signal }
|
|
181003
181070
|
);
|
|
181004
181071
|
const state = createGrepRecordState();
|
|
181005
181072
|
const maxAtomLength = Math.max(...params.candidates.map((candidate) => candidate.atom.length));
|
|
@@ -181019,7 +181086,7 @@ async function lookupReferences(params) {
|
|
|
181019
181086
|
child.stderr.resume();
|
|
181020
181087
|
const status = await new Promise((resolve3) => {
|
|
181021
181088
|
child.once("error", (error49) => {
|
|
181022
|
-
spawnError = error49.message;
|
|
181089
|
+
spawnError = error49.cause instanceof Error ? error49.cause.message : error49.message;
|
|
181023
181090
|
resolve3(null);
|
|
181024
181091
|
});
|
|
181025
181092
|
child.once("close", resolve3);
|
|
@@ -181107,8 +181174,10 @@ async function createChangeImpactArtifact(ctx, params) {
|
|
|
181107
181174
|
const lookup2 = await lookupReferences({
|
|
181108
181175
|
candidates: candidates.selected,
|
|
181109
181176
|
addedLines: candidates.addedLines,
|
|
181110
|
-
treeish: params.headSha
|
|
181177
|
+
treeish: params.headSha,
|
|
181178
|
+
signal: AbortSignal.any([params.signal, AbortSignal.timeout(LOOKUP_BUDGET_MS)])
|
|
181111
181179
|
});
|
|
181180
|
+
params.signal.throwIfAborted();
|
|
181112
181181
|
const artifact = formatArtifact({
|
|
181113
181182
|
entries: candidates.selected.map((change) => {
|
|
181114
181183
|
return { change, bucket: lookup2.buckets.get(change.atom) ?? { count: 0, samples: [] } };
|
|
@@ -181120,7 +181189,7 @@ async function createChangeImpactArtifact(ctx, params) {
|
|
|
181120
181189
|
headSha: params.headSha,
|
|
181121
181190
|
lookupError: lookup2.error
|
|
181122
181191
|
});
|
|
181123
|
-
const path4 =
|
|
181192
|
+
const path4 = join9(ctx.tmpdir, `pr-${params.pullNumber}-${params.headSha.slice(0, 7)}-impact.md`);
|
|
181124
181193
|
writeFileSync7(path4, artifact.content);
|
|
181125
181194
|
return {
|
|
181126
181195
|
path: path4,
|
|
@@ -181374,7 +181443,7 @@ function readNumber(params) {
|
|
|
181374
181443
|
import { execSync } from "node:child_process";
|
|
181375
181444
|
import { createHash } from "node:crypto";
|
|
181376
181445
|
import { readFileSync as readFileSync4, realpathSync, unlinkSync } from "node:fs";
|
|
181377
|
-
import { join as
|
|
181446
|
+
import { join as join10 } from "node:path";
|
|
181378
181447
|
|
|
181379
181448
|
// utils/shell.ts
|
|
181380
181449
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
@@ -181464,7 +181533,7 @@ function resolveHooksDir(cwd, gitPath) {
|
|
|
181464
181533
|
cwd,
|
|
181465
181534
|
log: false
|
|
181466
181535
|
}).trim();
|
|
181467
|
-
const hooksDir =
|
|
181536
|
+
const hooksDir = join10(commonDir, "hooks");
|
|
181468
181537
|
hooksDirCache.set(cwd, hooksDir);
|
|
181469
181538
|
return hooksDir;
|
|
181470
181539
|
}
|
|
@@ -181511,7 +181580,8 @@ async function $git(subcommand, args2, options) {
|
|
|
181511
181580
|
GIT_CONFIG_COUNT: "0",
|
|
181512
181581
|
GIT_CONFIG_PARAMETERS: ""
|
|
181513
181582
|
},
|
|
181514
|
-
activityTimeout: 0
|
|
181583
|
+
activityTimeout: 0,
|
|
181584
|
+
signal: options.signal
|
|
181515
181585
|
});
|
|
181516
181586
|
if (result.stderr.includes("askpass-compromised")) {
|
|
181517
181587
|
log.info("askpass code was replayed after revoke \u2014 token has been revoked");
|
|
@@ -181578,9 +181648,10 @@ import { spawnSync as spawnSync7 } from "node:child_process";
|
|
|
181578
181648
|
// mcp/shell.ts
|
|
181579
181649
|
import { spawn as spawn3, spawnSync as spawnSync6 } from "node:child_process";
|
|
181580
181650
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
181581
|
-
import {
|
|
181651
|
+
import { once } from "node:events";
|
|
181652
|
+
import { closeSync, existsSync as existsSync7, openSync, writeFileSync as writeFileSync8 } from "node:fs";
|
|
181582
181653
|
import { userInfo as userInfo2 } from "node:os";
|
|
181583
|
-
import { join as
|
|
181654
|
+
import { join as join11 } from "node:path";
|
|
181584
181655
|
import { setTimeout as sleep3 } from "node:timers/promises";
|
|
181585
181656
|
|
|
181586
181657
|
// node_modules/.pnpm/@toon-format+toon@1.4.0/node_modules/@toon-format/toon/dist/index.mjs
|
|
@@ -182224,7 +182295,7 @@ function buildFsMounts(repoDir) {
|
|
|
182224
182295
|
function shimMount() {
|
|
182225
182296
|
const tempDir = process.env.PULLFROG_TEMP_DIR;
|
|
182226
182297
|
if (!tempDir) return "";
|
|
182227
|
-
const dir =
|
|
182298
|
+
const dir = join11(tempDir, "pm-bin").replace(/'/g, "'\\''");
|
|
182228
182299
|
return `[ -d '${dir}' ] && mount --bind '${dir}' '${dir}' 2>/dev/null && mount -o remount,bind,ro '${dir}' 2>/dev/null;`;
|
|
182229
182300
|
}
|
|
182230
182301
|
var _repoRoot;
|
|
@@ -182395,7 +182466,7 @@ ${spawnError}` : spawnError;
|
|
|
182395
182466
|
}
|
|
182396
182467
|
function capOutput(output) {
|
|
182397
182468
|
if (output.length <= MAX_OUTPUT_CHARS) return output;
|
|
182398
|
-
const fullPath =
|
|
182469
|
+
const fullPath = join11(getTempDir(), `shell-${randomUUID3().slice(0, 8)}.log`);
|
|
182399
182470
|
writeFileSync8(fullPath, output);
|
|
182400
182471
|
const elided = output.length - MAX_OUTPUT_CHARS;
|
|
182401
182472
|
return `... [${elided} chars truncated; output saved to ${fullPath}] ...
|
|
@@ -182432,6 +182503,7 @@ Do NOT use this tool for git commands \u2014 use the dedicated git tools instead
|
|
|
182432
182503
|
}
|
|
182433
182504
|
const timeout = Math.min(params.timeout ?? 3e4, 12e4);
|
|
182434
182505
|
const cwd = params.working_directory ?? process.cwd();
|
|
182506
|
+
if (!existsSync7(cwd)) throw new Error(`working_directory does not exist: ${cwd}`);
|
|
182435
182507
|
const env2 = resolveEnv(ctx.payload.shell === "enabled" ? "inherit" : "restricted");
|
|
182436
182508
|
if (params.command.includes("agent-browser")) {
|
|
182437
182509
|
const daemonError = ensureBrowserDaemon(ctx.toolState);
|
|
@@ -182450,8 +182522,8 @@ Do NOT use this tool for git commands \u2014 use the dedicated git tools instead
|
|
|
182450
182522
|
if (params.background) {
|
|
182451
182523
|
const tempDir = getTempDir();
|
|
182452
182524
|
const handle = `bg-${randomUUID3().slice(0, 8)}`;
|
|
182453
|
-
const outputPath =
|
|
182454
|
-
const pidPath =
|
|
182525
|
+
const outputPath = join11(tempDir, `${handle}.log`);
|
|
182526
|
+
const pidPath = join11(tempDir, `${handle}.pid`);
|
|
182455
182527
|
const logFd = openSync(outputPath, "a");
|
|
182456
182528
|
let proc;
|
|
182457
182529
|
try {
|
|
@@ -182465,7 +182537,8 @@ Do NOT use this tool for git commands \u2014 use the dedicated git tools instead
|
|
|
182465
182537
|
closeSync(logFd);
|
|
182466
182538
|
}
|
|
182467
182539
|
if (!proc.pid) {
|
|
182468
|
-
|
|
182540
|
+
const [error49] = await once(proc, "error");
|
|
182541
|
+
throw new Error(`failed to start background process: ${error49.message}`);
|
|
182469
182542
|
}
|
|
182470
182543
|
proc.unref();
|
|
182471
182544
|
writeFileSync8(pidPath, `${proc.pid}
|
|
@@ -182742,12 +182815,12 @@ function postProcessRangeDiff(raw2, contextLines = 3) {
|
|
|
182742
182815
|
// mcp/git.ts
|
|
182743
182816
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
182744
182817
|
import { writeFileSync as writeFileSync9 } from "node:fs";
|
|
182745
|
-
import { join as
|
|
182818
|
+
import { join as join14 } from "node:path";
|
|
182746
182819
|
|
|
182747
182820
|
// utils/apiCommit.ts
|
|
182748
182821
|
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
182749
182822
|
import { lstat, readlink } from "node:fs/promises";
|
|
182750
|
-
import { join as
|
|
182823
|
+
import { join as join12 } from "node:path";
|
|
182751
182824
|
var GITHUB_API = "https://api.github.com";
|
|
182752
182825
|
var MAX_BLOB_BYTES = 30 * 1024 * 1024;
|
|
182753
182826
|
var BLOB_UPLOAD_CONCURRENCY = 8;
|
|
@@ -182812,7 +182885,7 @@ async function assertApiCommittable(files) {
|
|
|
182812
182885
|
const root = getRepoRoot();
|
|
182813
182886
|
const attrs = $(
|
|
182814
182887
|
"git",
|
|
182815
|
-
["check-attr", "filter", "-z", "--", ...present.map((p) =>
|
|
182888
|
+
["check-attr", "filter", "-z", "--", ...present.map((p) => join12(root, p))],
|
|
182816
182889
|
{ log: false }
|
|
182817
182890
|
);
|
|
182818
182891
|
const parts = attrs.split("\0");
|
|
@@ -182824,7 +182897,7 @@ async function assertApiCommittable(files) {
|
|
|
182824
182897
|
}
|
|
182825
182898
|
}
|
|
182826
182899
|
for (const path4 of present) {
|
|
182827
|
-
const stat = await lstat(
|
|
182900
|
+
const stat = await lstat(join12(root, path4));
|
|
182828
182901
|
if (stat.isDirectory()) {
|
|
182829
182902
|
throw new Error(
|
|
182830
182903
|
`'${path4}' is a directory (nested repository or submodule?) \u2014 signed commits only support files and symlinks.`
|
|
@@ -182833,7 +182906,7 @@ async function assertApiCommittable(files) {
|
|
|
182833
182906
|
}
|
|
182834
182907
|
}
|
|
182835
182908
|
async function createBlobEntry(params) {
|
|
182836
|
-
const absPath =
|
|
182909
|
+
const absPath = join12(params.repoRoot, params.path);
|
|
182837
182910
|
const stat = await lstat(absPath);
|
|
182838
182911
|
if (stat.size > MAX_BLOB_BYTES) {
|
|
182839
182912
|
throw new Error(
|
|
@@ -183013,7 +183086,7 @@ this usually means your local branch has commits that were never pushed \u2014 s
|
|
|
183013
183086
|
var core4 = __toESM(require_core(), 1);
|
|
183014
183087
|
import { createSign } from "node:crypto";
|
|
183015
183088
|
import { rename, writeFile } from "node:fs/promises";
|
|
183016
|
-
import { dirname as dirname3, join as
|
|
183089
|
+
import { dirname as dirname3, join as join13 } from "node:path";
|
|
183017
183090
|
|
|
183018
183091
|
// node_modules/.pnpm/@octokit+plugin-throttling@11.0.5_@octokit+core@7.0.8/node_modules/@octokit/plugin-throttling/dist-bundle/index.js
|
|
183019
183092
|
var import_light = __toESM(require_light(), 1);
|
|
@@ -187562,7 +187635,7 @@ function getGitHubUsageSummary() {
|
|
|
187562
187635
|
}
|
|
187563
187636
|
async function writeGitHubUsageSummaryToFile(path4) {
|
|
187564
187637
|
const summary2 = getGitHubUsageSummary();
|
|
187565
|
-
const tmpPath =
|
|
187638
|
+
const tmpPath = join13(dirname3(path4), `.usage-summary-${process.pid}.tmp`);
|
|
187566
187639
|
await writeFile(tmpPath, JSON.stringify(summary2));
|
|
187567
187640
|
await rename(tmpPath, path4);
|
|
187568
187641
|
}
|
|
@@ -188190,7 +188263,7 @@ function countAhead(head, base, cwd) {
|
|
|
188190
188263
|
function spillGitOutput(params) {
|
|
188191
188264
|
const tempDir = process.env.PULLFROG_TEMP_DIR;
|
|
188192
188265
|
if (!tempDir) throw new Error("PULLFROG_TEMP_DIR not set");
|
|
188193
|
-
const outputPath =
|
|
188266
|
+
const outputPath = join14(tempDir, `git-${params.command}-${randomUUID4().slice(0, 8)}.txt`);
|
|
188194
188267
|
writeFileSync9(outputPath, params.output);
|
|
188195
188268
|
const previewByLines = params.output.split("\n").slice(0, OVERFLOW_PREVIEW_LINES).join("\n");
|
|
188196
188269
|
const preview = previewByLines.length <= OVERFLOW_PREVIEW_MAX_CHARS ? previewByLines : `${previewByLines.slice(0, OVERFLOW_PREVIEW_MAX_CHARS)}\u2026`;
|
|
@@ -189845,11 +189918,11 @@ async function reportReviewNodeId(ctx, params) {
|
|
|
189845
189918
|
|
|
189846
189919
|
// utils/setup.ts
|
|
189847
189920
|
import { execFileSync as execFileSync6, execSync as execSync2 } from "node:child_process";
|
|
189848
|
-
import { mkdtempSync, readdirSync, realpathSync as realpathSync2, rmSync as rmSync2, unlinkSync as unlinkSync2 } from "node:fs";
|
|
189921
|
+
import { mkdtempSync as mkdtempSync2, readdirSync, realpathSync as realpathSync2, rmSync as rmSync2, unlinkSync as unlinkSync2 } from "node:fs";
|
|
189849
189922
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
189850
|
-
import { join as
|
|
189923
|
+
import { join as join15 } from "node:path";
|
|
189851
189924
|
function createTempDirectory() {
|
|
189852
|
-
const path4 =
|
|
189925
|
+
const path4 = mkdtempSync2(join15(tmpdir2(), "pullfrog-"));
|
|
189853
189926
|
process.env.PULLFROG_TEMP_DIR = path4;
|
|
189854
189927
|
log.info(`\xBB created temp dir at ${path4}`);
|
|
189855
189928
|
return { path: path4, [Symbol.dispose]: () => removeTempDirectory(path4) };
|
|
@@ -189909,13 +189982,13 @@ function wipeRunnerLeakSurface() {
|
|
|
189909
189982
|
return [];
|
|
189910
189983
|
}
|
|
189911
189984
|
};
|
|
189912
|
-
const fileCommandsDir =
|
|
189985
|
+
const fileCommandsDir = join15(runnerTemp, "_runner_file_commands");
|
|
189913
189986
|
for (const entry of listDir(fileCommandsDir)) {
|
|
189914
|
-
tryUnlink(
|
|
189987
|
+
tryUnlink(join15(fileCommandsDir, entry));
|
|
189915
189988
|
}
|
|
189916
189989
|
for (const entry of listDir(runnerTemp)) {
|
|
189917
189990
|
if (entry.endsWith(".sh") || /^git-credentials-.*\.config$/.test(entry)) {
|
|
189918
|
-
tryUnlink(
|
|
189991
|
+
tryUnlink(join15(runnerTemp, entry));
|
|
189919
189992
|
}
|
|
189920
189993
|
}
|
|
189921
189994
|
if (wiped.length > 0) {
|
|
@@ -190187,7 +190260,7 @@ async function ensureBeforeShaReachable(params) {
|
|
|
190187
190260
|
"origin",
|
|
190188
190261
|
tempBranch
|
|
190189
190262
|
],
|
|
190190
|
-
{ token: params.gitToken, refreshGitToken: params.refreshGitToken },
|
|
190263
|
+
{ token: params.gitToken, refreshGitToken: params.refreshGitToken, signal: params.signal },
|
|
190191
190264
|
`before_sha temp branch ${tempBranch}`
|
|
190192
190265
|
);
|
|
190193
190266
|
log.debug(`\xBB fetched before_sha via temp branch ${tempBranch}`);
|
|
@@ -190228,7 +190301,7 @@ function cleanupStaleGitLocks() {
|
|
|
190228
190301
|
} catch {
|
|
190229
190302
|
continue;
|
|
190230
190303
|
}
|
|
190231
|
-
if (now - mtimeMs < STALE_LOCK_AGE_MS) continue;
|
|
190304
|
+
if (now - mtimeMs < STALE_LOCK_AGE_MS || mtimeMs >= performance.timeOrigin) continue;
|
|
190232
190305
|
try {
|
|
190233
190306
|
unlinkSync3(relPath);
|
|
190234
190307
|
log.warning(`\xBB removed stale ${relPath} from prior run`);
|
|
@@ -190261,7 +190334,7 @@ async function abortIfPullRequestMoved(args2) {
|
|
|
190261
190334
|
);
|
|
190262
190335
|
}
|
|
190263
190336
|
async function checkoutPrBranch(pr, params) {
|
|
190264
|
-
const { octokit, owner, name, gitToken, refreshGitToken, toolState, beforeSha } = params;
|
|
190337
|
+
const { octokit, owner, name, gitToken, refreshGitToken, toolState, beforeSha, signal } = params;
|
|
190265
190338
|
rejectIfLeadingDash(pr.baseRef, "PR base ref");
|
|
190266
190339
|
rejectIfLeadingDash(pr.headRef, "PR head ref");
|
|
190267
190340
|
const repoState = requireRepoState(toolState, owner, name);
|
|
@@ -190275,7 +190348,7 @@ async function checkoutPrBranch(pr, params) {
|
|
|
190275
190348
|
log.debug(`\xBB fetching base branch (${pr.baseRef})...`);
|
|
190276
190349
|
await $gitFetchWithDeepen(
|
|
190277
190350
|
["--no-tags", "origin", pr.baseRef],
|
|
190278
|
-
{ token: gitToken, refreshGitToken },
|
|
190351
|
+
{ token: gitToken, refreshGitToken, signal },
|
|
190279
190352
|
`base branch ${pr.baseRef}`
|
|
190280
190353
|
);
|
|
190281
190354
|
if (!alreadyOnBranch) {
|
|
@@ -190286,7 +190359,7 @@ async function checkoutPrBranch(pr, params) {
|
|
|
190286
190359
|
try {
|
|
190287
190360
|
await $gitFetchWithDeepen(
|
|
190288
190361
|
["--no-tags", "origin", `+pull/${pr.number}/head:${localBranch}`],
|
|
190289
|
-
{ token: gitToken, refreshGitToken },
|
|
190362
|
+
{ token: gitToken, refreshGitToken, signal },
|
|
190290
190363
|
`PR #${pr.number}`
|
|
190291
190364
|
);
|
|
190292
190365
|
} catch (e) {
|
|
@@ -190311,7 +190384,8 @@ async function checkoutPrBranch(pr, params) {
|
|
|
190311
190384
|
repo: name,
|
|
190312
190385
|
gitToken,
|
|
190313
190386
|
refreshGitToken,
|
|
190314
|
-
isShallow
|
|
190387
|
+
isShallow,
|
|
190388
|
+
signal
|
|
190315
190389
|
}) : false;
|
|
190316
190390
|
if (isShallow) {
|
|
190317
190391
|
let deepenDepth = 0;
|
|
@@ -190346,7 +190420,8 @@ async function checkoutPrBranch(pr, params) {
|
|
|
190346
190420
|
if (deepenDepth) {
|
|
190347
190421
|
log.debug(`\xBB deepening by ${deepenDepth} to reach merge base...`);
|
|
190348
190422
|
await $git("fetch", [`--deepen=${deepenDepth}`, "--no-tags", "origin"], {
|
|
190349
|
-
token: gitToken
|
|
190423
|
+
token: gitToken,
|
|
190424
|
+
signal
|
|
190350
190425
|
});
|
|
190351
190426
|
}
|
|
190352
190427
|
}
|
|
@@ -190384,6 +190459,7 @@ async function checkoutPrBranch(pr, params) {
|
|
|
190384
190459
|
remoteBranch: pr.headRef,
|
|
190385
190460
|
localBranch
|
|
190386
190461
|
};
|
|
190462
|
+
signal.throwIfAborted();
|
|
190387
190463
|
const postCheckoutHook = await executeLifecycleHook({
|
|
190388
190464
|
event: "post-checkout",
|
|
190389
190465
|
script: params.postCheckoutScript,
|
|
@@ -190394,19 +190470,30 @@ async function checkoutPrBranch(pr, params) {
|
|
|
190394
190470
|
}
|
|
190395
190471
|
var inFlightCheckouts = /* @__PURE__ */ new Map();
|
|
190396
190472
|
var CHECKOUT_DEADLINE_MS = 57e4;
|
|
190397
|
-
|
|
190398
|
-
|
|
190399
|
-
const
|
|
190400
|
-
|
|
190473
|
+
var CHECKOUT_ABORT_GRACE_MS = 15e3;
|
|
190474
|
+
function withCheckoutDeadline(run, pullNumber) {
|
|
190475
|
+
const exceeded = `checkout_pr #${pullNumber} exceeded ${CHECKOUT_DEADLINE_MS / 1e3}s`;
|
|
190476
|
+
const retry = "retry the call to start a fresh attempt.";
|
|
190477
|
+
const controller = new AbortController();
|
|
190478
|
+
const abortTimer = setTimeout(
|
|
190479
|
+
() => controller.abort(new Error(`${exceeded} and was cancelled; ${retry}`)),
|
|
190480
|
+
CHECKOUT_DEADLINE_MS
|
|
190481
|
+
);
|
|
190482
|
+
let backstopTimer;
|
|
190483
|
+
const backstop = new Promise((_, reject) => {
|
|
190484
|
+
backstopTimer = setTimeout(
|
|
190401
190485
|
() => reject(
|
|
190402
190486
|
new Error(
|
|
190403
|
-
|
|
190487
|
+
`${exceeded} and was asked to cancel, but a step that cannot be interrupted (the post-checkout hook or a GitHub API call) was still running ${CHECKOUT_ABORT_GRACE_MS / 1e3}s later. ${retry}`
|
|
190404
190488
|
)
|
|
190405
190489
|
),
|
|
190406
|
-
CHECKOUT_DEADLINE_MS
|
|
190490
|
+
CHECKOUT_DEADLINE_MS + CHECKOUT_ABORT_GRACE_MS
|
|
190407
190491
|
);
|
|
190408
190492
|
});
|
|
190409
|
-
return Promise.race([
|
|
190493
|
+
return Promise.race([run(controller.signal), backstop]).finally(() => {
|
|
190494
|
+
clearTimeout(abortTimer);
|
|
190495
|
+
clearTimeout(backstopTimer);
|
|
190496
|
+
});
|
|
190410
190497
|
}
|
|
190411
190498
|
function headsEqual(a, b) {
|
|
190412
190499
|
if (a.kind === "branch" && b.kind === "branch") return a.name === b.name;
|
|
@@ -190418,7 +190505,7 @@ function describeHead(h) {
|
|
|
190418
190505
|
return `detached HEAD \`${h.sha}\``;
|
|
190419
190506
|
}
|
|
190420
190507
|
function CheckoutPrTool(ctx) {
|
|
190421
|
-
const runCheckout = async (pull_number) => {
|
|
190508
|
+
const runCheckout = async (pull_number, signal) => {
|
|
190422
190509
|
const prResponse = await ctx.octokit.rest.pulls.get({
|
|
190423
190510
|
owner: ctx.repo.owner,
|
|
190424
190511
|
repo: ctx.repo.name,
|
|
@@ -190447,7 +190534,8 @@ function CheckoutPrTool(ctx) {
|
|
|
190447
190534
|
toolState: ctx.toolState,
|
|
190448
190535
|
shell: ctx.payload.shell,
|
|
190449
190536
|
postCheckoutScript: ctx.postCheckoutScript,
|
|
190450
|
-
beforeSha: primary.beforeSha
|
|
190537
|
+
beforeSha: primary.beforeSha,
|
|
190538
|
+
signal
|
|
190451
190539
|
});
|
|
190452
190540
|
const tempDir = process.env.PULLFROG_TEMP_DIR;
|
|
190453
190541
|
if (!tempDir) {
|
|
@@ -190469,7 +190557,7 @@ function CheckoutPrTool(ctx) {
|
|
|
190469
190557
|
headSha: checkoutSha
|
|
190470
190558
|
});
|
|
190471
190559
|
if (incremental.status === "ok") {
|
|
190472
|
-
incrementalDiffPath =
|
|
190560
|
+
incrementalDiffPath = join16(
|
|
190473
190561
|
tempDir,
|
|
190474
190562
|
`pr-${pull_number}-${beforeShort}-${headShort}-incremental.diff`
|
|
190475
190563
|
);
|
|
@@ -190486,7 +190574,7 @@ function CheckoutPrTool(ctx) {
|
|
|
190486
190574
|
const diffPreview = formatResult.content.split("\n").slice(0, 100).join("\n");
|
|
190487
190575
|
log.debug(`formatted diff preview (first 100 lines):
|
|
190488
190576
|
${diffPreview}`);
|
|
190489
|
-
const diffPath =
|
|
190577
|
+
const diffPath = join16(tempDir, `pr-${pull_number}-${headShort}.diff`);
|
|
190490
190578
|
writeFileSync10(diffPath, formatResult.content);
|
|
190491
190579
|
log.debug(`wrote diff to ${diffPath} (${formatResult.content.length} bytes)`);
|
|
190492
190580
|
primary.diffCoverage = createDiffCoverageState({
|
|
@@ -190526,7 +190614,8 @@ ${diffPreview}`);
|
|
|
190526
190614
|
files: formatResult.files,
|
|
190527
190615
|
pullNumber: pull_number,
|
|
190528
190616
|
baseSha: prResponse.data.base.sha,
|
|
190529
|
-
headSha: checkoutSha
|
|
190617
|
+
headSha: checkoutSha,
|
|
190618
|
+
signal
|
|
190530
190619
|
});
|
|
190531
190620
|
impactPath = impact.path;
|
|
190532
190621
|
log.info(
|
|
@@ -190632,7 +190721,10 @@ ${dirty}`
|
|
|
190632
190721
|
);
|
|
190633
190722
|
}
|
|
190634
190723
|
}
|
|
190635
|
-
const promise2 = withCheckoutDeadline(
|
|
190724
|
+
const promise2 = withCheckoutDeadline(
|
|
190725
|
+
(signal) => runCheckout(pull_number, signal),
|
|
190726
|
+
pull_number
|
|
190727
|
+
);
|
|
190636
190728
|
inFlightCheckouts.set(pull_number, promise2);
|
|
190637
190729
|
try {
|
|
190638
190730
|
return await promise2;
|
|
@@ -190645,7 +190737,7 @@ ${dirty}`
|
|
|
190645
190737
|
|
|
190646
190738
|
// mcp/checkSuite.ts
|
|
190647
190739
|
import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync11 } from "node:fs";
|
|
190648
|
-
import { join as
|
|
190740
|
+
import { join as join17 } from "node:path";
|
|
190649
190741
|
var GetCheckSuiteLogs = type({
|
|
190650
190742
|
check_suite_id: type.number.describe("the id from check_suite.id")
|
|
190651
190743
|
});
|
|
@@ -190741,7 +190833,7 @@ function GetCheckSuiteLogsTool(ctx) {
|
|
|
190741
190833
|
if (!tempDir) {
|
|
190742
190834
|
throw new Error("PULLFROG_TEMP_DIR not set");
|
|
190743
190835
|
}
|
|
190744
|
-
const logsDir =
|
|
190836
|
+
const logsDir = join17(tempDir, "ci-logs");
|
|
190745
190837
|
mkdirSync8(logsDir, { recursive: true });
|
|
190746
190838
|
const jobResults = [];
|
|
190747
190839
|
for (const run of failedRuns) {
|
|
@@ -190768,7 +190860,7 @@ function GetCheckSuiteLogsTool(ctx) {
|
|
|
190768
190860
|
);
|
|
190769
190861
|
}
|
|
190770
190862
|
const logsText = await logsResult.text();
|
|
190771
|
-
const logPath =
|
|
190863
|
+
const logPath = join17(logsDir, `job-${job.id}.log`);
|
|
190772
190864
|
writeFileSync11(logPath, logsText);
|
|
190773
190865
|
const analysis = analyzeLog(logsText, 80);
|
|
190774
190866
|
const failedSteps = job.steps?.filter((s) => s.conclusion === "failure").map((s) => `Step ${s.number}: ${s.name}`) ?? [];
|
|
@@ -190818,7 +190910,7 @@ function GetCheckSuiteLogsTool(ctx) {
|
|
|
190818
190910
|
|
|
190819
190911
|
// mcp/commitInfo.ts
|
|
190820
190912
|
import { writeFileSync as writeFileSync12 } from "node:fs";
|
|
190821
|
-
import { join as
|
|
190913
|
+
import { join as join18 } from "node:path";
|
|
190822
190914
|
var CommitInfo = type({
|
|
190823
190915
|
sha: type.string.describe("the commit SHA (full or abbreviated) to fetch")
|
|
190824
190916
|
});
|
|
@@ -190842,7 +190934,7 @@ function CommitInfoTool(ctx) {
|
|
|
190842
190934
|
"PULLFROG_TEMP_DIR not set - get_commit_info must run in pullfrog action context"
|
|
190843
190935
|
);
|
|
190844
190936
|
}
|
|
190845
|
-
const diffFile =
|
|
190937
|
+
const diffFile = join18(tempDir, `commit-${sha.slice(0, 7)}.diff`);
|
|
190846
190938
|
writeFileSync12(diffFile, formatResult.content);
|
|
190847
190939
|
log.debug(`wrote commit diff to ${diffFile} (${formatResult.content.length} bytes)`);
|
|
190848
190940
|
return {
|
|
@@ -190979,14 +191071,14 @@ function ReopenCurrentTool(ctx) {
|
|
|
190979
191071
|
import { performance as performance8 } from "node:perf_hooks";
|
|
190980
191072
|
|
|
190981
191073
|
// prep/installNodeDependencies.ts
|
|
190982
|
-
import { existsSync as
|
|
190983
|
-
import { join as
|
|
191074
|
+
import { existsSync as existsSync9 } from "node:fs";
|
|
191075
|
+
import { join as join20 } from "node:path";
|
|
190984
191076
|
|
|
190985
191077
|
// utils/packageManager.ts
|
|
190986
191078
|
var import_semver2 = __toESM(require_semver2(), 1);
|
|
190987
|
-
import { existsSync as
|
|
191079
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
190988
191080
|
import { mkdir, readFile as readFile2 } from "node:fs/promises";
|
|
190989
|
-
import { delimiter, join as
|
|
191081
|
+
import { delimiter, join as join19 } from "node:path";
|
|
190990
191082
|
var SUPPORTED_NAMES = ["npm", "pnpm", "yarn", "bun"];
|
|
190991
191083
|
var COREPACK_MANAGED = ["pnpm", "yarn"];
|
|
190992
191084
|
function isSupported(name) {
|
|
@@ -191024,8 +191116,8 @@ function parseDevEnginesField(field) {
|
|
|
191024
191116
|
};
|
|
191025
191117
|
}
|
|
191026
191118
|
async function resolvePackageManagerSpec(cwd) {
|
|
191027
|
-
const pkgPath =
|
|
191028
|
-
if (!
|
|
191119
|
+
const pkgPath = join19(cwd, "package.json");
|
|
191120
|
+
if (!existsSync8(pkgPath)) return null;
|
|
191029
191121
|
let pkg;
|
|
191030
191122
|
try {
|
|
191031
191123
|
pkg = JSON.parse(await readFile2(pkgPath, "utf-8"));
|
|
@@ -191083,7 +191175,7 @@ async function currentVersion(name) {
|
|
|
191083
191175
|
return result.stdout.trim();
|
|
191084
191176
|
}
|
|
191085
191177
|
function packageManagerBinDir(tmpdir3) {
|
|
191086
|
-
return
|
|
191178
|
+
return join19(tmpdir3, "pm-bin");
|
|
191087
191179
|
}
|
|
191088
191180
|
async function ensurePackageManager(params) {
|
|
191089
191181
|
const spec = params.spec;
|
|
@@ -191146,7 +191238,7 @@ async function installFallback(name, installSpec) {
|
|
|
191146
191238
|
});
|
|
191147
191239
|
if (result.exitCode !== 0) return result.stderr || `failed to install ${name}`;
|
|
191148
191240
|
if (name === "deno") {
|
|
191149
|
-
const denoPath =
|
|
191241
|
+
const denoPath = join19(process.env.HOME || "", ".deno", "bin");
|
|
191150
191242
|
process.env.PATH = `${denoPath}:${process.env.PATH}`;
|
|
191151
191243
|
}
|
|
191152
191244
|
log.info(`\xBB installed ${name}`);
|
|
@@ -191179,8 +191271,8 @@ async function isCommandAvailable2(command) {
|
|
|
191179
191271
|
var installNodeDependencies = {
|
|
191180
191272
|
name: "installNodeDependencies",
|
|
191181
191273
|
shouldRun: () => {
|
|
191182
|
-
const packageJsonPath =
|
|
191183
|
-
return
|
|
191274
|
+
const packageJsonPath = join20(process.cwd(), "package.json");
|
|
191275
|
+
return existsSync9(packageJsonPath);
|
|
191184
191276
|
},
|
|
191185
191277
|
run: async (options) => {
|
|
191186
191278
|
const declared = await resolvePackageManagerSpec(process.cwd());
|
|
@@ -191274,14 +191366,14 @@ ${errorMessage}`]
|
|
|
191274
191366
|
};
|
|
191275
191367
|
|
|
191276
191368
|
// prep/installPythonDependencies.ts
|
|
191277
|
-
import { existsSync as
|
|
191278
|
-
import { join as
|
|
191369
|
+
import { existsSync as existsSync10, readFileSync as readFileSync5 } from "node:fs";
|
|
191370
|
+
import { join as join21 } from "node:path";
|
|
191279
191371
|
function declaresBuildSystem(path4) {
|
|
191280
191372
|
return /^\s*\[\s*build-system\s*\]/m.test(readFileSync5(path4, "utf8"));
|
|
191281
191373
|
}
|
|
191282
191374
|
function configApplies(config3, cwd) {
|
|
191283
|
-
const path4 =
|
|
191284
|
-
return
|
|
191375
|
+
const path4 = join21(cwd, config3.file);
|
|
191376
|
+
return existsSync10(path4) && (config3.applies?.(path4) ?? true);
|
|
191285
191377
|
}
|
|
191286
191378
|
var PYTHON_CONFIGS = [
|
|
191287
191379
|
{
|
|
@@ -191603,7 +191695,7 @@ function AwaitDependencyInstallationTool(ctx) {
|
|
|
191603
191695
|
|
|
191604
191696
|
// mcp/gh.ts
|
|
191605
191697
|
import { mkdirSync as mkdirSync9 } from "node:fs";
|
|
191606
|
-
import { join as
|
|
191698
|
+
import { join as join22 } from "node:path";
|
|
191607
191699
|
var GH_TIMEOUT_MS = 12e4;
|
|
191608
191700
|
var GhParams = type({
|
|
191609
191701
|
args: type.string.array().describe(
|
|
@@ -191625,7 +191717,7 @@ function GhTool(ctx) {
|
|
|
191625
191717
|
description: `Run the GitHub CLI against ${ctx.repo.owner}/${ctx.repo.name}. Pass argv as an array \u2014 there is no shell, so pipes, redirects and \`&&\` do not work; use gh's own \`--json\`/\`--jq\` flags instead. \`gh api graphql\` is available. Authenticated with ${permissions ? formatPermissions(permissions) : "no permissions"} on this repo only; anything else 403s. Pushing, merging and ref writes are not available \u2014 use the git tools.`,
|
|
191626
191718
|
parameters: GhParams,
|
|
191627
191719
|
execute: execute(async (params) => {
|
|
191628
|
-
const configDir =
|
|
191720
|
+
const configDir = join22(ctx.tmpdir, "gh-config");
|
|
191629
191721
|
mkdirSync9(configDir, { recursive: true });
|
|
191630
191722
|
const command = ["gh", ...params.args.map(shellQuote)].join(" ");
|
|
191631
191723
|
const result = await runSandboxed({
|
|
@@ -192349,7 +192441,7 @@ function PullRequestInfoTool(ctx) {
|
|
|
192349
192441
|
|
|
192350
192442
|
// mcp/reviewComments.ts
|
|
192351
192443
|
import { writeFileSync as writeFileSync13 } from "node:fs";
|
|
192352
|
-
import { join as
|
|
192444
|
+
import { join as join23 } from "node:path";
|
|
192353
192445
|
var COMMENTS_PER_THREAD = 50;
|
|
192354
192446
|
var REVIEW_THREADS_QUERY = `
|
|
192355
192447
|
query ($owner: String!, $name: String!, $prNumber: Int!, $cursor: String) {
|
|
@@ -192843,7 +192935,7 @@ function GetReviewCommentsTool(ctx) {
|
|
|
192843
192935
|
throw new Error("PULLFROG_TEMP_DIR not set");
|
|
192844
192936
|
}
|
|
192845
192937
|
const filename = `review-${params.review_id}-threads.md`;
|
|
192846
|
-
const commentsPath =
|
|
192938
|
+
const commentsPath = join23(tempDir, filename);
|
|
192847
192939
|
writeFileSync13(commentsPath, formatted.content);
|
|
192848
192940
|
log.debug(`wrote ${threadBlocks.length} threads to ${commentsPath}`);
|
|
192849
192941
|
return {
|
|
@@ -193213,7 +193305,7 @@ function UploadFileTool(ctx) {
|
|
|
193213
193305
|
|
|
193214
193306
|
// mcp/xrepo.ts
|
|
193215
193307
|
import { mkdirSync as mkdirSync10, rmSync as rmSync3 } from "node:fs";
|
|
193216
|
-
import { join as
|
|
193308
|
+
import { join as join24 } from "node:path";
|
|
193217
193309
|
function assertValidRepoName(name) {
|
|
193218
193310
|
if (!/^[A-Za-z0-9._-]+$/.test(name) || name.startsWith("-") || name.includes("..")) {
|
|
193219
193311
|
throw new Error(
|
|
@@ -193293,7 +193385,7 @@ function CheckoutRepoTool(ctx) {
|
|
|
193293
193385
|
return { path: existing.dir, access: existing.access, note: "already checked out." };
|
|
193294
193386
|
}
|
|
193295
193387
|
const access = accessFor(ctx, repo);
|
|
193296
|
-
const dir =
|
|
193388
|
+
const dir = join24(ctx.tmpdir, "xrepo", repo);
|
|
193297
193389
|
mkdirSync10(dir, { recursive: true });
|
|
193298
193390
|
const state = ensureRepoState(ctx.toolState, { owner, name: repo, dir, access });
|
|
193299
193391
|
const rc = resolveRepoCtx(ctx, repo);
|
|
@@ -193659,6 +193751,7 @@ function isByokSetupError(text) {
|
|
|
193659
193751
|
var SECRETS_UNAVAILABLE_MARKER = "couldn't load your Pullfrog secrets";
|
|
193660
193752
|
var ROUTER_UNFUNDED_MARKER = "your Pullfrog Router balance is empty";
|
|
193661
193753
|
var CREDENTIAL_REJECTED_MARKER = "was rejected by its provider";
|
|
193754
|
+
var CREDENTIAL_POOL_REFUSED_MARKER = "No configured credential can serve this run";
|
|
193662
193755
|
var NoUsableCredentialError = class extends Error {
|
|
193663
193756
|
};
|
|
193664
193757
|
function throwKeyError(params) {
|
|
@@ -193925,7 +194018,8 @@ function isOAuthCredentialExpiredError(text) {
|
|
|
193925
194018
|
text
|
|
193926
194019
|
) || // the provider no longer recognises the token at all (#1086) — same dead
|
|
193927
194020
|
// credential, phrased as a lookup miss rather than a state transition.
|
|
193928
|
-
/Could not find the appropriate key in your authentication token/i.test(text) ||
|
|
194021
|
+
/Could not find the appropriate key in your authentication token/i.test(text) || // the reversed word order: `Encountered invalidated oauth token for user` (#1474).
|
|
194022
|
+
/(?:expired|invalidated|revoked) (?:OAuth|authentication) token/i.test(text) || /Token refresh failed/i.test(text)
|
|
193929
194023
|
);
|
|
193930
194024
|
}
|
|
193931
194025
|
function isMalformedKeyError(text) {
|
|
@@ -193972,7 +194066,7 @@ function buildRejectedCredentialError(params) {
|
|
|
193972
194066
|
].join("\n");
|
|
193973
194067
|
}
|
|
193974
194068
|
const where = params.inPullfrogStore ? `[Update it in Pullfrog \u2192](${settingsUrl})` : `[Update the GitHub Actions secret \u2192](https://github.com/${params.owner}/${params.name}/settings/secrets/actions)`;
|
|
193975
|
-
const keysUrl =
|
|
194069
|
+
const keysUrl = providerDashboard(params.credential)?.keys;
|
|
193976
194070
|
const issue3 = keysUrl ? `[Issue a new key \u2192](${keysUrl}) \xB7 ` : "";
|
|
193977
194071
|
return [
|
|
193978
194072
|
`**Your \`${params.credential}\` ${CREDENTIAL_REJECTED_MARKER}**${detail}, so the agent never ran.`,
|
|
@@ -193982,12 +194076,43 @@ function buildRejectedCredentialError(params) {
|
|
|
193982
194076
|
`${issue3}${where} \xB7 [Model settings \u2192](${settingsUrl}) \xB7 [Setup docs \u2192](https://docs.pullfrog.com/keys) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
|
|
193983
194077
|
].join("\n");
|
|
193984
194078
|
}
|
|
193985
|
-
function
|
|
194079
|
+
function providerDashboard(credential) {
|
|
193986
194080
|
for (const [id, provider2] of Object.entries(providers)) {
|
|
193987
|
-
if (provider2.envVars.includes(credential)) return PROVIDER_DASHBOARDS[id]
|
|
194081
|
+
if (provider2.envVars.includes(credential)) return PROVIDER_DASHBOARDS[id];
|
|
193988
194082
|
}
|
|
193989
194083
|
return void 0;
|
|
193990
194084
|
}
|
|
194085
|
+
function buildCredentialPoolRefusedError(params) {
|
|
194086
|
+
const settingsUrl = `${getApiUrl()}/console/${params.owner}/${params.name}`;
|
|
194087
|
+
return [
|
|
194088
|
+
`**${CREDENTIAL_POOL_REFUSED_MARKER}.** Every credential configured for \`${params.model}\` was rejected or is out of quota, so the agent never ran.`,
|
|
194089
|
+
"",
|
|
194090
|
+
...params.refused.map(
|
|
194091
|
+
(credential) => `- ${describeRefusal({ ...params, credential, settingsUrl })}`
|
|
194092
|
+
),
|
|
194093
|
+
"",
|
|
194094
|
+
`[Model settings \u2192](${settingsUrl}) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
|
|
194095
|
+
].join("\n");
|
|
194096
|
+
}
|
|
194097
|
+
function describeRefusal(params) {
|
|
194098
|
+
const credential = params.credential;
|
|
194099
|
+
const verdict = credential.verdict;
|
|
194100
|
+
const workflow = credential.source === "workflow";
|
|
194101
|
+
const head = `\`${credential.name}\` from ${workflow ? "the workflow" : `${credential.source} scope`}: \`${verdict.detail}\` \u2014`;
|
|
194102
|
+
const subscription = SUBSCRIPTION_CREDENTIALS[credential.name];
|
|
194103
|
+
const dashboard = providerDashboard(credential.name);
|
|
194104
|
+
if (verdict.status === "exhausted" && subscription) {
|
|
194105
|
+
const wait = verdict.resetAt ? `it resets at **${verdict.resetAt.toISOString().slice(0, 16).replace("T", " ")} UTC**; re-trigger after that` : "re-trigger after the limit resets";
|
|
194106
|
+
return `${head} ${wait}, or add an API key for this provider, which Pullfrog falls back to automatically.`;
|
|
194107
|
+
}
|
|
194108
|
+
if (verdict.status === "exhausted")
|
|
194109
|
+
return `${head} add credit or raise its quota with the provider.${dashboard ? ` [Top up \u2192](${dashboard.billing})` : ""}`;
|
|
194110
|
+
if (subscription)
|
|
194111
|
+
return `${head} re-authenticate with \`${subscription.command}\`, or switch this repo to a model you hold an API key for. [Re-authenticate \u2192](${subscription.docs})`;
|
|
194112
|
+
const where = workflow ? `[Update the GitHub Actions secret \u2192](https://github.com/${params.owner}/${params.name}/settings/secrets/actions)` : `[Update it in Pullfrog \u2192](${params.settingsUrl})`;
|
|
194113
|
+
const issue3 = dashboard?.keys ? `[Issue a new key \u2192](${dashboard.keys}) \xB7 ` : "";
|
|
194114
|
+
return `${head} issue a new key and update the copy Pullfrog uses. ${issue3}${where}`;
|
|
194115
|
+
}
|
|
193991
194116
|
function formatApiKeyErrorSummary(params) {
|
|
193992
194117
|
if (params.raw.includes(MISSING_KEY_MARKER)) {
|
|
193993
194118
|
if (params.raw.startsWith(`**${MISSING_KEY_MARKER}**`)) return params.raw;
|
|
@@ -194760,11 +194885,16 @@ async function selectConfiguredCredential(input) {
|
|
|
194760
194885
|
if (option.candidate) log.info(`\xBB selected ${option.name} from ${from}`);
|
|
194761
194886
|
return true;
|
|
194762
194887
|
}
|
|
194763
|
-
refused.push(
|
|
194888
|
+
refused.push({ name: option.name, source: option.candidate?.source ?? "workflow", verdict });
|
|
194764
194889
|
log.info(`\xBB ${option.name} from ${from} unavailable; trying the next credential`);
|
|
194765
194890
|
}
|
|
194766
194891
|
throw new Error(
|
|
194767
|
-
|
|
194892
|
+
buildCredentialPoolRefusedError({
|
|
194893
|
+
model,
|
|
194894
|
+
refused,
|
|
194895
|
+
owner: input.ctx.repo.owner,
|
|
194896
|
+
name: input.ctx.repo.name
|
|
194897
|
+
})
|
|
194768
194898
|
);
|
|
194769
194899
|
}
|
|
194770
194900
|
async function probe(input) {
|
|
@@ -194805,7 +194935,7 @@ function activate(input) {
|
|
|
194805
194935
|
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
194806
194936
|
import { writeFileSync as writeFileSync14 } from "node:fs";
|
|
194807
194937
|
import { createServer as createServer3 } from "node:http";
|
|
194808
|
-
import { join as
|
|
194938
|
+
import { join as join25 } from "node:path";
|
|
194809
194939
|
var REVOKED_TRAP_MS = 6e4;
|
|
194810
194940
|
function revokeGitHubToken(token) {
|
|
194811
194941
|
fetch("https://api.github.com/installation/token", {
|
|
@@ -194874,7 +195004,7 @@ async function startGitAuthServer(tmpdir3) {
|
|
|
194874
195004
|
function writeAskpassScript(code) {
|
|
194875
195005
|
const scriptId = randomUUID6();
|
|
194876
195006
|
const scriptName = `askpass-${scriptId}.js`;
|
|
194877
|
-
const scriptPath =
|
|
195007
|
+
const scriptPath = join25(tmpdir3, scriptName);
|
|
194878
195008
|
const content = [
|
|
194879
195009
|
`#!/usr/bin/env node`,
|
|
194880
195010
|
`var a=process.argv[2]||"";`,
|
|
@@ -195323,7 +195453,7 @@ function resolveInstructions(ctx) {
|
|
|
195323
195453
|
|
|
195324
195454
|
// utils/learnings.ts
|
|
195325
195455
|
import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
|
|
195326
|
-
import { dirname as dirname4, join as
|
|
195456
|
+
import { dirname as dirname4, join as join26 } from "node:path";
|
|
195327
195457
|
|
|
195328
195458
|
// utils/learningsTruncate.ts
|
|
195329
195459
|
var MAX_LEARNINGS_LENGTH = 1e5;
|
|
@@ -195341,10 +195471,10 @@ function truncateAtLineBoundary(body, cap) {
|
|
|
195341
195471
|
var LEARNINGS_FILE_NAME = "pullfrog-learnings.md";
|
|
195342
195472
|
var XREPO_LEARNINGS_FILE_NAME = "pullfrog-xrepo-learnings.md";
|
|
195343
195473
|
function learningsFilePath(tmpdir3) {
|
|
195344
|
-
return
|
|
195474
|
+
return join26(tmpdir3, LEARNINGS_FILE_NAME);
|
|
195345
195475
|
}
|
|
195346
195476
|
function xrepoLearningsFilePath(tmpdir3) {
|
|
195347
|
-
return
|
|
195477
|
+
return join26(tmpdir3, XREPO_LEARNINGS_FILE_NAME);
|
|
195348
195478
|
}
|
|
195349
195479
|
async function seedLearningsFile(params) {
|
|
195350
195480
|
const path4 = learningsFilePath(params.tmpdir);
|
|
@@ -195895,7 +196025,9 @@ async function resolveTokens(params) {
|
|
|
195895
196025
|
// commit-status check-runs for branch protection. the app already grants
|
|
195896
196026
|
// checks:write; this scopes the MCP token up to use it.
|
|
195897
196027
|
checks: "write",
|
|
195898
|
-
|
|
196028
|
+
// write (not read) so postReviewCleanup can dispatch the follow-up re-review
|
|
196029
|
+
// for commits pushed while a review was in flight; read 403s it.
|
|
196030
|
+
actions: "write"
|
|
195899
196031
|
};
|
|
195900
196032
|
const mcpToken = await acquireNewToken({
|
|
195901
196033
|
repos: writeRepos,
|
|
@@ -196281,7 +196413,7 @@ async function resolveTrialFallback(ctx) {
|
|
|
196281
196413
|
|
|
196282
196414
|
// utils/prSummary.ts
|
|
196283
196415
|
import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
|
|
196284
|
-
import { dirname as dirname5, join as
|
|
196416
|
+
import { dirname as dirname5, join as join27 } from "node:path";
|
|
196285
196417
|
var SUMMARY_FILE_NAME = "pullfrog-summary.md";
|
|
196286
196418
|
var SUMMARY_SCAFFOLD = `# PR summary
|
|
196287
196419
|
|
|
@@ -196291,7 +196423,7 @@ var SUMMARY_SCAFFOLD = `# PR summary
|
|
|
196291
196423
|
var MIN_SNAPSHOT_LENGTH = 60;
|
|
196292
196424
|
var MAX_SNAPSHOT_LENGTH = 32768;
|
|
196293
196425
|
function summaryFilePath(tmpdir3) {
|
|
196294
|
-
return
|
|
196426
|
+
return join27(tmpdir3, SUMMARY_FILE_NAME);
|
|
196295
196427
|
}
|
|
196296
196428
|
async function seedSummaryFile(params) {
|
|
196297
196429
|
const path4 = summaryFilePath(params.tmpdir);
|
|
@@ -196560,7 +196692,7 @@ function formatBillingExhaustedBody(diagnostic) {
|
|
|
196560
196692
|
|
|
196561
196693
|
// utils/runErrorRenderer.ts
|
|
196562
196694
|
function isProviderModelNotFoundError(message) {
|
|
196563
|
-
return message
|
|
196695
|
+
return /ProviderModelNotFoundError|\bModel not found: /.test(message);
|
|
196564
196696
|
}
|
|
196565
196697
|
function isContextOverflowError(message) {
|
|
196566
196698
|
return /Prompt is too long/i.test(message) || /Input exceeds context window/i.test(message) || /maximum context length is \d+ tokens/i.test(message) || /Session too large to compact/i.test(message);
|
|
@@ -196657,6 +196789,7 @@ function detectProviderId(message) {
|
|
|
196657
196789
|
if (/requires more credits|Key limit exceeded \(total limit\)|openrouter\.ai/i.test(message)) {
|
|
196658
196790
|
return "openrouter";
|
|
196659
196791
|
}
|
|
196792
|
+
if (/platform\.openai\.com/i.test(message)) return "openai";
|
|
196660
196793
|
return null;
|
|
196661
196794
|
}
|
|
196662
196795
|
var OPENROUTER_PRIVACY_URL = "https://openrouter.ai/settings/privacy";
|
|
@@ -196754,12 +196887,38 @@ function formatByokSetupSummary(input) {
|
|
|
196754
196887
|
].join("\n");
|
|
196755
196888
|
}
|
|
196756
196889
|
function formatProviderModelNotFoundSummary(input) {
|
|
196757
|
-
return `The configured model is
|
|
196890
|
+
return `The configured model is not in OpenCode's catalog. Pick a different model for \`${input.owner}/${input.name}\` where it is set \u2014 the Pullfrog console, the workflow's \`model\` input, or the \`PULLFROG_MODEL\` variable \u2014 or contact support if this persists.
|
|
196758
196891
|
|
|
196759
196892
|
\`\`\`
|
|
196760
196893
|
${input.raw}
|
|
196761
196894
|
\`\`\``;
|
|
196762
196895
|
}
|
|
196896
|
+
function formatProviderUsageLimit(input) {
|
|
196897
|
+
return [
|
|
196898
|
+
"**The provider's usage limit for this account has been reached.**",
|
|
196899
|
+
"",
|
|
196900
|
+
"A subscription window resets on its own, so re-trigger after it does; a limit set on a pay-as-you-go workspace has to be raised. A model on another provider can run in the meantime.",
|
|
196901
|
+
"",
|
|
196902
|
+
`[Model settings \u2192](${getApiUrl()}/console/${input.owner}/${input.name}) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`,
|
|
196903
|
+
"",
|
|
196904
|
+
`\`\`\`
|
|
196905
|
+
${input.raw}
|
|
196906
|
+
\`\`\``
|
|
196907
|
+
].join("\n");
|
|
196908
|
+
}
|
|
196909
|
+
function formatProviderErrorBody(input) {
|
|
196910
|
+
return [
|
|
196911
|
+
"**The model provider returned an error, so this run stopped.**",
|
|
196912
|
+
"",
|
|
196913
|
+
"The error is below. If it names your key, plan or balance, fix that with the provider or pick another model before re-triggering.",
|
|
196914
|
+
"",
|
|
196915
|
+
`[Model settings \u2192](${getApiUrl()}/console/${input.owner}/${input.name}) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`,
|
|
196916
|
+
"",
|
|
196917
|
+
`\`\`\`
|
|
196918
|
+
${input.raw}
|
|
196919
|
+
\`\`\``
|
|
196920
|
+
].join("\n");
|
|
196921
|
+
}
|
|
196763
196922
|
function renderRunError(input) {
|
|
196764
196923
|
const billingError = input.routerActive && isRouterKeylimitExhaustedError(input.errorMessage) ? new BillingError(input.errorMessage, { code: "router_keylimit_exhausted" }) : null;
|
|
196765
196924
|
if (billingError) {
|
|
@@ -196775,7 +196934,7 @@ function renderRunError(input) {
|
|
|
196775
196934
|
if (input.errorMessage.includes(ROUTER_UNFUNDED_MARKER)) {
|
|
196776
196935
|
return { summary: input.errorMessage, comment: input.errorMessage };
|
|
196777
196936
|
}
|
|
196778
|
-
if (input.errorMessage.includes(CREDENTIAL_REJECTED_MARKER)) {
|
|
196937
|
+
if (input.errorMessage.includes(CREDENTIAL_REJECTED_MARKER) || input.errorMessage.includes(CREDENTIAL_POOL_REFUSED_MARKER)) {
|
|
196779
196938
|
return { summary: input.errorMessage, comment: input.errorMessage };
|
|
196780
196939
|
}
|
|
196781
196940
|
const isHang = input.errorMessage.startsWith("activity timeout") || input.errorMessage.startsWith("agent still pending");
|
|
@@ -196798,6 +196957,16 @@ ${body}`, comment: body };
|
|
|
196798
196957
|
});
|
|
196799
196958
|
return { summary: `### \u274C Pullfrog failed
|
|
196800
196959
|
|
|
196960
|
+
${body}`, comment: body };
|
|
196961
|
+
}
|
|
196962
|
+
if (isProviderUsageLimit(input.errorMessage)) {
|
|
196963
|
+
const body = formatProviderUsageLimit({
|
|
196964
|
+
owner: input.repo.owner,
|
|
196965
|
+
name: input.repo.name,
|
|
196966
|
+
raw: input.errorMessage
|
|
196967
|
+
});
|
|
196968
|
+
return { summary: `### \u274C Pullfrog failed
|
|
196969
|
+
|
|
196801
196970
|
${body}`, comment: body };
|
|
196802
196971
|
}
|
|
196803
196972
|
if (isProviderMissingCredential(input.errorMessage)) {
|
|
@@ -196892,6 +197061,16 @@ ${hangBody}`,
|
|
|
196892
197061
|
comment: explained ? hangBody : formatMinimalFailureComment(input.repo)
|
|
196893
197062
|
};
|
|
196894
197063
|
}
|
|
197064
|
+
if (input.errorMessage.startsWith("provider error: ")) {
|
|
197065
|
+
const body = formatProviderErrorBody({
|
|
197066
|
+
owner: input.repo.owner,
|
|
197067
|
+
name: input.repo.name,
|
|
197068
|
+
raw: input.errorMessage
|
|
197069
|
+
});
|
|
197070
|
+
return { summary: `### \u274C Pullfrog failed
|
|
197071
|
+
|
|
197072
|
+
${body}`, comment: body };
|
|
197073
|
+
}
|
|
196895
197074
|
const genericBody = formatGenericFailure(input.errorMessage);
|
|
196896
197075
|
return {
|
|
196897
197076
|
summary: `### \u274C Pullfrog failed
|
|
@@ -197702,7 +197881,7 @@ async function main() {
|
|
|
197702
197881
|
const repoDir = payload.cwd;
|
|
197703
197882
|
const preIntrospectionDirty = repoDir ? await dirtyTrackedPaths({ cwd: repoDir }) : null;
|
|
197704
197883
|
const opencodeCliPath = await agents.opencode.install();
|
|
197705
|
-
captureBaselineModels(opencodeCliPath);
|
|
197884
|
+
captureBaselineModels({ cliPath: opencodeCliPath, tmpdir: tmpdir3 });
|
|
197706
197885
|
const injected = [];
|
|
197707
197886
|
if (runContext.dbSecrets) {
|
|
197708
197887
|
for (const [key, value2] of Object.entries(runContext.dbSecrets)) {
|
|
@@ -197719,7 +197898,7 @@ async function main() {
|
|
|
197719
197898
|
await initializeCredentialPool({ ctx: runContext, slug: payload.model, injected });
|
|
197720
197899
|
installCodexAuth();
|
|
197721
197900
|
installXaiAuth();
|
|
197722
|
-
captureAuthorizedModels(opencodeCliPath);
|
|
197901
|
+
captureAuthorizedModels({ cliPath: opencodeCliPath, tmpdir: tmpdir3 });
|
|
197723
197902
|
if (preIntrospectionDirty && repoDir) {
|
|
197724
197903
|
await restoreDirtiedSince({
|
|
197725
197904
|
before: preIntrospectionDirty,
|
|
@@ -198057,8 +198236,8 @@ ${instructions.user}` : null,
|
|
|
198057
198236
|
log.info(instructions.full);
|
|
198058
198237
|
});
|
|
198059
198238
|
if (agentId === "opencode") {
|
|
198060
|
-
const pluginDir =
|
|
198061
|
-
const hasPlugins =
|
|
198239
|
+
const pluginDir = join28(process.cwd(), ".opencode", "plugin");
|
|
198240
|
+
const hasPlugins = existsSync11(pluginDir) && readdirSync2(pluginDir).some((f) => /\.[jt]sx?$/.test(f));
|
|
198062
198241
|
if (hasPlugins && toolState.dependencyInstallation?.promise) {
|
|
198063
198242
|
log.info(
|
|
198064
198243
|
"\xBB .opencode/plugin/ detected \u2014 awaiting dependency installation before agent start"
|