Ivan's Inferences #7

2 GitHub Actions YAML Files That Let Every Engineer Ship a Ton of Code

Max Heckel - Author profile picture
Max Heckel
· 7 min read
AI agentsGitHub ActionsEngineeringDeveloper productivityOpen source

Here's how I opened 80 pull requests in a single two-week sprint: I assigned an AI agent a stack of tasks, walked away from my machine, and let it work. It implemented the changes, opened the PRs, requested review, waited for comments to come in, and addressed them — all of it, without me sitting there watching a progress bar.

I posted about this on LinkedIn recently and, honestly, I'm still a little baffled more engineering teams aren't doing it. The tool is called Ivan. It's free. It's open source. It runs natively in GitHub Actions. And the setup is small enough to read in one sitting — which is exactly what the rest of this post is.

Here's a concrete example from our own agents repo. At 18:45 one afternoon, an engineer filed an issue: the meeting-summary tools were returning summary: null for meetings that were still processing, making Ari look broken. He typed one line in a comment: @ivan-agent /build.

By 19:09 — 24 minutes later — there was an open pull request. By 19:33, it had already been updated to address the first round of review comments. Nobody wrote a design doc. Nobody picked it up in a sprint planning meeting. The issue became a PR faster than most of us can find a parking spot.

That's Ivan: a GitHub Action that turns an issue comment into a real, reviewable pull request. We've been running it in ~/Sites/agents for a few weeks now, and it has quietly become one of the highest-leverage things in our engineering workflow.

What Ivan actually is

Ivan is a CLI (@ariso-ai/ivan on npm) that breaks a task down, implements it with Claude, and opens a pull request. We wrap that CLI in two GitHub Actions workflows so it runs the moment someone asks for it, right inside a normal GitHub conversation — no separate dashboard, no second tool to open.

And here's the part worth saying plainly: Ivan is free and open source, MIT-licensed, at github.com/ariso-ai/ivan. We built it, we use it every day in this repo, and anyone can npm i -g @ariso-ai/ivan and point it at their own codebase for nothing. There's no hosted product behind a paywall here — the whole thing runs with your own Anthropic and OpenAI keys, on your own infrastructure (your laptop, or GitHub's runners, take your pick).

Two comment commands drive the GitHub Actions integration:

  • @ivan-agent /build on an issue — Ivan reads the issue body, implements it, and opens a PR.
  • @ivan-agent /address on a PR (or a reply to a specific review comment) — Ivan reads the review feedback and pushes a fix.

That's the entire interface. You don't context-switch into a new tool — you talk to Ivan the same way you'd talk to a teammate, in the thread where the work already lives.

The loop closes itself

The part that actually moved the productivity needle isn't the /build command by itself — plenty of "AI opens a PR" tools exist. It's that the ivanagent.yml workflow chains straight into ivanagent-address.yml automatically:

  1. /build opens a PR.
  2. The workflow waits 15 minutes — just enough time for CodeRabbit and a human reviewer to leave comments.
  3. It runs ivan address <pr-number> --yes on its own, no second command needed, folding in whatever feedback showed up.

And a second, quieter loop closes on top of that: Ari — our own product — watches for feature requests surfaced from meetings, follow-ups, and feedback, files them as GitHub issues, and posts @ivan-agent /build on them itself. In the transcript above, the ari-agent bot account is the one that triggered the build, not a person. The feature request goes from "raised somewhere in the product" to "PR open for human review" without an engineer ever typing the trigger.

We didn't set out to build a fully autonomous pipeline. It happened because each piece — Ari filing the issue, Ivan building it, Ivan addressing the review — was already automatable, and chaining them together was just... not turning the chain off.

It's more than a build button

The two workflows above cover the "issue becomes a PR" path, but the CLI underneath them does a lot more than that, and it's all available whether you trigger it from Actions or run it by hand:

  • Expert mode (ivan --mode expert "task") splits the work across two personas — an Implementer that writes the code and a read-only Architect (running on Opus by default) that critiques the plan and the diff before either reaches your PR. It's a principal-engineer review loop that runs automatically, and it bails out early once the Architect approves instead of grinding through fixed rounds.
  • Self-review (ivan --self-review "task") has Claude review its own branch before the PR even opens.
  • Prompt rewriting — you can see it right there in our workflow YAML as "rewritePrompt": true. Ivan reads the repo first and rewrites a rough issue description into a sharper prompt before handing it to the implementer, so a two-sentence issue still produces a well-scoped task.
  • Bulk comment addressingivan address with no arguments scans every open PR for unresolved comments or failing checks and works through all of them; ivan address --from-user alice narrows it to a specific reviewer.
  • Risk analysis on demand — we run ivan risk-analysis in a third workflow (risk-analysis-on-release.yml) that posts a security/risk assessment as a PR comment on every release.

None of this requires babysitting. You kick it off, walk away, and come back to a PR — or a dozen — waiting for review.

The numbers

GitHub PR activity before and after adopting Ivan

Over the last couple of weeks in the agents repo, the ivan-agent bot account has authored around 200 pull requests, and roughly two-thirds of them have been merged — real, shipped changes, not demos. A sampling of recent ones:

  • Ivan: Enhance Work Activity Generation with Google Drive Activity for Google Workspace Users
  • Ivan: Add User Preference Fetch Tool to Preferences Skill
  • Ivan: Downgrade Slack Token Error to Warning for Meeting Notes Distribution
  • Ivan: Refactor MCP Server Skill Middleware for Human-Friendly Skill Names
  • Ivan: Enhance Verification Logic for Acknowledgment-Only Responses

Some are one-line copy fixes. Some touch three files and add a couple hundred lines across web-api and web-ui. All of them started as a sentence someone (or something) wrote in an issue, and ended as a diff a human reviewed and merged.

Not everything survives review — about 30% get closed rather than merged, which is exactly what you'd want from a system that proposes work instead of quietly slipping it in. The point was never "never review it." The point is that the first draft — the part that used to eat an afternoon — now shows up in twenty minutes.

That's the repo-wide number. My own 80-PRs-in-two-weeks number from the intro comes from the same mechanism, just pointed at myself instead of a bot account: assign tasks, walk away, come back to diffs. It's not that Ivan writes code faster than I would; it's that it writes code while I'm not there.

The raw workflows

Here's the actual YAML running in .github/workflows/. Nothing hidden — this is the whole mechanism, secrets and all (the values themselves live in GitHub's encrypted secrets store, never in the repo).

ivanagent.yml — handles @ivan-agent /build

name: Ivan Agent Handler on: issue_comment: types: [created] jobs: handle-ivanagent: if: contains(github.event.comment.body, '@ivan-agent /build') runs-on: ubuntu-latest-arm-m environment: ivan permissions: issues: write contents: write pull-requests: write steps: - name: Check required secrets run: | if [ -z "${{ secrets.OPEN_AI_KEY }}" ]; then echo "Error: OPEN_AI_KEY secret is not set" exit 1 fi if [ -z "${{ secrets.ANTHROPIC_KEY }}" ]; then echo "Error: ANTHROPIC_KEY secret is not set" exit 1 fi if [ -z "${{ secrets.PAT }}" ]; then echo "Error: PAT secret is not set" exit 1 fi echo "All required secrets are present" - name: Checkout repository uses: actions/checkout@v6 with: token: ${{ secrets.PAT }} fetch-depth: 0 - name: Set up Node.js uses: actions/setup-node@v6 with: cache: 'npm' node-version-file: '.nvmrc' - name: Create Ivan config directory run: mkdir -p ~/.ivan - name: Write Ivan config env: OPENAI_KEY: ${{ secrets.OPEN_AI_KEY }} ANTHROPIC_KEY: ${{ secrets.ANTHROPIC_KEY }} GH_PAT: ${{ secrets.PAT }} run: | jq -n \ --arg openai "$OPENAI_KEY" \ --arg anthropic "$ANTHROPIC_KEY" \ --arg pat "$GH_PAT" \ '{ openaiApiKey: $openai, anthropicApiKey: $anthropic, version: "1.0.0", executorType: "sdk", githubAuthType: "pat", githubPat: $pat, repoInstructionsDeclined: {}, repoInstructions: {}, reviewAgent: "@coderabbitai" }' \ > ~/.ivan/config.json echo "Config file created at ~/.ivan/config.json" - name: Install Ivan CLI run: | npm install -g @ariso-ai/ivan@latest --prefix ~/.local echo "Ivan installed successfully" ~/.local/bin/ivan --version - name: Add eyes reaction to comment uses: peter-evans/create-or-update-comment@v4 with: comment-id: ${{ github.event.comment.id }} reactions: eyes - name: Get issue body id: get_issue uses: actions/github-script@v7 with: script: | const issue = await github.rest.issues.get({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number }); return issue.data.body; result-encoding: string - name: Run Ivan command id: run_ivan env: ISSUE_BODY: ${{ steps.get_issue.outputs.result }} run: | # Escape the issue body for JSON ESCAPED_ISSUE=$(echo "$ISSUE_BODY" | jq -Rs .) # Run ivan command with output streaming and also capture to file set +e ~/.local/bin/ivan -c "{\"tasks\": [$ESCAPED_ISSUE], \"generateSubtasks\": false, \"prStrategy\": \"multiple\", \"rewritePrompt\": true}" 2>&1 | tee /tmp/ivan_output.txt EXIT_CODE=${PIPESTATUS[0]} set -e if [ $EXIT_CODE -ne 0 ]; then echo "ivan_failed=true" >> $GITHUB_OUTPUT else echo "ivan_failed=false" >> $GITHUB_OUTPUT fi # Extract PR URL from output file (looking for github.com PR URLs) PR_URL=$(grep -oP 'https://github\.com/[^/]+/[^/]+/pull/\d+' /tmp/ivan_output.txt | head -1 || echo "") if [ -n "$PR_URL" ]; then echo "pr_url=$PR_URL" >> $GITHUB_OUTPUT # Extract PR number from URL PR_NUMBER=$(echo "$PR_URL" | grep -oP '/pull/\d+$' | grep -oP '\d+') echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT else echo "pr_url=" >> $GITHUB_OUTPUT echo "pr_number=" >> $GITHUB_OUTPUT fi - name: Comment on issue with PR link and output if: steps.run_ivan.outputs.pr_url != '' && steps.run_ivan.outputs.ivan_failed == 'false' uses: actions/github-script@v7 with: script: | const fs = require('fs'); const ivanOutput = fs.readFileSync('/tmp/ivan_output.txt', 'utf8'); const prUrl = '${{ steps.run_ivan.outputs.pr_url }}'; const body = '✅ Ivan has created a pull request: ' + prUrl + '\n\n' + '⏳ **Addressing comments now...**\n\n' + '<details>\n' + '<summary>View Ivan command output</summary>\n\n' + '```\n' + ivanOutput + '\n```\n\n' + '</details>'; await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body: body }); - name: Comment on issue if Ivan failed if: steps.run_ivan.outputs.ivan_failed == 'true' uses: actions/github-script@v7 with: script: | await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body: `❌ Ivan command failed. Please check the workflow logs for details.` }); - name: Comment on issue if no PR found if: steps.run_ivan.outputs.pr_url == '' && steps.run_ivan.outputs.ivan_failed == 'false' uses: actions/github-script@v7 with: script: | await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body: `⚠️ Ivan command completed but no pull request URL was found in the output.` }); - name: Wait for 15 minutes if: steps.run_ivan.outputs.pr_number != '' && steps.run_ivan.outputs.ivan_failed == 'false' run: | echo "Waiting 15 minutes before running ivan address command..." sleep 900 - name: Run Ivan address command if: steps.run_ivan.outputs.pr_number != '' && steps.run_ivan.outputs.ivan_failed == 'false' run: | echo "Running ivan address for PR #${{ steps.run_ivan.outputs.pr_number }}" ~/.local/bin/ivan address ${{ steps.run_ivan.outputs.pr_number }} --yes 2>&1 | tee /tmp/ivan_address_output.txt - name: Comment on issue after addressing PR if: steps.run_ivan.outputs.pr_number != '' && steps.run_ivan.outputs.ivan_failed == 'false' uses: actions/github-script@v7 with: script: | const fs = require('fs'); const addressOutput = fs.readFileSync('/tmp/ivan_address_output.txt', 'utf8'); const prNumber = '${{ steps.run_ivan.outputs.pr_number }}'; const body = '✅ **Ivan has finished addressing comments on PR #' + prNumber + '**\n\n' + 'The PR has been updated based on any review comments received.\n\n' + '<details>\n' + '<summary>View ivan address command output</summary>\n\n' + '```\n' + addressOutput + '\n```\n\n' + '</details>'; await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body: body });

ivanagent-address.yml — handles @ivan-agent /address

This one fires from two different event types — a plain issue comment on a PR, or a reply to a specific review comment thread — and it resolves which PR and which discussion thread to target before calling ivan address.

name: Ivan Address PR Comments on: issue_comment: types: [created] pull_request_review_comment: types: [created] jobs: handle-address: if: | contains(github.event.comment.body, '@ivan-agent /address') && (github.event_name == 'pull_request_review_comment' || github.event.issue.pull_request != null) runs-on: ubuntu-latest-arm-m environment: ivan permissions: issues: write contents: write pull-requests: write steps: - name: Check required secrets run: | if [ -z "${{ secrets.OPEN_AI_KEY }}" ]; then echo "Error: OPEN_AI_KEY secret is not set" exit 1 fi if [ -z "${{ secrets.ANTHROPIC_KEY }}" ]; then echo "Error: ANTHROPIC_KEY secret is not set" exit 1 fi echo "All required secrets are present" - name: Checkout repository uses: actions/checkout@v4 with: token: ${{ secrets.GITHUB_TOKEN }} fetch-depth: 0 - name: Set up Node.js uses: actions/setup-node@v6 with: cache: 'npm' node-version-file: '.nvmrc' - name: Create Ivan config directory run: mkdir -p ~/.ivan - name: Write Ivan config env: OPENAI_KEY: ${{ secrets.OPEN_AI_KEY }} ANTHROPIC_KEY: ${{ secrets.ANTHROPIC_KEY }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | jq -n \ --arg openai "$OPENAI_KEY" \ --arg anthropic "$ANTHROPIC_KEY" \ --arg pat "$GH_TOKEN" \ '{ openaiApiKey: $openai, anthropicApiKey: $anthropic, version: "1.0.0", executorType: "sdk", githubAuthType: "pat", githubPat: $pat, repoInstructionsDeclined: {}, repoInstructions: {}, reviewAgent: "@coderabbitai" }' \ > ~/.ivan/config.json echo "Config file created at ~/.ivan/config.json" - name: Install Ivan CLI run: | npm install -g @ariso-ai/ivan@latest --prefix ~/.local echo "Ivan installed successfully" ~/.local/bin/ivan --version - name: Add eyes reaction to comment uses: actions/github-script@v7 with: script: | const commentId = ${{ github.event.comment.id }}; if (context.eventName === 'pull_request_review_comment') { await github.rest.reactions.createForPullRequestReviewComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: commentId, content: 'eyes' }); } else { await github.rest.reactions.createForIssueComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: commentId, content: 'eyes' }); } - name: Run Ivan address command id: run_ivan_address env: IN_REPLY_TO_ID: ${{ github.event.comment.in_reply_to_id }} EVENT_NAME: ${{ github.event_name }} ISSUE_PR_NUMBER: ${{ github.event.issue.number }} REVIEW_PR_NUMBER: ${{ github.event.pull_request.number }} run: | if [ "$EVENT_NAME" = "pull_request_review_comment" ]; then PR_NUMBER=$REVIEW_PR_NUMBER else PR_NUMBER=$ISSUE_PR_NUMBER fi echo "Running ivan address for PR #$PR_NUMBER" DISCUSSIONS_FLAG="" if [ -n "$IN_REPLY_TO_ID" ]; then DISCUSSIONS_FLAG="--discussions discussion_r${IN_REPLY_TO_ID}" echo "Comment is a reply to comment #${IN_REPLY_TO_ID}, passing: $DISCUSSIONS_FLAG" fi set +e ~/.local/bin/ivan address $PR_NUMBER --non-interactive $DISCUSSIONS_FLAG 2>&1 | tee /tmp/ivan_address_output.txt EXIT_CODE=${PIPESTATUS[0]} set -e if [ $EXIT_CODE -ne 0 ]; then echo "ivan_failed=true" >> $GITHUB_OUTPUT else echo "ivan_failed=false" >> $GITHUB_OUTPUT fi echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT - name: Comment with address output if: steps.run_ivan_address.outputs.ivan_failed == 'false' uses: actions/github-script@v7 with: script: | const fs = require('fs'); const addressOutput = fs.readFileSync('/tmp/ivan_address_output.txt', 'utf8'); const prNumber = '${{ steps.run_ivan_address.outputs.pr_number }}'; const body = '✅ **Ivan has finished addressing comments on PR #' + prNumber + '**\n\n' + 'The PR has been updated based on the review comments.\n\n' + '<details>\n' + '<summary>View ivan address command output</summary>\n\n' + '```\n' + addressOutput + '\n```\n\n' + '</details>'; await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: parseInt(prNumber), body: body }); - name: Comment if Ivan address failed if: steps.run_ivan_address.outputs.ivan_failed == 'true' uses: actions/github-script@v7 with: script: | const prNumber = '${{ steps.run_ivan_address.outputs.pr_number }}'; await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: parseInt(prNumber), body: '❌ `ivan address` command failed. Please check the workflow logs for details.' });

What's not fancy about this

There's no orchestration platform, no queue, no custom dashboard. It's two YAML files in .github/workflows/, a config file written from GitHub secrets, and a CLI that already knows how to talk to Claude and GitHub. The if: conditions do the routing. github-script posts the comments. A sleep 900 is the entire scheduling system for "give the reviewers a few minutes first."

That's on purpose. The trigger is a comment because comments are where the context already is — the issue title, the acceptance criteria, the back-and-forth in the thread. Ivan doesn't need a separate briefing step; it reads the same thing a human engineer would read before picking up the ticket.

Why this is a big deal for us

The honest version of "improve productivity" here isn't "AI writes all our code now." A third of what Ivan opens gets closed without merging, and everything that does merge still gets human review. What changed is where the floor is. The slowest part of shipping a small, well-scoped change used to be someone finding the time to sit down and start it. Now the floor is: write the issue clearly, and twenty minutes later there's something concrete to react to.

Multiply that by the volume — roughly 200 PRs and counting from one bot account in a few weeks, across a repo with a real product behind it — and it stops being a novelty and starts being how a meaningful slice of our maintenance and small-feature work actually gets done. Engineers spend their attention on the interesting 20%: the design decisions, the gnarly bugs, the things that actually need a human in the loop. Ivan handles getting from "we should do this" to "here's a diff to look at."

We didn't build a system to replace engineers. We built a system that makes the distance between deciding to do something and having a PR to review as short as we could make it. Twenty-four minutes, in the example that opened this post. Sometimes it's less.

So back to why I'm shocked: this is free, MIT-licensed, and installs with one command. Nobody's paying for a seat, waiting on a procurement cycle, or evaluating a vendor. It's npm i -g @ariso-ai/ivan, ivan add-action, and a comment on an issue. Grab it at github.com/ariso-ai/ivan and see how far away the floor moves for you — I genuinely don't understand why every team with a GitHub repo isn't already running this.

Share this post
Max Heckel - Author profile picture
Max Heckel

Max Heckel is the founding engineer and CTO of Ariso. Before starting Ariso, he worked at Google, McGraw Hill, JupiterOne, and created SciSummary.

LinkedIn

Who's Ivan?

He writes half our code, and he's free to use. Check it out on npm or GitHub.

Ready to try Ari?

The AI player-coach that gives every employee the tools to lead themselves.

Try Ari Free