fix-ci Classification runs with no AI provider at all

ci repair

It fixes the build it can explain

Most red builds are one of six things, and half of them are not your fault. buddy fix-ci reads the log, classifies the failure, checks whether the base branch is already broken, and repairs it when the fix is unambiguous — or tells you plainly that it is not.

$ buddy fix-ci --pr 128

             __
    (\,------'()'--o    reading the failing run
     (_    ___    /~"   ci / test (ubuntu-latest)
      (_)_)  (_)_)

  classified   lockfile-drift
  evidence     "lockfile had changes, but
                --frozen-lockfile was set"
  base branch  green — the failure is ours
  mechanical   yes

  → regenerating bun.lock
  → committed to buddy/update-react-18
  → posted the outcome on #128

  fixed. attempt 1 of 3.
🔬

Six failures, told apart

Lock file drift, runner flake, install failure, type error, test failure and lint. Each has a signature matched against the log, and each carries the evidence lines that produced the verdict — so the classification is auditable rather than asserted.

🌿

Checks the base first

If the same failure happens on the base branch, the pull request did not cause it. Buddy says so instead of trying to repair somebody else's breakage on your branch.

⚙️

Mechanical before model

Lock file drift is regenerated deterministically — no provider, no tokens, no guessing. The agent is only reached for failures that genuinely need reading the code.

🛑

A hard attempt limit

Prior attempts on the pull request are counted, and the run stops at the limit. A repair bot that retries forever is worse than a red build, because a red build is honest.

🌱

It commits, it does not merge

Repairs land on the working branch as an ordinary commit you review. fix-ci mode may write, run commands and use git — on that branch, in a workspace it cannot escape.

🗒️

Explains itself either way

Every run posts an outcome: what it classified, what it did, and — when it declined — why the fix was not the bot's to make.

From a failing run

Repair is driven by the run that failed, because the diagnosis is a function of that job's log. The workflow generated by buddy setup wires this up: when a run on a Buddy branch fails, the fix-ci job reads its log and reports back on the pull request.

A comment cannot say which run failed, so @buddy fix-ci in a thread replies pointing at that job rather than guessing.

From the CLI

buddy fix-ci --run-id 12345 --pr 128
buddy fix-ci --run-id 12345 --pr 128 --dry-run   # classify and report, change nothing

--run-id supplies the log to diagnose. --pr is where the report goes, and also where the attempt counter lives — without it Buddy cannot tell a first attempt from a fourth.

From a workflow

name: Buddy Fix CI
on:
  workflow_run:
    workflows: [CI]
    types: [completed]

jobs:
  repair:
    if: github.event.workflow_run.conclusion == 'failure'
    runs-on: ubuntu-latest
    permissions:
      contents: write
      pull-requests: write
      actions: read
    steps:
      - uses: actions/checkout@v4
      - uses: oven-sh/setup-bun@v2
      - run: bunx @buddysh/buddy fix-ci
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}

How classification works

The log is matched against failure signatures before any model is involved. That ordering matters for cost and for trust: a stale lock file is a pattern, not a judgement call, and paying a language model to recognise one is silly.

KindWhat it meansRepair
lockfile-driftThe lock file is out of step with its manifestRegenerated, committed and pushed
flakeNetwork, rate limit or runner problemWorth one retry
installDependencies could not be resolvedAgent, or reported
type-errorThe change does not type-checkAgent
test-failureAn assertion failedAgent, or reported
lintLint or formatting violationsAgent
unknownNothing recognisableReported, with the interesting log lines

Re-running after a mechanical fix

The lock-file repair commits to the branch and pushes it, but the pull request does not go green by itself. The job pushes with GITHUB_TOKEN, which keeps the commit attributed to the bot rather than to whoever owns a personal access token — and GitHub deliberately does not start a new workflow run for a push made with it.

So the fix lands, and the checks need re-running: from the Actions tab, or by the next push to the branch. Buddy tells you which of the two happened in the comment it leaves.

With no AI provider configured, classification and the mechanical repairs still work. You lose the agent-driven fixes, not the diagnosis.

What it is allowed to do

fix-ci is an agent mode drawing from the read, write, shell, git and comment tiers. Its limits are structural rather than advisory:

  • Commands run from an empty environment plus an allowlist, so a command the agent runs cannot authenticate to your registry, your cloud or GitHub — the credentials are simply not there.
  • Every path is resolved against the workspace and rejected if it escapes, checked twice so a symlink inside the workspace cannot satisfy the first check and land outside it.
  • Tool calls, wall clock and tokens are each independently bounded.

Conversations · Finishing touches · The agent runtime · Dependency updates