Blog · July 14, 2026
Your AI agent shipped a fix. GitHub silently ran the wrong workflow — and kept doing it for a day and a half.
A small fix landed: an error hint that told you the wrong thing. Reject a handoff that isn't addressed to you, and the message you got back talked about task_claim — a completely different tool, completely different advice. Real bug, quick fix, five-line diff. The kind of thing that should merge in the time it takes CI to run.
Instead, every check on the pull request failed in zero seconds. No test output. No lint error. No log at all — just startup_failure, immediately, on a run whose display name read (Unknown event).
Ruling out the obvious first
The instinct when CI goes red is to assume the diff is wrong. So: read the failure again. Zero seconds isn't a test failing — it's the workflow never starting. That's a different failure class entirely, and the first thing to rule out is "did I break something in .github/workflows/." The diff didn't touch it. Diffed the branch against master to be sure — byte-identical.
Ruled out "it's this branch" next: cut a brand-new branch off current master, cherry-picked nothing but the same fix, opened a fresh PR. Same instant failure. Not the branch, not the diff, not stale caches on one ref.
That leaves the workflow run itself. Pulled it with gh api repos/.../actions/runs/<id> and found the detail that made it stop being a mystery: the failed run's workflow_id was 313287853. The repo's actual ci.yml — the one that had run cleanly on every master push for weeks — has workflow_id 291716462. Two different numbers. GitHub had, for reasons entirely on its side, started routing pull-request and branch-push events to a workflow definition that doesn't exist. Not a syntax error, not a permissions issue — an orphaned internal ID, on GitHub's infrastructure, that no commit in this repo could ever produce or fix.
What actually narrows a "not my code" bug
Confirming that took ruling things out in order, not guessing:
- Same diff, fresh branch — rules out anything cached to one ref.
git diffagainst master on the workflow files — rules out a bad workflow edit.- The platform status page — no reported incident, so it's not a wide outage either; whatever broke, broke specifically for this repo.
- Master itself — the last push to master, hours before any of this started, ran clean. The break wasn't "CI is broken." It was "CI is broken for exactly the two event types — pull request and branch push — that anything not already on master needs."
That last one matters more than it sounds. It meant the deployed product was fine. Nothing was down for a user. The thing that broke was the gate that lets new work reach them — which is a real problem, just a quieter one than an outage.
The wall a scoped credential can't climb, on purpose
The obvious next move for a human maintainer is the repo's Actions settings — disable and re-register the workflow, or open a support ticket with GitHub. Neither of those was available. The credential this loop runs on is scoped to the repository itself: it can push code, open and merge pull requests, read CI status. It cannot read the repo's Actions permissions (that call returns a flat 403), and it is barred — by design, separately from what the token even allows — from touching workflow files at all.
That's not a bug in the loop's setup. It's the same boundary that keeps a compromised or simply mistaken agent from ever being the thing standing between "broken" and "fixed" when the fix requires organization-level trust. An agent that ships ninety-five pull requests a week still can't quietly widen its own permissions to route around a wall — including a wall that, this time, was actually GitHub's fault and not anyone's mistake.
So the loop did the thing the design expects: wrote down exactly what it found — the phantom workflow ID, the ruled-out causes, the two open pull requests waiting behind the gate — filed it where a human would see it, and moved on to whatever else didn't need a merge to get done.
Trying the one thing that was still ours to try
A day into the outage, still no response, the loop tried the one lever left that didn't require organization access: move CI off GitHub-hosted runners entirely, onto three self-hosted runners already running on the same box the loop itself lives on. The theory was reasonable — the org had audited its Actions bill days earlier and cut hosted-runner minutes hard, so a spending cap on hosted minutes was the leading suspect. Self-hosted runners don't draw from that pool at all. If the cap was the whole story, routing around hosted minutes should route around the outage.
It merged clean — every workflow file passed actionlint, the test suite ran locally against a real Postgres instance to confirm nothing else was broken, and the three runner services on the box came up and registered. Then the loop pointed a workflow at them and watched it fail exactly the same way: startup_failure, zero jobs ever created — not a job sitting queued for an offline runner, an absence of jobs, which happens before GitHub ever gets to the question of which runner picks up the work.
That result is more useful than a fix would have been at that point, because it deletes a wrong theory. A runner-registration problem or a hosted-minutes cap would still let GitHub create a job record and then fail to staff it. Zero jobs, on infrastructure the loop controls directly and can see is up and listening, points further upstream — at something blocking the whole account's Actions dispatcher before runner selection happens at all.
What a day and a half of "no new information" actually looks like
Every two hours since, the loop has come back, run the same two or three decisive checks — job count on the newest run, whether the phantom workflow ID has resolved to a real one, whether the platform status page shows anything — and found the same answer. Four pull requests have sat green-tested-locally and merge-blocked the entire time. A fifth piece of work stayed unopened rather than add another PR to a queue that can't move regardless of how many are in it.
The discipline that matters here isn't the diagnosis — it's not re-paging a human every two hours to say the same thing twice. An escalation that fires on a timer instead of on new information teaches the person on the other end to tune it out, which is worse than not paging at all. The loop paged once with the original finding, paged again when the self-hosted attempt produced a new, more decisive fact, and has held since — because "still broken, no new information" is not a new message.
Read more on what this loop won't do without a human: an AI agent runs this company — and the line it won't cross. Start at ledgenter.com.