pullfrog 0.1.4 → 0.1.6
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/sessionLabeler.d.ts +38 -18
- package/dist/cli.mjs +310 -118
- package/dist/index.js +298 -106
- package/dist/internal.js +5 -4
- package/dist/mcp/geminiSanitizer.d.ts +15 -4
- package/dist/utils/normalizeEnv.d.ts +21 -1
- package/dist/utils/subprocess.d.ts +40 -0
- package/dist/utils/timer.d.ts +11 -0
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -19935,10 +19935,10 @@ var require_core = __commonJS({
|
|
|
19935
19935
|
(0, command_1.issueCommand)("set-env", { name }, convertedVal);
|
|
19936
19936
|
}
|
|
19937
19937
|
exports.exportVariable = exportVariable;
|
|
19938
|
-
function
|
|
19938
|
+
function setSecret5(secret) {
|
|
19939
19939
|
(0, command_1.issueCommand)("add-mask", {}, secret);
|
|
19940
19940
|
}
|
|
19941
|
-
exports.setSecret =
|
|
19941
|
+
exports.setSecret = setSecret5;
|
|
19942
19942
|
function addPath(inputPath) {
|
|
19943
19943
|
const filePath = process.env["GITHUB_PATH"] || "";
|
|
19944
19944
|
if (filePath) {
|
|
@@ -47954,7 +47954,7 @@ var require_core3 = __commonJS({
|
|
|
47954
47954
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47955
47955
|
var id_1 = require_id();
|
|
47956
47956
|
var ref_1 = require_ref();
|
|
47957
|
-
var
|
|
47957
|
+
var core9 = [
|
|
47958
47958
|
"$schema",
|
|
47959
47959
|
"$id",
|
|
47960
47960
|
"$defs",
|
|
@@ -47964,7 +47964,7 @@ var require_core3 = __commonJS({
|
|
|
47964
47964
|
id_1.default,
|
|
47965
47965
|
ref_1.default
|
|
47966
47966
|
];
|
|
47967
|
-
exports.default =
|
|
47967
|
+
exports.default = core9;
|
|
47968
47968
|
}
|
|
47969
47969
|
});
|
|
47970
47970
|
|
|
@@ -99202,12 +99202,12 @@ var import_picocolors2 = __toESM(require_picocolors(), 1);
|
|
|
99202
99202
|
import { basename as basename2 } from "node:path";
|
|
99203
99203
|
|
|
99204
99204
|
// commands/gha.ts
|
|
99205
|
-
var
|
|
99205
|
+
var core8 = __toESM(require_core(), 1);
|
|
99206
99206
|
var import_arg = __toESM(require_arg(), 1);
|
|
99207
99207
|
import { dirname as dirname6 } from "node:path";
|
|
99208
99208
|
|
|
99209
99209
|
// main.ts
|
|
99210
|
-
var
|
|
99210
|
+
var core7 = __toESM(require_core(), 1);
|
|
99211
99211
|
import { existsSync as existsSync7, readdirSync } from "node:fs";
|
|
99212
99212
|
import { readFile as readFile4 } from "node:fs/promises";
|
|
99213
99213
|
import { join as join17 } from "node:path";
|
|
@@ -108238,8 +108238,7 @@ var providers = {
|
|
|
108238
108238
|
"gpt-5-nano": {
|
|
108239
108239
|
displayName: "GPT Nano",
|
|
108240
108240
|
resolve: "opencode/gpt-5-nano",
|
|
108241
|
-
|
|
108242
|
-
isFree: true
|
|
108241
|
+
openRouterResolve: "openrouter/openai/gpt-5-nano"
|
|
108243
108242
|
},
|
|
108244
108243
|
"mimo-v2-pro-free": {
|
|
108245
108244
|
displayName: "MiMo V2 Pro",
|
|
@@ -108468,6 +108467,11 @@ async function apiFetch(options) {
|
|
|
108468
108467
|
if (bypassSecret) {
|
|
108469
108468
|
headers["x-vercel-protection-bypass"] = bypassSecret;
|
|
108470
108469
|
}
|
|
108470
|
+
if (!options.body) {
|
|
108471
|
+
for (const key of Object.keys(headers)) {
|
|
108472
|
+
if (key.toLowerCase() === "content-type") delete headers[key];
|
|
108473
|
+
}
|
|
108474
|
+
}
|
|
108471
108475
|
log.debug(`api fetch: ${options.method ?? "GET"} ${url4.pathname}`);
|
|
108472
108476
|
const init = {
|
|
108473
108477
|
method: options.method ?? "GET",
|
|
@@ -109156,8 +109160,11 @@ function sanitizeToolForGemini(tool2) {
|
|
|
109156
109160
|
}
|
|
109157
109161
|
function isGeminiRouted(ctx) {
|
|
109158
109162
|
const effective = ctx.payload.proxyModel ?? ctx.resolvedModel ?? ctx.payload.model;
|
|
109159
|
-
if (!effective) return
|
|
109160
|
-
|
|
109163
|
+
if (!effective) return true;
|
|
109164
|
+
const normalized = effective.toLowerCase();
|
|
109165
|
+
if (normalized.includes("gemini")) return true;
|
|
109166
|
+
if (!normalized.includes("/")) return true;
|
|
109167
|
+
return false;
|
|
109161
109168
|
}
|
|
109162
109169
|
|
|
109163
109170
|
// mcp/shared.ts
|
|
@@ -110018,12 +110025,41 @@ function installSignalHandler() {
|
|
|
110018
110025
|
killTrackedChildren();
|
|
110019
110026
|
});
|
|
110020
110027
|
}
|
|
110028
|
+
var DEFAULT_MAX_RETAINED_BYTES = 8 * 1024 * 1024;
|
|
110029
|
+
var TailBuffer = class {
|
|
110030
|
+
// explicit field declarations rather than constructor parameter properties:
|
|
110031
|
+
// node's strip-only TS loader (used by action/test/run.ts in CI) rejects
|
|
110032
|
+
// `constructor(private readonly cap: number)` with ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX.
|
|
110033
|
+
cap;
|
|
110034
|
+
buffer = "";
|
|
110035
|
+
truncatedBytes = 0;
|
|
110036
|
+
constructor(cap) {
|
|
110037
|
+
this.cap = cap;
|
|
110038
|
+
}
|
|
110039
|
+
append(chunk) {
|
|
110040
|
+
if (this.cap <= 0) return;
|
|
110041
|
+
this.buffer += chunk;
|
|
110042
|
+
if (this.buffer.length > this.cap) {
|
|
110043
|
+
const drop = this.buffer.length - this.cap;
|
|
110044
|
+
this.truncatedBytes += drop;
|
|
110045
|
+
this.buffer = this.buffer.slice(drop);
|
|
110046
|
+
}
|
|
110047
|
+
}
|
|
110048
|
+
toString() {
|
|
110049
|
+
if (this.truncatedBytes === 0) return this.buffer;
|
|
110050
|
+
const mib = (this.truncatedBytes / 1024 / 1024).toFixed(1);
|
|
110051
|
+
return `... [${mib} MiB truncated by retain:tail cap] ...
|
|
110052
|
+
${this.buffer}`;
|
|
110053
|
+
}
|
|
110054
|
+
};
|
|
110021
110055
|
async function spawn(options) {
|
|
110022
110056
|
const activityTimeoutMs = options.activityTimeout ?? DEFAULT_ACTIVITY_TIMEOUT_MS;
|
|
110023
110057
|
installSignalHandler();
|
|
110024
110058
|
const startTime = performance3.now();
|
|
110025
|
-
|
|
110026
|
-
|
|
110059
|
+
const retain = options.retain ?? "tail";
|
|
110060
|
+
const cap = options.maxRetainedBytes ?? DEFAULT_MAX_RETAINED_BYTES;
|
|
110061
|
+
const stdoutBuffer = retain === "none" ? null : new TailBuffer(cap);
|
|
110062
|
+
const stderrBuffer = retain === "none" ? null : new TailBuffer(cap);
|
|
110027
110063
|
const killGroup = options.killGroup ?? false;
|
|
110028
110064
|
return new Promise((resolve3, reject) => {
|
|
110029
110065
|
const child = nodeSpawn(options.cmd, options.args, {
|
|
@@ -110097,17 +110133,29 @@ async function spawn(options) {
|
|
|
110097
110133
|
}
|
|
110098
110134
|
if (child.stdout) {
|
|
110099
110135
|
child.stdout.on("data", (data) => {
|
|
110100
|
-
|
|
110101
|
-
|
|
110102
|
-
|
|
110103
|
-
|
|
110136
|
+
try {
|
|
110137
|
+
updateActivity();
|
|
110138
|
+
const chunk = data.toString();
|
|
110139
|
+
stdoutBuffer?.append(chunk);
|
|
110140
|
+
options.onStdout?.(chunk);
|
|
110141
|
+
} catch (err) {
|
|
110142
|
+
log.debug(
|
|
110143
|
+
`spawn stdout handler threw: ${err instanceof Error ? err.message : String(err)}`
|
|
110144
|
+
);
|
|
110145
|
+
}
|
|
110104
110146
|
});
|
|
110105
110147
|
}
|
|
110106
110148
|
if (child.stderr) {
|
|
110107
110149
|
child.stderr.on("data", (data) => {
|
|
110108
|
-
|
|
110109
|
-
|
|
110110
|
-
|
|
110150
|
+
try {
|
|
110151
|
+
const chunk = data.toString();
|
|
110152
|
+
stderrBuffer?.append(chunk);
|
|
110153
|
+
options.onStderr?.(chunk);
|
|
110154
|
+
} catch (err) {
|
|
110155
|
+
log.debug(
|
|
110156
|
+
`spawn stderr handler threw: ${err instanceof Error ? err.message : String(err)}`
|
|
110157
|
+
);
|
|
110158
|
+
}
|
|
110111
110159
|
});
|
|
110112
110160
|
}
|
|
110113
110161
|
child.on("close", (exitCode, signal) => {
|
|
@@ -110134,7 +110182,7 @@ async function spawn(options) {
|
|
|
110134
110182
|
return;
|
|
110135
110183
|
}
|
|
110136
110184
|
let resolvedExitCode = exitCode ?? 0;
|
|
110137
|
-
let resolvedStderr = stderrBuffer;
|
|
110185
|
+
let resolvedStderr = stderrBuffer?.toString() ?? "";
|
|
110138
110186
|
if (exitCode === null && signal) {
|
|
110139
110187
|
const killMsg = `[spawn] ${options.cmd}: killed by signal ${signal}`;
|
|
110140
110188
|
resolvedStderr = resolvedStderr ? `${resolvedStderr}
|
|
@@ -110142,7 +110190,7 @@ ${killMsg}` : killMsg;
|
|
|
110142
110190
|
resolvedExitCode = 1;
|
|
110143
110191
|
}
|
|
110144
110192
|
resolve3({
|
|
110145
|
-
stdout: stdoutBuffer,
|
|
110193
|
+
stdout: stdoutBuffer?.toString() ?? "",
|
|
110146
110194
|
stderr: resolvedStderr,
|
|
110147
110195
|
exitCode: resolvedExitCode,
|
|
110148
110196
|
durationMs
|
|
@@ -110156,11 +110204,12 @@ ${killMsg}` : killMsg;
|
|
|
110156
110204
|
if (activityCheckIntervalId) clearInterval(activityCheckIntervalId);
|
|
110157
110205
|
const errMsg = `[spawn] ${options.cmd}: ${error49.message}`;
|
|
110158
110206
|
console.error(errMsg);
|
|
110159
|
-
|
|
110207
|
+
const existingStderr = stderrBuffer?.toString() ?? "";
|
|
110208
|
+
const finalStderr = existingStderr ? `${existingStderr}
|
|
110160
110209
|
${errMsg}` : errMsg;
|
|
110161
110210
|
resolve3({
|
|
110162
|
-
stdout: stdoutBuffer,
|
|
110163
|
-
stderr:
|
|
110211
|
+
stdout: stdoutBuffer?.toString() ?? "",
|
|
110212
|
+
stderr: finalStderr,
|
|
110164
110213
|
exitCode: 1,
|
|
110165
110214
|
durationMs
|
|
110166
110215
|
});
|
|
@@ -138069,7 +138118,7 @@ var require_core4 = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
138069
138118
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
138070
138119
|
const id_1 = require_id2();
|
|
138071
138120
|
const ref_1 = require_ref2();
|
|
138072
|
-
const
|
|
138121
|
+
const core9 = [
|
|
138073
138122
|
"$schema",
|
|
138074
138123
|
"$id",
|
|
138075
138124
|
"$defs",
|
|
@@ -138079,7 +138128,7 @@ var require_core4 = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
138079
138128
|
id_1.default,
|
|
138080
138129
|
ref_1.default
|
|
138081
138130
|
];
|
|
138082
|
-
exports.default =
|
|
138131
|
+
exports.default = core9;
|
|
138083
138132
|
}));
|
|
138084
138133
|
var require_limitNumber2 = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
138085
138134
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -142589,7 +142638,7 @@ var import_semver = __toESM(require_semver2(), 1);
|
|
|
142589
142638
|
// package.json
|
|
142590
142639
|
var package_default = {
|
|
142591
142640
|
name: "pullfrog",
|
|
142592
|
-
version: "0.1.
|
|
142641
|
+
version: "0.1.6",
|
|
142593
142642
|
type: "module",
|
|
142594
142643
|
bin: {
|
|
142595
142644
|
pullfrog: "dist/cli.mjs",
|
|
@@ -146666,6 +146715,8 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
146666
146715
|
- **4\u20135 lenses (high-stakes subsystem touches)** \u2014 any billing/payments change (billing-subsystem + correctness + security + operational-readiness); new auth flow (auth-subsystem + correctness + security + test-integrity); schema migration (schema-migration-subsystem + correctness + operational-readiness + impact); cross-subsystem PR that touches billing AND auth AND schema (one subsystem lens per domain + correctness)
|
|
146667
146716
|
- **6+ lenses** \u2014 almost always a smell; you're either covering overlapping ground or this PR should have been split. push back via the review body rather than expanding lens count.
|
|
146668
146717
|
|
|
146718
|
+
**lens-add discipline.** Each lens needs to clear a specific bar before you dispatch it: name the concrete failure mode this lens would catch *that the diff plausibly introduces*, in one sentence. "Could apply", "good to have", "for completeness" do not qualify. If you can't name what the lens is going to find, drop it. The "when unsure, treat as non-trivial" rule above is for the trivial-vs-non-trivial gate at step 3 \u2014 it does not license expanding lens count without articulated risk. Every extra lens adds wall-time, log noise, and pulls subagent attention onto speculative angles, which biases the final review toward bloat-shaped findings.
|
|
146719
|
+
|
|
146669
146720
|
lenses come in two flavors, and you can mix them:
|
|
146670
146721
|
- **themed lenses** \u2014 a perspective applied across the whole diff (correctness, security, user-journey, performance, etc.).
|
|
146671
146722
|
- **subsystem lenses** \u2014 a domain-scoped frame for high-stakes subsystems the PR touches (e.g. "the auth lens", "the billing lens", "the schema-migration lens"). a subsystem lens is "review the PR specifically for what could go wrong in this subsystem" and naturally combines theme + scope. **for high-stakes domains, lead with the subsystem lens rather than the generic themed equivalent** \u2014 "billing-subsystem" outperforms "correctness on billing code" because the framing primes the subagent to remember domain-specific failure modes (double-charges, refund races, currency rounding, dispute flows) the generic lens misses.
|
|
@@ -146673,7 +146724,7 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
|
|
|
146673
146724
|
starter menu (combine, omit, or invent your own):
|
|
146674
146725
|
- **correctness & invariants** \u2014 bugs, races, error handling, edge cases, state-machine boundaries
|
|
146675
146726
|
- **impact** \u2014 when the PR removes features, deletes exports, renames identifiers, or changes architectural patterns: stale references in code, tests, docs (\`docs/\`, \`wiki/\`), comments, configs, UI
|
|
146676
|
-
- **research-validated assumptions** \u2014 third-party API contracts, SDK semantics, framework directives, version-gated behavior. the subagent must verify load-bearing claims via web search and quote source URLs.
|
|
146727
|
+
- **research-validated assumptions** \u2014 third-party API contracts, SDK semantics, framework directives, version-gated behavior. **only pick when the PR's correctness depends on the contract behaving a specific way** \u2014 not when the API is merely used. An idempotency key as a backstop, a timeout as a hint, a retry as belt-and-suspenders: not load-bearing, skip this lens. The bar is "if the third-party contract differs from what the diff assumes, the PR is incorrect." When dispatched, the subagent must verify load-bearing claims via web search and quote source URLs.
|
|
146677
146728
|
- **security** \u2014 new endpoints, authZ, input validation, secrets handling, replay/CSRF/injection, cross-tenant isolation
|
|
146678
146729
|
- **user-journey** \u2014 UX-touching flows: walk through happy path and failure modes as a user
|
|
146679
146730
|
- **operational readiness** \u2014 observability, alerting, migrations (forward + rollback), feature flags, on-call burden
|
|
@@ -146762,7 +146813,7 @@ ${PR_SUMMARY_FORMAT}`
|
|
|
146762
146813
|
"Looks trivial but isn't" (do NOT skip \u2014 same anti-patterns as Review mode): 1-line changes to SQL/regex/auth/billing/permissions/signature-verification code; flipping feature-flag defaults or retry/timeout constants; money/tax/HTTP-method/redirect changes; tightening or loosening a comparison operator; mixed diffs with a semantic line buried in formatting.
|
|
146763
146814
|
When unsure, treat as non-trivial.
|
|
146764
146815
|
|
|
146765
|
-
otherwise pick lenses by where the new commits concentrate risk \u2014 **there's no fixed count**, same calibration as Review mode (1 lens for pure refactor / isolated fix; 2\u20133 for typical features; 4\u20135 for high-stakes subsystem touches; 6+ is a smell). lens framing follows Review mode: themed lenses (correctness & invariants, impact when new commits remove/rename/deprecate things, research-validated assumptions, security, user-journey, operational readiness, integration & cross-cutting, test integrity, performance, holistic) and subsystem lenses (auth, billing, schema migration, etc.) \u2014 for high-stakes domains lead with the subsystem lens rather than the generic themed equivalent.
|
|
146816
|
+
otherwise pick lenses by where the new commits concentrate risk \u2014 **there's no fixed count**, same calibration as Review mode (1 lens for pure refactor / isolated fix; 2\u20133 for typical features; 4\u20135 for high-stakes subsystem touches; 6+ is a smell). same **lens-add discipline** as Review mode applies: each lens needs to name the concrete failure mode it would catch *that the new commits plausibly introduce* \u2014 "could apply" doesn't qualify, drop it. **research-validated assumptions** specifically: only pick when the new commits' correctness depends on a third-party contract behaving a specific way; merely using an API doesn't qualify. lens framing follows Review mode: themed lenses (correctness & invariants, impact when new commits remove/rename/deprecate things, research-validated assumptions, security, user-journey, operational readiness, integration & cross-cutting, test integrity, performance, holistic) and subsystem lenses (auth, billing, schema migration, etc.) \u2014 for high-stakes domains lead with the subsystem lens rather than the generic themed equivalent.
|
|
146766
146817
|
|
|
146767
146818
|
dispatch one \`${REVIEWER_AGENT_NAME}\` subagent per lens \u2014 its baked-in system prompt enforces the non-mutative + non-recursive contract (read-only file/search/web tools and read-only MCP queries; no writes, shell side effects, state-changing MCP calls, or nested subagent dispatch). dispatch them in a **single assistant turn with multiple parallel subagent calls** (serial dispatch collapses the fan-out). if a subagent errors out, times out, or returns nothing usable, retry once with the same lens; if it still fails, proceed with partial coverage and note the missing lens in the review body \u2014 do not skip step 5 entirely on a single subagent failure. each subagent gets:
|
|
146768
146819
|
- the diff scope (incremental diff path if available, full diff otherwise). do NOT tell them to skip pre-existing issues \u2014 that suppresses regressions the new commits amplified; the "issues must be NEW" filter lives at aggregation time (step 6), not in the subagent prompt
|
|
@@ -147132,20 +147183,30 @@ var ThinkingTimer = class {
|
|
|
147132
147183
|
maximumFractionDigits: 1
|
|
147133
147184
|
});
|
|
147134
147185
|
lastToolResultTimestamp = null;
|
|
147186
|
+
formatLine;
|
|
147187
|
+
// node's native TS strip-only mode does not support parameter properties,
|
|
147188
|
+
// so the formatter is declared as a field and assigned in the body.
|
|
147189
|
+
constructor(formatLine = (l) => l) {
|
|
147190
|
+
this.formatLine = formatLine;
|
|
147191
|
+
}
|
|
147135
147192
|
markToolResult() {
|
|
147136
147193
|
this.lastToolResultTimestamp = performance5.now();
|
|
147137
|
-
log.debug(
|
|
147194
|
+
log.debug(
|
|
147195
|
+
this.formatLine(`\xBB thinking timer: markToolResult at ${this.lastToolResultTimestamp}`)
|
|
147196
|
+
);
|
|
147138
147197
|
}
|
|
147139
147198
|
markToolCall() {
|
|
147140
147199
|
const now = performance5.now();
|
|
147141
147200
|
log.debug(
|
|
147142
|
-
|
|
147201
|
+
this.formatLine(
|
|
147202
|
+
`\xBB thinking timer: markToolCall at ${now}, lastToolResult=${this.lastToolResultTimestamp}`
|
|
147203
|
+
)
|
|
147143
147204
|
);
|
|
147144
147205
|
if (this.lastToolResultTimestamp === null) return;
|
|
147145
147206
|
const elapsed = now - this.lastToolResultTimestamp;
|
|
147146
147207
|
if (elapsed < THINKING_THRESHOLD) return;
|
|
147147
147208
|
const seconds = elapsed / 1e3;
|
|
147148
|
-
log.info(`\xBB thought for ${this.durationFormatter.format(seconds)}`);
|
|
147209
|
+
log.info(this.formatLine(`\xBB thought for ${this.durationFormatter.format(seconds)}`));
|
|
147149
147210
|
}
|
|
147150
147211
|
};
|
|
147151
147212
|
|
|
@@ -147394,19 +147455,39 @@ function deriveLabelFromTaskInput(input) {
|
|
|
147394
147455
|
}
|
|
147395
147456
|
var SessionLabeler = class {
|
|
147396
147457
|
labels = /* @__PURE__ */ new Map();
|
|
147458
|
+
labelsByToolUseId = /* @__PURE__ */ new Map();
|
|
147397
147459
|
pendingLabels = [];
|
|
147398
147460
|
fallbackCounter = 0;
|
|
147399
|
-
|
|
147461
|
+
/**
|
|
147462
|
+
* Record a Task/Agent tool dispatch.
|
|
147463
|
+
*
|
|
147464
|
+
* @param input Task tool input — used to derive the lens label.
|
|
147465
|
+
* @param toolUseId Optional Agent tool_use id. When provided, future events
|
|
147466
|
+
* carrying `parent_tool_use_id === toolUseId` resolve
|
|
147467
|
+
* directly to this label without consuming the FIFO queue
|
|
147468
|
+
* (Claude path). Always also pushed to the FIFO queue so
|
|
147469
|
+
* the OpenCode path still works when toolUseId is absent.
|
|
147470
|
+
*/
|
|
147471
|
+
recordTaskDispatch(input, toolUseId) {
|
|
147400
147472
|
const label = deriveLabelFromTaskInput(input);
|
|
147401
147473
|
this.pendingLabels.push(label);
|
|
147474
|
+
if (toolUseId) this.labelsByToolUseId.set(toolUseId, label);
|
|
147402
147475
|
return label;
|
|
147403
147476
|
}
|
|
147404
147477
|
/**
|
|
147405
|
-
* Return a label for the given
|
|
147406
|
-
*
|
|
147407
|
-
*
|
|
147478
|
+
* Return a label for the given event.
|
|
147479
|
+
*
|
|
147480
|
+
* @param sessionID Session id from the event (OpenCode: per-session;
|
|
147481
|
+
* Claude: shared across orchestrator + subagents).
|
|
147482
|
+
* @param parentToolUseId Claude's `parent_tool_use_id` — non-null on
|
|
147483
|
+
* subagent messages. When set and known, takes
|
|
147484
|
+
* priority over the FIFO/sessionID path.
|
|
147408
147485
|
*/
|
|
147409
|
-
labelFor(sessionID) {
|
|
147486
|
+
labelFor(sessionID, parentToolUseId) {
|
|
147487
|
+
if (parentToolUseId) {
|
|
147488
|
+
const direct = this.labelsByToolUseId.get(parentToolUseId);
|
|
147489
|
+
if (direct) return direct;
|
|
147490
|
+
}
|
|
147410
147491
|
if (!sessionID) return ORCHESTRATOR_LABEL;
|
|
147411
147492
|
const existing = this.labels.get(sessionID);
|
|
147412
147493
|
if (existing) return existing;
|
|
@@ -147478,8 +147559,7 @@ function stripProviderPrefix(specifier) {
|
|
|
147478
147559
|
const slashIndex = specifier.indexOf("/");
|
|
147479
147560
|
return slashIndex > 0 ? specifier.slice(slashIndex + 1) : specifier;
|
|
147480
147561
|
}
|
|
147481
|
-
function resolveEffort(
|
|
147482
|
-
if (model?.includes("opus")) return "max";
|
|
147562
|
+
function resolveEffort(_model) {
|
|
147483
147563
|
return "high";
|
|
147484
147564
|
}
|
|
147485
147565
|
function tailLines(text, maxCodeUnits) {
|
|
@@ -147491,7 +147571,23 @@ function tailLines(text, maxCodeUnits) {
|
|
|
147491
147571
|
async function runClaude(params) {
|
|
147492
147572
|
const startTime = performance6.now();
|
|
147493
147573
|
let eventCount = 0;
|
|
147494
|
-
const
|
|
147574
|
+
const labeler = new SessionLabeler();
|
|
147575
|
+
function eventLabel(event) {
|
|
147576
|
+
return labeler.labelFor(event.session_id ?? null, event.parent_tool_use_id ?? null);
|
|
147577
|
+
}
|
|
147578
|
+
function withLabel(label, message) {
|
|
147579
|
+
return label === ORCHESTRATOR_LABEL ? message : formatWithLabel(label, message);
|
|
147580
|
+
}
|
|
147581
|
+
const thinkingTimers = /* @__PURE__ */ new Map();
|
|
147582
|
+
function timerFor(label) {
|
|
147583
|
+
let t2 = thinkingTimers.get(label);
|
|
147584
|
+
if (!t2) {
|
|
147585
|
+
const formatLine = (line) => label === ORCHESTRATOR_LABEL ? line : formatWithLabel(label, line);
|
|
147586
|
+
t2 = new ThinkingTimer(formatLine);
|
|
147587
|
+
thinkingTimers.set(label, t2);
|
|
147588
|
+
}
|
|
147589
|
+
return t2;
|
|
147590
|
+
}
|
|
147495
147591
|
let finalOutput = "";
|
|
147496
147592
|
let sessionId;
|
|
147497
147593
|
let resultErrorSubtype = null;
|
|
@@ -147512,17 +147608,22 @@ async function runClaude(params) {
|
|
|
147512
147608
|
} : void 0;
|
|
147513
147609
|
}
|
|
147514
147610
|
const handlers2 = {
|
|
147515
|
-
system: (
|
|
147516
|
-
|
|
147611
|
+
system: (event) => {
|
|
147612
|
+
const label = eventLabel(event);
|
|
147613
|
+
log.debug(withLabel(label, `\xBB ${params.label} system event`));
|
|
147517
147614
|
},
|
|
147518
147615
|
assistant: (event) => {
|
|
147519
147616
|
const content = event.message?.content;
|
|
147520
147617
|
if (!content) return;
|
|
147618
|
+
const label = eventLabel(event);
|
|
147619
|
+
const boxTitle = label === ORCHESTRATOR_LABEL ? params.label : `${params.label} [${label}]`;
|
|
147521
147620
|
for (const block of content) {
|
|
147522
147621
|
if (block.type === "text" && block.text?.trim()) {
|
|
147523
147622
|
const message = block.text.trim();
|
|
147524
|
-
log.box(message, { title:
|
|
147525
|
-
|
|
147623
|
+
log.box(message, { title: boxTitle });
|
|
147624
|
+
if (label === ORCHESTRATOR_LABEL) {
|
|
147625
|
+
finalOutput = message;
|
|
147626
|
+
}
|
|
147526
147627
|
} else if (block.type === "tool_use") {
|
|
147527
147628
|
const toolName = block.name || "unknown";
|
|
147528
147629
|
if (params.onToolUse) {
|
|
@@ -147531,20 +147632,25 @@ async function runClaude(params) {
|
|
|
147531
147632
|
input: block.input
|
|
147532
147633
|
});
|
|
147533
147634
|
}
|
|
147534
|
-
|
|
147535
|
-
|
|
147536
|
-
|
|
147635
|
+
timerFor(label).markToolCall();
|
|
147636
|
+
const inputFormatted = formatJsonValue(block.input || {});
|
|
147637
|
+
const toolCallLine = inputFormatted !== "{}" ? `\xBB ${toolName}(${inputFormatted})` : `\xBB ${toolName}()`;
|
|
147638
|
+
log.info(withLabel(label, toolCallLine));
|
|
147639
|
+
if ((toolName === "Task" || toolName === "Agent") && block.input && typeof block.input === "object") {
|
|
147537
147640
|
const taskInput = block.input;
|
|
147538
|
-
const
|
|
147641
|
+
const dispatchedLabel = labeler.recordTaskDispatch(taskInput, block.id ?? null);
|
|
147539
147642
|
log.info(
|
|
147540
|
-
|
|
147643
|
+
withLabel(
|
|
147644
|
+
label,
|
|
147645
|
+
`\xBB dispatching subagent: ${dispatchedLabel}` + (taskInput.subagent_type ? ` (subagent_type=${taskInput.subagent_type})` : "")
|
|
147646
|
+
)
|
|
147541
147647
|
);
|
|
147542
147648
|
}
|
|
147543
147649
|
if (toolName.includes("report_progress") && params.todoTracker) {
|
|
147544
147650
|
log.debug("\xBB report_progress detected, disabling todo tracking");
|
|
147545
147651
|
params.todoTracker.cancel();
|
|
147546
147652
|
}
|
|
147547
|
-
if (toolName === "TodoWrite" && params.todoTracker?.enabled) {
|
|
147653
|
+
if (toolName === "TodoWrite" && params.todoTracker?.enabled && label === ORCHESTRATOR_LABEL) {
|
|
147548
147654
|
params.todoTracker.update(block.input);
|
|
147549
147655
|
}
|
|
147550
147656
|
}
|
|
@@ -147560,17 +147666,18 @@ async function runClaude(params) {
|
|
|
147560
147666
|
user: (event) => {
|
|
147561
147667
|
const content = event.message?.content;
|
|
147562
147668
|
if (!content) return;
|
|
147669
|
+
const label = eventLabel(event);
|
|
147563
147670
|
for (const block of content) {
|
|
147564
147671
|
if (typeof block === "string") continue;
|
|
147565
147672
|
if (block.type === "tool_result") {
|
|
147566
|
-
|
|
147673
|
+
timerFor(label).markToolResult();
|
|
147567
147674
|
const outputContent = typeof block.content === "string" ? block.content : Array.isArray(block.content) ? block.content.map(
|
|
147568
147675
|
(entry) => typeof entry === "string" ? entry : typeof entry === "object" && entry !== null && "text" in entry ? String(entry.text) : JSON.stringify(entry)
|
|
147569
147676
|
).join("\n") : String(block.content);
|
|
147570
147677
|
if (block.is_error) {
|
|
147571
|
-
log.info(`\xBB tool error: ${outputContent}`);
|
|
147678
|
+
log.info(withLabel(label, `\xBB tool error: ${outputContent}`));
|
|
147572
147679
|
} else {
|
|
147573
|
-
log.debug(`\xBB tool output: ${outputContent}`);
|
|
147680
|
+
log.debug(withLabel(label, `\xBB tool output: ${outputContent}`));
|
|
147574
147681
|
}
|
|
147575
147682
|
}
|
|
147576
147683
|
}
|
|
@@ -147640,7 +147747,7 @@ async function runClaude(params) {
|
|
|
147640
147747
|
};
|
|
147641
147748
|
const recentStderr = [];
|
|
147642
147749
|
let lastProviderError = null;
|
|
147643
|
-
|
|
147750
|
+
const output = new TailBuffer(DEFAULT_MAX_RETAINED_BYTES);
|
|
147644
147751
|
let stdoutBuffer = "";
|
|
147645
147752
|
try {
|
|
147646
147753
|
const result = await spawn({
|
|
@@ -147657,9 +147764,14 @@ async function runClaude(params) {
|
|
|
147657
147764
|
// there's no shim-orphan issue like opencode-ai/bin/opencode, but
|
|
147658
147765
|
// detached + killGroup is the right default for any agent runtime.
|
|
147659
147766
|
killGroup: true,
|
|
147767
|
+
// claude already drains every chunk via onStdout (NDJSON parsing) and
|
|
147768
|
+
// onStderr (recentStderr ring buffer). retaining a second copy in the
|
|
147769
|
+
// spawn wrapper would grow unbounded for long sessions and previously
|
|
147770
|
+
// crashed the wrapper with RangeError. see issue #680.
|
|
147771
|
+
retain: "none",
|
|
147660
147772
|
onStdout: async (chunk) => {
|
|
147661
147773
|
const text = chunk.toString();
|
|
147662
|
-
output
|
|
147774
|
+
output.append(text);
|
|
147663
147775
|
markActivity();
|
|
147664
147776
|
stdoutBuffer += text;
|
|
147665
147777
|
const lines = stdoutBuffer.split("\n");
|
|
@@ -147734,16 +147846,18 @@ ${stderrContext}`);
|
|
|
147734
147846
|
const usage = buildUsage();
|
|
147735
147847
|
if (result.exitCode !== 0) {
|
|
147736
147848
|
const errorContext = lastProviderError ? ` (${lastProviderError})` : "";
|
|
147737
|
-
const
|
|
147738
|
-
const
|
|
147849
|
+
const stdoutSnapshot = output.toString();
|
|
147850
|
+
const stderrSnapshot = recentStderr.join("\n");
|
|
147851
|
+
const truncatedStdout = stdoutSnapshot ? tailLines(stdoutSnapshot, 2048) : "";
|
|
147852
|
+
const errorMessage = lastResultError || stderrSnapshot || truncatedStdout || `unknown error - no output from Claude CLI${errorContext}`;
|
|
147739
147853
|
log.error(
|
|
147740
147854
|
`${params.label} exited with code ${result.exitCode}${errorContext}: ${errorMessage}`
|
|
147741
147855
|
);
|
|
147742
|
-
log.debug(`stdout: ${
|
|
147743
|
-
log.debug(`stderr: ${
|
|
147856
|
+
log.debug(`stdout: ${stdoutSnapshot.substring(0, 500)}`);
|
|
147857
|
+
log.debug(`stderr: ${stderrSnapshot.substring(0, 500)}`);
|
|
147744
147858
|
return {
|
|
147745
147859
|
success: false,
|
|
147746
|
-
output: finalOutput ||
|
|
147860
|
+
output: finalOutput || stdoutSnapshot,
|
|
147747
147861
|
error: errorMessage,
|
|
147748
147862
|
usage,
|
|
147749
147863
|
sessionId
|
|
@@ -147752,7 +147866,7 @@ ${stderrContext}`);
|
|
|
147752
147866
|
if (eventCount === 0 && lastProviderError) {
|
|
147753
147867
|
return {
|
|
147754
147868
|
success: false,
|
|
147755
|
-
output: finalOutput || output,
|
|
147869
|
+
output: finalOutput || output.toString(),
|
|
147756
147870
|
error: `provider error: ${lastProviderError}`,
|
|
147757
147871
|
usage,
|
|
147758
147872
|
sessionId
|
|
@@ -147761,13 +147875,13 @@ ${stderrContext}`);
|
|
|
147761
147875
|
if (resultErrorSubtype) {
|
|
147762
147876
|
return {
|
|
147763
147877
|
success: false,
|
|
147764
|
-
output: finalOutput || output,
|
|
147878
|
+
output: finalOutput || output.toString(),
|
|
147765
147879
|
error: lastResultError || `result subtype: ${resultErrorSubtype}`,
|
|
147766
147880
|
usage,
|
|
147767
147881
|
sessionId
|
|
147768
147882
|
};
|
|
147769
147883
|
}
|
|
147770
|
-
return { success: true, output: finalOutput || output, usage, sessionId };
|
|
147884
|
+
return { success: true, output: finalOutput || output.toString(), usage, sessionId };
|
|
147771
147885
|
} catch (error49) {
|
|
147772
147886
|
params.todoTracker?.cancel();
|
|
147773
147887
|
const duration4 = performance6.now() - startTime;
|
|
@@ -147786,7 +147900,7 @@ ${stderrContext}`
|
|
|
147786
147900
|
);
|
|
147787
147901
|
return {
|
|
147788
147902
|
success: false,
|
|
147789
|
-
output: finalOutput || output,
|
|
147903
|
+
output: finalOutput || output.toString(),
|
|
147790
147904
|
error: `${errorMessage} [${diagnosis}]`,
|
|
147791
147905
|
usage: buildUsage(),
|
|
147792
147906
|
sessionId
|
|
@@ -148022,6 +148136,15 @@ function buildSecurityConfig(ctx, model) {
|
|
|
148022
148136
|
[pullfrogMcpName]: { type: "remote", url: ctx.mcpServerUrl }
|
|
148023
148137
|
},
|
|
148024
148138
|
agent: buildReviewerAgentConfig(),
|
|
148139
|
+
// opt into opencode's experimental `batch` tool (added in
|
|
148140
|
+
// anomalyco/opencode PR #2983, opt-in via `experimental.batch_tool`). it
|
|
148141
|
+
// exposes a single `batch` tool that runs 1-25 independent tool calls
|
|
148142
|
+
// (read/grep/glob/bash/etc.) concurrently in one assistant turn, which
|
|
148143
|
+
// collapses the dominant grep→20×read pattern into a single round trip.
|
|
148144
|
+
// edits are explicitly disallowed inside the batch upstream. paired with
|
|
148145
|
+
// the "Parallel tool execution" guidance in utils/instructions.ts so the
|
|
148146
|
+
// model actually reaches for it. see wiki/prompt.md.
|
|
148147
|
+
experimental: { batch_tool: true },
|
|
148025
148148
|
provider: {
|
|
148026
148149
|
google: {
|
|
148027
148150
|
models: Object.fromEntries(
|
|
@@ -148094,7 +148217,6 @@ function autoSelectModel(cliPath) {
|
|
|
148094
148217
|
async function runOpenCode(params) {
|
|
148095
148218
|
const startTime = performance7.now();
|
|
148096
148219
|
let eventCount = 0;
|
|
148097
|
-
const thinkingTimer = new ThinkingTimer();
|
|
148098
148220
|
let finalOutput = "";
|
|
148099
148221
|
let accumulatedTokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
148100
148222
|
let accumulatedCostUsd = 0;
|
|
@@ -148111,6 +148233,16 @@ async function runOpenCode(params) {
|
|
|
148111
148233
|
function withLabel(label, message) {
|
|
148112
148234
|
return label === ORCHESTRATOR_LABEL ? message : formatWithLabel(label, message);
|
|
148113
148235
|
}
|
|
148236
|
+
const thinkingTimers = /* @__PURE__ */ new Map();
|
|
148237
|
+
function timerFor(label) {
|
|
148238
|
+
let t2 = thinkingTimers.get(label);
|
|
148239
|
+
if (!t2) {
|
|
148240
|
+
const formatLine = (line) => label === ORCHESTRATOR_LABEL ? line : formatWithLabel(label, line);
|
|
148241
|
+
t2 = new ThinkingTimer(formatLine);
|
|
148242
|
+
thinkingTimers.set(label, t2);
|
|
148243
|
+
}
|
|
148244
|
+
return t2;
|
|
148245
|
+
}
|
|
148114
148246
|
const taskDispatchByCallID = /* @__PURE__ */ new Map();
|
|
148115
148247
|
const pendingTaskDispatches = [];
|
|
148116
148248
|
const knownNonTaskCallIDs = /* @__PURE__ */ new Set();
|
|
@@ -148259,7 +148391,7 @@ async function runOpenCode(params) {
|
|
|
148259
148391
|
input: event.part?.state?.input
|
|
148260
148392
|
});
|
|
148261
148393
|
}
|
|
148262
|
-
|
|
148394
|
+
timerFor(label).markToolCall();
|
|
148263
148395
|
const inputFormatted = formatJsonValue(event.part?.state?.input || {});
|
|
148264
148396
|
const toolCallLine = inputFormatted !== "{}" ? `\xBB ${toolName}(${inputFormatted})` : `\xBB ${toolName}()`;
|
|
148265
148397
|
log.info(withLabel(label, toolCallLine));
|
|
@@ -148283,7 +148415,7 @@ async function runOpenCode(params) {
|
|
|
148283
148415
|
const status = event.part?.state?.status || event.status || "unknown";
|
|
148284
148416
|
const output2 = event.part?.state?.output || event.output;
|
|
148285
148417
|
const label = eventLabel(event);
|
|
148286
|
-
|
|
148418
|
+
timerFor(label).markToolResult();
|
|
148287
148419
|
if (taskDispatchByCallID.size > 0 || pendingTaskDispatches.length > 0) {
|
|
148288
148420
|
if (toolId && taskDispatchByCallID.has(toolId)) {
|
|
148289
148421
|
const dispatch = taskDispatchByCallID.get(toolId);
|
|
@@ -148408,7 +148540,7 @@ async function runOpenCode(params) {
|
|
|
148408
148540
|
const recentStderr = [];
|
|
148409
148541
|
let lastProviderError = null;
|
|
148410
148542
|
let agentErrorEvent = null;
|
|
148411
|
-
|
|
148543
|
+
const output = new TailBuffer(DEFAULT_MAX_RETAINED_BYTES);
|
|
148412
148544
|
let stdoutBuffer = "";
|
|
148413
148545
|
try {
|
|
148414
148546
|
const result = await spawn({
|
|
@@ -148426,6 +148558,11 @@ async function runOpenCode(params) {
|
|
|
148426
148558
|
// never fires — producing zombie runs. detached + killGroup nukes the
|
|
148427
148559
|
// whole tree.
|
|
148428
148560
|
killGroup: true,
|
|
148561
|
+
// we already drain every chunk via onStdout/onStderr (NDJSON parsing
|
|
148562
|
+
// + recentStderr ring buffer). retaining a second copy in the spawn
|
|
148563
|
+
// wrapper would grow unbounded for multi-lens Reviews and previously
|
|
148564
|
+
// crashed the wrapper with RangeError at ~1 GiB. see issue #680.
|
|
148565
|
+
retain: "none",
|
|
148429
148566
|
// NB: we used to pass `isPausedExternally: isSubagentInFlight` to suspend
|
|
148430
148567
|
// the activity timer during subagent dispatches. unnecessary now that
|
|
148431
148568
|
// our injected plugin (action/agents/opencodePlugin.ts) re-emits
|
|
@@ -148435,7 +148572,7 @@ async function runOpenCode(params) {
|
|
|
148435
148572
|
// (~3.3 plugin events/sec during a typical subagent run).
|
|
148436
148573
|
onStdout: async (chunk) => {
|
|
148437
148574
|
const text = chunk.toString();
|
|
148438
|
-
output
|
|
148575
|
+
output.append(text);
|
|
148439
148576
|
markActivity();
|
|
148440
148577
|
stdoutBuffer += text;
|
|
148441
148578
|
const lines = stdoutBuffer.split("\n");
|
|
@@ -148524,18 +148661,25 @@ ${stderrContext}`);
|
|
|
148524
148661
|
const usage = buildUsage();
|
|
148525
148662
|
if (result.exitCode !== 0) {
|
|
148526
148663
|
const errorContext = lastProviderError ? ` (${lastProviderError})` : "";
|
|
148527
|
-
const
|
|
148664
|
+
const stdoutSnapshot = output.toString();
|
|
148665
|
+
const stderrSnapshot = recentStderr.join("\n");
|
|
148666
|
+
const errorMessage = stderrSnapshot || stdoutSnapshot || `unknown error - no output from OpenCode CLI${errorContext}`;
|
|
148528
148667
|
log.error(
|
|
148529
148668
|
`${params.label} exited with code ${result.exitCode}${errorContext}: ${errorMessage}`
|
|
148530
148669
|
);
|
|
148531
|
-
log.debug(`stdout: ${
|
|
148532
|
-
log.debug(`stderr: ${
|
|
148533
|
-
return {
|
|
148670
|
+
log.debug(`stdout: ${stdoutSnapshot.substring(0, 500)}`);
|
|
148671
|
+
log.debug(`stderr: ${stderrSnapshot.substring(0, 500)}`);
|
|
148672
|
+
return {
|
|
148673
|
+
success: false,
|
|
148674
|
+
output: finalOutput || stdoutSnapshot,
|
|
148675
|
+
error: errorMessage,
|
|
148676
|
+
usage
|
|
148677
|
+
};
|
|
148534
148678
|
}
|
|
148535
148679
|
if (eventCount === 0 && lastProviderError) {
|
|
148536
148680
|
return {
|
|
148537
148681
|
success: false,
|
|
148538
|
-
output: finalOutput || output,
|
|
148682
|
+
output: finalOutput || output.toString(),
|
|
148539
148683
|
error: `provider error: ${lastProviderError}`,
|
|
148540
148684
|
usage
|
|
148541
148685
|
};
|
|
@@ -148546,12 +148690,12 @@ ${stderrContext}`);
|
|
|
148546
148690
|
const errorMessage = errorEvent.error?.data?.message || errorEvent.error?.name || JSON.stringify(errorEvent);
|
|
148547
148691
|
return {
|
|
148548
148692
|
success: false,
|
|
148549
|
-
output: finalOutput || output,
|
|
148693
|
+
output: finalOutput || output.toString(),
|
|
148550
148694
|
error: `${errorName}: ${errorMessage}`,
|
|
148551
148695
|
usage
|
|
148552
148696
|
};
|
|
148553
148697
|
}
|
|
148554
|
-
return { success: true, output: finalOutput || output, usage };
|
|
148698
|
+
return { success: true, output: finalOutput || output.toString(), usage };
|
|
148555
148699
|
} catch (error49) {
|
|
148556
148700
|
params.todoTracker?.cancel();
|
|
148557
148701
|
const duration4 = performance7.now() - startTime;
|
|
@@ -148570,7 +148714,7 @@ ${stderrContext}`
|
|
|
148570
148714
|
);
|
|
148571
148715
|
return {
|
|
148572
148716
|
success: false,
|
|
148573
|
-
output: finalOutput || output,
|
|
148717
|
+
output: finalOutput || output.toString(),
|
|
148574
148718
|
error: `${errorMessage} [${diagnosis}]`,
|
|
148575
148719
|
usage: buildUsage()
|
|
148576
148720
|
};
|
|
@@ -152476,6 +152620,14 @@ function isOIDCAvailable() {
|
|
|
152476
152620
|
process.env.ACTIONS_ID_TOKEN_REQUEST_URL && process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN
|
|
152477
152621
|
);
|
|
152478
152622
|
}
|
|
152623
|
+
var TokenExchangeError = class extends Error {
|
|
152624
|
+
status;
|
|
152625
|
+
constructor(status, message) {
|
|
152626
|
+
super(message);
|
|
152627
|
+
this.name = "TokenExchangeError";
|
|
152628
|
+
this.status = status;
|
|
152629
|
+
}
|
|
152630
|
+
};
|
|
152479
152631
|
async function acquireTokenViaOIDC(opts) {
|
|
152480
152632
|
const oidcToken = await core2.getIDToken("pullfrog-api");
|
|
152481
152633
|
const repos = [...opts?.repos ?? []];
|
|
@@ -152500,7 +152652,16 @@ async function acquireTokenViaOIDC(opts) {
|
|
|
152500
152652
|
});
|
|
152501
152653
|
clearTimeout(timeoutId);
|
|
152502
152654
|
if (!tokenResponse.ok) {
|
|
152503
|
-
|
|
152655
|
+
let serverMessage;
|
|
152656
|
+
try {
|
|
152657
|
+
const body = await tokenResponse.json();
|
|
152658
|
+
if (typeof body.error === "string") serverMessage = body.error;
|
|
152659
|
+
} catch {
|
|
152660
|
+
}
|
|
152661
|
+
throw new TokenExchangeError(
|
|
152662
|
+
tokenResponse.status,
|
|
152663
|
+
serverMessage ?? `Token exchange failed: ${tokenResponse.status} ${tokenResponse.statusText}`
|
|
152664
|
+
);
|
|
152504
152665
|
}
|
|
152505
152666
|
const tokenData = await tokenResponse.json();
|
|
152506
152667
|
return tokenData.token;
|
|
@@ -152621,7 +152782,10 @@ async function acquireNewToken(opts) {
|
|
|
152621
152782
|
if (isOIDCAvailable()) {
|
|
152622
152783
|
return await retry(() => acquireTokenViaOIDC(opts), {
|
|
152623
152784
|
label: "token exchange",
|
|
152624
|
-
shouldRetry: (error49) =>
|
|
152785
|
+
shouldRetry: (error49) => {
|
|
152786
|
+
if (error49 instanceof TokenExchangeError) return error49.status >= 500 || error49.status === 429;
|
|
152787
|
+
return error49 instanceof Error && (error49.message.includes("timed out") || error49.message.includes("fetch failed") || error49.message.includes("ECONNRESET") || error49.message.includes("ETIMEDOUT"));
|
|
152788
|
+
}
|
|
152625
152789
|
});
|
|
152626
152790
|
} else {
|
|
152627
152791
|
return await acquireTokenViaGitHubApp(opts);
|
|
@@ -153168,6 +153332,21 @@ ${getStandaloneModeInstructions(ctx.payload.event.trigger, t2, ctx.outputSchema)
|
|
|
153168
153332
|
|
|
153169
153333
|
Trust the tools \u2014 do not repeatedly verify file contents or git status after operations. If a tool reports success, proceed to the next step. Only verify if you encounter an actual error. Exception: right before \`${t2("push_branch")}\`, ensure the working tree is clean \u2014 that tool rejects dirty trees, and tests you ran earlier often leave untracked output.
|
|
153170
153334
|
|
|
153335
|
+
### Parallel tool execution
|
|
153336
|
+
|
|
153337
|
+
For maximum efficiency, whenever you need to perform multiple independent operations, invoke all relevant tools simultaneously in a single assistant turn rather than sequentially. The dominant failure mode is grep \u2192 read \u2192 read \u2192 read \u2192 read across separate turns when one round trip would do. Always parallelize when calls are independent:
|
|
153338
|
+
- reading multiple files (especially after a grep returns candidates)
|
|
153339
|
+
- multiple greps with different patterns
|
|
153340
|
+
- glob + grep + read combos
|
|
153341
|
+
- listing multiple directories
|
|
153342
|
+
- inspecting multiple MCP tools or resources
|
|
153343
|
+
|
|
153344
|
+
Do NOT parallelize operations that depend on prior output (e.g. create a file then read it), or ordered stateful mutations. Edits are not parallelizable \u2014 sequence those normally.${ctx.agentId === "opencode" ? `
|
|
153345
|
+
|
|
153346
|
+
On OpenCode you also have a \`batch\` tool that bundles 1-25 independent calls into one wrapper call. Reach for it whenever you have >=2 independent calls. Native parallel tool_use and \`batch\` both achieve one round trip instead of N \u2014 use whichever your provider supports best.` : `
|
|
153347
|
+
|
|
153348
|
+
Emit multiple \`tool_use\` blocks in the same assistant message for independent calls \u2014 the runtime executes them concurrently. Do not wait for one tool result before issuing the next independent call.`}
|
|
153349
|
+
|
|
153171
153350
|
### Command execution
|
|
153172
153351
|
|
|
153173
153352
|
Never use \`sleep\` to wait for commands to complete. Commands run synchronously \u2014 when the shell tool returns, the command has finished.
|
|
@@ -153294,10 +153473,22 @@ async function readLearningsFile(path3) {
|
|
|
153294
153473
|
}
|
|
153295
153474
|
|
|
153296
153475
|
// utils/normalizeEnv.ts
|
|
153297
|
-
|
|
153298
|
-
|
|
153299
|
-
|
|
153476
|
+
var core4 = __toESM(require_core(), 1);
|
|
153477
|
+
function sanitizeSecret(key, value2) {
|
|
153478
|
+
const trimmed = value2.trim();
|
|
153479
|
+
if (trimmed.length === 0) {
|
|
153480
|
+
log.warning(
|
|
153481
|
+
`\xBB ${key} is whitespace-only \u2014 leaving env var unchanged. check your secret value.`
|
|
153482
|
+
);
|
|
153483
|
+
return null;
|
|
153484
|
+
}
|
|
153485
|
+
if (trimmed !== value2) {
|
|
153486
|
+
log.warning(
|
|
153487
|
+
`\xBB stripped whitespace from ${key} (whitespace in secret values breaks GitHub Actions log masking)`
|
|
153488
|
+
);
|
|
153300
153489
|
}
|
|
153490
|
+
core4.setSecret(trimmed);
|
|
153491
|
+
return trimmed;
|
|
153301
153492
|
}
|
|
153302
153493
|
function normalizeEnv() {
|
|
153303
153494
|
const upperKeys = /* @__PURE__ */ new Map();
|
|
@@ -153308,11 +153499,6 @@ function normalizeEnv() {
|
|
|
153308
153499
|
upperKeys.set(upper2, existing);
|
|
153309
153500
|
}
|
|
153310
153501
|
for (const [upperKey, keys] of upperKeys) {
|
|
153311
|
-
if (isSensitiveEnvName(upperKey)) {
|
|
153312
|
-
for (const key of keys) {
|
|
153313
|
-
maskValue(process.env[key]);
|
|
153314
|
-
}
|
|
153315
|
-
}
|
|
153316
153502
|
if (keys.length === 1) {
|
|
153317
153503
|
const key = keys[0];
|
|
153318
153504
|
if (key !== upperKey) {
|
|
@@ -153335,10 +153521,17 @@ function normalizeEnv() {
|
|
|
153335
153521
|
}
|
|
153336
153522
|
process.env[upperKey] = preferredValue;
|
|
153337
153523
|
}
|
|
153524
|
+
for (const key of Object.keys(process.env)) {
|
|
153525
|
+
if (!isSensitiveEnvName(key)) continue;
|
|
153526
|
+
const value2 = process.env[key];
|
|
153527
|
+
if (typeof value2 !== "string" || value2.length === 0) continue;
|
|
153528
|
+
const sanitized = sanitizeSecret(key, value2);
|
|
153529
|
+
if (sanitized !== null) process.env[key] = sanitized;
|
|
153530
|
+
}
|
|
153338
153531
|
}
|
|
153339
153532
|
|
|
153340
153533
|
// utils/payload.ts
|
|
153341
|
-
var
|
|
153534
|
+
var core5 = __toESM(require_core(), 1);
|
|
153342
153535
|
import { isAbsolute as isAbsolute2, resolve as resolve2 } from "node:path";
|
|
153343
153536
|
|
|
153344
153537
|
// utils/versioning.ts
|
|
@@ -153402,7 +153595,7 @@ function resolveCwd(cwd) {
|
|
|
153402
153595
|
return workspace ? resolve2(workspace, cwd) : cwd;
|
|
153403
153596
|
}
|
|
153404
153597
|
function resolvePromptInput() {
|
|
153405
|
-
const prompt =
|
|
153598
|
+
const prompt = core5.getInput("prompt", { required: true });
|
|
153406
153599
|
let parsed2;
|
|
153407
153600
|
try {
|
|
153408
153601
|
parsed2 = JSON.parse(prompt);
|
|
@@ -153418,11 +153611,11 @@ function resolvePromptInput() {
|
|
|
153418
153611
|
}
|
|
153419
153612
|
function resolveNonPromptInputs() {
|
|
153420
153613
|
return Inputs.omit("prompt").assert({
|
|
153421
|
-
model:
|
|
153422
|
-
timeout:
|
|
153423
|
-
cwd:
|
|
153424
|
-
push:
|
|
153425
|
-
shell:
|
|
153614
|
+
model: core5.getInput("model") || void 0,
|
|
153615
|
+
timeout: core5.getInput("timeout") || void 0,
|
|
153616
|
+
cwd: core5.getInput("cwd") || void 0,
|
|
153617
|
+
push: core5.getInput("push") || void 0,
|
|
153618
|
+
shell: core5.getInput("shell") || void 0
|
|
153426
153619
|
});
|
|
153427
153620
|
}
|
|
153428
153621
|
var isPullfrog = (actor) => {
|
|
@@ -153637,8 +153830,7 @@ async function fetchRunContext(params) {
|
|
|
153637
153830
|
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
153638
153831
|
try {
|
|
153639
153832
|
const headers = {
|
|
153640
|
-
Authorization: `Bearer ${params.token}
|
|
153641
|
-
"Content-Type": "application/json"
|
|
153833
|
+
Authorization: `Bearer ${params.token}`
|
|
153642
153834
|
};
|
|
153643
153835
|
if (params.oidcToken) {
|
|
153644
153836
|
headers["X-GitHub-OIDC-Token"] = params.oidcToken;
|
|
@@ -153679,13 +153871,13 @@ async function fetchRunContext(params) {
|
|
|
153679
153871
|
}
|
|
153680
153872
|
|
|
153681
153873
|
// utils/runContextData.ts
|
|
153682
|
-
var
|
|
153874
|
+
var core6 = __toESM(require_core(), 1);
|
|
153683
153875
|
async function resolveRunContextData(params) {
|
|
153684
153876
|
log.info(`\xBB running Pullfrog v${package_default.version}...`);
|
|
153685
153877
|
const repoContext = parseRepoContext();
|
|
153686
153878
|
let oidcToken;
|
|
153687
153879
|
try {
|
|
153688
|
-
oidcToken = await
|
|
153880
|
+
oidcToken = await core6.getIDToken("pullfrog-api");
|
|
153689
153881
|
} catch {
|
|
153690
153882
|
}
|
|
153691
153883
|
const [repoResponse, runContext] = await Promise.all([
|
|
@@ -153988,7 +154180,7 @@ async function resolveRun(params) {
|
|
|
153988
154180
|
|
|
153989
154181
|
// main.ts
|
|
153990
154182
|
function resolveOutputSchema() {
|
|
153991
|
-
const raw2 =
|
|
154183
|
+
const raw2 = core7.getInput("output_schema");
|
|
153992
154184
|
if (!raw2) return void 0;
|
|
153993
154185
|
let parsed2;
|
|
153994
154186
|
try {
|
|
@@ -154054,7 +154246,7 @@ function formatBillingErrorSummary(error49, owner) {
|
|
|
154054
154246
|
return [
|
|
154055
154247
|
"**Add a card to start using Pullfrog Router.**",
|
|
154056
154248
|
"",
|
|
154057
|
-
"Router proxies OpenRouter at raw cost \u2014 no platform markup
|
|
154249
|
+
"Router proxies OpenRouter at raw cost \u2014 no platform markup. Add a card and we'll auto-reload your wallet so runs keep flowing.",
|
|
154058
154250
|
"",
|
|
154059
154251
|
`[Add a card \u2192](${billingConsoleUrl(owner, "model-access")})`
|
|
154060
154252
|
].join("\n");
|
|
@@ -154156,7 +154348,7 @@ async function buildProxyTokenHeaders(ctx) {
|
|
|
154156
154348
|
if (ctx.oidcCredentials) {
|
|
154157
154349
|
process.env.ACTIONS_ID_TOKEN_REQUEST_URL = ctx.oidcCredentials.requestUrl;
|
|
154158
154350
|
process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = ctx.oidcCredentials.requestToken;
|
|
154159
|
-
const oidcToken = await
|
|
154351
|
+
const oidcToken = await core7.getIDToken("pullfrog-api");
|
|
154160
154352
|
delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
154161
154353
|
delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
154162
154354
|
return { Authorization: `Bearer ${oidcToken}` };
|
|
@@ -154178,7 +154370,7 @@ async function resolveProxyModel(ctx) {
|
|
|
154178
154370
|
const key = await mintProxyKey({ oidcCredentials: ctx.oidcCredentials, repo: ctx.repo });
|
|
154179
154371
|
if (!key) return;
|
|
154180
154372
|
process.env.OPENROUTER_API_KEY = key;
|
|
154181
|
-
|
|
154373
|
+
core7.setSecret(key);
|
|
154182
154374
|
ctx.payload.proxyModel = ctx.proxyModel;
|
|
154183
154375
|
const label = ctx.oss ? "oss" : "router";
|
|
154184
154376
|
log.info(`\xBB proxy: ${label} \u2192 ${ctx.proxyModel}`);
|
|
@@ -154230,12 +154422,12 @@ async function persistLearnings(ctx) {
|
|
|
154230
154422
|
});
|
|
154231
154423
|
if (!response.ok) {
|
|
154232
154424
|
const error49 = await response.text().catch(() => "(no body)");
|
|
154233
|
-
log.
|
|
154425
|
+
log.warning(`learnings persist failed (${response.status}): ${error49}`);
|
|
154234
154426
|
return;
|
|
154235
154427
|
}
|
|
154236
154428
|
log.info("\xBB learnings updated");
|
|
154237
154429
|
} catch (err) {
|
|
154238
|
-
log.
|
|
154430
|
+
log.warning(`learnings persist failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
154239
154431
|
}
|
|
154240
154432
|
}
|
|
154241
154433
|
async function persistSummary(ctx) {
|
|
@@ -154290,8 +154482,8 @@ async function main() {
|
|
|
154290
154482
|
if (runContext.dbSecrets) {
|
|
154291
154483
|
for (const [key, value2] of Object.entries(runContext.dbSecrets)) {
|
|
154292
154484
|
if (!process.env[key]) {
|
|
154293
|
-
|
|
154294
|
-
|
|
154485
|
+
const sanitized = sanitizeSecret(key, value2);
|
|
154486
|
+
if (sanitized !== null) process.env[key] = sanitized;
|
|
154295
154487
|
}
|
|
154296
154488
|
}
|
|
154297
154489
|
const count = Object.keys(runContext.dbSecrets).length;
|
|
@@ -154629,7 +154821,7 @@ ${instructions.user}` : null,
|
|
|
154629
154821
|
}
|
|
154630
154822
|
if (toolState.output) {
|
|
154631
154823
|
log.info(`::pullfrog-output::${Buffer.from(toolState.output).toString("base64")}`);
|
|
154632
|
-
|
|
154824
|
+
core7.setOutput("result", toolState.output);
|
|
154633
154825
|
}
|
|
154634
154826
|
return await handleAgentResult({
|
|
154635
154827
|
result,
|
|
@@ -154718,27 +154910,27 @@ async function runMain() {
|
|
|
154718
154910
|
}
|
|
154719
154911
|
} catch (error49) {
|
|
154720
154912
|
const errorMessage = error49 instanceof Error ? error49.message : "unknown error occurred";
|
|
154721
|
-
|
|
154913
|
+
core8.setFailed(`action failed: ${errorMessage}`);
|
|
154722
154914
|
}
|
|
154723
154915
|
}
|
|
154724
154916
|
async function tokenMain() {
|
|
154725
|
-
const reposInput =
|
|
154917
|
+
const reposInput = core8.getInput("repos");
|
|
154726
154918
|
const additionalRepos = reposInput ? reposInput.split(",").map((r) => r.trim()).filter(Boolean) : [];
|
|
154727
154919
|
const token = await acquireNewToken({ repos: additionalRepos });
|
|
154728
|
-
|
|
154729
|
-
|
|
154730
|
-
|
|
154920
|
+
core8.setSecret(token);
|
|
154921
|
+
core8.saveState(STATE_TOKEN, token);
|
|
154922
|
+
core8.setOutput("token", token);
|
|
154731
154923
|
const scope2 = additionalRepos.length ? `current repo + ${additionalRepos.join(", ")}` : "current repo only";
|
|
154732
|
-
|
|
154924
|
+
core8.info(`\xBB installation token acquired (${scope2})`);
|
|
154733
154925
|
}
|
|
154734
154926
|
async function tokenPost() {
|
|
154735
|
-
const token =
|
|
154927
|
+
const token = core8.getState(STATE_TOKEN);
|
|
154736
154928
|
if (!token) {
|
|
154737
|
-
|
|
154929
|
+
core8.debug("no token found in state, skipping revocation");
|
|
154738
154930
|
return;
|
|
154739
154931
|
}
|
|
154740
154932
|
await revokeGitHubInstallationToken(token);
|
|
154741
|
-
|
|
154933
|
+
core8.info("\xBB installation token revoked");
|
|
154742
154934
|
}
|
|
154743
154935
|
function printGhaUsage(params) {
|
|
154744
154936
|
params.stream(`usage: ${params.prog} gha [subcommand]
|
|
@@ -154854,7 +155046,7 @@ async function run(args2) {
|
|
|
154854
155046
|
}
|
|
154855
155047
|
} catch (error49) {
|
|
154856
155048
|
const message = error49 instanceof Error ? error49.message : String(error49);
|
|
154857
|
-
|
|
155049
|
+
core8.setFailed(message);
|
|
154858
155050
|
}
|
|
154859
155051
|
}
|
|
154860
155052
|
|
|
@@ -156518,7 +156710,7 @@ async function run2() {
|
|
|
156518
156710
|
}
|
|
156519
156711
|
|
|
156520
156712
|
// cli.ts
|
|
156521
|
-
var VERSION10 = "0.1.
|
|
156713
|
+
var VERSION10 = "0.1.6";
|
|
156522
156714
|
var bin = basename2(process.argv[1] || "");
|
|
156523
156715
|
var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
|
|
156524
156716
|
var rawArgs = process.argv.slice(2);
|