Compare commits

...

10 Commits

Author SHA1 Message Date
Colin McDonnell d5ab3706db 173 2026-02-25 01:38:04 +00:00
Colin McDonnell f8a871f723 fix sudo-unshare sandbox: drop privileges after PROC_CLEANUP (#383)
the sudo-unshare sandbox path runs the entire command as root, causing
files modified by shell commands (e.g. git merge) to become root-owned.
this breaks file_write/file_edit which run in the Node.js parent process
as the normal user (EACCES errors).

after PROC_CLEANUP (which needs root for umount/mount), drop back to
the original user via `su -p` so file operations match the uid of the
parent process. security-neutral: PID namespace isolation is the barrier,
not privilege level inside it.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-25 01:37:40 +00:00
Colin McDonnell 52ec35790a fix review reply trigger: treat replies to Pullfrog threads as implicit triggers, only add eyes reaction when dispatching
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 19:28:08 +00:00
Colin McDonnell edd240f535 bump action to 0.0.172 (fix version regression from #354 squash merge)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 15:36:51 +00:00
Colin McDonnell cd1ea5267c fix node24 PATH propagation and improve action logging (#381)
Add node24 binary directory to PATH in action entry point so spawned
processes (pnpm, npm, etc.) resolve to the correct node version instead
of the runner's default v20. Improve delegate task result logging with
success/failure status and summaries. Use collapsible log groups for
dependency install output instead of raw streaming.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 15:32:32 +00:00
Colin McDonnell 4f1e4a2e7a fix formatting in shell.ts
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 14:41:56 +00:00
Colin McDonnell 73836d9c8f harden proc isolation and filter cross-fork check suite PRs
shell sandbox: double-umount + remount /proc to prevent exfiltration
when agent peels off --mount-proc overlay.

webhooks: filter check_suite.pull_requests to same-repo PRs only
(excludes cross-fork sync PRs) and skip merged/closed PRs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 14:39:39 +00:00
Mateusz Burzyński da72d0d6ee Fixed semantic conflict between #377 and #354 (#380)
* Move out checking out PRs from `etupGit` (#377 intent)

* Keep subagent-related changes from #377
2026-02-24 14:20:59 +00:00
Colin McDonnell b472aa1ba9 fix(opencode): add effort-aware model overrides and OpenRouter guidance (#354)
* chore: create empty commit for PR

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(action): trigger preview repo creation workflow

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(opencode): merge repo opencode config and log provider key presence

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(opencode): add effort-aware model override precedence

Add OPENCODE_MODEL_MINI and OPENCODE_MODEL_MAX support with fallback to OPENCODE_MODEL and auto-selection, and keep provider resolution aligned via inline OpenCode config when overrides are used. Update workflow env wiring, test allowlists, and docs (including OpenRouter setup guidance and sidebar ordering) to document the new behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(effort): clarify OpenCode precedence and add section link

Add a concise OpenCode precedence callout in the summary area and link directly to the OpenCode section for full details and examples.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Restructure dash (#372)

* Restructure dash

* WIP

* WIP

* refactor trigger UI: extract PR summary card, add mentions section, rename labels

Co-authored-by: Cursor <cursoragent@cursor.com>

* clean up console UI: remove info icons from section descriptions, rename mentions trigger

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix review feedback: layout, terminology, form scope

- extract console sidebar sections to module-level constant
- align three-column layout breakpoints to xl (match sidebar visibility)
- fix mixed shell/bash terminology in beta page
- scope FormProvider to trigger sections only, restore autoComplete="off"

Co-authored-by: Cursor <cursoragent@cursor.com>

* Bump

---------

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: create empty commit for PR

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(opencode): merge repo opencode config and log provider key presence

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(opencode): add effort-aware model override precedence

Add OPENCODE_MODEL_MINI and OPENCODE_MODEL_MAX support with fallback to OPENCODE_MODEL and auto-selection, and keep provider resolution aligned via inline OpenCode config when overrides are used. Update workflow env wiring, test allowlists, and docs (including OpenRouter setup guidance and sidebar ordering) to document the new behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: update agent rules and OpenCode config docs

Align AGENTS guidance with current preferences and apply review-driven wording updates in OpenCode-related files.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:05:26 +00:00
Colin McDonnell e2d8dfeebf Clarify shell guidance and delegation checklists
Standardize orchestrator/subagent instructions on the MCP shell tool and format mode guidance as explicit checklists to make delegation flows easier to follow. Bump the action package version to 0.0.171 for this release.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 01:28:50 +00:00
18 changed files with 612 additions and 277 deletions
+2
View File
@@ -39,6 +39,8 @@ jobs:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GEMINI_MODEL: ${{ vars.GEMINI_MODEL }}
OPENCODE_MODEL: ${{ vars.OPENCODE_MODEL }}
OPENCODE_MODEL_MINI: ${{ vars.OPENCODE_MODEL_MINI }}
OPENCODE_MODEL_MAX: ${{ vars.OPENCODE_MODEL_MAX }}
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
+1 -1
View File
@@ -1,4 +1,4 @@
<!-- test preview system --> <!-- test bypass 2 -->
<!-- test preview system --> <!-- test bypass 2 --> <!-- trigger preview repo creation -->
<p align="center">
<h1 align="center">
<picture>
+218 -22
View File
@@ -1,7 +1,7 @@
// changes to effort level configuration should be reflected in wiki/effort.md and docs/effort.mdx
// changes to tool permissions should be reflected in wiki/granular-tools.md
// changes to web search configuration should be reflected in wiki/websearch.md
import { mkdirSync, writeFileSync } from "node:fs";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { performance } from "node:perf_hooks";
import { ghPullfrogMcpName } from "../external.ts";
@@ -38,6 +38,159 @@ function detectProviderError(text: string): string | null {
return null;
}
type OpenCodeConfig = {
mcp?: Record<string, unknown>;
permission?: Record<string, unknown>;
provider?: Record<string, unknown>;
model?: string;
enabled_providers?: string[];
[key: string]: unknown;
};
type RecordPropertyContext = {
value: unknown;
key: string;
};
type RepoConfigLoadContext = {
repoConfigPath: string;
};
type ProviderFromModelContext = {
model: string;
};
type InlineConfigOverrideContext = {
model: string;
};
type InlineConfigOverride = {
providerId: string;
content: string;
};
type ModelOverrideResolutionContext = {
effort: AgentRunContext["payload"]["effort"];
env: NodeJS.ProcessEnv;
};
type ModelOverrideResolution = {
model: string;
source: "OPENCODE_MODEL_MINI" | "OPENCODE_MODEL_MAX" | "OPENCODE_MODEL";
};
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function getRecordProperty(ctx: RecordPropertyContext): Record<string, unknown> | undefined {
if (!isRecord(ctx.value)) {
return undefined;
}
const propertyValue = ctx.value[ctx.key];
if (!isRecord(propertyValue)) {
return undefined;
}
return propertyValue;
}
function loadRepoOpenCodeConfig(ctx: RepoConfigLoadContext): OpenCodeConfig | undefined {
if (!existsSync(ctx.repoConfigPath)) {
log.info(`» repo opencode.json not found at ${ctx.repoConfigPath}`);
return undefined;
}
try {
const rawConfig = readFileSync(ctx.repoConfigPath, "utf-8");
const parsedConfig = JSON.parse(rawConfig);
if (!isRecord(parsedConfig)) {
log.warning(`» repo opencode.json is not an object: ${ctx.repoConfigPath}`);
return undefined;
}
const providerConfig = getRecordProperty({ value: parsedConfig, key: "provider" });
if (providerConfig) {
const providerNames = Object.keys(providerConfig);
log.info(`» repo opencode provider config detected: ${providerNames.join(", ")}`);
}
const result: OpenCodeConfig = parsedConfig;
log.info(`» loaded repo opencode.json from ${ctx.repoConfigPath}`);
return result;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.warning(`» failed to parse repo opencode.json at ${ctx.repoConfigPath}: ${errorMessage}`);
return undefined;
}
}
function parseProviderFromModel(ctx: ProviderFromModelContext): string | undefined {
const trimmedModel = ctx.model.trim();
const slashIndex = trimmedModel.indexOf("/");
if (slashIndex <= 0) {
return undefined;
}
const providerId = trimmedModel.slice(0, slashIndex).trim().toLowerCase();
if (!providerId) {
return undefined;
}
return providerId;
}
function buildInlineConfigOverride(
ctx: InlineConfigOverrideContext
): InlineConfigOverride | undefined {
const providerId = parseProviderFromModel({ model: ctx.model });
if (!providerId) {
return undefined;
}
const inlineConfig: OpenCodeConfig = {
model: ctx.model,
enabled_providers: [providerId],
};
return {
providerId,
content: JSON.stringify(inlineConfig),
};
}
function readNonEmptyEnvVar(ctx: { env: NodeJS.ProcessEnv; name: string }): string | undefined {
const value = ctx.env[ctx.name];
if (!value) {
return undefined;
}
const trimmed = value.trim();
if (!trimmed) {
return undefined;
}
return trimmed;
}
function resolveModelOverride(
ctx: ModelOverrideResolutionContext
): ModelOverrideResolution | undefined {
if (ctx.effort === "mini") {
const miniModel = readNonEmptyEnvVar({ env: ctx.env, name: "OPENCODE_MODEL_MINI" });
if (miniModel) {
return { model: miniModel, source: "OPENCODE_MODEL_MINI" };
}
}
if (ctx.effort === "max") {
const maxModel = readNonEmptyEnvVar({ env: ctx.env, name: "OPENCODE_MODEL_MAX" });
if (maxModel) {
return { model: maxModel, source: "OPENCODE_MODEL_MAX" };
}
}
const baseModel = readNonEmptyEnvVar({ env: ctx.env, name: "OPENCODE_MODEL" });
if (!baseModel) {
return undefined;
}
return { model: baseModel, source: "OPENCODE_MODEL" };
}
async function installOpencode(): Promise<string> {
return await installFromNpmTarball({
packageName: "opencode-ai",
@@ -66,13 +219,18 @@ export const opencode = agent({
// this is critical for debugging since opencode run suppresses errors by default (Issue #752).
const args = ["run", ctx.instructions.full, "--format", "json", "--print-logs"];
// only override model when OPENCODE_MODEL is set (e.g., test environments with
// restricted API quotas). in production, OpenCode auto-selects the best available
// model based on which provider API keys are present.
const modelOverride = process.env.OPENCODE_MODEL;
// resolve model override from environment.
// precedence:
// 1) effort-specific overrides (OPENCODE_MODEL_MINI / OPENCODE_MODEL_MAX)
// 2) OPENCODE_MODEL fallback
// 3) OpenCode auto-select
const modelOverride = resolveModelOverride({
effort: ctx.payload.effort,
env: process.env,
});
if (modelOverride) {
args.push("--model", modelOverride);
log.info(`» model: ${modelOverride} (override)`);
args.push("--model", modelOverride.model);
log.info(`» model: ${modelOverride.model} (override via ${modelOverride.source})`);
} else {
log.info(`» model: auto-selected by OpenCode`);
}
@@ -89,6 +247,31 @@ export const opencode = agent({
GOOGLE_GENERATIVE_AI_API_KEY:
process.env.GOOGLE_GENERATIVE_AI_API_KEY || process.env.GEMINI_API_KEY,
};
if (modelOverride) {
const inlineOverride = buildInlineConfigOverride({ model: modelOverride.model });
if (inlineOverride) {
env.OPENCODE_CONFIG_CONTENT = inlineOverride.content;
log.info(
`» OpenCode inline config override enabled: provider=${inlineOverride.providerId}, model=${modelOverride.model}`
);
} else {
log.warning(
`» skipping OpenCode inline config override: unable to parse provider from model "${modelOverride.model}"`
);
}
}
const hasOpenRouterKey = Boolean(env.OPENROUTER_API_KEY);
const hasAnthropicKey = Boolean(env.ANTHROPIC_API_KEY);
const hasOpenAiKey = Boolean(env.OPENAI_API_KEY);
const hasGoogleKey = Boolean(
env.GOOGLE_API_KEY || env.GEMINI_API_KEY || env.GOOGLE_GENERATIVE_AI_API_KEY
);
log.info(
`» provider key presence: OPENROUTER=${hasOpenRouterKey ? "set" : "unset"}, ANTHROPIC=${hasAnthropicKey ? "set" : "unset"}, OPENAI=${hasOpenAiKey ? "set" : "unset"}, GOOGLE=${hasGoogleKey ? "set" : "unset"}`
);
// OpenCode doesn't support GitHub App installation tokens
delete env.GITHUB_TOKEN;
@@ -194,7 +377,7 @@ export const opencode = agent({
lastProviderError = providerError;
log.info(`» provider error detected (${providerError}): ${trimmed.substring(0, 500)}`);
} else {
// OpenCode's --print-logs output goes to stderr. demote internal
//agent OpenCode's --print-logs output goes to stderr. demote internal
// INFO/DEBUG bus traffic to debug so it doesn't drown out tool
// call logs in the GitHub Actions step output.
log.debug(trimmed);
@@ -301,28 +484,41 @@ function configureOpenCode(ctx: AgentRunContext): void {
const configDir = join(ctx.tmpdir, ".config", "opencode");
mkdirSync(configDir, { recursive: true });
const configPath = join(configDir, "opencode.json");
const repoConfigPath = join(process.cwd(), "opencode.json");
const repoConfig = loadRepoOpenCodeConfig({ repoConfigPath });
if (repoConfig?.model) {
log.info(`» repo opencode model configured: ${repoConfig.model}`);
}
// build MCP servers config
const opencodeMcpServers = {
[ghPullfrogMcpName]: { type: "remote" as const, url: ctx.mcpServerUrl },
};
const opencodeMcpServers: Record<string, unknown> = {};
const repoMcpServers = getRecordProperty({ value: repoConfig, key: "mcp" });
if (repoMcpServers) {
Object.assign(opencodeMcpServers, repoMcpServers);
}
opencodeMcpServers[ghPullfrogMcpName] = { type: "remote" as const, url: ctx.mcpServerUrl };
// build permission object based on tool permissions
// note: OpenCode has no built-in web search tool
const shell = ctx.payload.shell;
const permission = {
edit: "deny",
read: "deny",
bash: shell !== "enabled" ? "deny" : "allow",
webfetch: ctx.payload.web === "disabled" ? "deny" : "allow",
external_directory: "deny",
};
const permission: Record<string, unknown> = {};
const repoPermission = getRecordProperty({ value: repoConfig, key: "permission" });
if (repoPermission) {
Object.assign(permission, repoPermission);
}
permission.edit = "deny";
permission.read = "deny";
permission.bash = shell !== "enabled" ? "deny" : "allow";
permission.webfetch = ctx.payload.web === "disabled" ? "deny" : "allow";
permission.external_directory = "deny";
// build complete config in one object
const config = {
mcp: opencodeMcpServers,
permission,
};
const config: OpenCodeConfig = {};
if (repoConfig) {
Object.assign(config, repoConfig);
}
config.mcp = opencodeMcpServers;
config.permission = permission;
const configJson = JSON.stringify(config, null, 2);
try {
+257 -127
View File
@@ -56274,7 +56274,7 @@ var require_snapshot_recorder = __commonJS({
"node_modules/.pnpm/undici@7.16.0/node_modules/undici/lib/mock/snapshot-recorder.js"(exports, module) {
"use strict";
var { writeFile, readFile, mkdir } = __require("node:fs/promises");
var { dirname: dirname2, resolve: resolve3 } = __require("node:path");
var { dirname: dirname3, resolve: resolve3 } = __require("node:path");
var { setTimeout: setTimeout2, clearTimeout: clearTimeout2 } = __require("node:timers");
var { InvalidArgumentError, UndiciError } = require_errors4();
var { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = require_snapshot_utils();
@@ -56498,7 +56498,7 @@ var require_snapshot_recorder = __commonJS({
throw new InvalidArgumentError("Snapshot path is required");
}
const resolvedPath = resolve3(path3);
await mkdir(dirname2(resolvedPath), { recursive: true });
await mkdir(dirname3(resolvedPath), { recursive: true });
const data = Array.from(this.#snapshots.entries()).map(([hash2, snapshot2]) => ({
hash: hash2,
snapshot: snapshot2
@@ -97612,6 +97612,7 @@ var require_semver2 = __commonJS({
// entry.ts
var core5 = __toESM(require_core(), 1);
import { dirname as dirname2 } from "node:path";
// node_modules/.pnpm/@ark+util@0.56.0/node_modules/@ark/util/out/arrays.js
var liftArray = (data) => Array.isArray(data) ? data : [data];
@@ -136400,7 +136401,7 @@ var subagentSystemPreamble = `You are a focused subagent. Complete the task auto
Your tools are limited to:
- **File operations**: \`${ghPullfrogMcpName}/file_read\`, \`file_write\`, \`file_edit\`, \`file_delete\`, \`list_directory\`. Native file tools (Read, Write, StrReplace, etc.) are disabled \u2014 use the MCP versions.
- **Shell**: \`${ghPullfrogMcpName}/bash\` (if available). Use this for local git operations (\`git add\`, \`git commit\`, \`git diff\`, \`git log\`, \`git status\`), running tests, builds, and linters.
- **Shell**: \`${ghPullfrogMcpName}/shell\` (if available). Use this for local git operations (\`git add\`, \`git commit\`, \`git diff\`, \`git log\`, \`git status\`), running tests, builds, and linters.
- **Read-only GitHub**: \`get_pull_request\`, \`get_issue\`, \`get_issue_comments\`, \`get_issue_events\`, \`get_review_comments\`, \`list_pull_request_reviews\`, \`get_check_suite_logs\`, \`get_commit_info\`.
- **Output**: \`${ghPullfrogMcpName}/upload_file\`, \`${ghPullfrogMcpName}/set_output\`.
@@ -141611,7 +141612,7 @@ function buildTaskResult(label, effort, subagent, error49) {
function DelegateTool(ctx) {
return tool({
name: "delegate",
description: "Delegate research, local coding tasks, and codebase investigations to subagents. Accepts an array of tasks that run in parallel \u2014 use this to fan out work (e.g. reviewing different areas of a PR simultaneously). Each subagent receives ONLY the instructions you provide (plus a system preamble enforcing set_output). Use select_mode first to get guidance on how to craft instructions. Subagents have file operations, bash, read-only GitHub tools (PR/issue info, review comments, check suite logs), and upload_file. They have NO git/checkout tools (would conflict between parallel subagents), NO dependency tools, and NO GitHub-write tools (commenting, reviews, labels, issues). All state-mutating and user-facing operations are your responsibility as orchestrator.",
description: "Delegate research, local coding tasks, and codebase investigations to subagents. Accepts an array of tasks that run in parallel \u2014 use this to fan out work (e.g. reviewing different areas of a PR simultaneously). Each subagent receives ONLY the instructions you provide (plus a system preamble enforcing set_output). Use select_mode first to get guidance on how to craft instructions. Subagents have file operations, shell, read-only GitHub tools (PR/issue info, review comments, check suite logs), and upload_file. They have NO git/checkout tools (would conflict between parallel subagents), NO dependency tools, and NO GitHub-write tools (commenting, reviews, labels, issues). All state-mutating and user-facing operations are your responsibility as orchestrator.",
parameters: DelegateParams,
execute: execute(async (params) => {
if (ctx.toolState.selfSubagentId) {
@@ -141626,7 +141627,10 @@ function DelegateTool(ctx) {
if (!ctx.toolState.selectedMode) {
log.info(`\xBB warning: delegating without calling select_mode first (mode=${mode})`);
}
log.info(`\xBB delegating ${params.tasks.length} task(s) in parallel (mode=${mode})`);
const n = params.tasks.length;
log.info(
`\xBB delegating ${n} task${n === 1 ? "" : "s"}${n > 1 ? " in parallel" : ""} (mode=${mode})`
);
const taskEntries = params.tasks.map((task) => {
const effort = task.effort ?? "auto";
const subagent = createSubagentState({ ctx, mode, label: task.label });
@@ -141646,10 +141650,10 @@ function DelegateTool(ctx) {
const results = taskEntries.map((entry, i) => {
const outcome = settled[i];
const error49 = outcome.status === "rejected" ? String(outcome.reason) : outcome.value.error;
log.debug(
`\xBB task "${entry.task.label}" result: output=${entry.subagent.output !== void 0}, status=${entry.subagent.status}`
);
return buildTaskResult(entry.task.label, entry.effort, entry.subagent, error49);
const result = buildTaskResult(entry.task.label, entry.effort, entry.subagent, error49);
const status = result.success ? "succeeded" : "failed";
log.box(result.summary, { title: `task "${entry.task.label}" ${status}` });
return result;
});
const succeeded = results.filter((r) => r.success).length;
log.info(`\xBB delegation completed: ${succeeded}/${results.length} succeeded (mode=${mode})`);
@@ -142071,12 +142075,15 @@ var installNodeDependencies = {
const result = await spawn({
cmd: resolved.command,
args: resolved.args,
env: { PATH: process.env.PATH || "", HOME: process.env.HOME || "" },
onStdout: (chunk) => process.stdout.write(chunk),
onStderr: (chunk) => process.stderr.write(chunk)
env: { PATH: process.env.PATH || "", HOME: process.env.HOME || "" }
});
const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
if (output) {
log.startGroup(`${fullCommand} output`);
log.info(output);
log.endGroup();
}
if (result.exitCode !== 0) {
const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
const errorMessage = output || `exited with code ${result.exitCode}`;
return {
language: "node",
@@ -142213,20 +142220,26 @@ var installPythonDependencies = {
}
}
const [cmd, ...args2] = config3.installCmd;
log.info(`\xBB running: ${cmd} ${args2.join(" ")}`);
const fullCommand = `${cmd} ${args2.join(" ")}`;
log.info(`\xBB running: ${fullCommand}`);
const result = await spawn({
cmd,
args: args2,
env: { PATH: process.env.PATH || "", HOME: process.env.HOME || "" },
onStderr: (chunk) => process.stderr.write(chunk)
env: { PATH: process.env.PATH || "", HOME: process.env.HOME || "" }
});
const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
if (output) {
log.startGroup(`${fullCommand} output`);
log.info(output);
log.endGroup();
}
if (result.exitCode !== 0) {
return {
language: "python",
packageManager: config3.tool,
configFile: config3.file,
dependenciesInstalled: false,
issues: [result.stderr || `${cmd} exited with code ${result.exitCode}`]
issues: [output || `${cmd} exited with code ${result.exitCode}`]
};
}
return {
@@ -143746,11 +143759,8 @@ var SelectModeParams = type({
function resolveMode(modes2, modeName) {
return modes2.find((m) => m.name.toLowerCase() === modeName.toLowerCase()) ?? null;
}
function defaultGuidance(mode) {
return `Delegate a subagent for this "${mode.name}" task via the \`tasks\` array. Craft a self-contained prompt that includes all context the subagent needs. Subagents have file ops, bash, and read-only GitHub tools \u2014 but NO git/checkout, dependency, GitHub-write, or remote-mutating tools. All state-mutating and user-facing operations are your responsibility as orchestrator.`;
}
var modeGuidance = {
Build: `For Build tasks, consider a multi-phase approach:
Build: `### Checklist
1. **plan phase** (optional, for complex tasks): delegate a subagent to analyze the requirements, read AGENTS.md and relevant code, and produce a step-by-step implementation plan. Include \`${ghPullfrogMcpName}/set_output\` with the plan so it returns to you. Use mini or auto effort. You can also use \`ask_question\` for codebase questions/investigations.
@@ -143765,7 +143775,7 @@ var modeGuidance = {
- instruct the subagent to plan its approach before writing code: identify which files need to change, key design decisions, and edge cases. for non-trivial changes, consider whether there's a more elegant approach before committing to implementation.
- testing expectations: run relevant tests/lints before committing
- pre-commit quality check: instruct the subagent to review its own diff before committing \u2014 verify only intended changes are present, no debug artifacts or commented-out code remain, and no unrelated files were modified. the change should be clean enough that a senior engineer would approve it without hesitation. for non-trivial changes, ask whether there's a simpler way to achieve the same result.
- commit locally via bash (\`git add . && git commit -m "..."\`)
- commit locally via shell (\`git add . && git commit -m "..."\`)
- call \`${ghPullfrogMcpName}/set_output\` with a concise summary including the branch name (this is how results get back to you)
4. **review phase** (optional, for non-trivial changes): before pushing, delegate a review subagent to check the pending diff. Use \`ask_question\` for quick spot-checks, or delegate a full Review subagent for high-stakes changes. This catches issues before they're public.
@@ -143775,30 +143785,32 @@ var modeGuidance = {
- create a PR via \`${ghPullfrogMcpName}/create_pull_request\`
- call \`${ghPullfrogMcpName}/report_progress\` with the final summary including PR link
### Notes
For simple, well-defined tasks, a single build subagent is sufficient \u2014 skip the plan and review phases.
Your subagent receives ONLY what you write. Include file paths, constraints, conventions, and any context from AGENTS.md or the codebase directly in the prompt. Subagents have file ops, bash, and read-only GitHub tools \u2014 but NO git/checkout, dependency, GitHub-write, or remote-mutating tools.`,
AddressReviews: `Delegate a single subagent to address PR review feedback.
Your subagent receives ONLY what you write. Include file paths, constraints, conventions, and any context from AGENTS.md or the codebase directly in the prompt. Subagents have file ops, shell, and read-only GitHub tools \u2014 but NO git/checkout, dependency, GitHub-write, or remote-mutating tools.`,
AddressReviews: `### Checklist
Before delegating, checkout the PR branch yourself via \`${ghPullfrogMcpName}/checkout_pr\` \u2014 subagents have no git/checkout tools.
1. Before delegating, checkout the PR branch yourself via \`${ghPullfrogMcpName}/checkout_pr\` \u2014 subagents have no git/checkout tools.
Include in its prompt:
2. Include in its prompt:
- instruct it to fetch review comments via \`${ghPullfrogMcpName}/get_review_comments\` (subagents have read-only GitHub tools)
- for each comment: understand the feedback, make the code change, and record what was done
- test changes, then review the diff before committing \u2014 verify only intended changes are present, no debug artifacts remain, and the changes are clean enough that a senior engineer would approve without hesitation
- commit locally via bash (\`git add . && git commit -m "..."\`)
- commit locally via shell (\`git add . && git commit -m "..."\`)
- call \`${ghPullfrogMcpName}/set_output\` with a JSON object: \`{ "summary": "...", "replies": [{ "comment_id": 123, "thread_id": "...", "reply": "Fixed by ..." }, ...] }\` \u2014 this is how results get back to you
After the subagent completes:
3. After the subagent completes:
- push changes via \`${ghPullfrogMcpName}/push_branch\`
- reply to each comment using \`${ghPullfrogMcpName}/reply_to_review_comment\` with the subagent's suggested replies
- resolve addressed threads via \`${ghPullfrogMcpName}/resolve_review_thread\`
- call \`${ghPullfrogMcpName}/report_progress\` with a brief summary
Use auto or max effort depending on review complexity.`,
Review: `For reviews, delegate multiple focused subagents in parallel \u2014 each investigating a different area or aspect of the PR.
### Effort
### Approach
Use auto or max effort depending on review complexity.`,
Review: `### Checklist
1. Checkout the PR via \`${ghPullfrogMcpName}/checkout_pr\` \u2014 this returns PR metadata and a \`diffPath\`. Read the diff to identify the major areas of change.
2. Delegate multiple subagents in a single \`${ghPullfrogMcpName}/delegate\` call, each focused on a specific area. For example, a PR touching action/, components/, and prisma/ might get three subagents: "action-review", "frontend-review", "schema-review".
@@ -143825,61 +143837,58 @@ After all tasks complete, consolidate into a **single** review:
- if no subagent found actionable issues, skip the review \u2014 just call \`report_progress\` noting the PR was reviewed
Use max effort for thorough reviews.`,
Plan: `Delegate a single planning subagent:
Plan: `### Checklist
Include in its prompt:
- the task to plan for
- relevant codebase context (file paths, architecture notes from AGENTS.md)
- instruct it to produce a structured, actionable plan with clear milestones
- call \`${ghPullfrogMcpName}/set_output\` with the plan (this is how results get back to you \u2014 you'll need the plan to craft the next subagent's prompt)
1. Include in its prompt:
- the task to plan for
- relevant codebase context (file paths, architecture notes from AGENTS.md)
- instruct it to produce a structured, actionable plan with clear milestones
- call \`${ghPullfrogMcpName}/set_output\` with the plan (this is how results get back to you \u2014 you'll need the plan to craft the next subagent's prompt)
2. After the subagent completes, call \`${ghPullfrogMcpName}/report_progress\` with the plan.
After the subagent completes, call \`${ghPullfrogMcpName}/report_progress\` with the plan.
### Effort
Use mini or auto effort. After receiving the plan, you may delegate a Build subagent to implement it.`,
Fix: `For CI fix tasks, consider a focused single-phase approach.
Fix: `### Checklist
Before delegating, checkout the PR branch yourself via \`${ghPullfrogMcpName}/checkout_pr\` \u2014 subagents have no git/checkout tools.
1. Before delegating, checkout the PR branch yourself via \`${ghPullfrogMcpName}/checkout_pr\` \u2014 subagents have no git/checkout tools.
Delegate a single fix subagent with:
2. Delegate a single fix subagent with:
- the check_suite_id to fetch logs via \`${ghPullfrogMcpName}/get_check_suite_logs\` (subagents have read-only GitHub tools)
- the PR diff file path (from checkout_pr result) so it can understand what the PR changed
- CRITICAL: instruct it to verify the failure was INTRODUCED BY THIS PR before fixing. If unrelated, abort and report.
- instruct it to read the workflow file, reproduce locally with the EXACT same commands CI runs
- fix the issue, then verify the fix by re-running the exact CI command
- pre-commit quality check: review the diff before committing \u2014 verify only the fix is present, no debug artifacts, no unrelated changes. the fix should be clean enough that a senior engineer would approve it without hesitation.
- commit locally via bash (\`git add . && git commit -m "..."\`)
- commit locally via shell (\`git add . && git commit -m "..."\`)
- call \`${ghPullfrogMcpName}/set_output\` with a concise summary: what failed, why, and the fix applied (this is how results get back to you)
After the subagent completes:
3. After the subagent completes:
- push changes via \`${ghPullfrogMcpName}/push_branch\`
- call \`${ghPullfrogMcpName}/report_progress\` with the diagnosis and fix summary
### Effort
Use auto effort.`,
Task: `Handle this general-purpose task. For simple operations (labeling, commenting, answering questions, running a single command), you can often handle it directly without delegation.
Task: `### Checklist
When the task involves **substantial work** \u2014 code changes across multiple files, multi-step investigations, or tasks that benefit from focused context \u2014 use \`delegate\` and \`ask_question\` liberally:
- \`ask_question\`: quick codebase research, finding files, understanding architecture. Use freely \u2014 multiple calls in sequence is fine.
- \`delegate\`: research, local coding tasks, and codebase investigations. Each subagent gets dedicated context, so break complex work into focused subtasks and delegate each one. For independent subtasks, batch them in a single \`${ghPullfrogMcpName}/delegate\` call to run in parallel.
### When delegating
Include in each task's prompt:
- the full subtask description with all relevant context
- exactly what information to return. the subagent's output is your only way to get results back \u2014 be precise about what you need.
- if code changes are needed: branch naming, testing, commit instructions (do NOT instruct to push or create PR)
- if code changes are needed: instruct it to review its own diff before committing \u2014 verify only intended changes are present, no debug artifacts remain, and the changes are clean enough that a senior engineer would approve without hesitation
### Post-delegation
- call \`${ghPullfrogMcpName}/report_progress\` with results
- if the task involved code changes, push via \`${ghPullfrogMcpName}/push_branch\` and create a PR via \`${ghPullfrogMcpName}/create_pull_request\`
- if the task involved labeling, commenting, or other GitHub operations, perform those directly
Use mini effort for simple research tasks, auto for typical tasks, max for complex multi-file changes.`
1. Handle this general-purpose task. For simple operations (labeling, commenting, answering questions, running a single command), you can often handle it directly without delegation.
2. When the task involves **substantial work** \u2014 code changes across multiple files, multi-step investigations, or tasks that benefit from focused context \u2014 use \`delegate\` and \`ask_question\` liberally:
- \`ask_question\`: quick codebase research, finding files, understanding architecture. Use freely \u2014 multiple calls in sequence is fine.
- \`delegate\`: research, local coding tasks, and codebase investigations. Each subagent gets dedicated context, so break complex work into focused subtasks and delegate each one. For independent subtasks, batch them in a single \`${ghPullfrogMcpName}/delegate\` call to run in parallel.
3. Include in each task's prompt:
- the full subtask description with all relevant context
- exactly what information to return. the subagent's output is your only way to get results back \u2014 be precise about what you need.
- if code changes are needed: branch naming, testing, commit instructions (do NOT instruct to push or create PR)
- if code changes are needed: instruct it to review its own diff before committing \u2014 verify only intended changes are present, no debug artifacts remain, and the changes are clean enough that a senior engineer would approve without hesitation
4. Post-delegation:
- call \`${ghPullfrogMcpName}/report_progress\` with results
- if the task involved code changes, push via \`${ghPullfrogMcpName}/push_branch\` and create a PR via \`${ghPullfrogMcpName}/create_pull_request\`
- if the task involved labeling, commenting, or other GitHub operations, perform those directly
5. Use mini effort for simple research tasks, auto for typical tasks, max for complex multi-file changes.`
};
function buildOrchestratorGuidance(mode) {
const guidance = modeGuidance[mode.name] ?? defaultGuidance(mode);
const guidance = modeGuidance[mode.name] ?? "";
return {
modeName: mode.name,
description: mode.description,
@@ -143913,6 +143922,7 @@ function SelectModeTool(ctx) {
import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
import { randomUUID as randomUUID3 } from "node:crypto";
import { closeSync, openSync, writeFileSync as writeFileSync7 } from "node:fs";
import { userInfo } from "node:os";
import { join as join9 } from "node:path";
var ShellParams = type({
command: "string",
@@ -143959,13 +143969,14 @@ function detectSandboxMethod() {
log.info("PID namespace isolation not available - falling back to env filtering only");
return "none";
}
var PROC_CLEANUP = "umount /proc 2>/dev/null; umount /proc 2>/dev/null; mount -t proc proc /proc 2>/dev/null;";
function spawnShell(params) {
const spawnOpts = { env: params.env, cwd: params.cwd, stdio: params.stdio, detached: true };
const sandboxMethod = detectSandboxMethod();
if (sandboxMethod === "unshare") {
return spawn2(
"unshare",
["--pid", "--fork", "--mount-proc", "bash", "-c", params.command],
["--pid", "--fork", "--mount-proc", "bash", "-c", `${PROC_CLEANUP} ${params.command}`],
spawnOpts
);
}
@@ -143976,6 +143987,8 @@ function spawnShell(params) {
envArgs.push(`${k}=${v}`);
}
}
const username = userInfo().username;
const escaped = params.command.replace(/'/g, "'\\''");
return spawn2(
"sudo",
[
@@ -143987,10 +144000,9 @@ function spawnShell(params) {
"--mount-proc",
"bash",
"-c",
params.command
`${PROC_CLEANUP} exec su -p -s /bin/bash ${username} -c '${escaped}'`
],
{ ...spawnOpts, env: {} }
// empty env since we pass via sudo env
);
}
return spawn2("bash", ["-c", params.command], spawnOpts);
@@ -144242,43 +144254,26 @@ function isAddressInUse(error49) {
const message = getErrorMessage(error49).toLowerCase();
return message.includes("eaddrinuse") || message.includes("address already in use");
}
function buildSubagentTools(ctx) {
const tools = [
IssueInfoTool(ctx),
GetIssueCommentsTool(ctx),
GetIssueEventsTool(ctx),
PullRequestInfoTool(ctx),
CommitInfoTool(ctx),
GetReviewCommentsTool(ctx),
ListPullRequestReviewsTool(ctx),
GetCheckSuiteLogsTool(ctx),
UploadFileTool(ctx),
SetOutputTool(ctx),
FileReadTool(ctx),
FileWriteTool(ctx),
FileEditTool(ctx),
FileDeleteTool(ctx),
ListDirectoryTool(ctx)
];
if (ctx.payload.bash === "restricted") {
tools.push(BashTool(ctx));
tools.push(KillBackgroundTool(ctx));
}
return tools;
}
function buildOrchestratorTools(ctx) {
function buildCommonTools(ctx) {
const tools = [
StartDependencyInstallationTool(ctx),
AwaitDependencyInstallationTool(ctx),
CreateCommentTool(ctx),
EditCommentTool(ctx),
ReplyToReviewCommentTool(ctx),
IssueTool(ctx),
IssueInfoTool(ctx),
GetIssueCommentsTool(ctx),
GetIssueEventsTool(ctx),
CreatePullRequestReviewTool(ctx),
PullRequestInfoTool(ctx),
CommitInfoTool(ctx),
CheckoutPrTool(ctx),
GetReviewCommentsTool(ctx),
ListPullRequestReviewsTool(ctx),
ResolveReviewThreadTool(ctx),
GetCheckSuiteLogsTool(ctx),
AddLabelsTool(ctx),
GitTool(ctx),
GitFetchTool(ctx),
UploadFileTool(ctx),
@@ -144287,14 +144282,17 @@ function buildOrchestratorTools(ctx) {
FileWriteTool(ctx),
FileEditTool(ctx),
FileDeleteTool(ctx),
ListDirectoryTool(ctx),
CreateCommentTool(ctx),
EditCommentTool(ctx),
ReplyToReviewCommentTool(ctx),
CreatePullRequestReviewTool(ctx),
ResolveReviewThreadTool(ctx),
IssueTool(ctx),
AddLabelsTool(ctx),
ListDirectoryTool(ctx)
];
if (ctx.payload.shell === "restricted") {
tools.push(ShellTool(ctx));
tools.push(KillBackgroundTool(ctx));
}
return tools;
}
function buildOrchestratorTools(ctx) {
return [
...buildCommonTools(ctx),
ReportProgressTool(ctx),
SelectModeTool(ctx),
DelegateTool(ctx),
@@ -144305,11 +144303,9 @@ function buildOrchestratorTools(ctx) {
CreatePullRequestTool(ctx),
UpdatePullRequestBodyTool(ctx)
];
if (ctx.payload.shell === "restricted") {
tools.push(ShellTool(ctx));
tools.push(KillBackgroundTool(ctx));
}
return tools;
}
function buildSubagentTools(ctx) {
return buildCommonTools(ctx);
}
async function tryStartMcpServer(ctx, tools, port) {
const server = new FastMCP({ name: ghPullfrogMcpName, version: "0.0.1" });
@@ -144643,7 +144639,7 @@ import { join as join11 } from "node:path";
// package.json
var package_default = {
name: "@pullfrog/pullfrog",
version: "0.0.170",
version: "0.0.173",
type: "module",
files: [
"index.js",
@@ -146044,7 +146040,7 @@ function configureGeminiSettings(ctx) {
}
// agents/opencode.ts
import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync12 } from "node:fs";
import { existsSync as existsSync7, mkdirSync as mkdirSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync12 } from "node:fs";
import { join as join15 } from "node:path";
import { performance as performance7 } from "node:perf_hooks";
var OPENCODE_CLI_VERSION = "1.1.56";
@@ -146065,6 +146061,101 @@ function detectProviderError(text) {
}
return null;
}
function isRecord(value2) {
return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
}
function getRecordProperty(ctx) {
if (!isRecord(ctx.value)) {
return void 0;
}
const propertyValue = ctx.value[ctx.key];
if (!isRecord(propertyValue)) {
return void 0;
}
return propertyValue;
}
function loadRepoOpenCodeConfig(ctx) {
if (!existsSync7(ctx.repoConfigPath)) {
log.info(`\xBB repo opencode.json not found at ${ctx.repoConfigPath}`);
return void 0;
}
try {
const rawConfig = readFileSync7(ctx.repoConfigPath, "utf-8");
const parsedConfig = JSON.parse(rawConfig);
if (!isRecord(parsedConfig)) {
log.warning(`\xBB repo opencode.json is not an object: ${ctx.repoConfigPath}`);
return void 0;
}
const providerConfig = getRecordProperty({ value: parsedConfig, key: "provider" });
if (providerConfig) {
const providerNames = Object.keys(providerConfig);
log.info(`\xBB repo opencode provider config detected: ${providerNames.join(", ")}`);
}
const result = parsedConfig;
log.info(`\xBB loaded repo opencode.json from ${ctx.repoConfigPath}`);
return result;
} catch (error49) {
const errorMessage = error49 instanceof Error ? error49.message : String(error49);
log.warning(`\xBB failed to parse repo opencode.json at ${ctx.repoConfigPath}: ${errorMessage}`);
return void 0;
}
}
function parseProviderFromModel(ctx) {
const trimmedModel = ctx.model.trim();
const slashIndex = trimmedModel.indexOf("/");
if (slashIndex <= 0) {
return void 0;
}
const providerId = trimmedModel.slice(0, slashIndex).trim().toLowerCase();
if (!providerId) {
return void 0;
}
return providerId;
}
function buildInlineConfigOverride(ctx) {
const providerId = parseProviderFromModel({ model: ctx.model });
if (!providerId) {
return void 0;
}
const inlineConfig = {
model: ctx.model,
enabled_providers: [providerId]
};
return {
providerId,
content: JSON.stringify(inlineConfig)
};
}
function readNonEmptyEnvVar(ctx) {
const value2 = ctx.env[ctx.name];
if (!value2) {
return void 0;
}
const trimmed = value2.trim();
if (!trimmed) {
return void 0;
}
return trimmed;
}
function resolveModelOverride(ctx) {
if (ctx.effort === "mini") {
const miniModel = readNonEmptyEnvVar({ env: ctx.env, name: "OPENCODE_MODEL_MINI" });
if (miniModel) {
return { model: miniModel, source: "OPENCODE_MODEL_MINI" };
}
}
if (ctx.effort === "max") {
const maxModel = readNonEmptyEnvVar({ env: ctx.env, name: "OPENCODE_MODEL_MAX" });
if (maxModel) {
return { model: maxModel, source: "OPENCODE_MODEL_MAX" };
}
}
const baseModel = readNonEmptyEnvVar({ env: ctx.env, name: "OPENCODE_MODEL" });
if (!baseModel) {
return void 0;
}
return { model: baseModel, source: "OPENCODE_MODEL" };
}
async function installOpencode() {
return await installFromNpmTarball({
packageName: "opencode-ai",
@@ -146083,10 +146174,13 @@ var opencode = agent({
mkdirSync9(configDir, { recursive: true });
configureOpenCode(ctx);
const args2 = ["run", ctx.instructions.full, "--format", "json", "--print-logs"];
const modelOverride = process.env.OPENCODE_MODEL;
const modelOverride = resolveModelOverride({
effort: ctx.payload.effort,
env: process.env
});
if (modelOverride) {
args2.push("--model", modelOverride);
log.info(`\xBB model: ${modelOverride} (override)`);
args2.push("--model", modelOverride.model);
log.info(`\xBB model: ${modelOverride.model} (override via ${modelOverride.source})`);
} else {
log.info(`\xBB model: auto-selected by OpenCode`);
}
@@ -146098,6 +146192,28 @@ var opencode = agent({
// set GOOGLE_GENERATIVE_AI_API_KEY alias for Google provider compatibility (if not already set)
GOOGLE_GENERATIVE_AI_API_KEY: process.env.GOOGLE_GENERATIVE_AI_API_KEY || process.env.GEMINI_API_KEY
};
if (modelOverride) {
const inlineOverride = buildInlineConfigOverride({ model: modelOverride.model });
if (inlineOverride) {
env2.OPENCODE_CONFIG_CONTENT = inlineOverride.content;
log.info(
`\xBB OpenCode inline config override enabled: provider=${inlineOverride.providerId}, model=${modelOverride.model}`
);
} else {
log.warning(
`\xBB skipping OpenCode inline config override: unable to parse provider from model "${modelOverride.model}"`
);
}
}
const hasOpenRouterKey = Boolean(env2.OPENROUTER_API_KEY);
const hasAnthropicKey = Boolean(env2.ANTHROPIC_API_KEY);
const hasOpenAiKey = Boolean(env2.OPENAI_API_KEY);
const hasGoogleKey = Boolean(
env2.GOOGLE_API_KEY || env2.GEMINI_API_KEY || env2.GOOGLE_GENERATIVE_AI_API_KEY
);
log.info(
`\xBB provider key presence: OPENROUTER=${hasOpenRouterKey ? "set" : "unset"}, ANTHROPIC=${hasAnthropicKey ? "set" : "unset"}, OPENAI=${hasOpenAiKey ? "set" : "unset"}, GOOGLE=${hasGoogleKey ? "set" : "unset"}`
);
delete env2.GITHUB_TOKEN;
const repoDir = process.cwd();
log.debug(`\xBB starting OpenCode: ${cliPath} ${args2.join(" ")}`);
@@ -146250,21 +146366,34 @@ function configureOpenCode(ctx) {
const configDir = join15(ctx.tmpdir, ".config", "opencode");
mkdirSync9(configDir, { recursive: true });
const configPath = join15(configDir, "opencode.json");
const opencodeMcpServers = {
[ghPullfrogMcpName]: { type: "remote", url: ctx.mcpServerUrl }
};
const repoConfigPath = join15(process.cwd(), "opencode.json");
const repoConfig = loadRepoOpenCodeConfig({ repoConfigPath });
if (repoConfig?.model) {
log.info(`\xBB repo opencode model configured: ${repoConfig.model}`);
}
const opencodeMcpServers = {};
const repoMcpServers = getRecordProperty({ value: repoConfig, key: "mcp" });
if (repoMcpServers) {
Object.assign(opencodeMcpServers, repoMcpServers);
}
opencodeMcpServers[ghPullfrogMcpName] = { type: "remote", url: ctx.mcpServerUrl };
const shell = ctx.payload.shell;
const permission = {
edit: "deny",
read: "deny",
bash: shell !== "enabled" ? "deny" : "allow",
webfetch: ctx.payload.web === "disabled" ? "deny" : "allow",
external_directory: "deny"
};
const config3 = {
mcp: opencodeMcpServers,
permission
};
const permission = {};
const repoPermission = getRecordProperty({ value: repoConfig, key: "permission" });
if (repoPermission) {
Object.assign(permission, repoPermission);
}
permission.edit = "deny";
permission.read = "deny";
permission.bash = shell !== "enabled" ? "deny" : "allow";
permission.webfetch = ctx.payload.web === "disabled" ? "deny" : "allow";
permission.external_directory = "deny";
const config3 = {};
if (repoConfig) {
Object.assign(config3, repoConfig);
}
config3.mcp = opencodeMcpServers;
config3.permission = permission;
const configJson = JSON.stringify(config3, null, 2);
try {
writeFileSync12(configPath, configJson, "utf-8");
@@ -146955,13 +147084,13 @@ When all delegations are complete, call \`${ghPullfrogMcpName}/set_output\` with
### Subagent capabilities
Subagents have: file operations, bash (for local git, tests, builds), read-only GitHub queries, and upload_file. They do NOT have: \`git\`, \`checkout_pr\`, \`push_branch\`, \`create_pull_request\`, \`create_pull_request_review\`, \`report_progress\`, \`create_issue_comment\`, \`reply_to_review_comment\`, \`resolve_review_thread\`, \`delegate\`, \`ask_question\`, or any dependency/remote-mutating tools. All GitHub-write and state-mutating operations are your responsibility.
Subagents have: file operations, shell (for local git, tests, builds), read-only GitHub queries, and upload_file. They do NOT have: \`git\`, \`checkout_pr\`, \`push_branch\`, \`create_pull_request\`, \`create_pull_request_review\`, \`report_progress\`, \`create_issue_comment\`, \`reply_to_review_comment\`, \`resolve_review_thread\`, \`delegate\`, \`ask_question\`, or any dependency/remote-mutating tools. All GitHub-write and state-mutating operations are your responsibility.
### Prompt-crafting rules
- Subagents have NO context beyond what you write. No repo instructions, no event data, no user prompt.
- Specify exactly what information the subagent should return. The subagent's \`set_output\` call is your only way to get results back \u2014 be precise about what you need.
- Instruct subagents to use bash for local git (\`git add\`, \`git commit\`, \`git diff\`, \`git status\`).
- Instruct subagents to use shell for local git (\`git add\`, \`git commit\`, \`git diff\`, \`git status\`).
- Never instruct a subagent to push, create PRs, submit reviews, or post comments.
- For multi-phase flows, pass results from earlier phases into the next delegate call's prompts.
- You do NOT need to instruct subagents to call \`set_output\` \u2014 the system preamble handles this.
@@ -147584,6 +147713,7 @@ ${instructions.user}` : null,
}
// entry.ts
process.env.PATH = `${dirname2(process.execPath)}:${process.env.PATH}`;
async function run() {
try {
const result = await main();
+6
View File
@@ -4,9 +4,15 @@
* entry point for pullfrog/pullfrog - unified action
*/
import { dirname } from "node:path";
import * as core from "@actions/core";
import { main } from "./main.ts";
// GitHub Actions runs the action entry point with the node24 binary specified
// in action.yml, but doesn't add that binary's directory to PATH. Without this,
// spawned processes (pnpm, npm, etc.) resolve to the runner's default node (v20).
process.env.PATH = `${dirname(process.execPath)}:${process.env.PATH}`;
async function run(): Promise<void> {
try {
const result = await main();
+9 -6
View File
@@ -57,7 +57,7 @@ export function DelegateTool(ctx: ToolContext) {
return tool({
name: "delegate",
description:
"Delegate research, local coding tasks, and codebase investigations to subagents. Accepts an array of tasks that run in parallel — use this to fan out work (e.g. reviewing different areas of a PR simultaneously). Each subagent receives ONLY the instructions you provide (plus a system preamble enforcing set_output). Use select_mode first to get guidance on how to craft instructions. Subagents have file operations, bash, read-only GitHub tools (PR/issue info, review comments, check suite logs), and upload_file. They have NO git/checkout tools (would conflict between parallel subagents), NO dependency tools, and NO GitHub-write tools (commenting, reviews, labels, issues). All state-mutating and user-facing operations are your responsibility as orchestrator.",
"Delegate research, local coding tasks, and codebase investigations to subagents. Accepts an array of tasks that run in parallel — use this to fan out work (e.g. reviewing different areas of a PR simultaneously). Each subagent receives ONLY the instructions you provide (plus a system preamble enforcing set_output). Use select_mode first to get guidance on how to craft instructions. Subagents have file operations, shell, read-only GitHub tools (PR/issue info, review comments, check suite logs), and upload_file. They have NO git/checkout tools (would conflict between parallel subagents), NO dependency tools, and NO GitHub-write tools (commenting, reviews, labels, issues). All state-mutating and user-facing operations are your responsibility as orchestrator.",
parameters: DelegateParams,
execute: execute(async (params) => {
if (ctx.toolState.selfSubagentId) {
@@ -77,7 +77,10 @@ export function DelegateTool(ctx: ToolContext) {
}
// matched by delegate test validators — update tests if changed
log.info(`» delegating ${params.tasks.length} task(s) in parallel (mode=${mode})`);
const n = params.tasks.length;
log.info(
`» delegating ${n} task${n === 1 ? "" : "s"}${n > 1 ? " in parallel" : ""} (mode=${mode})`
);
const taskEntries = params.tasks.map((task) => {
const effort = task.effort ?? "auto";
@@ -100,10 +103,10 @@ export function DelegateTool(ctx: ToolContext) {
const results: DelegateTaskResult[] = taskEntries.map((entry, i) => {
const outcome = settled[i];
const error = outcome.status === "rejected" ? String(outcome.reason) : outcome.value.error;
log.debug(
`» task "${entry.task.label}" result: output=${entry.subagent.output !== undefined}, status=${entry.subagent.status}`
);
return buildTaskResult(entry.task.label, entry.effort, entry.subagent, error);
const result = buildTaskResult(entry.task.label, entry.effort, entry.subagent, error);
const status = result.success ? "succeeded" : "failed";
log.box(result.summary, { title: `task "${entry.task.label}" ${status}` });
return result;
});
const succeeded = results.filter((r) => r.success).length;
+44 -49
View File
@@ -14,12 +14,8 @@ function resolveMode(modes: Mode[], modeName: string): Mode | null {
return modes.find((m) => m.name.toLowerCase() === modeName.toLowerCase()) ?? null;
}
function defaultGuidance(mode: Mode): string {
return `Delegate a subagent for this "${mode.name}" task via the \`tasks\` array. Craft a self-contained prompt that includes all context the subagent needs. Subagents have file ops, bash, and read-only GitHub tools — but NO git/checkout, dependency, GitHub-write, or remote-mutating tools. All state-mutating and user-facing operations are your responsibility as orchestrator.`;
}
const modeGuidance: Record<string, string> = {
Build: `For Build tasks, consider a multi-phase approach:
Build: `### Checklist
1. **plan phase** (optional, for complex tasks): delegate a subagent to analyze the requirements, read AGENTS.md and relevant code, and produce a step-by-step implementation plan. Include \`${ghPullfrogMcpName}/set_output\` with the plan so it returns to you. Use mini or auto effort. You can also use \`ask_question\` for codebase questions/investigations.
@@ -34,7 +30,7 @@ const modeGuidance: Record<string, string> = {
- instruct the subagent to plan its approach before writing code: identify which files need to change, key design decisions, and edge cases. for non-trivial changes, consider whether there's a more elegant approach before committing to implementation.
- testing expectations: run relevant tests/lints before committing
- pre-commit quality check: instruct the subagent to review its own diff before committing — verify only intended changes are present, no debug artifacts or commented-out code remain, and no unrelated files were modified. the change should be clean enough that a senior engineer would approve it without hesitation. for non-trivial changes, ask whether there's a simpler way to achieve the same result.
- commit locally via bash (\`git add . && git commit -m "..."\`)
- commit locally via shell (\`git add . && git commit -m "..."\`)
- call \`${ghPullfrogMcpName}/set_output\` with a concise summary including the branch name (this is how results get back to you)
4. **review phase** (optional, for non-trivial changes): before pushing, delegate a review subagent to check the pending diff. Use \`ask_question\` for quick spot-checks, or delegate a full Review subagent for high-stakes changes. This catches issues before they're public.
@@ -44,32 +40,34 @@ const modeGuidance: Record<string, string> = {
- create a PR via \`${ghPullfrogMcpName}/create_pull_request\`
- call \`${ghPullfrogMcpName}/report_progress\` with the final summary including PR link
### Notes
For simple, well-defined tasks, a single build subagent is sufficient — skip the plan and review phases.
Your subagent receives ONLY what you write. Include file paths, constraints, conventions, and any context from AGENTS.md or the codebase directly in the prompt. Subagents have file ops, bash, and read-only GitHub tools — but NO git/checkout, dependency, GitHub-write, or remote-mutating tools.`,
Your subagent receives ONLY what you write. Include file paths, constraints, conventions, and any context from AGENTS.md or the codebase directly in the prompt. Subagents have file ops, shell, and read-only GitHub tools — but NO git/checkout, dependency, GitHub-write, or remote-mutating tools.`,
AddressReviews: `Delegate a single subagent to address PR review feedback.
AddressReviews: `### Checklist
Before delegating, checkout the PR branch yourself via \`${ghPullfrogMcpName}/checkout_pr\` — subagents have no git/checkout tools.
1. Before delegating, checkout the PR branch yourself via \`${ghPullfrogMcpName}/checkout_pr\` — subagents have no git/checkout tools.
Include in its prompt:
2. Include in its prompt:
- instruct it to fetch review comments via \`${ghPullfrogMcpName}/get_review_comments\` (subagents have read-only GitHub tools)
- for each comment: understand the feedback, make the code change, and record what was done
- test changes, then review the diff before committing — verify only intended changes are present, no debug artifacts remain, and the changes are clean enough that a senior engineer would approve without hesitation
- commit locally via bash (\`git add . && git commit -m "..."\`)
- commit locally via shell (\`git add . && git commit -m "..."\`)
- call \`${ghPullfrogMcpName}/set_output\` with a JSON object: \`{ "summary": "...", "replies": [{ "comment_id": 123, "thread_id": "...", "reply": "Fixed by ..." }, ...] }\` — this is how results get back to you
After the subagent completes:
3. After the subagent completes:
- push changes via \`${ghPullfrogMcpName}/push_branch\`
- reply to each comment using \`${ghPullfrogMcpName}/reply_to_review_comment\` with the subagent's suggested replies
- resolve addressed threads via \`${ghPullfrogMcpName}/resolve_review_thread\`
- call \`${ghPullfrogMcpName}/report_progress\` with a brief summary
### Effort
Use auto or max effort depending on review complexity.`,
Review: `For reviews, delegate multiple focused subagents in parallel — each investigating a different area or aspect of the PR.
### Approach
Review: `### Checklist
1. Checkout the PR via \`${ghPullfrogMcpName}/checkout_pr\` — this returns PR metadata and a \`diffPath\`. Read the diff to identify the major areas of change.
2. Delegate multiple subagents in a single \`${ghPullfrogMcpName}/delegate\` call, each focused on a specific area. For example, a PR touching action/, components/, and prisma/ might get three subagents: "action-review", "frontend-review", "schema-review".
@@ -97,60 +95,57 @@ After all tasks complete, consolidate into a **single** review:
Use max effort for thorough reviews.`,
Plan: `Delegate a single planning subagent:
Plan: `### Checklist
Include in its prompt:
- the task to plan for
- relevant codebase context (file paths, architecture notes from AGENTS.md)
- instruct it to produce a structured, actionable plan with clear milestones
- call \`${ghPullfrogMcpName}/set_output\` with the plan (this is how results get back to you — you'll need the plan to craft the next subagent's prompt)
1. Include in its prompt:
- the task to plan for
- relevant codebase context (file paths, architecture notes from AGENTS.md)
- instruct it to produce a structured, actionable plan with clear milestones
- call \`${ghPullfrogMcpName}/set_output\` with the plan (this is how results get back to you — you'll need the plan to craft the next subagent's prompt)
2. After the subagent completes, call \`${ghPullfrogMcpName}/report_progress\` with the plan.
After the subagent completes, call \`${ghPullfrogMcpName}/report_progress\` with the plan.
### Effort
Use mini or auto effort. After receiving the plan, you may delegate a Build subagent to implement it.`,
Fix: `For CI fix tasks, consider a focused single-phase approach.
Fix: `### Checklist
Before delegating, checkout the PR branch yourself via \`${ghPullfrogMcpName}/checkout_pr\` — subagents have no git/checkout tools.
1. Before delegating, checkout the PR branch yourself via \`${ghPullfrogMcpName}/checkout_pr\` — subagents have no git/checkout tools.
Delegate a single fix subagent with:
2. Delegate a single fix subagent with:
- the check_suite_id to fetch logs via \`${ghPullfrogMcpName}/get_check_suite_logs\` (subagents have read-only GitHub tools)
- the PR diff file path (from checkout_pr result) so it can understand what the PR changed
- CRITICAL: instruct it to verify the failure was INTRODUCED BY THIS PR before fixing. If unrelated, abort and report.
- instruct it to read the workflow file, reproduce locally with the EXACT same commands CI runs
- fix the issue, then verify the fix by re-running the exact CI command
- pre-commit quality check: review the diff before committing — verify only the fix is present, no debug artifacts, no unrelated changes. the fix should be clean enough that a senior engineer would approve it without hesitation.
- commit locally via bash (\`git add . && git commit -m "..."\`)
- commit locally via shell (\`git add . && git commit -m "..."\`)
- call \`${ghPullfrogMcpName}/set_output\` with a concise summary: what failed, why, and the fix applied (this is how results get back to you)
After the subagent completes:
3. After the subagent completes:
- push changes via \`${ghPullfrogMcpName}/push_branch\`
- call \`${ghPullfrogMcpName}/report_progress\` with the diagnosis and fix summary
### Effort
Use auto effort.`,
Task: `Handle this general-purpose task. For simple operations (labeling, commenting, answering questions, running a single command), you can often handle it directly without delegation.
Task: `### Checklist
When the task involves **substantial work** — code changes across multiple files, multi-step investigations, or tasks that benefit from focused context — use \`delegate\` and \`ask_question\` liberally:
- \`ask_question\`: quick codebase research, finding files, understanding architecture. Use freely — multiple calls in sequence is fine.
- \`delegate\`: research, local coding tasks, and codebase investigations. Each subagent gets dedicated context, so break complex work into focused subtasks and delegate each one. For independent subtasks, batch them in a single \`${ghPullfrogMcpName}/delegate\` call to run in parallel.
### When delegating
Include in each task's prompt:
- the full subtask description with all relevant context
- exactly what information to return. the subagent's output is your only way to get results back — be precise about what you need.
- if code changes are needed: branch naming, testing, commit instructions (do NOT instruct to push or create PR)
- if code changes are needed: instruct it to review its own diff before committing — verify only intended changes are present, no debug artifacts remain, and the changes are clean enough that a senior engineer would approve without hesitation
### Post-delegation
- call \`${ghPullfrogMcpName}/report_progress\` with results
- if the task involved code changes, push via \`${ghPullfrogMcpName}/push_branch\` and create a PR via \`${ghPullfrogMcpName}/create_pull_request\`
- if the task involved labeling, commenting, or other GitHub operations, perform those directly
Use mini effort for simple research tasks, auto for typical tasks, max for complex multi-file changes.`,
1. Handle this general-purpose task. For simple operations (labeling, commenting, answering questions, running a single command), you can often handle it directly without delegation.
2. When the task involves **substantial work** — code changes across multiple files, multi-step investigations, or tasks that benefit from focused context — use \`delegate\` and \`ask_question\` liberally:
- \`ask_question\`: quick codebase research, finding files, understanding architecture. Use freely — multiple calls in sequence is fine.
- \`delegate\`: research, local coding tasks, and codebase investigations. Each subagent gets dedicated context, so break complex work into focused subtasks and delegate each one. For independent subtasks, batch them in a single \`${ghPullfrogMcpName}/delegate\` call to run in parallel.
3. Include in each task's prompt:
- the full subtask description with all relevant context
- exactly what information to return. the subagent's output is your only way to get results back — be precise about what you need.
- if code changes are needed: branch naming, testing, commit instructions (do NOT instruct to push or create PR)
- if code changes are needed: instruct it to review its own diff before committing — verify only intended changes are present, no debug artifacts remain, and the changes are clean enough that a senior engineer would approve without hesitation
4. Post-delegation:
- call \`${ghPullfrogMcpName}/report_progress\` with results
- if the task involved code changes, push via \`${ghPullfrogMcpName}/push_branch\` and create a PR via \`${ghPullfrogMcpName}/create_pull_request\`
- if the task involved labeling, commenting, or other GitHub operations, perform those directly
5. Use mini effort for simple research tasks, auto for typical tasks, max for complex multi-file changes.`,
};
type OrchestratorGuidance = {
@@ -160,7 +155,7 @@ type OrchestratorGuidance = {
};
function buildOrchestratorGuidance(mode: Mode): OrchestratorGuidance {
const guidance = modeGuidance[mode.name] ?? defaultGuidance(mode);
const guidance = modeGuidance[mode.name] ?? "";
return {
modeName: mode.name,
description: mode.description,
+30 -48
View File
@@ -188,50 +188,27 @@ function isAddressInUse(error: unknown): boolean {
return message.includes("eaddrinuse") || message.includes("address already in use");
}
// subagent tools: file ops, bash, read-only GitHub, upload, set_output.
// no git/checkout (mutates shared state), no dependencies (shared state),
// no GitHub-write (user-facing side effects), no delegation/remote-mutating.
function buildSubagentTools(ctx: ToolContext): Tool<any, any>[] {
const tools: Tool<any, any>[] = [
IssueInfoTool(ctx),
GetIssueCommentsTool(ctx),
GetIssueEventsTool(ctx),
PullRequestInfoTool(ctx),
CommitInfoTool(ctx),
GetReviewCommentsTool(ctx),
ListPullRequestReviewsTool(ctx),
GetCheckSuiteLogsTool(ctx),
UploadFileTool(ctx),
SetOutputTool(ctx),
FileReadTool(ctx),
FileWriteTool(ctx),
FileEditTool(ctx),
FileDeleteTool(ctx),
ListDirectoryTool(ctx),
];
if (ctx.payload.bash === "restricted") {
tools.push(BashTool(ctx));
tools.push(KillBackgroundTool(ctx));
}
return tools;
}
// orchestrator gets everything: file ops, bash, git, GitHub, delegation, remote-mutating
function buildOrchestratorTools(ctx: ToolContext): Tool<any, any>[] {
// tools shared by both orchestrator and subagent servers
function buildCommonTools(ctx: ToolContext): Tool<any, any>[] {
const tools: Tool<any, any>[] = [
StartDependencyInstallationTool(ctx),
AwaitDependencyInstallationTool(ctx),
CreateCommentTool(ctx),
EditCommentTool(ctx),
ReplyToReviewCommentTool(ctx),
IssueTool(ctx),
IssueInfoTool(ctx),
GetIssueCommentsTool(ctx),
GetIssueEventsTool(ctx),
CreatePullRequestReviewTool(ctx),
PullRequestInfoTool(ctx),
CommitInfoTool(ctx),
CheckoutPrTool(ctx),
GetReviewCommentsTool(ctx),
ListPullRequestReviewsTool(ctx),
ResolveReviewThreadTool(ctx),
GetCheckSuiteLogsTool(ctx),
AddLabelsTool(ctx),
GitTool(ctx),
GitFetchTool(ctx),
UploadFileTool(ctx),
@@ -241,22 +218,6 @@ function buildOrchestratorTools(ctx: ToolContext): Tool<any, any>[] {
FileEditTool(ctx),
FileDeleteTool(ctx),
ListDirectoryTool(ctx),
CreateCommentTool(ctx),
EditCommentTool(ctx),
ReplyToReviewCommentTool(ctx),
CreatePullRequestReviewTool(ctx),
ResolveReviewThreadTool(ctx),
IssueTool(ctx),
AddLabelsTool(ctx),
ReportProgressTool(ctx),
SelectModeTool(ctx),
DelegateTool(ctx),
AskQuestionTool(ctx),
PushBranchTool(ctx),
PushTagsTool(ctx),
DeleteBranchTool(ctx),
CreatePullRequestTool(ctx),
UpdatePullRequestBodyTool(ctx),
];
// only add ShellTool when shell is "restricted"
@@ -271,6 +232,27 @@ function buildOrchestratorTools(ctx: ToolContext): Tool<any, any>[] {
return tools;
}
// orchestrator gets common tools + delegation + remote-mutating tools
function buildOrchestratorTools(ctx: ToolContext): Tool<any, any>[] {
return [
...buildCommonTools(ctx),
ReportProgressTool(ctx),
SelectModeTool(ctx),
DelegateTool(ctx),
AskQuestionTool(ctx),
PushBranchTool(ctx),
PushTagsTool(ctx),
DeleteBranchTool(ctx),
CreatePullRequestTool(ctx),
UpdatePullRequestBodyTool(ctx),
];
}
// subagent gets only common tools (no delegation, no remote mutation)
function buildSubagentTools(ctx: ToolContext): Tool<any, any>[] {
return buildCommonTools(ctx);
}
type McpStartResult = {
server: FastMCP;
url: string;
+18 -11
View File
@@ -2,6 +2,7 @@
import { type ChildProcess, type StdioOptions, spawn, spawnSync } from "node:child_process";
import { randomUUID } from "node:crypto";
import { closeSync, openSync, writeFileSync } from "node:fs";
import { userInfo } from "node:os";
import { join } from "node:path";
import { type } from "arktype";
import { log } from "../utils/log.ts";
@@ -82,33 +83,39 @@ function detectSandboxMethod(): SandboxMethod {
return "none";
}
// strip inherited proc mount that sits underneath --mount-proc's overlay.
// --mount-proc mounts fresh proc on top, but `umount /proc` peels it off and exposes the
// host's proc with all host PIDs — allowing /proc/<pid>/environ exfiltration.
// double-umount removes both layers, then a clean mount gives only sandbox PIDs.
// on unprivileged systems where umount fails, --mount-proc still provides isolation
// (the agent also can't umount in that case).
const PROC_CLEANUP =
"umount /proc 2>/dev/null; umount /proc 2>/dev/null; mount -t proc proc /proc 2>/dev/null;";
function spawnShell(params: SpawnParams): ChildProcess {
const spawnOpts = { env: params.env, cwd: params.cwd, stdio: params.stdio, detached: true };
const sandboxMethod = detectSandboxMethod();
if (sandboxMethod === "unshare") {
// use PID namespace isolation to prevent reading /proc/$PPID/environ
// this creates a new PID namespace where:
// 1. the subprocess becomes PID 1 in its namespace
// 2. parent PIDs are not visible (PPID = 0)
// 3. fresh /proc is mounted showing only sandbox PIDs
// combined with resolveEnv("restricted"), this prevents all /proc-based secret theft
return spawn(
"unshare",
["--pid", "--fork", "--mount-proc", "bash", "-c", params.command],
["--pid", "--fork", "--mount-proc", "bash", "-c", `${PROC_CLEANUP} ${params.command}`],
spawnOpts
);
}
if (sandboxMethod === "sudo-unshare") {
// on GHA runners, unprivileged namespaces are blocked but sudo works
// pass filtered env via sudo env command since sudo clears environment
const envArgs: string[] = [];
for (const [k, v] of Object.entries(params.env)) {
if (v !== undefined) {
envArgs.push(`${k}=${v}`);
}
}
// drop back to original user after PROC_CLEANUP so files aren't owned by root.
// sudo is only needed for unshare; the actual command should run as the normal user
// to avoid ownership mismatches with file_write/file_edit (which run in the Node.js parent).
const username = userInfo().username;
const escaped = params.command.replace(/'/g, "'\\''");
return spawn(
"sudo",
[
@@ -120,9 +127,9 @@ function spawnShell(params: SpawnParams): ChildProcess {
"--mount-proc",
"bash",
"-c",
params.command,
`${PROC_CLEANUP} exec su -p -s /bin/bash ${username} -c '${escaped}'`,
],
{ ...spawnOpts, env: {} } // empty env since we pass via sudo env
{ ...spawnOpts, env: {} }
);
}
+1 -1
View File
@@ -71,7 +71,7 @@ const subagentSystemPreamble = `You are a focused subagent. Complete the task au
Your tools are limited to:
- **File operations**: \`${ghPullfrogMcpName}/file_read\`, \`file_write\`, \`file_edit\`, \`file_delete\`, \`list_directory\`. Native file tools (Read, Write, StrReplace, etc.) are disabled — use the MCP versions.
- **Shell**: \`${ghPullfrogMcpName}/bash\` (if available). Use this for local git operations (\`git add\`, \`git commit\`, \`git diff\`, \`git log\`, \`git status\`), running tests, builds, and linters.
- **Shell**: \`${ghPullfrogMcpName}/shell\` (if available). Use this for local git operations (\`git add\`, \`git commit\`, \`git diff\`, \`git log\`, \`git status\`), running tests, builds, and linters.
- **Read-only GitHub**: \`get_pull_request\`, \`get_issue\`, \`get_issue_comments\`, \`get_issue_events\`, \`get_review_comments\`, \`list_pull_request_reviews\`, \`get_check_suite_logs\`, \`get_commit_info\`.
- **Output**: \`${ghPullfrogMcpName}/upload_file\`, \`${ghPullfrogMcpName}/set_output\`.
+1 -1
View File
@@ -82,7 +82,7 @@ describe("per-server tool isolation - integration", () => {
orchestratorServer.addTool(mockTool("push_branch", "push branch"));
orchestratorServer.addTool(mockTool("create_pull_request", "create PR"));
// subagent gets ONLY file ops, bash, read-only GitHub, upload, set_output
// subagent gets ONLY file ops, shell, read-only GitHub, upload, set_output
subagentServer = new FastMCP({ name: "subagent", version: "0.0.1" });
subagentServer.addTool(mockTool("file_read", "read a file"));
subagentServer.addTool(mockTool("set_output", "set output"));
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@pullfrog/pullfrog",
"version": "0.0.170",
"version": "0.0.173",
"type": "module",
"files": [
"index.js",
+1 -1
View File
@@ -41235,7 +41235,7 @@ var Effort = type.enumerated("mini", "auto", "max");
// package.json
var package_default = {
name: "@pullfrog/pullfrog",
version: "0.0.170",
version: "0.0.173",
type: "module",
files: [
"index.js",
+7 -4
View File
@@ -159,13 +159,16 @@ export const installNodeDependencies: PrepDefinition = {
cmd: resolved.command,
args: resolved.args,
env: { PATH: process.env.PATH || "", HOME: process.env.HOME || "" },
onStdout: (chunk) => process.stdout.write(chunk),
onStderr: (chunk) => process.stderr.write(chunk),
});
const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
if (output) {
log.startGroup(`${fullCommand} output`);
log.info(output);
log.endGroup();
}
if (result.exitCode !== 0) {
// combine stdout and stderr for better error context (pnpm often outputs errors to stdout)
const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
const errorMessage = output || `exited with code ${result.exitCode}`;
return {
language: "node",
+10 -3
View File
@@ -162,21 +162,28 @@ export const installPythonDependencies: PrepDefinition = {
// run the install command
const [cmd, ...args] = config.installCmd;
log.info(`» running: ${cmd} ${args.join(" ")}`);
const fullCommand = `${cmd} ${args.join(" ")}`;
log.info(`» running: ${fullCommand}`);
const result = await spawn({
cmd,
args,
env: { PATH: process.env.PATH || "", HOME: process.env.HOME || "" },
onStderr: (chunk) => process.stderr.write(chunk),
});
const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
if (output) {
log.startGroup(`${fullCommand} output`);
log.info(output);
log.endGroup();
}
if (result.exitCode !== 0) {
return {
language: "python",
packageManager: config.tool,
configFile: config.file,
dependenciesInstalled: false,
issues: [result.stderr || `${cmd} exited with code ${result.exitCode}`],
issues: [output || `${cmd} exited with code ${result.exitCode}`],
};
}
+2
View File
@@ -65,6 +65,8 @@ const expectedAgentEnvVars = [
"GITHUB_TOKEN",
...new Set(Object.values(agentsManifest).flatMap((a) => a.apiKeyNames)),
"GEMINI_MODEL",
"OPENCODE_MODEL_MAX",
"OPENCODE_MODEL_MINI",
"OPENCODE_MODEL",
].sort();
+2
View File
@@ -120,6 +120,8 @@ const testEnvAllowList = new Set([
"GOOGLE_GENERATIVE_AI_API_KEY",
"CURSOR_API_KEY",
"OPENCODE_MODEL", // override OpenCode model (e.g. google/gemini-3-flash-preview) for tests or user preference
"OPENCODE_MODEL_MINI", // effort-specific OpenCode model override for mini effort
"OPENCODE_MODEL_MAX", // effort-specific OpenCode model override for max effort
"GEMINI_MODEL", // override Gemini model (e.g. gemini-3-pro-preview) for tests or user preference
"LOG_LEVEL",
"DEBUG",
+2 -2
View File
@@ -388,13 +388,13 @@ When all delegations are complete, call \`${ghPullfrogMcpName}/set_output\` with
### Subagent capabilities
Subagents have: file operations, bash (for local git, tests, builds), read-only GitHub queries, and upload_file. They do NOT have: \`git\`, \`checkout_pr\`, \`push_branch\`, \`create_pull_request\`, \`create_pull_request_review\`, \`report_progress\`, \`create_issue_comment\`, \`reply_to_review_comment\`, \`resolve_review_thread\`, \`delegate\`, \`ask_question\`, or any dependency/remote-mutating tools. All GitHub-write and state-mutating operations are your responsibility.
Subagents have: file operations, shell (for local git, tests, builds), read-only GitHub queries, and upload_file. They do NOT have: \`git\`, \`checkout_pr\`, \`push_branch\`, \`create_pull_request\`, \`create_pull_request_review\`, \`report_progress\`, \`create_issue_comment\`, \`reply_to_review_comment\`, \`resolve_review_thread\`, \`delegate\`, \`ask_question\`, or any dependency/remote-mutating tools. All GitHub-write and state-mutating operations are your responsibility.
### Prompt-crafting rules
- Subagents have NO context beyond what you write. No repo instructions, no event data, no user prompt.
- Specify exactly what information the subagent should return. The subagent's \`set_output\` call is your only way to get results back — be precise about what you need.
- Instruct subagents to use bash for local git (\`git add\`, \`git commit\`, \`git diff\`, \`git status\`).
- Instruct subagents to use shell for local git (\`git add\`, \`git commit\`, \`git diff\`, \`git status\`).
- Never instruct a subagent to push, create PRs, submit reviews, or post comments.
- For multi-phase flows, pass results from earlier phases into the next delegate call's prompts.
- You do NOT need to instruct subagents to call \`set_output\` — the system preamble handles this.