Closing a ticket should have taken three minutes. It took forty, because my close-and-ship skill walked into a version number it had made up.
The skill handles the close-and-ship sequence for hotfixes on the legacy platform: transition the JIRA ticket, set the fix version, merge the branch, tag the release, push. I’d been trusting it for months without thinking hard about any individual step. The part that went wrong was version detection. To figure out which hotfix version the current branch belonged to, the skill ran git describe --tags --match "hotfix/*" against the local repo and took whatever it found.
That seemed reasonable. Git tags are version labels. Hotfix tags name the version. Of course that’s the source of truth.
Except it wasn’t.
The assumption that didn’t hold
Hotfix tags in the release flow get pushed to origin/master during the merge. But only during the merge, and only from whichever machine ran it. If a prior session had merged locally without pushing, or if the CI system had run the merge on its own side, the tag was present on master but not yet fetched to my local clone. The git describe call hit local tag state, not network state.
In this ticket’s case, the branch had already been merged into master from an earlier session. The merge commit was there. The tag was there, on origin. But my local clone didn’t have it yet. So git describe returned a best-fit from the previous release, named the wrong version, and the skill concluded the branch hadn’t shipped yet. It tried to re-merge a branch that was already in master.
git merge caught the conflict and declined to proceed. Nothing actually broke. But the skill had manufactured a confident version number from stale signal, and I spent twenty minutes figuring out why a merge was failing on work that was already done.
The fix
The reliable signal for “which hotfix version did this branch land in” is not the local tag. It is the merge commit message on origin/master.
When a hotfix branch merges, the commit message reads Merge branch 'hotfix/X.Y.Z' into master. That string lives in the remote’s commit history. It does not depend on whether tags were fetched locally. It does not depend on local state at all.
I changed the version detection to run git log origin/master --merges --oneline --grep="hotfix/" -1, parse the branch name from the commit message, and extract the version from that. One call, no local tag lookups, authoritative by definition because origin/master is the release ledger.
The skill stopped manufacturing versions. The same test case went through clean.
The second thing the cleanup caught
While I was in the close payload code, I read the JIRA transition block. The skill was setting fixVersion on close. It was not setting Sprint.
JIRA treats these as independent fields. Push a close transition without Sprint and the ticket either inherits whatever sprint it was in at creation or lands on no sprint at all. For reporting, a closed hotfix with no sprint is a ghost: it counts toward velocity in no sprint, it doesn’t show in sprint retrospectives, and the board query for “what shipped this sprint” misses it.
I added Sprint to every close payload. That required one call to resolve the active sprint ID from the JIRA API at transition time, since sprint IDs don’t stay stable across boards the way version IDs do.
This was the quieter failure. No errors. JIRA accepted the payload, the ticket closed, and the gap only showed up later in sprint metrics. The kind of thing that lives undetected for weeks.
What this is actually about
There is a tempting framing for failures like these: the agent made a mistake, so you need a better prompt. That framing is looking at the wrong layer.
The version detection failure was not a reasoning error. The model reasoned correctly given what it was told. The problem was that “use git tags for version state” is a plausible-sounding operational rule with no backing system that can prove it. Tags are convenient labels, but they are not a ledger. The merge commit on origin/master is a ledger. The skill should have been reading the ledger from the start.
The sprint omission was not a reasoning error either. Nobody told the skill that sprint was a required field. It assembled a minimal close payload from the fields it knew about, JIRA accepted it without complaint, and the gap lived quietly in reporting state.
Both bugs are in the process contract around the model, not in the model’s output. The contract said “use these sources, set these fields,” and the contract was incomplete. Fixing the contract fixed the behavior permanently. Better prompting would have been a bandage on a plumbing problem.
This is the actual work in AI-assisted automation: not getting the model to reason better, but auditing every operational decision in the workflow wrapper for whether it is tied to a system that can actually prove what it claims. Release version? Read the merge ledger, not the local cache. Active sprint? Fetch it at transition time, not at ticket creation. Every field an agent infers from a “reasonable assumption” is a field that will eventually manufacture wrong state.
Before you close your next hotfix, do not trust local tag state. Run git fetch origin, check the merge commit on origin/master for the branch name and version, then look up the active sprint ID through your tracker’s API and add it to the close payload before you submit it. If either step already lives inside a script, go read that script today and confirm it does exactly this, not a cached or assumed value.