The ticket hit LIVE at 4:49 in the afternoon. Fix Version stamped, every doneness field green, agent moved on. By seven that evening there was a report that the bug it fixed was still happening in production. git log master..hotfix/1.4.22 printed the fix, sitting unmerged, on a ticket JIRA called shipped. The final merge to master had never run.
How the wrong state looked official
The production app’s hotfix release flow ends in a merge to master, a tag push, and CI deploying on that push. That final step is where the LIVE transition belongs.
On 1.4.22, the merge never ran at all. The agent working the ticket committed the fix to the hotfix branch, verified the new behavior end to end from a working copy where the code existed, and closed the ticket. The close flow fired the LIVE transition, the transition payload stamped the fix version, and JIRA accepted all of it. Production kept serving the code without the fix.
So JIRA showed the ticket as LIVE because a transition had fired. The fix version was stamped because the payload included it. Every JIRA field that tracks doneness agreed: the ticket was done. The repo said otherwise, and nothing was checking the repo.
The process produced confident-looking state without grounding it in the artifact that actually mattered.
Five and a half hours of LIVE that wasn’t
I ran the real flow that night. hotfix/1.4.22 still existed with the right commits, so the merge to master was one command, the tag pushed, CI deployed. The code went live at 10:26 that night, five and a half hours after JIRA first said it had.
Then I verified the deploy from production itself. The release included a small diagnostic endpoint that only exists in the new code, so probing it from outside answers “is this actually live” directly. The first probe returned the pre-deploy signature. A few minutes later it returned the new one, and only then did I believe my own release.
The part that stings is what happened inside the gap. The bug the hotfix fixed kept reaching users the entire time the ticket said LIVE. That is the only reason the drift was found: a report came in that the broken behavior was still there, hours after every field in the tracker said it was gone. The verification during the original close was real, but it ran against the working copy that had the fix. Production never did.
What I added to close the gap
I updated the release orchestrator script that drives JIRA transitions when a ticket closes. It now checks git log origin/master..hotfix/<version> before allowing a LIVE transition on any hotfix ticket. If the hotfix branch has commits that master does not, the script halts with an explicit error. The close fails closed: no ticket reaches LIVE status while the repo says the merge is pending.
Beyond that guard, I added a PreToolUse hook in Claude Code’s settings to block any agent-initiated call that would trigger the LIVE transition outside of that orchestrator. An agent working on a hotfix ticket cannot fire LIVE as a side effect of closing a subtask or resolving a field. The transition has to come through the orchestrator, where the repo check lives.
I also added paired numbering to the workflow documentation. Each repo step in the hotfix release flow now carries the number of the JIRA state change it licenses, so a state change with no matching repo artifact reads as wrong on the page. If someone reads the doc after a partial run, the pairing shows which step got skipped.
The pattern behind the failure
I had a mental model that said: if JIRA says a thing is done, that is a reliable signal it is done. That model was built on successful releases where JIRA state and repo state happened to agree. Nothing structural guaranteed the agreement. Every release before this one had simply been executed correctly, and I read that streak as a guarantee.
The process debt here was a workflow that was willing to accept a final state that was false. The LIVE transition fired because nothing at the moment of firing asked whether the thing it announced had happened. If you run a similar flow, the fix is to make any step that changes ticket status verify the repo state it is supposed to reflect before it is allowed to run.
The hook and the orchestrator guard do not make the process smarter. They remove the path where the wrong state can be produced at all.
The next missing assertion
The obvious follow-up: which other transitions still fire on trust? At least one. The branch-start transition, which fires when a branch is cut and work begins, had no assertion that the branch actually exists on the remote. An agent can fire it before pushing. I know this the same way I should have known about LIVE, by reading the orchestrator and listing what it asserts against what it merely announces.
So far the repo check only guards LIVE. The branch-start transition still fires without one, and I have not yet gone through the rest of the orchestrator to add the others.