Closing a ticket should have taken three minutes. It took forty, because our 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. We’d been trusting it for months without thinking hard about any individual step. The part that went wrong was version detection. To work 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, and hotfix tags name the version, so of course that’s the source of truth.
It wasn’t.
Where the tag lookup went wrong
Hotfix tags in the release flow get pushed to origin/master during the merge. Only during the merge, and only from whichever machine ran it. If a prior session had merged without pushing, or if CI had run the merge on its own side, the tag existed on master but had not been fetched to our local clone. The git describe call read local tag state, not network state.
That is what happened on this ticket. The branch had already been merged into master from an earlier session. The merge commit was there, and the tag was there on origin. Our local clone just 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. It tried to re-merge a branch that was already in master.
git merge caught the conflict and declined to proceed, so nothing actually broke. But the skill had produced a confident version number from stale signal, and we spent twenty minutes working out why a merge was failing on work that was already done.
Reading the ledger instead
The reliable signal for which hotfix version a branch landed in is the merge commit message on origin/master. When a hotfix branch merges, the 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, or on any local state.
We changed the version detection to run git log origin/master --merges --oneline --grep="hotfix/" -1, parse the branch name out of the commit message, and take the version from that. One call, no local tag lookups, and authoritative by definition because origin/master is the release ledger.
The skill stopped inventing versions, and the same test case went through clean.
With the close payload code open, we 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 keeps whatever sprint it had 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.
We added Sprint to every close payload. That took 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 one raised no errors at all. JIRA accepted the payload, the ticket closed, and the gap only surfaced later in sprint metrics. It could have sat there for weeks.
Both bugs were in the contract
There is a tempting reading of failures like these: the agent made a mistake, so it needs a better prompt. That points at the wrong layer.
The model reasoned correctly from what it was given. “Use git tags for version state” is a plausible operational rule with no system behind it that can prove the answer. Tags are convenient labels, but they are not a ledger. The merge commit on origin/master is a ledger, and the skill should have been reading it from the start.
The sprint omission had the same root. 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 in reporting state.
The contract around the model said “use these sources, set these fields,” and it was incomplete. Fixing the contract fixed the behavior permanently, where better prompting would have been a bandage on a plumbing problem.
So the work is auditing every operational decision in the workflow wrapper and asking whether it is tied to a system that can prove what it claims. Release version? Read the merge ledger, not the local cache. Active sprint? Fetch it when the ticket transitions. Every field an agent infers from a reasonable assumption is a field that will eventually produce wrong state.
If your own hotfix close lives inside a script, go read it today. Check where it gets the version and whether it sets every field your reporting depends on.