pullfrog 0.1.96 → 0.1.98
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 +656 -452
- package/dist/index.js +740 -545
- package/dist/internal.js +82 -29
- 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 +2 -2
package/dist/index.js
CHANGED
|
@@ -98088,14 +98088,14 @@ var require_turndown_cjs = __commonJS({
|
|
|
98088
98088
|
} else if (node2.nodeType === 1) {
|
|
98089
98089
|
replacement = replacementForNode.call(self2, node2);
|
|
98090
98090
|
}
|
|
98091
|
-
return
|
|
98091
|
+
return join29(output, replacement);
|
|
98092
98092
|
}, "");
|
|
98093
98093
|
}
|
|
98094
98094
|
function postProcess(output) {
|
|
98095
98095
|
var self2 = this;
|
|
98096
98096
|
this.rules.forEach(function(rule) {
|
|
98097
98097
|
if (typeof rule.append === "function") {
|
|
98098
|
-
output =
|
|
98098
|
+
output = join29(output, rule.append(self2.options));
|
|
98099
98099
|
}
|
|
98100
98100
|
});
|
|
98101
98101
|
return output.replace(/^[\t\r\n]+/, "").replace(/[\t\r\n\s]+$/, "");
|
|
@@ -98107,7 +98107,7 @@ var require_turndown_cjs = __commonJS({
|
|
|
98107
98107
|
if (whitespace.leading || whitespace.trailing) content = content.trim();
|
|
98108
98108
|
return whitespace.leading + rule.replacement(content, node2, this.options) + whitespace.trailing;
|
|
98109
98109
|
}
|
|
98110
|
-
function
|
|
98110
|
+
function join29(output, replacement) {
|
|
98111
98111
|
var s1 = trimTrailingNewlines(output);
|
|
98112
98112
|
var s2 = trimLeadingNewlines(replacement);
|
|
98113
98113
|
var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
|
|
@@ -102898,9 +102898,9 @@ var init_file_type = __esm({
|
|
|
102898
102898
|
});
|
|
102899
102899
|
|
|
102900
102900
|
// main.ts
|
|
102901
|
-
import { existsSync as
|
|
102901
|
+
import { existsSync as existsSync11, readdirSync as readdirSync2 } from "node:fs";
|
|
102902
102902
|
import { readFile as readFile5 } from "node:fs/promises";
|
|
102903
|
-
import { join as
|
|
102903
|
+
import { join as join28 } from "node:path";
|
|
102904
102904
|
|
|
102905
102905
|
// node_modules/.pnpm/package-manager-detector@1.8.0/node_modules/package-manager-detector/dist/commands.mjs
|
|
102906
102906
|
function splitRunArgs(args2, valueFlags = []) {
|
|
@@ -103333,8 +103333,9 @@ var providers = {
|
|
|
103333
103333
|
},
|
|
103334
103334
|
"claude-haiku": {
|
|
103335
103335
|
displayName: "Claude Haiku",
|
|
103336
|
-
resolve: "anthropic/claude-haiku-
|
|
103337
|
-
|
|
103336
|
+
resolve: "anthropic/claude-haiku-5-5",
|
|
103337
|
+
effort: ["low", "medium", "high", "xhigh", "max"],
|
|
103338
|
+
openRouterResolve: "openrouter/anthropic/claude-haiku-5.5"
|
|
103338
103339
|
}
|
|
103339
103340
|
}
|
|
103340
103341
|
}),
|
|
@@ -103752,8 +103753,9 @@ var providers = {
|
|
|
103752
103753
|
},
|
|
103753
103754
|
"claude-haiku": {
|
|
103754
103755
|
displayName: "Claude Haiku",
|
|
103755
|
-
resolve: "opencode/claude-haiku-
|
|
103756
|
-
|
|
103756
|
+
resolve: "opencode/claude-haiku-5-5",
|
|
103757
|
+
effort: ["low", "medium", "high", "xhigh", "max"],
|
|
103758
|
+
openRouterResolve: "openrouter/anthropic/claude-haiku-5.5"
|
|
103757
103759
|
},
|
|
103758
103760
|
"gpt-sol": {
|
|
103759
103761
|
displayName: "GPT Sol",
|
|
@@ -103909,6 +103911,14 @@ var providers = {
|
|
|
103909
103911
|
openRouterEffort: [],
|
|
103910
103912
|
openRouterResolve: "openrouter/qwen/qwen3.8-flash"
|
|
103911
103913
|
},
|
|
103914
|
+
// Zen lists Mistral Large 4 at half its list price for launch, level with
|
|
103915
|
+
// the routers.
|
|
103916
|
+
"mistral-large": {
|
|
103917
|
+
displayName: "Mistral Large",
|
|
103918
|
+
resolve: "opencode/mistral-large-4",
|
|
103919
|
+
effort: ["none", "high"],
|
|
103920
|
+
openRouterResolve: "openrouter/mistralai/mistral-large-4-0"
|
|
103921
|
+
},
|
|
103912
103922
|
"gpt-5-nano": {
|
|
103913
103923
|
displayName: "GPT Nano",
|
|
103914
103924
|
resolve: "opencode/gpt-5.4-nano",
|
|
@@ -103957,12 +103967,12 @@ var providers = {
|
|
|
103957
103967
|
fallback: "opencode/big-pickle"
|
|
103958
103968
|
},
|
|
103959
103969
|
// Zen's FREE stealth preview (listed 2026-09-23), the third after
|
|
103960
|
-
// big-pickle and union-alpha.
|
|
103961
|
-
//
|
|
103962
|
-
//
|
|
103970
|
+
// big-pickle and union-alpha. Zen's docs: zero retention, no training.
|
|
103971
|
+
// unlike the first two it publishes an effort ladder, so the gate
|
|
103972
|
+
// mirrors one here.
|
|
103963
103973
|
"space-bunny": {
|
|
103964
103974
|
displayName: "Space Bunny",
|
|
103965
|
-
description: "Stealth preview
|
|
103975
|
+
description: "Stealth preview, free for a limited time",
|
|
103966
103976
|
resolve: "opencode/space-bunny-free",
|
|
103967
103977
|
effort: ["low", "medium", "high", "xhigh", "max"],
|
|
103968
103978
|
// free to run, still gated on the provider's own OPENCODE_API_KEY —
|
|
@@ -103980,17 +103990,46 @@ var providers = {
|
|
|
103980
103990
|
// see the big-pickle note above (#1077).
|
|
103981
103991
|
isFree: true
|
|
103982
103992
|
},
|
|
103983
|
-
// Zen's FREE stealth preview (listed 2026-10-01), the fifth promo. Zen
|
|
103984
|
-
//
|
|
103993
|
+
// Zen's FREE stealth preview (listed 2026-10-01), the fifth promo. Zen's
|
|
103994
|
+
// docs: data may be used to improve the model during the free period.
|
|
103985
103995
|
"fledge-alpha": {
|
|
103986
103996
|
displayName: "Fledge Alpha",
|
|
103987
|
-
description: "Stealth preview
|
|
103997
|
+
description: "Stealth preview; data may be used to improve it",
|
|
103988
103998
|
resolve: "opencode/fledge-alpha-free",
|
|
103989
103999
|
effort: ["low", "high", "max"],
|
|
103990
104000
|
// free to run, still gated on the provider's own OPENCODE_API_KEY —
|
|
103991
104001
|
// see the big-pickle note above (#1077).
|
|
104002
|
+
isFree: true,
|
|
104003
|
+
// Zen dropped the id from `/v1/models` and its docs on 2026-10-08 and
|
|
104004
|
+
// answers a 500 for it; stored picks land on the other free stealth
|
|
104005
|
+
// model, as `union-alpha` does.
|
|
104006
|
+
fallback: "opencode/big-pickle"
|
|
104007
|
+
},
|
|
104008
|
+
// Zen's FREE preview (listed 2026-10-06), the sixth promo, on Fledge
|
|
104009
|
+
// Alpha's terms.
|
|
104010
|
+
exo: {
|
|
104011
|
+
displayName: "Exo",
|
|
104012
|
+
description: "Preview; data may be used to improve it",
|
|
104013
|
+
resolve: "opencode/exo-free",
|
|
104014
|
+
// Zen publishes a single rung.
|
|
104015
|
+
effort: ["high"],
|
|
104016
|
+
// free to run, still gated on the provider's own OPENCODE_API_KEY —
|
|
104017
|
+
// see the big-pickle note above (#1077).
|
|
103992
104018
|
isFree: true
|
|
103993
104019
|
},
|
|
104020
|
+
// Zen's FREE StepFun preview (listed 2026-10-08), the seventh promo. Zen's
|
|
104021
|
+
// docs: zero retention, no training. Go lists the same free id; one key
|
|
104022
|
+
// reaches both. hidden until a published `pullfrog` resolves it.
|
|
104023
|
+
"step-5-preview": {
|
|
104024
|
+
displayName: "Step 5 Preview",
|
|
104025
|
+
description: "StepFun preview, free for a limited time",
|
|
104026
|
+
resolve: "opencode/step-5-preview-free",
|
|
104027
|
+
effort: ["low", "medium", "high"],
|
|
104028
|
+
// free to run, still gated on the provider's own OPENCODE_API_KEY —
|
|
104029
|
+
// see the big-pickle note above (#1077).
|
|
104030
|
+
isFree: true,
|
|
104031
|
+
hidden: true
|
|
104032
|
+
},
|
|
103994
104033
|
// Zen's live free MiMo, and the second free row in a menu that big-pickle
|
|
103995
104034
|
// was alone in since `mimo-v2-pro-free` lost its model.
|
|
103996
104035
|
mimo: {
|
|
@@ -104275,6 +104314,9 @@ var providers = {
|
|
|
104275
104314
|
"claude-haiku": {
|
|
104276
104315
|
displayName: "Claude Haiku",
|
|
104277
104316
|
resolve: "openrouter/~anthropic/claude-haiku-latest",
|
|
104317
|
+
// the rolling alias moved to Haiku 5.5 on 2026-10-07, which publishes
|
|
104318
|
+
// the ladder Haiku 4.5 did not.
|
|
104319
|
+
effort: ["low", "medium", "high", "xhigh", "max"],
|
|
104278
104320
|
openRouterResolve: "openrouter/~anthropic/claude-haiku-latest"
|
|
104279
104321
|
},
|
|
104280
104322
|
// pinned to the explicit Sol/Terra/Luna tiers (not the ~openai/gpt-latest
|
|
@@ -104446,6 +104488,14 @@ var providers = {
|
|
|
104446
104488
|
resolve: "openrouter/meta/muse-spark-1.3-contributor",
|
|
104447
104489
|
effort: ["minimal", "low", "medium", "high", "xhigh", "max"],
|
|
104448
104490
|
openRouterResolve: "openrouter/meta/muse-spark-1.3-contributor"
|
|
104491
|
+
},
|
|
104492
|
+
// Mistral's flagship (public preview, 2026-10-06), on the routers and
|
|
104493
|
+
// Zen only: the catalog carries no direct Mistral provider.
|
|
104494
|
+
"mistral-large": {
|
|
104495
|
+
displayName: "Mistral Large",
|
|
104496
|
+
resolve: "openrouter/mistralai/mistral-large-4-0",
|
|
104497
|
+
effort: ["none", "high"],
|
|
104498
|
+
openRouterResolve: "openrouter/mistralai/mistral-large-4-0"
|
|
104449
104499
|
}
|
|
104450
104500
|
}
|
|
104451
104501
|
}),
|
|
@@ -104476,7 +104526,8 @@ var providers = {
|
|
|
104476
104526
|
},
|
|
104477
104527
|
"claude-haiku": {
|
|
104478
104528
|
displayName: "Claude Haiku",
|
|
104479
|
-
resolve: "vercel/anthropic/claude-haiku-
|
|
104529
|
+
resolve: "vercel/anthropic/claude-haiku-5.5",
|
|
104530
|
+
effort: ["none", "low", "medium", "high", "xhigh", "max"]
|
|
104480
104531
|
},
|
|
104481
104532
|
"gpt-sol": {
|
|
104482
104533
|
displayName: "GPT Sol",
|
|
@@ -104537,6 +104588,11 @@ var providers = {
|
|
|
104537
104588
|
displayName: "Muse Spark",
|
|
104538
104589
|
resolve: "vercel/meta/muse-spark-1.3",
|
|
104539
104590
|
effort: ["minimal", "low", "medium", "high", "xhigh"]
|
|
104591
|
+
},
|
|
104592
|
+
"mistral-large": {
|
|
104593
|
+
displayName: "Mistral Large",
|
|
104594
|
+
resolve: "vercel/mistral/mistral-large-4",
|
|
104595
|
+
effort: ["none", "low", "medium", "high"]
|
|
104540
104596
|
}
|
|
104541
104597
|
}
|
|
104542
104598
|
})
|
|
@@ -105417,7 +105473,13 @@ var PROVIDER_ERROR_PATTERNS = [
|
|
|
105417
105473
|
// …/settings/credits`) matched neither this nor `requires more credits`, so
|
|
105418
105474
|
// the one run in that episode that was a genuinely empty wallet rendered with
|
|
105419
105475
|
// no CTA at all while runs 1-6 rendered correctly.
|
|
105420
|
-
|
|
105476
|
+
// `account funds` is Zen's (#1474).
|
|
105477
|
+
{
|
|
105478
|
+
regex: /Insufficient (?:balance|credits|account funds)/i,
|
|
105479
|
+
label: PROVIDER_BILLING_EXHAUSTED_LABEL
|
|
105480
|
+
},
|
|
105481
|
+
// OpenAI's `You have no credits remaining` (#1474).
|
|
105482
|
+
{ regex: /no credits remaining/i, label: PROVIDER_BILLING_EXHAUSTED_LABEL },
|
|
105421
105483
|
{ regex: /credit balance is too low/i, label: PROVIDER_BILLING_EXHAUSTED_LABEL },
|
|
105422
105484
|
// "spending cap" (Gemini) and "spending limit" (xAI, #1076) are the same
|
|
105423
105485
|
// condition in two house styles — a configured ceiling was reached.
|
|
@@ -105538,6 +105600,9 @@ function isRouterKeylimitExhaustedError(text) {
|
|
|
105538
105600
|
function isProviderBillingExhausted(text) {
|
|
105539
105601
|
return findProviderErrorMatch(text)?.label === PROVIDER_BILLING_EXHAUSTED_LABEL;
|
|
105540
105602
|
}
|
|
105603
|
+
function isProviderUsageLimit(text) {
|
|
105604
|
+
return findProviderErrorMatch(text)?.label === PROVIDER_USAGE_LIMIT_LABEL;
|
|
105605
|
+
}
|
|
105541
105606
|
function isProviderNoRoutableEndpoints(text) {
|
|
105542
105607
|
return findProviderErrorMatch(text)?.label === PROVIDER_NO_ENDPOINTS_LABEL;
|
|
105543
105608
|
}
|
|
@@ -105609,7 +105674,7 @@ var import_semver = __toESM(require_semver2(), 1);
|
|
|
105609
105674
|
// package.json
|
|
105610
105675
|
var package_default = {
|
|
105611
105676
|
name: "pullfrog",
|
|
105612
|
-
version: "0.1.
|
|
105677
|
+
version: "0.1.98",
|
|
105613
105678
|
cliContractVersion: "1",
|
|
105614
105679
|
type: "module",
|
|
105615
105680
|
bin: {
|
|
@@ -105637,7 +105702,7 @@ var package_default = {
|
|
|
105637
105702
|
},
|
|
105638
105703
|
devDependencies: {
|
|
105639
105704
|
"@actions/core": "^1.11.1",
|
|
105640
|
-
"@anthropic-ai/claude-code": "2.1.
|
|
105705
|
+
"@anthropic-ai/claude-code": "2.1.293",
|
|
105641
105706
|
"@ark/fs": "0.56.0",
|
|
105642
105707
|
"@ark/util": "0.56.0",
|
|
105643
105708
|
"@clack/prompts": "^1.2.0",
|
|
@@ -105906,6 +105971,7 @@ ${this.buffer}`;
|
|
|
105906
105971
|
};
|
|
105907
105972
|
async function spawn(options) {
|
|
105908
105973
|
const activityTimeoutMs = options.activityTimeout ?? DEFAULT_ACTIVITY_TIMEOUT_MS;
|
|
105974
|
+
options.signal?.throwIfAborted();
|
|
105909
105975
|
installSignalHandler();
|
|
105910
105976
|
const startTime = performance3.now();
|
|
105911
105977
|
const retain = options.retain ?? "tail";
|
|
@@ -105941,17 +106007,18 @@ async function spawn(options) {
|
|
|
105941
106007
|
let isActivityTimedOut = false;
|
|
105942
106008
|
let lastActivityTime = performance3.now();
|
|
105943
106009
|
let killedAtIdleMs;
|
|
106010
|
+
const terminate = () => {
|
|
106011
|
+
if (sigkillEscalatorId) return;
|
|
106012
|
+
killSelf("SIGTERM");
|
|
106013
|
+
sigkillEscalatorId = setTimeout(() => killSelf("SIGKILL"), 5e3);
|
|
106014
|
+
};
|
|
105944
106015
|
if (options.timeout) {
|
|
105945
106016
|
timeoutId = setTimeout(() => {
|
|
105946
106017
|
isTimedOut = true;
|
|
105947
|
-
|
|
105948
|
-
sigkillEscalatorId = setTimeout(() => {
|
|
105949
|
-
if (!child.killed) {
|
|
105950
|
-
killSelf("SIGKILL");
|
|
105951
|
-
}
|
|
105952
|
-
}, 5e3);
|
|
106018
|
+
terminate();
|
|
105953
106019
|
}, options.timeout);
|
|
105954
106020
|
}
|
|
106021
|
+
options.signal?.addEventListener("abort", terminate, { once: true });
|
|
105955
106022
|
if (activityTimeoutMs > 0) {
|
|
105956
106023
|
log.debug(
|
|
105957
106024
|
`spawn activity timer: pid=${child.pid} cmd=${options.cmd} timeout=${activityTimeoutMs}ms`
|
|
@@ -106016,6 +106083,11 @@ async function spawn(options) {
|
|
|
106016
106083
|
if (timeoutId) clearTimeout(timeoutId);
|
|
106017
106084
|
if (sigkillEscalatorId) clearTimeout(sigkillEscalatorId);
|
|
106018
106085
|
if (activityCheckIntervalId) clearInterval(activityCheckIntervalId);
|
|
106086
|
+
options.signal?.removeEventListener("abort", terminate);
|
|
106087
|
+
if (options.signal?.aborted) {
|
|
106088
|
+
reject(options.signal.reason);
|
|
106089
|
+
return;
|
|
106090
|
+
}
|
|
106019
106091
|
if (isTimedOut) {
|
|
106020
106092
|
reject(
|
|
106021
106093
|
new SpawnTimeoutError(`process timed out after ${options.timeout}ms`, SPAWN_TIMEOUT_CODE)
|
|
@@ -106054,6 +106126,7 @@ ${killMsg}` : killMsg;
|
|
|
106054
106126
|
if (timeoutId) clearTimeout(timeoutId);
|
|
106055
106127
|
if (sigkillEscalatorId) clearTimeout(sigkillEscalatorId);
|
|
106056
106128
|
if (activityCheckIntervalId) clearInterval(activityCheckIntervalId);
|
|
106129
|
+
options.signal?.removeEventListener("abort", terminate);
|
|
106057
106130
|
const errMsg = `[spawn] ${options.cmd}: ${error49.message}`;
|
|
106058
106131
|
console.error(errMsg);
|
|
106059
106132
|
const existingStderr = stderrBuffer?.toString() ?? "";
|
|
@@ -106295,17 +106368,16 @@ Report findings clearly with file:line references and quoted evidence where poss
|
|
|
106295
106368
|
// modes.ts
|
|
106296
106369
|
var PR_SUMMARY_FORMAT = `### Default format
|
|
106297
106370
|
|
|
106298
|
-
The body has at most
|
|
106371
|
+
The body has at most two parts, in this order:
|
|
106299
106372
|
|
|
106300
106373
|
1. **Reviewed changes preamble** \u2014 a bolded \`**Reviewed changes**\` lead-in with one sentence on what was reviewed in this run (for \`IncrementalReview\`: what changed since the prior pullfrog review), then a bullet list of the substantive changes \u2014 short bolded title, one sentence each. A reviewer should understand the full reviewed scope from this list alone. Close the preamble with the metadata comment below.
|
|
106301
106374
|
2. **Cross-cutting issue sections** (zero or more) \u2014 one \`### {emoji} {what's wrong, not what to do}\` heading per concern.
|
|
106302
|
-
3. **\`### \u2139\uFE0F Nitpicks\`** at the very bottom, if any \u2014 a flat bullet list, no technical-details block. Only for what has no runtime effect: a stale or wrong comment or doc, a misleading name, a typo, dead code, a missing test. Never formatting or whitespace.
|
|
106303
106375
|
|
|
106304
106376
|
**Inline vs. body.** Concerns that anchor to a specific line go inline (the \`comments\` parameter), even when their implications are broad. Body \`### \` sections are reserved for concerns that have **no line to anchor to** \u2014 *absence* (something the diff should have done but didn't), *sequencing* (rollout / deletion / migration order), *design decisions only the human can make*, or *scope questions the diff raises but doesn't address*. With no non-anchorable concerns, the body is just the preamble + metadata.
|
|
106305
106377
|
|
|
106306
106378
|
**Severity emoji** on every \`### \` heading, and nowhere else: \u{1F6A8} critical (blocks merge \u2014 data loss, security, broken core flow) \xB7 \u26A0\uFE0F important (must address before merging) \xB7 \u2139\uFE0F informational (mergeable as-is).
|
|
106307
106379
|
|
|
106308
|
-
**Behavior decides placement, not fix size.** A finding that changes what the code does \u2014 a wrong value or message, a crash, a race, a leak, a missed cleanup, a slow path \u2014 gets its own inline comment (or a \`### \` section if no line anchors it) at its real severity, however small the fix.
|
|
106380
|
+
**Behavior decides placement, not fix size.** A finding that changes what the code does \u2014 a wrong value or message, a crash, a race, a leak, a missed cleanup, a slow path \u2014 gets its own inline comment (or a \`### \` section if no line anchors it) at its real severity, however small the fix.
|
|
106309
106381
|
|
|
106310
106382
|
**Blank line between every block-level element.** GitHub's markdown parser requires one before and after HTML tags (\`<details>\`, \`<summary>\`, \`<sub>\`, \`<br/>\`) \u2014 without it GitHub treats what follows as a continuation of the HTML block and renders your markdown as literal text. This is a parser quirk, not a style preference, and it permanently breaks the posted review.
|
|
106311
106383
|
|
|
@@ -106454,8 +106526,6 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
106454
106526
|
|
|
106455
106527
|
3. **triage**: orient yourself on the PR \u2014 identify *what kind of thing this is* (domain it touches, seams it crosses, external contracts it depends on, user-facing surfaces it changes). pull as much context as you need to render a confident, well-grounded review: read related files, grep for callers of changed symbols, check tests that exercise the touched paths, fetch related GitHub state. **you are the synthesizer** \u2014 never delegate understanding to subagents.
|
|
106456
106528
|
|
|
106457
|
-
when the diff adds or changes a test, check that it can actually fail: a test that would still pass with the bug present is theatre, not coverage. the usual tell is a loose assertion standing where an exact one belongs \u2014 \`>=\` or a truthiness check over an expected value, or a snapshot that absorbs whatever it is handed. read the assertion against the behavior it claims to pin, not against whether it currently passes.
|
|
106458
|
-
|
|
106459
106529
|
skip the deeper pass and submit a \`No new issues found.\` review per step 7 only when the diff has **no behavioral surface at all** \u2014 doc typos, whitespace/formatting, lockfile or generated-code regeneration, a mechanical rename whose only effect is import-path updates. line count is not the signal: a one-line change to auth, money, SQL, a comparison operator, a redirect, or a config default is not trivial.
|
|
106460
106530
|
|
|
106461
106531
|
4. **specialist decision**: after reading the complete diff, name the questions you still cannot answer confidently yourself, and dispatch one \`${REVIEWER_AGENT_NAME}\` specialist per question. a question qualifies only when a specialist could return evidence that **changes your disposition** on the PR \u2014 generic requests for another look, extra confidence, or polish do not. most reviews need zero or one; some need several.
|
|
@@ -106479,8 +106549,6 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
106479
106549
|
|
|
106480
106550
|
6. **aggregate & draft**: when specialist results land, merge findings; de-dup overlaps (two specialists catching the same issue = higher-confidence signal); trace each finding yourself before accepting it. drop praise, style preferences, speculative/unverified claims, findings about pre-existing code unrelated to the PR (heuristic: if the finding's root cause lives in lines this PR added or modified, it's in scope; otherwise drop unless the PR plausibly introduced or amplified the regression), and anything not actionable. also drop **bloat-shaped findings** \u2014 proposed fixes that would add defensive checks for cases that can't happen, abstractions used once, comments restating obvious code, tests asserting tautologies, or "just-in-case" guards. subagents are fallible and bias toward recommending changes; the bar for an actionable inline comment is sound + correct + elegant. recommending a change that improves only one of the three (or worse, degrades elegance to nominally improve correctness) makes the codebase worse, not better.
|
|
106481
106551
|
|
|
106482
|
-
**Hunt for non-anchored concerns before drafting.** After collecting your anchored findings, deliberately scan for concerns that have no specific line to point at \u2014 typically: deletion / cleanup plans for code the diff replaces or shadows; rollout sequencing (what happens to in-flight state during deploy / revert?); scope questions that only the human can answer (e.g. is the legacy path going away or is this a long-term dual track?); architectural risks the diff opens up that aren't a single-line bug.
|
|
106483
|
-
|
|
106484
106552
|
for surviving findings, draft inline comments with NEW line numbers from the diff \u2014 attach a \`<details>Technical details</details>\` block to any inline comment whose fix is non-trivial or has cross-file implications (see Inline technical details in the format below). every comment must be actionable, 2-3 sentences max in the visible part, one per root cause \u2014 name its other sites in that comment rather than repeating it. use GitHub permalink format for code references. for impact-analysis findings (stale references after rename/remove), report them in the review body ordered by severity (runtime breakage > incorrect docs > stale comments) rather than as inline comments unless they're anchored to a specific line.
|
|
106485
106553
|
|
|
106486
106554
|
7. **submit**: ALWAYS submit exactly one review via \`${t("create_pull_request_review")}\`. Do NOT call \`report_progress\` \u2014 the review is the final record and the progress comment will be cleaned up automatically.
|
|
@@ -106502,7 +106570,7 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
106502
106570
|
\`approved: false\`. Body opens with \`> [!CAUTION]\\n> This PR introduces ...\`, followed by the PR summary. Include all inline comments via \`comments\`.
|
|
106503
106571
|
- **must-address non-critical findings** (real consequences if shipped \u2014 incorrect behavior in non-critical paths, missing validation on user input, regressions the author should fix before merge):
|
|
106504
106572
|
\`approved: false\`. Body opens with \`> [!IMPORTANT]\\n> ...\`, followed by the PR summary. Reserve this tier for findings with concrete fallout \u2014 do NOT use \`[!IMPORTANT]\` for nits, style preferences, or "consider also" suggestions. Include all inline comments via \`comments\`.
|
|
106505
|
-
- **minor suggestions only** (low-stakes findings
|
|
106573
|
+
- **minor suggestions only** (low-stakes findings):
|
|
106506
106574
|
\`approved: false\`. Body opens with \`> \u2139\uFE0F No critical issues \u2014 minor suggestions inline.\\n\\n\` followed by the PR summary. Include all inline comments via \`comments\`. Vary the wording after the emoji to fit the review (e.g. "Minor suggestions only.", "Two rough edges worth a look."), but always keep the \u2139\uFE0F prefix and keep it short.
|
|
106507
106575
|
- **informational observations** (mergeable as-is, nothing actionable \u2014 e.g. prior feedback addressed cleanly, calling out something noteworthy without recommending a change):
|
|
106508
106576
|
\`approved: true\`. Body opens with \`> \u2705 No new issues found.\\n\\n\` followed by the PR summary. Do NOT include inline \`comments\` \u2014 the \u2705 signals "no action needed", which contradicts an actionable anchor; if a point is concrete enough to anchor to a line, downgrade the whole review to "minor suggestions only" (\`approved: false\`) instead.
|
|
@@ -106545,8 +106613,6 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
106545
106613
|
|
|
106546
106614
|
5. **triage**: orient on the *incremental* changes \u2014 domain, seams, external contracts, user-facing surfaces. pull as much context as you need to render a confident review: read related files, grep for callers of changed symbols, check tests that exercise the touched paths. **you are the synthesizer.**
|
|
106547
106615
|
|
|
106548
|
-
a test added or changed in this delta must be able to fail \u2014 one that would still pass with the bug present is theatre, not coverage. the tell is a loose assertion where an exact one belongs (\`>=\` or a truthiness check over an expected value, a snapshot that absorbs whatever it is handed).
|
|
106549
|
-
|
|
106550
106616
|
skip the deeper pass and jump to step 10's non-substantive path (do NOT submit a review) only when the incremental changes have **no behavioral surface at all** \u2014 formatting, comment tweaks, import reordering, lockfile regen, a mechanical rename of import paths. line count is not the signal: a one-line change to auth, money, SQL, a comparison operator, a redirect, or a config default is not trivial.
|
|
106551
106617
|
|
|
106552
106618
|
6. **specialist decision**: after covering the incremental and full diffs, name the questions about the new changes that you still cannot answer confidently yourself, and dispatch one \`${REVIEWER_AGENT_NAME}\` specialist per question. a question qualifies only when a specialist could return evidence that **changes your disposition** on the PR \u2014 generic requests for another look, extra confidence, or polish do not. most incremental reviews need zero or one, especially thread-reply re-reviews; some need several.
|
|
@@ -106570,11 +106636,9 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
106570
106636
|
|
|
106571
106637
|
8. **aggregate, draft, self-critique**: merge findings (yours + output from every specialist you dispatched); de-dup overlaps; trace each finding yourself. drop praise, style preferences, speculative/unverified claims, findings about pre-existing code unrelated to the new commits, anything not actionable, and anything that re-states prior review feedback (heuristic: if the finding's root cause lives in lines the *new commits* added or modified, it's in scope; otherwise drop). also drop **bloat-shaped findings** \u2014 proposed fixes that would add defensive checks for cases that can't happen, abstractions used once, comments restating obvious code, tests asserting tautologies, or "just-in-case" guards. subagents are fallible and bias toward recommending changes; the bar for an actionable inline comment is sound + correct + elegant. recommending a change that improves only one of the three (or degrades elegance to nominally improve correctness) makes the codebase worse, not better. To compute "lines the new commits added or modified": if \`incrementalDiffPath\` from step 2 is present, use it directly. Otherwise, take the prior Pullfrog review's \`commit_id\` (returned alongside each entry from \`${t("list_pull_request_reviews")}\` in step 4) and run \`git diff <prior-review-sha>..HEAD\` to isolate the lines added since that review.
|
|
106572
106638
|
|
|
106573
|
-
**Hunt for non-anchored concerns before drafting.** After collecting your anchored findings, deliberately scan for concerns that have no specific line to point at \u2014 typically: deletion / cleanup plans for code the new commits replace or shadow; rollout sequencing (what happens to in-flight state during deploy / revert?); scope questions that only the human can answer (e.g. is the legacy path going away or is this a long-term dual track?); architectural risks the new commits open up that aren't a single-line bug.
|
|
106574
|
-
|
|
106575
106639
|
draft inline comments with NEW line numbers from the full PR diff \u2014 attach a \`<details>Technical details</details>\` block to any inline comment whose fix is non-trivial or has cross-file implications (see Inline technical details in the format below). every comment must be actionable, 2-3 sentences max in the visible part, one per root cause \u2014 name its other sites in that comment rather than repeating it.
|
|
106576
106640
|
|
|
106577
|
-
9. **build the review body**: use the same default format as Review mode (preamble + optional cross-cutting \`### \` sections
|
|
106641
|
+
9. **build the review body**: use the same default format as Review mode (preamble + optional cross-cutting \`### \` sections) \u2014 scoped to the **incremental delta**, not the full PR. The "Reviewed changes" bullets describe what changed since the prior pullfrog review (each bullet starts with a past-tense verb, e.g. \`- Extracted shared CLI runtime into a single module\`). Do NOT include a separate "Prior review feedback" checklist \u2014 that's tracked in the rolling PR summary snapshot for the next agent run, and surfacing it in the user-facing body is noise (changes that addressed prior feedback are already covered by the Reviewed-changes bullets). In some cases you may receive a complete diff for the whole PR instead of an incremental one; when this happens, determine what changed since Pullfrog's most recent review yourself before drafting bullets.
|
|
106578
106642
|
|
|
106579
106643
|
10. Submit \u2014 every run must end with EXACTLY ONE of \`${t("create_pull_request_review")}\` (substantive review) or \`${t("report_progress")}\` (no-review acknowledgement). do NOT call \`create_issue_comment\` for review output.
|
|
106580
106644
|
|
|
@@ -106585,7 +106649,7 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
106585
106649
|
- IF NO NEW ISSUES, NON-SUBSTANTIVE CHANGES ONLY (trivial formatting, import reordering, comment tweaks): do NOT submit a review. Instead call \`${t("report_progress")}\` with \`no_review_needed: true\` and a 1-2 sentence note explaining no review was warranted (e.g. "No new issues. Changes since last review are formatting-only."). this leaves a visible signal that the run completed.
|
|
106586
106650
|
- ELSE IF NEW CRITICAL ISSUES (blocks merge \u2014 bugs, security, data loss, broken core flows): call \`${t("create_pull_request_review")}\` with \`approved: false\`, all comments, and the review body. body opens with \`> [!CAUTION]\\n> This PR introduces ...\`, followed by the PR summary using the default format below.
|
|
106587
106651
|
- ELSE IF NEW MUST-ADDRESS NON-CRITICAL FINDINGS (real consequences if shipped \u2014 incorrect behavior, missing validation, regressions the author should fix before merge): call \`${t("create_pull_request_review")}\` with \`approved: false\`, all comments, and the review body. body opens with \`> [!IMPORTANT]\\n> ...\`, followed by the PR summary using the default format below. Do NOT use this tier for nits, style preferences, or "consider also" suggestions.
|
|
106588
|
-
- ELSE IF NEW MINOR SUGGESTIONS ONLY (low-stakes findings
|
|
106652
|
+
- ELSE IF NEW MINOR SUGGESTIONS ONLY (low-stakes findings): call \`${t("create_pull_request_review")}\` with \`approved: false\`, all comments, and the review body. body opens with \`> \u2139\uFE0F No critical issues \u2014 minor suggestions inline.\\n\\n\` (vary the wording after \u2139\uFE0F to fit the review), followed by the PR summary using the default format below.
|
|
106589
106653
|
- ELSE IF INFORMATIONAL OBSERVATIONS (mergeable as-is, but worth surfacing \u2014 e.g. prior feedback addressed cleanly, or a noteworthy positive observation): call \`${t("create_pull_request_review")}\` with \`approved: true\`, NO inline comments, and the review body. body opens with \`> \u2705 No new issues found.\\n\\n\` (or similar friendly green opener), followed by the PR summary using the default format below. If a point is concrete enough to anchor to a line, downgrade the whole review to "minor suggestions only" (\`approved: false\`) instead \u2014 the \u2705 signals "no action needed", which contradicts an actionable anchor.
|
|
106590
106654
|
- ELSE IF NO NEW ISSUES, SUBSTANTIVE CHANGES (new functionality, behavior changes, or fixes to prior review feedback): call \`${t("create_pull_request_review")}\` to create a PR review. If all previous reviews have been properly addressed and no new issues were discovered, set \`approved: true\`. body opens with \`> \u2705 No new issues found.\\n\\n\`, followed by the PR summary using the default format below.
|
|
106591
106655
|
|
|
@@ -107802,7 +107866,7 @@ import { performance as performance6 } from "node:perf_hooks";
|
|
|
107802
107866
|
|
|
107803
107867
|
// utils/codexHome.ts
|
|
107804
107868
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
107805
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync2, writeFileSync as writeFileSync4 } from "node:fs";
|
|
107869
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync5, mkdtempSync, readFileSync as readFileSync2, writeFileSync as writeFileSync4 } from "node:fs";
|
|
107806
107870
|
import { homedir as homedir2, userInfo } from "node:os";
|
|
107807
107871
|
import { join as join5 } from "node:path";
|
|
107808
107872
|
|
|
@@ -107853,205 +107917,6 @@ function stringifyCodexAuthBody(body) {
|
|
|
107853
107917
|
`;
|
|
107854
107918
|
}
|
|
107855
107919
|
|
|
107856
|
-
// utils/xaiOAuth.ts
|
|
107857
|
-
function parseXaiAuthBody(raw2) {
|
|
107858
|
-
let parsed2;
|
|
107859
|
-
try {
|
|
107860
|
-
parsed2 = JSON.parse(raw2);
|
|
107861
|
-
} catch {
|
|
107862
|
-
return null;
|
|
107863
|
-
}
|
|
107864
|
-
if (!parsed2 || typeof parsed2 !== "object") return null;
|
|
107865
|
-
const v = parsed2;
|
|
107866
|
-
if (v.auth_mode !== "grok") return null;
|
|
107867
|
-
const tokens = v.tokens;
|
|
107868
|
-
if (!tokens || typeof tokens !== "object") return null;
|
|
107869
|
-
const t = tokens;
|
|
107870
|
-
if (typeof t.access_token !== "string" || t.access_token.length === 0) return null;
|
|
107871
|
-
if (typeof t.refresh_token !== "string" || t.refresh_token.length === 0) return null;
|
|
107872
|
-
return {
|
|
107873
|
-
auth_mode: "grok",
|
|
107874
|
-
...typeof v.refresh_rejected_at === "string" ? { refresh_rejected_at: v.refresh_rejected_at } : {},
|
|
107875
|
-
tokens: { access_token: t.access_token, refresh_token: t.refresh_token },
|
|
107876
|
-
...typeof v.last_refresh === "string" ? { last_refresh: v.last_refresh } : {}
|
|
107877
|
-
};
|
|
107878
|
-
}
|
|
107879
|
-
|
|
107880
|
-
// utils/codexHome.ts
|
|
107881
|
-
var CODEX_AUTH_ENV = "CODEX_AUTH_JSON";
|
|
107882
|
-
var XAI_AUTH_ENV = "GROK_AUTH_JSON";
|
|
107883
|
-
var installedSubscriptionPaths = /* @__PURE__ */ new Map();
|
|
107884
|
-
function clearInstalledSubscription(name) {
|
|
107885
|
-
const path4 = installedSubscriptionPaths.get(name);
|
|
107886
|
-
if (!path4 || !existsSync4(path4)) return;
|
|
107887
|
-
const value2 = JSON.parse(readFileSync2(path4, "utf8"));
|
|
107888
|
-
if (!value2 || typeof value2 !== "object") return;
|
|
107889
|
-
if (name === "CODEX_AUTH_JSON" && "openai" in value2) delete value2.openai;
|
|
107890
|
-
if (name === "GROK_AUTH_JSON" && "xai" in value2) delete value2.xai;
|
|
107891
|
-
writeFileSync4(path4, `${JSON.stringify(value2)}
|
|
107892
|
-
`, { mode: 384 });
|
|
107893
|
-
installedSubscriptionPaths.delete(name);
|
|
107894
|
-
}
|
|
107895
|
-
var PULLFROG_DATA_DIR = "/var/lib/pullfrog";
|
|
107896
|
-
function isRejectedChain(body) {
|
|
107897
|
-
if (!body.refresh_rejected_at) return false;
|
|
107898
|
-
log.warning(
|
|
107899
|
-
`\xBB ${CODEX_AUTH_ENV} was rejected by OpenAI at ${body.refresh_rejected_at} and cannot be refreshed \u2014 re-run \`npx pullfrog auth codex\`. skipping it for this run.`
|
|
107900
|
-
);
|
|
107901
|
-
return true;
|
|
107902
|
-
}
|
|
107903
|
-
function installCodexAuth() {
|
|
107904
|
-
const raw2 = process.env[CODEX_AUTH_ENV];
|
|
107905
|
-
if (!raw2) return null;
|
|
107906
|
-
const body = parseCodexAuthBody(raw2);
|
|
107907
|
-
if (!body) {
|
|
107908
|
-
log.warning(`\xBB ${CODEX_AUTH_ENV} present but malformed; ignoring`);
|
|
107909
|
-
return null;
|
|
107910
|
-
}
|
|
107911
|
-
if (isRejectedChain(body)) return null;
|
|
107912
|
-
const expiresMs = decodeJwtExpMs(body.tokens.access_token) ?? 0;
|
|
107913
|
-
const xdgDataHome = resolveDataHome();
|
|
107914
|
-
if (!xdgDataHome) return null;
|
|
107915
|
-
const opencodeDir = join5(xdgDataHome, "opencode");
|
|
107916
|
-
const authPath = join5(opencodeDir, "auth.json");
|
|
107917
|
-
writeOpenCodeAuthEntry({
|
|
107918
|
-
opencodeDir,
|
|
107919
|
-
authPath,
|
|
107920
|
-
provider: "openai",
|
|
107921
|
-
entry: {
|
|
107922
|
-
type: "oauth",
|
|
107923
|
-
refresh: body.tokens.refresh_token,
|
|
107924
|
-
access: body.tokens.access_token,
|
|
107925
|
-
expires: expiresMs,
|
|
107926
|
-
...body.tokens.account_id ? { accountId: body.tokens.account_id } : {}
|
|
107927
|
-
}
|
|
107928
|
-
});
|
|
107929
|
-
process.env.XDG_DATA_HOME = xdgDataHome;
|
|
107930
|
-
log.info(`\xBB installed Codex auth at ${authPath}`);
|
|
107931
|
-
installedSubscriptionPaths.set(CODEX_AUTH_ENV, authPath);
|
|
107932
|
-
return {
|
|
107933
|
-
authPath,
|
|
107934
|
-
xdgDataHome,
|
|
107935
|
-
originalRefresh: body.tokens.refresh_token,
|
|
107936
|
-
originalIdToken: body.tokens.id_token
|
|
107937
|
-
};
|
|
107938
|
-
}
|
|
107939
|
-
function writeOpenCodeAuthEntry(params) {
|
|
107940
|
-
let existing = {};
|
|
107941
|
-
if (existsSync4(params.authPath)) {
|
|
107942
|
-
try {
|
|
107943
|
-
const parsed2 = JSON.parse(readFileSync2(params.authPath, "utf8"));
|
|
107944
|
-
if (parsed2 && typeof parsed2 === "object") existing = parsed2;
|
|
107945
|
-
} catch {
|
|
107946
|
-
log.warning(`\xBB ${params.authPath} was unreadable; rewriting it`);
|
|
107947
|
-
}
|
|
107948
|
-
}
|
|
107949
|
-
const merged = { ...existing, [params.provider]: params.entry };
|
|
107950
|
-
mkdirSync5(params.opencodeDir, { recursive: true });
|
|
107951
|
-
writeFileSync4(params.authPath, `${JSON.stringify(merged, null, 2)}
|
|
107952
|
-
`, { mode: 384 });
|
|
107953
|
-
}
|
|
107954
|
-
function installXaiAuth() {
|
|
107955
|
-
const raw2 = process.env[XAI_AUTH_ENV];
|
|
107956
|
-
if (!raw2) return null;
|
|
107957
|
-
const body = parseXaiAuthBody(raw2);
|
|
107958
|
-
if (!body) {
|
|
107959
|
-
log.warning(`\xBB ${XAI_AUTH_ENV} present but malformed; ignoring`);
|
|
107960
|
-
return null;
|
|
107961
|
-
}
|
|
107962
|
-
if (isXaiRejectedChain(body)) return null;
|
|
107963
|
-
const xdgDataHome = resolveDataHome();
|
|
107964
|
-
if (!xdgDataHome) return null;
|
|
107965
|
-
const opencodeDir = join5(xdgDataHome, "opencode");
|
|
107966
|
-
const authPath = join5(opencodeDir, "auth.json");
|
|
107967
|
-
writeOpenCodeAuthEntry({
|
|
107968
|
-
opencodeDir,
|
|
107969
|
-
authPath,
|
|
107970
|
-
provider: "xai",
|
|
107971
|
-
// opencode refreshes when `expires` is inside its skew window, and falls
|
|
107972
|
-
// back to the JWT's own exp when the stored value is absent. Decoding it
|
|
107973
|
-
// here means the first request uses the token we already hold instead of
|
|
107974
|
-
// burning a rotation on startup.
|
|
107975
|
-
entry: {
|
|
107976
|
-
type: "oauth",
|
|
107977
|
-
refresh: body.tokens.refresh_token,
|
|
107978
|
-
access: body.tokens.access_token,
|
|
107979
|
-
expires: decodeJwtExpMs(body.tokens.access_token) ?? 0
|
|
107980
|
-
}
|
|
107981
|
-
});
|
|
107982
|
-
process.env.XDG_DATA_HOME = xdgDataHome;
|
|
107983
|
-
log.info(`\xBB installed Grok auth at ${authPath}`);
|
|
107984
|
-
installedSubscriptionPaths.set(XAI_AUTH_ENV, authPath);
|
|
107985
|
-
return { authPath, xdgDataHome, originalRefresh: body.tokens.refresh_token };
|
|
107986
|
-
}
|
|
107987
|
-
function isXaiRejectedChain(body) {
|
|
107988
|
-
if (!body.refresh_rejected_at) return false;
|
|
107989
|
-
log.warning(
|
|
107990
|
-
`\xBB ${XAI_AUTH_ENV} was rejected by xAI at ${body.refresh_rejected_at} and cannot be refreshed \u2014 re-run \`npx pullfrog auth grok\`. skipping it for this run.`
|
|
107991
|
-
);
|
|
107992
|
-
return true;
|
|
107993
|
-
}
|
|
107994
|
-
function installCodexHome() {
|
|
107995
|
-
const raw2 = process.env[CODEX_AUTH_ENV];
|
|
107996
|
-
if (!raw2) return null;
|
|
107997
|
-
const body = parseCodexAuthBody(raw2);
|
|
107998
|
-
if (!body) {
|
|
107999
|
-
log.warning(`\xBB ${CODEX_AUTH_ENV} present but malformed; ignoring`);
|
|
108000
|
-
return null;
|
|
108001
|
-
}
|
|
108002
|
-
if (isRejectedChain(body)) return null;
|
|
108003
|
-
if (!body.tokens.id_token) {
|
|
108004
|
-
log.warning(
|
|
108005
|
-
`\xBB ${CODEX_AUTH_ENV} carries no id_token \u2014 the codex CLI cannot load it. re-run \`npx pullfrog auth codex\` to mint a complete credential.`
|
|
108006
|
-
);
|
|
108007
|
-
return null;
|
|
108008
|
-
}
|
|
108009
|
-
const root = resolveDataHome();
|
|
108010
|
-
if (!root) return null;
|
|
108011
|
-
const codexHome = join5(root, "codex");
|
|
108012
|
-
const authPath = join5(codexHome, "auth.json");
|
|
108013
|
-
mkdirSync5(codexHome, { recursive: true });
|
|
108014
|
-
writeFileSync4(authPath, stringifyCodexAuthBody(body), { mode: 384 });
|
|
108015
|
-
log.info(`\xBB installed Codex auth at ${authPath}`);
|
|
108016
|
-
return { codexHome, authPath, originalRefresh: body.tokens.refresh_token };
|
|
108017
|
-
}
|
|
108018
|
-
var dataHome;
|
|
108019
|
-
var unhostedSubscriptions = /* @__PURE__ */ new Set();
|
|
108020
|
-
function canInstallSubscription(name) {
|
|
108021
|
-
return name !== CODEX_AUTH_ENV && name !== XAI_AUTH_ENV || resolveDataHome() !== null;
|
|
108022
|
-
}
|
|
108023
|
-
function resolveDataHome() {
|
|
108024
|
-
if (process.env.CI !== "true") return join5(homedir2(), ".local", "share");
|
|
108025
|
-
if (dataHome === void 0) dataHome = bootstrapPullfrogDataDir();
|
|
108026
|
-
return dataHome;
|
|
108027
|
-
}
|
|
108028
|
-
function bootstrapPullfrogDataDir() {
|
|
108029
|
-
const user = userInfo().username;
|
|
108030
|
-
let primaryGroup;
|
|
108031
|
-
try {
|
|
108032
|
-
primaryGroup = execFileSync3("id", ["-gn", user], { stdio: "pipe", encoding: "utf-8" }).trim();
|
|
108033
|
-
} catch {
|
|
108034
|
-
primaryGroup = user;
|
|
108035
|
-
}
|
|
108036
|
-
try {
|
|
108037
|
-
execFileSync3("sudo", ["-n", "mkdir", "-p", PULLFROG_DATA_DIR], { stdio: "pipe" });
|
|
108038
|
-
execFileSync3("sudo", ["-n", "chown", `${user}:${primaryGroup}`, PULLFROG_DATA_DIR], {
|
|
108039
|
-
stdio: "pipe"
|
|
108040
|
-
});
|
|
108041
|
-
execFileSync3("sudo", ["-n", "chmod", "700", PULLFROG_DATA_DIR], { stdio: "pipe" });
|
|
108042
|
-
} catch (err) {
|
|
108043
|
-
log.warning(
|
|
108044
|
-
`\xBB subscription auth skipped: could not create ${PULLFROG_DATA_DIR} (${err instanceof Error ? err.message : String(err)}). it must live outside the agent sandbox's reach, which needs passwordless sudo on this runner; runs on a model that needs the subscription will report no key.`
|
|
108045
|
-
);
|
|
108046
|
-
for (const name of [CODEX_AUTH_ENV, XAI_AUTH_ENV]) {
|
|
108047
|
-
if (process.env[name]) unhostedSubscriptions.add(name);
|
|
108048
|
-
delete process.env[name];
|
|
108049
|
-
}
|
|
108050
|
-
return null;
|
|
108051
|
-
}
|
|
108052
|
-
return PULLFROG_DATA_DIR;
|
|
108053
|
-
}
|
|
108054
|
-
|
|
108055
107920
|
// utils/oauthWriteback.ts
|
|
108056
107921
|
var core2 = __toESM(require_core(), 1);
|
|
108057
107922
|
|
|
@@ -110281,6 +110146,7 @@ async function apiFetch(options) {
|
|
|
110281
110146
|
|
|
110282
110147
|
// utils/oauthWriteback.ts
|
|
110283
110148
|
var OAUTH_WRITEBACK_STATE = "oauth_writeback";
|
|
110149
|
+
var SUBSCRIPTION_DATA_DIR_STATE = "subscription_data_dir";
|
|
110284
110150
|
|
|
110285
110151
|
// utils/secretCommands.ts
|
|
110286
110152
|
var core3 = __toESM(require_core(), 1);
|
|
@@ -110291,6 +110157,212 @@ function saveSecretState(name, value2) {
|
|
|
110291
110157
|
if (isGitHubActions) core3.saveState(name, value2);
|
|
110292
110158
|
}
|
|
110293
110159
|
|
|
110160
|
+
// utils/xaiOAuth.ts
|
|
110161
|
+
function parseXaiAuthBody(raw2) {
|
|
110162
|
+
let parsed2;
|
|
110163
|
+
try {
|
|
110164
|
+
parsed2 = JSON.parse(raw2);
|
|
110165
|
+
} catch {
|
|
110166
|
+
return null;
|
|
110167
|
+
}
|
|
110168
|
+
if (!parsed2 || typeof parsed2 !== "object") return null;
|
|
110169
|
+
const v = parsed2;
|
|
110170
|
+
if (v.auth_mode !== "grok") return null;
|
|
110171
|
+
const tokens = v.tokens;
|
|
110172
|
+
if (!tokens || typeof tokens !== "object") return null;
|
|
110173
|
+
const t = tokens;
|
|
110174
|
+
if (typeof t.access_token !== "string" || t.access_token.length === 0) return null;
|
|
110175
|
+
if (typeof t.refresh_token !== "string" || t.refresh_token.length === 0) return null;
|
|
110176
|
+
return {
|
|
110177
|
+
auth_mode: "grok",
|
|
110178
|
+
...typeof v.refresh_rejected_at === "string" ? { refresh_rejected_at: v.refresh_rejected_at } : {},
|
|
110179
|
+
tokens: { access_token: t.access_token, refresh_token: t.refresh_token },
|
|
110180
|
+
...typeof v.last_refresh === "string" ? { last_refresh: v.last_refresh } : {}
|
|
110181
|
+
};
|
|
110182
|
+
}
|
|
110183
|
+
|
|
110184
|
+
// utils/codexHome.ts
|
|
110185
|
+
var CODEX_AUTH_ENV = "CODEX_AUTH_JSON";
|
|
110186
|
+
var XAI_AUTH_ENV = "GROK_AUTH_JSON";
|
|
110187
|
+
var installedSubscriptionPaths = /* @__PURE__ */ new Map();
|
|
110188
|
+
var installedDataHome;
|
|
110189
|
+
function getSubscriptionDataHome() {
|
|
110190
|
+
return installedDataHome;
|
|
110191
|
+
}
|
|
110192
|
+
function clearInstalledSubscription(name) {
|
|
110193
|
+
const path4 = installedSubscriptionPaths.get(name);
|
|
110194
|
+
if (!path4 || !existsSync4(path4)) return;
|
|
110195
|
+
const value2 = JSON.parse(readFileSync2(path4, "utf8"));
|
|
110196
|
+
if (!value2 || typeof value2 !== "object") return;
|
|
110197
|
+
if (name === "CODEX_AUTH_JSON" && "openai" in value2) delete value2.openai;
|
|
110198
|
+
if (name === "GROK_AUTH_JSON" && "xai" in value2) delete value2.xai;
|
|
110199
|
+
writeFileSync4(path4, `${JSON.stringify(value2)}
|
|
110200
|
+
`, { mode: 384 });
|
|
110201
|
+
installedSubscriptionPaths.delete(name);
|
|
110202
|
+
}
|
|
110203
|
+
var PULLFROG_DATA_DIR = "/var/lib/pullfrog";
|
|
110204
|
+
function isRejectedChain(body) {
|
|
110205
|
+
if (!body.refresh_rejected_at) return false;
|
|
110206
|
+
log.warning(
|
|
110207
|
+
`\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.`
|
|
110208
|
+
);
|
|
110209
|
+
return true;
|
|
110210
|
+
}
|
|
110211
|
+
function installCodexAuth() {
|
|
110212
|
+
const raw2 = process.env[CODEX_AUTH_ENV];
|
|
110213
|
+
if (!raw2) return null;
|
|
110214
|
+
const body = parseCodexAuthBody(raw2);
|
|
110215
|
+
if (!body) {
|
|
110216
|
+
log.warning(`\xBB ${CODEX_AUTH_ENV} present but malformed; ignoring`);
|
|
110217
|
+
return null;
|
|
110218
|
+
}
|
|
110219
|
+
if (isRejectedChain(body)) return null;
|
|
110220
|
+
const expiresMs = decodeJwtExpMs(body.tokens.access_token) ?? 0;
|
|
110221
|
+
const xdgDataHome = resolveDataHome();
|
|
110222
|
+
if (!xdgDataHome) return null;
|
|
110223
|
+
const opencodeDir = join5(xdgDataHome, "opencode");
|
|
110224
|
+
const authPath = join5(opencodeDir, "auth.json");
|
|
110225
|
+
writeOpenCodeAuthEntry({
|
|
110226
|
+
opencodeDir,
|
|
110227
|
+
authPath,
|
|
110228
|
+
provider: "openai",
|
|
110229
|
+
entry: {
|
|
110230
|
+
type: "oauth",
|
|
110231
|
+
refresh: body.tokens.refresh_token,
|
|
110232
|
+
access: body.tokens.access_token,
|
|
110233
|
+
expires: expiresMs,
|
|
110234
|
+
...body.tokens.account_id ? { accountId: body.tokens.account_id } : {}
|
|
110235
|
+
}
|
|
110236
|
+
});
|
|
110237
|
+
installedDataHome = xdgDataHome;
|
|
110238
|
+
log.info(`\xBB installed Codex auth at ${authPath}`);
|
|
110239
|
+
installedSubscriptionPaths.set(CODEX_AUTH_ENV, authPath);
|
|
110240
|
+
return {
|
|
110241
|
+
authPath,
|
|
110242
|
+
originalRefresh: body.tokens.refresh_token,
|
|
110243
|
+
originalIdToken: body.tokens.id_token
|
|
110244
|
+
};
|
|
110245
|
+
}
|
|
110246
|
+
function writeOpenCodeAuthEntry(params) {
|
|
110247
|
+
let existing = {};
|
|
110248
|
+
if (existsSync4(params.authPath)) {
|
|
110249
|
+
try {
|
|
110250
|
+
const parsed2 = JSON.parse(readFileSync2(params.authPath, "utf8"));
|
|
110251
|
+
if (parsed2 && typeof parsed2 === "object") existing = parsed2;
|
|
110252
|
+
} catch {
|
|
110253
|
+
log.warning(`\xBB ${params.authPath} was unreadable; rewriting it`);
|
|
110254
|
+
}
|
|
110255
|
+
}
|
|
110256
|
+
const merged = { ...existing, [params.provider]: params.entry };
|
|
110257
|
+
mkdirSync5(params.opencodeDir, { recursive: true });
|
|
110258
|
+
writeFileSync4(params.authPath, `${JSON.stringify(merged, null, 2)}
|
|
110259
|
+
`, { mode: 384 });
|
|
110260
|
+
}
|
|
110261
|
+
function installXaiAuth() {
|
|
110262
|
+
const raw2 = process.env[XAI_AUTH_ENV];
|
|
110263
|
+
if (!raw2) return null;
|
|
110264
|
+
const body = parseXaiAuthBody(raw2);
|
|
110265
|
+
if (!body) {
|
|
110266
|
+
log.warning(`\xBB ${XAI_AUTH_ENV} present but malformed; ignoring`);
|
|
110267
|
+
return null;
|
|
110268
|
+
}
|
|
110269
|
+
if (isXaiRejectedChain(body)) return null;
|
|
110270
|
+
const xdgDataHome = resolveDataHome();
|
|
110271
|
+
if (!xdgDataHome) return null;
|
|
110272
|
+
const opencodeDir = join5(xdgDataHome, "opencode");
|
|
110273
|
+
const authPath = join5(opencodeDir, "auth.json");
|
|
110274
|
+
writeOpenCodeAuthEntry({
|
|
110275
|
+
opencodeDir,
|
|
110276
|
+
authPath,
|
|
110277
|
+
provider: "xai",
|
|
110278
|
+
// opencode refreshes when `expires` is inside its skew window, and falls
|
|
110279
|
+
// back to the JWT's own exp when the stored value is absent. Decoding it
|
|
110280
|
+
// here means the first request uses the token we already hold instead of
|
|
110281
|
+
// burning a rotation on startup.
|
|
110282
|
+
entry: {
|
|
110283
|
+
type: "oauth",
|
|
110284
|
+
refresh: body.tokens.refresh_token,
|
|
110285
|
+
access: body.tokens.access_token,
|
|
110286
|
+
expires: decodeJwtExpMs(body.tokens.access_token) ?? 0
|
|
110287
|
+
}
|
|
110288
|
+
});
|
|
110289
|
+
installedDataHome = xdgDataHome;
|
|
110290
|
+
log.info(`\xBB installed Grok auth at ${authPath}`);
|
|
110291
|
+
installedSubscriptionPaths.set(XAI_AUTH_ENV, authPath);
|
|
110292
|
+
return { authPath, originalRefresh: body.tokens.refresh_token };
|
|
110293
|
+
}
|
|
110294
|
+
function isXaiRejectedChain(body) {
|
|
110295
|
+
if (!body.refresh_rejected_at) return false;
|
|
110296
|
+
log.warning(
|
|
110297
|
+
`\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.`
|
|
110298
|
+
);
|
|
110299
|
+
return true;
|
|
110300
|
+
}
|
|
110301
|
+
function installCodexHome() {
|
|
110302
|
+
const raw2 = process.env[CODEX_AUTH_ENV];
|
|
110303
|
+
if (!raw2) return null;
|
|
110304
|
+
const body = parseCodexAuthBody(raw2);
|
|
110305
|
+
if (!body) {
|
|
110306
|
+
log.warning(`\xBB ${CODEX_AUTH_ENV} present but malformed; ignoring`);
|
|
110307
|
+
return null;
|
|
110308
|
+
}
|
|
110309
|
+
if (isRejectedChain(body)) return null;
|
|
110310
|
+
if (!body.tokens.id_token) {
|
|
110311
|
+
log.warning(
|
|
110312
|
+
`\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.`
|
|
110313
|
+
);
|
|
110314
|
+
return null;
|
|
110315
|
+
}
|
|
110316
|
+
const root = resolveDataHome();
|
|
110317
|
+
if (!root) return null;
|
|
110318
|
+
const codexHome = join5(root, "codex");
|
|
110319
|
+
const authPath = join5(codexHome, "auth.json");
|
|
110320
|
+
mkdirSync5(codexHome, { recursive: true });
|
|
110321
|
+
writeFileSync4(authPath, stringifyCodexAuthBody(body), { mode: 384 });
|
|
110322
|
+
log.info(`\xBB installed Codex auth at ${authPath}`);
|
|
110323
|
+
return { codexHome, authPath, originalRefresh: body.tokens.refresh_token };
|
|
110324
|
+
}
|
|
110325
|
+
var dataHome;
|
|
110326
|
+
var unhostedSubscriptions = /* @__PURE__ */ new Set();
|
|
110327
|
+
function canInstallSubscription(name) {
|
|
110328
|
+
return name !== CODEX_AUTH_ENV && name !== XAI_AUTH_ENV || resolveDataHome() !== null;
|
|
110329
|
+
}
|
|
110330
|
+
function resolveDataHome() {
|
|
110331
|
+
if (process.env.CI !== "true") return join5(homedir2(), ".local", "share");
|
|
110332
|
+
if (dataHome === void 0) dataHome = bootstrapPullfrogDataDir();
|
|
110333
|
+
return dataHome;
|
|
110334
|
+
}
|
|
110335
|
+
function bootstrapPullfrogDataDir() {
|
|
110336
|
+
const user = userInfo().username;
|
|
110337
|
+
let primaryGroup;
|
|
110338
|
+
try {
|
|
110339
|
+
primaryGroup = execFileSync3("id", ["-gn", user], { stdio: "pipe", encoding: "utf-8" }).trim();
|
|
110340
|
+
} catch {
|
|
110341
|
+
primaryGroup = user;
|
|
110342
|
+
}
|
|
110343
|
+
try {
|
|
110344
|
+
execFileSync3("sudo", ["-n", "mkdir", "-p", PULLFROG_DATA_DIR], { stdio: "pipe" });
|
|
110345
|
+
execFileSync3("sudo", ["-n", "chown", `${user}:${primaryGroup}`, PULLFROG_DATA_DIR], {
|
|
110346
|
+
stdio: "pipe"
|
|
110347
|
+
});
|
|
110348
|
+
execFileSync3("sudo", ["-n", "chmod", "700", PULLFROG_DATA_DIR], { stdio: "pipe" });
|
|
110349
|
+
} catch (err) {
|
|
110350
|
+
log.warning(
|
|
110351
|
+
`\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.`
|
|
110352
|
+
);
|
|
110353
|
+
for (const name of [CODEX_AUTH_ENV, XAI_AUTH_ENV]) {
|
|
110354
|
+
if (process.env[name]) unhostedSubscriptions.add(name);
|
|
110355
|
+
delete process.env[name];
|
|
110356
|
+
}
|
|
110357
|
+
return null;
|
|
110358
|
+
}
|
|
110359
|
+
const runDir = mkdtempSync(join5(PULLFROG_DATA_DIR, "run-"));
|
|
110360
|
+
if (process.env.RUNNER_ENVIRONMENT !== "github-hosted") {
|
|
110361
|
+
saveSecretState(SUBSCRIPTION_DATA_DIR_STATE, runDir);
|
|
110362
|
+
}
|
|
110363
|
+
return runDir;
|
|
110364
|
+
}
|
|
110365
|
+
|
|
110294
110366
|
// utils/secrets.ts
|
|
110295
110367
|
var SENSITIVE_PATTERNS = [
|
|
110296
110368
|
/_KEY$/i,
|
|
@@ -110937,9 +111009,8 @@ var codex = agent({
|
|
|
110937
111009
|
initialUsage: initial.usage,
|
|
110938
111010
|
reflectionPrompt: buildReflectionPrompt(ctx.toolState),
|
|
110939
111011
|
// a resume needs the thread codex recorded. without one there is nothing
|
|
110940
|
-
// to continue, and `resume --last`
|
|
110941
|
-
//
|
|
110942
|
-
// runner with a shared CODEX_HOME is somebody else's run.
|
|
111012
|
+
// to continue, and `resume --last` could pick another run's session off CI,
|
|
111013
|
+
// where CODEX_HOME is the shared ~/.local/share/codex (in CI it is per run).
|
|
110943
111014
|
canResume: (r) => r.threadId !== void 0,
|
|
110944
111015
|
resume: async (c) => runCodex({
|
|
110945
111016
|
...runnerArgs,
|
|
@@ -110966,7 +111037,7 @@ var codex = agent({
|
|
|
110966
111037
|
import { spawn as nodeSpawn2 } from "node:child_process";
|
|
110967
111038
|
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync6 } from "node:fs";
|
|
110968
111039
|
import { totalmem } from "node:os";
|
|
110969
|
-
import { join as
|
|
111040
|
+
import { join as join8 } from "node:path";
|
|
110970
111041
|
import { performance as performance7 } from "node:perf_hooks";
|
|
110971
111042
|
|
|
110972
111043
|
// node_modules/.pnpm/@opencode-ai+sdk@1.18.29/node_modules/@opencode-ai/sdk/dist/v2/gen/core/serverSentEvents.gen.js
|
|
@@ -116375,200 +116446,27 @@ var import_cross_spawn = __toESM(require_cross_spawn(), 1);
|
|
|
116375
116446
|
// agents/opencode.ts
|
|
116376
116447
|
var import_undici = __toESM(require_undici2(), 1);
|
|
116377
116448
|
|
|
116378
|
-
// utils/worktree.ts
|
|
116379
|
-
async function dirtyTrackedPaths(params = {}) {
|
|
116380
|
-
const result = await spawn({
|
|
116381
|
-
cmd: "git",
|
|
116382
|
-
args: ["diff", "--name-only", "HEAD"],
|
|
116383
|
-
env: process.env,
|
|
116384
|
-
cwd: params.cwd ?? process.cwd(),
|
|
116385
|
-
activityTimeout: 0
|
|
116386
|
-
});
|
|
116387
|
-
if (result.exitCode !== 0) {
|
|
116388
|
-
throw new Error(
|
|
116389
|
-
`git diff --name-only HEAD failed (exit ${result.exitCode}): ${result.stderr.trim() || "(no stderr)"}`
|
|
116390
|
-
);
|
|
116391
|
-
}
|
|
116392
|
-
return new Set(result.stdout.split("\n").filter(Boolean));
|
|
116393
|
-
}
|
|
116394
|
-
async function restoreDirtiedSince(params) {
|
|
116395
|
-
const dirtied = [...await dirtyTrackedPaths({ cwd: params.cwd ?? process.cwd() })].filter(
|
|
116396
|
-
(path4) => !params.before.has(path4)
|
|
116397
|
-
);
|
|
116398
|
-
if (dirtied.length === 0) return;
|
|
116399
|
-
const result = await spawn({
|
|
116400
|
-
cmd: "git",
|
|
116401
|
-
args: ["restore", "--staged", "--worktree", "--", ...dirtied],
|
|
116402
|
-
env: process.env,
|
|
116403
|
-
cwd: params.cwd ?? process.cwd(),
|
|
116404
|
-
activityTimeout: 0
|
|
116405
|
-
});
|
|
116406
|
-
if (result.exitCode !== 0) {
|
|
116407
|
-
log.warning(
|
|
116408
|
-
`\xBB failed to restore ${dirtied.length} tracked file(s) modified by ${params.actor}: ${result.stderr.trim() || "(no stderr)"}`
|
|
116409
|
-
);
|
|
116410
|
-
return;
|
|
116411
|
-
}
|
|
116412
|
-
log.info(
|
|
116413
|
-
`\xBB restored ${dirtied.length} tracked file(s) modified by ${params.actor}: ${dirtied.join(", ")}`
|
|
116414
|
-
);
|
|
116415
|
-
}
|
|
116416
|
-
|
|
116417
|
-
// agents/opencodePlugin.ts
|
|
116418
|
-
var PULLFROG_BUS_EVENT_TYPE = "pullfrog_bus_event";
|
|
116419
|
-
var PULLFROG_OPENCODE_GATE_PLUGIN_FILENAME = "pullfrog-subagent-gate.ts";
|
|
116420
|
-
var PULLFROG_OPENCODE_PLUGIN_SOURCE = `// AUTOGENERATED by Pullfrog. do not edit; it'll be overwritten on the next run.
|
|
116421
|
-
// surfaces opencode subagent activity that the CLI's run-loop discards. see
|
|
116422
|
-
// action/agents/opencodePlugin.ts in pullfrog/app for why this exists. lives
|
|
116423
|
-
// inside the per-run tmpdir (XDG_CONFIG_HOME/opencode/plugin/), never inside
|
|
116424
|
-
// the user's working tree. the subagent gate ships as a separate plugin
|
|
116425
|
-
// (pullfrog-subagent-gate.ts).
|
|
116426
|
-
|
|
116427
|
-
const PULLFROG_BUS_EVENT_TYPE = ${JSON.stringify(PULLFROG_BUS_EVENT_TYPE)};
|
|
116428
|
-
|
|
116429
|
-
// orchestratorSessionID locks to the first sessionID we observe on the bus
|
|
116430
|
-
// (\`message.part.updated\`). opencode's run command creates exactly one
|
|
116431
|
-
// top-level session before any subagent is dispatched, and the orchestrator
|
|
116432
|
-
// emits at least one event before any subagent does \u2014 so first-seen-wins
|
|
116433
|
-
// captures the orchestrator; every subagent part has a non-matching
|
|
116434
|
-
// sessionID.
|
|
116435
|
-
let orchestratorSessionID: string | undefined;
|
|
116436
|
-
|
|
116437
|
-
function isOrchestratorTaskDispatch(part: {
|
|
116438
|
-
type?: string;
|
|
116439
|
-
tool?: string;
|
|
116440
|
-
state?: { status?: string };
|
|
116441
|
-
} | undefined): boolean {
|
|
116442
|
-
if (!part || part.type !== "tool") return false;
|
|
116443
|
-
if (part.tool !== "task") return false;
|
|
116444
|
-
// only forward at status="running" (not "pending"). at pending the
|
|
116445
|
-
// state.input is still {} \u2014 the orchestrator has emitted the part shell
|
|
116446
|
-
// but the LLM hasn't filled in description/subagent_type/prompt yet. by
|
|
116447
|
-
// running, input is populated and recordTaskDispatch can derive the lens
|
|
116448
|
-
// label correctly.
|
|
116449
|
-
return part.state?.status === "running";
|
|
116450
|
-
}
|
|
116451
|
-
|
|
116452
|
-
export default async function pullfrogEventsPlugin() {
|
|
116453
|
-
return {
|
|
116454
|
-
event: async (input: {
|
|
116455
|
-
event: {
|
|
116456
|
-
type: string;
|
|
116457
|
-
properties?: {
|
|
116458
|
-
part?: {
|
|
116459
|
-
sessionID?: string;
|
|
116460
|
-
type?: string;
|
|
116461
|
-
tool?: string;
|
|
116462
|
-
state?: { status?: string };
|
|
116463
|
-
};
|
|
116464
|
-
};
|
|
116465
|
-
};
|
|
116466
|
-
}) => {
|
|
116467
|
-
const event = input?.event;
|
|
116468
|
-
if (!event || typeof event !== "object") return;
|
|
116469
|
-
if (event.type !== "message.part.updated") return;
|
|
116470
|
-
const part = event.properties?.part;
|
|
116471
|
-
const sessionID = part?.sessionID;
|
|
116472
|
-
if (typeof sessionID !== "string" || sessionID.length === 0) return;
|
|
116473
|
-
if (orchestratorSessionID === undefined) orchestratorSessionID = sessionID;
|
|
116474
|
-
|
|
116475
|
-
if (sessionID === orchestratorSessionID) {
|
|
116476
|
-
// skip orchestrator events EXCEPT early task dispatches.
|
|
116477
|
-
if (!isOrchestratorTaskDispatch(part)) return;
|
|
116478
|
-
}
|
|
116479
|
-
|
|
116480
|
-
try {
|
|
116481
|
-
const line = JSON.stringify({
|
|
116482
|
-
type: PULLFROG_BUS_EVENT_TYPE,
|
|
116483
|
-
bus_event: event,
|
|
116484
|
-
});
|
|
116485
|
-
process.stdout.write(line + "\\n");
|
|
116486
|
-
} catch {
|
|
116487
|
-
// a circular reference or BigInt etc. would throw; swallow rather
|
|
116488
|
-
// than letting a single bad event take down the plugin.
|
|
116489
|
-
}
|
|
116490
|
-
},
|
|
116491
|
-
};
|
|
116492
|
-
}
|
|
116493
|
-
`;
|
|
116494
|
-
function buildOpencodeSubagentGateSource(deniedTools) {
|
|
116495
|
-
return `// AUTOGENERATED by Pullfrog. do not edit; it'll be overwritten on the next run.
|
|
116496
|
-
// hard-blocks state-mutating MCP tool calls from opencode subagents (PR
|
|
116497
|
-
// following pullfrog/app#796 \u2014 see action/agents/subagentToolGates.ts). see
|
|
116498
|
-
// action/agents/opencodePlugin.ts in pullfrog/app for why this exists. lives
|
|
116499
|
-
// inside the per-run tmpdir (XDG_CONFIG_HOME/opencode/plugin/), never inside
|
|
116500
|
-
// the user's working tree.
|
|
116501
|
-
|
|
116502
|
-
// embedded at template-render time so the plugin source has no imports. the
|
|
116503
|
-
// denied set is derived from each tool's \`mutates\` flag \u2014 see
|
|
116504
|
-
// action/agents/subagentToolGates.ts.
|
|
116505
|
-
const SUBAGENT_DENIED_TOOLS = new Set(${JSON.stringify(deniedTools)});
|
|
116506
|
-
|
|
116507
|
-
// opencode presents MCP tools as \`pullfrog_<bare>\`; strip that to compare
|
|
116508
|
-
// against the canonical bare names in SUBAGENT_DENIED_TOOLS. native tools
|
|
116509
|
-
// (read, write, bash, task, etc.) lack the prefix and never match the deny
|
|
116510
|
-
// list anyway.
|
|
116511
|
-
function stripPullfrogPrefix(toolName: string): string {
|
|
116512
|
-
if (toolName.startsWith("pullfrog_")) return toolName.slice("pullfrog_".length);
|
|
116513
|
-
return toolName;
|
|
116514
|
-
}
|
|
116515
|
-
|
|
116516
|
-
// orchestratorSessionID locks to the first sessionID we observe on
|
|
116517
|
-
// \`tool.execute.before\`. the orchestrator must invoke a tool (e.g. its
|
|
116518
|
-
// \`task\` dispatch) before any subagent session exists, so first-seen-wins
|
|
116519
|
-
// always captures the orchestrator; every subagent call therefore has a
|
|
116520
|
-
// non-matching sessionID and is subject to the deny check.
|
|
116521
|
-
let orchestratorSessionID: string | undefined;
|
|
116522
|
-
|
|
116523
|
-
export default async function pullfrogSubagentGatePlugin() {
|
|
116524
|
-
return {
|
|
116525
|
-
"tool.execute.before": async (
|
|
116526
|
-
input: { tool?: string; sessionID?: string; callID?: string },
|
|
116527
|
-
_output: { args?: Record<string, unknown> }
|
|
116528
|
-
) => {
|
|
116529
|
-
// input: { tool, sessionID, callID } \u2014 confirmed against
|
|
116530
|
-
// anomalyco/opencode packages/opencode/src/session/tools.ts which
|
|
116531
|
-
// calls plugin.trigger("tool.execute.before", { tool, sessionID,
|
|
116532
|
-
// callID }, { args }). throwing here surfaces as a tool error to the
|
|
116533
|
-
// model (Effect's run.promise propagates the rejection through the
|
|
116534
|
-
// AI SDK's tool-runtime), so the subagent sees a denial and can
|
|
116535
|
-
// pick a different action.
|
|
116536
|
-
const sessionID = typeof input?.sessionID === "string" ? input.sessionID : "";
|
|
116537
|
-
const tool = typeof input?.tool === "string" ? input.tool : "";
|
|
116538
|
-
// above the sessionID check: the bash block must never depend on a
|
|
116539
|
-
// session id being present.
|
|
116540
|
-
if (tool === "bash") {
|
|
116541
|
-
throw new Error("the native bash tool is disabled in Pullfrog runs.");
|
|
116542
|
-
}
|
|
116543
|
-
if (!sessionID || !tool) return;
|
|
116544
|
-
if (orchestratorSessionID === undefined) orchestratorSessionID = sessionID;
|
|
116545
|
-
if (sessionID === orchestratorSessionID) return;
|
|
116546
|
-
const bare = stripPullfrogPrefix(tool);
|
|
116547
|
-
if (!SUBAGENT_DENIED_TOOLS.has(bare)) return;
|
|
116548
|
-
throw new Error(
|
|
116549
|
-
"subagent attempted to call denied tool '" + bare + "'. " +
|
|
116550
|
-
"subagents share the orchestrator's in-process working tree and toolState; " +
|
|
116551
|
-
"state-changing MCP tools (checkout_pr, push_branch, create_pull_request_review, " +
|
|
116552
|
-
"report_progress, etc.) are reserved for the orchestrator. " +
|
|
116553
|
-
"report findings back to the orchestrator and let it perform the mutation."
|
|
116554
|
-
);
|
|
116555
|
-
},
|
|
116556
|
-
};
|
|
116557
|
-
}
|
|
116558
|
-
`;
|
|
116559
|
-
}
|
|
116560
|
-
|
|
116561
116449
|
// utils/openCodeModels.ts
|
|
116562
116450
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
116451
|
+
import { join as join7 } from "node:path";
|
|
116563
116452
|
var baseline;
|
|
116564
116453
|
var authorized;
|
|
116565
116454
|
var failure;
|
|
116566
116455
|
var ANSI_PATTERN = /\[[0-9;]*m/g;
|
|
116567
|
-
function
|
|
116568
|
-
|
|
116456
|
+
function openCodeHomeEnv(tmpdir3) {
|
|
116457
|
+
return {
|
|
116458
|
+
HOME: tmpdir3,
|
|
116459
|
+
XDG_CONFIG_HOME: join7(tmpdir3, ".config"),
|
|
116460
|
+
XDG_DATA_HOME: getSubscriptionDataHome() ?? join7(tmpdir3, ".local", "share"),
|
|
116461
|
+
XDG_CACHE_HOME: join7(tmpdir3, ".cache"),
|
|
116462
|
+
XDG_STATE_HOME: join7(tmpdir3, ".local", "state")
|
|
116463
|
+
};
|
|
116464
|
+
}
|
|
116465
|
+
function readModels(params) {
|
|
116466
|
+
const result = spawnSync3(params.cliPath, ["models"], {
|
|
116569
116467
|
encoding: "utf-8",
|
|
116570
116468
|
timeout: 3e4,
|
|
116571
|
-
env: process.env,
|
|
116469
|
+
env: { ...process.env, ...openCodeHomeEnv(params.tmpdir) },
|
|
116572
116470
|
stdio: ["ignore", "pipe", "pipe"]
|
|
116573
116471
|
});
|
|
116574
116472
|
if (result.status !== 0) {
|
|
@@ -116585,12 +116483,12 @@ function readModels(cliPath) {
|
|
|
116585
116483
|
function getModelsFailure() {
|
|
116586
116484
|
return failure;
|
|
116587
116485
|
}
|
|
116588
|
-
function captureBaselineModels(
|
|
116589
|
-
baseline = readModels(
|
|
116486
|
+
function captureBaselineModels(params) {
|
|
116487
|
+
baseline = readModels(params);
|
|
116590
116488
|
log.debug(`\xBB opencode baseline: ${baseline.size} models`);
|
|
116591
116489
|
}
|
|
116592
|
-
function captureAuthorizedModels(
|
|
116593
|
-
authorized = readModels(
|
|
116490
|
+
function captureAuthorizedModels(params) {
|
|
116491
|
+
authorized = readModels(params);
|
|
116594
116492
|
const base = baseline;
|
|
116595
116493
|
if (base) {
|
|
116596
116494
|
const diff = [...authorized].filter((m) => !base.has(m));
|
|
@@ -116610,6 +116508,190 @@ function getAuthorizedModels() {
|
|
|
116610
116508
|
return authorized;
|
|
116611
116509
|
}
|
|
116612
116510
|
|
|
116511
|
+
// utils/worktree.ts
|
|
116512
|
+
async function dirtyTrackedPaths(params = {}) {
|
|
116513
|
+
const result = await spawn({
|
|
116514
|
+
cmd: "git",
|
|
116515
|
+
args: ["diff", "--name-only", "HEAD"],
|
|
116516
|
+
env: process.env,
|
|
116517
|
+
cwd: params.cwd ?? process.cwd(),
|
|
116518
|
+
activityTimeout: 0
|
|
116519
|
+
});
|
|
116520
|
+
if (result.exitCode !== 0) {
|
|
116521
|
+
throw new Error(
|
|
116522
|
+
`git diff --name-only HEAD failed (exit ${result.exitCode}): ${result.stderr.trim() || "(no stderr)"}`
|
|
116523
|
+
);
|
|
116524
|
+
}
|
|
116525
|
+
return new Set(result.stdout.split("\n").filter(Boolean));
|
|
116526
|
+
}
|
|
116527
|
+
async function restoreDirtiedSince(params) {
|
|
116528
|
+
const dirtied = [...await dirtyTrackedPaths({ cwd: params.cwd ?? process.cwd() })].filter(
|
|
116529
|
+
(path4) => !params.before.has(path4)
|
|
116530
|
+
);
|
|
116531
|
+
if (dirtied.length === 0) return;
|
|
116532
|
+
const result = await spawn({
|
|
116533
|
+
cmd: "git",
|
|
116534
|
+
args: ["restore", "--staged", "--worktree", "--", ...dirtied],
|
|
116535
|
+
env: process.env,
|
|
116536
|
+
cwd: params.cwd ?? process.cwd(),
|
|
116537
|
+
activityTimeout: 0
|
|
116538
|
+
});
|
|
116539
|
+
if (result.exitCode !== 0) {
|
|
116540
|
+
log.warning(
|
|
116541
|
+
`\xBB failed to restore ${dirtied.length} tracked file(s) modified by ${params.actor}: ${result.stderr.trim() || "(no stderr)"}`
|
|
116542
|
+
);
|
|
116543
|
+
return;
|
|
116544
|
+
}
|
|
116545
|
+
log.info(
|
|
116546
|
+
`\xBB restored ${dirtied.length} tracked file(s) modified by ${params.actor}: ${dirtied.join(", ")}`
|
|
116547
|
+
);
|
|
116548
|
+
}
|
|
116549
|
+
|
|
116550
|
+
// agents/opencodePlugin.ts
|
|
116551
|
+
var PULLFROG_BUS_EVENT_TYPE = "pullfrog_bus_event";
|
|
116552
|
+
var PULLFROG_OPENCODE_GATE_PLUGIN_FILENAME = "pullfrog-subagent-gate.ts";
|
|
116553
|
+
var PULLFROG_OPENCODE_PLUGIN_SOURCE = `// AUTOGENERATED by Pullfrog. do not edit; it'll be overwritten on the next run.
|
|
116554
|
+
// surfaces opencode subagent activity that the CLI's run-loop discards. see
|
|
116555
|
+
// action/agents/opencodePlugin.ts in pullfrog/app for why this exists. lives
|
|
116556
|
+
// inside the per-run tmpdir (XDG_CONFIG_HOME/opencode/plugin/), never inside
|
|
116557
|
+
// the user's working tree. the subagent gate ships as a separate plugin
|
|
116558
|
+
// (pullfrog-subagent-gate.ts).
|
|
116559
|
+
|
|
116560
|
+
const PULLFROG_BUS_EVENT_TYPE = ${JSON.stringify(PULLFROG_BUS_EVENT_TYPE)};
|
|
116561
|
+
|
|
116562
|
+
// orchestratorSessionID locks to the first sessionID we observe on the bus
|
|
116563
|
+
// (\`message.part.updated\`). opencode's run command creates exactly one
|
|
116564
|
+
// top-level session before any subagent is dispatched, and the orchestrator
|
|
116565
|
+
// emits at least one event before any subagent does \u2014 so first-seen-wins
|
|
116566
|
+
// captures the orchestrator; every subagent part has a non-matching
|
|
116567
|
+
// sessionID.
|
|
116568
|
+
let orchestratorSessionID: string | undefined;
|
|
116569
|
+
|
|
116570
|
+
function isOrchestratorTaskDispatch(part: {
|
|
116571
|
+
type?: string;
|
|
116572
|
+
tool?: string;
|
|
116573
|
+
state?: { status?: string };
|
|
116574
|
+
} | undefined): boolean {
|
|
116575
|
+
if (!part || part.type !== "tool") return false;
|
|
116576
|
+
if (part.tool !== "task") return false;
|
|
116577
|
+
// only forward at status="running" (not "pending"). at pending the
|
|
116578
|
+
// state.input is still {} \u2014 the orchestrator has emitted the part shell
|
|
116579
|
+
// but the LLM hasn't filled in description/subagent_type/prompt yet. by
|
|
116580
|
+
// running, input is populated and recordTaskDispatch can derive the lens
|
|
116581
|
+
// label correctly.
|
|
116582
|
+
return part.state?.status === "running";
|
|
116583
|
+
}
|
|
116584
|
+
|
|
116585
|
+
export default async function pullfrogEventsPlugin() {
|
|
116586
|
+
return {
|
|
116587
|
+
event: async (input: {
|
|
116588
|
+
event: {
|
|
116589
|
+
type: string;
|
|
116590
|
+
properties?: {
|
|
116591
|
+
part?: {
|
|
116592
|
+
sessionID?: string;
|
|
116593
|
+
type?: string;
|
|
116594
|
+
tool?: string;
|
|
116595
|
+
state?: { status?: string };
|
|
116596
|
+
};
|
|
116597
|
+
};
|
|
116598
|
+
};
|
|
116599
|
+
}) => {
|
|
116600
|
+
const event = input?.event;
|
|
116601
|
+
if (!event || typeof event !== "object") return;
|
|
116602
|
+
if (event.type !== "message.part.updated") return;
|
|
116603
|
+
const part = event.properties?.part;
|
|
116604
|
+
const sessionID = part?.sessionID;
|
|
116605
|
+
if (typeof sessionID !== "string" || sessionID.length === 0) return;
|
|
116606
|
+
if (orchestratorSessionID === undefined) orchestratorSessionID = sessionID;
|
|
116607
|
+
|
|
116608
|
+
if (sessionID === orchestratorSessionID) {
|
|
116609
|
+
// skip orchestrator events EXCEPT early task dispatches.
|
|
116610
|
+
if (!isOrchestratorTaskDispatch(part)) return;
|
|
116611
|
+
}
|
|
116612
|
+
|
|
116613
|
+
try {
|
|
116614
|
+
const line = JSON.stringify({
|
|
116615
|
+
type: PULLFROG_BUS_EVENT_TYPE,
|
|
116616
|
+
bus_event: event,
|
|
116617
|
+
});
|
|
116618
|
+
process.stdout.write(line + "\\n");
|
|
116619
|
+
} catch {
|
|
116620
|
+
// a circular reference or BigInt etc. would throw; swallow rather
|
|
116621
|
+
// than letting a single bad event take down the plugin.
|
|
116622
|
+
}
|
|
116623
|
+
},
|
|
116624
|
+
};
|
|
116625
|
+
}
|
|
116626
|
+
`;
|
|
116627
|
+
function buildOpencodeSubagentGateSource(ctx) {
|
|
116628
|
+
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.`;
|
|
116629
|
+
return `// AUTOGENERATED by Pullfrog. do not edit; it'll be overwritten on the next run.
|
|
116630
|
+
// hard-blocks state-mutating MCP tool calls from opencode subagents (PR
|
|
116631
|
+
// following pullfrog/app#796 \u2014 see action/agents/subagentToolGates.ts). see
|
|
116632
|
+
// action/agents/opencodePlugin.ts in pullfrog/app for why this exists. lives
|
|
116633
|
+
// inside the per-run tmpdir (XDG_CONFIG_HOME/opencode/plugin/), never inside
|
|
116634
|
+
// the user's working tree.
|
|
116635
|
+
|
|
116636
|
+
// embedded at template-render time so the plugin source has no imports. the
|
|
116637
|
+
// denied set is derived from each tool's \`mutates\` flag \u2014 see
|
|
116638
|
+
// action/agents/subagentToolGates.ts.
|
|
116639
|
+
const SUBAGENT_DENIED_TOOLS = new Set(${JSON.stringify(ctx.subagentDeniedTools)});
|
|
116640
|
+
|
|
116641
|
+
// opencode presents MCP tools as \`pullfrog_<bare>\`; strip that to compare
|
|
116642
|
+
// against the canonical bare names in SUBAGENT_DENIED_TOOLS. native tools
|
|
116643
|
+
// (read, write, bash, task, etc.) lack the prefix and never match the deny
|
|
116644
|
+
// list anyway.
|
|
116645
|
+
function stripPullfrogPrefix(toolName: string): string {
|
|
116646
|
+
if (toolName.startsWith("pullfrog_")) return toolName.slice("pullfrog_".length);
|
|
116647
|
+
return toolName;
|
|
116648
|
+
}
|
|
116649
|
+
|
|
116650
|
+
// orchestratorSessionID locks to the first sessionID we observe on
|
|
116651
|
+
// \`tool.execute.before\`. the orchestrator must invoke a tool (e.g. its
|
|
116652
|
+
// \`task\` dispatch) before any subagent session exists, so first-seen-wins
|
|
116653
|
+
// always captures the orchestrator; every subagent call therefore has a
|
|
116654
|
+
// non-matching sessionID and is subject to the deny check.
|
|
116655
|
+
let orchestratorSessionID: string | undefined;
|
|
116656
|
+
|
|
116657
|
+
export default async function pullfrogSubagentGatePlugin() {
|
|
116658
|
+
return {
|
|
116659
|
+
"tool.execute.before": async (
|
|
116660
|
+
input: { tool?: string; sessionID?: string; callID?: string },
|
|
116661
|
+
_output: { args?: Record<string, unknown> }
|
|
116662
|
+
) => {
|
|
116663
|
+
// input: { tool, sessionID, callID } \u2014 confirmed against
|
|
116664
|
+
// anomalyco/opencode packages/opencode/src/session/tools.ts which
|
|
116665
|
+
// calls plugin.trigger("tool.execute.before", { tool, sessionID,
|
|
116666
|
+
// callID }, { args }). throwing here surfaces as a tool error to the
|
|
116667
|
+
// model (Effect's run.promise propagates the rejection through the
|
|
116668
|
+
// AI SDK's tool-runtime), so the subagent sees a denial and can
|
|
116669
|
+
// pick a different action.
|
|
116670
|
+
const sessionID = typeof input?.sessionID === "string" ? input.sessionID : "";
|
|
116671
|
+
const tool = typeof input?.tool === "string" ? input.tool : "";
|
|
116672
|
+
// above the sessionID check: the bash block must never depend on a
|
|
116673
|
+
// session id being present.
|
|
116674
|
+
if (tool === "bash") {
|
|
116675
|
+
throw new Error(${JSON.stringify(bashRefusal)});
|
|
116676
|
+
}
|
|
116677
|
+
if (!sessionID || !tool) return;
|
|
116678
|
+
if (orchestratorSessionID === undefined) orchestratorSessionID = sessionID;
|
|
116679
|
+
if (sessionID === orchestratorSessionID) return;
|
|
116680
|
+
const bare = stripPullfrogPrefix(tool);
|
|
116681
|
+
if (!SUBAGENT_DENIED_TOOLS.has(bare)) return;
|
|
116682
|
+
throw new Error(
|
|
116683
|
+
"subagent attempted to call denied tool '" + bare + "'. " +
|
|
116684
|
+
"subagents share the orchestrator's in-process working tree and toolState; " +
|
|
116685
|
+
"state-changing MCP tools (checkout_pr, push_branch, create_pull_request_review, " +
|
|
116686
|
+
"report_progress, etc.) are reserved for the orchestrator. " +
|
|
116687
|
+
"report findings back to the orchestrator and let it perform the mutation."
|
|
116688
|
+
);
|
|
116689
|
+
},
|
|
116690
|
+
};
|
|
116691
|
+
}
|
|
116692
|
+
`;
|
|
116693
|
+
}
|
|
116694
|
+
|
|
116613
116695
|
// agents/subagentModels.ts
|
|
116614
116696
|
function deriveSubagentModels(orchestratorSpec) {
|
|
116615
116697
|
if (!orchestratorSpec) return { reviewer: void 0 };
|
|
@@ -116984,6 +117066,7 @@ async function dispatchEvent(ctx, event) {
|
|
|
116984
117066
|
const err = event.properties.error;
|
|
116985
117067
|
const message = err ? extractErrorMessage(err) : "(no error payload)";
|
|
116986
117068
|
log.info(`\xBB ${ctx.label} session error: ${message}`);
|
|
117069
|
+
if (err) ctx.lastSessionError = message;
|
|
116987
117070
|
return;
|
|
116988
117071
|
}
|
|
116989
117072
|
}
|
|
@@ -117122,6 +117205,7 @@ async function runPromptTurn(ctx, params) {
|
|
|
117122
117205
|
ctx.promptMessageID = void 0;
|
|
117123
117206
|
ctx.diagnostic.sawModelOutput = false;
|
|
117124
117207
|
ctx.diagnostic.idleSec = void 0;
|
|
117208
|
+
ctx.lastSessionError = void 0;
|
|
117125
117209
|
let assistant;
|
|
117126
117210
|
let returnedParts;
|
|
117127
117211
|
let networkError = null;
|
|
@@ -117166,11 +117250,12 @@ async function runPromptTurn(ctx, params) {
|
|
|
117166
117250
|
}
|
|
117167
117251
|
const cause = ctx.serverExitCause() ?? ctx.diagnostic.lastProviderError;
|
|
117168
117252
|
const diagnosis = cause ? ` \u2014 likely cause: ${cause}` : "";
|
|
117253
|
+
const promptCause = ctx.serverExitCause() ?? (ctx.diagnostic.sawModelOutput ? void 0 : ctx.lastSessionError) ?? ctx.diagnostic.lastProviderError;
|
|
117169
117254
|
if (networkError) {
|
|
117170
117255
|
return {
|
|
117171
117256
|
success: false,
|
|
117172
117257
|
output: finalText,
|
|
117173
|
-
error: params.signal.aborted ? `activity timeout: the model went silent and the turn was aborted by the activity watchdog (${networkError})${diagnosis}` : `opencode prompt failed: ${networkError}${
|
|
117258
|
+
error: params.signal.aborted ? `activity timeout: the model went silent and the turn was aborted by the activity watchdog (${networkError})${diagnosis}` : `opencode prompt failed: ${networkError}${promptCause ? ` \u2014 likely cause: ${promptCause}` : ""}`,
|
|
117174
117259
|
usage
|
|
117175
117260
|
};
|
|
117176
117261
|
}
|
|
@@ -117362,15 +117447,14 @@ var opencode = agent({
|
|
|
117362
117447
|
const isBedrockRoute = rawModel !== void 0 && bedrockModelId !== void 0 && bedrockModelId === rawModel;
|
|
117363
117448
|
const vertexModel = resolveVertexOpenCodeModel(rawModel);
|
|
117364
117449
|
const model = vertexModel ?? (isBedrockRoute ? `amazon-bedrock/${rawModel}` : rawModel);
|
|
117365
|
-
const
|
|
117366
|
-
|
|
117367
|
-
|
|
117368
|
-
|
|
117369
|
-
const opencodePluginDir = join7(homeEnv.XDG_CONFIG_HOME, "opencode", "plugin");
|
|
117450
|
+
const codexAuth = installCodexAuth();
|
|
117451
|
+
const xaiAuth = installXaiAuth();
|
|
117452
|
+
const homeEnv = openCodeHomeEnv(ctx.tmpdir);
|
|
117453
|
+
const opencodePluginDir = join8(homeEnv.XDG_CONFIG_HOME, "opencode", "plugin");
|
|
117370
117454
|
mkdirSync7(opencodePluginDir, { recursive: true });
|
|
117371
117455
|
writeFileSync6(
|
|
117372
|
-
|
|
117373
|
-
buildOpencodeSubagentGateSource(ctx
|
|
117456
|
+
join8(opencodePluginDir, PULLFROG_OPENCODE_GATE_PLUGIN_FILENAME),
|
|
117457
|
+
buildOpencodeSubagentGateSource(ctx)
|
|
117374
117458
|
);
|
|
117375
117459
|
const agentBrowserVersion = getDevDependencyVersion("agent-browser");
|
|
117376
117460
|
addSkill({
|
|
@@ -117380,8 +117464,6 @@ var opencode = agent({
|
|
|
117380
117464
|
agent: "opencode"
|
|
117381
117465
|
});
|
|
117382
117466
|
installBundledSkills({ home: homeEnv.HOME });
|
|
117383
|
-
const codexAuth = installCodexAuth();
|
|
117384
|
-
const xaiAuth = installXaiAuth();
|
|
117385
117467
|
const permissionOverride = JSON.stringify({
|
|
117386
117468
|
external_directory: { "*": "deny", "/tmp/*": "allow" },
|
|
117387
117469
|
read: { "*": "allow", ...GIT_NATIVE_READ_DENY_OPENCODE },
|
|
@@ -117400,7 +117482,6 @@ var opencode = agent({
|
|
|
117400
117482
|
delete env2.OPENCODE_EXPERIMENTAL_CODE_MODE;
|
|
117401
117483
|
const writebacks = [];
|
|
117402
117484
|
if (codexAuth) {
|
|
117403
|
-
env2.XDG_DATA_HOME = codexAuth.xdgDataHome;
|
|
117404
117485
|
delete env2.OPENAI_API_KEY;
|
|
117405
117486
|
writebacks.push({
|
|
117406
117487
|
secretName: "CODEX_AUTH_JSON",
|
|
@@ -117411,7 +117492,6 @@ var opencode = agent({
|
|
|
117411
117492
|
});
|
|
117412
117493
|
}
|
|
117413
117494
|
if (xaiAuth) {
|
|
117414
|
-
env2.XDG_DATA_HOME = xaiAuth.xdgDataHome;
|
|
117415
117495
|
delete env2.XAI_API_KEY;
|
|
117416
117496
|
writebacks.push({
|
|
117417
117497
|
secretName: "GROK_AUTH_JSON",
|
|
@@ -117484,6 +117564,7 @@ var opencode = agent({
|
|
|
117484
117564
|
mcpToolCalls: 0,
|
|
117485
117565
|
recentStderr: server.recentStderr,
|
|
117486
117566
|
serverExitCause: server.exitCause,
|
|
117567
|
+
lastSessionError: void 0,
|
|
117487
117568
|
diagnostic: {
|
|
117488
117569
|
label: "Pullfrog",
|
|
117489
117570
|
recentStderr: server.recentStderr,
|
|
@@ -172402,7 +172483,7 @@ function closeBrowserDaemon(toolState) {
|
|
|
172402
172483
|
// mcp/checkout.ts
|
|
172403
172484
|
import { createHash as createHash2 } from "node:crypto";
|
|
172404
172485
|
import { statSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync10 } from "node:fs";
|
|
172405
|
-
import { join as
|
|
172486
|
+
import { join as join16 } from "node:path";
|
|
172406
172487
|
|
|
172407
172488
|
// node_modules/.pnpm/@ark+schema@0.56.0/node_modules/@ark/schema/out/shared/errors.js
|
|
172408
172489
|
var ArkError = class _ArkError extends CastableBase {
|
|
@@ -180773,13 +180854,14 @@ function ensureRepoState(toolState, init) {
|
|
|
180773
180854
|
// utils/changeImpact.ts
|
|
180774
180855
|
import { spawn as spawn2 } from "node:child_process";
|
|
180775
180856
|
import { writeFileSync as writeFileSync7 } from "node:fs";
|
|
180776
|
-
import { join as
|
|
180857
|
+
import { join as join9 } from "node:path";
|
|
180777
180858
|
var MAX_CANDIDATES = 24;
|
|
180778
180859
|
var MAX_ATOM_LENGTH = 128;
|
|
180779
180860
|
var MAX_ATOMS = 12;
|
|
180780
180861
|
var MAX_REFERENCES = 6;
|
|
180781
180862
|
var MAX_EXCERPT = 160;
|
|
180782
180863
|
var MAX_BYTES = 16e3;
|
|
180864
|
+
var LOOKUP_BUDGET_MS = 6e4;
|
|
180783
180865
|
function isSymbolShaped(atom) {
|
|
180784
180866
|
return atom.length >= 4 && atom.length <= MAX_ATOM_LENGTH && /[A-Z_]/.test(atom);
|
|
180785
180867
|
}
|
|
@@ -180982,7 +181064,7 @@ function consumeGrepChunk(params) {
|
|
|
180982
181064
|
async function lookupReferences(params) {
|
|
180983
181065
|
const buckets = emptyBuckets(params.candidates);
|
|
180984
181066
|
if (params.candidates.length === 0) return { buckets, error: void 0 };
|
|
180985
|
-
const
|
|
181067
|
+
const pattern = params.candidates.map((candidate) => candidate.atom).join("|");
|
|
180986
181068
|
const child = spawn2(
|
|
180987
181069
|
"git",
|
|
180988
181070
|
[
|
|
@@ -180993,13 +181075,14 @@ async function lookupReferences(params) {
|
|
|
180993
181075
|
"-z",
|
|
180994
181076
|
"-I",
|
|
180995
181077
|
"-w",
|
|
180996
|
-
"-
|
|
180997
|
-
|
|
181078
|
+
"-E",
|
|
181079
|
+
"-e",
|
|
181080
|
+
pattern,
|
|
180998
181081
|
params.treeish,
|
|
180999
181082
|
"--",
|
|
181000
181083
|
":(top)"
|
|
181001
181084
|
],
|
|
181002
|
-
{ env: resolveEnv(void 0), stdio: ["ignore", "pipe", "pipe"] }
|
|
181085
|
+
{ env: resolveEnv(void 0), stdio: ["ignore", "pipe", "pipe"], signal: params.signal }
|
|
181003
181086
|
);
|
|
181004
181087
|
const state = createGrepRecordState();
|
|
181005
181088
|
const maxAtomLength = Math.max(...params.candidates.map((candidate) => candidate.atom.length));
|
|
@@ -181019,7 +181102,7 @@ async function lookupReferences(params) {
|
|
|
181019
181102
|
child.stderr.resume();
|
|
181020
181103
|
const status = await new Promise((resolve3) => {
|
|
181021
181104
|
child.once("error", (error49) => {
|
|
181022
|
-
spawnError = error49.message;
|
|
181105
|
+
spawnError = error49.cause instanceof Error ? error49.cause.message : error49.message;
|
|
181023
181106
|
resolve3(null);
|
|
181024
181107
|
});
|
|
181025
181108
|
child.once("close", resolve3);
|
|
@@ -181107,8 +181190,10 @@ async function createChangeImpactArtifact(ctx, params) {
|
|
|
181107
181190
|
const lookup2 = await lookupReferences({
|
|
181108
181191
|
candidates: candidates.selected,
|
|
181109
181192
|
addedLines: candidates.addedLines,
|
|
181110
|
-
treeish: params.headSha
|
|
181193
|
+
treeish: params.headSha,
|
|
181194
|
+
signal: AbortSignal.any([params.signal, AbortSignal.timeout(LOOKUP_BUDGET_MS)])
|
|
181111
181195
|
});
|
|
181196
|
+
params.signal.throwIfAborted();
|
|
181112
181197
|
const artifact = formatArtifact({
|
|
181113
181198
|
entries: candidates.selected.map((change) => {
|
|
181114
181199
|
return { change, bucket: lookup2.buckets.get(change.atom) ?? { count: 0, samples: [] } };
|
|
@@ -181120,7 +181205,7 @@ async function createChangeImpactArtifact(ctx, params) {
|
|
|
181120
181205
|
headSha: params.headSha,
|
|
181121
181206
|
lookupError: lookup2.error
|
|
181122
181207
|
});
|
|
181123
|
-
const path4 =
|
|
181208
|
+
const path4 = join9(ctx.tmpdir, `pr-${params.pullNumber}-${params.headSha.slice(0, 7)}-impact.md`);
|
|
181124
181209
|
writeFileSync7(path4, artifact.content);
|
|
181125
181210
|
return {
|
|
181126
181211
|
path: path4,
|
|
@@ -181374,7 +181459,7 @@ function readNumber(params) {
|
|
|
181374
181459
|
import { execSync } from "node:child_process";
|
|
181375
181460
|
import { createHash } from "node:crypto";
|
|
181376
181461
|
import { readFileSync as readFileSync4, realpathSync, unlinkSync } from "node:fs";
|
|
181377
|
-
import { join as
|
|
181462
|
+
import { join as join10 } from "node:path";
|
|
181378
181463
|
|
|
181379
181464
|
// utils/shell.ts
|
|
181380
181465
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
@@ -181464,7 +181549,7 @@ function resolveHooksDir(cwd, gitPath) {
|
|
|
181464
181549
|
cwd,
|
|
181465
181550
|
log: false
|
|
181466
181551
|
}).trim();
|
|
181467
|
-
const hooksDir =
|
|
181552
|
+
const hooksDir = join10(commonDir, "hooks");
|
|
181468
181553
|
hooksDirCache.set(cwd, hooksDir);
|
|
181469
181554
|
return hooksDir;
|
|
181470
181555
|
}
|
|
@@ -181511,7 +181596,8 @@ async function $git(subcommand, args2, options) {
|
|
|
181511
181596
|
GIT_CONFIG_COUNT: "0",
|
|
181512
181597
|
GIT_CONFIG_PARAMETERS: ""
|
|
181513
181598
|
},
|
|
181514
|
-
activityTimeout: 0
|
|
181599
|
+
activityTimeout: 0,
|
|
181600
|
+
signal: options.signal
|
|
181515
181601
|
});
|
|
181516
181602
|
if (result.stderr.includes("askpass-compromised")) {
|
|
181517
181603
|
log.info("askpass code was replayed after revoke \u2014 token has been revoked");
|
|
@@ -181578,9 +181664,10 @@ import { spawnSync as spawnSync7 } from "node:child_process";
|
|
|
181578
181664
|
// mcp/shell.ts
|
|
181579
181665
|
import { spawn as spawn3, spawnSync as spawnSync6 } from "node:child_process";
|
|
181580
181666
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
181581
|
-
import {
|
|
181667
|
+
import { once } from "node:events";
|
|
181668
|
+
import { closeSync, existsSync as existsSync7, openSync, writeFileSync as writeFileSync8 } from "node:fs";
|
|
181582
181669
|
import { userInfo as userInfo2 } from "node:os";
|
|
181583
|
-
import { join as
|
|
181670
|
+
import { join as join11 } from "node:path";
|
|
181584
181671
|
import { setTimeout as sleep3 } from "node:timers/promises";
|
|
181585
181672
|
|
|
181586
181673
|
// node_modules/.pnpm/@toon-format+toon@1.4.0/node_modules/@toon-format/toon/dist/index.mjs
|
|
@@ -182224,7 +182311,7 @@ function buildFsMounts(repoDir) {
|
|
|
182224
182311
|
function shimMount() {
|
|
182225
182312
|
const tempDir = process.env.PULLFROG_TEMP_DIR;
|
|
182226
182313
|
if (!tempDir) return "";
|
|
182227
|
-
const dir =
|
|
182314
|
+
const dir = join11(tempDir, "pm-bin").replace(/'/g, "'\\''");
|
|
182228
182315
|
return `[ -d '${dir}' ] && mount --bind '${dir}' '${dir}' 2>/dev/null && mount -o remount,bind,ro '${dir}' 2>/dev/null;`;
|
|
182229
182316
|
}
|
|
182230
182317
|
var _repoRoot;
|
|
@@ -182395,7 +182482,7 @@ ${spawnError}` : spawnError;
|
|
|
182395
182482
|
}
|
|
182396
182483
|
function capOutput(output) {
|
|
182397
182484
|
if (output.length <= MAX_OUTPUT_CHARS) return output;
|
|
182398
|
-
const fullPath =
|
|
182485
|
+
const fullPath = join11(getTempDir(), `shell-${randomUUID3().slice(0, 8)}.log`);
|
|
182399
182486
|
writeFileSync8(fullPath, output);
|
|
182400
182487
|
const elided = output.length - MAX_OUTPUT_CHARS;
|
|
182401
182488
|
return `... [${elided} chars truncated; output saved to ${fullPath}] ...
|
|
@@ -182432,6 +182519,7 @@ Do NOT use this tool for git commands \u2014 use the dedicated git tools instead
|
|
|
182432
182519
|
}
|
|
182433
182520
|
const timeout = Math.min(params.timeout ?? 3e4, 12e4);
|
|
182434
182521
|
const cwd = params.working_directory ?? process.cwd();
|
|
182522
|
+
if (!existsSync7(cwd)) throw new Error(`working_directory does not exist: ${cwd}`);
|
|
182435
182523
|
const env2 = resolveEnv(ctx.payload.shell === "enabled" ? "inherit" : "restricted");
|
|
182436
182524
|
if (params.command.includes("agent-browser")) {
|
|
182437
182525
|
const daemonError = ensureBrowserDaemon(ctx.toolState);
|
|
@@ -182450,8 +182538,8 @@ Do NOT use this tool for git commands \u2014 use the dedicated git tools instead
|
|
|
182450
182538
|
if (params.background) {
|
|
182451
182539
|
const tempDir = getTempDir();
|
|
182452
182540
|
const handle = `bg-${randomUUID3().slice(0, 8)}`;
|
|
182453
|
-
const outputPath =
|
|
182454
|
-
const pidPath =
|
|
182541
|
+
const outputPath = join11(tempDir, `${handle}.log`);
|
|
182542
|
+
const pidPath = join11(tempDir, `${handle}.pid`);
|
|
182455
182543
|
const logFd = openSync(outputPath, "a");
|
|
182456
182544
|
let proc;
|
|
182457
182545
|
try {
|
|
@@ -182465,7 +182553,8 @@ Do NOT use this tool for git commands \u2014 use the dedicated git tools instead
|
|
|
182465
182553
|
closeSync(logFd);
|
|
182466
182554
|
}
|
|
182467
182555
|
if (!proc.pid) {
|
|
182468
|
-
|
|
182556
|
+
const [error49] = await once(proc, "error");
|
|
182557
|
+
throw new Error(`failed to start background process: ${error49.message}`);
|
|
182469
182558
|
}
|
|
182470
182559
|
proc.unref();
|
|
182471
182560
|
writeFileSync8(pidPath, `${proc.pid}
|
|
@@ -182742,12 +182831,12 @@ function postProcessRangeDiff(raw2, contextLines = 3) {
|
|
|
182742
182831
|
// mcp/git.ts
|
|
182743
182832
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
182744
182833
|
import { writeFileSync as writeFileSync9 } from "node:fs";
|
|
182745
|
-
import { join as
|
|
182834
|
+
import { join as join14 } from "node:path";
|
|
182746
182835
|
|
|
182747
182836
|
// utils/apiCommit.ts
|
|
182748
182837
|
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
182749
182838
|
import { lstat, readlink } from "node:fs/promises";
|
|
182750
|
-
import { join as
|
|
182839
|
+
import { join as join12 } from "node:path";
|
|
182751
182840
|
var GITHUB_API = "https://api.github.com";
|
|
182752
182841
|
var MAX_BLOB_BYTES = 30 * 1024 * 1024;
|
|
182753
182842
|
var BLOB_UPLOAD_CONCURRENCY = 8;
|
|
@@ -182812,7 +182901,7 @@ async function assertApiCommittable(files) {
|
|
|
182812
182901
|
const root = getRepoRoot();
|
|
182813
182902
|
const attrs = $(
|
|
182814
182903
|
"git",
|
|
182815
|
-
["check-attr", "filter", "-z", "--", ...present.map((p) =>
|
|
182904
|
+
["check-attr", "filter", "-z", "--", ...present.map((p) => join12(root, p))],
|
|
182816
182905
|
{ log: false }
|
|
182817
182906
|
);
|
|
182818
182907
|
const parts = attrs.split("\0");
|
|
@@ -182824,7 +182913,7 @@ async function assertApiCommittable(files) {
|
|
|
182824
182913
|
}
|
|
182825
182914
|
}
|
|
182826
182915
|
for (const path4 of present) {
|
|
182827
|
-
const stat = await lstat(
|
|
182916
|
+
const stat = await lstat(join12(root, path4));
|
|
182828
182917
|
if (stat.isDirectory()) {
|
|
182829
182918
|
throw new Error(
|
|
182830
182919
|
`'${path4}' is a directory (nested repository or submodule?) \u2014 signed commits only support files and symlinks.`
|
|
@@ -182833,7 +182922,7 @@ async function assertApiCommittable(files) {
|
|
|
182833
182922
|
}
|
|
182834
182923
|
}
|
|
182835
182924
|
async function createBlobEntry(params) {
|
|
182836
|
-
const absPath =
|
|
182925
|
+
const absPath = join12(params.repoRoot, params.path);
|
|
182837
182926
|
const stat = await lstat(absPath);
|
|
182838
182927
|
if (stat.size > MAX_BLOB_BYTES) {
|
|
182839
182928
|
throw new Error(
|
|
@@ -183013,7 +183102,7 @@ this usually means your local branch has commits that were never pushed \u2014 s
|
|
|
183013
183102
|
var core4 = __toESM(require_core(), 1);
|
|
183014
183103
|
import { createSign } from "node:crypto";
|
|
183015
183104
|
import { rename, writeFile } from "node:fs/promises";
|
|
183016
|
-
import { dirname as dirname3, join as
|
|
183105
|
+
import { dirname as dirname3, join as join13 } from "node:path";
|
|
183017
183106
|
|
|
183018
183107
|
// node_modules/.pnpm/@octokit+plugin-throttling@11.0.5_@octokit+core@7.0.8/node_modules/@octokit/plugin-throttling/dist-bundle/index.js
|
|
183019
183108
|
var import_light = __toESM(require_light(), 1);
|
|
@@ -187562,7 +187651,7 @@ function getGitHubUsageSummary() {
|
|
|
187562
187651
|
}
|
|
187563
187652
|
async function writeGitHubUsageSummaryToFile(path4) {
|
|
187564
187653
|
const summary2 = getGitHubUsageSummary();
|
|
187565
|
-
const tmpPath =
|
|
187654
|
+
const tmpPath = join13(dirname3(path4), `.usage-summary-${process.pid}.tmp`);
|
|
187566
187655
|
await writeFile(tmpPath, JSON.stringify(summary2));
|
|
187567
187656
|
await rename(tmpPath, path4);
|
|
187568
187657
|
}
|
|
@@ -188190,7 +188279,7 @@ function countAhead(head, base, cwd) {
|
|
|
188190
188279
|
function spillGitOutput(params) {
|
|
188191
188280
|
const tempDir = process.env.PULLFROG_TEMP_DIR;
|
|
188192
188281
|
if (!tempDir) throw new Error("PULLFROG_TEMP_DIR not set");
|
|
188193
|
-
const outputPath =
|
|
188282
|
+
const outputPath = join14(tempDir, `git-${params.command}-${randomUUID4().slice(0, 8)}.txt`);
|
|
188194
188283
|
writeFileSync9(outputPath, params.output);
|
|
188195
188284
|
const previewByLines = params.output.split("\n").slice(0, OVERFLOW_PREVIEW_LINES).join("\n");
|
|
188196
188285
|
const preview = previewByLines.length <= OVERFLOW_PREVIEW_MAX_CHARS ? previewByLines : `${previewByLines.slice(0, OVERFLOW_PREVIEW_MAX_CHARS)}\u2026`;
|
|
@@ -189845,11 +189934,11 @@ async function reportReviewNodeId(ctx, params) {
|
|
|
189845
189934
|
|
|
189846
189935
|
// utils/setup.ts
|
|
189847
189936
|
import { execFileSync as execFileSync6, execSync as execSync2 } from "node:child_process";
|
|
189848
|
-
import { mkdtempSync, readdirSync, realpathSync as realpathSync2, rmSync as rmSync2, unlinkSync as unlinkSync2 } from "node:fs";
|
|
189937
|
+
import { mkdtempSync as mkdtempSync2, readdirSync, realpathSync as realpathSync2, rmSync as rmSync2, unlinkSync as unlinkSync2 } from "node:fs";
|
|
189849
189938
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
189850
|
-
import { join as
|
|
189939
|
+
import { join as join15 } from "node:path";
|
|
189851
189940
|
function createTempDirectory() {
|
|
189852
|
-
const path4 =
|
|
189941
|
+
const path4 = mkdtempSync2(join15(tmpdir2(), "pullfrog-"));
|
|
189853
189942
|
process.env.PULLFROG_TEMP_DIR = path4;
|
|
189854
189943
|
log.info(`\xBB created temp dir at ${path4}`);
|
|
189855
189944
|
return { path: path4, [Symbol.dispose]: () => removeTempDirectory(path4) };
|
|
@@ -189909,13 +189998,13 @@ function wipeRunnerLeakSurface() {
|
|
|
189909
189998
|
return [];
|
|
189910
189999
|
}
|
|
189911
190000
|
};
|
|
189912
|
-
const fileCommandsDir =
|
|
190001
|
+
const fileCommandsDir = join15(runnerTemp, "_runner_file_commands");
|
|
189913
190002
|
for (const entry of listDir(fileCommandsDir)) {
|
|
189914
|
-
tryUnlink(
|
|
190003
|
+
tryUnlink(join15(fileCommandsDir, entry));
|
|
189915
190004
|
}
|
|
189916
190005
|
for (const entry of listDir(runnerTemp)) {
|
|
189917
190006
|
if (entry.endsWith(".sh") || /^git-credentials-.*\.config$/.test(entry)) {
|
|
189918
|
-
tryUnlink(
|
|
190007
|
+
tryUnlink(join15(runnerTemp, entry));
|
|
189919
190008
|
}
|
|
189920
190009
|
}
|
|
189921
190010
|
if (wiped.length > 0) {
|
|
@@ -190187,7 +190276,7 @@ async function ensureBeforeShaReachable(params) {
|
|
|
190187
190276
|
"origin",
|
|
190188
190277
|
tempBranch
|
|
190189
190278
|
],
|
|
190190
|
-
{ token: params.gitToken, refreshGitToken: params.refreshGitToken },
|
|
190279
|
+
{ token: params.gitToken, refreshGitToken: params.refreshGitToken, signal: params.signal },
|
|
190191
190280
|
`before_sha temp branch ${tempBranch}`
|
|
190192
190281
|
);
|
|
190193
190282
|
log.debug(`\xBB fetched before_sha via temp branch ${tempBranch}`);
|
|
@@ -190228,7 +190317,7 @@ function cleanupStaleGitLocks() {
|
|
|
190228
190317
|
} catch {
|
|
190229
190318
|
continue;
|
|
190230
190319
|
}
|
|
190231
|
-
if (now - mtimeMs < STALE_LOCK_AGE_MS) continue;
|
|
190320
|
+
if (now - mtimeMs < STALE_LOCK_AGE_MS || mtimeMs >= performance.timeOrigin) continue;
|
|
190232
190321
|
try {
|
|
190233
190322
|
unlinkSync3(relPath);
|
|
190234
190323
|
log.warning(`\xBB removed stale ${relPath} from prior run`);
|
|
@@ -190261,7 +190350,7 @@ async function abortIfPullRequestMoved(args2) {
|
|
|
190261
190350
|
);
|
|
190262
190351
|
}
|
|
190263
190352
|
async function checkoutPrBranch(pr, params) {
|
|
190264
|
-
const { octokit, owner, name, gitToken, refreshGitToken, toolState, beforeSha } = params;
|
|
190353
|
+
const { octokit, owner, name, gitToken, refreshGitToken, toolState, beforeSha, signal } = params;
|
|
190265
190354
|
rejectIfLeadingDash(pr.baseRef, "PR base ref");
|
|
190266
190355
|
rejectIfLeadingDash(pr.headRef, "PR head ref");
|
|
190267
190356
|
const repoState = requireRepoState(toolState, owner, name);
|
|
@@ -190275,7 +190364,7 @@ async function checkoutPrBranch(pr, params) {
|
|
|
190275
190364
|
log.debug(`\xBB fetching base branch (${pr.baseRef})...`);
|
|
190276
190365
|
await $gitFetchWithDeepen(
|
|
190277
190366
|
["--no-tags", "origin", pr.baseRef],
|
|
190278
|
-
{ token: gitToken, refreshGitToken },
|
|
190367
|
+
{ token: gitToken, refreshGitToken, signal },
|
|
190279
190368
|
`base branch ${pr.baseRef}`
|
|
190280
190369
|
);
|
|
190281
190370
|
if (!alreadyOnBranch) {
|
|
@@ -190286,7 +190375,7 @@ async function checkoutPrBranch(pr, params) {
|
|
|
190286
190375
|
try {
|
|
190287
190376
|
await $gitFetchWithDeepen(
|
|
190288
190377
|
["--no-tags", "origin", `+pull/${pr.number}/head:${localBranch}`],
|
|
190289
|
-
{ token: gitToken, refreshGitToken },
|
|
190378
|
+
{ token: gitToken, refreshGitToken, signal },
|
|
190290
190379
|
`PR #${pr.number}`
|
|
190291
190380
|
);
|
|
190292
190381
|
} catch (e) {
|
|
@@ -190311,7 +190400,8 @@ async function checkoutPrBranch(pr, params) {
|
|
|
190311
190400
|
repo: name,
|
|
190312
190401
|
gitToken,
|
|
190313
190402
|
refreshGitToken,
|
|
190314
|
-
isShallow
|
|
190403
|
+
isShallow,
|
|
190404
|
+
signal
|
|
190315
190405
|
}) : false;
|
|
190316
190406
|
if (isShallow) {
|
|
190317
190407
|
let deepenDepth = 0;
|
|
@@ -190346,7 +190436,8 @@ async function checkoutPrBranch(pr, params) {
|
|
|
190346
190436
|
if (deepenDepth) {
|
|
190347
190437
|
log.debug(`\xBB deepening by ${deepenDepth} to reach merge base...`);
|
|
190348
190438
|
await $git("fetch", [`--deepen=${deepenDepth}`, "--no-tags", "origin"], {
|
|
190349
|
-
token: gitToken
|
|
190439
|
+
token: gitToken,
|
|
190440
|
+
signal
|
|
190350
190441
|
});
|
|
190351
190442
|
}
|
|
190352
190443
|
}
|
|
@@ -190384,6 +190475,7 @@ async function checkoutPrBranch(pr, params) {
|
|
|
190384
190475
|
remoteBranch: pr.headRef,
|
|
190385
190476
|
localBranch
|
|
190386
190477
|
};
|
|
190478
|
+
signal.throwIfAborted();
|
|
190387
190479
|
const postCheckoutHook = await executeLifecycleHook({
|
|
190388
190480
|
event: "post-checkout",
|
|
190389
190481
|
script: params.postCheckoutScript,
|
|
@@ -190394,19 +190486,30 @@ async function checkoutPrBranch(pr, params) {
|
|
|
190394
190486
|
}
|
|
190395
190487
|
var inFlightCheckouts = /* @__PURE__ */ new Map();
|
|
190396
190488
|
var CHECKOUT_DEADLINE_MS = 57e4;
|
|
190397
|
-
|
|
190398
|
-
|
|
190399
|
-
const
|
|
190400
|
-
|
|
190489
|
+
var CHECKOUT_ABORT_GRACE_MS = 15e3;
|
|
190490
|
+
function withCheckoutDeadline(run, pullNumber) {
|
|
190491
|
+
const exceeded = `checkout_pr #${pullNumber} exceeded ${CHECKOUT_DEADLINE_MS / 1e3}s`;
|
|
190492
|
+
const retry = "retry the call to start a fresh attempt.";
|
|
190493
|
+
const controller = new AbortController();
|
|
190494
|
+
const abortTimer = setTimeout(
|
|
190495
|
+
() => controller.abort(new Error(`${exceeded} and was cancelled; ${retry}`)),
|
|
190496
|
+
CHECKOUT_DEADLINE_MS
|
|
190497
|
+
);
|
|
190498
|
+
let backstopTimer;
|
|
190499
|
+
const backstop = new Promise((_, reject) => {
|
|
190500
|
+
backstopTimer = setTimeout(
|
|
190401
190501
|
() => reject(
|
|
190402
190502
|
new Error(
|
|
190403
|
-
|
|
190503
|
+
`${exceeded} and was asked to cancel, but a step that cannot be interrupted (the post-checkout hook or a GitHub API call) was still running ${CHECKOUT_ABORT_GRACE_MS / 1e3}s later. ${retry}`
|
|
190404
190504
|
)
|
|
190405
190505
|
),
|
|
190406
|
-
CHECKOUT_DEADLINE_MS
|
|
190506
|
+
CHECKOUT_DEADLINE_MS + CHECKOUT_ABORT_GRACE_MS
|
|
190407
190507
|
);
|
|
190408
190508
|
});
|
|
190409
|
-
return Promise.race([
|
|
190509
|
+
return Promise.race([run(controller.signal), backstop]).finally(() => {
|
|
190510
|
+
clearTimeout(abortTimer);
|
|
190511
|
+
clearTimeout(backstopTimer);
|
|
190512
|
+
});
|
|
190410
190513
|
}
|
|
190411
190514
|
function headsEqual(a, b) {
|
|
190412
190515
|
if (a.kind === "branch" && b.kind === "branch") return a.name === b.name;
|
|
@@ -190418,7 +190521,7 @@ function describeHead(h) {
|
|
|
190418
190521
|
return `detached HEAD \`${h.sha}\``;
|
|
190419
190522
|
}
|
|
190420
190523
|
function CheckoutPrTool(ctx) {
|
|
190421
|
-
const runCheckout = async (pull_number) => {
|
|
190524
|
+
const runCheckout = async (pull_number, signal) => {
|
|
190422
190525
|
const prResponse = await ctx.octokit.rest.pulls.get({
|
|
190423
190526
|
owner: ctx.repo.owner,
|
|
190424
190527
|
repo: ctx.repo.name,
|
|
@@ -190447,7 +190550,8 @@ function CheckoutPrTool(ctx) {
|
|
|
190447
190550
|
toolState: ctx.toolState,
|
|
190448
190551
|
shell: ctx.payload.shell,
|
|
190449
190552
|
postCheckoutScript: ctx.postCheckoutScript,
|
|
190450
|
-
beforeSha: primary.beforeSha
|
|
190553
|
+
beforeSha: primary.beforeSha,
|
|
190554
|
+
signal
|
|
190451
190555
|
});
|
|
190452
190556
|
const tempDir = process.env.PULLFROG_TEMP_DIR;
|
|
190453
190557
|
if (!tempDir) {
|
|
@@ -190469,7 +190573,7 @@ function CheckoutPrTool(ctx) {
|
|
|
190469
190573
|
headSha: checkoutSha
|
|
190470
190574
|
});
|
|
190471
190575
|
if (incremental.status === "ok") {
|
|
190472
|
-
incrementalDiffPath =
|
|
190576
|
+
incrementalDiffPath = join16(
|
|
190473
190577
|
tempDir,
|
|
190474
190578
|
`pr-${pull_number}-${beforeShort}-${headShort}-incremental.diff`
|
|
190475
190579
|
);
|
|
@@ -190486,7 +190590,7 @@ function CheckoutPrTool(ctx) {
|
|
|
190486
190590
|
const diffPreview = formatResult.content.split("\n").slice(0, 100).join("\n");
|
|
190487
190591
|
log.debug(`formatted diff preview (first 100 lines):
|
|
190488
190592
|
${diffPreview}`);
|
|
190489
|
-
const diffPath =
|
|
190593
|
+
const diffPath = join16(tempDir, `pr-${pull_number}-${headShort}.diff`);
|
|
190490
190594
|
writeFileSync10(diffPath, formatResult.content);
|
|
190491
190595
|
log.debug(`wrote diff to ${diffPath} (${formatResult.content.length} bytes)`);
|
|
190492
190596
|
primary.diffCoverage = createDiffCoverageState({
|
|
@@ -190526,7 +190630,8 @@ ${diffPreview}`);
|
|
|
190526
190630
|
files: formatResult.files,
|
|
190527
190631
|
pullNumber: pull_number,
|
|
190528
190632
|
baseSha: prResponse.data.base.sha,
|
|
190529
|
-
headSha: checkoutSha
|
|
190633
|
+
headSha: checkoutSha,
|
|
190634
|
+
signal
|
|
190530
190635
|
});
|
|
190531
190636
|
impactPath = impact.path;
|
|
190532
190637
|
log.info(
|
|
@@ -190632,7 +190737,10 @@ ${dirty}`
|
|
|
190632
190737
|
);
|
|
190633
190738
|
}
|
|
190634
190739
|
}
|
|
190635
|
-
const promise2 = withCheckoutDeadline(
|
|
190740
|
+
const promise2 = withCheckoutDeadline(
|
|
190741
|
+
(signal) => runCheckout(pull_number, signal),
|
|
190742
|
+
pull_number
|
|
190743
|
+
);
|
|
190636
190744
|
inFlightCheckouts.set(pull_number, promise2);
|
|
190637
190745
|
try {
|
|
190638
190746
|
return await promise2;
|
|
@@ -190645,7 +190753,7 @@ ${dirty}`
|
|
|
190645
190753
|
|
|
190646
190754
|
// mcp/checkSuite.ts
|
|
190647
190755
|
import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync11 } from "node:fs";
|
|
190648
|
-
import { join as
|
|
190756
|
+
import { join as join17 } from "node:path";
|
|
190649
190757
|
var GetCheckSuiteLogs = type({
|
|
190650
190758
|
check_suite_id: type.number.describe("the id from check_suite.id")
|
|
190651
190759
|
});
|
|
@@ -190741,7 +190849,7 @@ function GetCheckSuiteLogsTool(ctx) {
|
|
|
190741
190849
|
if (!tempDir) {
|
|
190742
190850
|
throw new Error("PULLFROG_TEMP_DIR not set");
|
|
190743
190851
|
}
|
|
190744
|
-
const logsDir =
|
|
190852
|
+
const logsDir = join17(tempDir, "ci-logs");
|
|
190745
190853
|
mkdirSync8(logsDir, { recursive: true });
|
|
190746
190854
|
const jobResults = [];
|
|
190747
190855
|
for (const run of failedRuns) {
|
|
@@ -190768,7 +190876,7 @@ function GetCheckSuiteLogsTool(ctx) {
|
|
|
190768
190876
|
);
|
|
190769
190877
|
}
|
|
190770
190878
|
const logsText = await logsResult.text();
|
|
190771
|
-
const logPath =
|
|
190879
|
+
const logPath = join17(logsDir, `job-${job.id}.log`);
|
|
190772
190880
|
writeFileSync11(logPath, logsText);
|
|
190773
190881
|
const analysis = analyzeLog(logsText, 80);
|
|
190774
190882
|
const failedSteps = job.steps?.filter((s) => s.conclusion === "failure").map((s) => `Step ${s.number}: ${s.name}`) ?? [];
|
|
@@ -190818,7 +190926,7 @@ function GetCheckSuiteLogsTool(ctx) {
|
|
|
190818
190926
|
|
|
190819
190927
|
// mcp/commitInfo.ts
|
|
190820
190928
|
import { writeFileSync as writeFileSync12 } from "node:fs";
|
|
190821
|
-
import { join as
|
|
190929
|
+
import { join as join18 } from "node:path";
|
|
190822
190930
|
var CommitInfo = type({
|
|
190823
190931
|
sha: type.string.describe("the commit SHA (full or abbreviated) to fetch")
|
|
190824
190932
|
});
|
|
@@ -190842,7 +190950,7 @@ function CommitInfoTool(ctx) {
|
|
|
190842
190950
|
"PULLFROG_TEMP_DIR not set - get_commit_info must run in pullfrog action context"
|
|
190843
190951
|
);
|
|
190844
190952
|
}
|
|
190845
|
-
const diffFile =
|
|
190953
|
+
const diffFile = join18(tempDir, `commit-${sha.slice(0, 7)}.diff`);
|
|
190846
190954
|
writeFileSync12(diffFile, formatResult.content);
|
|
190847
190955
|
log.debug(`wrote commit diff to ${diffFile} (${formatResult.content.length} bytes)`);
|
|
190848
190956
|
return {
|
|
@@ -190979,14 +191087,14 @@ function ReopenCurrentTool(ctx) {
|
|
|
190979
191087
|
import { performance as performance8 } from "node:perf_hooks";
|
|
190980
191088
|
|
|
190981
191089
|
// prep/installNodeDependencies.ts
|
|
190982
|
-
import { existsSync as
|
|
190983
|
-
import { join as
|
|
191090
|
+
import { existsSync as existsSync9 } from "node:fs";
|
|
191091
|
+
import { join as join20 } from "node:path";
|
|
190984
191092
|
|
|
190985
191093
|
// utils/packageManager.ts
|
|
190986
191094
|
var import_semver2 = __toESM(require_semver2(), 1);
|
|
190987
|
-
import { existsSync as
|
|
191095
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
190988
191096
|
import { mkdir, readFile as readFile2 } from "node:fs/promises";
|
|
190989
|
-
import { delimiter, join as
|
|
191097
|
+
import { delimiter, join as join19 } from "node:path";
|
|
190990
191098
|
var SUPPORTED_NAMES = ["npm", "pnpm", "yarn", "bun"];
|
|
190991
191099
|
var COREPACK_MANAGED = ["pnpm", "yarn"];
|
|
190992
191100
|
function isSupported(name) {
|
|
@@ -191024,8 +191132,8 @@ function parseDevEnginesField(field) {
|
|
|
191024
191132
|
};
|
|
191025
191133
|
}
|
|
191026
191134
|
async function resolvePackageManagerSpec(cwd) {
|
|
191027
|
-
const pkgPath =
|
|
191028
|
-
if (!
|
|
191135
|
+
const pkgPath = join19(cwd, "package.json");
|
|
191136
|
+
if (!existsSync8(pkgPath)) return null;
|
|
191029
191137
|
let pkg;
|
|
191030
191138
|
try {
|
|
191031
191139
|
pkg = JSON.parse(await readFile2(pkgPath, "utf-8"));
|
|
@@ -191083,7 +191191,7 @@ async function currentVersion(name) {
|
|
|
191083
191191
|
return result.stdout.trim();
|
|
191084
191192
|
}
|
|
191085
191193
|
function packageManagerBinDir(tmpdir3) {
|
|
191086
|
-
return
|
|
191194
|
+
return join19(tmpdir3, "pm-bin");
|
|
191087
191195
|
}
|
|
191088
191196
|
async function ensurePackageManager(params) {
|
|
191089
191197
|
const spec = params.spec;
|
|
@@ -191146,7 +191254,7 @@ async function installFallback(name, installSpec) {
|
|
|
191146
191254
|
});
|
|
191147
191255
|
if (result.exitCode !== 0) return result.stderr || `failed to install ${name}`;
|
|
191148
191256
|
if (name === "deno") {
|
|
191149
|
-
const denoPath =
|
|
191257
|
+
const denoPath = join19(process.env.HOME || "", ".deno", "bin");
|
|
191150
191258
|
process.env.PATH = `${denoPath}:${process.env.PATH}`;
|
|
191151
191259
|
}
|
|
191152
191260
|
log.info(`\xBB installed ${name}`);
|
|
@@ -191179,8 +191287,8 @@ async function isCommandAvailable2(command) {
|
|
|
191179
191287
|
var installNodeDependencies = {
|
|
191180
191288
|
name: "installNodeDependencies",
|
|
191181
191289
|
shouldRun: () => {
|
|
191182
|
-
const packageJsonPath =
|
|
191183
|
-
return
|
|
191290
|
+
const packageJsonPath = join20(process.cwd(), "package.json");
|
|
191291
|
+
return existsSync9(packageJsonPath);
|
|
191184
191292
|
},
|
|
191185
191293
|
run: async (options) => {
|
|
191186
191294
|
const declared = await resolvePackageManagerSpec(process.cwd());
|
|
@@ -191274,14 +191382,14 @@ ${errorMessage}`]
|
|
|
191274
191382
|
};
|
|
191275
191383
|
|
|
191276
191384
|
// prep/installPythonDependencies.ts
|
|
191277
|
-
import { existsSync as
|
|
191278
|
-
import { join as
|
|
191385
|
+
import { existsSync as existsSync10, readFileSync as readFileSync5 } from "node:fs";
|
|
191386
|
+
import { join as join21 } from "node:path";
|
|
191279
191387
|
function declaresBuildSystem(path4) {
|
|
191280
191388
|
return /^\s*\[\s*build-system\s*\]/m.test(readFileSync5(path4, "utf8"));
|
|
191281
191389
|
}
|
|
191282
191390
|
function configApplies(config3, cwd) {
|
|
191283
|
-
const path4 =
|
|
191284
|
-
return
|
|
191391
|
+
const path4 = join21(cwd, config3.file);
|
|
191392
|
+
return existsSync10(path4) && (config3.applies?.(path4) ?? true);
|
|
191285
191393
|
}
|
|
191286
191394
|
var PYTHON_CONFIGS = [
|
|
191287
191395
|
{
|
|
@@ -191603,7 +191711,7 @@ function AwaitDependencyInstallationTool(ctx) {
|
|
|
191603
191711
|
|
|
191604
191712
|
// mcp/gh.ts
|
|
191605
191713
|
import { mkdirSync as mkdirSync9 } from "node:fs";
|
|
191606
|
-
import { join as
|
|
191714
|
+
import { join as join22 } from "node:path";
|
|
191607
191715
|
var GH_TIMEOUT_MS = 12e4;
|
|
191608
191716
|
var GhParams = type({
|
|
191609
191717
|
args: type.string.array().describe(
|
|
@@ -191625,7 +191733,7 @@ function GhTool(ctx) {
|
|
|
191625
191733
|
description: `Run the GitHub CLI against ${ctx.repo.owner}/${ctx.repo.name}. Pass argv as an array \u2014 there is no shell, so pipes, redirects and \`&&\` do not work; use gh's own \`--json\`/\`--jq\` flags instead. \`gh api graphql\` is available. Authenticated with ${permissions ? formatPermissions(permissions) : "no permissions"} on this repo only; anything else 403s. Pushing, merging and ref writes are not available \u2014 use the git tools.`,
|
|
191626
191734
|
parameters: GhParams,
|
|
191627
191735
|
execute: execute(async (params) => {
|
|
191628
|
-
const configDir =
|
|
191736
|
+
const configDir = join22(ctx.tmpdir, "gh-config");
|
|
191629
191737
|
mkdirSync9(configDir, { recursive: true });
|
|
191630
191738
|
const command = ["gh", ...params.args.map(shellQuote)].join(" ");
|
|
191631
191739
|
const result = await runSandboxed({
|
|
@@ -192349,7 +192457,7 @@ function PullRequestInfoTool(ctx) {
|
|
|
192349
192457
|
|
|
192350
192458
|
// mcp/reviewComments.ts
|
|
192351
192459
|
import { writeFileSync as writeFileSync13 } from "node:fs";
|
|
192352
|
-
import { join as
|
|
192460
|
+
import { join as join23 } from "node:path";
|
|
192353
192461
|
var COMMENTS_PER_THREAD = 50;
|
|
192354
192462
|
var REVIEW_THREADS_QUERY = `
|
|
192355
192463
|
query ($owner: String!, $name: String!, $prNumber: Int!, $cursor: String) {
|
|
@@ -192843,7 +192951,7 @@ function GetReviewCommentsTool(ctx) {
|
|
|
192843
192951
|
throw new Error("PULLFROG_TEMP_DIR not set");
|
|
192844
192952
|
}
|
|
192845
192953
|
const filename = `review-${params.review_id}-threads.md`;
|
|
192846
|
-
const commentsPath =
|
|
192954
|
+
const commentsPath = join23(tempDir, filename);
|
|
192847
192955
|
writeFileSync13(commentsPath, formatted.content);
|
|
192848
192956
|
log.debug(`wrote ${threadBlocks.length} threads to ${commentsPath}`);
|
|
192849
192957
|
return {
|
|
@@ -193213,7 +193321,7 @@ function UploadFileTool(ctx) {
|
|
|
193213
193321
|
|
|
193214
193322
|
// mcp/xrepo.ts
|
|
193215
193323
|
import { mkdirSync as mkdirSync10, rmSync as rmSync3 } from "node:fs";
|
|
193216
|
-
import { join as
|
|
193324
|
+
import { join as join24 } from "node:path";
|
|
193217
193325
|
function assertValidRepoName(name) {
|
|
193218
193326
|
if (!/^[A-Za-z0-9._-]+$/.test(name) || name.startsWith("-") || name.includes("..")) {
|
|
193219
193327
|
throw new Error(
|
|
@@ -193293,7 +193401,7 @@ function CheckoutRepoTool(ctx) {
|
|
|
193293
193401
|
return { path: existing.dir, access: existing.access, note: "already checked out." };
|
|
193294
193402
|
}
|
|
193295
193403
|
const access = accessFor(ctx, repo);
|
|
193296
|
-
const dir =
|
|
193404
|
+
const dir = join24(ctx.tmpdir, "xrepo", repo);
|
|
193297
193405
|
mkdirSync10(dir, { recursive: true });
|
|
193298
193406
|
const state = ensureRepoState(ctx.toolState, { owner, name: repo, dir, access });
|
|
193299
193407
|
const rc = resolveRepoCtx(ctx, repo);
|
|
@@ -193659,6 +193767,7 @@ function isByokSetupError(text) {
|
|
|
193659
193767
|
var SECRETS_UNAVAILABLE_MARKER = "couldn't load your Pullfrog secrets";
|
|
193660
193768
|
var ROUTER_UNFUNDED_MARKER = "your Pullfrog Router balance is empty";
|
|
193661
193769
|
var CREDENTIAL_REJECTED_MARKER = "was rejected by its provider";
|
|
193770
|
+
var CREDENTIAL_POOL_REFUSED_MARKER = "No configured credential can serve this run";
|
|
193662
193771
|
var NoUsableCredentialError = class extends Error {
|
|
193663
193772
|
};
|
|
193664
193773
|
function throwKeyError(params) {
|
|
@@ -193925,7 +194034,8 @@ function isOAuthCredentialExpiredError(text) {
|
|
|
193925
194034
|
text
|
|
193926
194035
|
) || // the provider no longer recognises the token at all (#1086) — same dead
|
|
193927
194036
|
// credential, phrased as a lookup miss rather than a state transition.
|
|
193928
|
-
/Could not find the appropriate key in your authentication token/i.test(text) ||
|
|
194037
|
+
/Could not find the appropriate key in your authentication token/i.test(text) || // the reversed word order: `Encountered invalidated oauth token for user` (#1474).
|
|
194038
|
+
/(?:expired|invalidated|revoked) (?:OAuth|authentication) token/i.test(text) || /Token refresh failed/i.test(text)
|
|
193929
194039
|
);
|
|
193930
194040
|
}
|
|
193931
194041
|
function isMalformedKeyError(text) {
|
|
@@ -193972,7 +194082,7 @@ function buildRejectedCredentialError(params) {
|
|
|
193972
194082
|
].join("\n");
|
|
193973
194083
|
}
|
|
193974
194084
|
const where = params.inPullfrogStore ? `[Update it in Pullfrog \u2192](${settingsUrl})` : `[Update the GitHub Actions secret \u2192](https://github.com/${params.owner}/${params.name}/settings/secrets/actions)`;
|
|
193975
|
-
const keysUrl =
|
|
194085
|
+
const keysUrl = providerDashboard(params.credential)?.keys;
|
|
193976
194086
|
const issue3 = keysUrl ? `[Issue a new key \u2192](${keysUrl}) \xB7 ` : "";
|
|
193977
194087
|
return [
|
|
193978
194088
|
`**Your \`${params.credential}\` ${CREDENTIAL_REJECTED_MARKER}**${detail}, so the agent never ran.`,
|
|
@@ -193982,12 +194092,43 @@ function buildRejectedCredentialError(params) {
|
|
|
193982
194092
|
`${issue3}${where} \xB7 [Model settings \u2192](${settingsUrl}) \xB7 [Setup docs \u2192](https://docs.pullfrog.com/keys) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
|
|
193983
194093
|
].join("\n");
|
|
193984
194094
|
}
|
|
193985
|
-
function
|
|
194095
|
+
function providerDashboard(credential) {
|
|
193986
194096
|
for (const [id, provider2] of Object.entries(providers)) {
|
|
193987
|
-
if (provider2.envVars.includes(credential)) return PROVIDER_DASHBOARDS[id]
|
|
194097
|
+
if (provider2.envVars.includes(credential)) return PROVIDER_DASHBOARDS[id];
|
|
193988
194098
|
}
|
|
193989
194099
|
return void 0;
|
|
193990
194100
|
}
|
|
194101
|
+
function buildCredentialPoolRefusedError(params) {
|
|
194102
|
+
const settingsUrl = `${getApiUrl()}/console/${params.owner}/${params.name}`;
|
|
194103
|
+
return [
|
|
194104
|
+
`**${CREDENTIAL_POOL_REFUSED_MARKER}.** Every credential configured for \`${params.model}\` was rejected or is out of quota, so the agent never ran.`,
|
|
194105
|
+
"",
|
|
194106
|
+
...params.refused.map(
|
|
194107
|
+
(credential) => `- ${describeRefusal({ ...params, credential, settingsUrl })}`
|
|
194108
|
+
),
|
|
194109
|
+
"",
|
|
194110
|
+
`[Model settings \u2192](${settingsUrl}) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
|
|
194111
|
+
].join("\n");
|
|
194112
|
+
}
|
|
194113
|
+
function describeRefusal(params) {
|
|
194114
|
+
const credential = params.credential;
|
|
194115
|
+
const verdict = credential.verdict;
|
|
194116
|
+
const workflow = credential.source === "workflow";
|
|
194117
|
+
const head = `\`${credential.name}\` from ${workflow ? "the workflow" : `${credential.source} scope`}: \`${verdict.detail}\` \u2014`;
|
|
194118
|
+
const subscription = SUBSCRIPTION_CREDENTIALS[credential.name];
|
|
194119
|
+
const dashboard = providerDashboard(credential.name);
|
|
194120
|
+
if (verdict.status === "exhausted" && subscription) {
|
|
194121
|
+
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";
|
|
194122
|
+
return `${head} ${wait}, or add an API key for this provider, which Pullfrog falls back to automatically.`;
|
|
194123
|
+
}
|
|
194124
|
+
if (verdict.status === "exhausted")
|
|
194125
|
+
return `${head} add credit or raise its quota with the provider.${dashboard ? ` [Top up \u2192](${dashboard.billing})` : ""}`;
|
|
194126
|
+
if (subscription)
|
|
194127
|
+
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})`;
|
|
194128
|
+
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})`;
|
|
194129
|
+
const issue3 = dashboard?.keys ? `[Issue a new key \u2192](${dashboard.keys}) \xB7 ` : "";
|
|
194130
|
+
return `${head} issue a new key and update the copy Pullfrog uses. ${issue3}${where}`;
|
|
194131
|
+
}
|
|
193991
194132
|
function formatApiKeyErrorSummary(params) {
|
|
193992
194133
|
if (params.raw.includes(MISSING_KEY_MARKER)) {
|
|
193993
194134
|
if (params.raw.startsWith(`**${MISSING_KEY_MARKER}**`)) return params.raw;
|
|
@@ -194760,11 +194901,16 @@ async function selectConfiguredCredential(input) {
|
|
|
194760
194901
|
if (option.candidate) log.info(`\xBB selected ${option.name} from ${from}`);
|
|
194761
194902
|
return true;
|
|
194762
194903
|
}
|
|
194763
|
-
refused.push(
|
|
194904
|
+
refused.push({ name: option.name, source: option.candidate?.source ?? "workflow", verdict });
|
|
194764
194905
|
log.info(`\xBB ${option.name} from ${from} unavailable; trying the next credential`);
|
|
194765
194906
|
}
|
|
194766
194907
|
throw new Error(
|
|
194767
|
-
|
|
194908
|
+
buildCredentialPoolRefusedError({
|
|
194909
|
+
model,
|
|
194910
|
+
refused,
|
|
194911
|
+
owner: input.ctx.repo.owner,
|
|
194912
|
+
name: input.ctx.repo.name
|
|
194913
|
+
})
|
|
194768
194914
|
);
|
|
194769
194915
|
}
|
|
194770
194916
|
async function probe(input) {
|
|
@@ -194805,7 +194951,7 @@ function activate(input) {
|
|
|
194805
194951
|
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
194806
194952
|
import { writeFileSync as writeFileSync14 } from "node:fs";
|
|
194807
194953
|
import { createServer as createServer3 } from "node:http";
|
|
194808
|
-
import { join as
|
|
194954
|
+
import { join as join25 } from "node:path";
|
|
194809
194955
|
var REVOKED_TRAP_MS = 6e4;
|
|
194810
194956
|
function revokeGitHubToken(token) {
|
|
194811
194957
|
fetch("https://api.github.com/installation/token", {
|
|
@@ -194874,7 +195020,7 @@ async function startGitAuthServer(tmpdir3) {
|
|
|
194874
195020
|
function writeAskpassScript(code) {
|
|
194875
195021
|
const scriptId = randomUUID6();
|
|
194876
195022
|
const scriptName = `askpass-${scriptId}.js`;
|
|
194877
|
-
const scriptPath =
|
|
195023
|
+
const scriptPath = join25(tmpdir3, scriptName);
|
|
194878
195024
|
const content = [
|
|
194879
195025
|
`#!/usr/bin/env node`,
|
|
194880
195026
|
`var a=process.argv[2]||"";`,
|
|
@@ -195323,7 +195469,7 @@ function resolveInstructions(ctx) {
|
|
|
195323
195469
|
|
|
195324
195470
|
// utils/learnings.ts
|
|
195325
195471
|
import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
|
|
195326
|
-
import { dirname as dirname4, join as
|
|
195472
|
+
import { dirname as dirname4, join as join26 } from "node:path";
|
|
195327
195473
|
|
|
195328
195474
|
// utils/learningsTruncate.ts
|
|
195329
195475
|
var MAX_LEARNINGS_LENGTH = 1e5;
|
|
@@ -195341,10 +195487,10 @@ function truncateAtLineBoundary(body, cap) {
|
|
|
195341
195487
|
var LEARNINGS_FILE_NAME = "pullfrog-learnings.md";
|
|
195342
195488
|
var XREPO_LEARNINGS_FILE_NAME = "pullfrog-xrepo-learnings.md";
|
|
195343
195489
|
function learningsFilePath(tmpdir3) {
|
|
195344
|
-
return
|
|
195490
|
+
return join26(tmpdir3, LEARNINGS_FILE_NAME);
|
|
195345
195491
|
}
|
|
195346
195492
|
function xrepoLearningsFilePath(tmpdir3) {
|
|
195347
|
-
return
|
|
195493
|
+
return join26(tmpdir3, XREPO_LEARNINGS_FILE_NAME);
|
|
195348
195494
|
}
|
|
195349
195495
|
async function seedLearningsFile(params) {
|
|
195350
195496
|
const path4 = learningsFilePath(params.tmpdir);
|
|
@@ -195895,7 +196041,9 @@ async function resolveTokens(params) {
|
|
|
195895
196041
|
// commit-status check-runs for branch protection. the app already grants
|
|
195896
196042
|
// checks:write; this scopes the MCP token up to use it.
|
|
195897
196043
|
checks: "write",
|
|
195898
|
-
|
|
196044
|
+
// write (not read) so postReviewCleanup can dispatch the follow-up re-review
|
|
196045
|
+
// for commits pushed while a review was in flight; read 403s it.
|
|
196046
|
+
actions: "write"
|
|
195899
196047
|
};
|
|
195900
196048
|
const mcpToken = await acquireNewToken({
|
|
195901
196049
|
repos: writeRepos,
|
|
@@ -196281,7 +196429,7 @@ async function resolveTrialFallback(ctx) {
|
|
|
196281
196429
|
|
|
196282
196430
|
// utils/prSummary.ts
|
|
196283
196431
|
import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
|
|
196284
|
-
import { dirname as dirname5, join as
|
|
196432
|
+
import { dirname as dirname5, join as join27 } from "node:path";
|
|
196285
196433
|
var SUMMARY_FILE_NAME = "pullfrog-summary.md";
|
|
196286
196434
|
var SUMMARY_SCAFFOLD = `# PR summary
|
|
196287
196435
|
|
|
@@ -196291,7 +196439,7 @@ var SUMMARY_SCAFFOLD = `# PR summary
|
|
|
196291
196439
|
var MIN_SNAPSHOT_LENGTH = 60;
|
|
196292
196440
|
var MAX_SNAPSHOT_LENGTH = 32768;
|
|
196293
196441
|
function summaryFilePath(tmpdir3) {
|
|
196294
|
-
return
|
|
196442
|
+
return join27(tmpdir3, SUMMARY_FILE_NAME);
|
|
196295
196443
|
}
|
|
196296
196444
|
async function seedSummaryFile(params) {
|
|
196297
196445
|
const path4 = summaryFilePath(params.tmpdir);
|
|
@@ -196560,7 +196708,7 @@ function formatBillingExhaustedBody(diagnostic) {
|
|
|
196560
196708
|
|
|
196561
196709
|
// utils/runErrorRenderer.ts
|
|
196562
196710
|
function isProviderModelNotFoundError(message) {
|
|
196563
|
-
return message
|
|
196711
|
+
return /ProviderModelNotFoundError|\bModel not found: /.test(message);
|
|
196564
196712
|
}
|
|
196565
196713
|
function isContextOverflowError(message) {
|
|
196566
196714
|
return /Prompt is too long/i.test(message) || /Input exceeds context window/i.test(message) || /maximum context length is \d+ tokens/i.test(message) || /Session too large to compact/i.test(message);
|
|
@@ -196657,6 +196805,7 @@ function detectProviderId(message) {
|
|
|
196657
196805
|
if (/requires more credits|Key limit exceeded \(total limit\)|openrouter\.ai/i.test(message)) {
|
|
196658
196806
|
return "openrouter";
|
|
196659
196807
|
}
|
|
196808
|
+
if (/platform\.openai\.com/i.test(message)) return "openai";
|
|
196660
196809
|
return null;
|
|
196661
196810
|
}
|
|
196662
196811
|
var OPENROUTER_PRIVACY_URL = "https://openrouter.ai/settings/privacy";
|
|
@@ -196754,12 +196903,38 @@ function formatByokSetupSummary(input) {
|
|
|
196754
196903
|
].join("\n");
|
|
196755
196904
|
}
|
|
196756
196905
|
function formatProviderModelNotFoundSummary(input) {
|
|
196757
|
-
return `The configured model is
|
|
196906
|
+
return `The configured model is not in OpenCode's catalog. Pick a different model for \`${input.owner}/${input.name}\` where it is set \u2014 the Pullfrog console, the workflow's \`model\` input, or the \`PULLFROG_MODEL\` variable \u2014 or contact support if this persists.
|
|
196758
196907
|
|
|
196759
196908
|
\`\`\`
|
|
196760
196909
|
${input.raw}
|
|
196761
196910
|
\`\`\``;
|
|
196762
196911
|
}
|
|
196912
|
+
function formatProviderUsageLimit(input) {
|
|
196913
|
+
return [
|
|
196914
|
+
"**The provider's usage limit for this account has been reached.**",
|
|
196915
|
+
"",
|
|
196916
|
+
"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.",
|
|
196917
|
+
"",
|
|
196918
|
+
`[Model settings \u2192](${getApiUrl()}/console/${input.owner}/${input.name}) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`,
|
|
196919
|
+
"",
|
|
196920
|
+
`\`\`\`
|
|
196921
|
+
${input.raw}
|
|
196922
|
+
\`\`\``
|
|
196923
|
+
].join("\n");
|
|
196924
|
+
}
|
|
196925
|
+
function formatProviderErrorBody(input) {
|
|
196926
|
+
return [
|
|
196927
|
+
"**The model provider returned an error, so this run stopped.**",
|
|
196928
|
+
"",
|
|
196929
|
+
"The error is below. If it names your key, plan or balance, fix that with the provider or pick another model before re-triggering.",
|
|
196930
|
+
"",
|
|
196931
|
+
`[Model settings \u2192](${getApiUrl()}/console/${input.owner}/${input.name}) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`,
|
|
196932
|
+
"",
|
|
196933
|
+
`\`\`\`
|
|
196934
|
+
${input.raw}
|
|
196935
|
+
\`\`\``
|
|
196936
|
+
].join("\n");
|
|
196937
|
+
}
|
|
196763
196938
|
function renderRunError(input) {
|
|
196764
196939
|
const billingError = input.routerActive && isRouterKeylimitExhaustedError(input.errorMessage) ? new BillingError(input.errorMessage, { code: "router_keylimit_exhausted" }) : null;
|
|
196765
196940
|
if (billingError) {
|
|
@@ -196775,7 +196950,7 @@ function renderRunError(input) {
|
|
|
196775
196950
|
if (input.errorMessage.includes(ROUTER_UNFUNDED_MARKER)) {
|
|
196776
196951
|
return { summary: input.errorMessage, comment: input.errorMessage };
|
|
196777
196952
|
}
|
|
196778
|
-
if (input.errorMessage.includes(CREDENTIAL_REJECTED_MARKER)) {
|
|
196953
|
+
if (input.errorMessage.includes(CREDENTIAL_REJECTED_MARKER) || input.errorMessage.includes(CREDENTIAL_POOL_REFUSED_MARKER)) {
|
|
196779
196954
|
return { summary: input.errorMessage, comment: input.errorMessage };
|
|
196780
196955
|
}
|
|
196781
196956
|
const isHang = input.errorMessage.startsWith("activity timeout") || input.errorMessage.startsWith("agent still pending");
|
|
@@ -196798,6 +196973,16 @@ ${body}`, comment: body };
|
|
|
196798
196973
|
});
|
|
196799
196974
|
return { summary: `### \u274C Pullfrog failed
|
|
196800
196975
|
|
|
196976
|
+
${body}`, comment: body };
|
|
196977
|
+
}
|
|
196978
|
+
if (isProviderUsageLimit(input.errorMessage)) {
|
|
196979
|
+
const body = formatProviderUsageLimit({
|
|
196980
|
+
owner: input.repo.owner,
|
|
196981
|
+
name: input.repo.name,
|
|
196982
|
+
raw: input.errorMessage
|
|
196983
|
+
});
|
|
196984
|
+
return { summary: `### \u274C Pullfrog failed
|
|
196985
|
+
|
|
196801
196986
|
${body}`, comment: body };
|
|
196802
196987
|
}
|
|
196803
196988
|
if (isProviderMissingCredential(input.errorMessage)) {
|
|
@@ -196892,6 +197077,16 @@ ${hangBody}`,
|
|
|
196892
197077
|
comment: explained ? hangBody : formatMinimalFailureComment(input.repo)
|
|
196893
197078
|
};
|
|
196894
197079
|
}
|
|
197080
|
+
if (input.errorMessage.startsWith("provider error: ")) {
|
|
197081
|
+
const body = formatProviderErrorBody({
|
|
197082
|
+
owner: input.repo.owner,
|
|
197083
|
+
name: input.repo.name,
|
|
197084
|
+
raw: input.errorMessage
|
|
197085
|
+
});
|
|
197086
|
+
return { summary: `### \u274C Pullfrog failed
|
|
197087
|
+
|
|
197088
|
+
${body}`, comment: body };
|
|
197089
|
+
}
|
|
196895
197090
|
const genericBody = formatGenericFailure(input.errorMessage);
|
|
196896
197091
|
return {
|
|
196897
197092
|
summary: `### \u274C Pullfrog failed
|
|
@@ -197702,7 +197897,7 @@ async function main() {
|
|
|
197702
197897
|
const repoDir = payload.cwd;
|
|
197703
197898
|
const preIntrospectionDirty = repoDir ? await dirtyTrackedPaths({ cwd: repoDir }) : null;
|
|
197704
197899
|
const opencodeCliPath = await agents.opencode.install();
|
|
197705
|
-
captureBaselineModels(opencodeCliPath);
|
|
197900
|
+
captureBaselineModels({ cliPath: opencodeCliPath, tmpdir: tmpdir3 });
|
|
197706
197901
|
const injected = [];
|
|
197707
197902
|
if (runContext.dbSecrets) {
|
|
197708
197903
|
for (const [key, value2] of Object.entries(runContext.dbSecrets)) {
|
|
@@ -197719,7 +197914,7 @@ async function main() {
|
|
|
197719
197914
|
await initializeCredentialPool({ ctx: runContext, slug: payload.model, injected });
|
|
197720
197915
|
installCodexAuth();
|
|
197721
197916
|
installXaiAuth();
|
|
197722
|
-
captureAuthorizedModels(opencodeCliPath);
|
|
197917
|
+
captureAuthorizedModels({ cliPath: opencodeCliPath, tmpdir: tmpdir3 });
|
|
197723
197918
|
if (preIntrospectionDirty && repoDir) {
|
|
197724
197919
|
await restoreDirtiedSince({
|
|
197725
197920
|
before: preIntrospectionDirty,
|
|
@@ -198057,8 +198252,8 @@ ${instructions.user}` : null,
|
|
|
198057
198252
|
log.info(instructions.full);
|
|
198058
198253
|
});
|
|
198059
198254
|
if (agentId === "opencode") {
|
|
198060
|
-
const pluginDir =
|
|
198061
|
-
const hasPlugins =
|
|
198255
|
+
const pluginDir = join28(process.cwd(), ".opencode", "plugin");
|
|
198256
|
+
const hasPlugins = existsSync11(pluginDir) && readdirSync2(pluginDir).some((f) => /\.[jt]sx?$/.test(f));
|
|
198062
198257
|
if (hasPlugins && toolState.dependencyInstallation?.promise) {
|
|
198063
198258
|
log.info(
|
|
198064
198259
|
"\xBB .opencode/plugin/ detected \u2014 awaiting dependency installation before agent start"
|