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/cli.mjs
CHANGED
|
@@ -98361,14 +98361,14 @@ var require_turndown_cjs = __commonJS({
|
|
|
98361
98361
|
} else if (node2.nodeType === 1) {
|
|
98362
98362
|
replacement = replacementForNode.call(self2, node2);
|
|
98363
98363
|
}
|
|
98364
|
-
return
|
|
98364
|
+
return join31(output, replacement);
|
|
98365
98365
|
}, "");
|
|
98366
98366
|
}
|
|
98367
98367
|
function postProcess(output) {
|
|
98368
98368
|
var self2 = this;
|
|
98369
98369
|
this.rules.forEach(function(rule) {
|
|
98370
98370
|
if (typeof rule.append === "function") {
|
|
98371
|
-
output =
|
|
98371
|
+
output = join31(output, rule.append(self2.options));
|
|
98372
98372
|
}
|
|
98373
98373
|
});
|
|
98374
98374
|
return output.replace(/^[\t\r\n]+/, "").replace(/[\t\r\n\s]+$/, "");
|
|
@@ -98380,7 +98380,7 @@ var require_turndown_cjs = __commonJS({
|
|
|
98380
98380
|
if (whitespace.leading || whitespace.trailing) content = content.trim();
|
|
98381
98381
|
return whitespace.leading + rule.replacement(content, node2, this.options) + whitespace.trailing;
|
|
98382
98382
|
}
|
|
98383
|
-
function
|
|
98383
|
+
function join31(output, replacement) {
|
|
98384
98384
|
var s1 = trimTrailingNewlines(output);
|
|
98385
98385
|
var s2 = trimLeadingNewlines(replacement);
|
|
98386
98386
|
var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
|
|
@@ -106963,7 +106963,7 @@ var import_picocolors = __toESM(require_picocolors(), 1);
|
|
|
106963
106963
|
// package.json
|
|
106964
106964
|
var package_default = {
|
|
106965
106965
|
name: "pullfrog",
|
|
106966
|
-
version: "0.1.
|
|
106966
|
+
version: "0.1.97",
|
|
106967
106967
|
cliContractVersion: "1",
|
|
106968
106968
|
type: "module",
|
|
106969
106969
|
bin: {
|
|
@@ -108607,9 +108607,9 @@ var import_arg6 = __toESM(require_arg(), 1);
|
|
|
108607
108607
|
import { dirname as dirname6 } from "node:path";
|
|
108608
108608
|
|
|
108609
108609
|
// main.ts
|
|
108610
|
-
import { existsSync as
|
|
108610
|
+
import { existsSync as existsSync13, readdirSync as readdirSync2 } from "node:fs";
|
|
108611
108611
|
import { readFile as readFile6 } from "node:fs/promises";
|
|
108612
|
-
import { join as
|
|
108612
|
+
import { join as join29 } from "node:path";
|
|
108613
108613
|
|
|
108614
108614
|
// node_modules/.pnpm/package-manager-detector@1.8.0/node_modules/package-manager-detector/dist/commands.mjs
|
|
108615
108615
|
function splitRunArgs(args2, valueFlags = []) {
|
|
@@ -109618,6 +109618,15 @@ var providers = {
|
|
|
109618
109618
|
openRouterEffort: [],
|
|
109619
109619
|
openRouterResolve: "openrouter/qwen/qwen3.8-flash"
|
|
109620
109620
|
},
|
|
109621
|
+
// Zen lists Mistral Large 4 at half its list price for launch, level with
|
|
109622
|
+
// the routers. hidden until a published `pullfrog` resolves it, as there.
|
|
109623
|
+
"mistral-large": {
|
|
109624
|
+
displayName: "Mistral Large",
|
|
109625
|
+
resolve: "opencode/mistral-large-4",
|
|
109626
|
+
effort: ["none", "high"],
|
|
109627
|
+
openRouterResolve: "openrouter/mistralai/mistral-large-4-0",
|
|
109628
|
+
hidden: true
|
|
109629
|
+
},
|
|
109621
109630
|
"gpt-5-nano": {
|
|
109622
109631
|
displayName: "GPT Nano",
|
|
109623
109632
|
resolve: "opencode/gpt-5.4-nano",
|
|
@@ -109666,12 +109675,12 @@ var providers = {
|
|
|
109666
109675
|
fallback: "opencode/big-pickle"
|
|
109667
109676
|
},
|
|
109668
109677
|
// Zen's FREE stealth preview (listed 2026-09-23), the third after
|
|
109669
|
-
// big-pickle and union-alpha.
|
|
109670
|
-
//
|
|
109671
|
-
//
|
|
109678
|
+
// big-pickle and union-alpha. Zen's docs: zero retention, no training.
|
|
109679
|
+
// unlike the first two it publishes an effort ladder, so the gate
|
|
109680
|
+
// mirrors one here.
|
|
109672
109681
|
"space-bunny": {
|
|
109673
109682
|
displayName: "Space Bunny",
|
|
109674
|
-
description: "Stealth preview
|
|
109683
|
+
description: "Stealth preview, free for a limited time",
|
|
109675
109684
|
resolve: "opencode/space-bunny-free",
|
|
109676
109685
|
effort: ["low", "medium", "high", "xhigh", "max"],
|
|
109677
109686
|
// free to run, still gated on the provider's own OPENCODE_API_KEY —
|
|
@@ -109689,17 +109698,30 @@ var providers = {
|
|
|
109689
109698
|
// see the big-pickle note above (#1077).
|
|
109690
109699
|
isFree: true
|
|
109691
109700
|
},
|
|
109692
|
-
// Zen's FREE stealth preview (listed 2026-10-01), the fifth promo. Zen
|
|
109693
|
-
//
|
|
109701
|
+
// Zen's FREE stealth preview (listed 2026-10-01), the fifth promo. Zen's
|
|
109702
|
+
// docs: data may be used to improve the model during the free period.
|
|
109694
109703
|
"fledge-alpha": {
|
|
109695
109704
|
displayName: "Fledge Alpha",
|
|
109696
|
-
description: "Stealth preview
|
|
109705
|
+
description: "Stealth preview; data may be used to improve it",
|
|
109697
109706
|
resolve: "opencode/fledge-alpha-free",
|
|
109698
109707
|
effort: ["low", "high", "max"],
|
|
109699
109708
|
// free to run, still gated on the provider's own OPENCODE_API_KEY —
|
|
109700
109709
|
// see the big-pickle note above (#1077).
|
|
109701
109710
|
isFree: true
|
|
109702
109711
|
},
|
|
109712
|
+
// Zen's FREE preview (listed 2026-10-06), the sixth promo, on Fledge
|
|
109713
|
+
// Alpha's terms. hidden until a published `pullfrog` resolves it.
|
|
109714
|
+
exo: {
|
|
109715
|
+
displayName: "Exo",
|
|
109716
|
+
description: "Preview; data may be used to improve it",
|
|
109717
|
+
resolve: "opencode/exo-free",
|
|
109718
|
+
// Zen publishes a single rung.
|
|
109719
|
+
effort: ["high"],
|
|
109720
|
+
// free to run, still gated on the provider's own OPENCODE_API_KEY —
|
|
109721
|
+
// see the big-pickle note above (#1077).
|
|
109722
|
+
isFree: true,
|
|
109723
|
+
hidden: true
|
|
109724
|
+
},
|
|
109703
109725
|
// Zen's live free MiMo, and the second free row in a menu that big-pickle
|
|
109704
109726
|
// was alone in since `mimo-v2-pro-free` lost its model.
|
|
109705
109727
|
mimo: {
|
|
@@ -110155,6 +110177,17 @@ var providers = {
|
|
|
110155
110177
|
resolve: "openrouter/meta/muse-spark-1.3-contributor",
|
|
110156
110178
|
effort: ["minimal", "low", "medium", "high", "xhigh", "max"],
|
|
110157
110179
|
openRouterResolve: "openrouter/meta/muse-spark-1.3-contributor"
|
|
110180
|
+
},
|
|
110181
|
+
// Mistral's flagship (public preview, 2026-10-06), on the routers only:
|
|
110182
|
+
// the catalog carries no direct Mistral provider. out of the pickers
|
|
110183
|
+
// until a published `pullfrog` resolves the slug; see "A new alias
|
|
110184
|
+
// reaches the picker at merge".
|
|
110185
|
+
"mistral-large": {
|
|
110186
|
+
displayName: "Mistral Large",
|
|
110187
|
+
resolve: "openrouter/mistralai/mistral-large-4-0",
|
|
110188
|
+
effort: ["none", "high"],
|
|
110189
|
+
openRouterResolve: "openrouter/mistralai/mistral-large-4-0",
|
|
110190
|
+
hidden: true
|
|
110158
110191
|
}
|
|
110159
110192
|
}
|
|
110160
110193
|
}),
|
|
@@ -110246,6 +110279,13 @@ var providers = {
|
|
|
110246
110279
|
displayName: "Muse Spark",
|
|
110247
110280
|
resolve: "vercel/meta/muse-spark-1.3",
|
|
110248
110281
|
effort: ["minimal", "low", "medium", "high", "xhigh"]
|
|
110282
|
+
},
|
|
110283
|
+
// hidden until a published `pullfrog` resolves it, as on OpenRouter.
|
|
110284
|
+
"mistral-large": {
|
|
110285
|
+
displayName: "Mistral Large",
|
|
110286
|
+
resolve: "vercel/mistral/mistral-large-4",
|
|
110287
|
+
effort: ["none", "low", "medium", "high"],
|
|
110288
|
+
hidden: true
|
|
110249
110289
|
}
|
|
110250
110290
|
}
|
|
110251
110291
|
})
|
|
@@ -111126,7 +111166,13 @@ var PROVIDER_ERROR_PATTERNS = [
|
|
|
111126
111166
|
// …/settings/credits`) matched neither this nor `requires more credits`, so
|
|
111127
111167
|
// the one run in that episode that was a genuinely empty wallet rendered with
|
|
111128
111168
|
// no CTA at all while runs 1-6 rendered correctly.
|
|
111129
|
-
|
|
111169
|
+
// `account funds` is Zen's (#1474).
|
|
111170
|
+
{
|
|
111171
|
+
regex: /Insufficient (?:balance|credits|account funds)/i,
|
|
111172
|
+
label: PROVIDER_BILLING_EXHAUSTED_LABEL
|
|
111173
|
+
},
|
|
111174
|
+
// OpenAI's `You have no credits remaining` (#1474).
|
|
111175
|
+
{ regex: /no credits remaining/i, label: PROVIDER_BILLING_EXHAUSTED_LABEL },
|
|
111130
111176
|
{ regex: /credit balance is too low/i, label: PROVIDER_BILLING_EXHAUSTED_LABEL },
|
|
111131
111177
|
// "spending cap" (Gemini) and "spending limit" (xAI, #1076) are the same
|
|
111132
111178
|
// condition in two house styles — a configured ceiling was reached.
|
|
@@ -111247,6 +111293,9 @@ function isRouterKeylimitExhaustedError(text) {
|
|
|
111247
111293
|
function isProviderBillingExhausted(text) {
|
|
111248
111294
|
return findProviderErrorMatch(text)?.label === PROVIDER_BILLING_EXHAUSTED_LABEL;
|
|
111249
111295
|
}
|
|
111296
|
+
function isProviderUsageLimit(text) {
|
|
111297
|
+
return findProviderErrorMatch(text)?.label === PROVIDER_USAGE_LIMIT_LABEL;
|
|
111298
|
+
}
|
|
111250
111299
|
function isProviderNoRoutableEndpoints(text) {
|
|
111251
111300
|
return findProviderErrorMatch(text)?.label === PROVIDER_NO_ENDPOINTS_LABEL;
|
|
111252
111301
|
}
|
|
@@ -111505,6 +111554,7 @@ ${this.buffer}`;
|
|
|
111505
111554
|
};
|
|
111506
111555
|
async function spawn3(options) {
|
|
111507
111556
|
const activityTimeoutMs = options.activityTimeout ?? DEFAULT_ACTIVITY_TIMEOUT_MS;
|
|
111557
|
+
options.signal?.throwIfAborted();
|
|
111508
111558
|
installSignalHandler();
|
|
111509
111559
|
const startTime = performance3.now();
|
|
111510
111560
|
const retain = options.retain ?? "tail";
|
|
@@ -111540,17 +111590,18 @@ async function spawn3(options) {
|
|
|
111540
111590
|
let isActivityTimedOut = false;
|
|
111541
111591
|
let lastActivityTime = performance3.now();
|
|
111542
111592
|
let killedAtIdleMs;
|
|
111593
|
+
const terminate = () => {
|
|
111594
|
+
if (sigkillEscalatorId) return;
|
|
111595
|
+
killSelf("SIGTERM");
|
|
111596
|
+
sigkillEscalatorId = setTimeout(() => killSelf("SIGKILL"), 5e3);
|
|
111597
|
+
};
|
|
111543
111598
|
if (options.timeout) {
|
|
111544
111599
|
timeoutId = setTimeout(() => {
|
|
111545
111600
|
isTimedOut = true;
|
|
111546
|
-
|
|
111547
|
-
sigkillEscalatorId = setTimeout(() => {
|
|
111548
|
-
if (!child.killed) {
|
|
111549
|
-
killSelf("SIGKILL");
|
|
111550
|
-
}
|
|
111551
|
-
}, 5e3);
|
|
111601
|
+
terminate();
|
|
111552
111602
|
}, options.timeout);
|
|
111553
111603
|
}
|
|
111604
|
+
options.signal?.addEventListener("abort", terminate, { once: true });
|
|
111554
111605
|
if (activityTimeoutMs > 0) {
|
|
111555
111606
|
log2.debug(
|
|
111556
111607
|
`spawn activity timer: pid=${child.pid} cmd=${options.cmd} timeout=${activityTimeoutMs}ms`
|
|
@@ -111615,6 +111666,11 @@ async function spawn3(options) {
|
|
|
111615
111666
|
if (timeoutId) clearTimeout(timeoutId);
|
|
111616
111667
|
if (sigkillEscalatorId) clearTimeout(sigkillEscalatorId);
|
|
111617
111668
|
if (activityCheckIntervalId) clearInterval(activityCheckIntervalId);
|
|
111669
|
+
options.signal?.removeEventListener("abort", terminate);
|
|
111670
|
+
if (options.signal?.aborted) {
|
|
111671
|
+
reject(options.signal.reason);
|
|
111672
|
+
return;
|
|
111673
|
+
}
|
|
111618
111674
|
if (isTimedOut) {
|
|
111619
111675
|
reject(
|
|
111620
111676
|
new SpawnTimeoutError(`process timed out after ${options.timeout}ms`, SPAWN_TIMEOUT_CODE)
|
|
@@ -111653,6 +111709,7 @@ ${killMsg}` : killMsg;
|
|
|
111653
111709
|
if (timeoutId) clearTimeout(timeoutId);
|
|
111654
111710
|
if (sigkillEscalatorId) clearTimeout(sigkillEscalatorId);
|
|
111655
111711
|
if (activityCheckIntervalId) clearInterval(activityCheckIntervalId);
|
|
111712
|
+
options.signal?.removeEventListener("abort", terminate);
|
|
111656
111713
|
const errMsg = `[spawn] ${options.cmd}: ${error49.message}`;
|
|
111657
111714
|
console.error(errMsg);
|
|
111658
111715
|
const existingStderr = stderrBuffer?.toString() ?? "";
|
|
@@ -111894,17 +111951,16 @@ Report findings clearly with file:line references and quoted evidence where poss
|
|
|
111894
111951
|
// modes.ts
|
|
111895
111952
|
var PR_SUMMARY_FORMAT = `### Default format
|
|
111896
111953
|
|
|
111897
|
-
The body has at most
|
|
111954
|
+
The body has at most two parts, in this order:
|
|
111898
111955
|
|
|
111899
111956
|
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.
|
|
111900
111957
|
2. **Cross-cutting issue sections** (zero or more) \u2014 one \`### {emoji} {what's wrong, not what to do}\` heading per concern.
|
|
111901
|
-
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.
|
|
111902
111958
|
|
|
111903
111959
|
**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.
|
|
111904
111960
|
|
|
111905
111961
|
**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).
|
|
111906
111962
|
|
|
111907
|
-
**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.
|
|
111963
|
+
**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.
|
|
111908
111964
|
|
|
111909
111965
|
**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.
|
|
111910
111966
|
|
|
@@ -112053,8 +112109,6 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
112053
112109
|
|
|
112054
112110
|
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.
|
|
112055
112111
|
|
|
112056
|
-
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.
|
|
112057
|
-
|
|
112058
112112
|
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.
|
|
112059
112113
|
|
|
112060
112114
|
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.
|
|
@@ -112078,8 +112132,6 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
112078
112132
|
|
|
112079
112133
|
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.
|
|
112080
112134
|
|
|
112081
|
-
**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.
|
|
112082
|
-
|
|
112083
112135
|
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.
|
|
112084
112136
|
|
|
112085
112137
|
7. **submit**: ALWAYS submit exactly one review via \`${t2("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.
|
|
@@ -112101,7 +112153,7 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
112101
112153
|
\`approved: false\`. Body opens with \`> [!CAUTION]\\n> This PR introduces ...\`, followed by the PR summary. Include all inline comments via \`comments\`.
|
|
112102
112154
|
- **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):
|
|
112103
112155
|
\`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\`.
|
|
112104
|
-
- **minor suggestions only** (low-stakes findings
|
|
112156
|
+
- **minor suggestions only** (low-stakes findings):
|
|
112105
112157
|
\`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.
|
|
112106
112158
|
- **informational observations** (mergeable as-is, nothing actionable \u2014 e.g. prior feedback addressed cleanly, calling out something noteworthy without recommending a change):
|
|
112107
112159
|
\`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.
|
|
@@ -112144,8 +112196,6 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
112144
112196
|
|
|
112145
112197
|
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.**
|
|
112146
112198
|
|
|
112147
|
-
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).
|
|
112148
|
-
|
|
112149
112199
|
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.
|
|
112150
112200
|
|
|
112151
112201
|
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.
|
|
@@ -112169,11 +112219,9 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
112169
112219
|
|
|
112170
112220
|
8. **aggregate, draft, self-critique**: merge findings (yours + output from every specialist you dispatched); de-dup overlaps; trace each finding yourself. drop praise, style preferences, speculative/unverified claims, findings about pre-existing code unrelated to the new commits, anything not actionable, and anything that re-states prior review feedback (heuristic: if the finding's root cause lives in lines the *new commits* added or modified, it's in scope; otherwise drop). also drop **bloat-shaped findings** \u2014 proposed fixes that would add defensive checks for cases that can't happen, abstractions used once, comments restating obvious code, tests asserting tautologies, or "just-in-case" guards. subagents are fallible and bias toward recommending changes; the bar for an actionable inline comment is sound + correct + elegant. recommending a change that improves only one of the three (or degrades elegance to nominally improve correctness) makes the codebase worse, not better. To compute "lines the new commits added or modified": if \`incrementalDiffPath\` from step 2 is present, use it directly. Otherwise, take the prior Pullfrog review's \`commit_id\` (returned alongside each entry from \`${t2("list_pull_request_reviews")}\` in step 4) and run \`git diff <prior-review-sha>..HEAD\` to isolate the lines added since that review.
|
|
112171
112221
|
|
|
112172
|
-
**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.
|
|
112173
|
-
|
|
112174
112222
|
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.
|
|
112175
112223
|
|
|
112176
|
-
9. **build the review body**: use the same default format as Review mode (preamble + optional cross-cutting \`### \` sections
|
|
112224
|
+
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.
|
|
112177
112225
|
|
|
112178
112226
|
10. Submit \u2014 every run must end with EXACTLY ONE of \`${t2("create_pull_request_review")}\` (substantive review) or \`${t2("report_progress")}\` (no-review acknowledgement). do NOT call \`create_issue_comment\` for review output.
|
|
112179
112227
|
|
|
@@ -112184,7 +112232,7 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
112184
112232
|
- IF NO NEW ISSUES, NON-SUBSTANTIVE CHANGES ONLY (trivial formatting, import reordering, comment tweaks): do NOT submit a review. Instead call \`${t2("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.
|
|
112185
112233
|
- ELSE IF NEW CRITICAL ISSUES (blocks merge \u2014 bugs, security, data loss, broken core flows): call \`${t2("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.
|
|
112186
112234
|
- 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 \`${t2("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.
|
|
112187
|
-
- ELSE IF NEW MINOR SUGGESTIONS ONLY (low-stakes findings
|
|
112235
|
+
- ELSE IF NEW MINOR SUGGESTIONS ONLY (low-stakes findings): call \`${t2("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.
|
|
112188
112236
|
- ELSE IF INFORMATIONAL OBSERVATIONS (mergeable as-is, but worth surfacing \u2014 e.g. prior feedback addressed cleanly, or a noteworthy positive observation): call \`${t2("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.
|
|
112189
112237
|
- ELSE IF NO NEW ISSUES, SUBSTANTIVE CHANGES (new functionality, behavior changes, or fixes to prior review feedback): call \`${t2("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.
|
|
112190
112238
|
|
|
@@ -113401,186 +113449,13 @@ import { performance as performance6 } from "node:perf_hooks";
|
|
|
113401
113449
|
|
|
113402
113450
|
// utils/codexHome.ts
|
|
113403
113451
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
113404
|
-
import { existsSync as
|
|
113452
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, mkdtempSync as mkdtempSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
113405
113453
|
import { homedir as homedir2, userInfo } from "node:os";
|
|
113406
113454
|
import { join as join6 } from "node:path";
|
|
113407
|
-
var CODEX_AUTH_ENV = "CODEX_AUTH_JSON";
|
|
113408
|
-
var XAI_AUTH_ENV = "GROK_AUTH_JSON";
|
|
113409
|
-
var installedSubscriptionPaths = /* @__PURE__ */ new Map();
|
|
113410
|
-
function clearInstalledSubscription(name) {
|
|
113411
|
-
const path4 = installedSubscriptionPaths.get(name);
|
|
113412
|
-
if (!path4 || !existsSync5(path4)) return;
|
|
113413
|
-
const value2 = JSON.parse(readFileSync3(path4, "utf8"));
|
|
113414
|
-
if (!value2 || typeof value2 !== "object") return;
|
|
113415
|
-
if (name === "CODEX_AUTH_JSON" && "openai" in value2) delete value2.openai;
|
|
113416
|
-
if (name === "GROK_AUTH_JSON" && "xai" in value2) delete value2.xai;
|
|
113417
|
-
writeFileSync5(path4, `${JSON.stringify(value2)}
|
|
113418
|
-
`, { mode: 384 });
|
|
113419
|
-
installedSubscriptionPaths.delete(name);
|
|
113420
|
-
}
|
|
113421
|
-
var PULLFROG_DATA_DIR = "/var/lib/pullfrog";
|
|
113422
|
-
function isRejectedChain(body) {
|
|
113423
|
-
if (!body.refresh_rejected_at) return false;
|
|
113424
|
-
log2.warning(
|
|
113425
|
-
`\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.`
|
|
113426
|
-
);
|
|
113427
|
-
return true;
|
|
113428
|
-
}
|
|
113429
|
-
function installCodexAuth() {
|
|
113430
|
-
const raw2 = process.env[CODEX_AUTH_ENV];
|
|
113431
|
-
if (!raw2) return null;
|
|
113432
|
-
const body = parseCodexAuthBody(raw2);
|
|
113433
|
-
if (!body) {
|
|
113434
|
-
log2.warning(`\xBB ${CODEX_AUTH_ENV} present but malformed; ignoring`);
|
|
113435
|
-
return null;
|
|
113436
|
-
}
|
|
113437
|
-
if (isRejectedChain(body)) return null;
|
|
113438
|
-
const expiresMs = decodeJwtExpMs(body.tokens.access_token) ?? 0;
|
|
113439
|
-
const xdgDataHome = resolveDataHome();
|
|
113440
|
-
if (!xdgDataHome) return null;
|
|
113441
|
-
const opencodeDir = join6(xdgDataHome, "opencode");
|
|
113442
|
-
const authPath = join6(opencodeDir, "auth.json");
|
|
113443
|
-
writeOpenCodeAuthEntry({
|
|
113444
|
-
opencodeDir,
|
|
113445
|
-
authPath,
|
|
113446
|
-
provider: "openai",
|
|
113447
|
-
entry: {
|
|
113448
|
-
type: "oauth",
|
|
113449
|
-
refresh: body.tokens.refresh_token,
|
|
113450
|
-
access: body.tokens.access_token,
|
|
113451
|
-
expires: expiresMs,
|
|
113452
|
-
...body.tokens.account_id ? { accountId: body.tokens.account_id } : {}
|
|
113453
|
-
}
|
|
113454
|
-
});
|
|
113455
|
-
process.env.XDG_DATA_HOME = xdgDataHome;
|
|
113456
|
-
log2.info(`\xBB installed Codex auth at ${authPath}`);
|
|
113457
|
-
installedSubscriptionPaths.set(CODEX_AUTH_ENV, authPath);
|
|
113458
|
-
return {
|
|
113459
|
-
authPath,
|
|
113460
|
-
xdgDataHome,
|
|
113461
|
-
originalRefresh: body.tokens.refresh_token,
|
|
113462
|
-
originalIdToken: body.tokens.id_token
|
|
113463
|
-
};
|
|
113464
|
-
}
|
|
113465
|
-
function writeOpenCodeAuthEntry(params) {
|
|
113466
|
-
let existing = {};
|
|
113467
|
-
if (existsSync5(params.authPath)) {
|
|
113468
|
-
try {
|
|
113469
|
-
const parsed2 = JSON.parse(readFileSync3(params.authPath, "utf8"));
|
|
113470
|
-
if (parsed2 && typeof parsed2 === "object") existing = parsed2;
|
|
113471
|
-
} catch {
|
|
113472
|
-
log2.warning(`\xBB ${params.authPath} was unreadable; rewriting it`);
|
|
113473
|
-
}
|
|
113474
|
-
}
|
|
113475
|
-
const merged = { ...existing, [params.provider]: params.entry };
|
|
113476
|
-
mkdirSync5(params.opencodeDir, { recursive: true });
|
|
113477
|
-
writeFileSync5(params.authPath, `${JSON.stringify(merged, null, 2)}
|
|
113478
|
-
`, { mode: 384 });
|
|
113479
|
-
}
|
|
113480
|
-
function installXaiAuth() {
|
|
113481
|
-
const raw2 = process.env[XAI_AUTH_ENV];
|
|
113482
|
-
if (!raw2) return null;
|
|
113483
|
-
const body = parseXaiAuthBody(raw2);
|
|
113484
|
-
if (!body) {
|
|
113485
|
-
log2.warning(`\xBB ${XAI_AUTH_ENV} present but malformed; ignoring`);
|
|
113486
|
-
return null;
|
|
113487
|
-
}
|
|
113488
|
-
if (isXaiRejectedChain(body)) return null;
|
|
113489
|
-
const xdgDataHome = resolveDataHome();
|
|
113490
|
-
if (!xdgDataHome) return null;
|
|
113491
|
-
const opencodeDir = join6(xdgDataHome, "opencode");
|
|
113492
|
-
const authPath = join6(opencodeDir, "auth.json");
|
|
113493
|
-
writeOpenCodeAuthEntry({
|
|
113494
|
-
opencodeDir,
|
|
113495
|
-
authPath,
|
|
113496
|
-
provider: "xai",
|
|
113497
|
-
// opencode refreshes when `expires` is inside its skew window, and falls
|
|
113498
|
-
// back to the JWT's own exp when the stored value is absent. Decoding it
|
|
113499
|
-
// here means the first request uses the token we already hold instead of
|
|
113500
|
-
// burning a rotation on startup.
|
|
113501
|
-
entry: {
|
|
113502
|
-
type: "oauth",
|
|
113503
|
-
refresh: body.tokens.refresh_token,
|
|
113504
|
-
access: body.tokens.access_token,
|
|
113505
|
-
expires: decodeJwtExpMs(body.tokens.access_token) ?? 0
|
|
113506
|
-
}
|
|
113507
|
-
});
|
|
113508
|
-
process.env.XDG_DATA_HOME = xdgDataHome;
|
|
113509
|
-
log2.info(`\xBB installed Grok auth at ${authPath}`);
|
|
113510
|
-
installedSubscriptionPaths.set(XAI_AUTH_ENV, authPath);
|
|
113511
|
-
return { authPath, xdgDataHome, originalRefresh: body.tokens.refresh_token };
|
|
113512
|
-
}
|
|
113513
|
-
function isXaiRejectedChain(body) {
|
|
113514
|
-
if (!body.refresh_rejected_at) return false;
|
|
113515
|
-
log2.warning(
|
|
113516
|
-
`\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.`
|
|
113517
|
-
);
|
|
113518
|
-
return true;
|
|
113519
|
-
}
|
|
113520
|
-
function installCodexHome() {
|
|
113521
|
-
const raw2 = process.env[CODEX_AUTH_ENV];
|
|
113522
|
-
if (!raw2) return null;
|
|
113523
|
-
const body = parseCodexAuthBody(raw2);
|
|
113524
|
-
if (!body) {
|
|
113525
|
-
log2.warning(`\xBB ${CODEX_AUTH_ENV} present but malformed; ignoring`);
|
|
113526
|
-
return null;
|
|
113527
|
-
}
|
|
113528
|
-
if (isRejectedChain(body)) return null;
|
|
113529
|
-
if (!body.tokens.id_token) {
|
|
113530
|
-
log2.warning(
|
|
113531
|
-
`\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.`
|
|
113532
|
-
);
|
|
113533
|
-
return null;
|
|
113534
|
-
}
|
|
113535
|
-
const root = resolveDataHome();
|
|
113536
|
-
if (!root) return null;
|
|
113537
|
-
const codexHome = join6(root, "codex");
|
|
113538
|
-
const authPath = join6(codexHome, "auth.json");
|
|
113539
|
-
mkdirSync5(codexHome, { recursive: true });
|
|
113540
|
-
writeFileSync5(authPath, stringifyCodexAuthBody(body), { mode: 384 });
|
|
113541
|
-
log2.info(`\xBB installed Codex auth at ${authPath}`);
|
|
113542
|
-
return { codexHome, authPath, originalRefresh: body.tokens.refresh_token };
|
|
113543
|
-
}
|
|
113544
|
-
var dataHome;
|
|
113545
|
-
var unhostedSubscriptions = /* @__PURE__ */ new Set();
|
|
113546
|
-
function canInstallSubscription(name) {
|
|
113547
|
-
return name !== CODEX_AUTH_ENV && name !== XAI_AUTH_ENV || resolveDataHome() !== null;
|
|
113548
|
-
}
|
|
113549
|
-
function resolveDataHome() {
|
|
113550
|
-
if (process.env.CI !== "true") return join6(homedir2(), ".local", "share");
|
|
113551
|
-
if (dataHome === void 0) dataHome = bootstrapPullfrogDataDir();
|
|
113552
|
-
return dataHome;
|
|
113553
|
-
}
|
|
113554
|
-
function bootstrapPullfrogDataDir() {
|
|
113555
|
-
const user = userInfo().username;
|
|
113556
|
-
let primaryGroup;
|
|
113557
|
-
try {
|
|
113558
|
-
primaryGroup = execFileSync4("id", ["-gn", user], { stdio: "pipe", encoding: "utf-8" }).trim();
|
|
113559
|
-
} catch {
|
|
113560
|
-
primaryGroup = user;
|
|
113561
|
-
}
|
|
113562
|
-
try {
|
|
113563
|
-
execFileSync4("sudo", ["-n", "mkdir", "-p", PULLFROG_DATA_DIR], { stdio: "pipe" });
|
|
113564
|
-
execFileSync4("sudo", ["-n", "chown", `${user}:${primaryGroup}`, PULLFROG_DATA_DIR], {
|
|
113565
|
-
stdio: "pipe"
|
|
113566
|
-
});
|
|
113567
|
-
execFileSync4("sudo", ["-n", "chmod", "700", PULLFROG_DATA_DIR], { stdio: "pipe" });
|
|
113568
|
-
} catch (err) {
|
|
113569
|
-
log2.warning(
|
|
113570
|
-
`\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.`
|
|
113571
|
-
);
|
|
113572
|
-
for (const name of [CODEX_AUTH_ENV, XAI_AUTH_ENV]) {
|
|
113573
|
-
if (process.env[name]) unhostedSubscriptions.add(name);
|
|
113574
|
-
delete process.env[name];
|
|
113575
|
-
}
|
|
113576
|
-
return null;
|
|
113577
|
-
}
|
|
113578
|
-
return PULLFROG_DATA_DIR;
|
|
113579
|
-
}
|
|
113580
113455
|
|
|
113581
113456
|
// utils/oauthWriteback.ts
|
|
113582
113457
|
var core2 = __toESM(require_core(), 1);
|
|
113583
|
-
import { existsSync as
|
|
113458
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3, rmSync as rmSync3 } from "node:fs";
|
|
113584
113459
|
|
|
113585
113460
|
// utils/apiUrl.ts
|
|
113586
113461
|
function isLocalUrl(url4) {
|
|
@@ -113712,7 +113587,15 @@ function detectXaiRefresh(params) {
|
|
|
113712
113587
|
|
|
113713
113588
|
// utils/oauthWriteback.ts
|
|
113714
113589
|
var OAUTH_WRITEBACK_STATE = "oauth_writeback";
|
|
113590
|
+
var SUBSCRIPTION_DATA_DIR_STATE = "subscription_data_dir";
|
|
113715
113591
|
async function runOAuthWriteback() {
|
|
113592
|
+
await writeBackEntries();
|
|
113593
|
+
const dataDir = core2.getState(SUBSCRIPTION_DATA_DIR_STATE);
|
|
113594
|
+
if (!dataDir) return;
|
|
113595
|
+
rmSync3(dataDir, { recursive: true, force: true });
|
|
113596
|
+
core2.info(`oauth post-hook: removed ${dataDir}`);
|
|
113597
|
+
}
|
|
113598
|
+
async function writeBackEntries() {
|
|
113716
113599
|
const raw2 = core2.getState(OAUTH_WRITEBACK_STATE);
|
|
113717
113600
|
if (!raw2) {
|
|
113718
113601
|
core2.info("oauth post-hook: no writeback state \u2014 skipping");
|
|
@@ -113738,13 +113621,13 @@ async function writeBackEntry(apiToken, entry) {
|
|
|
113738
113621
|
core2.warning("oauth post-hook: incomplete writeback entry \u2014 skipping");
|
|
113739
113622
|
return;
|
|
113740
113623
|
}
|
|
113741
|
-
if (!
|
|
113624
|
+
if (!existsSync5(entry.authPath)) {
|
|
113742
113625
|
core2.info(`oauth post-hook: ${entry.authPath} not found \u2014 nothing to write back`);
|
|
113743
113626
|
return;
|
|
113744
113627
|
}
|
|
113745
113628
|
let authFileContent;
|
|
113746
113629
|
try {
|
|
113747
|
-
authFileContent =
|
|
113630
|
+
authFileContent = readFileSync3(entry.authPath, "utf8");
|
|
113748
113631
|
} catch (err) {
|
|
113749
113632
|
core2.warning(`oauth post-hook: cannot read ${entry.authPath} \u2014 ${err}`);
|
|
113750
113633
|
return;
|
|
@@ -113801,6 +113684,188 @@ function setSecretOutput(name, value2) {
|
|
|
113801
113684
|
if (isGitHubActions) core3.setOutput(name, value2);
|
|
113802
113685
|
}
|
|
113803
113686
|
|
|
113687
|
+
// utils/codexHome.ts
|
|
113688
|
+
var CODEX_AUTH_ENV = "CODEX_AUTH_JSON";
|
|
113689
|
+
var XAI_AUTH_ENV = "GROK_AUTH_JSON";
|
|
113690
|
+
var installedSubscriptionPaths = /* @__PURE__ */ new Map();
|
|
113691
|
+
var installedDataHome;
|
|
113692
|
+
function getSubscriptionDataHome() {
|
|
113693
|
+
return installedDataHome;
|
|
113694
|
+
}
|
|
113695
|
+
function clearInstalledSubscription(name) {
|
|
113696
|
+
const path4 = installedSubscriptionPaths.get(name);
|
|
113697
|
+
if (!path4 || !existsSync6(path4)) return;
|
|
113698
|
+
const value2 = JSON.parse(readFileSync4(path4, "utf8"));
|
|
113699
|
+
if (!value2 || typeof value2 !== "object") return;
|
|
113700
|
+
if (name === "CODEX_AUTH_JSON" && "openai" in value2) delete value2.openai;
|
|
113701
|
+
if (name === "GROK_AUTH_JSON" && "xai" in value2) delete value2.xai;
|
|
113702
|
+
writeFileSync5(path4, `${JSON.stringify(value2)}
|
|
113703
|
+
`, { mode: 384 });
|
|
113704
|
+
installedSubscriptionPaths.delete(name);
|
|
113705
|
+
}
|
|
113706
|
+
var PULLFROG_DATA_DIR = "/var/lib/pullfrog";
|
|
113707
|
+
function isRejectedChain(body) {
|
|
113708
|
+
if (!body.refresh_rejected_at) return false;
|
|
113709
|
+
log2.warning(
|
|
113710
|
+
`\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.`
|
|
113711
|
+
);
|
|
113712
|
+
return true;
|
|
113713
|
+
}
|
|
113714
|
+
function installCodexAuth() {
|
|
113715
|
+
const raw2 = process.env[CODEX_AUTH_ENV];
|
|
113716
|
+
if (!raw2) return null;
|
|
113717
|
+
const body = parseCodexAuthBody(raw2);
|
|
113718
|
+
if (!body) {
|
|
113719
|
+
log2.warning(`\xBB ${CODEX_AUTH_ENV} present but malformed; ignoring`);
|
|
113720
|
+
return null;
|
|
113721
|
+
}
|
|
113722
|
+
if (isRejectedChain(body)) return null;
|
|
113723
|
+
const expiresMs = decodeJwtExpMs(body.tokens.access_token) ?? 0;
|
|
113724
|
+
const xdgDataHome = resolveDataHome();
|
|
113725
|
+
if (!xdgDataHome) return null;
|
|
113726
|
+
const opencodeDir = join6(xdgDataHome, "opencode");
|
|
113727
|
+
const authPath = join6(opencodeDir, "auth.json");
|
|
113728
|
+
writeOpenCodeAuthEntry({
|
|
113729
|
+
opencodeDir,
|
|
113730
|
+
authPath,
|
|
113731
|
+
provider: "openai",
|
|
113732
|
+
entry: {
|
|
113733
|
+
type: "oauth",
|
|
113734
|
+
refresh: body.tokens.refresh_token,
|
|
113735
|
+
access: body.tokens.access_token,
|
|
113736
|
+
expires: expiresMs,
|
|
113737
|
+
...body.tokens.account_id ? { accountId: body.tokens.account_id } : {}
|
|
113738
|
+
}
|
|
113739
|
+
});
|
|
113740
|
+
installedDataHome = xdgDataHome;
|
|
113741
|
+
log2.info(`\xBB installed Codex auth at ${authPath}`);
|
|
113742
|
+
installedSubscriptionPaths.set(CODEX_AUTH_ENV, authPath);
|
|
113743
|
+
return {
|
|
113744
|
+
authPath,
|
|
113745
|
+
originalRefresh: body.tokens.refresh_token,
|
|
113746
|
+
originalIdToken: body.tokens.id_token
|
|
113747
|
+
};
|
|
113748
|
+
}
|
|
113749
|
+
function writeOpenCodeAuthEntry(params) {
|
|
113750
|
+
let existing = {};
|
|
113751
|
+
if (existsSync6(params.authPath)) {
|
|
113752
|
+
try {
|
|
113753
|
+
const parsed2 = JSON.parse(readFileSync4(params.authPath, "utf8"));
|
|
113754
|
+
if (parsed2 && typeof parsed2 === "object") existing = parsed2;
|
|
113755
|
+
} catch {
|
|
113756
|
+
log2.warning(`\xBB ${params.authPath} was unreadable; rewriting it`);
|
|
113757
|
+
}
|
|
113758
|
+
}
|
|
113759
|
+
const merged = { ...existing, [params.provider]: params.entry };
|
|
113760
|
+
mkdirSync5(params.opencodeDir, { recursive: true });
|
|
113761
|
+
writeFileSync5(params.authPath, `${JSON.stringify(merged, null, 2)}
|
|
113762
|
+
`, { mode: 384 });
|
|
113763
|
+
}
|
|
113764
|
+
function installXaiAuth() {
|
|
113765
|
+
const raw2 = process.env[XAI_AUTH_ENV];
|
|
113766
|
+
if (!raw2) return null;
|
|
113767
|
+
const body = parseXaiAuthBody(raw2);
|
|
113768
|
+
if (!body) {
|
|
113769
|
+
log2.warning(`\xBB ${XAI_AUTH_ENV} present but malformed; ignoring`);
|
|
113770
|
+
return null;
|
|
113771
|
+
}
|
|
113772
|
+
if (isXaiRejectedChain(body)) return null;
|
|
113773
|
+
const xdgDataHome = resolveDataHome();
|
|
113774
|
+
if (!xdgDataHome) return null;
|
|
113775
|
+
const opencodeDir = join6(xdgDataHome, "opencode");
|
|
113776
|
+
const authPath = join6(opencodeDir, "auth.json");
|
|
113777
|
+
writeOpenCodeAuthEntry({
|
|
113778
|
+
opencodeDir,
|
|
113779
|
+
authPath,
|
|
113780
|
+
provider: "xai",
|
|
113781
|
+
// opencode refreshes when `expires` is inside its skew window, and falls
|
|
113782
|
+
// back to the JWT's own exp when the stored value is absent. Decoding it
|
|
113783
|
+
// here means the first request uses the token we already hold instead of
|
|
113784
|
+
// burning a rotation on startup.
|
|
113785
|
+
entry: {
|
|
113786
|
+
type: "oauth",
|
|
113787
|
+
refresh: body.tokens.refresh_token,
|
|
113788
|
+
access: body.tokens.access_token,
|
|
113789
|
+
expires: decodeJwtExpMs(body.tokens.access_token) ?? 0
|
|
113790
|
+
}
|
|
113791
|
+
});
|
|
113792
|
+
installedDataHome = xdgDataHome;
|
|
113793
|
+
log2.info(`\xBB installed Grok auth at ${authPath}`);
|
|
113794
|
+
installedSubscriptionPaths.set(XAI_AUTH_ENV, authPath);
|
|
113795
|
+
return { authPath, originalRefresh: body.tokens.refresh_token };
|
|
113796
|
+
}
|
|
113797
|
+
function isXaiRejectedChain(body) {
|
|
113798
|
+
if (!body.refresh_rejected_at) return false;
|
|
113799
|
+
log2.warning(
|
|
113800
|
+
`\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.`
|
|
113801
|
+
);
|
|
113802
|
+
return true;
|
|
113803
|
+
}
|
|
113804
|
+
function installCodexHome() {
|
|
113805
|
+
const raw2 = process.env[CODEX_AUTH_ENV];
|
|
113806
|
+
if (!raw2) return null;
|
|
113807
|
+
const body = parseCodexAuthBody(raw2);
|
|
113808
|
+
if (!body) {
|
|
113809
|
+
log2.warning(`\xBB ${CODEX_AUTH_ENV} present but malformed; ignoring`);
|
|
113810
|
+
return null;
|
|
113811
|
+
}
|
|
113812
|
+
if (isRejectedChain(body)) return null;
|
|
113813
|
+
if (!body.tokens.id_token) {
|
|
113814
|
+
log2.warning(
|
|
113815
|
+
`\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.`
|
|
113816
|
+
);
|
|
113817
|
+
return null;
|
|
113818
|
+
}
|
|
113819
|
+
const root = resolveDataHome();
|
|
113820
|
+
if (!root) return null;
|
|
113821
|
+
const codexHome = join6(root, "codex");
|
|
113822
|
+
const authPath = join6(codexHome, "auth.json");
|
|
113823
|
+
mkdirSync5(codexHome, { recursive: true });
|
|
113824
|
+
writeFileSync5(authPath, stringifyCodexAuthBody(body), { mode: 384 });
|
|
113825
|
+
log2.info(`\xBB installed Codex auth at ${authPath}`);
|
|
113826
|
+
return { codexHome, authPath, originalRefresh: body.tokens.refresh_token };
|
|
113827
|
+
}
|
|
113828
|
+
var dataHome;
|
|
113829
|
+
var unhostedSubscriptions = /* @__PURE__ */ new Set();
|
|
113830
|
+
function canInstallSubscription(name) {
|
|
113831
|
+
return name !== CODEX_AUTH_ENV && name !== XAI_AUTH_ENV || resolveDataHome() !== null;
|
|
113832
|
+
}
|
|
113833
|
+
function resolveDataHome() {
|
|
113834
|
+
if (process.env.CI !== "true") return join6(homedir2(), ".local", "share");
|
|
113835
|
+
if (dataHome === void 0) dataHome = bootstrapPullfrogDataDir();
|
|
113836
|
+
return dataHome;
|
|
113837
|
+
}
|
|
113838
|
+
function bootstrapPullfrogDataDir() {
|
|
113839
|
+
const user = userInfo().username;
|
|
113840
|
+
let primaryGroup;
|
|
113841
|
+
try {
|
|
113842
|
+
primaryGroup = execFileSync4("id", ["-gn", user], { stdio: "pipe", encoding: "utf-8" }).trim();
|
|
113843
|
+
} catch {
|
|
113844
|
+
primaryGroup = user;
|
|
113845
|
+
}
|
|
113846
|
+
try {
|
|
113847
|
+
execFileSync4("sudo", ["-n", "mkdir", "-p", PULLFROG_DATA_DIR], { stdio: "pipe" });
|
|
113848
|
+
execFileSync4("sudo", ["-n", "chown", `${user}:${primaryGroup}`, PULLFROG_DATA_DIR], {
|
|
113849
|
+
stdio: "pipe"
|
|
113850
|
+
});
|
|
113851
|
+
execFileSync4("sudo", ["-n", "chmod", "700", PULLFROG_DATA_DIR], { stdio: "pipe" });
|
|
113852
|
+
} catch (err) {
|
|
113853
|
+
log2.warning(
|
|
113854
|
+
`\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.`
|
|
113855
|
+
);
|
|
113856
|
+
for (const name of [CODEX_AUTH_ENV, XAI_AUTH_ENV]) {
|
|
113857
|
+
if (process.env[name]) unhostedSubscriptions.add(name);
|
|
113858
|
+
delete process.env[name];
|
|
113859
|
+
}
|
|
113860
|
+
return null;
|
|
113861
|
+
}
|
|
113862
|
+
const runDir = mkdtempSync2(join6(PULLFROG_DATA_DIR, "run-"));
|
|
113863
|
+
if (process.env.RUNNER_ENVIRONMENT !== "github-hosted") {
|
|
113864
|
+
saveSecretState(SUBSCRIPTION_DATA_DIR_STATE, runDir);
|
|
113865
|
+
}
|
|
113866
|
+
return runDir;
|
|
113867
|
+
}
|
|
113868
|
+
|
|
113804
113869
|
// utils/secrets.ts
|
|
113805
113870
|
var SENSITIVE_PATTERNS = [
|
|
113806
113871
|
/_KEY$/i,
|
|
@@ -114447,9 +114512,8 @@ var codex = agent({
|
|
|
114447
114512
|
initialUsage: initial.usage,
|
|
114448
114513
|
reflectionPrompt: buildReflectionPrompt(ctx.toolState),
|
|
114449
114514
|
// a resume needs the thread codex recorded. without one there is nothing
|
|
114450
|
-
// to continue, and `resume --last`
|
|
114451
|
-
//
|
|
114452
|
-
// runner with a shared CODEX_HOME is somebody else's run.
|
|
114515
|
+
// to continue, and `resume --last` could pick another run's session off CI,
|
|
114516
|
+
// where CODEX_HOME is the shared ~/.local/share/codex (in CI it is per run).
|
|
114453
114517
|
canResume: (r2) => r2.threadId !== void 0,
|
|
114454
114518
|
resume: async (c2) => runCodex2({
|
|
114455
114519
|
...runnerArgs,
|
|
@@ -114476,7 +114540,7 @@ var codex = agent({
|
|
|
114476
114540
|
import { spawn as nodeSpawn2 } from "node:child_process";
|
|
114477
114541
|
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "node:fs";
|
|
114478
114542
|
import { totalmem } from "node:os";
|
|
114479
|
-
import { join as
|
|
114543
|
+
import { join as join9 } from "node:path";
|
|
114480
114544
|
import { performance as performance7 } from "node:perf_hooks";
|
|
114481
114545
|
|
|
114482
114546
|
// node_modules/.pnpm/@opencode-ai+sdk@1.18.29/node_modules/@opencode-ai/sdk/dist/v2/gen/core/serverSentEvents.gen.js
|
|
@@ -119885,200 +119949,27 @@ var import_cross_spawn = __toESM(require_cross_spawn(), 1);
|
|
|
119885
119949
|
// agents/opencode.ts
|
|
119886
119950
|
var import_undici = __toESM(require_undici2(), 1);
|
|
119887
119951
|
|
|
119888
|
-
// utils/worktree.ts
|
|
119889
|
-
async function dirtyTrackedPaths(params = {}) {
|
|
119890
|
-
const result = await spawn3({
|
|
119891
|
-
cmd: "git",
|
|
119892
|
-
args: ["diff", "--name-only", "HEAD"],
|
|
119893
|
-
env: process.env,
|
|
119894
|
-
cwd: params.cwd ?? process.cwd(),
|
|
119895
|
-
activityTimeout: 0
|
|
119896
|
-
});
|
|
119897
|
-
if (result.exitCode !== 0) {
|
|
119898
|
-
throw new Error(
|
|
119899
|
-
`git diff --name-only HEAD failed (exit ${result.exitCode}): ${result.stderr.trim() || "(no stderr)"}`
|
|
119900
|
-
);
|
|
119901
|
-
}
|
|
119902
|
-
return new Set(result.stdout.split("\n").filter(Boolean));
|
|
119903
|
-
}
|
|
119904
|
-
async function restoreDirtiedSince(params) {
|
|
119905
|
-
const dirtied = [...await dirtyTrackedPaths({ cwd: params.cwd ?? process.cwd() })].filter(
|
|
119906
|
-
(path4) => !params.before.has(path4)
|
|
119907
|
-
);
|
|
119908
|
-
if (dirtied.length === 0) return;
|
|
119909
|
-
const result = await spawn3({
|
|
119910
|
-
cmd: "git",
|
|
119911
|
-
args: ["restore", "--staged", "--worktree", "--", ...dirtied],
|
|
119912
|
-
env: process.env,
|
|
119913
|
-
cwd: params.cwd ?? process.cwd(),
|
|
119914
|
-
activityTimeout: 0
|
|
119915
|
-
});
|
|
119916
|
-
if (result.exitCode !== 0) {
|
|
119917
|
-
log2.warning(
|
|
119918
|
-
`\xBB failed to restore ${dirtied.length} tracked file(s) modified by ${params.actor}: ${result.stderr.trim() || "(no stderr)"}`
|
|
119919
|
-
);
|
|
119920
|
-
return;
|
|
119921
|
-
}
|
|
119922
|
-
log2.info(
|
|
119923
|
-
`\xBB restored ${dirtied.length} tracked file(s) modified by ${params.actor}: ${dirtied.join(", ")}`
|
|
119924
|
-
);
|
|
119925
|
-
}
|
|
119926
|
-
|
|
119927
|
-
// agents/opencodePlugin.ts
|
|
119928
|
-
var PULLFROG_BUS_EVENT_TYPE = "pullfrog_bus_event";
|
|
119929
|
-
var PULLFROG_OPENCODE_GATE_PLUGIN_FILENAME = "pullfrog-subagent-gate.ts";
|
|
119930
|
-
var PULLFROG_OPENCODE_PLUGIN_SOURCE = `// AUTOGENERATED by Pullfrog. do not edit; it'll be overwritten on the next run.
|
|
119931
|
-
// surfaces opencode subagent activity that the CLI's run-loop discards. see
|
|
119932
|
-
// action/agents/opencodePlugin.ts in pullfrog/app for why this exists. lives
|
|
119933
|
-
// inside the per-run tmpdir (XDG_CONFIG_HOME/opencode/plugin/), never inside
|
|
119934
|
-
// the user's working tree. the subagent gate ships as a separate plugin
|
|
119935
|
-
// (pullfrog-subagent-gate.ts).
|
|
119936
|
-
|
|
119937
|
-
const PULLFROG_BUS_EVENT_TYPE = ${JSON.stringify(PULLFROG_BUS_EVENT_TYPE)};
|
|
119938
|
-
|
|
119939
|
-
// orchestratorSessionID locks to the first sessionID we observe on the bus
|
|
119940
|
-
// (\`message.part.updated\`). opencode's run command creates exactly one
|
|
119941
|
-
// top-level session before any subagent is dispatched, and the orchestrator
|
|
119942
|
-
// emits at least one event before any subagent does \u2014 so first-seen-wins
|
|
119943
|
-
// captures the orchestrator; every subagent part has a non-matching
|
|
119944
|
-
// sessionID.
|
|
119945
|
-
let orchestratorSessionID: string | undefined;
|
|
119946
|
-
|
|
119947
|
-
function isOrchestratorTaskDispatch(part: {
|
|
119948
|
-
type?: string;
|
|
119949
|
-
tool?: string;
|
|
119950
|
-
state?: { status?: string };
|
|
119951
|
-
} | undefined): boolean {
|
|
119952
|
-
if (!part || part.type !== "tool") return false;
|
|
119953
|
-
if (part.tool !== "task") return false;
|
|
119954
|
-
// only forward at status="running" (not "pending"). at pending the
|
|
119955
|
-
// state.input is still {} \u2014 the orchestrator has emitted the part shell
|
|
119956
|
-
// but the LLM hasn't filled in description/subagent_type/prompt yet. by
|
|
119957
|
-
// running, input is populated and recordTaskDispatch can derive the lens
|
|
119958
|
-
// label correctly.
|
|
119959
|
-
return part.state?.status === "running";
|
|
119960
|
-
}
|
|
119961
|
-
|
|
119962
|
-
export default async function pullfrogEventsPlugin() {
|
|
119963
|
-
return {
|
|
119964
|
-
event: async (input: {
|
|
119965
|
-
event: {
|
|
119966
|
-
type: string;
|
|
119967
|
-
properties?: {
|
|
119968
|
-
part?: {
|
|
119969
|
-
sessionID?: string;
|
|
119970
|
-
type?: string;
|
|
119971
|
-
tool?: string;
|
|
119972
|
-
state?: { status?: string };
|
|
119973
|
-
};
|
|
119974
|
-
};
|
|
119975
|
-
};
|
|
119976
|
-
}) => {
|
|
119977
|
-
const event = input?.event;
|
|
119978
|
-
if (!event || typeof event !== "object") return;
|
|
119979
|
-
if (event.type !== "message.part.updated") return;
|
|
119980
|
-
const part = event.properties?.part;
|
|
119981
|
-
const sessionID = part?.sessionID;
|
|
119982
|
-
if (typeof sessionID !== "string" || sessionID.length === 0) return;
|
|
119983
|
-
if (orchestratorSessionID === undefined) orchestratorSessionID = sessionID;
|
|
119984
|
-
|
|
119985
|
-
if (sessionID === orchestratorSessionID) {
|
|
119986
|
-
// skip orchestrator events EXCEPT early task dispatches.
|
|
119987
|
-
if (!isOrchestratorTaskDispatch(part)) return;
|
|
119988
|
-
}
|
|
119989
|
-
|
|
119990
|
-
try {
|
|
119991
|
-
const line = JSON.stringify({
|
|
119992
|
-
type: PULLFROG_BUS_EVENT_TYPE,
|
|
119993
|
-
bus_event: event,
|
|
119994
|
-
});
|
|
119995
|
-
process.stdout.write(line + "\\n");
|
|
119996
|
-
} catch {
|
|
119997
|
-
// a circular reference or BigInt etc. would throw; swallow rather
|
|
119998
|
-
// than letting a single bad event take down the plugin.
|
|
119999
|
-
}
|
|
120000
|
-
},
|
|
120001
|
-
};
|
|
120002
|
-
}
|
|
120003
|
-
`;
|
|
120004
|
-
function buildOpencodeSubagentGateSource(deniedTools) {
|
|
120005
|
-
return `// AUTOGENERATED by Pullfrog. do not edit; it'll be overwritten on the next run.
|
|
120006
|
-
// hard-blocks state-mutating MCP tool calls from opencode subagents (PR
|
|
120007
|
-
// following pullfrog/app#796 \u2014 see action/agents/subagentToolGates.ts). see
|
|
120008
|
-
// action/agents/opencodePlugin.ts in pullfrog/app for why this exists. lives
|
|
120009
|
-
// inside the per-run tmpdir (XDG_CONFIG_HOME/opencode/plugin/), never inside
|
|
120010
|
-
// the user's working tree.
|
|
120011
|
-
|
|
120012
|
-
// embedded at template-render time so the plugin source has no imports. the
|
|
120013
|
-
// denied set is derived from each tool's \`mutates\` flag \u2014 see
|
|
120014
|
-
// action/agents/subagentToolGates.ts.
|
|
120015
|
-
const SUBAGENT_DENIED_TOOLS = new Set(${JSON.stringify(deniedTools)});
|
|
120016
|
-
|
|
120017
|
-
// opencode presents MCP tools as \`pullfrog_<bare>\`; strip that to compare
|
|
120018
|
-
// against the canonical bare names in SUBAGENT_DENIED_TOOLS. native tools
|
|
120019
|
-
// (read, write, bash, task, etc.) lack the prefix and never match the deny
|
|
120020
|
-
// list anyway.
|
|
120021
|
-
function stripPullfrogPrefix(toolName: string): string {
|
|
120022
|
-
if (toolName.startsWith("pullfrog_")) return toolName.slice("pullfrog_".length);
|
|
120023
|
-
return toolName;
|
|
120024
|
-
}
|
|
120025
|
-
|
|
120026
|
-
// orchestratorSessionID locks to the first sessionID we observe on
|
|
120027
|
-
// \`tool.execute.before\`. the orchestrator must invoke a tool (e.g. its
|
|
120028
|
-
// \`task\` dispatch) before any subagent session exists, so first-seen-wins
|
|
120029
|
-
// always captures the orchestrator; every subagent call therefore has a
|
|
120030
|
-
// non-matching sessionID and is subject to the deny check.
|
|
120031
|
-
let orchestratorSessionID: string | undefined;
|
|
120032
|
-
|
|
120033
|
-
export default async function pullfrogSubagentGatePlugin() {
|
|
120034
|
-
return {
|
|
120035
|
-
"tool.execute.before": async (
|
|
120036
|
-
input: { tool?: string; sessionID?: string; callID?: string },
|
|
120037
|
-
_output: { args?: Record<string, unknown> }
|
|
120038
|
-
) => {
|
|
120039
|
-
// input: { tool, sessionID, callID } \u2014 confirmed against
|
|
120040
|
-
// anomalyco/opencode packages/opencode/src/session/tools.ts which
|
|
120041
|
-
// calls plugin.trigger("tool.execute.before", { tool, sessionID,
|
|
120042
|
-
// callID }, { args }). throwing here surfaces as a tool error to the
|
|
120043
|
-
// model (Effect's run.promise propagates the rejection through the
|
|
120044
|
-
// AI SDK's tool-runtime), so the subagent sees a denial and can
|
|
120045
|
-
// pick a different action.
|
|
120046
|
-
const sessionID = typeof input?.sessionID === "string" ? input.sessionID : "";
|
|
120047
|
-
const tool = typeof input?.tool === "string" ? input.tool : "";
|
|
120048
|
-
// above the sessionID check: the bash block must never depend on a
|
|
120049
|
-
// session id being present.
|
|
120050
|
-
if (tool === "bash") {
|
|
120051
|
-
throw new Error("the native bash tool is disabled in Pullfrog runs.");
|
|
120052
|
-
}
|
|
120053
|
-
if (!sessionID || !tool) return;
|
|
120054
|
-
if (orchestratorSessionID === undefined) orchestratorSessionID = sessionID;
|
|
120055
|
-
if (sessionID === orchestratorSessionID) return;
|
|
120056
|
-
const bare = stripPullfrogPrefix(tool);
|
|
120057
|
-
if (!SUBAGENT_DENIED_TOOLS.has(bare)) return;
|
|
120058
|
-
throw new Error(
|
|
120059
|
-
"subagent attempted to call denied tool '" + bare + "'. " +
|
|
120060
|
-
"subagents share the orchestrator's in-process working tree and toolState; " +
|
|
120061
|
-
"state-changing MCP tools (checkout_pr, push_branch, create_pull_request_review, " +
|
|
120062
|
-
"report_progress, etc.) are reserved for the orchestrator. " +
|
|
120063
|
-
"report findings back to the orchestrator and let it perform the mutation."
|
|
120064
|
-
);
|
|
120065
|
-
},
|
|
120066
|
-
};
|
|
120067
|
-
}
|
|
120068
|
-
`;
|
|
120069
|
-
}
|
|
120070
|
-
|
|
120071
119952
|
// utils/openCodeModels.ts
|
|
120072
119953
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
119954
|
+
import { join as join8 } from "node:path";
|
|
120073
119955
|
var baseline;
|
|
120074
119956
|
var authorized;
|
|
120075
119957
|
var failure;
|
|
120076
119958
|
var ANSI_PATTERN = /\[[0-9;]*m/g;
|
|
120077
|
-
function
|
|
120078
|
-
|
|
119959
|
+
function openCodeHomeEnv(tmpdir4) {
|
|
119960
|
+
return {
|
|
119961
|
+
HOME: tmpdir4,
|
|
119962
|
+
XDG_CONFIG_HOME: join8(tmpdir4, ".config"),
|
|
119963
|
+
XDG_DATA_HOME: getSubscriptionDataHome() ?? join8(tmpdir4, ".local", "share"),
|
|
119964
|
+
XDG_CACHE_HOME: join8(tmpdir4, ".cache"),
|
|
119965
|
+
XDG_STATE_HOME: join8(tmpdir4, ".local", "state")
|
|
119966
|
+
};
|
|
119967
|
+
}
|
|
119968
|
+
function readModels(params) {
|
|
119969
|
+
const result = spawnSync4(params.cliPath, ["models"], {
|
|
120079
119970
|
encoding: "utf-8",
|
|
120080
119971
|
timeout: 3e4,
|
|
120081
|
-
env: process.env,
|
|
119972
|
+
env: { ...process.env, ...openCodeHomeEnv(params.tmpdir) },
|
|
120082
119973
|
stdio: ["ignore", "pipe", "pipe"]
|
|
120083
119974
|
});
|
|
120084
119975
|
if (result.status !== 0) {
|
|
@@ -120095,12 +119986,12 @@ function readModels(cliPath) {
|
|
|
120095
119986
|
function getModelsFailure() {
|
|
120096
119987
|
return failure;
|
|
120097
119988
|
}
|
|
120098
|
-
function captureBaselineModels(
|
|
120099
|
-
baseline = readModels(
|
|
119989
|
+
function captureBaselineModels(params) {
|
|
119990
|
+
baseline = readModels(params);
|
|
120100
119991
|
log2.debug(`\xBB opencode baseline: ${baseline.size} models`);
|
|
120101
119992
|
}
|
|
120102
|
-
function captureAuthorizedModels(
|
|
120103
|
-
authorized = readModels(
|
|
119993
|
+
function captureAuthorizedModels(params) {
|
|
119994
|
+
authorized = readModels(params);
|
|
120104
119995
|
const base = baseline;
|
|
120105
119996
|
if (base) {
|
|
120106
119997
|
const diff = [...authorized].filter((m) => !base.has(m));
|
|
@@ -120120,6 +120011,190 @@ function getAuthorizedModels() {
|
|
|
120120
120011
|
return authorized;
|
|
120121
120012
|
}
|
|
120122
120013
|
|
|
120014
|
+
// utils/worktree.ts
|
|
120015
|
+
async function dirtyTrackedPaths(params = {}) {
|
|
120016
|
+
const result = await spawn3({
|
|
120017
|
+
cmd: "git",
|
|
120018
|
+
args: ["diff", "--name-only", "HEAD"],
|
|
120019
|
+
env: process.env,
|
|
120020
|
+
cwd: params.cwd ?? process.cwd(),
|
|
120021
|
+
activityTimeout: 0
|
|
120022
|
+
});
|
|
120023
|
+
if (result.exitCode !== 0) {
|
|
120024
|
+
throw new Error(
|
|
120025
|
+
`git diff --name-only HEAD failed (exit ${result.exitCode}): ${result.stderr.trim() || "(no stderr)"}`
|
|
120026
|
+
);
|
|
120027
|
+
}
|
|
120028
|
+
return new Set(result.stdout.split("\n").filter(Boolean));
|
|
120029
|
+
}
|
|
120030
|
+
async function restoreDirtiedSince(params) {
|
|
120031
|
+
const dirtied = [...await dirtyTrackedPaths({ cwd: params.cwd ?? process.cwd() })].filter(
|
|
120032
|
+
(path4) => !params.before.has(path4)
|
|
120033
|
+
);
|
|
120034
|
+
if (dirtied.length === 0) return;
|
|
120035
|
+
const result = await spawn3({
|
|
120036
|
+
cmd: "git",
|
|
120037
|
+
args: ["restore", "--staged", "--worktree", "--", ...dirtied],
|
|
120038
|
+
env: process.env,
|
|
120039
|
+
cwd: params.cwd ?? process.cwd(),
|
|
120040
|
+
activityTimeout: 0
|
|
120041
|
+
});
|
|
120042
|
+
if (result.exitCode !== 0) {
|
|
120043
|
+
log2.warning(
|
|
120044
|
+
`\xBB failed to restore ${dirtied.length} tracked file(s) modified by ${params.actor}: ${result.stderr.trim() || "(no stderr)"}`
|
|
120045
|
+
);
|
|
120046
|
+
return;
|
|
120047
|
+
}
|
|
120048
|
+
log2.info(
|
|
120049
|
+
`\xBB restored ${dirtied.length} tracked file(s) modified by ${params.actor}: ${dirtied.join(", ")}`
|
|
120050
|
+
);
|
|
120051
|
+
}
|
|
120052
|
+
|
|
120053
|
+
// agents/opencodePlugin.ts
|
|
120054
|
+
var PULLFROG_BUS_EVENT_TYPE = "pullfrog_bus_event";
|
|
120055
|
+
var PULLFROG_OPENCODE_GATE_PLUGIN_FILENAME = "pullfrog-subagent-gate.ts";
|
|
120056
|
+
var PULLFROG_OPENCODE_PLUGIN_SOURCE = `// AUTOGENERATED by Pullfrog. do not edit; it'll be overwritten on the next run.
|
|
120057
|
+
// surfaces opencode subagent activity that the CLI's run-loop discards. see
|
|
120058
|
+
// action/agents/opencodePlugin.ts in pullfrog/app for why this exists. lives
|
|
120059
|
+
// inside the per-run tmpdir (XDG_CONFIG_HOME/opencode/plugin/), never inside
|
|
120060
|
+
// the user's working tree. the subagent gate ships as a separate plugin
|
|
120061
|
+
// (pullfrog-subagent-gate.ts).
|
|
120062
|
+
|
|
120063
|
+
const PULLFROG_BUS_EVENT_TYPE = ${JSON.stringify(PULLFROG_BUS_EVENT_TYPE)};
|
|
120064
|
+
|
|
120065
|
+
// orchestratorSessionID locks to the first sessionID we observe on the bus
|
|
120066
|
+
// (\`message.part.updated\`). opencode's run command creates exactly one
|
|
120067
|
+
// top-level session before any subagent is dispatched, and the orchestrator
|
|
120068
|
+
// emits at least one event before any subagent does \u2014 so first-seen-wins
|
|
120069
|
+
// captures the orchestrator; every subagent part has a non-matching
|
|
120070
|
+
// sessionID.
|
|
120071
|
+
let orchestratorSessionID: string | undefined;
|
|
120072
|
+
|
|
120073
|
+
function isOrchestratorTaskDispatch(part: {
|
|
120074
|
+
type?: string;
|
|
120075
|
+
tool?: string;
|
|
120076
|
+
state?: { status?: string };
|
|
120077
|
+
} | undefined): boolean {
|
|
120078
|
+
if (!part || part.type !== "tool") return false;
|
|
120079
|
+
if (part.tool !== "task") return false;
|
|
120080
|
+
// only forward at status="running" (not "pending"). at pending the
|
|
120081
|
+
// state.input is still {} \u2014 the orchestrator has emitted the part shell
|
|
120082
|
+
// but the LLM hasn't filled in description/subagent_type/prompt yet. by
|
|
120083
|
+
// running, input is populated and recordTaskDispatch can derive the lens
|
|
120084
|
+
// label correctly.
|
|
120085
|
+
return part.state?.status === "running";
|
|
120086
|
+
}
|
|
120087
|
+
|
|
120088
|
+
export default async function pullfrogEventsPlugin() {
|
|
120089
|
+
return {
|
|
120090
|
+
event: async (input: {
|
|
120091
|
+
event: {
|
|
120092
|
+
type: string;
|
|
120093
|
+
properties?: {
|
|
120094
|
+
part?: {
|
|
120095
|
+
sessionID?: string;
|
|
120096
|
+
type?: string;
|
|
120097
|
+
tool?: string;
|
|
120098
|
+
state?: { status?: string };
|
|
120099
|
+
};
|
|
120100
|
+
};
|
|
120101
|
+
};
|
|
120102
|
+
}) => {
|
|
120103
|
+
const event = input?.event;
|
|
120104
|
+
if (!event || typeof event !== "object") return;
|
|
120105
|
+
if (event.type !== "message.part.updated") return;
|
|
120106
|
+
const part = event.properties?.part;
|
|
120107
|
+
const sessionID = part?.sessionID;
|
|
120108
|
+
if (typeof sessionID !== "string" || sessionID.length === 0) return;
|
|
120109
|
+
if (orchestratorSessionID === undefined) orchestratorSessionID = sessionID;
|
|
120110
|
+
|
|
120111
|
+
if (sessionID === orchestratorSessionID) {
|
|
120112
|
+
// skip orchestrator events EXCEPT early task dispatches.
|
|
120113
|
+
if (!isOrchestratorTaskDispatch(part)) return;
|
|
120114
|
+
}
|
|
120115
|
+
|
|
120116
|
+
try {
|
|
120117
|
+
const line = JSON.stringify({
|
|
120118
|
+
type: PULLFROG_BUS_EVENT_TYPE,
|
|
120119
|
+
bus_event: event,
|
|
120120
|
+
});
|
|
120121
|
+
process.stdout.write(line + "\\n");
|
|
120122
|
+
} catch {
|
|
120123
|
+
// a circular reference or BigInt etc. would throw; swallow rather
|
|
120124
|
+
// than letting a single bad event take down the plugin.
|
|
120125
|
+
}
|
|
120126
|
+
},
|
|
120127
|
+
};
|
|
120128
|
+
}
|
|
120129
|
+
`;
|
|
120130
|
+
function buildOpencodeSubagentGateSource(ctx) {
|
|
120131
|
+
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.`;
|
|
120132
|
+
return `// AUTOGENERATED by Pullfrog. do not edit; it'll be overwritten on the next run.
|
|
120133
|
+
// hard-blocks state-mutating MCP tool calls from opencode subagents (PR
|
|
120134
|
+
// following pullfrog/app#796 \u2014 see action/agents/subagentToolGates.ts). see
|
|
120135
|
+
// action/agents/opencodePlugin.ts in pullfrog/app for why this exists. lives
|
|
120136
|
+
// inside the per-run tmpdir (XDG_CONFIG_HOME/opencode/plugin/), never inside
|
|
120137
|
+
// the user's working tree.
|
|
120138
|
+
|
|
120139
|
+
// embedded at template-render time so the plugin source has no imports. the
|
|
120140
|
+
// denied set is derived from each tool's \`mutates\` flag \u2014 see
|
|
120141
|
+
// action/agents/subagentToolGates.ts.
|
|
120142
|
+
const SUBAGENT_DENIED_TOOLS = new Set(${JSON.stringify(ctx.subagentDeniedTools)});
|
|
120143
|
+
|
|
120144
|
+
// opencode presents MCP tools as \`pullfrog_<bare>\`; strip that to compare
|
|
120145
|
+
// against the canonical bare names in SUBAGENT_DENIED_TOOLS. native tools
|
|
120146
|
+
// (read, write, bash, task, etc.) lack the prefix and never match the deny
|
|
120147
|
+
// list anyway.
|
|
120148
|
+
function stripPullfrogPrefix(toolName: string): string {
|
|
120149
|
+
if (toolName.startsWith("pullfrog_")) return toolName.slice("pullfrog_".length);
|
|
120150
|
+
return toolName;
|
|
120151
|
+
}
|
|
120152
|
+
|
|
120153
|
+
// orchestratorSessionID locks to the first sessionID we observe on
|
|
120154
|
+
// \`tool.execute.before\`. the orchestrator must invoke a tool (e.g. its
|
|
120155
|
+
// \`task\` dispatch) before any subagent session exists, so first-seen-wins
|
|
120156
|
+
// always captures the orchestrator; every subagent call therefore has a
|
|
120157
|
+
// non-matching sessionID and is subject to the deny check.
|
|
120158
|
+
let orchestratorSessionID: string | undefined;
|
|
120159
|
+
|
|
120160
|
+
export default async function pullfrogSubagentGatePlugin() {
|
|
120161
|
+
return {
|
|
120162
|
+
"tool.execute.before": async (
|
|
120163
|
+
input: { tool?: string; sessionID?: string; callID?: string },
|
|
120164
|
+
_output: { args?: Record<string, unknown> }
|
|
120165
|
+
) => {
|
|
120166
|
+
// input: { tool, sessionID, callID } \u2014 confirmed against
|
|
120167
|
+
// anomalyco/opencode packages/opencode/src/session/tools.ts which
|
|
120168
|
+
// calls plugin.trigger("tool.execute.before", { tool, sessionID,
|
|
120169
|
+
// callID }, { args }). throwing here surfaces as a tool error to the
|
|
120170
|
+
// model (Effect's run.promise propagates the rejection through the
|
|
120171
|
+
// AI SDK's tool-runtime), so the subagent sees a denial and can
|
|
120172
|
+
// pick a different action.
|
|
120173
|
+
const sessionID = typeof input?.sessionID === "string" ? input.sessionID : "";
|
|
120174
|
+
const tool = typeof input?.tool === "string" ? input.tool : "";
|
|
120175
|
+
// above the sessionID check: the bash block must never depend on a
|
|
120176
|
+
// session id being present.
|
|
120177
|
+
if (tool === "bash") {
|
|
120178
|
+
throw new Error(${JSON.stringify(bashRefusal)});
|
|
120179
|
+
}
|
|
120180
|
+
if (!sessionID || !tool) return;
|
|
120181
|
+
if (orchestratorSessionID === undefined) orchestratorSessionID = sessionID;
|
|
120182
|
+
if (sessionID === orchestratorSessionID) return;
|
|
120183
|
+
const bare = stripPullfrogPrefix(tool);
|
|
120184
|
+
if (!SUBAGENT_DENIED_TOOLS.has(bare)) return;
|
|
120185
|
+
throw new Error(
|
|
120186
|
+
"subagent attempted to call denied tool '" + bare + "'. " +
|
|
120187
|
+
"subagents share the orchestrator's in-process working tree and toolState; " +
|
|
120188
|
+
"state-changing MCP tools (checkout_pr, push_branch, create_pull_request_review, " +
|
|
120189
|
+
"report_progress, etc.) are reserved for the orchestrator. " +
|
|
120190
|
+
"report findings back to the orchestrator and let it perform the mutation."
|
|
120191
|
+
);
|
|
120192
|
+
},
|
|
120193
|
+
};
|
|
120194
|
+
}
|
|
120195
|
+
`;
|
|
120196
|
+
}
|
|
120197
|
+
|
|
120123
120198
|
// agents/subagentModels.ts
|
|
120124
120199
|
function deriveSubagentModels(orchestratorSpec) {
|
|
120125
120200
|
if (!orchestratorSpec) return { reviewer: void 0 };
|
|
@@ -120494,6 +120569,7 @@ async function dispatchEvent(ctx, event) {
|
|
|
120494
120569
|
const err = event.properties.error;
|
|
120495
120570
|
const message = err ? extractErrorMessage(err) : "(no error payload)";
|
|
120496
120571
|
log2.info(`\xBB ${ctx.label} session error: ${message}`);
|
|
120572
|
+
if (err) ctx.lastSessionError = message;
|
|
120497
120573
|
return;
|
|
120498
120574
|
}
|
|
120499
120575
|
}
|
|
@@ -120632,6 +120708,7 @@ async function runPromptTurn(ctx, params) {
|
|
|
120632
120708
|
ctx.promptMessageID = void 0;
|
|
120633
120709
|
ctx.diagnostic.sawModelOutput = false;
|
|
120634
120710
|
ctx.diagnostic.idleSec = void 0;
|
|
120711
|
+
ctx.lastSessionError = void 0;
|
|
120635
120712
|
let assistant;
|
|
120636
120713
|
let returnedParts;
|
|
120637
120714
|
let networkError = null;
|
|
@@ -120676,11 +120753,12 @@ async function runPromptTurn(ctx, params) {
|
|
|
120676
120753
|
}
|
|
120677
120754
|
const cause = ctx.serverExitCause() ?? ctx.diagnostic.lastProviderError;
|
|
120678
120755
|
const diagnosis = cause ? ` \u2014 likely cause: ${cause}` : "";
|
|
120756
|
+
const promptCause = ctx.serverExitCause() ?? (ctx.diagnostic.sawModelOutput ? void 0 : ctx.lastSessionError) ?? ctx.diagnostic.lastProviderError;
|
|
120679
120757
|
if (networkError) {
|
|
120680
120758
|
return {
|
|
120681
120759
|
success: false,
|
|
120682
120760
|
output: finalText,
|
|
120683
|
-
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}${
|
|
120761
|
+
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}` : ""}`,
|
|
120684
120762
|
usage
|
|
120685
120763
|
};
|
|
120686
120764
|
}
|
|
@@ -120872,15 +120950,14 @@ var opencode = agent({
|
|
|
120872
120950
|
const isBedrockRoute = rawModel !== void 0 && bedrockModelId !== void 0 && bedrockModelId === rawModel;
|
|
120873
120951
|
const vertexModel = resolveVertexOpenCodeModel(rawModel);
|
|
120874
120952
|
const model = vertexModel ?? (isBedrockRoute ? `amazon-bedrock/${rawModel}` : rawModel);
|
|
120875
|
-
const
|
|
120876
|
-
|
|
120877
|
-
|
|
120878
|
-
|
|
120879
|
-
const opencodePluginDir = join8(homeEnv.XDG_CONFIG_HOME, "opencode", "plugin");
|
|
120953
|
+
const codexAuth = installCodexAuth();
|
|
120954
|
+
const xaiAuth = installXaiAuth();
|
|
120955
|
+
const homeEnv = openCodeHomeEnv(ctx.tmpdir);
|
|
120956
|
+
const opencodePluginDir = join9(homeEnv.XDG_CONFIG_HOME, "opencode", "plugin");
|
|
120880
120957
|
mkdirSync7(opencodePluginDir, { recursive: true });
|
|
120881
120958
|
writeFileSync7(
|
|
120882
|
-
|
|
120883
|
-
buildOpencodeSubagentGateSource(ctx
|
|
120959
|
+
join9(opencodePluginDir, PULLFROG_OPENCODE_GATE_PLUGIN_FILENAME),
|
|
120960
|
+
buildOpencodeSubagentGateSource(ctx)
|
|
120884
120961
|
);
|
|
120885
120962
|
const agentBrowserVersion = getDevDependencyVersion("agent-browser");
|
|
120886
120963
|
addSkill({
|
|
@@ -120890,8 +120967,6 @@ var opencode = agent({
|
|
|
120890
120967
|
agent: "opencode"
|
|
120891
120968
|
});
|
|
120892
120969
|
installBundledSkills({ home: homeEnv.HOME });
|
|
120893
|
-
const codexAuth = installCodexAuth();
|
|
120894
|
-
const xaiAuth = installXaiAuth();
|
|
120895
120970
|
const permissionOverride = JSON.stringify({
|
|
120896
120971
|
external_directory: { "*": "deny", "/tmp/*": "allow" },
|
|
120897
120972
|
read: { "*": "allow", ...GIT_NATIVE_READ_DENY_OPENCODE },
|
|
@@ -120910,7 +120985,6 @@ var opencode = agent({
|
|
|
120910
120985
|
delete env2.OPENCODE_EXPERIMENTAL_CODE_MODE;
|
|
120911
120986
|
const writebacks = [];
|
|
120912
120987
|
if (codexAuth) {
|
|
120913
|
-
env2.XDG_DATA_HOME = codexAuth.xdgDataHome;
|
|
120914
120988
|
delete env2.OPENAI_API_KEY;
|
|
120915
120989
|
writebacks.push({
|
|
120916
120990
|
secretName: "CODEX_AUTH_JSON",
|
|
@@ -120921,7 +120995,6 @@ var opencode = agent({
|
|
|
120921
120995
|
});
|
|
120922
120996
|
}
|
|
120923
120997
|
if (xaiAuth) {
|
|
120924
|
-
env2.XDG_DATA_HOME = xaiAuth.xdgDataHome;
|
|
120925
120998
|
delete env2.XAI_API_KEY;
|
|
120926
120999
|
writebacks.push({
|
|
120927
121000
|
secretName: "GROK_AUTH_JSON",
|
|
@@ -120994,6 +121067,7 @@ var opencode = agent({
|
|
|
120994
121067
|
mcpToolCalls: 0,
|
|
120995
121068
|
recentStderr: server.recentStderr,
|
|
120996
121069
|
serverExitCause: server.exitCause,
|
|
121070
|
+
lastSessionError: void 0,
|
|
120997
121071
|
diagnostic: {
|
|
120998
121072
|
label: "Pullfrog",
|
|
120999
121073
|
recentStderr: server.recentStderr,
|
|
@@ -175912,7 +175986,7 @@ function closeBrowserDaemon(toolState) {
|
|
|
175912
175986
|
// mcp/checkout.ts
|
|
175913
175987
|
import { createHash as createHash2 } from "node:crypto";
|
|
175914
175988
|
import { statSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync11 } from "node:fs";
|
|
175915
|
-
import { join as
|
|
175989
|
+
import { join as join17 } from "node:path";
|
|
175916
175990
|
|
|
175917
175991
|
// node_modules/.pnpm/@ark+schema@0.56.0/node_modules/@ark/schema/out/shared/errors.js
|
|
175918
175992
|
var ArkError = class _ArkError extends CastableBase {
|
|
@@ -184283,13 +184357,14 @@ function ensureRepoState(toolState, init) {
|
|
|
184283
184357
|
// utils/changeImpact.ts
|
|
184284
184358
|
import { spawn as spawn4 } from "node:child_process";
|
|
184285
184359
|
import { writeFileSync as writeFileSync8 } from "node:fs";
|
|
184286
|
-
import { join as
|
|
184360
|
+
import { join as join10 } from "node:path";
|
|
184287
184361
|
var MAX_CANDIDATES = 24;
|
|
184288
184362
|
var MAX_ATOM_LENGTH = 128;
|
|
184289
184363
|
var MAX_ATOMS = 12;
|
|
184290
184364
|
var MAX_REFERENCES = 6;
|
|
184291
184365
|
var MAX_EXCERPT = 160;
|
|
184292
184366
|
var MAX_BYTES = 16e3;
|
|
184367
|
+
var LOOKUP_BUDGET_MS = 6e4;
|
|
184293
184368
|
function isSymbolShaped(atom) {
|
|
184294
184369
|
return atom.length >= 4 && atom.length <= MAX_ATOM_LENGTH && /[A-Z_]/.test(atom);
|
|
184295
184370
|
}
|
|
@@ -184492,7 +184567,7 @@ function consumeGrepChunk(params) {
|
|
|
184492
184567
|
async function lookupReferences(params) {
|
|
184493
184568
|
const buckets = emptyBuckets(params.candidates);
|
|
184494
184569
|
if (params.candidates.length === 0) return { buckets, error: void 0 };
|
|
184495
|
-
const
|
|
184570
|
+
const pattern = params.candidates.map((candidate) => candidate.atom).join("|");
|
|
184496
184571
|
const child = spawn4(
|
|
184497
184572
|
"git",
|
|
184498
184573
|
[
|
|
@@ -184503,13 +184578,14 @@ async function lookupReferences(params) {
|
|
|
184503
184578
|
"-z",
|
|
184504
184579
|
"-I",
|
|
184505
184580
|
"-w",
|
|
184506
|
-
"-
|
|
184507
|
-
|
|
184581
|
+
"-E",
|
|
184582
|
+
"-e",
|
|
184583
|
+
pattern,
|
|
184508
184584
|
params.treeish,
|
|
184509
184585
|
"--",
|
|
184510
184586
|
":(top)"
|
|
184511
184587
|
],
|
|
184512
|
-
{ env: resolveEnv(void 0), stdio: ["ignore", "pipe", "pipe"] }
|
|
184588
|
+
{ env: resolveEnv(void 0), stdio: ["ignore", "pipe", "pipe"], signal: params.signal }
|
|
184513
184589
|
);
|
|
184514
184590
|
const state = createGrepRecordState();
|
|
184515
184591
|
const maxAtomLength = Math.max(...params.candidates.map((candidate) => candidate.atom.length));
|
|
@@ -184529,7 +184605,7 @@ async function lookupReferences(params) {
|
|
|
184529
184605
|
child.stderr.resume();
|
|
184530
184606
|
const status = await new Promise((resolve3) => {
|
|
184531
184607
|
child.once("error", (error49) => {
|
|
184532
|
-
spawnError = error49.message;
|
|
184608
|
+
spawnError = error49.cause instanceof Error ? error49.cause.message : error49.message;
|
|
184533
184609
|
resolve3(null);
|
|
184534
184610
|
});
|
|
184535
184611
|
child.once("close", resolve3);
|
|
@@ -184617,8 +184693,10 @@ async function createChangeImpactArtifact(ctx, params) {
|
|
|
184617
184693
|
const lookup2 = await lookupReferences({
|
|
184618
184694
|
candidates: candidates.selected,
|
|
184619
184695
|
addedLines: candidates.addedLines,
|
|
184620
|
-
treeish: params.headSha
|
|
184696
|
+
treeish: params.headSha,
|
|
184697
|
+
signal: AbortSignal.any([params.signal, AbortSignal.timeout(LOOKUP_BUDGET_MS)])
|
|
184621
184698
|
});
|
|
184699
|
+
params.signal.throwIfAborted();
|
|
184622
184700
|
const artifact = formatArtifact({
|
|
184623
184701
|
entries: candidates.selected.map((change) => {
|
|
184624
184702
|
return { change, bucket: lookup2.buckets.get(change.atom) ?? { count: 0, samples: [] } };
|
|
@@ -184630,7 +184708,7 @@ async function createChangeImpactArtifact(ctx, params) {
|
|
|
184630
184708
|
headSha: params.headSha,
|
|
184631
184709
|
lookupError: lookup2.error
|
|
184632
184710
|
});
|
|
184633
|
-
const path4 =
|
|
184711
|
+
const path4 = join10(ctx.tmpdir, `pr-${params.pullNumber}-${params.headSha.slice(0, 7)}-impact.md`);
|
|
184634
184712
|
writeFileSync8(path4, artifact.content);
|
|
184635
184713
|
return {
|
|
184636
184714
|
path: path4,
|
|
@@ -184884,7 +184962,7 @@ function readNumber(params) {
|
|
|
184884
184962
|
import { execSync } from "node:child_process";
|
|
184885
184963
|
import { createHash } from "node:crypto";
|
|
184886
184964
|
import { readFileSync as readFileSync6, realpathSync, unlinkSync } from "node:fs";
|
|
184887
|
-
import { join as
|
|
184965
|
+
import { join as join11 } from "node:path";
|
|
184888
184966
|
|
|
184889
184967
|
// utils/shell.ts
|
|
184890
184968
|
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
@@ -184974,7 +185052,7 @@ function resolveHooksDir(cwd, gitPath) {
|
|
|
184974
185052
|
cwd,
|
|
184975
185053
|
log: false
|
|
184976
185054
|
}).trim();
|
|
184977
|
-
const hooksDir =
|
|
185055
|
+
const hooksDir = join11(commonDir, "hooks");
|
|
184978
185056
|
hooksDirCache.set(cwd, hooksDir);
|
|
184979
185057
|
return hooksDir;
|
|
184980
185058
|
}
|
|
@@ -185021,7 +185099,8 @@ async function $git(subcommand, args2, options) {
|
|
|
185021
185099
|
GIT_CONFIG_COUNT: "0",
|
|
185022
185100
|
GIT_CONFIG_PARAMETERS: ""
|
|
185023
185101
|
},
|
|
185024
|
-
activityTimeout: 0
|
|
185102
|
+
activityTimeout: 0,
|
|
185103
|
+
signal: options.signal
|
|
185025
185104
|
});
|
|
185026
185105
|
if (result.stderr.includes("askpass-compromised")) {
|
|
185027
185106
|
log2.info("askpass code was replayed after revoke \u2014 token has been revoked");
|
|
@@ -185088,9 +185167,10 @@ import { spawnSync as spawnSync8 } from "node:child_process";
|
|
|
185088
185167
|
// mcp/shell.ts
|
|
185089
185168
|
import { spawn as spawn5, spawnSync as spawnSync7 } from "node:child_process";
|
|
185090
185169
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
185091
|
-
import {
|
|
185170
|
+
import { once } from "node:events";
|
|
185171
|
+
import { closeSync, existsSync as existsSync9, openSync, writeFileSync as writeFileSync9 } from "node:fs";
|
|
185092
185172
|
import { userInfo as userInfo2 } from "node:os";
|
|
185093
|
-
import { join as
|
|
185173
|
+
import { join as join12 } from "node:path";
|
|
185094
185174
|
import { setTimeout as sleep3 } from "node:timers/promises";
|
|
185095
185175
|
|
|
185096
185176
|
// node_modules/.pnpm/@toon-format+toon@1.4.0/node_modules/@toon-format/toon/dist/index.mjs
|
|
@@ -185734,7 +185814,7 @@ function buildFsMounts(repoDir) {
|
|
|
185734
185814
|
function shimMount() {
|
|
185735
185815
|
const tempDir = process.env.PULLFROG_TEMP_DIR;
|
|
185736
185816
|
if (!tempDir) return "";
|
|
185737
|
-
const dir =
|
|
185817
|
+
const dir = join12(tempDir, "pm-bin").replace(/'/g, "'\\''");
|
|
185738
185818
|
return `[ -d '${dir}' ] && mount --bind '${dir}' '${dir}' 2>/dev/null && mount -o remount,bind,ro '${dir}' 2>/dev/null;`;
|
|
185739
185819
|
}
|
|
185740
185820
|
var _repoRoot;
|
|
@@ -185905,7 +185985,7 @@ ${spawnError}` : spawnError;
|
|
|
185905
185985
|
}
|
|
185906
185986
|
function capOutput(output) {
|
|
185907
185987
|
if (output.length <= MAX_OUTPUT_CHARS) return output;
|
|
185908
|
-
const fullPath =
|
|
185988
|
+
const fullPath = join12(getTempDir(), `shell-${randomUUID3().slice(0, 8)}.log`);
|
|
185909
185989
|
writeFileSync9(fullPath, output);
|
|
185910
185990
|
const elided = output.length - MAX_OUTPUT_CHARS;
|
|
185911
185991
|
return `... [${elided} chars truncated; output saved to ${fullPath}] ...
|
|
@@ -185942,6 +186022,7 @@ Do NOT use this tool for git commands \u2014 use the dedicated git tools instead
|
|
|
185942
186022
|
}
|
|
185943
186023
|
const timeout = Math.min(params.timeout ?? 3e4, 12e4);
|
|
185944
186024
|
const cwd = params.working_directory ?? process.cwd();
|
|
186025
|
+
if (!existsSync9(cwd)) throw new Error(`working_directory does not exist: ${cwd}`);
|
|
185945
186026
|
const env2 = resolveEnv(ctx.payload.shell === "enabled" ? "inherit" : "restricted");
|
|
185946
186027
|
if (params.command.includes("agent-browser")) {
|
|
185947
186028
|
const daemonError = ensureBrowserDaemon(ctx.toolState);
|
|
@@ -185960,8 +186041,8 @@ Do NOT use this tool for git commands \u2014 use the dedicated git tools instead
|
|
|
185960
186041
|
if (params.background) {
|
|
185961
186042
|
const tempDir = getTempDir();
|
|
185962
186043
|
const handle = `bg-${randomUUID3().slice(0, 8)}`;
|
|
185963
|
-
const outputPath =
|
|
185964
|
-
const pidPath =
|
|
186044
|
+
const outputPath = join12(tempDir, `${handle}.log`);
|
|
186045
|
+
const pidPath = join12(tempDir, `${handle}.pid`);
|
|
185965
186046
|
const logFd = openSync(outputPath, "a");
|
|
185966
186047
|
let proc;
|
|
185967
186048
|
try {
|
|
@@ -185975,7 +186056,8 @@ Do NOT use this tool for git commands \u2014 use the dedicated git tools instead
|
|
|
185975
186056
|
closeSync(logFd);
|
|
185976
186057
|
}
|
|
185977
186058
|
if (!proc.pid) {
|
|
185978
|
-
|
|
186059
|
+
const [error49] = await once(proc, "error");
|
|
186060
|
+
throw new Error(`failed to start background process: ${error49.message}`);
|
|
185979
186061
|
}
|
|
185980
186062
|
proc.unref();
|
|
185981
186063
|
writeFileSync9(pidPath, `${proc.pid}
|
|
@@ -186252,12 +186334,12 @@ function postProcessRangeDiff(raw2, contextLines = 3) {
|
|
|
186252
186334
|
// mcp/git.ts
|
|
186253
186335
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
186254
186336
|
import { writeFileSync as writeFileSync10 } from "node:fs";
|
|
186255
|
-
import { join as
|
|
186337
|
+
import { join as join15 } from "node:path";
|
|
186256
186338
|
|
|
186257
186339
|
// utils/apiCommit.ts
|
|
186258
186340
|
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
186259
186341
|
import { lstat, readlink } from "node:fs/promises";
|
|
186260
|
-
import { join as
|
|
186342
|
+
import { join as join13 } from "node:path";
|
|
186261
186343
|
var GITHUB_API = "https://api.github.com";
|
|
186262
186344
|
var MAX_BLOB_BYTES = 30 * 1024 * 1024;
|
|
186263
186345
|
var BLOB_UPLOAD_CONCURRENCY = 8;
|
|
@@ -186322,7 +186404,7 @@ async function assertApiCommittable(files) {
|
|
|
186322
186404
|
const root = getRepoRoot();
|
|
186323
186405
|
const attrs = $(
|
|
186324
186406
|
"git",
|
|
186325
|
-
["check-attr", "filter", "-z", "--", ...present.map((p) =>
|
|
186407
|
+
["check-attr", "filter", "-z", "--", ...present.map((p) => join13(root, p))],
|
|
186326
186408
|
{ log: false }
|
|
186327
186409
|
);
|
|
186328
186410
|
const parts = attrs.split("\0");
|
|
@@ -186334,7 +186416,7 @@ async function assertApiCommittable(files) {
|
|
|
186334
186416
|
}
|
|
186335
186417
|
}
|
|
186336
186418
|
for (const path4 of present) {
|
|
186337
|
-
const stat = await lstat(
|
|
186419
|
+
const stat = await lstat(join13(root, path4));
|
|
186338
186420
|
if (stat.isDirectory()) {
|
|
186339
186421
|
throw new Error(
|
|
186340
186422
|
`'${path4}' is a directory (nested repository or submodule?) \u2014 signed commits only support files and symlinks.`
|
|
@@ -186343,7 +186425,7 @@ async function assertApiCommittable(files) {
|
|
|
186343
186425
|
}
|
|
186344
186426
|
}
|
|
186345
186427
|
async function createBlobEntry(params) {
|
|
186346
|
-
const absPath =
|
|
186428
|
+
const absPath = join13(params.repoRoot, params.path);
|
|
186347
186429
|
const stat = await lstat(absPath);
|
|
186348
186430
|
if (stat.size > MAX_BLOB_BYTES) {
|
|
186349
186431
|
throw new Error(
|
|
@@ -186523,7 +186605,7 @@ this usually means your local branch has commits that were never pushed \u2014 s
|
|
|
186523
186605
|
var core4 = __toESM(require_core(), 1);
|
|
186524
186606
|
import { createSign } from "node:crypto";
|
|
186525
186607
|
import { rename, writeFile } from "node:fs/promises";
|
|
186526
|
-
import { dirname as dirname3, join as
|
|
186608
|
+
import { dirname as dirname3, join as join14 } from "node:path";
|
|
186527
186609
|
|
|
186528
186610
|
// node_modules/.pnpm/@octokit+plugin-throttling@11.0.5_@octokit+core@7.0.8/node_modules/@octokit/plugin-throttling/dist-bundle/index.js
|
|
186529
186611
|
var import_light = __toESM(require_light(), 1);
|
|
@@ -191072,7 +191154,7 @@ function getGitHubUsageSummary() {
|
|
|
191072
191154
|
}
|
|
191073
191155
|
async function writeGitHubUsageSummaryToFile(path4) {
|
|
191074
191156
|
const summary2 = getGitHubUsageSummary();
|
|
191075
|
-
const tmpPath =
|
|
191157
|
+
const tmpPath = join14(dirname3(path4), `.usage-summary-${process.pid}.tmp`);
|
|
191076
191158
|
await writeFile(tmpPath, JSON.stringify(summary2));
|
|
191077
191159
|
await rename(tmpPath, path4);
|
|
191078
191160
|
}
|
|
@@ -191700,7 +191782,7 @@ function countAhead(head, base, cwd) {
|
|
|
191700
191782
|
function spillGitOutput(params) {
|
|
191701
191783
|
const tempDir = process.env.PULLFROG_TEMP_DIR;
|
|
191702
191784
|
if (!tempDir) throw new Error("PULLFROG_TEMP_DIR not set");
|
|
191703
|
-
const outputPath =
|
|
191785
|
+
const outputPath = join15(tempDir, `git-${params.command}-${randomUUID4().slice(0, 8)}.txt`);
|
|
191704
191786
|
writeFileSync10(outputPath, params.output);
|
|
191705
191787
|
const previewByLines = params.output.split("\n").slice(0, OVERFLOW_PREVIEW_LINES).join("\n");
|
|
191706
191788
|
const preview = previewByLines.length <= OVERFLOW_PREVIEW_MAX_CHARS ? previewByLines : `${previewByLines.slice(0, OVERFLOW_PREVIEW_MAX_CHARS)}\u2026`;
|
|
@@ -193355,11 +193437,11 @@ async function reportReviewNodeId(ctx, params) {
|
|
|
193355
193437
|
|
|
193356
193438
|
// utils/setup.ts
|
|
193357
193439
|
import { execFileSync as execFileSync7, execSync as execSync2 } from "node:child_process";
|
|
193358
|
-
import { mkdtempSync as
|
|
193440
|
+
import { mkdtempSync as mkdtempSync3, readdirSync, realpathSync as realpathSync2, rmSync as rmSync4, unlinkSync as unlinkSync2 } from "node:fs";
|
|
193359
193441
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
193360
|
-
import { join as
|
|
193442
|
+
import { join as join16 } from "node:path";
|
|
193361
193443
|
function createTempDirectory() {
|
|
193362
|
-
const path4 =
|
|
193444
|
+
const path4 = mkdtempSync3(join16(tmpdir3(), "pullfrog-"));
|
|
193363
193445
|
process.env.PULLFROG_TEMP_DIR = path4;
|
|
193364
193446
|
log2.info(`\xBB created temp dir at ${path4}`);
|
|
193365
193447
|
return { path: path4, [Symbol.dispose]: () => removeTempDirectory(path4) };
|
|
@@ -193367,10 +193449,10 @@ function createTempDirectory() {
|
|
|
193367
193449
|
function removeTempDirectory(path4) {
|
|
193368
193450
|
try {
|
|
193369
193451
|
try {
|
|
193370
|
-
|
|
193452
|
+
rmSync4(path4, { recursive: true, force: true });
|
|
193371
193453
|
} catch {
|
|
193372
193454
|
execFileSync7("chmod", ["-R", "u+w", path4]);
|
|
193373
|
-
|
|
193455
|
+
rmSync4(path4, { recursive: true, force: true });
|
|
193374
193456
|
}
|
|
193375
193457
|
log2.debug(`\xBB removed temp dir at ${path4}`);
|
|
193376
193458
|
} catch (err) {
|
|
@@ -193419,13 +193501,13 @@ function wipeRunnerLeakSurface() {
|
|
|
193419
193501
|
return [];
|
|
193420
193502
|
}
|
|
193421
193503
|
};
|
|
193422
|
-
const fileCommandsDir =
|
|
193504
|
+
const fileCommandsDir = join16(runnerTemp, "_runner_file_commands");
|
|
193423
193505
|
for (const entry of listDir(fileCommandsDir)) {
|
|
193424
|
-
tryUnlink(
|
|
193506
|
+
tryUnlink(join16(fileCommandsDir, entry));
|
|
193425
193507
|
}
|
|
193426
193508
|
for (const entry of listDir(runnerTemp)) {
|
|
193427
193509
|
if (entry.endsWith(".sh") || /^git-credentials-.*\.config$/.test(entry)) {
|
|
193428
|
-
tryUnlink(
|
|
193510
|
+
tryUnlink(join16(runnerTemp, entry));
|
|
193429
193511
|
}
|
|
193430
193512
|
}
|
|
193431
193513
|
if (wiped.length > 0) {
|
|
@@ -193697,7 +193779,7 @@ async function ensureBeforeShaReachable(params) {
|
|
|
193697
193779
|
"origin",
|
|
193698
193780
|
tempBranch
|
|
193699
193781
|
],
|
|
193700
|
-
{ token: params.gitToken, refreshGitToken: params.refreshGitToken },
|
|
193782
|
+
{ token: params.gitToken, refreshGitToken: params.refreshGitToken, signal: params.signal },
|
|
193701
193783
|
`before_sha temp branch ${tempBranch}`
|
|
193702
193784
|
);
|
|
193703
193785
|
log2.debug(`\xBB fetched before_sha via temp branch ${tempBranch}`);
|
|
@@ -193738,7 +193820,7 @@ function cleanupStaleGitLocks() {
|
|
|
193738
193820
|
} catch {
|
|
193739
193821
|
continue;
|
|
193740
193822
|
}
|
|
193741
|
-
if (now - mtimeMs < STALE_LOCK_AGE_MS) continue;
|
|
193823
|
+
if (now - mtimeMs < STALE_LOCK_AGE_MS || mtimeMs >= performance.timeOrigin) continue;
|
|
193742
193824
|
try {
|
|
193743
193825
|
unlinkSync3(relPath);
|
|
193744
193826
|
log2.warning(`\xBB removed stale ${relPath} from prior run`);
|
|
@@ -193771,7 +193853,7 @@ async function abortIfPullRequestMoved(args2) {
|
|
|
193771
193853
|
);
|
|
193772
193854
|
}
|
|
193773
193855
|
async function checkoutPrBranch(pr, params) {
|
|
193774
|
-
const { octokit, owner, name, gitToken, refreshGitToken, toolState, beforeSha } = params;
|
|
193856
|
+
const { octokit, owner, name, gitToken, refreshGitToken, toolState, beforeSha, signal } = params;
|
|
193775
193857
|
rejectIfLeadingDash(pr.baseRef, "PR base ref");
|
|
193776
193858
|
rejectIfLeadingDash(pr.headRef, "PR head ref");
|
|
193777
193859
|
const repoState = requireRepoState(toolState, owner, name);
|
|
@@ -193785,7 +193867,7 @@ async function checkoutPrBranch(pr, params) {
|
|
|
193785
193867
|
log2.debug(`\xBB fetching base branch (${pr.baseRef})...`);
|
|
193786
193868
|
await $gitFetchWithDeepen(
|
|
193787
193869
|
["--no-tags", "origin", pr.baseRef],
|
|
193788
|
-
{ token: gitToken, refreshGitToken },
|
|
193870
|
+
{ token: gitToken, refreshGitToken, signal },
|
|
193789
193871
|
`base branch ${pr.baseRef}`
|
|
193790
193872
|
);
|
|
193791
193873
|
if (!alreadyOnBranch) {
|
|
@@ -193796,7 +193878,7 @@ async function checkoutPrBranch(pr, params) {
|
|
|
193796
193878
|
try {
|
|
193797
193879
|
await $gitFetchWithDeepen(
|
|
193798
193880
|
["--no-tags", "origin", `+pull/${pr.number}/head:${localBranch}`],
|
|
193799
|
-
{ token: gitToken, refreshGitToken },
|
|
193881
|
+
{ token: gitToken, refreshGitToken, signal },
|
|
193800
193882
|
`PR #${pr.number}`
|
|
193801
193883
|
);
|
|
193802
193884
|
} catch (e) {
|
|
@@ -193821,7 +193903,8 @@ async function checkoutPrBranch(pr, params) {
|
|
|
193821
193903
|
repo: name,
|
|
193822
193904
|
gitToken,
|
|
193823
193905
|
refreshGitToken,
|
|
193824
|
-
isShallow
|
|
193906
|
+
isShallow,
|
|
193907
|
+
signal
|
|
193825
193908
|
}) : false;
|
|
193826
193909
|
if (isShallow) {
|
|
193827
193910
|
let deepenDepth = 0;
|
|
@@ -193856,7 +193939,8 @@ async function checkoutPrBranch(pr, params) {
|
|
|
193856
193939
|
if (deepenDepth) {
|
|
193857
193940
|
log2.debug(`\xBB deepening by ${deepenDepth} to reach merge base...`);
|
|
193858
193941
|
await $git("fetch", [`--deepen=${deepenDepth}`, "--no-tags", "origin"], {
|
|
193859
|
-
token: gitToken
|
|
193942
|
+
token: gitToken,
|
|
193943
|
+
signal
|
|
193860
193944
|
});
|
|
193861
193945
|
}
|
|
193862
193946
|
}
|
|
@@ -193894,6 +193978,7 @@ async function checkoutPrBranch(pr, params) {
|
|
|
193894
193978
|
remoteBranch: pr.headRef,
|
|
193895
193979
|
localBranch
|
|
193896
193980
|
};
|
|
193981
|
+
signal.throwIfAborted();
|
|
193897
193982
|
const postCheckoutHook = await executeLifecycleHook({
|
|
193898
193983
|
event: "post-checkout",
|
|
193899
193984
|
script: params.postCheckoutScript,
|
|
@@ -193904,19 +193989,30 @@ async function checkoutPrBranch(pr, params) {
|
|
|
193904
193989
|
}
|
|
193905
193990
|
var inFlightCheckouts = /* @__PURE__ */ new Map();
|
|
193906
193991
|
var CHECKOUT_DEADLINE_MS = 57e4;
|
|
193907
|
-
|
|
193908
|
-
|
|
193909
|
-
const
|
|
193910
|
-
|
|
193992
|
+
var CHECKOUT_ABORT_GRACE_MS = 15e3;
|
|
193993
|
+
function withCheckoutDeadline(run4, pullNumber) {
|
|
193994
|
+
const exceeded = `checkout_pr #${pullNumber} exceeded ${CHECKOUT_DEADLINE_MS / 1e3}s`;
|
|
193995
|
+
const retry = "retry the call to start a fresh attempt.";
|
|
193996
|
+
const controller = new AbortController();
|
|
193997
|
+
const abortTimer = setTimeout(
|
|
193998
|
+
() => controller.abort(new Error(`${exceeded} and was cancelled; ${retry}`)),
|
|
193999
|
+
CHECKOUT_DEADLINE_MS
|
|
194000
|
+
);
|
|
194001
|
+
let backstopTimer;
|
|
194002
|
+
const backstop = new Promise((_, reject) => {
|
|
194003
|
+
backstopTimer = setTimeout(
|
|
193911
194004
|
() => reject(
|
|
193912
194005
|
new Error(
|
|
193913
|
-
|
|
194006
|
+
`${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}`
|
|
193914
194007
|
)
|
|
193915
194008
|
),
|
|
193916
|
-
CHECKOUT_DEADLINE_MS
|
|
194009
|
+
CHECKOUT_DEADLINE_MS + CHECKOUT_ABORT_GRACE_MS
|
|
193917
194010
|
);
|
|
193918
194011
|
});
|
|
193919
|
-
return Promise.race([
|
|
194012
|
+
return Promise.race([run4(controller.signal), backstop]).finally(() => {
|
|
194013
|
+
clearTimeout(abortTimer);
|
|
194014
|
+
clearTimeout(backstopTimer);
|
|
194015
|
+
});
|
|
193920
194016
|
}
|
|
193921
194017
|
function headsEqual(a2, b) {
|
|
193922
194018
|
if (a2.kind === "branch" && b.kind === "branch") return a2.name === b.name;
|
|
@@ -193928,7 +194024,7 @@ function describeHead(h2) {
|
|
|
193928
194024
|
return `detached HEAD \`${h2.sha}\``;
|
|
193929
194025
|
}
|
|
193930
194026
|
function CheckoutPrTool(ctx) {
|
|
193931
|
-
const runCheckout = async (pull_number) => {
|
|
194027
|
+
const runCheckout = async (pull_number, signal) => {
|
|
193932
194028
|
const prResponse = await ctx.octokit.rest.pulls.get({
|
|
193933
194029
|
owner: ctx.repo.owner,
|
|
193934
194030
|
repo: ctx.repo.name,
|
|
@@ -193957,7 +194053,8 @@ function CheckoutPrTool(ctx) {
|
|
|
193957
194053
|
toolState: ctx.toolState,
|
|
193958
194054
|
shell: ctx.payload.shell,
|
|
193959
194055
|
postCheckoutScript: ctx.postCheckoutScript,
|
|
193960
|
-
beforeSha: primary.beforeSha
|
|
194056
|
+
beforeSha: primary.beforeSha,
|
|
194057
|
+
signal
|
|
193961
194058
|
});
|
|
193962
194059
|
const tempDir = process.env.PULLFROG_TEMP_DIR;
|
|
193963
194060
|
if (!tempDir) {
|
|
@@ -193979,7 +194076,7 @@ function CheckoutPrTool(ctx) {
|
|
|
193979
194076
|
headSha: checkoutSha
|
|
193980
194077
|
});
|
|
193981
194078
|
if (incremental.status === "ok") {
|
|
193982
|
-
incrementalDiffPath =
|
|
194079
|
+
incrementalDiffPath = join17(
|
|
193983
194080
|
tempDir,
|
|
193984
194081
|
`pr-${pull_number}-${beforeShort}-${headShort}-incremental.diff`
|
|
193985
194082
|
);
|
|
@@ -193996,7 +194093,7 @@ function CheckoutPrTool(ctx) {
|
|
|
193996
194093
|
const diffPreview = formatResult.content.split("\n").slice(0, 100).join("\n");
|
|
193997
194094
|
log2.debug(`formatted diff preview (first 100 lines):
|
|
193998
194095
|
${diffPreview}`);
|
|
193999
|
-
const diffPath =
|
|
194096
|
+
const diffPath = join17(tempDir, `pr-${pull_number}-${headShort}.diff`);
|
|
194000
194097
|
writeFileSync11(diffPath, formatResult.content);
|
|
194001
194098
|
log2.debug(`wrote diff to ${diffPath} (${formatResult.content.length} bytes)`);
|
|
194002
194099
|
primary.diffCoverage = createDiffCoverageState({
|
|
@@ -194036,7 +194133,8 @@ ${diffPreview}`);
|
|
|
194036
194133
|
files: formatResult.files,
|
|
194037
194134
|
pullNumber: pull_number,
|
|
194038
194135
|
baseSha: prResponse.data.base.sha,
|
|
194039
|
-
headSha: checkoutSha
|
|
194136
|
+
headSha: checkoutSha,
|
|
194137
|
+
signal
|
|
194040
194138
|
});
|
|
194041
194139
|
impactPath = impact.path;
|
|
194042
194140
|
log2.info(
|
|
@@ -194142,7 +194240,10 @@ ${dirty}`
|
|
|
194142
194240
|
);
|
|
194143
194241
|
}
|
|
194144
194242
|
}
|
|
194145
|
-
const promise2 = withCheckoutDeadline(
|
|
194243
|
+
const promise2 = withCheckoutDeadline(
|
|
194244
|
+
(signal) => runCheckout(pull_number, signal),
|
|
194245
|
+
pull_number
|
|
194246
|
+
);
|
|
194146
194247
|
inFlightCheckouts.set(pull_number, promise2);
|
|
194147
194248
|
try {
|
|
194148
194249
|
return await promise2;
|
|
@@ -194155,7 +194256,7 @@ ${dirty}`
|
|
|
194155
194256
|
|
|
194156
194257
|
// mcp/checkSuite.ts
|
|
194157
194258
|
import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync12 } from "node:fs";
|
|
194158
|
-
import { join as
|
|
194259
|
+
import { join as join18 } from "node:path";
|
|
194159
194260
|
var GetCheckSuiteLogs = type({
|
|
194160
194261
|
check_suite_id: type.number.describe("the id from check_suite.id")
|
|
194161
194262
|
});
|
|
@@ -194251,7 +194352,7 @@ function GetCheckSuiteLogsTool(ctx) {
|
|
|
194251
194352
|
if (!tempDir) {
|
|
194252
194353
|
throw new Error("PULLFROG_TEMP_DIR not set");
|
|
194253
194354
|
}
|
|
194254
|
-
const logsDir =
|
|
194355
|
+
const logsDir = join18(tempDir, "ci-logs");
|
|
194255
194356
|
mkdirSync8(logsDir, { recursive: true });
|
|
194256
194357
|
const jobResults = [];
|
|
194257
194358
|
for (const run4 of failedRuns) {
|
|
@@ -194278,7 +194379,7 @@ function GetCheckSuiteLogsTool(ctx) {
|
|
|
194278
194379
|
);
|
|
194279
194380
|
}
|
|
194280
194381
|
const logsText = await logsResult.text();
|
|
194281
|
-
const logPath =
|
|
194382
|
+
const logPath = join18(logsDir, `job-${job.id}.log`);
|
|
194282
194383
|
writeFileSync12(logPath, logsText);
|
|
194283
194384
|
const analysis = analyzeLog(logsText, 80);
|
|
194284
194385
|
const failedSteps = job.steps?.filter((s) => s.conclusion === "failure").map((s) => `Step ${s.number}: ${s.name}`) ?? [];
|
|
@@ -194328,7 +194429,7 @@ function GetCheckSuiteLogsTool(ctx) {
|
|
|
194328
194429
|
|
|
194329
194430
|
// mcp/commitInfo.ts
|
|
194330
194431
|
import { writeFileSync as writeFileSync13 } from "node:fs";
|
|
194331
|
-
import { join as
|
|
194432
|
+
import { join as join19 } from "node:path";
|
|
194332
194433
|
var CommitInfo = type({
|
|
194333
194434
|
sha: type.string.describe("the commit SHA (full or abbreviated) to fetch")
|
|
194334
194435
|
});
|
|
@@ -194352,7 +194453,7 @@ function CommitInfoTool(ctx) {
|
|
|
194352
194453
|
"PULLFROG_TEMP_DIR not set - get_commit_info must run in pullfrog action context"
|
|
194353
194454
|
);
|
|
194354
194455
|
}
|
|
194355
|
-
const diffFile =
|
|
194456
|
+
const diffFile = join19(tempDir, `commit-${sha.slice(0, 7)}.diff`);
|
|
194356
194457
|
writeFileSync13(diffFile, formatResult.content);
|
|
194357
194458
|
log2.debug(`wrote commit diff to ${diffFile} (${formatResult.content.length} bytes)`);
|
|
194358
194459
|
return {
|
|
@@ -194489,14 +194590,14 @@ function ReopenCurrentTool(ctx) {
|
|
|
194489
194590
|
import { performance as performance8 } from "node:perf_hooks";
|
|
194490
194591
|
|
|
194491
194592
|
// prep/installNodeDependencies.ts
|
|
194492
|
-
import { existsSync as
|
|
194493
|
-
import { join as
|
|
194593
|
+
import { existsSync as existsSync11 } from "node:fs";
|
|
194594
|
+
import { join as join21 } from "node:path";
|
|
194494
194595
|
|
|
194495
194596
|
// utils/packageManager.ts
|
|
194496
194597
|
var import_semver2 = __toESM(require_semver2(), 1);
|
|
194497
|
-
import { existsSync as
|
|
194598
|
+
import { existsSync as existsSync10 } from "node:fs";
|
|
194498
194599
|
import { mkdir, readFile as readFile3 } from "node:fs/promises";
|
|
194499
|
-
import { delimiter as delimiter2, join as
|
|
194600
|
+
import { delimiter as delimiter2, join as join20 } from "node:path";
|
|
194500
194601
|
var SUPPORTED_NAMES = ["npm", "pnpm", "yarn", "bun"];
|
|
194501
194602
|
var COREPACK_MANAGED = ["pnpm", "yarn"];
|
|
194502
194603
|
function isSupported(name) {
|
|
@@ -194534,8 +194635,8 @@ function parseDevEnginesField(field) {
|
|
|
194534
194635
|
};
|
|
194535
194636
|
}
|
|
194536
194637
|
async function resolvePackageManagerSpec(cwd) {
|
|
194537
|
-
const pkgPath =
|
|
194538
|
-
if (!
|
|
194638
|
+
const pkgPath = join20(cwd, "package.json");
|
|
194639
|
+
if (!existsSync10(pkgPath)) return null;
|
|
194539
194640
|
let pkg;
|
|
194540
194641
|
try {
|
|
194541
194642
|
pkg = JSON.parse(await readFile3(pkgPath, "utf-8"));
|
|
@@ -194593,7 +194694,7 @@ async function currentVersion(name) {
|
|
|
194593
194694
|
return result.stdout.trim();
|
|
194594
194695
|
}
|
|
194595
194696
|
function packageManagerBinDir(tmpdir4) {
|
|
194596
|
-
return
|
|
194697
|
+
return join20(tmpdir4, "pm-bin");
|
|
194597
194698
|
}
|
|
194598
194699
|
async function ensurePackageManager(params) {
|
|
194599
194700
|
const spec = params.spec;
|
|
@@ -194656,7 +194757,7 @@ async function installFallback(name, installSpec) {
|
|
|
194656
194757
|
});
|
|
194657
194758
|
if (result.exitCode !== 0) return result.stderr || `failed to install ${name}`;
|
|
194658
194759
|
if (name === "deno") {
|
|
194659
|
-
const denoPath =
|
|
194760
|
+
const denoPath = join20(process.env.HOME || "", ".deno", "bin");
|
|
194660
194761
|
process.env.PATH = `${denoPath}:${process.env.PATH}`;
|
|
194661
194762
|
}
|
|
194662
194763
|
log2.info(`\xBB installed ${name}`);
|
|
@@ -194689,8 +194790,8 @@ async function isCommandAvailable2(command) {
|
|
|
194689
194790
|
var installNodeDependencies = {
|
|
194690
194791
|
name: "installNodeDependencies",
|
|
194691
194792
|
shouldRun: () => {
|
|
194692
|
-
const packageJsonPath =
|
|
194693
|
-
return
|
|
194793
|
+
const packageJsonPath = join21(process.cwd(), "package.json");
|
|
194794
|
+
return existsSync11(packageJsonPath);
|
|
194694
194795
|
},
|
|
194695
194796
|
run: async (options) => {
|
|
194696
194797
|
const declared = await resolvePackageManagerSpec(process.cwd());
|
|
@@ -194784,14 +194885,14 @@ ${errorMessage}`]
|
|
|
194784
194885
|
};
|
|
194785
194886
|
|
|
194786
194887
|
// prep/installPythonDependencies.ts
|
|
194787
|
-
import { existsSync as
|
|
194788
|
-
import { join as
|
|
194888
|
+
import { existsSync as existsSync12, readFileSync as readFileSync7 } from "node:fs";
|
|
194889
|
+
import { join as join22 } from "node:path";
|
|
194789
194890
|
function declaresBuildSystem(path4) {
|
|
194790
194891
|
return /^\s*\[\s*build-system\s*\]/m.test(readFileSync7(path4, "utf8"));
|
|
194791
194892
|
}
|
|
194792
194893
|
function configApplies(config3, cwd) {
|
|
194793
|
-
const path4 =
|
|
194794
|
-
return
|
|
194894
|
+
const path4 = join22(cwd, config3.file);
|
|
194895
|
+
return existsSync12(path4) && (config3.applies?.(path4) ?? true);
|
|
194795
194896
|
}
|
|
194796
194897
|
var PYTHON_CONFIGS = [
|
|
194797
194898
|
{
|
|
@@ -195113,7 +195214,7 @@ function AwaitDependencyInstallationTool(ctx) {
|
|
|
195113
195214
|
|
|
195114
195215
|
// mcp/gh.ts
|
|
195115
195216
|
import { mkdirSync as mkdirSync9 } from "node:fs";
|
|
195116
|
-
import { join as
|
|
195217
|
+
import { join as join23 } from "node:path";
|
|
195117
195218
|
var GH_TIMEOUT_MS = 12e4;
|
|
195118
195219
|
var GhParams = type({
|
|
195119
195220
|
args: type.string.array().describe(
|
|
@@ -195135,7 +195236,7 @@ function GhTool(ctx) {
|
|
|
195135
195236
|
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.`,
|
|
195136
195237
|
parameters: GhParams,
|
|
195137
195238
|
execute: execute(async (params) => {
|
|
195138
|
-
const configDir =
|
|
195239
|
+
const configDir = join23(ctx.tmpdir, "gh-config");
|
|
195139
195240
|
mkdirSync9(configDir, { recursive: true });
|
|
195140
195241
|
const command = ["gh", ...params.args.map(shellQuote)].join(" ");
|
|
195141
195242
|
const result = await runSandboxed({
|
|
@@ -195859,7 +195960,7 @@ function PullRequestInfoTool(ctx) {
|
|
|
195859
195960
|
|
|
195860
195961
|
// mcp/reviewComments.ts
|
|
195861
195962
|
import { writeFileSync as writeFileSync14 } from "node:fs";
|
|
195862
|
-
import { join as
|
|
195963
|
+
import { join as join24 } from "node:path";
|
|
195863
195964
|
var COMMENTS_PER_THREAD = 50;
|
|
195864
195965
|
var REVIEW_THREADS_QUERY = `
|
|
195865
195966
|
query ($owner: String!, $name: String!, $prNumber: Int!, $cursor: String) {
|
|
@@ -196353,7 +196454,7 @@ function GetReviewCommentsTool(ctx) {
|
|
|
196353
196454
|
throw new Error("PULLFROG_TEMP_DIR not set");
|
|
196354
196455
|
}
|
|
196355
196456
|
const filename = `review-${params.review_id}-threads.md`;
|
|
196356
|
-
const commentsPath =
|
|
196457
|
+
const commentsPath = join24(tempDir, filename);
|
|
196357
196458
|
writeFileSync14(commentsPath, formatted.content);
|
|
196358
196459
|
log2.debug(`wrote ${threadBlocks.length} threads to ${commentsPath}`);
|
|
196359
196460
|
return {
|
|
@@ -196722,8 +196823,8 @@ function UploadFileTool(ctx) {
|
|
|
196722
196823
|
}
|
|
196723
196824
|
|
|
196724
196825
|
// mcp/xrepo.ts
|
|
196725
|
-
import { mkdirSync as mkdirSync10, rmSync as
|
|
196726
|
-
import { join as
|
|
196826
|
+
import { mkdirSync as mkdirSync10, rmSync as rmSync5 } from "node:fs";
|
|
196827
|
+
import { join as join25 } from "node:path";
|
|
196727
196828
|
function assertValidRepoName(name) {
|
|
196728
196829
|
if (!/^[A-Za-z0-9._-]+$/.test(name) || name.startsWith("-") || name.includes("..")) {
|
|
196729
196830
|
throw new Error(
|
|
@@ -196803,7 +196904,7 @@ function CheckoutRepoTool(ctx) {
|
|
|
196803
196904
|
return { path: existing.dir, access: existing.access, note: "already checked out." };
|
|
196804
196905
|
}
|
|
196805
196906
|
const access = accessFor(ctx, repo);
|
|
196806
|
-
const dir =
|
|
196907
|
+
const dir = join25(ctx.tmpdir, "xrepo", repo);
|
|
196807
196908
|
mkdirSync10(dir, { recursive: true });
|
|
196808
196909
|
const state = ensureRepoState(ctx.toolState, { owner, name: repo, dir, access });
|
|
196809
196910
|
const rc = resolveRepoCtx(ctx, repo);
|
|
@@ -196834,7 +196935,7 @@ function CheckoutRepoTool(ctx) {
|
|
|
196834
196935
|
return { path: dir, access, defaultBranch };
|
|
196835
196936
|
} catch (err) {
|
|
196836
196937
|
ctx.toolState.repos.delete(repoKey(owner, repo));
|
|
196837
|
-
|
|
196938
|
+
rmSync5(dir, { recursive: true, force: true });
|
|
196838
196939
|
throw err;
|
|
196839
196940
|
}
|
|
196840
196941
|
})
|
|
@@ -197169,6 +197270,7 @@ function isByokSetupError(text) {
|
|
|
197169
197270
|
var SECRETS_UNAVAILABLE_MARKER = "couldn't load your Pullfrog secrets";
|
|
197170
197271
|
var ROUTER_UNFUNDED_MARKER = "your Pullfrog Router balance is empty";
|
|
197171
197272
|
var CREDENTIAL_REJECTED_MARKER = "was rejected by its provider";
|
|
197273
|
+
var CREDENTIAL_POOL_REFUSED_MARKER = "No configured credential can serve this run";
|
|
197172
197274
|
var NoUsableCredentialError = class extends Error {
|
|
197173
197275
|
};
|
|
197174
197276
|
function throwKeyError(params) {
|
|
@@ -197435,7 +197537,8 @@ function isOAuthCredentialExpiredError(text) {
|
|
|
197435
197537
|
text
|
|
197436
197538
|
) || // the provider no longer recognises the token at all (#1086) — same dead
|
|
197437
197539
|
// credential, phrased as a lookup miss rather than a state transition.
|
|
197438
|
-
/Could not find the appropriate key in your authentication token/i.test(text) ||
|
|
197540
|
+
/Could not find the appropriate key in your authentication token/i.test(text) || // the reversed word order: `Encountered invalidated oauth token for user` (#1474).
|
|
197541
|
+
/(?:expired|invalidated|revoked) (?:OAuth|authentication) token/i.test(text) || /Token refresh failed/i.test(text)
|
|
197439
197542
|
);
|
|
197440
197543
|
}
|
|
197441
197544
|
function isMalformedKeyError(text) {
|
|
@@ -197482,7 +197585,7 @@ function buildRejectedCredentialError(params) {
|
|
|
197482
197585
|
].join("\n");
|
|
197483
197586
|
}
|
|
197484
197587
|
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)`;
|
|
197485
|
-
const keysUrl =
|
|
197588
|
+
const keysUrl = providerDashboard(params.credential)?.keys;
|
|
197486
197589
|
const issue3 = keysUrl ? `[Issue a new key \u2192](${keysUrl}) \xB7 ` : "";
|
|
197487
197590
|
return [
|
|
197488
197591
|
`**Your \`${params.credential}\` ${CREDENTIAL_REJECTED_MARKER}**${detail}, so the agent never ran.`,
|
|
@@ -197492,12 +197595,43 @@ function buildRejectedCredentialError(params) {
|
|
|
197492
197595
|
`${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)`
|
|
197493
197596
|
].join("\n");
|
|
197494
197597
|
}
|
|
197495
|
-
function
|
|
197598
|
+
function providerDashboard(credential) {
|
|
197496
197599
|
for (const [id, provider2] of Object.entries(providers)) {
|
|
197497
|
-
if (provider2.envVars.includes(credential)) return PROVIDER_DASHBOARDS[id]
|
|
197600
|
+
if (provider2.envVars.includes(credential)) return PROVIDER_DASHBOARDS[id];
|
|
197498
197601
|
}
|
|
197499
197602
|
return void 0;
|
|
197500
197603
|
}
|
|
197604
|
+
function buildCredentialPoolRefusedError(params) {
|
|
197605
|
+
const settingsUrl = `${getApiUrl()}/console/${params.owner}/${params.name}`;
|
|
197606
|
+
return [
|
|
197607
|
+
`**${CREDENTIAL_POOL_REFUSED_MARKER}.** Every credential configured for \`${params.model}\` was rejected or is out of quota, so the agent never ran.`,
|
|
197608
|
+
"",
|
|
197609
|
+
...params.refused.map(
|
|
197610
|
+
(credential) => `- ${describeRefusal({ ...params, credential, settingsUrl })}`
|
|
197611
|
+
),
|
|
197612
|
+
"",
|
|
197613
|
+
`[Model settings \u2192](${settingsUrl}) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
|
|
197614
|
+
].join("\n");
|
|
197615
|
+
}
|
|
197616
|
+
function describeRefusal(params) {
|
|
197617
|
+
const credential = params.credential;
|
|
197618
|
+
const verdict = credential.verdict;
|
|
197619
|
+
const workflow = credential.source === "workflow";
|
|
197620
|
+
const head = `\`${credential.name}\` from ${workflow ? "the workflow" : `${credential.source} scope`}: \`${verdict.detail}\` \u2014`;
|
|
197621
|
+
const subscription = SUBSCRIPTION_CREDENTIALS[credential.name];
|
|
197622
|
+
const dashboard = providerDashboard(credential.name);
|
|
197623
|
+
if (verdict.status === "exhausted" && subscription) {
|
|
197624
|
+
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";
|
|
197625
|
+
return `${head} ${wait}, or add an API key for this provider, which Pullfrog falls back to automatically.`;
|
|
197626
|
+
}
|
|
197627
|
+
if (verdict.status === "exhausted")
|
|
197628
|
+
return `${head} add credit or raise its quota with the provider.${dashboard ? ` [Top up \u2192](${dashboard.billing})` : ""}`;
|
|
197629
|
+
if (subscription)
|
|
197630
|
+
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})`;
|
|
197631
|
+
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})`;
|
|
197632
|
+
const issue3 = dashboard?.keys ? `[Issue a new key \u2192](${dashboard.keys}) \xB7 ` : "";
|
|
197633
|
+
return `${head} issue a new key and update the copy Pullfrog uses. ${issue3}${where}`;
|
|
197634
|
+
}
|
|
197501
197635
|
function formatApiKeyErrorSummary(params) {
|
|
197502
197636
|
if (params.raw.includes(MISSING_KEY_MARKER)) {
|
|
197503
197637
|
if (params.raw.startsWith(`**${MISSING_KEY_MARKER}**`)) return params.raw;
|
|
@@ -198227,11 +198361,16 @@ async function selectConfiguredCredential(input) {
|
|
|
198227
198361
|
if (option.candidate) log2.info(`\xBB selected ${option.name} from ${from}`);
|
|
198228
198362
|
return true;
|
|
198229
198363
|
}
|
|
198230
|
-
refused.push(
|
|
198364
|
+
refused.push({ name: option.name, source: option.candidate?.source ?? "workflow", verdict });
|
|
198231
198365
|
log2.info(`\xBB ${option.name} from ${from} unavailable; trying the next credential`);
|
|
198232
198366
|
}
|
|
198233
198367
|
throw new Error(
|
|
198234
|
-
|
|
198368
|
+
buildCredentialPoolRefusedError({
|
|
198369
|
+
model,
|
|
198370
|
+
refused,
|
|
198371
|
+
owner: input.ctx.repo.owner,
|
|
198372
|
+
name: input.ctx.repo.name
|
|
198373
|
+
})
|
|
198235
198374
|
);
|
|
198236
198375
|
}
|
|
198237
198376
|
async function probe(input) {
|
|
@@ -198272,7 +198411,7 @@ function activate(input) {
|
|
|
198272
198411
|
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
198273
198412
|
import { writeFileSync as writeFileSync15 } from "node:fs";
|
|
198274
198413
|
import { createServer as createServer3 } from "node:http";
|
|
198275
|
-
import { join as
|
|
198414
|
+
import { join as join26 } from "node:path";
|
|
198276
198415
|
var REVOKED_TRAP_MS = 6e4;
|
|
198277
198416
|
function revokeGitHubToken(token) {
|
|
198278
198417
|
fetch("https://api.github.com/installation/token", {
|
|
@@ -198341,7 +198480,7 @@ async function startGitAuthServer(tmpdir4) {
|
|
|
198341
198480
|
function writeAskpassScript(code) {
|
|
198342
198481
|
const scriptId = randomUUID6();
|
|
198343
198482
|
const scriptName = `askpass-${scriptId}.js`;
|
|
198344
|
-
const scriptPath =
|
|
198483
|
+
const scriptPath = join26(tmpdir4, scriptName);
|
|
198345
198484
|
const content = [
|
|
198346
198485
|
`#!/usr/bin/env node`,
|
|
198347
198486
|
`var a=process.argv[2]||"";`,
|
|
@@ -198790,7 +198929,7 @@ function resolveInstructions(ctx) {
|
|
|
198790
198929
|
|
|
198791
198930
|
// utils/learnings.ts
|
|
198792
198931
|
import { mkdir as mkdir2, readFile as readFile4, writeFile as writeFile2 } from "node:fs/promises";
|
|
198793
|
-
import { dirname as dirname4, join as
|
|
198932
|
+
import { dirname as dirname4, join as join27 } from "node:path";
|
|
198794
198933
|
|
|
198795
198934
|
// utils/learningsTruncate.ts
|
|
198796
198935
|
var MAX_LEARNINGS_LENGTH = 1e5;
|
|
@@ -198808,10 +198947,10 @@ function truncateAtLineBoundary(body, cap) {
|
|
|
198808
198947
|
var LEARNINGS_FILE_NAME = "pullfrog-learnings.md";
|
|
198809
198948
|
var XREPO_LEARNINGS_FILE_NAME = "pullfrog-xrepo-learnings.md";
|
|
198810
198949
|
function learningsFilePath(tmpdir4) {
|
|
198811
|
-
return
|
|
198950
|
+
return join27(tmpdir4, LEARNINGS_FILE_NAME);
|
|
198812
198951
|
}
|
|
198813
198952
|
function xrepoLearningsFilePath(tmpdir4) {
|
|
198814
|
-
return
|
|
198953
|
+
return join27(tmpdir4, XREPO_LEARNINGS_FILE_NAME);
|
|
198815
198954
|
}
|
|
198816
198955
|
async function seedLearningsFile(params) {
|
|
198817
198956
|
const path4 = learningsFilePath(params.tmpdir);
|
|
@@ -199362,7 +199501,9 @@ async function resolveTokens(params) {
|
|
|
199362
199501
|
// commit-status check-runs for branch protection. the app already grants
|
|
199363
199502
|
// checks:write; this scopes the MCP token up to use it.
|
|
199364
199503
|
checks: "write",
|
|
199365
|
-
|
|
199504
|
+
// write (not read) so postReviewCleanup can dispatch the follow-up re-review
|
|
199505
|
+
// for commits pushed while a review was in flight; read 403s it.
|
|
199506
|
+
actions: "write"
|
|
199366
199507
|
};
|
|
199367
199508
|
const mcpToken = await acquireNewToken({
|
|
199368
199509
|
repos: writeRepos,
|
|
@@ -199748,7 +199889,7 @@ async function resolveTrialFallback(ctx) {
|
|
|
199748
199889
|
|
|
199749
199890
|
// utils/prSummary.ts
|
|
199750
199891
|
import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile3 } from "node:fs/promises";
|
|
199751
|
-
import { dirname as dirname5, join as
|
|
199892
|
+
import { dirname as dirname5, join as join28 } from "node:path";
|
|
199752
199893
|
var SUMMARY_FILE_NAME = "pullfrog-summary.md";
|
|
199753
199894
|
var SUMMARY_SCAFFOLD = `# PR summary
|
|
199754
199895
|
|
|
@@ -199758,7 +199899,7 @@ var SUMMARY_SCAFFOLD = `# PR summary
|
|
|
199758
199899
|
var MIN_SNAPSHOT_LENGTH = 60;
|
|
199759
199900
|
var MAX_SNAPSHOT_LENGTH = 32768;
|
|
199760
199901
|
function summaryFilePath(tmpdir4) {
|
|
199761
|
-
return
|
|
199902
|
+
return join28(tmpdir4, SUMMARY_FILE_NAME);
|
|
199762
199903
|
}
|
|
199763
199904
|
async function seedSummaryFile(params) {
|
|
199764
199905
|
const path4 = summaryFilePath(params.tmpdir);
|
|
@@ -200027,7 +200168,7 @@ function formatBillingExhaustedBody(diagnostic) {
|
|
|
200027
200168
|
|
|
200028
200169
|
// utils/runErrorRenderer.ts
|
|
200029
200170
|
function isProviderModelNotFoundError(message) {
|
|
200030
|
-
return message
|
|
200171
|
+
return /ProviderModelNotFoundError|\bModel not found: /.test(message);
|
|
200031
200172
|
}
|
|
200032
200173
|
function isContextOverflowError(message) {
|
|
200033
200174
|
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);
|
|
@@ -200124,6 +200265,7 @@ function detectProviderId(message) {
|
|
|
200124
200265
|
if (/requires more credits|Key limit exceeded \(total limit\)|openrouter\.ai/i.test(message)) {
|
|
200125
200266
|
return "openrouter";
|
|
200126
200267
|
}
|
|
200268
|
+
if (/platform\.openai\.com/i.test(message)) return "openai";
|
|
200127
200269
|
return null;
|
|
200128
200270
|
}
|
|
200129
200271
|
var OPENROUTER_PRIVACY_URL = "https://openrouter.ai/settings/privacy";
|
|
@@ -200221,12 +200363,38 @@ function formatByokSetupSummary(input) {
|
|
|
200221
200363
|
].join("\n");
|
|
200222
200364
|
}
|
|
200223
200365
|
function formatProviderModelNotFoundSummary(input) {
|
|
200224
|
-
return `The configured model is
|
|
200366
|
+
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.
|
|
200225
200367
|
|
|
200226
200368
|
\`\`\`
|
|
200227
200369
|
${input.raw}
|
|
200228
200370
|
\`\`\``;
|
|
200229
200371
|
}
|
|
200372
|
+
function formatProviderUsageLimit(input) {
|
|
200373
|
+
return [
|
|
200374
|
+
"**The provider's usage limit for this account has been reached.**",
|
|
200375
|
+
"",
|
|
200376
|
+
"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.",
|
|
200377
|
+
"",
|
|
200378
|
+
`[Model settings \u2192](${getApiUrl()}/console/${input.owner}/${input.name}) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`,
|
|
200379
|
+
"",
|
|
200380
|
+
`\`\`\`
|
|
200381
|
+
${input.raw}
|
|
200382
|
+
\`\`\``
|
|
200383
|
+
].join("\n");
|
|
200384
|
+
}
|
|
200385
|
+
function formatProviderErrorBody(input) {
|
|
200386
|
+
return [
|
|
200387
|
+
"**The model provider returned an error, so this run stopped.**",
|
|
200388
|
+
"",
|
|
200389
|
+
"The error is below. If it names your key, plan or balance, fix that with the provider or pick another model before re-triggering.",
|
|
200390
|
+
"",
|
|
200391
|
+
`[Model settings \u2192](${getApiUrl()}/console/${input.owner}/${input.name}) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`,
|
|
200392
|
+
"",
|
|
200393
|
+
`\`\`\`
|
|
200394
|
+
${input.raw}
|
|
200395
|
+
\`\`\``
|
|
200396
|
+
].join("\n");
|
|
200397
|
+
}
|
|
200230
200398
|
function renderRunError(input) {
|
|
200231
200399
|
const billingError = input.routerActive && isRouterKeylimitExhaustedError(input.errorMessage) ? new BillingError(input.errorMessage, { code: "router_keylimit_exhausted" }) : null;
|
|
200232
200400
|
if (billingError) {
|
|
@@ -200242,7 +200410,7 @@ function renderRunError(input) {
|
|
|
200242
200410
|
if (input.errorMessage.includes(ROUTER_UNFUNDED_MARKER)) {
|
|
200243
200411
|
return { summary: input.errorMessage, comment: input.errorMessage };
|
|
200244
200412
|
}
|
|
200245
|
-
if (input.errorMessage.includes(CREDENTIAL_REJECTED_MARKER)) {
|
|
200413
|
+
if (input.errorMessage.includes(CREDENTIAL_REJECTED_MARKER) || input.errorMessage.includes(CREDENTIAL_POOL_REFUSED_MARKER)) {
|
|
200246
200414
|
return { summary: input.errorMessage, comment: input.errorMessage };
|
|
200247
200415
|
}
|
|
200248
200416
|
const isHang = input.errorMessage.startsWith("activity timeout") || input.errorMessage.startsWith("agent still pending");
|
|
@@ -200265,6 +200433,16 @@ ${body}`, comment: body };
|
|
|
200265
200433
|
});
|
|
200266
200434
|
return { summary: `### \u274C Pullfrog failed
|
|
200267
200435
|
|
|
200436
|
+
${body}`, comment: body };
|
|
200437
|
+
}
|
|
200438
|
+
if (isProviderUsageLimit(input.errorMessage)) {
|
|
200439
|
+
const body = formatProviderUsageLimit({
|
|
200440
|
+
owner: input.repo.owner,
|
|
200441
|
+
name: input.repo.name,
|
|
200442
|
+
raw: input.errorMessage
|
|
200443
|
+
});
|
|
200444
|
+
return { summary: `### \u274C Pullfrog failed
|
|
200445
|
+
|
|
200268
200446
|
${body}`, comment: body };
|
|
200269
200447
|
}
|
|
200270
200448
|
if (isProviderMissingCredential(input.errorMessage)) {
|
|
@@ -200359,6 +200537,16 @@ ${hangBody}`,
|
|
|
200359
200537
|
comment: explained ? hangBody : formatMinimalFailureComment(input.repo)
|
|
200360
200538
|
};
|
|
200361
200539
|
}
|
|
200540
|
+
if (input.errorMessage.startsWith("provider error: ")) {
|
|
200541
|
+
const body = formatProviderErrorBody({
|
|
200542
|
+
owner: input.repo.owner,
|
|
200543
|
+
name: input.repo.name,
|
|
200544
|
+
raw: input.errorMessage
|
|
200545
|
+
});
|
|
200546
|
+
return { summary: `### \u274C Pullfrog failed
|
|
200547
|
+
|
|
200548
|
+
${body}`, comment: body };
|
|
200549
|
+
}
|
|
200362
200550
|
const genericBody = formatGenericFailure(input.errorMessage);
|
|
200363
200551
|
return {
|
|
200364
200552
|
summary: `### \u274C Pullfrog failed
|
|
@@ -201169,7 +201357,7 @@ async function main() {
|
|
|
201169
201357
|
const repoDir = payload.cwd;
|
|
201170
201358
|
const preIntrospectionDirty = repoDir ? await dirtyTrackedPaths({ cwd: repoDir }) : null;
|
|
201171
201359
|
const opencodeCliPath = await agents.opencode.install();
|
|
201172
|
-
captureBaselineModels(opencodeCliPath);
|
|
201360
|
+
captureBaselineModels({ cliPath: opencodeCliPath, tmpdir: tmpdir4 });
|
|
201173
201361
|
const injected = [];
|
|
201174
201362
|
if (runContext.dbSecrets) {
|
|
201175
201363
|
for (const [key, value2] of Object.entries(runContext.dbSecrets)) {
|
|
@@ -201186,7 +201374,7 @@ async function main() {
|
|
|
201186
201374
|
await initializeCredentialPool({ ctx: runContext, slug: payload.model, injected });
|
|
201187
201375
|
installCodexAuth();
|
|
201188
201376
|
installXaiAuth();
|
|
201189
|
-
captureAuthorizedModels(opencodeCliPath);
|
|
201377
|
+
captureAuthorizedModels({ cliPath: opencodeCliPath, tmpdir: tmpdir4 });
|
|
201190
201378
|
if (preIntrospectionDirty && repoDir) {
|
|
201191
201379
|
await restoreDirtiedSince({
|
|
201192
201380
|
before: preIntrospectionDirty,
|
|
@@ -201524,8 +201712,8 @@ ${instructions.user}` : null,
|
|
|
201524
201712
|
log2.info(instructions.full);
|
|
201525
201713
|
});
|
|
201526
201714
|
if (agentId === "opencode") {
|
|
201527
|
-
const pluginDir =
|
|
201528
|
-
const hasPlugins =
|
|
201715
|
+
const pluginDir = join29(process.cwd(), ".opencode", "plugin");
|
|
201716
|
+
const hasPlugins = existsSync13(pluginDir) && readdirSync2(pluginDir).some((f) => /\.[jt]sx?$/.test(f));
|
|
201529
201717
|
if (hasPlugins && toolState.dependencyInstallation?.promise) {
|
|
201530
201718
|
log2.info(
|
|
201531
201719
|
"\xBB .opencode/plugin/ detected \u2014 awaiting dependency installation before agent start"
|
|
@@ -202134,7 +202322,7 @@ async function run2() {
|
|
|
202134
202322
|
// commands/_prEvents.ts
|
|
202135
202323
|
import { mkdirSync as mkdirSync11, readFileSync as readFileSync10, writeFileSync as writeFileSync16 } from "node:fs";
|
|
202136
202324
|
import { homedir as homedir3 } from "node:os";
|
|
202137
|
-
import { join as
|
|
202325
|
+
import { join as join30 } from "node:path";
|
|
202138
202326
|
var REQUEST_TIMEOUT_MS = 35e3;
|
|
202139
202327
|
var SERVER_POLL_WINDOW_MS = 2e4;
|
|
202140
202328
|
var PrEventsAuthError = class extends Error {
|
|
@@ -202183,11 +202371,11 @@ async function pollPrEvents(ctx) {
|
|
|
202183
202371
|
}
|
|
202184
202372
|
}
|
|
202185
202373
|
function stateDir() {
|
|
202186
|
-
return process.env.PULLFROG_STATE_DIR ||
|
|
202374
|
+
return process.env.PULLFROG_STATE_DIR || join30(homedir3(), ".pullfrog");
|
|
202187
202375
|
}
|
|
202188
202376
|
function cursorPath(target) {
|
|
202189
202377
|
const key = [target.owner, target.repo, String(target.pr)].map(encodeURIComponent).join("__");
|
|
202190
|
-
return
|
|
202378
|
+
return join30(stateDir(), "watch", `${key}.cursor`);
|
|
202191
202379
|
}
|
|
202192
202380
|
function readCursor(target) {
|
|
202193
202381
|
try {
|
|
@@ -202200,7 +202388,7 @@ function readCursor(target) {
|
|
|
202200
202388
|
function writeCursor(target, cursor3) {
|
|
202201
202389
|
try {
|
|
202202
202390
|
const path4 = cursorPath(target);
|
|
202203
|
-
mkdirSync11(
|
|
202391
|
+
mkdirSync11(join30(stateDir(), "watch"), { recursive: true });
|
|
202204
202392
|
writeFileSync16(path4, cursor3, "utf-8");
|
|
202205
202393
|
} catch {
|
|
202206
202394
|
}
|
|
@@ -202255,7 +202443,7 @@ function resolveTarget2(repo) {
|
|
|
202255
202443
|
return { owner: match3[1], repo: match3[2] };
|
|
202256
202444
|
}
|
|
202257
202445
|
function cliVersion() {
|
|
202258
|
-
const parts = /^(\d+)\.(\d+)\.(\d+)$/.exec("0.1.
|
|
202446
|
+
const parts = /^(\d+)\.(\d+)\.(\d+)$/.exec("0.1.97");
|
|
202259
202447
|
if (!parts) return "0.0.0";
|
|
202260
202448
|
return `${Number(parts[1])}.${Number(parts[2])}.${Number(parts[3])}`;
|
|
202261
202449
|
}
|
|
@@ -202422,7 +202610,7 @@ async function runCli8(input) {
|
|
|
202422
202610
|
}
|
|
202423
202611
|
|
|
202424
202612
|
// cli.ts
|
|
202425
|
-
var VERSION10 = "0.1.
|
|
202613
|
+
var VERSION10 = "0.1.97";
|
|
202426
202614
|
var bin = basename2(process.argv[1] || "");
|
|
202427
202615
|
var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
|
|
202428
202616
|
var rawArgs = process.argv.slice(2);
|