Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d5ab3706db | |||
| f8a871f723 | |||
| 52ec35790a | |||
| edd240f535 | |||
| cd1ea5267c | |||
| 4f1e4a2e7a | |||
| 73836d9c8f | |||
| da72d0d6ee | |||
| b472aa1ba9 | |||
| e2d8dfeebf |
@@ -39,6 +39,8 @@ jobs:
|
|||||||
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||||
GEMINI_MODEL: ${{ vars.GEMINI_MODEL }}
|
GEMINI_MODEL: ${{ vars.GEMINI_MODEL }}
|
||||||
OPENCODE_MODEL: ${{ vars.OPENCODE_MODEL }}
|
OPENCODE_MODEL: ${{ vars.OPENCODE_MODEL }}
|
||||||
|
OPENCODE_MODEL_MINI: ${{ vars.OPENCODE_MODEL_MINI }}
|
||||||
|
OPENCODE_MODEL_MAX: ${{ vars.OPENCODE_MODEL_MAX }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<!-- test preview system --> <!-- test bypass 2 -->
|
<!-- test preview system --> <!-- test bypass 2 --> <!-- trigger preview repo creation -->
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<h1 align="center">
|
<h1 align="center">
|
||||||
<picture>
|
<picture>
|
||||||
|
|||||||
+218
-22
@@ -1,7 +1,7 @@
|
|||||||
// changes to effort level configuration should be reflected in wiki/effort.md and docs/effort.mdx
|
// 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 tool permissions should be reflected in wiki/granular-tools.md
|
||||||
// changes to web search configuration should be reflected in wiki/websearch.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 { join } from "node:path";
|
||||||
import { performance } from "node:perf_hooks";
|
import { performance } from "node:perf_hooks";
|
||||||
import { ghPullfrogMcpName } from "../external.ts";
|
import { ghPullfrogMcpName } from "../external.ts";
|
||||||
@@ -38,6 +38,159 @@ function detectProviderError(text: string): string | null {
|
|||||||
return 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> {
|
async function installOpencode(): Promise<string> {
|
||||||
return await installFromNpmTarball({
|
return await installFromNpmTarball({
|
||||||
packageName: "opencode-ai",
|
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).
|
// this is critical for debugging since opencode run suppresses errors by default (Issue #752).
|
||||||
const args = ["run", ctx.instructions.full, "--format", "json", "--print-logs"];
|
const args = ["run", ctx.instructions.full, "--format", "json", "--print-logs"];
|
||||||
|
|
||||||
// only override model when OPENCODE_MODEL is set (e.g., test environments with
|
// resolve model override from environment.
|
||||||
// restricted API quotas). in production, OpenCode auto-selects the best available
|
// precedence:
|
||||||
// model based on which provider API keys are present.
|
// 1) effort-specific overrides (OPENCODE_MODEL_MINI / OPENCODE_MODEL_MAX)
|
||||||
const modelOverride = process.env.OPENCODE_MODEL;
|
// 2) OPENCODE_MODEL fallback
|
||||||
|
// 3) OpenCode auto-select
|
||||||
|
const modelOverride = resolveModelOverride({
|
||||||
|
effort: ctx.payload.effort,
|
||||||
|
env: process.env,
|
||||||
|
});
|
||||||
if (modelOverride) {
|
if (modelOverride) {
|
||||||
args.push("--model", modelOverride);
|
args.push("--model", modelOverride.model);
|
||||||
log.info(`» model: ${modelOverride} (override)`);
|
log.info(`» model: ${modelOverride.model} (override via ${modelOverride.source})`);
|
||||||
} else {
|
} else {
|
||||||
log.info(`» model: auto-selected by OpenCode`);
|
log.info(`» model: auto-selected by OpenCode`);
|
||||||
}
|
}
|
||||||
@@ -89,6 +247,31 @@ export const opencode = agent({
|
|||||||
GOOGLE_GENERATIVE_AI_API_KEY:
|
GOOGLE_GENERATIVE_AI_API_KEY:
|
||||||
process.env.GOOGLE_GENERATIVE_AI_API_KEY || process.env.GEMINI_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
|
// OpenCode doesn't support GitHub App installation tokens
|
||||||
delete env.GITHUB_TOKEN;
|
delete env.GITHUB_TOKEN;
|
||||||
|
|
||||||
@@ -194,7 +377,7 @@ export const opencode = agent({
|
|||||||
lastProviderError = providerError;
|
lastProviderError = providerError;
|
||||||
log.info(`» provider error detected (${providerError}): ${trimmed.substring(0, 500)}`);
|
log.info(`» provider error detected (${providerError}): ${trimmed.substring(0, 500)}`);
|
||||||
} else {
|
} 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
|
// INFO/DEBUG bus traffic to debug so it doesn't drown out tool
|
||||||
// call logs in the GitHub Actions step output.
|
// call logs in the GitHub Actions step output.
|
||||||
log.debug(trimmed);
|
log.debug(trimmed);
|
||||||
@@ -301,28 +484,41 @@ function configureOpenCode(ctx: AgentRunContext): void {
|
|||||||
const configDir = join(ctx.tmpdir, ".config", "opencode");
|
const configDir = join(ctx.tmpdir, ".config", "opencode");
|
||||||
mkdirSync(configDir, { recursive: true });
|
mkdirSync(configDir, { recursive: true });
|
||||||
const configPath = join(configDir, "opencode.json");
|
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
|
// build MCP servers config
|
||||||
const opencodeMcpServers = {
|
const opencodeMcpServers: Record<string, unknown> = {};
|
||||||
[ghPullfrogMcpName]: { type: "remote" as const, url: ctx.mcpServerUrl },
|
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
|
// build permission object based on tool permissions
|
||||||
// note: OpenCode has no built-in web search tool
|
// note: OpenCode has no built-in web search tool
|
||||||
const shell = ctx.payload.shell;
|
const shell = ctx.payload.shell;
|
||||||
const permission = {
|
const permission: Record<string, unknown> = {};
|
||||||
edit: "deny",
|
const repoPermission = getRecordProperty({ value: repoConfig, key: "permission" });
|
||||||
read: "deny",
|
if (repoPermission) {
|
||||||
bash: shell !== "enabled" ? "deny" : "allow",
|
Object.assign(permission, repoPermission);
|
||||||
webfetch: ctx.payload.web === "disabled" ? "deny" : "allow",
|
}
|
||||||
external_directory: "deny",
|
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
|
// build complete config in one object
|
||||||
const config = {
|
const config: OpenCodeConfig = {};
|
||||||
mcp: opencodeMcpServers,
|
if (repoConfig) {
|
||||||
permission,
|
Object.assign(config, repoConfig);
|
||||||
};
|
}
|
||||||
|
config.mcp = opencodeMcpServers;
|
||||||
|
config.permission = permission;
|
||||||
|
|
||||||
const configJson = JSON.stringify(config, null, 2);
|
const configJson = JSON.stringify(config, null, 2);
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -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) {
|
"node_modules/.pnpm/undici@7.16.0/node_modules/undici/lib/mock/snapshot-recorder.js"(exports, module) {
|
||||||
"use strict";
|
"use strict";
|
||||||
var { writeFile, readFile, mkdir } = __require("node:fs/promises");
|
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 { setTimeout: setTimeout2, clearTimeout: clearTimeout2 } = __require("node:timers");
|
||||||
var { InvalidArgumentError, UndiciError } = require_errors4();
|
var { InvalidArgumentError, UndiciError } = require_errors4();
|
||||||
var { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = require_snapshot_utils();
|
var { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = require_snapshot_utils();
|
||||||
@@ -56498,7 +56498,7 @@ var require_snapshot_recorder = __commonJS({
|
|||||||
throw new InvalidArgumentError("Snapshot path is required");
|
throw new InvalidArgumentError("Snapshot path is required");
|
||||||
}
|
}
|
||||||
const resolvedPath = resolve3(path3);
|
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]) => ({
|
const data = Array.from(this.#snapshots.entries()).map(([hash2, snapshot2]) => ({
|
||||||
hash: hash2,
|
hash: hash2,
|
||||||
snapshot: snapshot2
|
snapshot: snapshot2
|
||||||
@@ -97612,6 +97612,7 @@ var require_semver2 = __commonJS({
|
|||||||
|
|
||||||
// entry.ts
|
// entry.ts
|
||||||
var core5 = __toESM(require_core(), 1);
|
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
|
// node_modules/.pnpm/@ark+util@0.56.0/node_modules/@ark/util/out/arrays.js
|
||||||
var liftArray = (data) => Array.isArray(data) ? data : [data];
|
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:
|
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.
|
- **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\`.
|
- **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\`.
|
- **Output**: \`${ghPullfrogMcpName}/upload_file\`, \`${ghPullfrogMcpName}/set_output\`.
|
||||||
|
|
||||||
@@ -141611,7 +141612,7 @@ function buildTaskResult(label, effort, subagent, error49) {
|
|||||||
function DelegateTool(ctx) {
|
function DelegateTool(ctx) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "delegate",
|
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,
|
parameters: DelegateParams,
|
||||||
execute: execute(async (params) => {
|
execute: execute(async (params) => {
|
||||||
if (ctx.toolState.selfSubagentId) {
|
if (ctx.toolState.selfSubagentId) {
|
||||||
@@ -141626,7 +141627,10 @@ function DelegateTool(ctx) {
|
|||||||
if (!ctx.toolState.selectedMode) {
|
if (!ctx.toolState.selectedMode) {
|
||||||
log.info(`\xBB warning: delegating without calling select_mode first (mode=${mode})`);
|
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 taskEntries = params.tasks.map((task) => {
|
||||||
const effort = task.effort ?? "auto";
|
const effort = task.effort ?? "auto";
|
||||||
const subagent = createSubagentState({ ctx, mode, label: task.label });
|
const subagent = createSubagentState({ ctx, mode, label: task.label });
|
||||||
@@ -141646,10 +141650,10 @@ function DelegateTool(ctx) {
|
|||||||
const results = taskEntries.map((entry, i) => {
|
const results = taskEntries.map((entry, i) => {
|
||||||
const outcome = settled[i];
|
const outcome = settled[i];
|
||||||
const error49 = outcome.status === "rejected" ? String(outcome.reason) : outcome.value.error;
|
const error49 = outcome.status === "rejected" ? String(outcome.reason) : outcome.value.error;
|
||||||
log.debug(
|
const result = buildTaskResult(entry.task.label, entry.effort, entry.subagent, error49);
|
||||||
`\xBB task "${entry.task.label}" result: output=${entry.subagent.output !== void 0}, status=${entry.subagent.status}`
|
const status = result.success ? "succeeded" : "failed";
|
||||||
);
|
log.box(result.summary, { title: `task "${entry.task.label}" ${status}` });
|
||||||
return buildTaskResult(entry.task.label, entry.effort, entry.subagent, error49);
|
return result;
|
||||||
});
|
});
|
||||||
const succeeded = results.filter((r) => r.success).length;
|
const succeeded = results.filter((r) => r.success).length;
|
||||||
log.info(`\xBB delegation completed: ${succeeded}/${results.length} succeeded (mode=${mode})`);
|
log.info(`\xBB delegation completed: ${succeeded}/${results.length} succeeded (mode=${mode})`);
|
||||||
@@ -142071,12 +142075,15 @@ var installNodeDependencies = {
|
|||||||
const result = await spawn({
|
const result = await spawn({
|
||||||
cmd: resolved.command,
|
cmd: resolved.command,
|
||||||
args: resolved.args,
|
args: resolved.args,
|
||||||
env: { PATH: process.env.PATH || "", HOME: process.env.HOME || "" },
|
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) {
|
if (result.exitCode !== 0) {
|
||||||
const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
||||||
const errorMessage = output || `exited with code ${result.exitCode}`;
|
const errorMessage = output || `exited with code ${result.exitCode}`;
|
||||||
return {
|
return {
|
||||||
language: "node",
|
language: "node",
|
||||||
@@ -142213,20 +142220,26 @@ var installPythonDependencies = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const [cmd, ...args2] = config3.installCmd;
|
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({
|
const result = await spawn({
|
||||||
cmd,
|
cmd,
|
||||||
args: args2,
|
args: args2,
|
||||||
env: { PATH: process.env.PATH || "", HOME: process.env.HOME || "" },
|
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) {
|
if (result.exitCode !== 0) {
|
||||||
return {
|
return {
|
||||||
language: "python",
|
language: "python",
|
||||||
packageManager: config3.tool,
|
packageManager: config3.tool,
|
||||||
configFile: config3.file,
|
configFile: config3.file,
|
||||||
dependenciesInstalled: false,
|
dependenciesInstalled: false,
|
||||||
issues: [result.stderr || `${cmd} exited with code ${result.exitCode}`]
|
issues: [output || `${cmd} exited with code ${result.exitCode}`]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -143746,11 +143759,8 @@ var SelectModeParams = type({
|
|||||||
function resolveMode(modes2, modeName) {
|
function resolveMode(modes2, modeName) {
|
||||||
return modes2.find((m) => m.name.toLowerCase() === modeName.toLowerCase()) ?? null;
|
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 = {
|
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.
|
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.
|
- 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
|
- 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.
|
- 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)
|
- 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.
|
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\`
|
- create a PR via \`${ghPullfrogMcpName}/create_pull_request\`
|
||||||
- call \`${ghPullfrogMcpName}/report_progress\` with the final summary including PR link
|
- 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.
|
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.`,
|
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: `Delegate a single subagent to address PR review feedback.
|
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)
|
- 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
|
- 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
|
- 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
|
- 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\`
|
- push changes via \`${ghPullfrogMcpName}/push_branch\`
|
||||||
- reply to each comment using \`${ghPullfrogMcpName}/reply_to_review_comment\` with the subagent's suggested replies
|
- reply to each comment using \`${ghPullfrogMcpName}/reply_to_review_comment\` with the subagent's suggested replies
|
||||||
- resolve addressed threads via \`${ghPullfrogMcpName}/resolve_review_thread\`
|
- resolve addressed threads via \`${ghPullfrogMcpName}/resolve_review_thread\`
|
||||||
- call \`${ghPullfrogMcpName}/report_progress\` with a brief summary
|
- call \`${ghPullfrogMcpName}/report_progress\` with a brief summary
|
||||||
|
|
||||||
Use auto or max effort depending on review complexity.`,
|
### Effort
|
||||||
Review: `For reviews, delegate multiple focused subagents in parallel \u2014 each investigating a different area or aspect of the PR.
|
|
||||||
|
|
||||||
### 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.
|
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".
|
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
|
- 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.`,
|
Use max effort for thorough reviews.`,
|
||||||
Plan: `Delegate a single planning subagent:
|
Plan: `### Checklist
|
||||||
|
|
||||||
Include in its prompt:
|
1. Include in its prompt:
|
||||||
- the task to plan for
|
- the task to plan for
|
||||||
- relevant codebase context (file paths, architecture notes from AGENTS.md)
|
- relevant codebase context (file paths, architecture notes from AGENTS.md)
|
||||||
- instruct it to produce a structured, actionable plan with clear milestones
|
- 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)
|
- 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.`,
|
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 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
|
- 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.
|
- 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
|
- 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
|
- 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.
|
- 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)
|
- 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\`
|
- push changes via \`${ghPullfrogMcpName}/push_branch\`
|
||||||
- call \`${ghPullfrogMcpName}/report_progress\` with the diagnosis and fix summary
|
- call \`${ghPullfrogMcpName}/report_progress\` with the diagnosis and fix summary
|
||||||
|
|
||||||
|
### Effort
|
||||||
|
|
||||||
Use auto 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:
|
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.
|
- \`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.
|
- \`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:
|
||||||
### When delegating
|
- 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.
|
||||||
Include in each task's prompt:
|
- if code changes are needed: branch naming, testing, commit instructions (do NOT instruct to push or create PR)
|
||||||
- the full subtask description with all relevant context
|
- 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
|
||||||
- exactly what information to return. the subagent's output is your only way to get results back \u2014 be precise about what you need.
|
4. Post-delegation:
|
||||||
- if code changes are needed: branch naming, testing, commit instructions (do NOT instruct to push or create PR)
|
- call \`${ghPullfrogMcpName}/report_progress\` with results
|
||||||
- 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
|
- 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
|
||||||
### Post-delegation
|
5. Use mini effort for simple research tasks, auto for typical tasks, max for complex multi-file changes.`
|
||||||
|
|
||||||
- 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.`
|
|
||||||
};
|
};
|
||||||
function buildOrchestratorGuidance(mode) {
|
function buildOrchestratorGuidance(mode) {
|
||||||
const guidance = modeGuidance[mode.name] ?? defaultGuidance(mode);
|
const guidance = modeGuidance[mode.name] ?? "";
|
||||||
return {
|
return {
|
||||||
modeName: mode.name,
|
modeName: mode.name,
|
||||||
description: mode.description,
|
description: mode.description,
|
||||||
@@ -143913,6 +143922,7 @@ function SelectModeTool(ctx) {
|
|||||||
import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
|
import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
|
||||||
import { randomUUID as randomUUID3 } from "node:crypto";
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
||||||
import { closeSync, openSync, writeFileSync as writeFileSync7 } from "node:fs";
|
import { closeSync, openSync, writeFileSync as writeFileSync7 } from "node:fs";
|
||||||
|
import { userInfo } from "node:os";
|
||||||
import { join as join9 } from "node:path";
|
import { join as join9 } from "node:path";
|
||||||
var ShellParams = type({
|
var ShellParams = type({
|
||||||
command: "string",
|
command: "string",
|
||||||
@@ -143959,13 +143969,14 @@ function detectSandboxMethod() {
|
|||||||
log.info("PID namespace isolation not available - falling back to env filtering only");
|
log.info("PID namespace isolation not available - falling back to env filtering only");
|
||||||
return "none";
|
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) {
|
function spawnShell(params) {
|
||||||
const spawnOpts = { env: params.env, cwd: params.cwd, stdio: params.stdio, detached: true };
|
const spawnOpts = { env: params.env, cwd: params.cwd, stdio: params.stdio, detached: true };
|
||||||
const sandboxMethod = detectSandboxMethod();
|
const sandboxMethod = detectSandboxMethod();
|
||||||
if (sandboxMethod === "unshare") {
|
if (sandboxMethod === "unshare") {
|
||||||
return spawn2(
|
return spawn2(
|
||||||
"unshare",
|
"unshare",
|
||||||
["--pid", "--fork", "--mount-proc", "bash", "-c", params.command],
|
["--pid", "--fork", "--mount-proc", "bash", "-c", `${PROC_CLEANUP} ${params.command}`],
|
||||||
spawnOpts
|
spawnOpts
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -143976,6 +143987,8 @@ function spawnShell(params) {
|
|||||||
envArgs.push(`${k}=${v}`);
|
envArgs.push(`${k}=${v}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const username = userInfo().username;
|
||||||
|
const escaped = params.command.replace(/'/g, "'\\''");
|
||||||
return spawn2(
|
return spawn2(
|
||||||
"sudo",
|
"sudo",
|
||||||
[
|
[
|
||||||
@@ -143987,10 +144000,9 @@ function spawnShell(params) {
|
|||||||
"--mount-proc",
|
"--mount-proc",
|
||||||
"bash",
|
"bash",
|
||||||
"-c",
|
"-c",
|
||||||
params.command
|
`${PROC_CLEANUP} exec su -p -s /bin/bash ${username} -c '${escaped}'`
|
||||||
],
|
],
|
||||||
{ ...spawnOpts, env: {} }
|
{ ...spawnOpts, env: {} }
|
||||||
// empty env since we pass via sudo env
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return spawn2("bash", ["-c", params.command], spawnOpts);
|
return spawn2("bash", ["-c", params.command], spawnOpts);
|
||||||
@@ -144242,43 +144254,26 @@ function isAddressInUse(error49) {
|
|||||||
const message = getErrorMessage(error49).toLowerCase();
|
const message = getErrorMessage(error49).toLowerCase();
|
||||||
return message.includes("eaddrinuse") || message.includes("address already in use");
|
return message.includes("eaddrinuse") || message.includes("address already in use");
|
||||||
}
|
}
|
||||||
function buildSubagentTools(ctx) {
|
function buildCommonTools(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) {
|
|
||||||
const tools = [
|
const tools = [
|
||||||
StartDependencyInstallationTool(ctx),
|
StartDependencyInstallationTool(ctx),
|
||||||
AwaitDependencyInstallationTool(ctx),
|
AwaitDependencyInstallationTool(ctx),
|
||||||
|
CreateCommentTool(ctx),
|
||||||
|
EditCommentTool(ctx),
|
||||||
|
ReplyToReviewCommentTool(ctx),
|
||||||
|
IssueTool(ctx),
|
||||||
IssueInfoTool(ctx),
|
IssueInfoTool(ctx),
|
||||||
GetIssueCommentsTool(ctx),
|
GetIssueCommentsTool(ctx),
|
||||||
GetIssueEventsTool(ctx),
|
GetIssueEventsTool(ctx),
|
||||||
|
CreatePullRequestReviewTool(ctx),
|
||||||
PullRequestInfoTool(ctx),
|
PullRequestInfoTool(ctx),
|
||||||
CommitInfoTool(ctx),
|
CommitInfoTool(ctx),
|
||||||
CheckoutPrTool(ctx),
|
CheckoutPrTool(ctx),
|
||||||
GetReviewCommentsTool(ctx),
|
GetReviewCommentsTool(ctx),
|
||||||
ListPullRequestReviewsTool(ctx),
|
ListPullRequestReviewsTool(ctx),
|
||||||
|
ResolveReviewThreadTool(ctx),
|
||||||
GetCheckSuiteLogsTool(ctx),
|
GetCheckSuiteLogsTool(ctx),
|
||||||
|
AddLabelsTool(ctx),
|
||||||
GitTool(ctx),
|
GitTool(ctx),
|
||||||
GitFetchTool(ctx),
|
GitFetchTool(ctx),
|
||||||
UploadFileTool(ctx),
|
UploadFileTool(ctx),
|
||||||
@@ -144287,14 +144282,17 @@ function buildOrchestratorTools(ctx) {
|
|||||||
FileWriteTool(ctx),
|
FileWriteTool(ctx),
|
||||||
FileEditTool(ctx),
|
FileEditTool(ctx),
|
||||||
FileDeleteTool(ctx),
|
FileDeleteTool(ctx),
|
||||||
ListDirectoryTool(ctx),
|
ListDirectoryTool(ctx)
|
||||||
CreateCommentTool(ctx),
|
];
|
||||||
EditCommentTool(ctx),
|
if (ctx.payload.shell === "restricted") {
|
||||||
ReplyToReviewCommentTool(ctx),
|
tools.push(ShellTool(ctx));
|
||||||
CreatePullRequestReviewTool(ctx),
|
tools.push(KillBackgroundTool(ctx));
|
||||||
ResolveReviewThreadTool(ctx),
|
}
|
||||||
IssueTool(ctx),
|
return tools;
|
||||||
AddLabelsTool(ctx),
|
}
|
||||||
|
function buildOrchestratorTools(ctx) {
|
||||||
|
return [
|
||||||
|
...buildCommonTools(ctx),
|
||||||
ReportProgressTool(ctx),
|
ReportProgressTool(ctx),
|
||||||
SelectModeTool(ctx),
|
SelectModeTool(ctx),
|
||||||
DelegateTool(ctx),
|
DelegateTool(ctx),
|
||||||
@@ -144305,11 +144303,9 @@ function buildOrchestratorTools(ctx) {
|
|||||||
CreatePullRequestTool(ctx),
|
CreatePullRequestTool(ctx),
|
||||||
UpdatePullRequestBodyTool(ctx)
|
UpdatePullRequestBodyTool(ctx)
|
||||||
];
|
];
|
||||||
if (ctx.payload.shell === "restricted") {
|
}
|
||||||
tools.push(ShellTool(ctx));
|
function buildSubagentTools(ctx) {
|
||||||
tools.push(KillBackgroundTool(ctx));
|
return buildCommonTools(ctx);
|
||||||
}
|
|
||||||
return tools;
|
|
||||||
}
|
}
|
||||||
async function tryStartMcpServer(ctx, tools, port) {
|
async function tryStartMcpServer(ctx, tools, port) {
|
||||||
const server = new FastMCP({ name: ghPullfrogMcpName, version: "0.0.1" });
|
const server = new FastMCP({ name: ghPullfrogMcpName, version: "0.0.1" });
|
||||||
@@ -144643,7 +144639,7 @@ import { join as join11 } from "node:path";
|
|||||||
// package.json
|
// package.json
|
||||||
var package_default = {
|
var package_default = {
|
||||||
name: "@pullfrog/pullfrog",
|
name: "@pullfrog/pullfrog",
|
||||||
version: "0.0.170",
|
version: "0.0.173",
|
||||||
type: "module",
|
type: "module",
|
||||||
files: [
|
files: [
|
||||||
"index.js",
|
"index.js",
|
||||||
@@ -146044,7 +146040,7 @@ function configureGeminiSettings(ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// agents/opencode.ts
|
// 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 { join as join15 } from "node:path";
|
||||||
import { performance as performance7 } from "node:perf_hooks";
|
import { performance as performance7 } from "node:perf_hooks";
|
||||||
var OPENCODE_CLI_VERSION = "1.1.56";
|
var OPENCODE_CLI_VERSION = "1.1.56";
|
||||||
@@ -146065,6 +146061,101 @@ function detectProviderError(text) {
|
|||||||
}
|
}
|
||||||
return null;
|
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() {
|
async function installOpencode() {
|
||||||
return await installFromNpmTarball({
|
return await installFromNpmTarball({
|
||||||
packageName: "opencode-ai",
|
packageName: "opencode-ai",
|
||||||
@@ -146083,10 +146174,13 @@ var opencode = agent({
|
|||||||
mkdirSync9(configDir, { recursive: true });
|
mkdirSync9(configDir, { recursive: true });
|
||||||
configureOpenCode(ctx);
|
configureOpenCode(ctx);
|
||||||
const args2 = ["run", ctx.instructions.full, "--format", "json", "--print-logs"];
|
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) {
|
if (modelOverride) {
|
||||||
args2.push("--model", modelOverride);
|
args2.push("--model", modelOverride.model);
|
||||||
log.info(`\xBB model: ${modelOverride} (override)`);
|
log.info(`\xBB model: ${modelOverride.model} (override via ${modelOverride.source})`);
|
||||||
} else {
|
} else {
|
||||||
log.info(`\xBB model: auto-selected by OpenCode`);
|
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)
|
// 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
|
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;
|
delete env2.GITHUB_TOKEN;
|
||||||
const repoDir = process.cwd();
|
const repoDir = process.cwd();
|
||||||
log.debug(`\xBB starting OpenCode: ${cliPath} ${args2.join(" ")}`);
|
log.debug(`\xBB starting OpenCode: ${cliPath} ${args2.join(" ")}`);
|
||||||
@@ -146250,21 +146366,34 @@ function configureOpenCode(ctx) {
|
|||||||
const configDir = join15(ctx.tmpdir, ".config", "opencode");
|
const configDir = join15(ctx.tmpdir, ".config", "opencode");
|
||||||
mkdirSync9(configDir, { recursive: true });
|
mkdirSync9(configDir, { recursive: true });
|
||||||
const configPath = join15(configDir, "opencode.json");
|
const configPath = join15(configDir, "opencode.json");
|
||||||
const opencodeMcpServers = {
|
const repoConfigPath = join15(process.cwd(), "opencode.json");
|
||||||
[ghPullfrogMcpName]: { type: "remote", url: ctx.mcpServerUrl }
|
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 shell = ctx.payload.shell;
|
||||||
const permission = {
|
const permission = {};
|
||||||
edit: "deny",
|
const repoPermission = getRecordProperty({ value: repoConfig, key: "permission" });
|
||||||
read: "deny",
|
if (repoPermission) {
|
||||||
bash: shell !== "enabled" ? "deny" : "allow",
|
Object.assign(permission, repoPermission);
|
||||||
webfetch: ctx.payload.web === "disabled" ? "deny" : "allow",
|
}
|
||||||
external_directory: "deny"
|
permission.edit = "deny";
|
||||||
};
|
permission.read = "deny";
|
||||||
const config3 = {
|
permission.bash = shell !== "enabled" ? "deny" : "allow";
|
||||||
mcp: opencodeMcpServers,
|
permission.webfetch = ctx.payload.web === "disabled" ? "deny" : "allow";
|
||||||
permission
|
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);
|
const configJson = JSON.stringify(config3, null, 2);
|
||||||
try {
|
try {
|
||||||
writeFileSync12(configPath, configJson, "utf-8");
|
writeFileSync12(configPath, configJson, "utf-8");
|
||||||
@@ -146955,13 +147084,13 @@ When all delegations are complete, call \`${ghPullfrogMcpName}/set_output\` with
|
|||||||
|
|
||||||
### Subagent capabilities
|
### 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
|
### Prompt-crafting rules
|
||||||
|
|
||||||
- Subagents have NO context beyond what you write. No repo instructions, no event data, no user prompt.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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
|
// entry.ts
|
||||||
|
process.env.PATH = `${dirname2(process.execPath)}:${process.env.PATH}`;
|
||||||
async function run() {
|
async function run() {
|
||||||
try {
|
try {
|
||||||
const result = await main();
|
const result = await main();
|
||||||
|
|||||||
@@ -4,9 +4,15 @@
|
|||||||
* entry point for pullfrog/pullfrog - unified action
|
* entry point for pullfrog/pullfrog - unified action
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { dirname } from "node:path";
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import { main } from "./main.ts";
|
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> {
|
async function run(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const result = await main();
|
const result = await main();
|
||||||
|
|||||||
+9
-6
@@ -57,7 +57,7 @@ export function DelegateTool(ctx: ToolContext) {
|
|||||||
return tool({
|
return tool({
|
||||||
name: "delegate",
|
name: "delegate",
|
||||||
description:
|
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,
|
parameters: DelegateParams,
|
||||||
execute: execute(async (params) => {
|
execute: execute(async (params) => {
|
||||||
if (ctx.toolState.selfSubagentId) {
|
if (ctx.toolState.selfSubagentId) {
|
||||||
@@ -77,7 +77,10 @@ export function DelegateTool(ctx: ToolContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// matched by delegate test validators — update tests if changed
|
// 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 taskEntries = params.tasks.map((task) => {
|
||||||
const effort = task.effort ?? "auto";
|
const effort = task.effort ?? "auto";
|
||||||
@@ -100,10 +103,10 @@ export function DelegateTool(ctx: ToolContext) {
|
|||||||
const results: DelegateTaskResult[] = taskEntries.map((entry, i) => {
|
const results: DelegateTaskResult[] = taskEntries.map((entry, i) => {
|
||||||
const outcome = settled[i];
|
const outcome = settled[i];
|
||||||
const error = outcome.status === "rejected" ? String(outcome.reason) : outcome.value.error;
|
const error = outcome.status === "rejected" ? String(outcome.reason) : outcome.value.error;
|
||||||
log.debug(
|
const result = buildTaskResult(entry.task.label, entry.effort, entry.subagent, error);
|
||||||
`» task "${entry.task.label}" result: output=${entry.subagent.output !== undefined}, status=${entry.subagent.status}`
|
const status = result.success ? "succeeded" : "failed";
|
||||||
);
|
log.box(result.summary, { title: `task "${entry.task.label}" ${status}` });
|
||||||
return buildTaskResult(entry.task.label, entry.effort, entry.subagent, error);
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
const succeeded = results.filter((r) => r.success).length;
|
const succeeded = results.filter((r) => r.success).length;
|
||||||
|
|||||||
+44
-49
@@ -14,12 +14,8 @@ function resolveMode(modes: Mode[], modeName: string): Mode | null {
|
|||||||
return modes.find((m) => m.name.toLowerCase() === modeName.toLowerCase()) ?? 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> = {
|
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.
|
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.
|
- 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
|
- 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.
|
- 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)
|
- 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.
|
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\`
|
- create a PR via \`${ghPullfrogMcpName}/create_pull_request\`
|
||||||
- call \`${ghPullfrogMcpName}/report_progress\` with the final summary including PR link
|
- 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.
|
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)
|
- 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
|
- 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
|
- 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
|
- 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\`
|
- push changes via \`${ghPullfrogMcpName}/push_branch\`
|
||||||
- reply to each comment using \`${ghPullfrogMcpName}/reply_to_review_comment\` with the subagent's suggested replies
|
- reply to each comment using \`${ghPullfrogMcpName}/reply_to_review_comment\` with the subagent's suggested replies
|
||||||
- resolve addressed threads via \`${ghPullfrogMcpName}/resolve_review_thread\`
|
- resolve addressed threads via \`${ghPullfrogMcpName}/resolve_review_thread\`
|
||||||
- call \`${ghPullfrogMcpName}/report_progress\` with a brief summary
|
- call \`${ghPullfrogMcpName}/report_progress\` with a brief summary
|
||||||
|
|
||||||
|
### Effort
|
||||||
|
|
||||||
Use auto or max effort depending on review complexity.`,
|
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.
|
Review: `### Checklist
|
||||||
|
|
||||||
### Approach
|
|
||||||
|
|
||||||
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.
|
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".
|
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.`,
|
Use max effort for thorough reviews.`,
|
||||||
|
|
||||||
Plan: `Delegate a single planning subagent:
|
Plan: `### Checklist
|
||||||
|
|
||||||
Include in its prompt:
|
1. Include in its prompt:
|
||||||
- the task to plan for
|
- the task to plan for
|
||||||
- relevant codebase context (file paths, architecture notes from AGENTS.md)
|
- relevant codebase context (file paths, architecture notes from AGENTS.md)
|
||||||
- instruct it to produce a structured, actionable plan with clear milestones
|
- 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)
|
- 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.`,
|
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 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
|
- 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.
|
- 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
|
- 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
|
- 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.
|
- 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)
|
- 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\`
|
- push changes via \`${ghPullfrogMcpName}/push_branch\`
|
||||||
- call \`${ghPullfrogMcpName}/report_progress\` with the diagnosis and fix summary
|
- call \`${ghPullfrogMcpName}/report_progress\` with the diagnosis and fix summary
|
||||||
|
|
||||||
|
### Effort
|
||||||
|
|
||||||
Use auto 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:
|
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.
|
- \`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.
|
- \`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:
|
||||||
### When delegating
|
- 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.
|
||||||
Include in each task's prompt:
|
- if code changes are needed: branch naming, testing, commit instructions (do NOT instruct to push or create PR)
|
||||||
- the full subtask description with all relevant context
|
- 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
|
||||||
- exactly what information to return. the subagent's output is your only way to get results back — be precise about what you need.
|
4. Post-delegation:
|
||||||
- if code changes are needed: branch naming, testing, commit instructions (do NOT instruct to push or create PR)
|
- call \`${ghPullfrogMcpName}/report_progress\` with results
|
||||||
- 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
|
- 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
|
||||||
### Post-delegation
|
5. Use mini effort for simple research tasks, auto for typical tasks, max for complex multi-file changes.`,
|
||||||
|
|
||||||
- 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.`,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type OrchestratorGuidance = {
|
type OrchestratorGuidance = {
|
||||||
@@ -160,7 +155,7 @@ type OrchestratorGuidance = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function buildOrchestratorGuidance(mode: Mode): OrchestratorGuidance {
|
function buildOrchestratorGuidance(mode: Mode): OrchestratorGuidance {
|
||||||
const guidance = modeGuidance[mode.name] ?? defaultGuidance(mode);
|
const guidance = modeGuidance[mode.name] ?? "";
|
||||||
return {
|
return {
|
||||||
modeName: mode.name,
|
modeName: mode.name,
|
||||||
description: mode.description,
|
description: mode.description,
|
||||||
|
|||||||
+30
-48
@@ -188,50 +188,27 @@ function isAddressInUse(error: unknown): boolean {
|
|||||||
return message.includes("eaddrinuse") || message.includes("address already in use");
|
return message.includes("eaddrinuse") || message.includes("address already in use");
|
||||||
}
|
}
|
||||||
|
|
||||||
// subagent tools: file ops, bash, read-only GitHub, upload, set_output.
|
// tools shared by both orchestrator and subagent servers
|
||||||
// no git/checkout (mutates shared state), no dependencies (shared state),
|
function buildCommonTools(ctx: ToolContext): Tool<any, any>[] {
|
||||||
// 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>[] {
|
|
||||||
const tools: Tool<any, any>[] = [
|
const tools: Tool<any, any>[] = [
|
||||||
StartDependencyInstallationTool(ctx),
|
StartDependencyInstallationTool(ctx),
|
||||||
AwaitDependencyInstallationTool(ctx),
|
AwaitDependencyInstallationTool(ctx),
|
||||||
|
CreateCommentTool(ctx),
|
||||||
|
EditCommentTool(ctx),
|
||||||
|
ReplyToReviewCommentTool(ctx),
|
||||||
|
IssueTool(ctx),
|
||||||
IssueInfoTool(ctx),
|
IssueInfoTool(ctx),
|
||||||
GetIssueCommentsTool(ctx),
|
GetIssueCommentsTool(ctx),
|
||||||
GetIssueEventsTool(ctx),
|
GetIssueEventsTool(ctx),
|
||||||
|
CreatePullRequestReviewTool(ctx),
|
||||||
PullRequestInfoTool(ctx),
|
PullRequestInfoTool(ctx),
|
||||||
CommitInfoTool(ctx),
|
CommitInfoTool(ctx),
|
||||||
CheckoutPrTool(ctx),
|
CheckoutPrTool(ctx),
|
||||||
GetReviewCommentsTool(ctx),
|
GetReviewCommentsTool(ctx),
|
||||||
ListPullRequestReviewsTool(ctx),
|
ListPullRequestReviewsTool(ctx),
|
||||||
|
ResolveReviewThreadTool(ctx),
|
||||||
GetCheckSuiteLogsTool(ctx),
|
GetCheckSuiteLogsTool(ctx),
|
||||||
|
AddLabelsTool(ctx),
|
||||||
GitTool(ctx),
|
GitTool(ctx),
|
||||||
GitFetchTool(ctx),
|
GitFetchTool(ctx),
|
||||||
UploadFileTool(ctx),
|
UploadFileTool(ctx),
|
||||||
@@ -241,22 +218,6 @@ function buildOrchestratorTools(ctx: ToolContext): Tool<any, any>[] {
|
|||||||
FileEditTool(ctx),
|
FileEditTool(ctx),
|
||||||
FileDeleteTool(ctx),
|
FileDeleteTool(ctx),
|
||||||
ListDirectoryTool(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"
|
// only add ShellTool when shell is "restricted"
|
||||||
@@ -271,6 +232,27 @@ function buildOrchestratorTools(ctx: ToolContext): Tool<any, any>[] {
|
|||||||
return tools;
|
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 = {
|
type McpStartResult = {
|
||||||
server: FastMCP;
|
server: FastMCP;
|
||||||
url: string;
|
url: string;
|
||||||
|
|||||||
+18
-11
@@ -2,6 +2,7 @@
|
|||||||
import { type ChildProcess, type StdioOptions, spawn, spawnSync } from "node:child_process";
|
import { type ChildProcess, type StdioOptions, spawn, spawnSync } from "node:child_process";
|
||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import { closeSync, openSync, writeFileSync } from "node:fs";
|
import { closeSync, openSync, writeFileSync } from "node:fs";
|
||||||
|
import { userInfo } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import { log } from "../utils/log.ts";
|
import { log } from "../utils/log.ts";
|
||||||
@@ -82,33 +83,39 @@ function detectSandboxMethod(): SandboxMethod {
|
|||||||
return "none";
|
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 {
|
function spawnShell(params: SpawnParams): ChildProcess {
|
||||||
const spawnOpts = { env: params.env, cwd: params.cwd, stdio: params.stdio, detached: true };
|
const spawnOpts = { env: params.env, cwd: params.cwd, stdio: params.stdio, detached: true };
|
||||||
const sandboxMethod = detectSandboxMethod();
|
const sandboxMethod = detectSandboxMethod();
|
||||||
|
|
||||||
if (sandboxMethod === "unshare") {
|
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(
|
return spawn(
|
||||||
"unshare",
|
"unshare",
|
||||||
["--pid", "--fork", "--mount-proc", "bash", "-c", params.command],
|
["--pid", "--fork", "--mount-proc", "bash", "-c", `${PROC_CLEANUP} ${params.command}`],
|
||||||
spawnOpts
|
spawnOpts
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sandboxMethod === "sudo-unshare") {
|
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[] = [];
|
const envArgs: string[] = [];
|
||||||
for (const [k, v] of Object.entries(params.env)) {
|
for (const [k, v] of Object.entries(params.env)) {
|
||||||
if (v !== undefined) {
|
if (v !== undefined) {
|
||||||
envArgs.push(`${k}=${v}`);
|
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(
|
return spawn(
|
||||||
"sudo",
|
"sudo",
|
||||||
[
|
[
|
||||||
@@ -120,9 +127,9 @@ function spawnShell(params: SpawnParams): ChildProcess {
|
|||||||
"--mount-proc",
|
"--mount-proc",
|
||||||
"bash",
|
"bash",
|
||||||
"-c",
|
"-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
@@ -71,7 +71,7 @@ const subagentSystemPreamble = `You are a focused subagent. Complete the task au
|
|||||||
|
|
||||||
Your tools are limited to:
|
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.
|
- **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\`.
|
- **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\`.
|
- **Output**: \`${ghPullfrogMcpName}/upload_file\`, \`${ghPullfrogMcpName}/set_output\`.
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ describe("per-server tool isolation - integration", () => {
|
|||||||
orchestratorServer.addTool(mockTool("push_branch", "push branch"));
|
orchestratorServer.addTool(mockTool("push_branch", "push branch"));
|
||||||
orchestratorServer.addTool(mockTool("create_pull_request", "create PR"));
|
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 = new FastMCP({ name: "subagent", version: "0.0.1" });
|
||||||
subagentServer.addTool(mockTool("file_read", "read a file"));
|
subagentServer.addTool(mockTool("file_read", "read a file"));
|
||||||
subagentServer.addTool(mockTool("set_output", "set output"));
|
subagentServer.addTool(mockTool("set_output", "set output"));
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@pullfrog/pullfrog",
|
"name": "@pullfrog/pullfrog",
|
||||||
"version": "0.0.170",
|
"version": "0.0.173",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"index.js",
|
"index.js",
|
||||||
|
|||||||
@@ -41235,7 +41235,7 @@ var Effort = type.enumerated("mini", "auto", "max");
|
|||||||
// package.json
|
// package.json
|
||||||
var package_default = {
|
var package_default = {
|
||||||
name: "@pullfrog/pullfrog",
|
name: "@pullfrog/pullfrog",
|
||||||
version: "0.0.170",
|
version: "0.0.173",
|
||||||
type: "module",
|
type: "module",
|
||||||
files: [
|
files: [
|
||||||
"index.js",
|
"index.js",
|
||||||
|
|||||||
@@ -159,13 +159,16 @@ export const installNodeDependencies: PrepDefinition = {
|
|||||||
cmd: resolved.command,
|
cmd: resolved.command,
|
||||||
args: resolved.args,
|
args: resolved.args,
|
||||||
env: { PATH: process.env.PATH || "", HOME: process.env.HOME || "" },
|
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) {
|
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}`;
|
const errorMessage = output || `exited with code ${result.exitCode}`;
|
||||||
return {
|
return {
|
||||||
language: "node",
|
language: "node",
|
||||||
|
|||||||
@@ -162,21 +162,28 @@ export const installPythonDependencies: PrepDefinition = {
|
|||||||
|
|
||||||
// run the install command
|
// run the install command
|
||||||
const [cmd, ...args] = config.installCmd;
|
const [cmd, ...args] = config.installCmd;
|
||||||
log.info(`» running: ${cmd} ${args.join(" ")}`);
|
const fullCommand = `${cmd} ${args.join(" ")}`;
|
||||||
|
log.info(`» running: ${fullCommand}`);
|
||||||
const result = await spawn({
|
const result = await spawn({
|
||||||
cmd,
|
cmd,
|
||||||
args,
|
args,
|
||||||
env: { PATH: process.env.PATH || "", HOME: process.env.HOME || "" },
|
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) {
|
if (result.exitCode !== 0) {
|
||||||
return {
|
return {
|
||||||
language: "python",
|
language: "python",
|
||||||
packageManager: config.tool,
|
packageManager: config.tool,
|
||||||
configFile: config.file,
|
configFile: config.file,
|
||||||
dependenciesInstalled: false,
|
dependenciesInstalled: false,
|
||||||
issues: [result.stderr || `${cmd} exited with code ${result.exitCode}`],
|
issues: [output || `${cmd} exited with code ${result.exitCode}`],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ const expectedAgentEnvVars = [
|
|||||||
"GITHUB_TOKEN",
|
"GITHUB_TOKEN",
|
||||||
...new Set(Object.values(agentsManifest).flatMap((a) => a.apiKeyNames)),
|
...new Set(Object.values(agentsManifest).flatMap((a) => a.apiKeyNames)),
|
||||||
"GEMINI_MODEL",
|
"GEMINI_MODEL",
|
||||||
|
"OPENCODE_MODEL_MAX",
|
||||||
|
"OPENCODE_MODEL_MINI",
|
||||||
"OPENCODE_MODEL",
|
"OPENCODE_MODEL",
|
||||||
].sort();
|
].sort();
|
||||||
|
|
||||||
|
|||||||
@@ -120,6 +120,8 @@ const testEnvAllowList = new Set([
|
|||||||
"GOOGLE_GENERATIVE_AI_API_KEY",
|
"GOOGLE_GENERATIVE_AI_API_KEY",
|
||||||
"CURSOR_API_KEY",
|
"CURSOR_API_KEY",
|
||||||
"OPENCODE_MODEL", // override OpenCode model (e.g. google/gemini-3-flash-preview) for tests or user preference
|
"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
|
"GEMINI_MODEL", // override Gemini model (e.g. gemini-3-pro-preview) for tests or user preference
|
||||||
"LOG_LEVEL",
|
"LOG_LEVEL",
|
||||||
"DEBUG",
|
"DEBUG",
|
||||||
|
|||||||
@@ -388,13 +388,13 @@ When all delegations are complete, call \`${ghPullfrogMcpName}/set_output\` with
|
|||||||
|
|
||||||
### Subagent capabilities
|
### 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
|
### Prompt-crafting rules
|
||||||
|
|
||||||
- Subagents have NO context beyond what you write. No repo instructions, no event data, no user prompt.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- You do NOT need to instruct subagents to call \`set_output\` — the system preamble handles this.
|
||||||
|
|||||||
Reference in New Issue
Block a user