* fix: prevent cross-PR push from subagent-induced branch switch
A workflow_dispatch run for zed-industries/cloud (workflow run 26036155393)
force-pushed the orchestrator's work onto an unrelated engineer's PR branch
(origin/reactivate-pro-plan, PR #2582). The orchestrator's reviewfrog subagent
called checkout_pr({pull_number: 2582}), which (1) moved the shared working
tree to pr-2582 and (2) persisted pushDest pointing at reactivate-pro-plan.
The orchestrator's subsequent commit + push_branch then clobbered the victim
PR. Recovery + disclosure in PR #2584.
Three compounding bugs closed here:
1. checkout_pr dirty-tree guard had a first-call hole: the previous condition
required ctx.toolState.issueNumber to already be set, so on workflow_dispatch
runs the first checkout_pr (commonly from a subagent) bypassed the guard
entirely. Now any PR switch with a dirty tree is refused, including the
first switch of a run. Idempotent same-PR re-checkouts are still absorbed
by alreadyOnBranch inside checkoutPrBranch.
2. push_branch trusted sticky pushDest blindly. Added a backstop: refuse
pushes where the local branch matches /^pr-(\d+)$/ AND pushDest.remoteBranch
differs from it AND the current run is not scoped to PR N (event.is_pr === true
&& event.issue_number === N). This catches subagent-induced silent branch
switches even if a future bug reintroduces a first-call hole in fix#1.
3. Build-mode self-review prompt told the orchestrator to ship "the output of
git diff" to the reviewer. The model in this run synthesized
git diff main...HEAD, which excludes uncommitted work — and Build self-review
runs BEFORE the commit, so the reviewer saw an empty diff and thrashed,
eventually calling checkout_pr on a random PR to find something to look at.
Prompt now specifies git diff origin/<base-branch> (two-dot, no HEAD),
which compares the working tree against the remote base.
Refs:
zed-industries/cloud workflow run 26036155393
zed-industries/cloud#2582 (victim)
zed-industries/cloud#2584 (disclosure)
* review: key dirty-tree guard on current branch + drop 'two-dot' misnomer
Address review feedback on PR #796.
1. checkout_pr dirty-tree guard now keys off the live current branch
(git rev-parse --abbrev-ref HEAD), not ctx.toolState.issueNumber.
issueNumber is ALSO set by get_issue / get_issue_comments /
get_issue_events, so a subagent doing get_issue(N) followed by
checkout_pr(N) on a dirty tree would have bypassed the original guard
(issueNumber === pull_number). The current branch is the actual
primitive for "would this call move HEAD" — querying it directly avoids
correlating on toolState that other tools write to.
2. modes.ts: drop the wrong "two-dot" label on git diff origin/<base>.
That's the single-rev form, not two-dot. Copilot was right that the
label was confusing/contradictory with the actually-shown command.
Pullfrog is a GitHub bot that brings the full power of your favorite coding agents into GitHub. It's open source and powered by GitHub Actions.
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.
issue created
issue labeled
PR 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.
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
name:Releaseon:push:tags:['v*']permissions:contents:writejobs:release:runs-on:ubuntu-lateststeps:- name:Checkoutuses:actions/checkout@v4with:fetch-depth:0- name:Generate release notesid:notesuses:pullfrog/pullfrog@v0with: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 releaserun:| 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:
name:Release Checkon:pull_request:types:[closed]jobs:check-release:if:github.event.pull_request.merged == trueruns-on:ubuntu-lateststeps:- uses:actions/checkout@v4- name:Install dependenciesrun:npm install --no-save --no-package-lock zod @actions/core- name:Generate Schemaid:schemarun:| 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 PRid:analysisuses:pullfrog/pullfrog@v0with: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 Resultrun:| # Parse the JSON result using fromJSON()
echo "Version: ${{ fromJSON(steps.analysis.outputs.result).version }}"
echo "Breaking: ${{ fromJSON(steps.analysis.outputs.result).isBreaking }}"