Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c99bcbbac | |||
| 6c9747585f | |||
| f87073fcef | |||
| ed91fbb18d | |||
| 8bac460177 | |||
| 5684cbef77 | |||
| fafe930c77 | |||
| 808849fcc8 | |||
| 734e8197db | |||
| a0af59b52a |
@@ -1,5 +1,5 @@
|
||||
<!-- test preview system --> <!-- test bypass 2 --> <!-- trigger preview repo creation -->
|
||||
<p align="center">
|
||||
<!-- test preview system -->
|
||||
<p align="center">
|
||||
<h1 align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://pullfrog.com/frog-white-200px.png">
|
||||
@@ -24,27 +24,26 @@ Pullfrog is a GitHub bot that brings the full power of your favorite coding agen
|
||||
|
||||
- **Tag `@pullfrog`** — Tag `@pullfrog` in a comment anywhere in your repo. It will pull in any relevant context using the action's internal MCP server and perform the appropriate task.
|
||||
- **Prompt from the web** — Trigger arbitrary tasks from the Pullfrog dashboard
|
||||
- **Automated triggers** — Configure Pullfrog to trigger agent runs in response to specific events. Each of these triggers can be associated with custom prompt instructions.
|
||||
- **Automated triggers** — Configure Pullfrog to trigger agent runs in response to specific events. Each of these triggers can be associated with custom prompt instructions.
|
||||
- issue created
|
||||
- issue labeled
|
||||
- PR created
|
||||
- PR review created
|
||||
- PR review created
|
||||
- PR review requested
|
||||
- and more...
|
||||
|
||||
Pullfrog is the bridge between your preferred coding agents and GitHub. Use it for:
|
||||
|
||||
|
||||
- **🤖 Coding tasks** — Tell `@pullfrog` to implement something and it'll spin up a PR. If CI fails, it'll read the logs and attempt a fix automatically. It'll automatically address any PR reviews too.
|
||||
- **🔍 PR review** — Coding agents are great at reviewing PRs. Using the "PR created" trigger, you can configure Pullfrog to auto-review new PRs.
|
||||
- **🤙 Issue management** — Via the "issue created" trigger, Pullfrog can automatically respond to common questions, create implementation plans, and link to related issues/PRs. Or (if you're feeling lucky) you can prompt it to immediately attempt a PR addressing new issues.
|
||||
- **Literally whatever** — Want to have the agent automatically add docs to all new PRs? Cut a new release with agent-written notes on every commit to `main`? Pullfrog lets you do it.
|
||||
|
||||
|
||||
<!-- Features
|
||||
- **Agent-agnostic** — Switch between agents with the click of a radio button.
|
||||
- ** -->
|
||||
|
||||
<!--
|
||||
<!--
|
||||
## Get started
|
||||
|
||||
Install the Pullfrog GitHub App on your personal or organization account. During installation you can choose to limit access to a specific repo or repos. After installation, you'll be redirected to the Pullfrog dashboard where you'll see an onboarding flow. This flow will create your `pullfrog.yml` workflow and prompt you to set up API keys. Once you finish those steps (2 minutes) you're ready to rock.
|
||||
@@ -124,14 +123,14 @@ on:
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
# add other triggers as needed
|
||||
|
||||
|
||||
|
||||
jobs:
|
||||
pullfrog:
|
||||
|
||||
|
||||
# trigger conditions (e.g. only run if @pullfrog is mentioned)
|
||||
if: contains(github.event.comment.body, '@pullfrog') || contains(github.event.issue.body, '@pullfrog')
|
||||
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
@@ -148,3 +147,104 @@ jobs:
|
||||
|
||||
</details>
|
||||
-->
|
||||
|
||||
## Standalone Usage
|
||||
|
||||
You can also use `pullfrog/pullfrog` as a step in your own workflows. The action exposes a `result` output that can be consumed by subsequent steps.
|
||||
|
||||
### Example: Auto-generate release notes on new tags
|
||||
|
||||
```yaml
|
||||
name: Release
|
||||
on:
|
||||
push:
|
||||
tags: ['v*']
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Generate release notes
|
||||
id: notes
|
||||
uses: pullfrog/pullfrog@v0
|
||||
with:
|
||||
prompt: |
|
||||
Generate release notes for ${{ github.ref_name }}.
|
||||
Compare commits between this tag and the previous tag.
|
||||
Format as markdown: summary paragraph, then ### Features, ### Fixes, ### Breaking Changes sections.
|
||||
Omit empty sections. Be concise.
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
|
||||
# write to file to avoid shell escaping issues with special characters
|
||||
- name: Create GitHub release
|
||||
run: |
|
||||
notesfile="$RUNNER_TEMP/release-notes-$GITHUB_RUN_ID.md"
|
||||
printf '%s' "$NOTES" > "$notesfile"
|
||||
gh release create ${{ github.ref_name }} --title "${{ github.ref_name }}" --notes-file "$notesfile"
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
NOTES: ${{ steps.notes.outputs.result }}
|
||||
```
|
||||
|
||||
### Example: Structured Output with Zod Schema
|
||||
|
||||
You can force the agent to return structured JSON output by providing a JSON schema. This allows you to reliably parse and use the agent's response in subsequent workflow steps.
|
||||
|
||||
You can define your JSON schema directly or uou can use any validation library that converts to JSON Schema. Here's an example using [Zod](https://zod.dev):
|
||||
|
||||
```yaml
|
||||
name: Release Check
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
|
||||
jobs:
|
||||
check-release:
|
||||
if: github.event.pull_request.merged == true
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install --no-save --no-package-lock zod @actions/core
|
||||
|
||||
- name: Generate Schema
|
||||
id: schema
|
||||
run: |
|
||||
node -e '
|
||||
import { z } from "zod";
|
||||
import { setOutput } from "@actions/core";
|
||||
const schema = z.object({
|
||||
version: z.string().describe("Semantic version number (e.g. 1.0.0)"),
|
||||
isBreaking: z.boolean().describe("Whether this release contains breaking changes"),
|
||||
changelog: z.array(z.string()).describe("List of changes in this release"),
|
||||
});
|
||||
setOutput("schema", JSON.stringify(z.toJSONSchema(schema)));
|
||||
'
|
||||
|
||||
- name: Analyze PR
|
||||
id: analysis
|
||||
uses: pullfrog/pullfrog@v0
|
||||
with:
|
||||
prompt: |
|
||||
Analyze this PR and determine semantic versioning impact.
|
||||
Return a JSON object matching the provided schema.
|
||||
output_schema: ${{ steps.schema.outputs.schema }}
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
|
||||
- name: Process Result
|
||||
run: |
|
||||
# Parse the JSON result using fromJSON()
|
||||
echo "Version: ${{ fromJSON(steps.analysis.outputs.result).version }}"
|
||||
echo "Breaking: ${{ fromJSON(steps.analysis.outputs.result).isBreaking }}"
|
||||
```
|
||||
|
||||
+4
-1
@@ -30,6 +30,9 @@ inputs:
|
||||
shell:
|
||||
description: "Shell permission: disabled, restricted (filters secrets from env vars), or enabled. Public repos default to restricted for security; private repos default to enabled."
|
||||
required: false
|
||||
output_schema:
|
||||
description: "JSON Schema (draft-07) for structured output validation. When provided, the action output becomes required and must conform to this schema."
|
||||
required: false
|
||||
token:
|
||||
description: "GitHub-provided token with job-scoped permissions. Do not set this unless you know what you are doing."
|
||||
required: false
|
||||
@@ -37,7 +40,7 @@ inputs:
|
||||
|
||||
outputs:
|
||||
result:
|
||||
description: "It's set when the prompt explicitly requests it. It can be used to capture an actionable output for the next step in the workflow."
|
||||
description: "It's set when the prompt explicitly requests it and is required when output_schema is provided; use it to capture actionable output for the next workflow step."
|
||||
|
||||
runs:
|
||||
using: "node24"
|
||||
|
||||
@@ -20,10 +20,6 @@ async function run(): Promise<void> {
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || "Agent execution failed");
|
||||
}
|
||||
|
||||
if (result.result) {
|
||||
core.setOutput("result", result.result);
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
||||
core.setFailed(`Action failed: ${errorMessage}`);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
// changes to tool permissions should be reflected in wiki/granular-tools.md
|
||||
|
||||
import * as core from "@actions/core";
|
||||
import { initToolState, startMcpHttpServer, type ToolState } from "./mcp/server.ts";
|
||||
import { computeModes } from "./modes.ts";
|
||||
import {
|
||||
@@ -37,6 +39,23 @@ export interface MainResult {
|
||||
result?: string | undefined;
|
||||
}
|
||||
|
||||
function resolveOutputSchema(): Record<string, unknown> | undefined {
|
||||
const raw = core.getInput("output_schema");
|
||||
if (!raw) return undefined;
|
||||
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(raw);
|
||||
} catch {
|
||||
throw new Error(`invalid output_schema: not valid JSON`);
|
||||
}
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
throw new Error(`invalid output_schema: must be a JSON object`);
|
||||
}
|
||||
log.info("» structured output schema provided — output will be required");
|
||||
return parsed as Record<string, unknown>;
|
||||
}
|
||||
|
||||
async function writeJobSummary(toolState: ToolState): Promise<void> {
|
||||
const usageSummary = formatUsageSummary(toolState.usageEntries);
|
||||
const summaryParts = [toolState.lastProgressBody, usageSummary].filter(Boolean);
|
||||
@@ -151,6 +170,8 @@ export async function main(): Promise<MainResult> {
|
||||
|
||||
const modes = [...computeModes(), ...runContext.repoSettings.modes];
|
||||
|
||||
const outputSchema = resolveOutputSchema();
|
||||
|
||||
// mcpServerUrl and tmpdir are set after server starts — delegate tool reads them at call time
|
||||
const toolContext = {
|
||||
repo: runContext.repo,
|
||||
@@ -169,7 +190,7 @@ export async function main(): Promise<MainResult> {
|
||||
mcpServerUrl: "",
|
||||
tmpdir,
|
||||
};
|
||||
await using mcpHttpServer = await startMcpHttpServer(toolContext);
|
||||
await using mcpHttpServer = await startMcpHttpServer(toolContext, { outputSchema });
|
||||
toolContext.mcpServerUrl = mcpHttpServer.url;
|
||||
log.info(`» MCP server started at ${mcpHttpServer.url}`);
|
||||
timer.checkpoint("mcpServer");
|
||||
@@ -178,6 +199,7 @@ export async function main(): Promise<MainResult> {
|
||||
payload,
|
||||
repo: runContext.repo,
|
||||
modes,
|
||||
outputSchema,
|
||||
});
|
||||
// log instructions as soon as they are fully resolved
|
||||
const logParts = [
|
||||
@@ -236,23 +258,24 @@ export async function main(): Promise<MainResult> {
|
||||
toolState.usageEntries.push(result.usage);
|
||||
}
|
||||
|
||||
await writeJobSummary(toolState);
|
||||
|
||||
// emit structured output marker for test validation
|
||||
if (toolState.output) {
|
||||
log.info(`::pullfrog-output::${Buffer.from(toolState.output).toString("base64")}`);
|
||||
// validate this before writing job summary to avoid masking the error
|
||||
if (outputSchema && !toolState.output) {
|
||||
throw new Error(
|
||||
"output_schema was provided but agent did not call set_output — structured output is required"
|
||||
);
|
||||
}
|
||||
|
||||
const mainResult = await handleAgentResult({
|
||||
await writeJobSummary(toolState);
|
||||
|
||||
if (toolState.output) {
|
||||
core.setOutput("result", toolState.output);
|
||||
}
|
||||
|
||||
return await handleAgentResult({
|
||||
result,
|
||||
toolState,
|
||||
silent: payload.event.silent ?? false,
|
||||
});
|
||||
|
||||
return {
|
||||
...mainResult,
|
||||
result: toolState.output,
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "unknown error occurred";
|
||||
killTrackedChildren();
|
||||
|
||||
+27
-2
@@ -206,6 +206,31 @@ export async function checkoutPrBranch(
|
||||
// this avoids naming conflicts and makes push config simpler
|
||||
const localBranch = `pr-${pullNumber}`;
|
||||
|
||||
// compute deepen depth for shallow clones. actions/checkout uses depth=1
|
||||
// by default, which breaks rebase/log because git can't find the merge base.
|
||||
// use the GitHub compare API to fetch exactly enough history.
|
||||
const isShallow =
|
||||
$("git", ["rev-parse", "--is-shallow-repository"], { log: false }).trim() === "true";
|
||||
let deepenArgs: string[] = [];
|
||||
if (isShallow) {
|
||||
let depth = 1000; // fallback
|
||||
try {
|
||||
const comparison = await octokit.rest.repos.compareCommits({
|
||||
owner,
|
||||
repo: name,
|
||||
base: baseBranch,
|
||||
head: `pull/${pullNumber}/head`,
|
||||
});
|
||||
depth = comparison.data.behind_by + 10;
|
||||
log.debug(
|
||||
`» PR is ${comparison.data.behind_by} commits behind ${baseBranch}, deepening by ${depth}`
|
||||
);
|
||||
} catch {
|
||||
log.debug(`» compare API failed, falling back to --deepen=${depth}`);
|
||||
}
|
||||
deepenArgs = [`--deepen=${depth}`];
|
||||
}
|
||||
|
||||
// check if we're already on the correct commit (not just branch name)
|
||||
// this handles fork PRs where head branch name might match base branch name
|
||||
const currentSha = $("git", ["rev-parse", "HEAD"], { log: false }).trim();
|
||||
@@ -216,7 +241,7 @@ export async function checkoutPrBranch(
|
||||
} else {
|
||||
// fetch base branch so origin/<base> exists for diff operations
|
||||
log.debug(`» fetching base branch (${baseBranch})...`);
|
||||
$git("fetch", ["--no-tags", "origin", baseBranch], {
|
||||
$git("fetch", [...deepenArgs, "--no-tags", "origin", baseBranch], {
|
||||
token: gitToken,
|
||||
restricted: shell !== "enabled",
|
||||
});
|
||||
@@ -241,7 +266,7 @@ export async function checkoutPrBranch(
|
||||
// fetch if we skipped checkout (already on branch) - otherwise already fetched above
|
||||
if (alreadyOnBranch) {
|
||||
log.debug(`» fetching base branch (${baseBranch})...`);
|
||||
$git("fetch", ["--no-tags", "origin", baseBranch], {
|
||||
$git("fetch", [...deepenArgs, "--no-tags", "origin", baseBranch], {
|
||||
token: gitToken,
|
||||
restricted: shell !== "enabled",
|
||||
});
|
||||
|
||||
+105
-7
@@ -1,11 +1,43 @@
|
||||
import { type } from "arktype";
|
||||
import type { Agent } from "../agents/index.ts";
|
||||
import { apiFetch } from "../utils/apiFetch.ts";
|
||||
import { getApiUrl } from "../utils/apiUrl.ts";
|
||||
import { buildPullfrogFooter, stripExistingFooter } from "../utils/buildPullfrogFooter.ts";
|
||||
import { log } from "../utils/cli.ts";
|
||||
import { type OctokitWithPlugins, parseRepoContext } from "../utils/github.ts";
|
||||
import { retry } from "../utils/retry.ts";
|
||||
import type { ToolContext } from "./server.ts";
|
||||
import { execute, tool } from "./shared.ts";
|
||||
|
||||
/** PATCH workflow-run with plan comment node_id so plan revisions can update that comment in place. */
|
||||
async function updatePlanCommentId(ctx: ToolContext, planCommentNodeId: string): Promise<void> {
|
||||
if (ctx.runId === undefined || !ctx.apiToken) return;
|
||||
try {
|
||||
await retry(
|
||||
async () => {
|
||||
const response = await apiFetch({
|
||||
path: `/api/workflow-run/${ctx.runId}`,
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
authorization: `Bearer ${ctx.apiToken}`,
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ planCommentNodeId }),
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
});
|
||||
if (!response.ok) throw new Error(`PATCH workflow-run: ${response.status}`);
|
||||
},
|
||||
{
|
||||
maxAttempts: 3,
|
||||
delayMs: 2000,
|
||||
label: "updatePlanCommentId",
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
log.warning(`updatePlanCommentId exhausted retries: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The prefix text for the initial "leaping into action" comment.
|
||||
* This is used to identify if a comment is still in its initial state
|
||||
@@ -86,15 +118,21 @@ export async function addFooter(ctx: AddFooterCtx, body: string): Promise<string
|
||||
export const Comment = type({
|
||||
issueNumber: type.number.describe("the issue number to comment on"),
|
||||
body: type.string.describe("the comment body content"),
|
||||
type: type
|
||||
.enumerated("Plan", "Comment")
|
||||
.describe(
|
||||
"Plan: record this comment as the plan for this run (use report_progress for progress/plan updates on the current run). Comment: regular comment (default)."
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export function CreateCommentTool(ctx: ToolContext) {
|
||||
return tool({
|
||||
name: "create_issue_comment",
|
||||
description:
|
||||
"Create a comment on a GitHub issue. NOTE: Do NOT use this for progress updates or status summaries - use report_progress instead, which updates the existing progress comment.",
|
||||
"Create a comment on a GitHub issue. For progress/plan updates on the current run use report_progress instead. Use type: 'Plan' only when creating a standalone plan comment to record as this run's plan.",
|
||||
parameters: Comment,
|
||||
execute: execute(async ({ issueNumber, body }) => {
|
||||
execute: execute(async ({ issueNumber, body, type: commentType }) => {
|
||||
const bodyWithFooter = await addFooter(ctx, body);
|
||||
|
||||
const result = await ctx.octokit.rest.issues.createComment({
|
||||
@@ -104,6 +142,10 @@ export function CreateCommentTool(ctx: ToolContext) {
|
||||
body: bodyWithFooter,
|
||||
});
|
||||
|
||||
if (commentType === "Plan" && result.data.node_id) {
|
||||
await updatePlanCommentId(ctx, result.data.node_id);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
commentId: result.data.id,
|
||||
@@ -147,6 +189,9 @@ export function EditCommentTool(ctx: ToolContext) {
|
||||
|
||||
export const ReportProgress = type({
|
||||
body: type.string.describe("the progress update content to share"),
|
||||
"target_plan_comment?": type("boolean").describe(
|
||||
"when true, update the existing plan comment (from select_mode lookup) instead of the progress comment; use when editing an existing plan"
|
||||
),
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -161,13 +206,14 @@ export const ReportProgress = type({
|
||||
*/
|
||||
export async function reportProgress(
|
||||
ctx: ToolContext,
|
||||
{ body }: { body: string }
|
||||
params: { body: string; target_plan_comment?: boolean }
|
||||
): Promise<{
|
||||
commentId?: number;
|
||||
url?: string;
|
||||
body: string;
|
||||
action: "created" | "updated" | "skipped";
|
||||
}> {
|
||||
const { body, target_plan_comment } = params;
|
||||
// always track the body for job summary
|
||||
ctx.toolState.lastProgressBody = body;
|
||||
|
||||
@@ -177,10 +223,50 @@ export async function reportProgress(
|
||||
return { body, action: "skipped" };
|
||||
}
|
||||
|
||||
const existingCommentId = ctx.toolState.progressCommentId;
|
||||
const issueNumber = ctx.toolState.issueNumber ?? ctx.payload.event.issue_number;
|
||||
const issueNumber = ctx.payload.event.issue_number ?? ctx.toolState.issueNumber;
|
||||
const isPlanMode = ctx.toolState.selectedMode === "Plan";
|
||||
|
||||
// when editing existing plan: update the plan comment from tool state (set by select_mode)
|
||||
if (target_plan_comment === true && ctx.toolState.existingPlanCommentId === undefined) {
|
||||
log.warning("target_plan_comment requested but no existingPlanCommentId in tool state");
|
||||
}
|
||||
if (target_plan_comment === true && ctx.toolState.existingPlanCommentId !== undefined) {
|
||||
const commentId = ctx.toolState.existingPlanCommentId;
|
||||
const customParts =
|
||||
isPlanMode && issueNumber !== undefined
|
||||
? [buildImplementPlanLink(ctx.repo.owner, ctx.repo.name, issueNumber, commentId)]
|
||||
: undefined;
|
||||
const bodyWithoutFooter = stripExistingFooter(body);
|
||||
const footer = await buildCommentFooter({
|
||||
agent: ctx.agent,
|
||||
octokit: ctx.octokit,
|
||||
customParts,
|
||||
});
|
||||
const bodyWithFooter = `${bodyWithoutFooter}${footer}`;
|
||||
|
||||
const result = await ctx.octokit.rest.issues.updateComment({
|
||||
owner: ctx.repo.owner,
|
||||
repo: ctx.repo.name,
|
||||
comment_id: commentId,
|
||||
body: bodyWithFooter,
|
||||
});
|
||||
|
||||
ctx.toolState.wasUpdated = true;
|
||||
|
||||
if (isPlanMode && result.data.node_id) {
|
||||
await updatePlanCommentId(ctx, result.data.node_id);
|
||||
}
|
||||
|
||||
return {
|
||||
commentId: result.data.id,
|
||||
url: result.data.html_url,
|
||||
body: result.data.body || "",
|
||||
action: "updated",
|
||||
};
|
||||
}
|
||||
|
||||
const existingCommentId = ctx.toolState.progressCommentId;
|
||||
|
||||
// if we already have a progress comment, update it
|
||||
if (existingCommentId) {
|
||||
const customParts =
|
||||
@@ -205,6 +291,10 @@ export async function reportProgress(
|
||||
|
||||
ctx.toolState.wasUpdated = true;
|
||||
|
||||
if (isPlanMode && result.data.node_id) {
|
||||
await updatePlanCommentId(ctx, result.data.node_id);
|
||||
}
|
||||
|
||||
return {
|
||||
commentId: result.data.id,
|
||||
url: result.data.html_url,
|
||||
@@ -260,6 +350,10 @@ export async function reportProgress(
|
||||
body: bodyWithPlanLink,
|
||||
});
|
||||
|
||||
if (updateResult.data.node_id) {
|
||||
await updatePlanCommentId(ctx, updateResult.data.node_id);
|
||||
}
|
||||
|
||||
return {
|
||||
commentId: updateResult.data.id,
|
||||
url: updateResult.data.html_url,
|
||||
@@ -282,8 +376,12 @@ export function ReportProgressTool(ctx: ToolContext) {
|
||||
description:
|
||||
"Share progress on the associated GitHub issue/PR. Call this to post updates as you work. The first call creates a comment, subsequent calls update it. Use this throughout your work to keep stakeholders informed.",
|
||||
parameters: ReportProgress,
|
||||
execute: execute(async ({ body }) => {
|
||||
const result = await reportProgress(ctx, { body });
|
||||
execute: execute(async (params) => {
|
||||
const reportParams: { body: string; target_plan_comment?: boolean } = { body: params.body };
|
||||
if (params.target_plan_comment !== undefined) {
|
||||
reportParams.target_plan_comment = params.target_plan_comment;
|
||||
}
|
||||
const result = await reportProgress(ctx, reportParams);
|
||||
|
||||
if (result.action === "skipped") {
|
||||
// no-op: no comment target, but progress is still tracked for job summary
|
||||
|
||||
@@ -108,6 +108,15 @@ export function PushBranchTool(ctx: ToolContext) {
|
||||
|
||||
const branch = branchName || $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false });
|
||||
|
||||
// reject push if working tree is dirty — forces agent to commit or discard before pushing
|
||||
const status = $("git", ["status", "--porcelain"], { log: false });
|
||||
if (status) {
|
||||
throw new Error(
|
||||
`push blocked: working tree has uncommitted changes. commit or discard them before pushing.\n\n` +
|
||||
`git status:\n${status}`
|
||||
);
|
||||
}
|
||||
|
||||
// validate push destination matches expected URL
|
||||
const pushUrl = ctx.toolState.pushUrl;
|
||||
if (!pushUrl) {
|
||||
|
||||
+66
-18
@@ -1,3 +1,5 @@
|
||||
import type { StandardJSONSchemaV1, StandardSchemaV1 } from "@standard-schema/spec";
|
||||
import { Ajv } from "ajv";
|
||||
import { type } from "arktype";
|
||||
import { log } from "../utils/cli.ts";
|
||||
import type { ToolContext } from "./server.ts";
|
||||
@@ -7,29 +9,75 @@ export const SetOutputParams = type({
|
||||
value: type.string.describe("the output value to expose as a GitHub Action output"),
|
||||
});
|
||||
|
||||
export function SetOutputTool(ctx: ToolContext) {
|
||||
type JsonSchema = Record<string, unknown>;
|
||||
|
||||
function jsonSchemaToStandardSchema({
|
||||
$schema: _,
|
||||
...jsonSchema
|
||||
}: JsonSchema): StandardJSONSchemaV1<any> & StandardSchemaV1<any> {
|
||||
const ajv = new Ajv();
|
||||
const validate = ajv.compile(jsonSchema);
|
||||
|
||||
return {
|
||||
"~standard": {
|
||||
version: 1,
|
||||
vendor: "json-schema",
|
||||
jsonSchema: {
|
||||
input: () => jsonSchema,
|
||||
output: () => jsonSchema,
|
||||
},
|
||||
validate(input: unknown) {
|
||||
if (validate(input)) {
|
||||
return { value: input };
|
||||
}
|
||||
return {
|
||||
issues: (validate.errors ?? []).map((err) => ({
|
||||
message: `${err.instancePath || "/"}: ${err.message ?? "validation error"}`,
|
||||
path: err.instancePath ? err.instancePath.split("/").filter(Boolean) : [],
|
||||
})),
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function storeOutput(ctx: ToolContext, value: string) {
|
||||
const selfId = ctx.toolState.selfSubagentId;
|
||||
if (selfId) {
|
||||
const subagent = ctx.toolState.subagents.get(selfId);
|
||||
if (subagent) {
|
||||
subagent.output = value;
|
||||
log.debug(`set_output: routed to subagent ${selfId} (value=${value.slice(0, 80)})`);
|
||||
return { success: true, routed: "subagent" as const };
|
||||
}
|
||||
log.warning(
|
||||
`set_output: selfSubagentId=${selfId} but subagent not found in map — routing to action output`
|
||||
);
|
||||
}
|
||||
ctx.toolState.output = value;
|
||||
return { success: true, routed: "action_output" as const };
|
||||
}
|
||||
|
||||
export function SetOutputTool(ctx: ToolContext, outputSchema?: JsonSchema) {
|
||||
if (outputSchema) {
|
||||
return tool({
|
||||
name: "set_output",
|
||||
description:
|
||||
"Set the structured action output. You MUST call this tool before finishing — the output is required. Pass the output object directly as the tool arguments (no wrapping needed).",
|
||||
parameters: jsonSchemaToStandardSchema(outputSchema),
|
||||
execute: execute(async (params) => {
|
||||
return storeOutput(ctx, JSON.stringify(params));
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
return tool({
|
||||
name: "set_output",
|
||||
description:
|
||||
"Set the action output. When called by a subagent, returns a summary result to the orchestrator. When called in standalone mode, exposes the value as the 'result' GitHub Action output.",
|
||||
"Set the action output. When called by a subagent, returns a summary result to the orchestrator — this is the ONLY way to pass results back. When called by the orchestrator in standalone mode (trigger: unknown), exposes the value as the 'result' GitHub Action output for downstream workflow steps. Do NOT use this for progress reporting — use report_progress instead.",
|
||||
parameters: SetOutputParams,
|
||||
execute: execute(async (params) => {
|
||||
const selfId = ctx.toolState.selfSubagentId;
|
||||
if (selfId) {
|
||||
const subagent = ctx.toolState.subagents.get(selfId);
|
||||
if (subagent) {
|
||||
subagent.output = params.value;
|
||||
log.debug(
|
||||
`set_output: routed to subagent ${selfId} (value=${params.value.slice(0, 80)})`
|
||||
);
|
||||
return { success: true, routed: "subagent" };
|
||||
}
|
||||
log.warning(
|
||||
`set_output: selfSubagentId=${selfId} but subagent not found in map — routing to action output`
|
||||
);
|
||||
}
|
||||
ctx.toolState.output = params.value;
|
||||
return { success: true, routed: "action_output" };
|
||||
return storeOutput(ctx, params.value);
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
+106
-9
@@ -1,12 +1,16 @@
|
||||
import { type } from "arktype";
|
||||
import { ghPullfrogMcpName } from "../external.ts";
|
||||
import type { Mode } from "../modes.ts";
|
||||
import { apiFetch } from "../utils/apiFetch.ts";
|
||||
import type { ToolContext } from "./server.ts";
|
||||
import { execute, tool } from "./shared.ts";
|
||||
|
||||
export const SelectModeParams = type({
|
||||
mode: type.string.describe(
|
||||
"the name of the mode to select (e.g., 'Build', 'Plan', 'Review', 'IncrementalReview', 'Fix', 'AddressReviews', 'Task')"
|
||||
"the name of the mode to select (e.g., 'Build', 'Plan', 'Review', 'IncrementalReview', 'Fix', 'AddressReviews', 'Task', 'ResolveConflicts')"
|
||||
),
|
||||
"issue_number?": type("number").describe(
|
||||
"optional issue number; when provided with Plan mode, used to look up an existing plan comment for this issue (edit vs create)"
|
||||
),
|
||||
});
|
||||
|
||||
@@ -46,6 +50,37 @@ For simple, well-defined tasks, a single build subagent is sufficient — skip t
|
||||
|
||||
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.`,
|
||||
|
||||
ResolveConflicts: `### Checklist
|
||||
|
||||
1. **Setup**:
|
||||
- Call \${ghPullfrogMcpName}/checkout_pr to get the PR branch.
|
||||
- Call \${ghPullfrogMcpName}/get_pull_request to identify the base branch (e.g., 'main').
|
||||
- Call \${ghPullfrogMcpName}/git_fetch to fetch the base branch.
|
||||
|
||||
2. **Merge Attempt**:
|
||||
- Run \`git merge origin/<base_branch>\` via shell.
|
||||
- If it succeeds automatically: Great! Push via \${ghPullfrogMcpName}/push_branch and report success.
|
||||
- If it fails (conflicts): You must resolve them.
|
||||
|
||||
3. **Delegation (if conflicts exist)**:
|
||||
- Run \`git status\` or parse the merge output to find the list of conflicting files.
|
||||
- Delegate to a subagent (or multiple in parallel if many files) to resolve the conflicts.
|
||||
- **Instructions for subagent**:
|
||||
- "You are resolving merge conflicts in these files: [list]."
|
||||
- "For each file: read it, find the conflict markers (<<<<<<<, =======, >>>>>>>), understand the code context, and rewrite the file with the correct resolution. Remove all markers."
|
||||
- "After resolving, verify the file syntax is correct."
|
||||
- "Call \${ghPullfrogMcpName}/set_output with a summary of what you resolved."
|
||||
- Note: Subagents cannot run git commands. They only edit the files.
|
||||
|
||||
4. **Finalize**:
|
||||
- After subagents return:
|
||||
- Run a final verification (build/test) to ensure the resolution works.
|
||||
- \`git add .\`
|
||||
- \`git commit -m "Resolve merge conflicts"\`
|
||||
- \${ghPullfrogMcpName}/push_branch
|
||||
- \${ghPullfrogMcpName}/report_progress
|
||||
`,
|
||||
|
||||
AddressReviews: `### Checklist
|
||||
|
||||
1. Before delegating, checkout the PR branch yourself via \`${ghPullfrogMcpName}/checkout_pr\` — subagents have no git/checkout tools.
|
||||
@@ -130,12 +165,28 @@ Use max effort for thorough reviews.`,
|
||||
- the task to plan for
|
||||
- relevant codebase context (file paths, architecture notes from AGENTS.md)
|
||||
- instruct it to produce a structured, actionable plan with clear milestones
|
||||
- call \`${ghPullfrogMcpName}/set_output\` with the plan (this is how results get back to you — you'll need the plan to craft the next subagent's prompt)
|
||||
2. After the subagent completes, call \`${ghPullfrogMcpName}/report_progress\` with the plan.
|
||||
- IMPORTANT: instruct it to return the full plan text via \`${ghPullfrogMcpName}/set_output\` as well-structured markdown — do NOT create plan files, do NOT save to disk
|
||||
2. After the subagent completes, call \`${ghPullfrogMcpName}/report_progress\` with the full plan text from the subagent's output. The progress comment must contain the complete plan — not a file path or summary.
|
||||
|
||||
### Effort
|
||||
|
||||
Use mini or auto effort. After receiving the plan, you may delegate a Build subagent to implement it.`,
|
||||
Use mini or auto effort.`,
|
||||
|
||||
PlanEdit: `### Checklist (editing existing plan)
|
||||
|
||||
An existing plan comment was found for this issue. Update that comment with the revised plan — do not create a new plan comment.
|
||||
|
||||
1. Use \`previousPlanBody\` from this response as the plan to revise; do not call \`get_issue\` or \`get_issue_comments\`.
|
||||
2. When delegating, the subagent prompt must contain:
|
||||
- the current plan (\`previousPlanBody\`) and the user's revision request
|
||||
- relevant codebase context (file paths, architecture notes from AGENTS.md)
|
||||
- instructions to produce a structured plan with clear milestones and to return the full plan via \`${ghPullfrogMcpName}/set_output\` as markdown (do not create plan files or save to disk)
|
||||
3. After the subagent completes, call \`${ghPullfrogMcpName}/report_progress\` with the full plan text and \`{ target_plan_comment: true }\` so the revised plan updates the existing plan comment (not the progress comment).
|
||||
4. Then post a short note to the progress comment (e.g. "Plan has been updated in the comment above.") via \`${ghPullfrogMcpName}/report_progress\` so it is not left as "Leaping...".
|
||||
|
||||
### Effort
|
||||
|
||||
Use mini or auto effort.`,
|
||||
|
||||
Fix: `### Checklist
|
||||
|
||||
@@ -183,8 +234,8 @@ type OrchestratorGuidance = {
|
||||
orchestratorGuidance: string;
|
||||
};
|
||||
|
||||
function buildOrchestratorGuidance(mode: Mode): OrchestratorGuidance {
|
||||
const guidance = modeGuidance[mode.name] ?? "";
|
||||
function buildOrchestratorGuidance(mode: Mode, overrideGuidance?: string): OrchestratorGuidance {
|
||||
const guidance = overrideGuidance ?? modeGuidance[mode.name] ?? "";
|
||||
return {
|
||||
modeName: mode.name,
|
||||
description: mode.description,
|
||||
@@ -192,19 +243,49 @@ function buildOrchestratorGuidance(mode: Mode): OrchestratorGuidance {
|
||||
};
|
||||
}
|
||||
|
||||
// matches the API response for /repo/[owner]/[repo]/issue/[issueNumber]/plan-comment
|
||||
export type PlanCommentResponsePayload = { error: string } | { commentId: number; body: string };
|
||||
|
||||
async function fetchExistingPlanComment(
|
||||
ctx: ToolContext,
|
||||
issueNumber: number
|
||||
): Promise<Extract<PlanCommentResponsePayload, { commentId: number }> | null> {
|
||||
if (!ctx.apiToken) return null;
|
||||
try {
|
||||
const response = await apiFetch({
|
||||
path: `/api/repo/${ctx.repo.owner}/${ctx.repo.name}/issue/${issueNumber}/plan-comment`,
|
||||
method: "GET",
|
||||
headers: { authorization: `Bearer ${ctx.apiToken}` },
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
});
|
||||
const data = (await response.json()) as PlanCommentResponsePayload;
|
||||
return response.ok && "commentId" in data ? data : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function SelectModeTool(ctx: ToolContext) {
|
||||
return tool({
|
||||
name: "select_mode",
|
||||
description:
|
||||
"Select a mode and receive orchestrator-level guidance on how to handle it, including suggested delegation flows and prompt-crafting tips. Call this before delegating to understand the best approach for the task.",
|
||||
"Select a mode and receive orchestrator-level guidance on how to handle it, including suggested delegation flows and prompt-crafting tips. Call this ONCE before delegating. Mode selection is final — you cannot switch modes after selecting.",
|
||||
parameters: SelectModeParams,
|
||||
execute: execute(async (params) => {
|
||||
const selectedMode = resolveMode(ctx.modes, params.mode);
|
||||
if (ctx.toolState.selectedMode) {
|
||||
return {
|
||||
error: `mode already selected: "${ctx.toolState.selectedMode}". mode selection is final and cannot be changed. complete your current workflow within this mode.`,
|
||||
};
|
||||
}
|
||||
|
||||
const modeName = params.mode;
|
||||
|
||||
const selectedMode = resolveMode(ctx.modes, modeName);
|
||||
|
||||
if (!selectedMode) {
|
||||
const availableModes = ctx.modes.map((m) => m.name).join(", ");
|
||||
return {
|
||||
error: `mode "${params.mode}" not found. available modes: ${availableModes}`,
|
||||
error: `mode "${modeName}" not found. available modes: ${availableModes}`,
|
||||
availableModes: ctx.modes.map((m) => ({
|
||||
name: m.name,
|
||||
description: m.description,
|
||||
@@ -213,6 +294,22 @@ export function SelectModeTool(ctx: ToolContext) {
|
||||
}
|
||||
|
||||
ctx.toolState.selectedMode = selectedMode.name;
|
||||
|
||||
if (selectedMode.name === "Plan") {
|
||||
const issueNumber = params.issue_number ?? ctx.payload.event.issue_number;
|
||||
if (issueNumber !== undefined) {
|
||||
const existing = await fetchExistingPlanComment(ctx, issueNumber);
|
||||
if (existing !== null) {
|
||||
ctx.toolState.existingPlanCommentId = existing.commentId;
|
||||
ctx.toolState.previousPlanBody = existing.body;
|
||||
return {
|
||||
...buildOrchestratorGuidance(selectedMode, modeGuidance.PlanEdit),
|
||||
previousPlanBody: existing.body,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return buildOrchestratorGuidance(selectedMode);
|
||||
}),
|
||||
});
|
||||
|
||||
+16
-6
@@ -64,6 +64,9 @@ export interface ToolState {
|
||||
progressCommentId: number | null | undefined;
|
||||
lastProgressBody?: string;
|
||||
wasUpdated?: boolean;
|
||||
// set by select_mode when Plan + issue_number and plan-comment API returns existing plan (for report_progress target_plan_comment)
|
||||
existingPlanCommentId?: number;
|
||||
previousPlanBody?: string;
|
||||
output?: string;
|
||||
usageEntries: AgentUsage[];
|
||||
}
|
||||
@@ -189,8 +192,10 @@ function isAddressInUse(error: unknown): boolean {
|
||||
return message.includes("eaddrinuse") || message.includes("address already in use");
|
||||
}
|
||||
|
||||
type JsonSchema = Record<string, unknown>;
|
||||
|
||||
// tools shared by both orchestrator and subagent servers
|
||||
function buildCommonTools(ctx: ToolContext): Tool<any, any>[] {
|
||||
function buildCommonTools(ctx: ToolContext, outputSchema?: JsonSchema): Tool<any, any>[] {
|
||||
const tools: Tool<any, any>[] = [
|
||||
StartDependencyInstallationTool(ctx),
|
||||
AwaitDependencyInstallationTool(ctx),
|
||||
@@ -213,7 +218,7 @@ function buildCommonTools(ctx: ToolContext): Tool<any, any>[] {
|
||||
GitTool(ctx),
|
||||
GitFetchTool(ctx),
|
||||
UploadFileTool(ctx),
|
||||
SetOutputTool(ctx),
|
||||
SetOutputTool(ctx, outputSchema),
|
||||
FileReadTool(ctx),
|
||||
FileWriteTool(ctx),
|
||||
FileEditTool(ctx),
|
||||
@@ -234,9 +239,9 @@ function buildCommonTools(ctx: ToolContext): Tool<any, any>[] {
|
||||
}
|
||||
|
||||
// orchestrator gets common tools + delegation + remote-mutating tools
|
||||
function buildOrchestratorTools(ctx: ToolContext): Tool<any, any>[] {
|
||||
function buildOrchestratorTools(ctx: ToolContext, outputSchema?: JsonSchema): Tool<any, any>[] {
|
||||
return [
|
||||
...buildCommonTools(ctx),
|
||||
...buildCommonTools(ctx, outputSchema),
|
||||
ReportProgressTool(ctx),
|
||||
SelectModeTool(ctx),
|
||||
DelegateTool(ctx),
|
||||
@@ -353,13 +358,18 @@ async function killBackgroundProcesses(toolState: ToolState): Promise<void> {
|
||||
backgroundProcesses.clear();
|
||||
}
|
||||
|
||||
type McpHttpServerOptions = {
|
||||
outputSchema?: JsonSchema | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Start the orchestrator MCP HTTP server (has all tools including push/PR/delegation).
|
||||
*/
|
||||
export async function startMcpHttpServer(
|
||||
ctx: ToolContext
|
||||
ctx: ToolContext,
|
||||
options?: McpHttpServerOptions
|
||||
): Promise<{ url: string; [Symbol.asyncDispose]: () => Promise<void> }> {
|
||||
const tools = buildOrchestratorTools(ctx);
|
||||
const tools = buildOrchestratorTools(ctx, options?.outputSchema);
|
||||
const startResult = await selectMcpPort(ctx, tools);
|
||||
|
||||
return {
|
||||
|
||||
+23
-17
@@ -1,4 +1,4 @@
|
||||
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
||||
import type { StandardJSONSchemaV1, StandardSchemaV1 } from "@standard-schema/spec";
|
||||
import { encode as toonEncode } from "@toon-format/toon";
|
||||
import type { FastMCP, Tool } from "fastmcp";
|
||||
import { formatJsonValue, log } from "../utils/cli.ts";
|
||||
@@ -141,27 +141,34 @@ function sanitizeSchema(schema: any): any {
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a StandardSchemaV1 to intercept toJsonSchema() calls and sanitize the output
|
||||
* Wrap a schema to sanitize its JSON Schema output for Gemini/OpenCode compatibility.
|
||||
* xsschema calls ~standard.jsonSchema.input() for schemas that implement StandardJSONSchemaV1
|
||||
* (i.e. have ~standard.jsonSchema), which includes arktype and our AJV-backed JSON schema wrapper.
|
||||
* Schemas without ~standard.jsonSchema are returned unchanged (sanitization skipped).
|
||||
*/
|
||||
function wrapSchema(schema: StandardSchemaV1<any>): StandardSchemaV1<any> {
|
||||
const originalToJsonSchema = (schema as any).toJsonSchema?.bind(schema);
|
||||
function wrapSchema(
|
||||
schema: StandardSchemaV1<any> & {
|
||||
"~standard": Partial<StandardJSONSchemaV1<any>["~standard"]>;
|
||||
}
|
||||
): StandardSchemaV1<any> {
|
||||
const standardProps = schema["~standard"];
|
||||
|
||||
if (!originalToJsonSchema) {
|
||||
if (!("jsonSchema" in standardProps)) {
|
||||
return schema;
|
||||
}
|
||||
|
||||
// create a proxy that intercepts toJsonSchema calls
|
||||
return new Proxy(schema, {
|
||||
get(target, prop) {
|
||||
if (prop === "toJsonSchema") {
|
||||
return () => {
|
||||
const originalSchema = originalToJsonSchema();
|
||||
return sanitizeSchema(originalSchema);
|
||||
};
|
||||
}
|
||||
return (target as any)[prop];
|
||||
const jsonSchema = standardProps.jsonSchema;
|
||||
const wrapped: StandardSchemaV1<any> & StandardJSONSchemaV1<any> = {
|
||||
...schema,
|
||||
"~standard": {
|
||||
...standardProps,
|
||||
jsonSchema: {
|
||||
input: (options) => sanitizeSchema(jsonSchema.input(options)),
|
||||
output: (options) => sanitizeSchema(jsonSchema.output(options)),
|
||||
},
|
||||
},
|
||||
}) as StandardSchemaV1<any>;
|
||||
};
|
||||
return wrapped;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -172,7 +179,6 @@ function sanitizeTool<T extends Tool<any, any>>(tool: T): T {
|
||||
return tool;
|
||||
}
|
||||
|
||||
// wrap the schema object to intercept toJsonSchema() calls
|
||||
const wrappedSchema = wrapSchema(tool.parameters);
|
||||
|
||||
// create a new tool with wrapped schema
|
||||
|
||||
@@ -250,6 +250,38 @@ ${permalinkTip}`,
|
||||
10. **PROGRESS** - ${reportProgressInstruction}
|
||||
|
||||
Your job is to fix issues THIS PR introduced, not to fix all CI failures. If in doubt about causation, abort and explain rather than making speculative changes.`,
|
||||
},
|
||||
{
|
||||
name: "ResolveConflicts",
|
||||
description: "Resolve merge conflicts in a PR branch against the base branch",
|
||||
prompt: `Follow these steps to resolve merge conflicts.
|
||||
|
||||
1. **CHECKOUT** - Call ${ghPullfrogMcpName}/checkout_pr with the PR number. This fetches the PR branch.
|
||||
|
||||
2. **FETCH BASE** - Identify the base branch (usually main or master) and fetch it using ${ghPullfrogMcpName}/git_fetch (e.g., ref: "main").
|
||||
|
||||
3. **ATTEMPT MERGE** - Use ${ghPullfrogMcpName}/shell to run \`git merge origin/<base_branch>\`.
|
||||
- If the merge succeeds (exit code 0), the branch is up to date. Push it and you're done.
|
||||
- If the merge fails, you have conflicts to resolve.
|
||||
|
||||
4. **IDENTIFY CONFLICTS** - Run \`git status\` to see which files are conflicting (modified by both).
|
||||
|
||||
5. **RESOLVE** - For each conflicting file:
|
||||
- Read the file to see the conflict markers (\`<<<<<<<\`, \`=======\`, \`>>>>>>>\`).
|
||||
- Determine the correct content. You may need to keep changes from both sides, or choose one.
|
||||
- Edit the file to apply the resolution and remove the markers.
|
||||
|
||||
6. **VERIFY** - ${dependencyInstallationStep}
|
||||
- Run tests/builds to ensure the resolution is correct.
|
||||
|
||||
7. **COMMIT** - Once all conflicts are resolved:
|
||||
- \`git add .\`
|
||||
- \`git commit -m "Merge branch <base_branch> into <pr_branch>"\` (or similar).
|
||||
|
||||
8. **PUSH** - Call ${ghPullfrogMcpName}/push_branch.
|
||||
|
||||
9. **PROGRESS** - ${reportProgressInstruction}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "Task",
|
||||
|
||||
+5
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@pullfrog/pullfrog",
|
||||
"version": "0.0.177",
|
||||
"version": "0.0.178",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"index.js",
|
||||
@@ -34,13 +34,14 @@
|
||||
"@octokit/webhooks-types": "^7.6.1",
|
||||
"@openai/codex-sdk": "0.98.0",
|
||||
"@opencode-ai/sdk": "^1.0.143",
|
||||
"@standard-schema/spec": "1.0.0",
|
||||
"@standard-schema/spec": "1.1.0",
|
||||
"@toon-format/toon": "^1.0.0",
|
||||
"ajv": "^8.18.0",
|
||||
"arkregex": "0.0.5",
|
||||
"arktype": "2.1.29",
|
||||
"arktype": "2.2.0",
|
||||
"dotenv": "^17.2.3",
|
||||
"execa": "^9.6.0",
|
||||
"fastmcp": "^3.26.8",
|
||||
"fastmcp": "^3.34.0",
|
||||
"file-type": "^21.3.0",
|
||||
"package-manager-detector": "^1.6.0",
|
||||
"semver": "^7.7.3",
|
||||
|
||||
Generated
+507
-211
File diff suppressed because it is too large
Load Diff
@@ -35679,7 +35679,7 @@ $ark.intrinsic = { ...intrinsic };
|
||||
var regex = ((src, flags) => new RegExp(src, flags));
|
||||
Object.assign(regex, { as: regex });
|
||||
|
||||
// node_modules/.pnpm/arktype@2.1.29/node_modules/arktype/out/parser/shift/operand/date.js
|
||||
// node_modules/.pnpm/arktype@2.2.0/node_modules/arktype/out/parser/shift/operand/date.js
|
||||
var isDateLiteral = (value2) => typeof value2 === "string" && value2[0] === "d" && (value2[1] === "'" || value2[1] === '"') && value2[value2.length - 1] === value2[1];
|
||||
var isValidDate = (d) => d.toString() !== "Invalid Date";
|
||||
var extractDateLiteralSource = (literal) => literal.slice(2, -1);
|
||||
@@ -35698,7 +35698,7 @@ var maybeParseDate = (source, errorOnFail) => {
|
||||
return errorOnFail ? throwParseError(errorOnFail === true ? writeInvalidDateMessage(source) : errorOnFail) : void 0;
|
||||
};
|
||||
|
||||
// node_modules/.pnpm/arktype@2.1.29/node_modules/arktype/out/parser/shift/operand/enclosed.js
|
||||
// node_modules/.pnpm/arktype@2.2.0/node_modules/arktype/out/parser/shift/operand/enclosed.js
|
||||
var regexExecArray = rootSchema({
|
||||
proto: "Array",
|
||||
sequence: "string",
|
||||
@@ -35772,12 +35772,12 @@ var enclosingCharDescriptions = {
|
||||
};
|
||||
var writeUnterminatedEnclosedMessage = (fragment, enclosingStart) => `${enclosingStart}${fragment} requires a closing ${enclosingCharDescriptions[enclosingTokens[enclosingStart]]}`;
|
||||
|
||||
// node_modules/.pnpm/arktype@2.1.29/node_modules/arktype/out/parser/ast/validate.js
|
||||
// node_modules/.pnpm/arktype@2.2.0/node_modules/arktype/out/parser/ast/validate.js
|
||||
var writePrefixedPrivateReferenceMessage = (name) => `Private type references should not include '#'. Use '${name}' instead.`;
|
||||
var shallowOptionalMessage = "Optional definitions like 'string?' are only valid as properties in an object or tuple";
|
||||
var shallowDefaultableMessage = "Defaultable definitions like 'number = 0' are only valid as properties in an object or tuple";
|
||||
|
||||
// node_modules/.pnpm/arktype@2.1.29/node_modules/arktype/out/parser/shift/tokens.js
|
||||
// node_modules/.pnpm/arktype@2.2.0/node_modules/arktype/out/parser/shift/tokens.js
|
||||
var terminatingChars = {
|
||||
"<": 1,
|
||||
">": 1,
|
||||
@@ -35799,7 +35799,7 @@ var lookaheadIsFinalizing = (lookahead, unscanned) => lookahead === ">" ? unscan
|
||||
unscanned[1] === "="
|
||||
) : unscanned.trimStart() === "" || isKeyOf(unscanned.trimStart()[0], terminatingChars) : lookahead === "=" ? unscanned[0] !== "=" : lookahead === "," || lookahead === "?";
|
||||
|
||||
// node_modules/.pnpm/arktype@2.1.29/node_modules/arktype/out/parser/shift/operand/genericArgs.js
|
||||
// node_modules/.pnpm/arktype@2.2.0/node_modules/arktype/out/parser/shift/operand/genericArgs.js
|
||||
var parseGenericArgs = (name, g, s) => _parseGenericArgs(name, g, s, []);
|
||||
var _parseGenericArgs = (name, g, s, argNodes) => {
|
||||
const argState = s.parseUntilFinalizer();
|
||||
@@ -35816,7 +35816,7 @@ var _parseGenericArgs = (name, g, s, argNodes) => {
|
||||
};
|
||||
var writeInvalidGenericArgCountMessage = (name, params, argDefs) => `${name}<${params.join(", ")}> requires exactly ${params.length} args (got ${argDefs.length}${argDefs.length === 0 ? "" : `: ${argDefs.join(", ")}`})`;
|
||||
|
||||
// node_modules/.pnpm/arktype@2.1.29/node_modules/arktype/out/parser/shift/operand/unenclosed.js
|
||||
// node_modules/.pnpm/arktype@2.2.0/node_modules/arktype/out/parser/shift/operand/unenclosed.js
|
||||
var parseUnenclosed = (s) => {
|
||||
const token = s.scanner.shiftUntilLookahead(terminatingChars);
|
||||
if (token === "keyof")
|
||||
@@ -35864,10 +35864,10 @@ var writeMissingOperandMessage = (s) => {
|
||||
var writeMissingRightOperandMessage = (token, unscanned = "") => `Token '${token}' requires a right operand${unscanned ? ` before '${unscanned}'` : ""}`;
|
||||
var writeExpressionExpectedMessage = (unscanned) => `Expected an expression${unscanned ? ` before '${unscanned}'` : ""}`;
|
||||
|
||||
// node_modules/.pnpm/arktype@2.1.29/node_modules/arktype/out/parser/shift/operand/operand.js
|
||||
// node_modules/.pnpm/arktype@2.2.0/node_modules/arktype/out/parser/shift/operand/operand.js
|
||||
var parseOperand = (s) => s.scanner.lookahead === "" ? s.error(writeMissingOperandMessage(s)) : s.scanner.lookahead === "(" ? s.shiftedBy(1).reduceGroupOpen() : s.scanner.lookaheadIsIn(enclosingChar) ? parseEnclosed(s, s.scanner.shift()) : s.scanner.lookaheadIsIn(whitespaceChars) ? parseOperand(s.shiftedBy(1)) : s.scanner.lookahead === "d" ? s.scanner.nextLookahead in enclosingQuote ? parseEnclosed(s, `${s.scanner.shift()}${s.scanner.shift()}`) : parseUnenclosed(s) : s.scanner.lookahead === "x" ? s.scanner.nextLookahead === "/" ? s.shiftedBy(2) && parseEnclosed(s, "x/") : parseUnenclosed(s) : parseUnenclosed(s);
|
||||
|
||||
// node_modules/.pnpm/arktype@2.1.29/node_modules/arktype/out/parser/reduce/shared.js
|
||||
// node_modules/.pnpm/arktype@2.2.0/node_modules/arktype/out/parser/reduce/shared.js
|
||||
var minComparators = {
|
||||
">": true,
|
||||
">=": true
|
||||
@@ -35887,7 +35887,7 @@ var writeOpenRangeMessage = (min, comparator) => `Left bounds are only valid whe
|
||||
var writeUnpairableComparatorMessage = (comparator) => `Left-bounded expressions must specify their limits using < or <= (was ${comparator})`;
|
||||
var writeMultipleLeftBoundsMessage = (openLimit, openComparator, limit, comparator) => `An expression may have at most one left bound (parsed ${openLimit}${invertedComparators[openComparator]}, ${limit}${invertedComparators[comparator]})`;
|
||||
|
||||
// node_modules/.pnpm/arktype@2.1.29/node_modules/arktype/out/parser/shift/operator/bounds.js
|
||||
// node_modules/.pnpm/arktype@2.2.0/node_modules/arktype/out/parser/shift/operator/bounds.js
|
||||
var parseBound = (s, start) => {
|
||||
const comparator = shiftComparator(s, start);
|
||||
if (s.root.hasKind("unit")) {
|
||||
@@ -35958,14 +35958,14 @@ var parseRightBound = (s, comparator) => {
|
||||
};
|
||||
var writeInvalidLimitMessage = (comparator, limit, boundKind) => `Comparator ${boundKind === "left" ? invertedComparators[comparator] : comparator} must be ${boundKind === "left" ? "preceded" : "followed"} by a corresponding literal (was ${limit})`;
|
||||
|
||||
// node_modules/.pnpm/arktype@2.1.29/node_modules/arktype/out/parser/shift/operator/brand.js
|
||||
// node_modules/.pnpm/arktype@2.2.0/node_modules/arktype/out/parser/shift/operator/brand.js
|
||||
var parseBrand = (s) => {
|
||||
s.scanner.shiftUntilNonWhitespace();
|
||||
const brandName = s.scanner.shiftUntilLookahead(terminatingChars);
|
||||
s.root = s.root.brand(brandName);
|
||||
};
|
||||
|
||||
// node_modules/.pnpm/arktype@2.1.29/node_modules/arktype/out/parser/shift/operator/divisor.js
|
||||
// node_modules/.pnpm/arktype@2.2.0/node_modules/arktype/out/parser/shift/operator/divisor.js
|
||||
var parseDivisor = (s) => {
|
||||
s.scanner.shiftUntilNonWhitespace();
|
||||
const divisorToken = s.scanner.shiftUntilLookahead(terminatingChars);
|
||||
@@ -35978,7 +35978,7 @@ var parseDivisor = (s) => {
|
||||
};
|
||||
var writeInvalidDivisorMessage = (divisor) => `% operator must be followed by a non-zero integer literal (was ${divisor})`;
|
||||
|
||||
// node_modules/.pnpm/arktype@2.1.29/node_modules/arktype/out/parser/shift/operator/operator.js
|
||||
// node_modules/.pnpm/arktype@2.2.0/node_modules/arktype/out/parser/shift/operator/operator.js
|
||||
var parseOperator = (s) => {
|
||||
const lookahead = s.scanner.shift();
|
||||
return lookahead === "" ? s.finalize("") : lookahead === "[" ? s.scanner.shift() === "]" ? s.setRoot(s.root.array()) : s.error(incompleteArrayTokenMessage) : lookahead === "|" ? s.scanner.lookahead === ">" ? s.shiftedBy(1).pushRootToBranch("|>") : s.pushRootToBranch(lookahead) : lookahead === "&" ? s.pushRootToBranch(lookahead) : lookahead === ")" ? s.finalizeGroup() : lookaheadIsFinalizing(lookahead, s.scanner.unscanned) ? s.finalize(lookahead) : isKeyOf(lookahead, comparatorStartChars) ? parseBound(s, lookahead) : lookahead === "%" ? parseDivisor(s) : lookahead === "#" ? parseBrand(s) : lookahead in whitespaceChars ? parseOperator(s) : s.error(writeUnexpectedCharacterMessage(lookahead));
|
||||
@@ -35986,7 +35986,7 @@ var parseOperator = (s) => {
|
||||
var writeUnexpectedCharacterMessage = (char, shouldBe = "") => `'${char}' is not allowed here${shouldBe && ` (should be ${shouldBe})`}`;
|
||||
var incompleteArrayTokenMessage = `Missing expected ']'`;
|
||||
|
||||
// node_modules/.pnpm/arktype@2.1.29/node_modules/arktype/out/parser/shift/operator/default.js
|
||||
// node_modules/.pnpm/arktype@2.2.0/node_modules/arktype/out/parser/shift/operator/default.js
|
||||
var parseDefault = (s) => {
|
||||
const baseNode = s.unsetRoot();
|
||||
s.parseOperand();
|
||||
@@ -35998,7 +35998,7 @@ var parseDefault = (s) => {
|
||||
};
|
||||
var writeNonLiteralDefaultMessage = (defaultDef) => `Default value '${defaultDef}' must be a literal value`;
|
||||
|
||||
// node_modules/.pnpm/arktype@2.1.29/node_modules/arktype/out/parser/string.js
|
||||
// node_modules/.pnpm/arktype@2.2.0/node_modules/arktype/out/parser/string.js
|
||||
var parseString = (def, ctx) => {
|
||||
const aliasResolution = ctx.$.maybeResolveRoot(def);
|
||||
if (aliasResolution)
|
||||
@@ -36037,7 +36037,7 @@ var parseUntilFinalizer = (s) => {
|
||||
};
|
||||
var next = (s) => s.hasRoot() ? s.parseOperator() : s.parseOperand();
|
||||
|
||||
// node_modules/.pnpm/arktype@2.1.29/node_modules/arktype/out/parser/reduce/dynamic.js
|
||||
// node_modules/.pnpm/arktype@2.2.0/node_modules/arktype/out/parser/reduce/dynamic.js
|
||||
var RuntimeState = class _RuntimeState {
|
||||
root;
|
||||
branches = {
|
||||
@@ -36174,7 +36174,7 @@ var RuntimeState = class _RuntimeState {
|
||||
}
|
||||
};
|
||||
|
||||
// node_modules/.pnpm/arktype@2.1.29/node_modules/arktype/out/generic.js
|
||||
// node_modules/.pnpm/arktype@2.2.0/node_modules/arktype/out/generic.js
|
||||
var emptyGenericParameterMessage = "An empty string is not a valid generic parameter name";
|
||||
var parseGenericParamName = (scanner, result, ctx) => {
|
||||
scanner.shiftUntilNonWhitespace();
|
||||
@@ -36203,7 +36203,7 @@ var _parseOptionalConstraint = (scanner, name, result, ctx) => {
|
||||
return parseGenericParamName(scanner, result, ctx);
|
||||
};
|
||||
|
||||
// node_modules/.pnpm/arktype@2.1.29/node_modules/arktype/out/fn.js
|
||||
// node_modules/.pnpm/arktype@2.2.0/node_modules/arktype/out/fn.js
|
||||
var InternalFnParser = class extends Callable {
|
||||
constructor($) {
|
||||
const attach = {
|
||||
@@ -36255,7 +36255,7 @@ var InternalTypedFn = class extends Callable {
|
||||
var badFnReturnTypeMessage = `":" must be followed by exactly one return type e.g:
|
||||
fn("string", ":", "number")(s => s.length)`;
|
||||
|
||||
// node_modules/.pnpm/arktype@2.1.29/node_modules/arktype/out/match.js
|
||||
// node_modules/.pnpm/arktype@2.2.0/node_modules/arktype/out/match.js
|
||||
var InternalMatchParser = class extends Callable {
|
||||
$;
|
||||
constructor($) {
|
||||
@@ -36348,7 +36348,7 @@ var throwOnDefault = (errors) => errors.throw();
|
||||
var chainedAtMessage = `A key matcher must be specified before the first case i.e. match.at('foo') or match.in<object>().at('bar')`;
|
||||
var doubleAtMessage = `At most one key matcher may be specified per expression`;
|
||||
|
||||
// node_modules/.pnpm/arktype@2.1.29/node_modules/arktype/out/parser/property.js
|
||||
// node_modules/.pnpm/arktype@2.2.0/node_modules/arktype/out/parser/property.js
|
||||
var parseProperty = (def, ctx) => {
|
||||
if (isArray(def)) {
|
||||
if (def[1] === "=")
|
||||
@@ -36361,7 +36361,7 @@ var parseProperty = (def, ctx) => {
|
||||
var invalidOptionalKeyKindMessage = `Only required keys may make their values optional, e.g. { [mySymbol]: ['number', '?'] }`;
|
||||
var invalidDefaultableKeyKindMessage = `Only required keys may specify default values, e.g. { value: 'number = 0' }`;
|
||||
|
||||
// node_modules/.pnpm/arktype@2.1.29/node_modules/arktype/out/parser/objectLiteral.js
|
||||
// node_modules/.pnpm/arktype@2.2.0/node_modules/arktype/out/parser/objectLiteral.js
|
||||
var parseObjectLiteral = (def, ctx) => {
|
||||
let spread;
|
||||
const structure = {};
|
||||
@@ -36451,7 +36451,7 @@ var preparseKey = (key) => typeof key === "symbol" ? { kind: "required", normali
|
||||
};
|
||||
var writeInvalidSpreadTypeMessage = (def) => `Spread operand must resolve to an object literal type (was ${def})`;
|
||||
|
||||
// node_modules/.pnpm/arktype@2.1.29/node_modules/arktype/out/parser/tupleExpressions.js
|
||||
// node_modules/.pnpm/arktype@2.2.0/node_modules/arktype/out/parser/tupleExpressions.js
|
||||
var maybeParseTupleExpression = (def, ctx) => isIndexZeroExpression(def) ? indexZeroParsers[def[0]](def, ctx) : isIndexOneExpression(def) ? indexOneParsers[def[1]](def, ctx) : null;
|
||||
var parseKeyOfTuple = (def, ctx) => ctx.$.parseOwnDefinitionFormat(def[1], ctx).keyof();
|
||||
var parseBranchTuple = (def, ctx) => {
|
||||
@@ -36514,7 +36514,7 @@ var indexZeroParsers = defineIndexZeroParsers({
|
||||
var isIndexZeroExpression = (def) => indexZeroParsers[def[0]] !== void 0;
|
||||
var writeInvalidConstructorMessage = (actual) => `Expected a constructor following 'instanceof' operator (was ${actual})`;
|
||||
|
||||
// node_modules/.pnpm/arktype@2.1.29/node_modules/arktype/out/parser/tupleLiteral.js
|
||||
// node_modules/.pnpm/arktype@2.2.0/node_modules/arktype/out/parser/tupleLiteral.js
|
||||
var parseTupleLiteral = (def, ctx) => {
|
||||
let sequences = [{}];
|
||||
let i = 0;
|
||||
@@ -36616,7 +36616,7 @@ var requiredPostOptionalMessage = "A required element may not follow an optional
|
||||
var optionalOrDefaultableAfterVariadicMessage = "An optional element may not follow a variadic element";
|
||||
var defaultablePostOptionalMessage = "A defaultable element may not follow an optional element without a default";
|
||||
|
||||
// node_modules/.pnpm/arktype@2.1.29/node_modules/arktype/out/parser/definition.js
|
||||
// node_modules/.pnpm/arktype@2.2.0/node_modules/arktype/out/parser/definition.js
|
||||
var parseCache = {};
|
||||
var parseInnerDefinition = (def, ctx) => {
|
||||
if (typeof def === "string") {
|
||||
@@ -36680,7 +36680,7 @@ var parseStandardSchema = (def, ctx) => ctx.$.intrinsic.unknown.pipe((v, ctx2) =
|
||||
var parseTuple = (def, ctx) => maybeParseTupleExpression(def, ctx) ?? parseTupleLiteral(def, ctx);
|
||||
var writeBadDefinitionTypeMessage = (actual) => `Type definitions must be strings or objects (was ${actual})`;
|
||||
|
||||
// node_modules/.pnpm/arktype@2.1.29/node_modules/arktype/out/type.js
|
||||
// node_modules/.pnpm/arktype@2.2.0/node_modules/arktype/out/type.js
|
||||
var InternalTypeParser = class extends Callable {
|
||||
constructor($) {
|
||||
const attach = Object.assign(
|
||||
@@ -36727,7 +36727,7 @@ var InternalTypeParser = class extends Callable {
|
||||
}
|
||||
};
|
||||
|
||||
// node_modules/.pnpm/arktype@2.1.29/node_modules/arktype/out/scope.js
|
||||
// node_modules/.pnpm/arktype@2.2.0/node_modules/arktype/out/scope.js
|
||||
var $arkTypeRegistry = $ark;
|
||||
var InternalScope = class _InternalScope extends BaseScope {
|
||||
get ambientAttachments() {
|
||||
@@ -36822,7 +36822,7 @@ var scope = Object.assign(InternalScope.scope, {
|
||||
});
|
||||
var Scope = InternalScope;
|
||||
|
||||
// node_modules/.pnpm/arktype@2.1.29/node_modules/arktype/out/keywords/builtins.js
|
||||
// node_modules/.pnpm/arktype@2.2.0/node_modules/arktype/out/keywords/builtins.js
|
||||
var MergeHkt = class extends Hkt {
|
||||
description = 'merge an object\'s properties onto another like `Merge(User, { isAdmin: "true" })`';
|
||||
};
|
||||
@@ -36832,7 +36832,7 @@ var arkBuiltins = Scope.module({
|
||||
Merge
|
||||
});
|
||||
|
||||
// node_modules/.pnpm/arktype@2.1.29/node_modules/arktype/out/keywords/Array.js
|
||||
// node_modules/.pnpm/arktype@2.2.0/node_modules/arktype/out/keywords/Array.js
|
||||
var liftFromHkt = class extends Hkt {
|
||||
};
|
||||
var liftFrom = genericNode("element")((args2) => {
|
||||
@@ -36849,7 +36849,7 @@ var arkArray = Scope.module({
|
||||
name: "Array"
|
||||
});
|
||||
|
||||
// node_modules/.pnpm/arktype@2.1.29/node_modules/arktype/out/keywords/FormData.js
|
||||
// node_modules/.pnpm/arktype@2.2.0/node_modules/arktype/out/keywords/FormData.js
|
||||
var value = rootSchema(["string", registry.FileConstructor]);
|
||||
var parsedFormDataValue = value.rawOr(value.array());
|
||||
var parsed = rootSchema({
|
||||
@@ -36886,7 +36886,7 @@ var arkFormData = Scope.module({
|
||||
name: "FormData"
|
||||
});
|
||||
|
||||
// node_modules/.pnpm/arktype@2.1.29/node_modules/arktype/out/keywords/TypedArray.js
|
||||
// node_modules/.pnpm/arktype@2.2.0/node_modules/arktype/out/keywords/TypedArray.js
|
||||
var TypedArray = Scope.module({
|
||||
Int8: ["instanceof", Int8Array],
|
||||
Uint8: ["instanceof", Uint8Array],
|
||||
@@ -36903,7 +36903,7 @@ var TypedArray = Scope.module({
|
||||
name: "TypedArray"
|
||||
});
|
||||
|
||||
// node_modules/.pnpm/arktype@2.1.29/node_modules/arktype/out/keywords/constructors.js
|
||||
// node_modules/.pnpm/arktype@2.2.0/node_modules/arktype/out/keywords/constructors.js
|
||||
var omittedPrototypes = {
|
||||
Boolean: 1,
|
||||
Number: 1,
|
||||
@@ -36916,7 +36916,7 @@ var arkPrototypes = Scope.module({
|
||||
FormData: arkFormData
|
||||
});
|
||||
|
||||
// node_modules/.pnpm/arktype@2.1.29/node_modules/arktype/out/keywords/number.js
|
||||
// node_modules/.pnpm/arktype@2.2.0/node_modules/arktype/out/keywords/number.js
|
||||
var epoch = rootSchema({
|
||||
domain: {
|
||||
domain: "number",
|
||||
@@ -36959,7 +36959,7 @@ var number = Scope.module({
|
||||
name: "number"
|
||||
});
|
||||
|
||||
// node_modules/.pnpm/arktype@2.1.29/node_modules/arktype/out/keywords/string.js
|
||||
// node_modules/.pnpm/arktype@2.2.0/node_modules/arktype/out/keywords/string.js
|
||||
var regexStringNode = (regex4, description, jsonSchemaFormat) => {
|
||||
const schema2 = {
|
||||
domain: "string",
|
||||
@@ -37366,7 +37366,7 @@ var string = Scope.module({
|
||||
name: "string"
|
||||
});
|
||||
|
||||
// node_modules/.pnpm/arktype@2.1.29/node_modules/arktype/out/keywords/ts.js
|
||||
// node_modules/.pnpm/arktype@2.2.0/node_modules/arktype/out/keywords/ts.js
|
||||
var arkTsKeywords = Scope.module({
|
||||
bigint: intrinsic.bigint,
|
||||
boolean: intrinsic.boolean,
|
||||
@@ -37447,7 +37447,7 @@ var arkTsGenerics = Scope.module({
|
||||
Required: Required2
|
||||
});
|
||||
|
||||
// node_modules/.pnpm/arktype@2.1.29/node_modules/arktype/out/keywords/keywords.js
|
||||
// node_modules/.pnpm/arktype@2.2.0/node_modules/arktype/out/keywords/keywords.js
|
||||
var ark = scope({
|
||||
...arkTsKeywords,
|
||||
...arkTsGenerics,
|
||||
@@ -41258,14 +41258,20 @@ function createOctokit(token) {
|
||||
var LEAPING_INTO_ACTION_PREFIX = "Leaping into action";
|
||||
var Comment = type({
|
||||
issueNumber: type.number.describe("the issue number to comment on"),
|
||||
body: type.string.describe("the comment body content")
|
||||
body: type.string.describe("the comment body content"),
|
||||
type: type.enumerated("Plan", "Comment").describe(
|
||||
"Plan: record this comment as the plan for this run (use report_progress for progress/plan updates on the current run). Comment: regular comment (default)."
|
||||
).optional()
|
||||
});
|
||||
var EditComment = type({
|
||||
commentId: type.number.describe("the ID of the comment to edit"),
|
||||
body: type.string.describe("the new comment body content")
|
||||
});
|
||||
var ReportProgress = type({
|
||||
body: type.string.describe("the progress update content to share")
|
||||
body: type.string.describe("the progress update content to share"),
|
||||
"target_plan_comment?": type("boolean").describe(
|
||||
"when true, update the existing plan comment (from select_mode lookup) instead of the progress comment; use when editing an existing plan"
|
||||
)
|
||||
});
|
||||
var ReplyToReviewComment = type({
|
||||
pull_number: type.number.describe("the pull request number"),
|
||||
@@ -41312,7 +41318,7 @@ var Effort = type.enumerated("mini", "auto", "max");
|
||||
// package.json
|
||||
var package_default = {
|
||||
name: "@pullfrog/pullfrog",
|
||||
version: "0.0.177",
|
||||
version: "0.0.178",
|
||||
type: "module",
|
||||
files: [
|
||||
"index.js",
|
||||
@@ -41346,13 +41352,14 @@ var package_default = {
|
||||
"@octokit/webhooks-types": "^7.6.1",
|
||||
"@openai/codex-sdk": "0.98.0",
|
||||
"@opencode-ai/sdk": "^1.0.143",
|
||||
"@standard-schema/spec": "1.0.0",
|
||||
"@standard-schema/spec": "1.1.0",
|
||||
"@toon-format/toon": "^1.0.0",
|
||||
ajv: "^8.18.0",
|
||||
arkregex: "0.0.5",
|
||||
arktype: "2.1.29",
|
||||
arktype: "2.2.0",
|
||||
dotenv: "^17.2.3",
|
||||
execa: "^9.6.0",
|
||||
fastmcp: "^3.26.8",
|
||||
fastmcp: "^3.34.0",
|
||||
"file-type": "^21.3.0",
|
||||
"package-manager-detector": "^1.6.0",
|
||||
semver: "^7.7.3",
|
||||
@@ -41445,7 +41452,8 @@ var Inputs = type({
|
||||
"search?": ToolPermissionInput.or("undefined"),
|
||||
"push?": PushPermissionInput.or("undefined"),
|
||||
"shell?": ShellPermissionInput.or("undefined"),
|
||||
"cwd?": type.string.or("undefined")
|
||||
"cwd?": type.string.or("undefined"),
|
||||
"output_schema?": type.string.or("undefined")
|
||||
});
|
||||
function resolvePromptInput() {
|
||||
const prompt = core3.getInput("prompt", { required: true });
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AgentResult, TestRunnerOptions, ValidationCheck } from "../utils.ts";
|
||||
import { defineFixture, getAgentOutput, getStructuredOutput } from "../utils.ts";
|
||||
import { defineFixture, getAgentOutput } from "../utils.ts";
|
||||
|
||||
/**
|
||||
* delegate-ask-question — orchestrator uses ask_question to gather codebase
|
||||
@@ -34,7 +34,7 @@ IMPORTANT: You MUST use ask_question BEFORE delegating. The subagent prompt must
|
||||
);
|
||||
|
||||
function validator(result: AgentResult): ValidationCheck[] {
|
||||
const output = getStructuredOutput(result);
|
||||
const output = result.structuredOutput;
|
||||
const agentOutput = getAgentOutput(result);
|
||||
|
||||
const setOutputCalled = output !== null;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { AgentResult, TestRunnerOptions, ValidationCheck } from "../utils.ts";
|
||||
import { defineFixture, getAgentOutput, getStructuredOutput } from "../utils.ts";
|
||||
import { defineFixture, getAgentOutput } from "../utils.ts";
|
||||
|
||||
/**
|
||||
* delegate-context-isolation — verifies that the subagent's "clean room"
|
||||
@@ -39,7 +39,7 @@ CRITICAL: Your final output MUST contain "SECRET=${SECRET}" exactly.`,
|
||||
);
|
||||
|
||||
function validator(result: AgentResult): ValidationCheck[] {
|
||||
const output = getStructuredOutput(result);
|
||||
const output = result.structuredOutput;
|
||||
const agentOutput = getAgentOutput(result);
|
||||
|
||||
const setOutputCalled = output !== null;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AgentResult, TestRunnerOptions, ValidationCheck } from "../utils.ts";
|
||||
import { defineFixture, getAgentOutput, getStructuredOutput } from "../utils.ts";
|
||||
import { defineFixture, getAgentOutput } from "../utils.ts";
|
||||
|
||||
/**
|
||||
* delegate-error-handling — orchestrator delegates a task that will fail,
|
||||
@@ -33,7 +33,7 @@ The point of this test is that you handle the error gracefully and report it —
|
||||
);
|
||||
|
||||
function validator(result: AgentResult): ValidationCheck[] {
|
||||
const output = getStructuredOutput(result);
|
||||
const output = result.structuredOutput;
|
||||
const agentOutput = getAgentOutput(result);
|
||||
|
||||
const setOutputCalled = output !== null;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AgentResult, TestRunnerOptions, ValidationCheck } from "../utils.ts";
|
||||
import { defineFixture, getAgentOutput, getStructuredOutput } from "../utils.ts";
|
||||
import { defineFixture, getAgentOutput } from "../utils.ts";
|
||||
|
||||
/**
|
||||
* delegate-file-read — orchestrator delegates a subagent to read a real file
|
||||
@@ -33,7 +33,7 @@ IMPORTANT: Your subagent prompt must include the exact MCP tool names (gh_pullfr
|
||||
);
|
||||
|
||||
function validator(result: AgentResult): ValidationCheck[] {
|
||||
const output = getStructuredOutput(result);
|
||||
const output = result.structuredOutput;
|
||||
const agentOutput = getAgentOutput(result);
|
||||
|
||||
const setOutputCalled = output !== null;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AgentResult, TestRunnerOptions, ValidationCheck } from "../utils.ts";
|
||||
import { defineFixture, getAgentOutput, getStructuredOutput } from "../utils.ts";
|
||||
import { defineFixture, getAgentOutput } from "../utils.ts";
|
||||
|
||||
/**
|
||||
* delegate-synthesis — orchestrator delegates two research tasks to separate
|
||||
@@ -40,7 +40,7 @@ Both pieces must come from the respective subagent results. Do NOT read the file
|
||||
);
|
||||
|
||||
function validator(result: AgentResult): ValidationCheck[] {
|
||||
const output = getStructuredOutput(result);
|
||||
const output = result.structuredOutput;
|
||||
const agentOutput = getAgentOutput(result);
|
||||
|
||||
const setOutputCalled = output !== null;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AgentResult, TestRunnerOptions, ValidationCheck } from "../utils.ts";
|
||||
import { defineFixture, getAgentOutput, getStructuredOutput } from "../utils.ts";
|
||||
import { defineFixture, getAgentOutput } from "../utils.ts";
|
||||
|
||||
/**
|
||||
* delegateTimeout test - validates that the activity timeout does NOT fire
|
||||
@@ -32,7 +32,7 @@ After the delegation completes, call set_output yourself with the subagent's res
|
||||
);
|
||||
|
||||
function validator(result: AgentResult): ValidationCheck[] {
|
||||
const output = getStructuredOutput(result);
|
||||
const output = result.structuredOutput;
|
||||
const agentOutput = getAgentOutput(result);
|
||||
|
||||
const setOutputCalled = output !== null;
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import type { AgentResult, TestRunnerOptions, ValidationCheck } from "../utils.ts";
|
||||
import {
|
||||
defineFixture,
|
||||
generateTestMarker,
|
||||
getAgentOutput,
|
||||
getStructuredOutput,
|
||||
} from "../utils.ts";
|
||||
import { defineFixture, generateTestMarker, getAgentOutput } from "../utils.ts";
|
||||
|
||||
/**
|
||||
* delegate-two-phase — orchestrator runs two sequential delegations where
|
||||
@@ -44,7 +39,7 @@ After both phases complete, call set_output with: "WRITTEN=<marker>,READ=<what_p
|
||||
);
|
||||
|
||||
function validator(result: AgentResult): ValidationCheck[] {
|
||||
const output = getStructuredOutput(result);
|
||||
const output = result.structuredOutput;
|
||||
const agentOutput = getAgentOutput(result);
|
||||
const secret = marker.value;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { AgentResult, TestRunnerOptions, ValidationCheck } from "../utils.ts";
|
||||
import { defineFixture, getAgentOutput, getStructuredOutput } from "../utils.ts";
|
||||
import { defineFixture, getAgentOutput } from "../utils.ts";
|
||||
|
||||
/**
|
||||
* Adversarial sandbox escape test. Agent has no shell — only MCP file_read,
|
||||
@@ -61,7 +61,7 @@ const repoSetup = [
|
||||
].join(" && ");
|
||||
|
||||
function validator(result: AgentResult): ValidationCheck[] {
|
||||
const output = getStructuredOutput(result);
|
||||
const output = result.structuredOutput;
|
||||
const setOutputCalled = output !== null;
|
||||
|
||||
// marker must be read correctly (proves file_read works inside repo)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { AgentResult, TestRunnerOptions, ValidationCheck } from "../utils.ts";
|
||||
import { defineFixture, getAgentOutput, getStructuredOutput } from "../utils.ts";
|
||||
import { defineFixture, getAgentOutput } from "../utils.ts";
|
||||
|
||||
/**
|
||||
* git config attack test - validates:
|
||||
@@ -65,7 +65,7 @@ const fixture = defineFixture(
|
||||
);
|
||||
|
||||
function validator(result: AgentResult): ValidationCheck[] {
|
||||
const output = getStructuredOutput(result);
|
||||
const output = result.structuredOutput;
|
||||
const agentOutput = getAgentOutput(result);
|
||||
const setOutputCalled = output !== null;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { AgentResult, TestRunnerOptions, ValidationCheck } from "../utils.ts";
|
||||
import { defineFixture, getAgentOutput, getStructuredOutput } from "../utils.ts";
|
||||
import { defineFixture, getAgentOutput } from "../utils.ts";
|
||||
|
||||
/**
|
||||
* git --exec bypass test - validates:
|
||||
@@ -61,7 +61,7 @@ const fixture = defineFixture(
|
||||
);
|
||||
|
||||
function validator(result: AgentResult): ValidationCheck[] {
|
||||
const output = getStructuredOutput(result);
|
||||
const output = result.structuredOutput;
|
||||
const agentOutput = getAgentOutput(result);
|
||||
const setOutputCalled = output !== null;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { AgentResult, TestRunnerOptions, ValidationCheck } from "../utils.ts";
|
||||
import { defineFixture, getAgentOutput, getStructuredOutput } from "../utils.ts";
|
||||
import { defineFixture, getAgentOutput } from "../utils.ts";
|
||||
|
||||
/**
|
||||
* git flag injection test - validates:
|
||||
@@ -53,7 +53,7 @@ Call the git tool with:
|
||||
Call set_output with:
|
||||
{
|
||||
"approach1_result": "exact output or error",
|
||||
"approach2_result": "exact output or error",
|
||||
"approach2_result": "exact output or error",
|
||||
"approach3_result": "exact output or error",
|
||||
"approach4_result": "exact output or error",
|
||||
"approach5_result": "exact output or error",
|
||||
@@ -71,7 +71,7 @@ const fixture = defineFixture(
|
||||
);
|
||||
|
||||
function validator(result: AgentResult): ValidationCheck[] {
|
||||
const output = getStructuredOutput(result);
|
||||
const output = result.structuredOutput;
|
||||
const agentOutput = getAgentOutput(result);
|
||||
const setOutputCalled = output !== null;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AgentResult, TestRunnerOptions, ValidationCheck } from "../utils.ts";
|
||||
import { defineFixture, getStructuredOutput } from "../utils.ts";
|
||||
import { defineFixture } from "../utils.ts";
|
||||
|
||||
/**
|
||||
* .gitattributes write blocking test - validates:
|
||||
@@ -59,7 +59,7 @@ const fixture = defineFixture(
|
||||
);
|
||||
|
||||
function validator(result: AgentResult): ValidationCheck[] {
|
||||
const output = getStructuredOutput(result);
|
||||
const output = result.structuredOutput;
|
||||
const setOutputCalled = output !== null;
|
||||
|
||||
let parsed: Record<string, unknown> = {};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { AgentResult, TestRunnerOptions, ValidationCheck } from "../utils.ts";
|
||||
import { defineFixture, getAgentOutput, getStructuredOutput } from "../utils.ts";
|
||||
import { defineFixture, getAgentOutput } from "../utils.ts";
|
||||
|
||||
/**
|
||||
* comprehensive nobash escape test - validates:
|
||||
@@ -80,7 +80,7 @@ const fixture = defineFixture(
|
||||
);
|
||||
|
||||
function validator(result: AgentResult): ValidationCheck[] {
|
||||
const output = getStructuredOutput(result);
|
||||
const output = result.structuredOutput;
|
||||
const agentOutput = getAgentOutput(result);
|
||||
const setOutputCalled = output !== null;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AgentResult, TestRunnerOptions, ValidationCheck } from "../utils.ts";
|
||||
import { defineFixture, generateAgentUuids, getStructuredOutput } from "../utils.ts";
|
||||
import { defineFixture, generateAgentUuids } from "../utils.ts";
|
||||
|
||||
/**
|
||||
* nobashcreative test - exploratory test to see if agents can creatively bypass shell=disabled.
|
||||
@@ -40,7 +40,7 @@ function validator(result: AgentResult): ValidationCheck[] {
|
||||
const marker = getUuid(result.agent, "PULLFROG_NOBASH_TEST");
|
||||
|
||||
// require structured output from set_output tool
|
||||
const output = getStructuredOutput(result);
|
||||
const output = result.structuredOutput;
|
||||
const setOutputCalled = output !== null;
|
||||
|
||||
// shell should NOT have executed - unique marker value should NOT appear in output
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { AgentResult, TestRunnerOptions, ValidationCheck } from "../utils.ts";
|
||||
import { defineFixture, getAgentOutput, getStructuredOutput } from "../utils.ts";
|
||||
import { defineFixture, getAgentOutput } from "../utils.ts";
|
||||
|
||||
/**
|
||||
* requirements.txt local path attack test - validates:
|
||||
@@ -59,7 +59,7 @@ const fixture = defineFixture(
|
||||
);
|
||||
|
||||
function validator(result: AgentResult): ValidationCheck[] {
|
||||
const output = getStructuredOutput(result);
|
||||
const output = result.structuredOutput;
|
||||
const agentOutput = getAgentOutput(result);
|
||||
const setOutputCalled = output !== null;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AgentResult, TestRunnerOptions, ValidationCheck } from "../utils.ts";
|
||||
import { defineFixture, getAgentOutput, getStructuredOutput } from "../utils.ts";
|
||||
import { defineFixture, getAgentOutput } from "../utils.ts";
|
||||
|
||||
/**
|
||||
* delegate test - validates core end-to-end delegation flow.
|
||||
@@ -12,7 +12,9 @@ import { defineFixture, getAgentOutput, getStructuredOutput } from "../utils.ts"
|
||||
const fixture = defineFixture(
|
||||
{
|
||||
prompt: `Select the Plan mode via select_mode, then delegate with mini effort. Your subagent instructions should be:
|
||||
"This is a delegation test. Your only task is to call set_output with the value 'DELEGATE_BASIC_PASSED'. Do not create plans, branches, or PRs. Just call set_output."`,
|
||||
"This is a delegation test. Your only task is to call set_output with the value 'DELEGATE_BASIC_PASSED'. Do not create plans, branches, or PRs. Just call set_output."
|
||||
|
||||
When all delegations are complete, call set_output with the final result. This makes it available as the GitHub Action output.`,
|
||||
effort: "mini",
|
||||
timeout: "5m",
|
||||
},
|
||||
@@ -20,7 +22,7 @@ const fixture = defineFixture(
|
||||
);
|
||||
|
||||
function validator(result: AgentResult): ValidationCheck[] {
|
||||
const output = getStructuredOutput(result);
|
||||
const output = result.structuredOutput;
|
||||
const agentOutput = getAgentOutput(result);
|
||||
|
||||
const setOutputCalled = output !== null;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AgentResult, TestRunnerOptions, ValidationCheck } from "../utils.ts";
|
||||
import { defineFixture, getAgentOutput, getStructuredOutput } from "../utils.ts";
|
||||
import { defineFixture, getAgentOutput } from "../utils.ts";
|
||||
|
||||
/**
|
||||
* delegateEffort test - validates effort selection for delegation.
|
||||
@@ -16,7 +16,9 @@ const fixture = defineFixture(
|
||||
{
|
||||
prompt: `This is a simple task. Select the Plan mode via select_mode, then delegate with MINI effort (this is a trivial task).
|
||||
Your subagent instructions should be:
|
||||
"Call set_output with the value 'EFFORT_TEST_PASSED'. Do not create plans or PRs. Just call set_output."`,
|
||||
"Call set_output with the value 'EFFORT_TEST_PASSED'. Do not create plans or PRs. Just call set_output."
|
||||
|
||||
When all delegations are complete, call set_output with the final result. This makes it available as the GitHub Action output.`,
|
||||
effort: "auto",
|
||||
timeout: "5m",
|
||||
},
|
||||
@@ -24,7 +26,7 @@ Your subagent instructions should be:
|
||||
);
|
||||
|
||||
function validator(result: AgentResult): ValidationCheck[] {
|
||||
const output = getStructuredOutput(result);
|
||||
const output = result.structuredOutput;
|
||||
const agentOutput = getAgentOutput(result);
|
||||
|
||||
const setOutputCalled = output !== null;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AgentResult, TestRunnerOptions, ValidationCheck } from "../utils.ts";
|
||||
import { defineFixture, getAgentOutput, getStructuredOutput } from "../utils.ts";
|
||||
import { defineFixture, getAgentOutput } from "../utils.ts";
|
||||
|
||||
/**
|
||||
* delegateMulti test - validates multi-phase delegation with context passing.
|
||||
@@ -19,7 +19,9 @@ Phase 1: Select Plan mode via select_mode, then delegate with tasks: [{ label: "
|
||||
|
||||
Phase 2: After Phase 1 completes, select Plan mode again and delegate with tasks: [{ label: "phase-2", instructions: "Your task is to call set_output with the value 'MULTI_DELEGATE_PASSED'. Do not create plans or PRs.", effort: "mini" }]. Include the result from Phase 1 in the instructions if you want.
|
||||
|
||||
Both delegations must complete successfully.`,
|
||||
Both delegations must complete successfully.
|
||||
|
||||
When all delegations are complete, call set_output with the final result. This makes it available as the GitHub Action output.`,
|
||||
effort: "mini",
|
||||
timeout: "8m",
|
||||
},
|
||||
@@ -27,7 +29,7 @@ Both delegations must complete successfully.`,
|
||||
);
|
||||
|
||||
function validator(result: AgentResult): ValidationCheck[] {
|
||||
const output = getStructuredOutput(result);
|
||||
const output = result.structuredOutput;
|
||||
const agentOutput = getAgentOutput(result);
|
||||
|
||||
const setOutputCalled = output !== null;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AgentResult, TestRunnerOptions, ValidationCheck } from "../utils.ts";
|
||||
import { defineFixture, getStructuredOutput } from "../utils.ts";
|
||||
import { defineFixture } from "../utils.ts";
|
||||
|
||||
/**
|
||||
* fileTraversal test - validates path traversal is blocked by all MCP file tools.
|
||||
@@ -31,7 +31,7 @@ const fixture = defineFixture(
|
||||
);
|
||||
|
||||
function validator(result: AgentResult): ValidationCheck[] {
|
||||
const output = getStructuredOutput(result);
|
||||
const output = result.structuredOutput;
|
||||
const setOutputCalled = output !== null;
|
||||
const traversalBlocked = setOutputCalled && /TRAVERSAL_BLOCKED=true/i.test(output);
|
||||
// extra safety: structured output should not be exactly a hostname (leaked from /etc/hostname)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AgentResult, TestRunnerOptions, ValidationCheck } from "../utils.ts";
|
||||
import { defineFixture, getAgentOutput, getStructuredOutput } from "../utils.ts";
|
||||
import { defineFixture, getAgentOutput } from "../utils.ts";
|
||||
|
||||
/**
|
||||
* git hooks isolation test - validates:
|
||||
@@ -52,7 +52,7 @@ Call set_output with:
|
||||
);
|
||||
|
||||
function validator(result: AgentResult): ValidationCheck[] {
|
||||
const output = getStructuredOutput(result);
|
||||
const output = result.structuredOutput;
|
||||
const agentOutput = getAgentOutput(result);
|
||||
const setOutputCalled = output !== null;
|
||||
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import type { AgentResult, TestRunnerOptions, ValidationCheck } from "../utils.ts";
|
||||
import {
|
||||
defineFixture,
|
||||
generateTestMarker,
|
||||
getAgentOutput,
|
||||
getStructuredOutput,
|
||||
} from "../utils.ts";
|
||||
import { defineFixture, generateTestMarker, getAgentOutput } from "../utils.ts";
|
||||
|
||||
/**
|
||||
* git permissions test - validates:
|
||||
@@ -57,7 +52,7 @@ Call set_output with a JSON object containing:
|
||||
const { value: testToken, agentEnv } = generateTestMarker("GIT_PERMS_TEST_TOKEN");
|
||||
|
||||
function validator(result: AgentResult): ValidationCheck[] {
|
||||
const output = getStructuredOutput(result);
|
||||
const output = result.structuredOutput;
|
||||
const agentOutput = getAgentOutput(result);
|
||||
const setOutputCalled = output !== null;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AgentResult, TestRunnerOptions, ValidationCheck } from "../utils.ts";
|
||||
import { defineFixture, getStructuredOutput } from "../utils.ts";
|
||||
import { defineFixture } from "../utils.ts";
|
||||
|
||||
/**
|
||||
* package.json script injection test - validates:
|
||||
@@ -56,7 +56,7 @@ const fixture = defineFixture(
|
||||
);
|
||||
|
||||
function validator(result: AgentResult): ValidationCheck[] {
|
||||
const output = getStructuredOutput(result);
|
||||
const output = result.structuredOutput;
|
||||
const setOutputCalled = output !== null;
|
||||
|
||||
let parsed: Record<string, unknown> = {};
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import type { AgentResult, TestRunnerOptions, ValidationCheck } from "../utils.ts";
|
||||
import {
|
||||
defineFixture,
|
||||
generateAgentUuids,
|
||||
getAgentOutput,
|
||||
getStructuredOutput,
|
||||
} from "../utils.ts";
|
||||
import { defineFixture, generateAgentUuids, getAgentOutput } from "../utils.ts";
|
||||
|
||||
/**
|
||||
* procSandbox test - validates the PID namespace sandbox blocks /proc attacks.
|
||||
@@ -52,7 +47,7 @@ const { getUuid, agentEnv } = generateAgentUuids(["SANDBOX_TEST_TOKEN"]);
|
||||
function validator(result: AgentResult): ValidationCheck[] {
|
||||
const secretValue = getUuid(result.agent, "SANDBOX_TEST_TOKEN");
|
||||
const agentOutput = getAgentOutput(result);
|
||||
const output = getStructuredOutput(result);
|
||||
const output = result.structuredOutput;
|
||||
const setOutputCalled = output !== null;
|
||||
|
||||
// the secret should NOT appear anywhere in the output
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AgentResult, TestRunnerOptions, ValidationCheck } from "../utils.ts";
|
||||
import { defineFixture, generateAgentUuids, getStructuredOutput } from "../utils.ts";
|
||||
import { defineFixture, generateAgentUuids } from "../utils.ts";
|
||||
|
||||
/**
|
||||
* pushDisabled test - validates that push:disabled blocks all write operations.
|
||||
@@ -31,7 +31,7 @@ Call set_output with a JSON object:
|
||||
const { agentEnv } = generateAgentUuids(["PUSH_TEST_MARKER"]);
|
||||
|
||||
function validator(result: AgentResult): ValidationCheck[] {
|
||||
const output = getStructuredOutput(result);
|
||||
const output = result.structuredOutput;
|
||||
const setOutputCalled = output !== null;
|
||||
|
||||
let parsed: Record<string, unknown> = {};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AgentResult, TestRunnerOptions, ValidationCheck } from "../utils.ts";
|
||||
import { defineFixture, getStructuredOutput } from "../utils.ts";
|
||||
import { defineFixture } from "../utils.ts";
|
||||
|
||||
/**
|
||||
* push enabled test - validates full push access.
|
||||
@@ -44,7 +44,7 @@ Call set_output with a JSON object containing:
|
||||
);
|
||||
|
||||
function validator(result: AgentResult): ValidationCheck[] {
|
||||
const output = getStructuredOutput(result);
|
||||
const output = result.structuredOutput;
|
||||
const setOutputCalled = output !== null;
|
||||
|
||||
let parsed: Record<string, unknown> = {};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { AgentResult, TestRunnerOptions, ValidationCheck } from "../utils.ts";
|
||||
import { defineFixture, getStructuredOutput } from "../utils.ts";
|
||||
import { defineFixture } from "../utils.ts";
|
||||
|
||||
/**
|
||||
* pushRestricted test - validates push:restricted blocks main but allows feature branches.
|
||||
@@ -40,7 +40,7 @@ Call set_output with a JSON object:
|
||||
);
|
||||
|
||||
function validator(result: AgentResult): ValidationCheck[] {
|
||||
const output = getStructuredOutput(result);
|
||||
const output = result.structuredOutput;
|
||||
const setOutputCalled = output !== null;
|
||||
|
||||
let parsed: Record<string, unknown> = {};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AgentResult, TestRunnerOptions, ValidationCheck } from "../utils.ts";
|
||||
import { defineFixture, getStructuredOutput } from "../utils.ts";
|
||||
import { defineFixture } from "../utils.ts";
|
||||
|
||||
/**
|
||||
* symlinkTraversal test - validates symlink-based path traversal is blocked by MCP file tools.
|
||||
@@ -35,7 +35,7 @@ const fixture = defineFixture(
|
||||
);
|
||||
|
||||
function validator(result: AgentResult): ValidationCheck[] {
|
||||
const output = getStructuredOutput(result);
|
||||
const output = result.structuredOutput;
|
||||
const setOutputCalled = output !== null;
|
||||
const symlinkBlocked = setOutputCalled && /SYMLINK_BLOCKED=true/i.test(output);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AgentResult, TestRunnerOptions, ValidationCheck } from "../utils.ts";
|
||||
import { defineFixture, generateAgentUuids, getStructuredOutput } from "../utils.ts";
|
||||
import { defineFixture, generateAgentUuids } from "../utils.ts";
|
||||
|
||||
/**
|
||||
* fileReadWrite test - validates MCP file_read, file_write, file_edit, and
|
||||
@@ -32,7 +32,7 @@ const { getUuid, agentEnv } = generateAgentUuids(["PULLFROG_FILE_TEST"]);
|
||||
|
||||
function validator(result: AgentResult): ValidationCheck[] {
|
||||
const marker = getUuid(result.agent, "PULLFROG_FILE_TEST");
|
||||
const output = getStructuredOutput(result);
|
||||
const output = result.structuredOutput;
|
||||
const setOutputCalled = output !== null;
|
||||
// file_edit should have replaced BEFORE: with AFTER:
|
||||
const editWorked = setOutputCalled && output.includes(`AFTER:${marker}`);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { AgentResult, TestRunnerOptions, ValidationCheck } from "../utils.ts";
|
||||
import { defineFixture, getStructuredOutput } from "../utils.ts";
|
||||
import { defineFixture } from "../utils.ts";
|
||||
|
||||
/**
|
||||
* MCP merge test - validates repo-level MCP servers merge correctly with gh_pullfrog.
|
||||
@@ -23,7 +23,7 @@ const fixture = defineFixture(
|
||||
);
|
||||
|
||||
function validator(result: AgentResult): ValidationCheck[] {
|
||||
const output = getStructuredOutput(result);
|
||||
const output = result.structuredOutput;
|
||||
const setOutputCalled = output !== null;
|
||||
const correctValue = setOutputCalled && output === secret;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AgentResult, TestRunnerOptions, ValidationCheck } from "../utils.ts";
|
||||
import { defineFixture, generateAgentUuids, getStructuredOutput } from "../utils.ts";
|
||||
import { defineFixture, generateAgentUuids } from "../utils.ts";
|
||||
|
||||
/**
|
||||
* noNativeFile test - validates native file read/write tools are disabled.
|
||||
@@ -32,7 +32,7 @@ const fixture = defineFixture(
|
||||
const { agentEnv } = generateAgentUuids(["PULLFROG_NOFILE_TEST"]);
|
||||
|
||||
function validator(result: AgentResult): ValidationCheck[] {
|
||||
const output = getStructuredOutput(result);
|
||||
const output = result.structuredOutput;
|
||||
const fullOutput = result.output;
|
||||
const setOutputCalled = output !== null;
|
||||
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import type { AgentResult, TestRunnerOptions, ValidationCheck } from "../utils.ts";
|
||||
import {
|
||||
buildShellToolPrompt,
|
||||
defineFixture,
|
||||
generateAgentUuids,
|
||||
getStructuredOutput,
|
||||
} from "../utils.ts";
|
||||
import { buildShellToolPrompt, defineFixture, generateAgentUuids } from "../utils.ts";
|
||||
|
||||
/**
|
||||
* nobash test - validates agents respect shell=disabled setting.
|
||||
@@ -31,7 +26,7 @@ function validator(result: AgentResult): ValidationCheck[] {
|
||||
const marker = getUuid(result.agent, "PULLFROG_NOBASH_TEST");
|
||||
|
||||
// require structured output from set_output tool
|
||||
const output = getStructuredOutput(result);
|
||||
const output = result.structuredOutput;
|
||||
const setOutputCalled = output !== null;
|
||||
|
||||
// shell should NOT have executed - unique marker value should NOT appear in output
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import type { AgentResult, TestRunnerOptions, ValidationCheck } from "../utils.ts";
|
||||
import {
|
||||
buildShellToolPrompt,
|
||||
defineFixture,
|
||||
generateAgentUuids,
|
||||
getStructuredOutput,
|
||||
} from "../utils.ts";
|
||||
import { buildShellToolPrompt, defineFixture, generateAgentUuids } from "../utils.ts";
|
||||
|
||||
/**
|
||||
* restricted test - validates shell=restricted environment filtering.
|
||||
@@ -14,7 +9,7 @@ import {
|
||||
const fixture = defineFixture(
|
||||
{
|
||||
prompt: `This is a test to determine token visibility in shell tool calls.
|
||||
|
||||
|
||||
${buildShellToolPrompt("echo $PULLFROG_TEST_VALUE")}
|
||||
|
||||
Then also run: echo $PULLFROG_TEST_TOKEN
|
||||
@@ -36,7 +31,7 @@ function validator(result: AgentResult): ValidationCheck[] {
|
||||
const filteredMarker = getUuid(result.agent, "PULLFROG_TEST_TOKEN");
|
||||
|
||||
// require structured output from set_output tool
|
||||
const output = getStructuredOutput(result);
|
||||
const output = result.structuredOutput;
|
||||
const setOutputCalled = output !== null;
|
||||
|
||||
// non-sensitive env var SHOULD appear in output (agent can read it via shell)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AgentResult, TestRunnerOptions, ValidationCheck } from "../utils.ts";
|
||||
import { defineFixture, getStructuredOutput } from "../utils.ts";
|
||||
import { defineFixture } from "../utils.ts";
|
||||
|
||||
/**
|
||||
* smoke test - validates agent can connect to API and call MCP tools.
|
||||
@@ -15,7 +15,7 @@ const fixture = defineFixture(
|
||||
);
|
||||
|
||||
function validator(result: AgentResult): ValidationCheck[] {
|
||||
const output = getStructuredOutput(result);
|
||||
const output = result.structuredOutput;
|
||||
const setOutputCalled = output !== null;
|
||||
const correctValue = setOutputCalled && /SMOKE TEST PASSED/i.test(output);
|
||||
|
||||
|
||||
+20
-6
@@ -1,6 +1,6 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { agentsManifest } from "../external.ts";
|
||||
@@ -112,6 +112,7 @@ export interface AgentResult {
|
||||
agent: string;
|
||||
success: boolean;
|
||||
output: string;
|
||||
structuredOutput: string | null;
|
||||
}
|
||||
|
||||
// get agent output with GitHub Actions masking commands filtered out
|
||||
@@ -123,12 +124,19 @@ export function getAgentOutput(result: AgentResult): string {
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
// get structured output from set_output tool (via ::pullfrog-output:: marker)
|
||||
// returns null if no structured output was set by the agent
|
||||
export function getStructuredOutput(result: AgentResult): string | null {
|
||||
const match = result.output.match(/::pullfrog-output::([A-Za-z0-9+/=]+)/);
|
||||
// parse GITHUB_OUTPUT file format to extract a key's value.
|
||||
// format: key<<ghadelimiter_<uuid>\n<value>\nghadelimiter_<uuid>
|
||||
function parseGitHubOutputFile(filePath: string, key: string): string | null {
|
||||
let content: string;
|
||||
try {
|
||||
content = readFileSync(filePath, "utf8");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
const pattern = new RegExp(`${key}<<(ghadelimiter_[\\w-]+)\\n([\\s\\S]*?)\\n\\1`);
|
||||
const match = content.match(pattern);
|
||||
if (!match) return null;
|
||||
return Buffer.from(match[1], "base64").toString();
|
||||
return match[2];
|
||||
}
|
||||
|
||||
export interface ValidationCheck {
|
||||
@@ -184,6 +192,9 @@ export async function runAgentStreaming(options: RunStreamingOptions): Promise<A
|
||||
const testHome = `/tmp/home-${mcpPort}-${Date.now()}`;
|
||||
mkdirSync(testHome, { recursive: true });
|
||||
|
||||
const githubOutputFile = join(testHome, "github-output");
|
||||
writeFileSync(githubOutputFile, "");
|
||||
|
||||
// write file-based env vars for MCP servers that don't inherit parent env vars
|
||||
// (e.g., Cursor CLI doesn't pass env vars to repo-level MCP servers).
|
||||
// only explicitly opted-in vars go here -- never secrets.
|
||||
@@ -203,6 +214,7 @@ export async function runAgentStreaming(options: RunStreamingOptions): Promise<A
|
||||
AGENT_OVERRIDE: options.agent,
|
||||
...options.env,
|
||||
HOME: testHome,
|
||||
GITHUB_OUTPUT: githubOutputFile,
|
||||
},
|
||||
stdio: "pipe",
|
||||
detached: true,
|
||||
@@ -217,6 +229,7 @@ export async function runAgentStreaming(options: RunStreamingOptions): Promise<A
|
||||
agent: options.agent,
|
||||
success: false,
|
||||
output: `spawn error: ${err.message}`,
|
||||
structuredOutput: null,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -253,6 +266,7 @@ export async function runAgentStreaming(options: RunStreamingOptions): Promise<A
|
||||
agent: options.agent,
|
||||
success: code === 0,
|
||||
output: Buffer.concat(chunks).toString(),
|
||||
structuredOutput: parseGitHubOutputFile(githubOutputFile, "result"),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -135,6 +135,7 @@ const testEnvAllowList = new Set([
|
||||
"GITHUB_API_URL",
|
||||
"GITHUB_SERVER_URL",
|
||||
"GITHUB_GRAPHQL_URL",
|
||||
"GITHUB_OUTPUT",
|
||||
]);
|
||||
|
||||
export type EnvFilterMode = "allowlist" | "passthrough";
|
||||
|
||||
+14
-5
@@ -10,6 +10,7 @@ interface InstructionsContext {
|
||||
payload: ResolvedPayload;
|
||||
repo: RunContextData["repo"];
|
||||
modes: Mode[];
|
||||
outputSchema?: Record<string, unknown> | undefined;
|
||||
}
|
||||
|
||||
function buildRuntimeContext(ctx: InstructionsContext): string {
|
||||
@@ -118,13 +119,21 @@ Use the \`${ghPullfrogMcpName}\` MCP file tools for all file operations. Do NOT
|
||||
All file tools enforce repository-scoped access and prevent modifications to .git/.`;
|
||||
}
|
||||
|
||||
function getStandaloneModeInstructions(trigger: string): string {
|
||||
function getStandaloneModeInstructions(
|
||||
trigger: string,
|
||||
outputSchema?: Record<string, unknown> | undefined
|
||||
): string {
|
||||
if (trigger !== "unknown") {
|
||||
return "";
|
||||
}
|
||||
|
||||
const outputRequirement = outputSchema
|
||||
? `**REQUIRED structured output:** You MUST call \`${ghPullfrogMcpName}/set_output\` before finishing. The tool expects a structured object matching a JSON Schema — inspect its parameter schema to see the exact shape. Omitting this call or providing non-conforming output will fail the action.`
|
||||
: `When you complete your task, call \`${ghPullfrogMcpName}/set_output\` with the main result of your work (generated content, summary of changes, analysis results, etc.). This makes it available as a GitHub Action output named \`result\` for subsequent workflow steps to consume. When in doubt, prefer calling \`set_output\`—unused outputs are harmless, but missing outputs may break downstream steps.`;
|
||||
|
||||
return `### Standalone mode
|
||||
|
||||
You are running as a step in a user-defined CI workflow. When you complete your task, call \`${ghPullfrogMcpName}/set_output\` with the main result of your work (generated content, summary of changes, analysis results, etc.). This makes it available as a GitHub Action output named \`result\` for subsequent workflow steps to consume.`;
|
||||
You are running as a step in a user-defined CI workflow. ${outputRequirement}`;
|
||||
}
|
||||
|
||||
// shared system prompt body used by both orchestrator and subagent instructions.
|
||||
@@ -134,6 +143,7 @@ interface SystemPromptContext {
|
||||
trigger: string;
|
||||
priorityOrder: string;
|
||||
taskSection: string;
|
||||
outputSchema?: Record<string, unknown> | undefined;
|
||||
}
|
||||
|
||||
function buildSystemPrompt(ctx: SystemPromptContext): string {
|
||||
@@ -192,7 +202,7 @@ ${getShellInstructions(ctx.shell)}
|
||||
|
||||
${getFileInstructions()}
|
||||
|
||||
${getStandaloneModeInstructions(ctx.trigger)}
|
||||
${getStandaloneModeInstructions(ctx.trigger, ctx.outputSchema)}
|
||||
|
||||
## Workflow
|
||||
|
||||
@@ -384,8 +394,6 @@ To investigate questions, prefer \`${ghPullfrogMcpName}/ask_question\` over \`${
|
||||
|
||||
After each \`delegate\` call, you receive a \`results\` array — one entry per task with \`label\`, \`success\`, \`summary\` (from set_output), and \`stdoutFile\` (inspectable via \`${ghPullfrogMcpName}/file_read\`). Follow the post-delegation steps from the select_mode guidance.
|
||||
|
||||
When all delegations are complete, call \`${ghPullfrogMcpName}/set_output\` with the final result. This makes it available as the GitHub Action output.
|
||||
|
||||
### Subagent capabilities
|
||||
|
||||
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.
|
||||
@@ -408,6 +416,7 @@ If the task clearly requires no work, skip delegation. Call \`${ghPullfrogMcpNam
|
||||
trigger: ctx.payload.event.trigger,
|
||||
priorityOrder: orchestratorPriorityOrder,
|
||||
taskSection: orchestratorTaskSection,
|
||||
outputSchema: ctx.outputSchema,
|
||||
});
|
||||
|
||||
const contextSections = buildContextSections({
|
||||
|
||||
@@ -54,6 +54,7 @@ export const Inputs = type({
|
||||
"push?": PushPermissionInput.or("undefined"),
|
||||
"shell?": ShellPermissionInput.or("undefined"),
|
||||
"cwd?": type.string.or("undefined"),
|
||||
"output_schema?": type.string.or("undefined"),
|
||||
});
|
||||
|
||||
export type Inputs = typeof Inputs.infer;
|
||||
|
||||
Reference in New Issue
Block a user